def test_parse_string(self): parsed_str = jinja_utils.parse_string('{{test}}', {'test': 'hi'}) self.assertEqual(parsed_str, 'hi') # Some parameters are missing. parsed_str = jinja_utils.parse_string('{{test}} and {{test2}}', {'test2': 'hi'}) self.assertEqual(parsed_str, ' and hi') # All parameters are missing. parsed_str = jinja_utils.parse_string('{{test}} and {{test2}}', {}) self.assertEqual(parsed_str, ' and ') # The string has no parameters. parsed_str = jinja_utils.parse_string('no params', {'param': 'hi'}) self.assertEqual(parsed_str, 'no params') # Integer parameters are used. parsed_str = jinja_utils.parse_string('int {{i}}', {'i': 2}) self.assertEqual(parsed_str, 'int 2') # Invalid input string is used. with self.assertRaisesRegexp(Exception, 'Unable to parse string with Jinja: {{'): jinja_utils.parse_string('{{', {'a': 3, 'b': 0}) # Invalid expression is used. parsed_str = jinja_utils.parse_string('{{ a/b }}', {'a': 1, 'b': 0}) self.assertEqual(parsed_str, python_utils.UNICODE('[CONTENT PARSING ERROR]'))
def get_reader_response_html(self, state_customization_args, context_params, answer): """Gets the parameterized HTML and iframes for a reader response.""" if not self.is_interactive: raise Exception( 'This method should only be called for interactive widgets.') parameters = self._get_widget_param_instances( state_customization_args, context_params) parameters['answer'] = answer html, iframe = self._response_template_and_iframe html = jinja_utils.parse_string(html, parameters) iframe = jinja_utils.parse_string(iframe, parameters) return html, iframe
def get_raw_code(self, state_customization_args, context_params, preview_mode=False): """Gets the raw code for a parameterized widget.""" return jinja_utils.parse_string( self.template, self._get_widget_param_instances( state_customization_args, context_params, preview_mode=preview_mode))
def evaluate_rule(definition, param_specs, answer_type, context_params, answer): """Evaluates a rule definition using context_params. Returns a boolean.""" if 'rule_type' not in definition: raise Exception('No rule type specified when constructing rule.') elif definition['rule_type'] == DEFAULT_RULE_TYPE: return True elif definition['rule_type'] == ATOMIC_RULE_TYPE: subject_name = definition['subject'] if subject_name == 'answer': subject_type = answer_type else: subject_type = param_specs[subject_name].obj_type all_rule_classes = get_rules_for_input_type(subject_type) rule = next(r for r in all_rule_classes if r.__name__ == definition['name']) param_list = [] param_defns = get_param_list(rule.description) for (param_name, obj_cls) in param_defns: parsed_param = definition['inputs'][param_name] if (isinstance(parsed_param, basestring) and '{{' in parsed_param): parsed_param = jinja_utils.parse_string( parsed_param, context_params) normalized_param = obj_cls.normalize(parsed_param) param_list.append(normalized_param) if subject_name == 'answer': subject = answer else: subject = context_params[subject_name] return rule(*param_list).eval(subject) elif definition['rule_type'] == AND_RULE_TYPE: for child_dict in definition['children']: if not evaluate_rule( child_dict, param_specs, answer_type, context_params, answer): return False return True elif definition['rule_type'] == OR_RULE_TYPE: for child_dict in definition['children']: if evaluate_rule( child_dict, param_specs, answer_type, context_params, answer): return True return False elif definition['rule_type'] == NOT_RULE_TYPE: return (not evaluate_rule( definition['child'], param_specs, answer_type, context_params, answer)) else: raise Exception('Unrecognized rule type %s' % definition['rule_type'])
def submit_answer(self, exploration_id, state_name, answer, params=None, unused_exploration_version=None): """Submits an answer as an exploration player and returns the corresponding dict. This function has strong parallels to code in PlayerServices.js which has the non-test code to perform the same functionality. This is replicated here so backend tests may utilize the functionality of PlayerServices.js without being able to access it. TODO(bhenning): Replicate this in an end-to-end Protractor test to protect against code skew here. """ if params is None: params = {} exploration = exp_services.get_exploration_by_id(exploration_id) # First, the answer must be classified. classify_result = self.post_json( '/explorehandler/classify/%s' % exploration_id, { 'old_state': exploration.states[state_name].to_dict(), 'params': params, 'answer': answer }) # Next, ensure the submission is recorded. self.post_json( '/explorehandler/answer_submitted_event/%s' % exploration_id, { 'answer': answer, 'params': params, 'version': exploration.version, 'old_state_name': state_name, 'answer_group_index': classify_result['answer_group_index'], 'rule_spec_index': classify_result['rule_spec_index'] }) # Now the next state's data must be calculated. outcome = classify_result['outcome'] new_state = exploration.states[outcome['dest']] params['answer'] = answer new_params = self.get_updated_param_dict(params, new_state.param_changes, exploration.param_specs) return { 'feedback_html': jinja_utils.parse_string( utils.get_random_choice(outcome['feedback']) if outcome['feedback'] else '', params), 'question_html': new_state.content[0].to_html(new_params), 'state_name': outcome['dest'] }
def to_html(self, params): """Exports this content object to an HTML string. The content object is parameterized using the parameters in `params`. """ if not isinstance(params, dict): raise Exception( 'Expected context params for parsing content to be a dict, ' 'received %s' % params) return '<div>%s</div>' % jinja_utils.parse_string(self.value, params)
def test_parse_string(self): parsed_str = jinja_utils.parse_string('{{test}}', {'test': 'hi'}) self.assertEqual(parsed_str, 'hi') # Some parameters are missing. parsed_str = jinja_utils.parse_string('{{test}} and {{test2}}', {'test2': 'hi'}) self.assertEqual(parsed_str, ' and hi') # All parameters are missing. parsed_str = jinja_utils.parse_string('{{test}} and {{test2}}', {}) self.assertEqual(parsed_str, ' and ') # The string has no parameters. parsed_str = jinja_utils.parse_string('no params', {'param': 'hi'}) self.assertEqual(parsed_str, 'no params') # Integer parameters are used. parsed_str = jinja_utils.parse_string('int {{i}}', {'i': 2}) self.assertEqual(parsed_str, 'int 2')
def test_parse_string(self): parsed_str = jinja_utils.parse_string('{{test}}', {'test': 'hi'}) self.assertEqual(parsed_str, 'hi') # Some parameters are missing. parsed_str = jinja_utils.parse_string( '{{test}} and {{test2}}', {'test2': 'hi'}) self.assertEqual(parsed_str, ' and hi') # All parameters are missing. parsed_str = jinja_utils.parse_string('{{test}} and {{test2}}', {}) self.assertEqual(parsed_str, ' and ') # The string has no parameters. parsed_str = jinja_utils.parse_string('no params', {'param': 'hi'}) self.assertEqual(parsed_str, 'no params') # Integer parameters are used. parsed_str = jinja_utils.parse_string('int {{i}}', {'i': 2}) self.assertEqual(parsed_str, 'int 2')
def submit_answer( self, exploration_id, state_name, answer, params=None, unused_exploration_version=None): """Submits an answer as an exploration player and returns the corresponding dict. This function has strong parallels to code in PlayerServices.js which has the non-test code to perform the same functionality. This is replicated here so backend tests may utilize the functionality of PlayerServices.js without being able to access it. TODO(bhenning): Replicate this in an end-to-end Protractor test to protect against code skew here. """ if params is None: params = {} exploration = exp_services.get_exploration_by_id(exploration_id) # First, the answer must be classified. classify_result = self.post_json( '/explorehandler/classify/%s' % exploration_id, { 'old_state': exploration.states[state_name].to_dict(), 'params': params, 'answer': answer } ) # Next, ensure the submission is recorded. self.post_json( '/explorehandler/answer_submitted_event/%s' % exploration_id, { 'answer': answer, 'params': params, 'version': exploration.version, 'old_state_name': state_name, 'answer_group_index': classify_result['answer_group_index'], 'rule_spec_index': classify_result['rule_spec_index'] } ) # Now the next state's data must be calculated. outcome = classify_result['outcome'] new_state = exploration.states[outcome['dest']] params['answer'] = answer new_params = self.get_updated_param_dict( params, new_state.param_changes, exploration.param_specs) return { 'feedback_html': jinja_utils.parse_string( utils.get_random_choice(outcome['feedback']) if outcome['feedback'] else '', params), 'question_html': new_state.content[0].to_html(new_params), 'state_name': outcome['dest'] }
def get_stats_log_html(self, state_customization_args, context_params, answer): """Gets the HTML for recording a reader response for the stats log. Returns an HTML string. """ if not self.is_interactive: raise Exception( 'This method should only be called for interactive widgets.') parameters = self._get_widget_param_instances( state_customization_args, context_params) parameters['answer'] = answer return jinja_utils.parse_string(self._stats_log_template, parameters)
def submit_answer_in_tests( exploration_id, state_name, answer, params, handler_name, version): """This function should only be used by tests.""" params['answer'] = answer exploration = exp_services.get_exploration_by_id( exploration_id, version=version) exp_param_specs = exploration.param_specs old_state = exploration.states[state_name] rule_spec = classify( exploration_id, exp_param_specs, old_state, handler_name, answer, params) old_interaction_instance = ( interaction_registry.Registry.get_interaction_by_id( old_state.interaction.id)) normalized_answer = old_interaction_instance.normalize_answer( answer, handler_name) # TODO(sll): Should this also depend on `params`? event_services.AnswerSubmissionEventHandler.record( exploration_id, version, state_name, handler_name, rule_spec, old_interaction_instance.get_stats_log_html( old_state.interaction.customization_args, normalized_answer)) # This is necessary due to legacy reasons with the old 'END' pseudostate # TODO(bhenning): remove the need for this in this function (should be # basing terminal logic on whether it is in a terminal state, not # specifically in the 'END' state) _OLD_END_DEST = 'END' new_state = ( None if rule_spec.dest == _OLD_END_DEST else exploration.states[rule_spec.dest]) finished = (rule_spec.dest == _OLD_END_DEST) new_params = _get_updated_param_dict( params, {} if finished else new_state.param_changes, exp_param_specs) return { 'feedback_html': jinja_utils.parse_string( rule_spec.get_feedback_string(), params), 'finished': finished, 'params': new_params, 'question_html': ( new_state.content[0].to_html(new_params) if not finished else ''), 'state_name': rule_spec.dest if not finished else None, }
def get_interactive_widget_tag(self, state_customization_args, context_params, preview_mode=False): """Gets the widgetParams attribute value for an interactive widget.""" if state_customization_args is None: state_customization_args = {} tag_name = ('oppia-interactive-%s' % utils.camelcase_to_hyphenated(self.id)) attr_strings = [] for param in self.params: value_generator = param.generator(**param.init_args) # Use the given customization args. If they do not exist, use the # default customization args for the parameter. args_to_use = ( state_customization_args[param.name] if param.name in state_customization_args else param.customization_args ) try: generated_value = value_generator.generate_value( context_params, **args_to_use) except Exception: if preview_mode: generated_value = value_generator.default_value else: raise # Normalize the generated values to the correct obj_type. param_value = ( obj_services.Registry.get_object_class_by_type( param.obj_type).normalize(generated_value)) prefix = '%s-with-' % utils.camelcase_to_hyphenated(param.name) for arg in param.customization_args: arg_name = '%s%s' % ( prefix, utils.camelcase_to_hyphenated(arg)) attr_strings.append( jinja_utils.parse_string( '{{arg_name}}="{{arg_value}}"', { 'arg_name': arg_name, 'arg_value': json.dumps(param_value), } ) ) return '<%s %s></%s>' % (tag_name, ' '.join(attr_strings), tag_name)
def get_stats_log_html(self, state_customization_args, answer): """Gets the HTML for recording a learner's response in the stats log. Returns an HTML string. """ customization_args = { ca_spec.name: ( state_customization_args[ca_spec.name]['value'] if ca_spec.name in state_customization_args else ca_spec.default_value ) for ca_spec in self.customization_arg_specs } customization_args['answer'] = answer return jinja_utils.parse_string( self._stats_log_template, customization_args, autoescape=False)
def get_reader_response_html(self, state_customization_args, context_params, answer, sticky): """Gets the parameterized HTML and iframes for a reader response.""" if not self.is_interactive: raise Exception( 'This method should only be called for interactive widgets.') parameters = self._get_widget_param_instances(state_customization_args, context_params) parameters['answer'] = answer # The widget stays through the state transition because it is marked # sticky in the exploration and the new state uses the same widget. parameters['stateSticky'] = sticky return jinja_utils.parse_string(self._response_template, parameters)
def get_reader_response_html(self, state_customization_args, context_params, answer, sticky): """Gets the parameterized HTML and iframes for a reader response.""" if not self.is_interactive: raise Exception( 'This method should only be called for interactive widgets.') parameters = self._get_widget_param_instances( state_customization_args, context_params) parameters['answer'] = answer # The widget stays through the state transition because it is marked # sticky in the exploration and the new state uses the same widget. parameters['stateSticky'] = sticky return jinja_utils.parse_string(self._response_template, parameters)
def get_stats_log_html(self, state_customization_args, context_params, answer): """Gets the HTML for recording a reader response for the stats log. Returns an HTML string. """ if not self.is_interactive: raise Exception( 'This method should only be called for interactive widgets.') parameters = self._get_widget_param_instances(state_customization_args, context_params) parameters['answer'] = answer return jinja_utils.parse_string(self._stats_log_template, parameters, autoescape=False)
def get_interactive_widget_tag(self, state_customization_args, context_params, preview_mode=False): """Gets the widgetParams attribute value for an interactive widget.""" if state_customization_args is None: state_customization_args = {} tag_name = ('oppia-interactive-%s' % utils.camelcase_to_hyphenated(self.id)) attr_strings = [] for param in self.params: value_generator = param.generator(**param.init_args) # Use the given customization args. If they do not exist, use the # default customization args for the parameter. args_to_use = (state_customization_args[param.name] if param.name in state_customization_args else param.customization_args) try: generated_value = value_generator.generate_value( context_params, **args_to_use) except Exception: if preview_mode: generated_value = value_generator.default_value else: raise # Normalize the generated values to the correct obj_type. param_value = (obj_services.Registry.get_object_class_by_type( param.obj_type).normalize(generated_value)) prefix = '%s-with-' % utils.camelcase_to_hyphenated(param.name) for arg in param.customization_args: arg_name = '%s%s' % (prefix, utils.camelcase_to_hyphenated(arg)) attr_strings.append( jinja_utils.parse_string( '{{arg_name}}="{{arg_value}}"', { 'arg_name': arg_name, 'arg_value': json.dumps(param_value), })) return '<%s %s></%s>' % (tag_name, ' '.join(attr_strings), tag_name)
def evaluate_rule(rule_spec, answer_type, context_params, answer, fs): """Evaluates a rule spec. Returns a float between 0.0 and 1.0.""" all_rule_classes = get_rules_for_obj_type(answer_type) rule = next(r for r in all_rule_classes if r.__name__ == rule_spec.rule_type) param_list = [] param_defns = get_param_list(rule.description) for (param_name, obj_cls) in param_defns: parsed_param = rule_spec.inputs[param_name] if (isinstance(parsed_param, basestring) and '{{' in parsed_param): parsed_param = jinja_utils.parse_string( parsed_param, context_params, autoescape=False) normalized_param = obj_cls.normalize(parsed_param) param_list.append(normalized_param) constructed_rule = rule(*param_list) constructed_rule.set_fs(fs) return constructed_rule.eval(answer)
def submit_answer_in_tests( exploration_id, state_name, answer, params, handler_name, version): """This function should only be used by tests.""" params['answer'] = answer exploration = exp_services.get_exploration_by_id( exploration_id, version=version) exp_param_specs = exploration.param_specs old_state = exploration.states[state_name] rule_spec = classify( exploration_id, exp_param_specs, old_state, handler_name, answer, params) old_interaction_instance = ( interaction_registry.Registry.get_interaction_by_id( old_state.interaction.id)) normalized_answer = old_interaction_instance.normalize_answer( answer, handler_name) # TODO(sll): Should this also depend on `params`? event_services.AnswerSubmissionEventHandler.record( exploration_id, version, state_name, handler_name, rule_spec, old_interaction_instance.get_stats_log_html( old_state.interaction.customization_args, normalized_answer)) new_state = ( None if rule_spec.dest == feconf.END_DEST else exploration.states[rule_spec.dest]) finished = (rule_spec.dest == feconf.END_DEST) new_params = _get_updated_param_dict( params, {} if finished else new_state.param_changes, exp_param_specs) return { 'feedback_html': jinja_utils.parse_string( rule_spec.get_feedback_string(), params), 'finished': finished, 'params': new_params, 'question_html': ( new_state.content[0].to_html(new_params) if not finished else ''), 'state_name': rule_spec.dest if not finished else None, }
def post(self, exploration_id, escaped_state_name): """Handles feedback interactions with readers.""" old_state_name = self.unescape_state_name(escaped_state_name) # The reader's answer. answer = self.payload.get('answer') # The answer handler (submit, click, etc.) handler = self.payload.get('handler') # Parameters associated with the reader. old_params = self.payload.get('params', {}) old_params['answer'] = answer # The reader's state history. state_history = self.payload['state_history'] # The version of the exploration. version = self.payload.get('version') values = {} exploration = exp_services.get_exploration_by_id( exploration_id, version=version) old_state = exploration.states[old_state_name] old_widget = widget_registry.Registry.get_widget_by_id( feconf.INTERACTIVE_PREFIX, old_state.widget.widget_id) answer = old_widget.normalize_answer(answer, handler) rule = exploration.classify( old_state_name, handler, answer, old_params) feedback = rule.get_feedback_string() new_state_name = rule.dest new_state = ( None if new_state_name == feconf.END_DEST else exploration.states[new_state_name]) stats_services.EventHandler.record_state_hit( exploration_id, new_state_name, (new_state_name not in state_history)) state_history.append(new_state_name) # If the new state widget is the same as the old state widget, and the # new state widget is sticky, do not render the reader response. The # interactive widget in the frontend should take care of this. # TODO(sll): This special-casing is not great; we should # make the interface for updating the frontend more generic so that # all the updates happen in the same place. Perhaps in the non-sticky # case we should call a frontend method named appendFeedback() or # similar. sticky = ( new_state_name != feconf.END_DEST and new_state.widget.sticky and new_state.widget.widget_id == old_state.widget.widget_id ) self._append_answer_to_stats_log( old_state, answer, exploration_id, old_state_name, old_params, handler, rule) # Append the reader's answer to the response HTML. reader_response_html = old_widget.get_reader_response_html( old_state.widget.customization_args, old_params, answer, sticky) values['reader_response_html'] = reader_response_html # Add Oppia's feedback to the response HTML. feedback_html = '<div>%s</div>' % jinja_utils.parse_string( feedback, old_params) # Add the content for the new state to the response HTML. finished = (new_state_name == feconf.END_DEST) state_has_changed = (old_state_name != new_state_name) new_params, question_html, interactive_html = ( self._append_content( exploration, sticky, finished, old_params, new_state, new_state_name, state_has_changed)) values.update({ 'interactive_html': interactive_html, 'exploration_id': exploration_id, 'state_name': new_state_name, 'feedback_html': feedback_html, 'question_html': question_html, 'params': new_params, 'finished': finished, 'state_history': state_history, }) self.render_json(values)
def post(self, exploration_id, escaped_state_name): """Handles feedback interactions with readers.""" old_state_name = self.unescape_state_name(escaped_state_name) # The reader's answer. answer = self.payload.get('answer') # The answer handler (submit, click, etc.) handler = self.payload.get('handler') # Parameters associated with the reader. old_params = self.payload.get('params', {}) old_params['answer'] = answer # The reader's state history. state_history = self.payload['state_history'] # The version of the exploration. version = self.payload.get('version') values = {} exploration = exp_services.get_exploration_by_id( exploration_id, version=version) old_state = exploration.states[old_state_name] old_widget = widget_registry.Registry.get_widget_by_id( feconf.INTERACTIVE_PREFIX, old_state.widget.widget_id) answer = old_widget.normalize_answer(answer, handler) rule = exploration.classify( old_state_name, handler, answer, old_params) feedback = rule.get_feedback_string() new_state_name = rule.dest new_state = ( None if new_state_name == feconf.END_DEST else exploration.states[new_state_name]) stats_services.EventHandler.record_state_hit( exploration_id, new_state_name, (new_state_name not in state_history)) state_history.append(new_state_name) # If the new state widget is the same as the old state widget, and the # new state widget is sticky, do not render the reader response. The # interactive widget in the frontend should take care of this. # TODO(sll): This special-casing is not great; we should # make the interface for updating the frontend more generic so that # all the updates happen in the same place. Perhaps in the non-sticky # case we should call a frontend method named appendFeedback() or # similar. sticky = ( new_state_name != feconf.END_DEST and new_state.widget.sticky and new_state.widget.widget_id == old_state.widget.widget_id ) self._append_answer_to_stats_log( old_state, answer, exploration_id, exploration.version, old_state_name, old_params, handler, rule) # Append the reader's answer to the response HTML. reader_response_html = old_widget.get_reader_response_html( old_state.widget.customization_args, old_params, answer, sticky) values['reader_response_html'] = reader_response_html # Add Oppia's feedback to the response HTML. feedback_html = '<div>%s</div>' % jinja_utils.parse_string( feedback, old_params) # Add the content for the new state to the response HTML. finished = (new_state_name == feconf.END_DEST) state_has_changed = (old_state_name != new_state_name) new_params, question_html, interactive_html = ( self._append_content( exploration, sticky, finished, old_params, new_state, new_state_name, state_has_changed)) values.update({ 'interactive_html': interactive_html, 'exploration_id': exploration_id, 'state_name': new_state_name, 'feedback_html': feedback_html, 'question_html': question_html, 'params': new_params, 'finished': finished, 'state_history': state_history, }) self.render_json(values)
def evaluate_rule(definition, param_specs, answer_type, context_params, answer, fs): """Evaluates a rule definition using context_params. Returns a boolean.""" if 'rule_type' not in definition: raise Exception('No rule type specified when constructing rule.') elif definition['rule_type'] == DEFAULT_RULE_TYPE: return True elif definition['rule_type'] == ATOMIC_RULE_TYPE: subject_name = definition['subject'] if subject_name == 'answer': subject_type = answer_type else: subject_type = param_specs[subject_name].obj_type all_rule_classes = get_rules_for_obj_type(subject_type) rule = next(r for r in all_rule_classes if r.__name__ == definition['name']) param_list = [] param_defns = get_param_list(rule.description) for (param_name, obj_cls) in param_defns: parsed_param = definition['inputs'][param_name] if (isinstance(parsed_param, basestring) and '{{' in parsed_param): parsed_param = jinja_utils.parse_string(parsed_param, context_params, autoescape=False) normalized_param = obj_cls.normalize(parsed_param) param_list.append(normalized_param) if subject_name == 'answer': subject = answer else: subject = context_params[subject_name] constructed_rule = rule(*param_list) constructed_rule.set_fs(fs) return constructed_rule.eval(subject) elif definition['rule_type'] == AND_RULE_TYPE: for child_dict in definition['children']: if not evaluate_rule(child_dict, param_specs, answer_type, context_params, answer): return False return True elif definition['rule_type'] == OR_RULE_TYPE: for child_dict in definition['children']: if evaluate_rule(child_dict, param_specs, answer_type, context_params, answer): return True return False elif definition['rule_type'] == NOT_RULE_TYPE: return (not evaluate_rule(definition['child'], param_specs, answer_type, context_params, answer)) else: raise Exception('Unrecognized rule type %s' % definition['rule_type'])
def export_content_to_html(content_array, block_number, params=None, escape_text_strings=True): """Takes a Content array and transforms it into HTML. Args: content_array: an array, each of whose members is of type Content. This object has two keys: type and value. The 'type' is one of the following: - 'text'; then the value is a text string - 'image'; then the value is an image ID - 'video'; then the value is a video ID - 'widget'; then the value is a JSON-encoded dict with keys 'id' and 'params', from which the raw widget HTML can be constructed block_number: the number of content blocks preceding this one. params: any parameters used for templatizing text strings. escape_text_strings: True if values supplied with content of type 'text' should be escaped after Jinja evaluation; False otherwise. Returns: the HTML string representing the array. Raises: InvalidInputException: if content has no 'type' attribute, or an invalid 'type' attribute. """ if params is None: params = {} JINJA_ENV = jinja_utils.get_jinja_env(feconf.FRONTEND_TEMPLATES_DIR) html, widget_array = '', [] for content in content_array: if content.type in ['text', 'image', 'video']: value = (jinja_utils.parse_string(content.value, params) if content.type == 'text' else content.value) if escape_text_strings: value = cgi.escape(value) html += JINJA_ENV.get_template('reader/content.html').render({ 'type': content.type, 'value': value, }) elif content.type == 'widget': # Ignore empty widget specifications. if not content.value: continue widget_dict = json.loads(content.value) widget = widget_domain.Registry.get_widget_by_id( feconf.NONINTERACTIVE_PREFIX, widget_dict['id']) widget_array_len = len(widget_array) html += JINJA_ENV.get_template('reader/content.html').render({ 'blockIndex': block_number, 'index': widget_array_len, 'type': content.type, }) widget_array.append({ 'blockIndex': block_number, 'index': widget_array_len, 'raw': widget.get_raw_code( widget_dict['customization_args'], params), }) else: raise utils.InvalidInputException( 'Invalid content type %s', content.type) return html, widget_array