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', 1) correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None) if correct_key is None: raise Exception('could not determine correct_key') number_answers = len(data['params'][name]) all_keys = [chr(ord('a') + i) for i in range(number_answers)] incorrect_keys = list(set(all_keys) - set([correct_key])) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = data['correct_answers'][name]['key'] data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': if len(incorrect_keys) > 0: data['raw_submitted_answers'][name] = random.choice(incorrect_keys) data['partial_scores'][name] = {'score': 0, 'weight': weight} else: # actually an invalid submission data['raw_submitted_answers'][name] = '0' data['format_errors'][name] = 'INVALID choice' elif result == 'invalid': data['raw_submitted_answers'][name] = '0' data['format_errors'][name] = 'INVALID choice' # FIXME: add more invalid choices else: raise Exception('invalid result: %s' % result)
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Cast both submitted and true answers as integers. a_tru = int(a_tru) a_sub = int(a_sub) if a_tru == a_sub: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = str(a_tru) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str(a_tru + (random.randint(1, 11) * random.choice([-1, 1]))) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize if random.choice([True, False]): data['raw_submitted_answers'][name] = '1 + 2' else: data['raw_submitted_answers'][name] = '3.4' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # Get the number of digits to output digits = pl.get_integer_attrib(element, 'digits', 2) # Get the presentation type presentation_type = pl.get_string_attrib(element, 'presentation-type', 'f') var_name = pl.get_string_attrib(element, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl-matrix-latex element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) if not np.isscalar(var_data): var_data = np.array(var_data) # Check if numpy array type is numeric (integer, float or complex) if np.issubdtype(var_data.dtype, np.number): # Check shape of variable if var_data.ndim != 2: raise Exception('Value in data["params"] for variable %s in pl-matrix-latex element must be 2D array or scalar' % var_name) else: raise Exception('Value in data["params"] for variable %s in pl-matrix-latex element must be numeric' % var_name) # Create string for latex matrix format html = pl.latex_from_2darray(var_data, presentation_type=presentation_type, digits=digits) return html
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) allow_blank = pl.get_string_attrib(element, 'allow-blank', False) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) if allow_blank: # no invalid answer implemented when allow-blank="true" result = random.choices(['correct', 'incorrect'], [5, 5])[0] else: result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = a_tru data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = a_tru + str((random.randint(1, 11) * random.choice([-1, 1]))) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': data['raw_submitted_answers'][name] = '' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) + ' + {:d}'.format(random.randint(1, 100)) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': invalid_type = random.choice(['float', 'complex', 'expression', 'function', 'variable', 'syntax', 'escape', 'comment']) if invalid_type == 'float': data['raw_submitted_answers'][name] = 'x + 1.234' s = 'Your answer contains the floating-point number ' + str(1.234) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error('x + 1.234', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'complex': data['raw_submitted_answers'][name] = 'x + (1+2j)' s = 'Your answer contains the complex number ' + str(2j) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error('x + (1+2j)', 7) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'expression': data['raw_submitted_answers'][name] = '1 and 0' s = 'Your answer has an invalid expression. ' s += '<br><br><pre>' + phs.point_to_error('1 and 0', 0) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'function': data['raw_submitted_answers'][name] = 'atan(x)' s = 'Your answer calls an invalid function "' + 'atan' + '". ' s += '<br><br><pre>' + phs.point_to_error('atan(x)', 0) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'variable': data['raw_submitted_answers'][name] = 'x + y' s = 'Your answer refers to an invalid variable "' + 'y' + '". ' s += '<br><br><pre>' + phs.point_to_error('x + y', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'syntax': data['raw_submitted_answers'][name] = 'x +* 1' s = 'Your answer has a syntax error. ' s += '<br><br><pre>' + phs.point_to_error('x +* 1', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'escape': data['raw_submitted_answers'][name] = 'x + 1\\n' s = 'Your answer must not contain the character "\\". ' s += '<br><br><pre>' + phs.point_to_error('x + 1\\n', 5) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'comment': data['raw_submitted_answers'][name] = 'x # some text' s = 'Your answer must not contain the character "#". ' s += '<br><br><pre>' + phs.point_to_error('x # some text', 2) + '</pre>' data['format_errors'][name] = s else: raise Exception('invalid invalid_type: %s' % invalid_type) else: raise Exception('invalid result: %s' % result)
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answer-name') # Check if this element is intended to produce a grade will_be_graded = pl.get_boolean_attrib(element, 'grade', True) if not will_be_graded: return # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get submitted answer (the "state") state = data['submitted_answers'].get(answer_name, None) if state is None: # This might happen. It means that, somehow, the hidden input element # did not get populated with the PLThreeJS state. The student is not at # fault, so we'll return nothing - don't grade. return # Get correct answer (if none, don't grade) a = data['correct_answers'].get(answer_name, None) if a is None: return # Get submitted position (as np.array([x, y, z])) p_sub = np.array(state['body_position']) # Get submitted orientation (as Quaternion - first, roll [x,y,z,w] to [w,x,y,z]) q_sub = pyquaternion.Quaternion(np.roll(state['body_quaternion'], 1)) # Get format of correct answer f = pl.get_string_attrib(element, 'answer-pose-format', 'rpy') # Get correct position (as np.array([x, y, z])) and orientation (as Quaternion) p_tru, q_tru = parse_correct_answer(f, a) # Find distance between submitted position and correct position error_in_translation = np.linalg.norm(p_sub - p_tru) # Find smallest angle of rotation between submitted orientation and correct orientation error_in_rotation = np.abs((q_tru.inverse * q_sub).degrees) # Get tolerances tol_translation = pl.get_float_attrib(element, 'tol-translation', 0.5) tol_rotation = pl.get_float_attrib(element, 'tol-rotation', 5) if (tol_translation <= 0): raise Exception('tol_translation must be a positive real number: {:g}'.format(tol_translation)) if (tol_rotation <= 0): raise Exception('tol_rotation must be a positive real number (angle in degrees): {:g}'.format(tol_rotation)) # Check if angle is no greater than tolerance if ((error_in_rotation <= tol_rotation) and (error_in_translation <= tol_translation)): data['partial_scores'][answer_name] = {'score': 1, 'weight': weight, 'feedback': {'error_in_rotation': error_in_rotation, 'error_in_translation': error_in_translation}} else: data['partial_scores'][answer_name] = {'score': 0, 'weight': weight, 'feedback': {'error_in_rotation': error_in_rotation, 'error_in_translation': error_in_translation}}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) allow_partial_credit = pl.get_boolean_attrib(element, 'allow-partial-credit', False) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) # Wrap true answer in ndarray (if it already is one, this does nothing) a_tru = np.array(a_tru) # Throw an error if true answer is not a 2D numpy array if a_tru.ndim != 2: raise ValueError('true answer must be a 2D array') else: m, n = np.shape(a_tru) result = random.choices(['correct', 'incorrect', 'incorrect'], [5, 5, 1])[0] number_of_correct = 0 feedback = {} for i in range(m): for j in range(n): each_entry_name = name + str(n * i + j + 1) if result == 'correct': data['raw_submitted_answers'][each_entry_name] = str(a_tru[i, j]) number_of_correct += 1 feedback.update({each_entry_name: 'correct'}) elif result == 'incorrect': data['raw_submitted_answers'][each_entry_name] = str(a_tru[i, j] + (random.uniform(1, 10) * random.choice([-1, 1]))) feedback.update({each_entry_name: 'incorrect'}) elif result == 'invalid': if random.choice([True, False]): data['raw_submitted_answers'][each_entry_name] = '1,2' data['format_errors'][each_entry_name] = '(Invalid format)' else: data['raw_submitted_answers'][name] = '' data['format_errors'][each_entry_name] = '(Invalid blank entry)' else: raise Exception('invalid result: %s' % result) if result == 'invalid': data['format_errors'][name] = 'At least one of the entries has invalid format (empty entries or not a double precision floating point number)' if number_of_correct == m * n: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: if not allow_partial_credit: score_value = 0 else: score_value = number_of_correct / (m * n) data['partial_scores'][name] = {'score': score_value, 'weight': weight, 'feedback': feedback}
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'correct-answer', 'label', 'suffix', 'display'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answer = pl.get_integer_attrib(element, 'correct-answer', None) if correct_answer is not None: if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['correct_answers'][name] = correct_answer
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) submitted_key = data['submitted_answers'].get(name, None) correct_key = data['correct_answers'].get(name, {'key': None}).get('key', None) score = 0 if (submitted_key is not None and submitted_key == correct_key): score = 1 data['partial_scores'][name] = {'score': score, 'weight': weight}
def test(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') weight = pl.get_integer_attrib(element, 'weight', 1) # Get correct answer a_tru = data['correct_answers'][name] # If correct answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_tru = pl.from_json(a_tru) # Wrap true answer in ndarray (if it already is one, this does nothing) a_tru = np.array(a_tru) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if random.choice([True, False]): # matlab if result == 'correct': data['raw_submitted_answers'][name] = pl.numpy_to_matlab(a_tru, ndigits=12, wtype='g') data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = pl.numpy_to_matlab(a_tru + (random.uniform(1, 10) * random.choice([-1, 1])), ndigits=12, wtype='g') data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize data['raw_submitted_answers'][name] = '[1, 2, 3]' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result) else: # python if result == 'correct': data['raw_submitted_answers'][name] = str(np.array(a_tru).tolist()) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str((a_tru + (random.uniform(1, 10) * random.choice([-1, 1]))).tolist()) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': # FIXME: add more invalid expressions, make text of format_errors # correct, and randomize data['raw_submitted_answers'][name] = '[[1, 2, 3], [4, 5]]' data['format_errors'][name] = 'invalid' else: raise Exception('invalid result: %s' % result)
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') # Get weight weight = pl.get_integer_attrib(element, 'weight', 1) # Get remove-spaces option remove_spaces = pl.get_string_attrib(element, 'remove-spaces', False) # Get remove-leading-trailing option remove_leading_trailing = pl.get_string_attrib(element, 'remove-leading-trailing', False) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = pl.from_json(data['correct_answers'].get(name, None)) if a_tru is None: return # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # If submitted answer is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) a_sub = pl.from_json(a_sub) # Remove the leading and trailing characters if (remove_leading_trailing): a_sub = a_sub.strip() # Remove the blank spaces between characters if (remove_spaces): a_sub = a_sub.replace(' ', '') if a_tru == a_sub: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') variables = get_variables_list(pl.get_string_attrib(element, 'variables', None)) allow_complex = pl.get_boolean_attrib(element, 'allow-complex', False) weight = pl.get_integer_attrib(element, 'weight', 1) # Get true answer (if it does not exist, create no grade - leave it # up to the question code) a_tru = data['correct_answers'].get(name, None) if a_tru is None: return # Get submitted answer (if it does not exist, score is zero) a_sub = data['submitted_answers'].get(name, None) if a_sub is None: data['partial_scores'][name] = {'score': 0, 'weight': weight} return # Parse true answer if isinstance(a_tru, str): # this is so instructors can specify the true answer simply as a string a_tru = phs.convert_string_to_sympy(a_tru, variables, allow_complex=allow_complex) else: a_tru = phs.json_to_sympy(a_tru, allow_complex=allow_complex) # Parse submitted answer if isinstance(a_sub, str): # this is for backward-compatibility a_sub = phs.convert_string_to_sympy(a_sub, variables, allow_complex=allow_complex) else: a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex) # Check equality correct = a_tru.equals(a_sub) if correct: data['partial_scores'][name] = {'score': 1, 'weight': weight} else: data['partial_scores'][name] = {'score': 0, 'weight': weight}
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') label = pl.get_string_attrib(element, 'label', LABEL_DEFAULT) variables_string = pl.get_string_attrib(element, 'variables', VARIABLES_DEFAULT) variables = get_variables_list(variables_string) display = pl.get_string_attrib(element, 'display', DISPLAY_DEFAULT) allow_complex = pl.get_boolean_attrib(element, 'allow-complex', ALLOW_COMPLEX_DEFAULT) imaginary_unit = pl.get_string_attrib(element, 'imaginary-unit-for-display', IMAGINARY_UNIT_FOR_DISPLAY_DEFAULT) size = pl.get_integer_attrib(element, 'size', SIZE_DEFAULT) operators = [ 'cos', 'sin', 'tan', 'exp', 'log', 'sqrt', '( )', '+', '-', '*', '/', '^', '**' ] constants = ['pi', 'e'] if data['panel'] == 'question': editable = data['editable'] raw_submitted_answer = data['raw_submitted_answers'].get(name, None) info_params = { 'format': True, 'variables': variables, 'operators': operators, 'constants': constants, 'allow_complex': allow_complex, } with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: info_params.pop('format', None) info_params['shortformat'] = True shortinfo = chevron.render(f, info_params).strip() html_params = { 'question': True, 'name': name, 'label': label, 'editable': editable, 'info': info, 'shortinfo': shortinfo, 'size': size, 'show_info': pl.get_boolean_attrib(element, 'show-help-text', SHOW_HELP_TEXT_DEFAULT), 'uuid': pl.get_uuid(), 'allow_complex': allow_complex, 'show_placeholder': size >= PLACEHOLDER_TEXT_THRESHOLD } partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) if score is not None: try: score = float(score) if score >= 1: html_params['correct'] = True elif score > 0: html_params['partial'] = math.floor(score * 100) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score' + score) if display == 'inline': html_params['inline'] = True elif display == 'block': html_params['block'] = True else: raise ValueError( 'method of display "%s" is not valid (must be "inline" or "block")' % display) if raw_submitted_answer is not None: html_params['raw_submitted_answer'] = escape(raw_submitted_answer) with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'submission': parse_error = data['format_errors'].get(name, None) html_params = { 'submission': True, 'label': label, 'parse_error': parse_error, 'uuid': pl.get_uuid() } if parse_error is None and name in data['submitted_answers']: a_sub = data['submitted_answers'][name] if isinstance(a_sub, str): # this is for backward-compatibility a_sub = phs.convert_string_to_sympy( a_sub, variables, allow_complex=allow_complex) else: a_sub = phs.json_to_sympy(a_sub, allow_complex=allow_complex) a_sub = a_sub.subs(sympy.I, sympy.Symbol(imaginary_unit)) html_params['a_sub'] = sympy.latex(a_sub) elif name not in data['submitted_answers']: html_params['missing_input'] = True html_params['parse_error'] = None else: # Use the existing format text in the invalid popup. info_params = { 'format': True, 'variables': variables, 'operators': operators, 'constants': constants, 'allow_complex': allow_complex, } with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() # Render invalid popup raw_submitted_answer = data['raw_submitted_answers'].get( name, None) with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: parse_error += chevron.render(f, { 'format_error': True, 'format_string': info }).strip() html_params['parse_error'] = parse_error 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) 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) html_params['error'] = html_params['parse_error'] or html_params.get( 'missing_input', False) with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': a_tru = data['correct_answers'].get(name, None) if a_tru is not None: if isinstance(a_tru, str): # this is so instructors can specify the true answer simply as a string a_tru = phs.convert_string_to_sympy( a_tru, variables, allow_complex=allow_complex) else: a_tru = phs.json_to_sympy(a_tru, allow_complex=allow_complex) a_tru = a_tru.subs(sympy.I, sympy.Symbol(imaginary_unit)) html_params = { 'answer': True, 'label': label, 'a_tru': sympy.latex(a_tru) } with open('pl-symbolic-input.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' else: raise Exception('Invalid panel type: %s' % data['panel']) return html
def prepare(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers_name'] optional_attribs = ['weight', 'number_answers', 'fixed_order', 'inline'] pl.check_attribs(element, required_attribs, optional_attribs) name = element.get('answers_name') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag == 'pl_answer': pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct < 1: raise Exception('pl_multiple_choice element must have at least one correct answer') number_answers = pl.get_integer_attrib(element, 'number_answers', len_total) number_answers = max(1, min(1 + len_incorrect, number_answers)) number_correct = 1 number_incorrect = number_answers - number_correct if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed_order', False) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer = None for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer = keyed_answer if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers-name') 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) digits = pl.get_integer_attrib(element, 'digits', DIGITS_DEFAULT) show_matlab = pl.get_boolean_attrib(element, 'show-matlab', SHOW_MATLAB_DEFAULT) show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', SHOW_MATHEMATICA_DEFAULT) show_python = pl.get_boolean_attrib(element, 'show-python', SHOW_PYTHON_DEFAULT) show_r = pl.get_boolean_attrib(element, 'show-r', SHOW_R_DEFAULT) default_tab = pl.get_string_attrib(element, 'default-tab', DEFAULT_TAB_DEFAULT) tab_list = ['matlab', 'mathematica', 'python', 'r'] if default_tab not in tab_list: raise Exception(f'invalid default-tab: {default_tab}') # Setting the default tab displayed_tab = [show_matlab, show_mathematica, show_python, show_r] if not any(displayed_tab): raise Exception('All tabs have been hidden from display. At least one tab must be shown.') default_tab_index = tab_list.index(default_tab) # If not displayed, make first visible tab the default if not displayed_tab[default_tab_index]: first_display = displayed_tab.index(True) default_tab = tab_list[first_display] default_tab_index = tab_list.index(default_tab) # Active tab should be the default tab default_tab_list = [False, False, False, False] default_tab_list[default_tab_index] = True [active_tab_matlab, active_tab_mathematica, active_tab_python, active_tab_r] = default_tab_list # Process parameter data matlab_data = '' mathematica_data = '' python_data = 'import numpy as np\n\n' r_data = '' for child in element: if child.tag == 'variable': # Raise exception if variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits']) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl-variable-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) # Get comment, if it exists var_matlab_comment = '' var_mathematica_comment = '' var_python_comment = '' var_r_comment = '' if pl.has_attrib(child, 'comment'): var_comment = pl.get_string_attrib(child, 'comment') var_matlab_comment = f' % {var_comment}' var_mathematica_comment = f' (* {var_comment} *)' var_python_comment = f' # {var_comment}' var_r_comment = f' # {var_comment}' # Get digit for child, if it exists if not pl.has_attrib(child, 'digits'): var_digits = digits else: var_digits = pl.get_string_attrib(child, 'digits') # Assembling Python array formatting if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim > 2: raise Exception('Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Mathematica reserved letters: C D E I K N O mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O'] if pl.inner_html(child) in mathematica_reserved: mathematica_suffix = 'm' else: mathematica_suffix = '' # Create string for matlab and python format var_name_disp = pl.inner_html(child) var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits) var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits) var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits) var_r_data = pl.string_from_numpy(var_data, language='r', digits=var_digits) matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n' mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n' python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n' r_data += f'{var_name_disp} = {var_r_data}{var_r_comment}\n' html_params = { 'active_tab_matlab': active_tab_matlab, 'active_tab_mathematica': active_tab_mathematica, 'active_tab_python': active_tab_python, 'active_tab_r': active_tab_r, 'show_matlab': show_matlab, 'show_mathematica': show_mathematica, 'show_python': show_python, 'show_r': show_r, 'matlab_data': matlab_data, 'mathematica_data': mathematica_data, 'python_data': python_data, 'r_data': r_data, 'uuid': pl.get_uuid() } with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) # 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 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) name = pl.get_string_attrib(element, 'answers-name') partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT) partial_credit_method = pl.get_string_attrib( element, 'partial-credit-method', PARTIAL_CREDIT_METHOD_DEFAULT) editable = data['editable'] # answer feedback is not displayed when partial credit is True # (unless the question is disabled) show_answer_feedback = True if partial_credit and editable: show_answer_feedback = False display_answers = data['params'].get(name, []) inline = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT) submitted_keys = data['submitted_answers'].get(name, []) # if there is only one key then it is passed as a string, # not as a length-one list, so we fix that next if isinstance(submitted_keys, str): submitted_keys = [submitted_keys] correct_answer_list = data['correct_answers'].get(name, []) correct_keys = [answer['key'] for answer in correct_answer_list] if data['panel'] == 'question': partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) answerset = [] for answer in display_answers: answer_html = { 'key': answer['key'], 'checked': (answer['key'] in submitted_keys), 'html': answer['html'].strip(), 'display_score_badge': score is not None and show_answer_feedback and answer['key'] in submitted_keys } if answer_html['display_score_badge']: answer_html['correct'] = (answer['key'] in correct_keys) answer_html['incorrect'] = (answer['key'] not in correct_keys) answerset.append(answer_html) info_params = {'format': True} # Adds decorative help text per bootstrap formatting guidelines: # http://getbootstrap.com/docs/4.0/components/forms/#help-text # Determine whether we should add a choice selection requirement hide_help_text = pl.get_boolean_attrib(element, 'hide-help-text', HIDE_HELP_TEXT_DEFAULT) if not hide_help_text: # Should we reveal the depth of the choice? detailed_help_text = pl.get_boolean_attrib( element, 'detailed-help-text', DETAILED_HELP_TEXT_DEFAULT) min_correct = pl.get_integer_attrib(element, 'min-correct', 1) max_correct = pl.get_integer_attrib(element, 'max-correct', len(correct_answer_list)) if detailed_help_text: if min_correct != max_correct: insert_text = ' between <b>%d</b> and <b>%d</b> options.' % ( min_correct, max_correct) helptext = '<small class="form-text text-muted">Select ' + insert_text + '</small>' else: insert_text = ' exactly <b>%d</b> options.' % (max_correct) helptext = '<small class="form-text text-muted">Select' + insert_text + '</small>' else: insert_text = ' at least one option.' helptext = '<small class="form-text text-muted">Select all possible options that apply.</small>' if partial_credit: if partial_credit_method == 'PC': gradingtext = 'You must select ' + insert_text + ' You will receive a score of <code>100% * (t - f) / n</code>, ' \ + 'where <code>t</code> is the number of true options that you select, <code>f</code> ' \ + 'is the number of false options that you select, and <code>n</code> is the total number of true options. ' \ + 'At minimum, you will receive a score of 0%.' else: gradingtext = 'You must select ' + insert_text + ' You will receive a score of <code>100% * (t + f) / ' + str(len(display_answers)) + '</code>, ' \ + 'where <code>t</code> is the number of true options that you select and <code>f</code> ' \ + 'is the number of false options that you do not select.' else: gradingtext = 'You must select' + insert_text + ' You will receive a score of 100% ' \ + 'if you select all options that are true and no options that are false. ' \ + 'Otherwise, you will receive a score of 0%.' info_params.update({'gradingtext': gradingtext}) with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f: info = chevron.render(f, info_params).strip() html_params = { 'question': True, 'name': name, 'editable': editable, 'uuid': pl.get_uuid(), 'info': info, 'answers': answerset, 'inline': inline } if not hide_help_text: html_params['helptext'] = helptext 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-checkbox.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) if parse_error is None: partial_score = data['partial_scores'].get(name, {'score': None}) score = partial_score.get('score', None) answers = [] for submitted_key in submitted_keys: submitted_answer = next( filter(lambda a: a['key'] == submitted_key, display_answers), None) answer_item = { 'key': submitted_key, 'html': submitted_answer['html'], 'display_score_badge': score is not None and show_answer_feedback } if answer_item['display_score_badge']: answer_item['correct'] = (submitted_key in correct_keys) answer_item['incorrect'] = (submitted_key not in correct_keys) answers.append(answer_item) html_params = { 'submission': True, 'display_score_badge': (score is not None), 'answers': answers, 'inline': inline } if html_params['display_score_badge']: 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-checkbox.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html_params = { 'submission': True, 'uuid': pl.get_uuid(), 'parse_error': parse_error, 'inline': inline, } with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() elif data['panel'] == 'answer': if not pl.get_boolean_attrib(element, 'hide-answer-panel', HIDE_ANSWER_PANEL_DEFAULT): correct_answer_list = data['correct_answers'].get(name, []) if len(correct_answer_list) == 0: raise ValueError('At least one option must be true.') else: html_params = { 'answer': True, 'inline': inline, 'answers': correct_answer_list } with open('pl-checkbox.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() else: html = '' else: raise ValueError('Invalid panel type: %s' % data['panel']) return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', 2) matlab_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception of variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=[]) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception( 'No value in data["params"] for variable %s in pl-matrix-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim != 2: raise Exception( 'Value in data["params"] for variable %s in pl-matrix-output element must be a scalar or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Create string for matlab and python format matlab_data += pl.inner_html( child) + ' = ' + pl.string_from_2darray( var_data, language='matlab', digits=digits) + ';\n' python_data += pl.inner_html( child) + ' = ' + prefix + pl.string_from_2darray( var_data, language='python', digits=digits) + suffix + '\n' html_params = { 'default_is_matlab': True, 'matlab_data': matlab_data, 'python_data': python_data, 'uuid': pl.get_uuid() } with open('pl-matrix-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) digits = pl.get_integer_attrib(element, 'digits', 2) show_matlab = pl.get_boolean_attrib(element, 'show-matlab', True) show_mathematica = pl.get_boolean_attrib(element, 'show-mathematica', True) show_python = pl.get_boolean_attrib(element, 'show-python', True) default_tab = pl.get_string_attrib(element, 'default-tab', 'matlab') tab_list = ['matlab', 'mathematica', 'python'] if default_tab not in tab_list: raise Exception(f'invalid default-tab: {default_tab}') # Setting the default tab displayed_tab = [show_matlab, show_mathematica, show_python] if not any(displayed_tab): raise Exception('All tabs have been hidden from display. At least one tab must be shown.') default_tab_index = tab_list.index(default_tab) # If not displayed, make first visible tab the default if not displayed_tab[default_tab_index]: first_display = displayed_tab.index(True) default_tab = tab_list[first_display] default_tab_index = tab_list.index(default_tab) # Active tab should be the default tab default_tab_list = [False, False, False] default_tab_list[default_tab_index] = True [active_tab_matlab, active_tab_mathematica, active_tab_python] = default_tab_list # Process parameter data matlab_data = '' mathematica_data = '' python_data = 'import numpy as np\n\n' for child in element: if child.tag == 'variable': # Raise exception if variable does not have a name pl.check_attribs(child, required_attribs=['params-name'], optional_attribs=['comment', 'digits']) # Get name of variable var_name = pl.get_string_attrib(child, 'params-name') # Get value of variable, raising exception if variable does not exist var_data = data['params'].get(var_name, None) if var_data is None: raise Exception('No value in data["params"] for variable %s in pl-variable-output element' % var_name) # If the variable is in a format generated by pl.to_json, convert it # back to a standard type (otherwise, do nothing) var_data = pl.from_json(var_data) # Get comment, if it exists var_matlab_comment = '' var_mathematica_comment = '' var_python_comment = '' if pl.has_attrib(child, 'comment'): var_comment = pl.get_string_attrib(child, 'comment') var_matlab_comment = f' % {var_comment}' var_mathematica_comment = f' (* {var_comment} *)' var_python_comment = f' # {var_comment}' # Get digit for child, if it exists if not pl.has_attrib(child, 'digits'): var_digits = digits else: var_digits = pl.get_string_attrib(child, 'digits') # Assembling Python array formatting if np.isscalar(var_data): prefix = '' suffix = '' else: # Wrap the variable in an ndarray (if it's already one, this does nothing) var_data = np.array(var_data) # Check shape of variable if var_data.ndim > 2: raise Exception('Value in data["params"] for variable %s in pl-variable-output element must be a scalar, a vector, or a 2D array' % var_name) # Create prefix/suffix so python string is np.array( ... ) prefix = 'np.array(' suffix = ')' # Mathematica reserved letters: C D E I K N O mathematica_reserved = ['C', 'D', 'E', 'I', 'K', 'N', 'O'] if pl.inner_html(child) in mathematica_reserved: mathematica_suffix = 'm' else: mathematica_suffix = '' # Create string for matlab and python format var_name_disp = pl.inner_html(child) var_matlab_data = pl.string_from_numpy(var_data, language='matlab', digits=var_digits) var_mathematica = pl.string_from_numpy(var_data, language='mathematica', digits=var_digits) var_python_data = pl.string_from_numpy(var_data, language='python', digits=var_digits) matlab_data += f'{var_name_disp} = {var_matlab_data};{var_matlab_comment}\n' mathematica_data += f'{var_name_disp}{mathematica_suffix} = {var_mathematica};{var_mathematica_comment}\n' python_data += f'{var_name_disp} = {prefix}{var_python_data}{suffix}{var_python_comment}\n' html_params = { 'active_tab_matlab': active_tab_matlab, 'active_tab_mathematica': active_tab_mathematica, 'active_tab_python': active_tab_python, 'show_matlab': show_matlab, 'show_mathematica': show_mathematica, 'show_python': show_python, 'matlab_data': matlab_data, 'mathematica_data': mathematica_data, 'python_data': python_data, 'uuid': pl.get_uuid() } with open('pl-variable-output.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params).strip() return html
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct < 1: raise Exception('pl-multiple-choice element must have at least one correct answer') number_answers = pl.get_integer_attrib(element, 'number-answers', len_total) number_answers = max(1, min(1 + len_incorrect, number_answers)) number_correct = 1 number_incorrect = number_answers - number_correct if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed-order', False) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer = None for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer = keyed_answer if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer
def test(element_html, element_index, data): element = lxml.html.fragment_fromstring(element_html) name = pl.get_string_attrib(element, 'answers_name') weight = pl.get_integer_attrib(element, 'weight', 1) result = random.choices(['correct', 'incorrect', 'invalid'], [5, 5, 1])[0] if result == 'correct': data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) data['partial_scores'][name] = {'score': 1, 'weight': weight} elif result == 'incorrect': data['raw_submitted_answers'][name] = str(pl.from_json(data['correct_answers'][name])) + ' + {:d}'.format(random.randint(1, 100)) data['partial_scores'][name] = {'score': 0, 'weight': weight} elif result == 'invalid': invalid_type = random.choice(['float', 'complex', 'expression', 'function', 'variable', 'syntax', 'escape', 'comment']) if invalid_type == 'float': data['raw_submitted_answers'][name] = 'x + 1.234' s = 'Your answer contains the floating-point number ' + str(1.234) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error('x + 1.234', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'complex': data['raw_submitted_answers'][name] = 'x + (1+2j)' s = 'Your answer contains the complex number ' + str(2j) + '. ' s += 'All numbers must be expressed as integers (or ratios of integers). ' s += '<br><br><pre>' + phs.point_to_error('x + (1+2j)', 7) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'expression': data['raw_submitted_answers'][name] = '1 and 0' s = 'Your answer has an invalid expression. ' s += '<br><br><pre>' + phs.point_to_error('1 and 0', 0) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'function': data['raw_submitted_answers'][name] = 'atan(x)' s = 'Your answer calls an invalid function "' + 'atan' + '". ' s += '<br><br><pre>' + phs.point_to_error('atan(x)', 0) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'variable': data['raw_submitted_answers'][name] = 'x + y' s = 'Your answer refers to an invalid variable "' + 'y' + '". ' s += '<br><br><pre>' + phs.point_to_error('x + y', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'syntax': data['raw_submitted_answers'][name] = 'x +* 1' s = 'Your answer has a syntax error. ' s += '<br><br><pre>' + phs.point_to_error('x +* 1', 4) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'escape': data['raw_submitted_answers'][name] = 'x + 1\\n' s = 'Your answer must not contain the character "\\". ' s += '<br><br><pre>' + phs.point_to_error('x + 1\\n', 5) + '</pre>' data['format_errors'][name] = s elif invalid_type == 'comment': data['raw_submitted_answers'][name] = 'x # some text' s = 'Your answer must not contain the character "#". ' s += '<br><br><pre>' + phs.point_to_error('x # some text', 2) + '</pre>' data['format_errors'][name] = s else: raise Exception('invalid invalid_type: %s' % invalid_type) else: raise Exception('invalid result: %s' % result) return data
def render(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answers-name') if data['panel'] == 'question': mcq_options = [] student_previous_submission = [] submission_indent = [] student_submission_dict_list = [] answer_name = pl.get_string_attrib(element, 'answers-name') source_header = pl.get_string_attrib(element, 'source-header', SOURCE_HEADER_DEFAULT) solution_header = pl.get_string_attrib(element, 'solution-header', SOLUTION_HEADER_DEFAULT) grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) mcq_options = data['params'][answer_name] mcq_options = filter_multiple_from_array(mcq_options, ['inner_html', 'uuid']) if answer_name in data['submitted_answers']: student_previous_submission = filter_multiple_from_array(data['submitted_answers'][answer_name], ['inner_html', 'uuid', 'indent']) mcq_options = [opt for opt in mcq_options if opt not in filter_multiple_from_array(student_previous_submission, ['inner_html', 'uuid'])] for index, option in enumerate(student_previous_submission): submission_indent = option.get('indent', None) if submission_indent is not None: submission_indent = (int(submission_indent) * TAB_SIZE_PX) + INDENT_OFFSET temp = {'inner_html': option['inner_html'], 'indent': submission_indent, 'uuid': option['uuid']} student_submission_dict_list.append(dict(temp)) dropzone_layout = pl.get_string_attrib(element, 'solution-placement', SOLUTION_PLACEMENT_DEFAULT) check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) max_indent = pl.get_integer_attrib(element, 'max-indent', MAX_INDENTION_DEFAULT) inline_layout = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT) help_text = 'Drag answer tiles into the answer area to the ' + dropzone_layout + '. ' if grading_method == 'unordered': help_text += '<br>Your answer ordering does not matter. ' elif grading_method != 'external': help_text += '<br>The ordering of your answer matters and is graded.' else: help_text += '<br>Your answer will be autograded; be sure to indent and order your answer properly.' if check_indentation: help_text += '<br><b>Your answer should be indented. </b> Indent your tiles by dragging them horizontally in the answer area.' uuid = pl.get_uuid() html_params = { 'question': True, 'answer_name': answer_name, 'options': mcq_options, 'source-header': source_header, 'solution-header': solution_header, 'submission_dict': student_submission_dict_list, 'dropzone_layout': 'pl-order-blocks-bottom' if dropzone_layout == 'bottom' else 'pl-order-blocks-right', 'check_indentation': 'true' if check_indentation else 'false', 'help_text': help_text, 'inline': 'inline' if inline_layout is True else None, 'max_indent': max_indent, 'uuid': uuid } with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params) return html elif data['panel'] == 'submission': if pl.get_string_attrib(element, 'grading-method', 'ordered') == 'external': return '' # external grader is responsible for displaying results screen student_submission = '' score = None feedback = None if answer_name in data['submitted_answers']: student_submission = [{ 'inner_html': attempt['inner_html'], 'indent': ((attempt['indent'] or 0) * TAB_SIZE_PX) + INDENT_OFFSET } for attempt in data['submitted_answers'][answer_name]] if answer_name in data['partial_scores']: score = data['partial_scores'][answer_name]['score'] feedback = data['partial_scores'][answer_name]['feedback'] html_params = { 'submission': True, 'parse-error': data['format_errors'].get(answer_name, None), 'student_submission': student_submission, 'feedback': feedback } if score is not None: try: score = float(score * 100) if score >= 100: html_params['correct'] = True elif score > 0: html_params['partially_correct'] = math.floor(score) else: html_params['incorrect'] = True except Exception: raise ValueError('invalid score: ' + data['partial_scores'][answer_name]['score']) with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params) return html elif data['panel'] == 'answer': if pl.get_string_attrib(element, 'grading-method', 'ordered') == 'external': try: base_path = data['options']['question_path'] file_lead_path = os.path.join(base_path, 'tests/ans.py') with open(file_lead_path, 'r') as file: solution_file = file.read() return f'<pl-code language="python">{solution_file}</pl-code>' except FileNotFoundError: return 'The reference solution is not provided for this question.' grading_mode = pl.get_string_attrib(element, 'grading-method', 'ordered') if grading_mode == 'unordered': grading_mode = 'in any order' elif grading_mode == 'dag' or grading_mode == 'ranking': grading_mode = 'one possible correct order' else: grading_mode = 'in the specified order' check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) indentation_message = ', with correct indentation' if check_indentation is True else None if answer_name in data['correct_answers']: question_solution = [{ 'inner_html': solution['inner_html'], 'indent': ((solution['indent'] or 0) * TAB_SIZE_PX) + INDENT_OFFSET } for solution in data['correct_answers'][answer_name]] html_params = { 'true_answer': True, 'question_solution': question_solution, 'grading_mode': grading_mode, 'indentation_message': indentation_message } with open('pl-order-blocks.mustache', 'r', encoding='utf-8') as f: html = chevron.render(f, html_params) return html else: return '' else: raise Exception('Invalid panel type')
def grade(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answers-name') student_answer = data['submitted_answers'][answer_name] grading_mode = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT) answer_weight = pl.get_integer_attrib(element, 'weight', WEIGHT_DEFAULT) partial_credit_type = pl.get_string_attrib(element, 'partial-credit', 'lcs') true_answer_list = data['correct_answers'][answer_name] final_score = 0 feedback = '' first_wrong = -1 if len(student_answer) == 0: data['format_errors'][answer_name] = 'Your submitted answer was empty.' return if check_indentation: indentations = {ans['uuid']: ans['indent'] for ans in true_answer_list} for ans in student_answer: if ans['indent'] != indentations.get(ans['uuid']): if 'tag' in ans: ans['tag'] = None else: ans['inner_html'] = None if grading_mode == 'unordered': true_answer_list = filter_multiple_from_array(true_answer_list, ['uuid', 'indent', 'inner_html']) correct_selections = [opt for opt in student_answer if opt in true_answer_list] incorrect_selections = [opt for opt in student_answer if opt not in true_answer_list] final_score = float((len(correct_selections) - len(incorrect_selections)) / len(true_answer_list)) final_score = max(0.0, final_score) # scores cannot be below 0 elif grading_mode == 'ordered': student_answer = [ans['inner_html'] for ans in student_answer] true_answer = [ans['inner_html'] for ans in true_answer_list] final_score = 1 if student_answer == true_answer else 0 elif grading_mode in ['ranking', 'dag']: submission = [ans['tag'] for ans in student_answer] depends_graph = {} group_belonging = {} if grading_mode == 'ranking': true_answer_list = sorted(true_answer_list, key=lambda x: int(x['ranking'])) true_answer = [answer['tag'] for answer in true_answer_list] tag_to_rank = {answer['tag']: answer['ranking'] for answer in true_answer_list} lines_of_rank = {rank: [tag for tag in tag_to_rank if tag_to_rank[tag] == rank] for rank in set(tag_to_rank.values())} cur_rank_depends = [] prev_rank = None for tag in true_answer: ranking = tag_to_rank[tag] if prev_rank is not None and ranking != prev_rank: cur_rank_depends = lines_of_rank[prev_rank] depends_graph[tag] = cur_rank_depends prev_rank = ranking elif grading_mode == 'dag': depends_graph = {ans['tag']: ans['depends'] for ans in true_answer_list} group_belonging = {ans['tag']: ans['group'] for ans in true_answer_list} num_initial_correct = grade_dag(submission, depends_graph, group_belonging) first_wrong = -1 if num_initial_correct == len(submission) else num_initial_correct true_answer_length = len(depends_graph.keys()) if partial_credit_type == 'none': if num_initial_correct == true_answer_length: final_score = 1 elif num_initial_correct < true_answer_length: final_score = 0 elif partial_credit_type == 'lcs': edit_distance = lcs_partial_credit(submission, depends_graph, group_belonging) final_score = max(0, float(true_answer_length - edit_distance) / true_answer_length) if final_score < 1: if feedback_type == 'none': feedback = '' elif feedback_type == 'first-wrong': if first_wrong == -1: feedback = FIRST_WRONG_FEEDBACK['incomplete'] else: feedback = FIRST_WRONG_FEEDBACK['wrong-at-block'].format(str(first_wrong + 1)) has_block_groups = group_belonging != {} and set(group_belonging.values()) != {None} if check_indentation: feedback += FIRST_WRONG_FEEDBACK['indentation'] if has_block_groups: feedback += FIRST_WRONG_FEEDBACK['block-group'] feedback += '</ul>' data['partial_scores'][answer_name] = {'score': round(final_score, 2), 'feedback': feedback, 'weight': answer_weight, 'first_wrong': first_wrong}
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = ['weight', 'number-answers', 'fixed-order', 'inline', 'none-of-the-above', 'all-of-the-above', 'hide-letter-keys'] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') correct_answers, incorrect_answers = categorize_options(element) len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect enable_nota = pl.get_boolean_attrib(element, 'none-of-the-above', NONE_OF_THE_ABOVE_DEFAULT) enable_aota = pl.get_boolean_attrib(element, 'all-of-the-above', ALL_OF_THE_ABOVE_DEFAULT) nota_correct = False aota_correct = False if enable_nota or enable_aota: prob_space = len_correct + enable_nota + enable_aota rand_int = random.randint(1, prob_space) # Either 'None of the above' or 'All of the above' is correct # with probability 1/(number_correct + enable-nota + enable-aota). # However, if len_correct is 0, nota_correct is guaranteed to be True. # Thus, if no correct option is provided, 'None of the above' will always # be correct, and 'All of the above' always incorrect nota_correct = enable_nota and (rand_int == 1 or len_correct == 0) # 'All of the above' will always be correct when no incorrect option is # provided, while still never both True aota_correct = enable_aota and (rand_int == 2 or len_incorrect == 0) and not nota_correct if len_correct < 1 and not enable_nota: # This means the code needs to handle the special case when len_correct == 0 raise Exception('pl-multiple-choice element must have at least 1 correct answer or set none-of-the-above') if enable_aota and len_correct < 2: # To prevent confusion on the client side raise Exception('pl-multiple-choice element must have at least 2 correct answers when all-of-the-above is set') # 1. Pick the choice(s) to display number_answers = pl.get_integer_attrib(element, 'number-answers', None) # determine if user provides number-answers set_num_answers = True if number_answers is None: set_num_answers = False number_answers = len_total + enable_nota + enable_aota # figure out how many choice(s) to choose from the *provided* choices, # excluding 'none-of-the-above' and 'all-of-the-above' number_answers -= (enable_nota + enable_aota) expected_num_answers = number_answers if enable_aota: # min number if 'All of the above' is correct number_answers = min(len_correct, number_answers) # raise exception when the *provided* number-answers can't be satisfied if set_num_answers and number_answers < expected_num_answers: raise Exception(f'Not enough correct choices for all-of-the-above. Need {expected_num_answers - number_answers} more') if enable_nota: # if nota correct number_answers = min(len_incorrect, number_answers) # raise exception when the *provided* number-answers can't be satisfied if set_num_answers and number_answers < expected_num_answers: raise Exception(f'Not enough incorrect choices for none-of-the-above. Need {expected_num_answers - number_answers} more') # this is the case for # - 'All of the above' is incorrect # - 'None of the above' is incorrect # - nota and aota disabled number_answers = min(min(1, len_correct) + len_incorrect, number_answers) if aota_correct: # when 'All of the above' is correct, we choose all from correct # and none from incorrect number_correct = number_answers number_incorrect = 0 elif nota_correct: # when 'None of the above' is correct, we choose all from incorrect # and none from correct number_correct = 0 number_incorrect = number_answers else: # PROOF: by the above probability, if len_correct == 0, then nota_correct # conversely; if not nota_correct, then len_correct != 0. Since len_correct # is none negative, this means len_correct >= 1. number_correct = 1 number_incorrect = max(0, number_answers - number_correct) if not (0 <= number_incorrect <= len_incorrect): raise Exception('INTERNAL ERROR: number_incorrect: (%d, %d, %d)' % (number_incorrect, len_incorrect, number_answers)) # 2. Sample correct and incorrect choices sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) # 3. Modify sampled choices fixed_order = pl.get_boolean_attrib(element, 'fixed-order', FIXED_ORDER_DEFAULT) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort(key=lambda a: a[0]) # sort by stored original index inline = pl.get_boolean_attrib(element, 'inline', INLINE_DEFAULT) if enable_aota: if inline: aota_text = 'All of these' else: aota_text = 'All of the above' # Add 'All of the above' option after shuffling sampled_answers.append((len_total, aota_correct, aota_text)) if enable_nota: if inline: nota_text = 'None of these' else: nota_text = 'None of the above' # Add 'None of the above' option after shuffling sampled_answers.append((len_total + 1, nota_correct, nota_text)) # 4. Write to data # Because 'All of the above' is below all the correct choice(s) when it's # true, the variable correct_answer will save it as correct, and # overwriting previous choice(s) display_answers = [] correct_answer = None for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer = keyed_answer if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) required_attribs = ['answers-name'] optional_attribs = [ 'weight', 'number-answers', 'min-correct', 'max-correct', 'fixed-order', 'inline', 'hide-answer-panel', 'hide-help-text', 'detailed-help-text', 'partial-credit', 'partial-credit-method' ] pl.check_attribs(element, required_attribs, optional_attribs) name = pl.get_string_attrib(element, 'answers-name') partial_credit = pl.get_boolean_attrib(element, 'partial-credit', PARTIAL_CREDIT_DEFAULT) partial_credit_method = pl.get_string_attrib(element, 'partial-credit-method', None) if not partial_credit and partial_credit_method is not None: raise Exception( 'Cannot specify partial-credit-method if partial-credit is not enabled' ) correct_answers = [] incorrect_answers = [] index = 0 for child in element: if child.tag in ['pl-answer', 'pl_answer']: pl.check_attribs(child, required_attribs=[], optional_attribs=['correct']) correct = pl.get_boolean_attrib(child, 'correct', False) child_html = pl.inner_html(child) answer_tuple = (index, correct, child_html) if correct: correct_answers.append(answer_tuple) else: incorrect_answers.append(answer_tuple) index += 1 len_correct = len(correct_answers) len_incorrect = len(incorrect_answers) len_total = len_correct + len_incorrect if len_correct == 0: raise ValueError('At least one option must be true.') number_answers = pl.get_integer_attrib(element, 'number-answers', len_total) min_correct = pl.get_integer_attrib(element, 'min-correct', 1) max_correct = pl.get_integer_attrib(element, 'max-correct', len(correct_answers)) if min_correct < 1: raise ValueError( 'The attribute min-correct is {:d} but must be at least 1'.format( min_correct)) # FIXME: why enforce a maximum number of options? max_answers = 26 # will not display more than 26 checkbox answers number_answers = max(0, min(len_total, min(max_answers, number_answers))) min_correct = min( len_correct, min(number_answers, max(0, max(number_answers - len_incorrect, min_correct)))) max_correct = min(len_correct, min(number_answers, max(min_correct, max_correct))) if not (0 <= min_correct <= max_correct <= len_correct): raise ValueError( 'INTERNAL ERROR: correct number: (%d, %d, %d, %d)' % (min_correct, max_correct, len_correct, len_incorrect)) min_incorrect = number_answers - max_correct max_incorrect = number_answers - min_correct if not (0 <= min_incorrect <= max_incorrect <= len_incorrect): raise ValueError( 'INTERNAL ERROR: incorrect number: (%d, %d, %d, %d)' % (min_incorrect, max_incorrect, len_incorrect, len_correct)) number_correct = random.randint(min_correct, max_correct) number_incorrect = number_answers - number_correct sampled_correct = random.sample(correct_answers, number_correct) sampled_incorrect = random.sample(incorrect_answers, number_incorrect) sampled_answers = sampled_correct + sampled_incorrect random.shuffle(sampled_answers) fixed_order = pl.get_boolean_attrib(element, 'fixed-order', FIXED_ORDER_DEFAULT) if fixed_order: # we can't simply skip the shuffle because we already broke the original # order by separating into correct/incorrect lists sampled_answers.sort( key=lambda a: a[0]) # sort by stored original index display_answers = [] correct_answer_list = [] for (i, (index, correct, html)) in enumerate(sampled_answers): keyed_answer = {'key': chr(ord('a') + i), 'html': html} display_answers.append(keyed_answer) if correct: correct_answer_list.append(keyed_answer) if name in data['params']: raise Exception('duplicate params variable name: %s' % name) if name in data['correct_answers']: raise Exception('duplicate correct_answers variable name: %s' % name) data['params'][name] = display_answers data['correct_answers'][name] = correct_answer_list
def prepare(element_html, data): element = lxml.html.fragment_fromstring(element_html) answer_name = pl.get_string_attrib(element, 'answers-name') required_attribs = ['answers-name'] optional_attribs = ['source-blocks-order', 'grading-method', 'indentation', 'source-header', 'solution-header', 'file-name', 'solution-placement', 'max-incorrect', 'min-incorrect', 'weight', 'inline', 'max-indent', 'feedback', 'partial-credit'] pl.check_attribs(element, required_attribs=required_attribs, optional_attribs=optional_attribs) check_indentation = pl.get_boolean_attrib(element, 'indentation', INDENTION_DEFAULT) grading_method = pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) feedback_type = pl.get_string_attrib(element, 'feedback', FEEDBACK_DEFAULT) if grading_method in ['dag', 'ranking']: partial_credit_type = pl.get_string_attrib(element, 'partial-credit', 'lcs') if partial_credit_type not in ['none', 'lcs']: raise Exception('partial credit type "' + partial_credit_type + '" is not available with the "' + grading_method + '" grading-method.') elif pl.get_string_attrib(element, 'partial-credit', None) is not None: raise Exception('You may only specify partial credit options in the DAG and ranking grading modes.') accepted_grading_method = ['ordered', 'unordered', 'ranking', 'dag', 'external'] if grading_method not in accepted_grading_method: raise Exception('The grading-method attribute must be one of the following: ' + ', '.join(accepted_grading_method)) if (grading_method not in ['dag', 'ranking'] and feedback_type != 'none') or \ (grading_method in ['dag', 'ranking'] and feedback_type not in ['none', 'first-wrong']): raise Exception('feedback type "' + feedback_type + '" is not available with the "' + grading_method + '" grading-method.') correct_answers = [] incorrect_answers = [] def prepare_tag(html_tags, index, group=None): if html_tags.tag != 'pl-answer': raise Exception('Any html tags nested inside <pl-order-blocks> must be <pl-answer> or <pl-block-group>. \ Any html tags nested inside <pl-block-group> must be <pl-answer>') if grading_method == 'external': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct']) elif grading_method == 'unordered': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'indent']) elif grading_method in ['ranking', 'ordered']: pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'ranking', 'indent']) elif grading_method == 'dag': pl.check_attribs(html_tags, required_attribs=[], optional_attribs=['correct', 'tag', 'depends', 'comment', 'indent']) is_correct = pl.get_boolean_attrib(html_tags, 'correct', PL_ANSWER_CORRECT_DEFAULT) answer_indent = pl.get_integer_attrib(html_tags, 'indent', None) inner_html = pl.inner_html(html_tags) ranking = pl.get_integer_attrib(html_tags, 'ranking', -1) tag = pl.get_string_attrib(html_tags, 'tag', None) if grading_method == 'ranking': tag = str(index) depends = pl.get_string_attrib(html_tags, 'depends', '') depends = depends.strip().split(',') if depends else [] if check_indentation is False and answer_indent is not None: raise Exception('<pl-answer> should not specify indentation if indentation is disabled.') answer_data_dict = {'inner_html': inner_html, 'indent': answer_indent, 'ranking': ranking, 'index': index, 'tag': tag, # set by HTML with DAG grader, set internally for ranking grader 'depends': depends, # only used with DAG grader 'group': group # only used with DAG grader } if is_correct: correct_answers.append(answer_data_dict) else: incorrect_answers.append(answer_data_dict) index = 0 group_counter = 0 for html_tags in element: # iterate through the html tags inside pl-order-blocks if html_tags.tag is etree.Comment: continue elif html_tags.tag == 'pl-block-group': if grading_method != 'dag': raise Exception('Block groups only supported in the "dag" grading mode.') group_counter += 1 for grouped_tag in html_tags: if html_tags.tag is etree.Comment: continue else: prepare_tag(grouped_tag, index, group_counter) index += 1 else: prepare_tag(html_tags, index) index += 1 if pl.get_string_attrib(element, 'grading-method', GRADING_METHOD_DEFAULT) != 'external' and len(correct_answers) == 0: raise Exception('There are no correct answers specified for this question.') all_incorrect_answers = len(incorrect_answers) max_incorrect = pl.get_integer_attrib(element, 'max-incorrect', all_incorrect_answers) min_incorrect = pl.get_integer_attrib(element, 'min-incorrect', all_incorrect_answers) if min_incorrect > len(incorrect_answers) or max_incorrect > len(incorrect_answers): raise Exception('The min-incorrect or max-incorrect attribute may not exceed the number of incorrect <pl-answers>.') if min_incorrect > max_incorrect: raise Exception('The attribute min-incorrect must be smaller than max-incorrect.') incorrect_answers_count = random.randint(min_incorrect, max_incorrect) sampled_correct_answers = correct_answers sampled_incorrect_answers = random.sample(incorrect_answers, incorrect_answers_count) mcq_options = sampled_correct_answers + sampled_incorrect_answers source_blocks_order = pl.get_string_attrib(element, 'source-blocks-order', SOURCE_BLOCKS_ORDER_DEFAULT) if source_blocks_order == 'random': random.shuffle(mcq_options) elif source_blocks_order == 'ordered': mcq_options.sort(key=lambda a: a['index']) else: raise Exception('The specified option for the "source-blocks-order" attribute is invalid.') # data['params'][answer_name] = filter_keys_from_array(mcq_options, 'inner_html') for option in mcq_options: option['uuid'] = pl.get_uuid() data['params'][answer_name] = mcq_options data['correct_answers'][answer_name] = correct_answers