def create_from_yaml(cls, yaml_file, user, title, category, exploration_id=None, image_id=None): """Creates an exploration from a YAML file.""" init_state_name = yaml_file[: yaml_file.index(":\n")] if not init_state_name or "\n" in init_state_name: raise Exception( "Invalid YAML file: the name of the initial state " "should be left-aligned on the first line and " "followed by a colon" ) exploration = cls.create( user, title, category, exploration_id=exploration_id, init_state_name=init_state_name, image_id=image_id ) init_state = State.get_by_name(init_state_name, exploration) try: exploration_dict = utils.dict_from_yaml(yaml_file) state_list = [] for state_name, state_description in exploration_dict.iteritems(): state = init_state if state_name == init_state_name else exploration.add_state(state_name) state_list.append({"state": state, "desc": state_description}) for index, state in enumerate(state_list): State.modify_using_dict(exploration, state["state"], state["desc"]) except Exception: exploration.delete() raise return exploration
def create(cls, user, title, category, exploration_id=None, init_state_name="Activity 1", image_id=None): """Creates and returns a new exploration.""" # Generate a new exploration id, if one wasn't passed in. exploration_id = exploration_id or cls.get_new_id(title) # Temporarily create a fake initial state key. state_id = State.get_new_id(init_state_name) fake_state_key = ndb.Key(Exploration, exploration_id, State, state_id) # Note that demo explorations do not have owners, so user may be None. exploration = cls( id=exploration_id, title=title, init_state=fake_state_key, category=category, image_id=image_id, states=[fake_state_key], editors=[user] if user else [], ) exploration.put() # Finally, create the initial state and check that it has the right key. new_init_state = State.create(exploration, init_state_name, state_id=state_id) assert fake_state_key == new_init_state.key return exploration
def test_state_class(self): """Test State Class.""" state = State(id="The exploration hash id") # A new state should have a default name property. self.assertEqual(state.name, feconf.DEFAULT_STATE_NAME) # A state that is put into the datastore must have a parent # exploration. with self.assertRaises(Exception): state.put()
def testStateClass(self): """Test State Class.""" # TODO(sll): Need to test that this model's parent must be an # exploration. o = State(id='The hash id') o.name = 'The name' o.content = [Content(type='text', value='The content')] self.assertEqual(o.key.id(), 'The hash id') self.assertEqual(o.name, 'The name') self.assertEqual(len(o.content), 1) self.assertEqual(o.content[0].type, 'text') self.assertEqual(o.content[0].value, 'The content')
def test_editor(self, exploration_id, state_id=None, **kwargs): """Gets the user and exploration id if the user can edit it. Args: self: the handler instance exploration_id: the exploration id state_id: the state id, if it exists **kwargs: any other arguments passed to the handler Returns: The user and exploration instance, if the user is authorized to edit this exploration. Also, the state instance, if one is supplied. Raises: self.NotLoggedInException: if there is no current user. self.UnauthorizedUserException: if the user exists but does not have the right credentials. """ user = users.get_current_user() if not user: self.redirect(users.create_login_url(self.request.uri)) return exploration = Exploration.get(exploration_id) if not exploration.is_editable_by(user): raise self.UnauthorizedUserException( '%s does not have the credentials to edit this exploration.', user) if not state_id: return handler(self, user, exploration, **kwargs) state = State.get(state_id, exploration) return handler(self, user, exploration, state, **kwargs)
def add_state(self, state_name): """Adds a new state, and returns it.""" if self._has_state_named(state_name): raise Exception("Duplicate state name %s" % state_name) state = State.create(self, state_name) self.states.append(state.key) self.put() return state
def test_create_and_get_state(self): """Test creation and retrieval of states.""" id_1 = "123" name_1 = "State 1" state_1 = State.create(self.exploration, name_1, state_id=id_1) fetched_state_1 = State.get(id_1, parent=self.exploration.key) self.assertEqual(fetched_state_1, state_1) fetched_state_by_name_1 = State.get_by_name(name_1, self.exploration) self.assertEqual(fetched_state_by_name_1, state_1) # Test the failure cases. id_2 = "fake_id" name_2 = "fake_name" with self.assertRaises(Exception): State.get(id_2, parent=self.exploration.key) fetched_state_by_name_2 = State.get_by_name(name_2, self.exploration, strict=False) self.assertIsNone(fetched_state_by_name_2) with self.assertRaises(Exception): State.get_by_name(name_2, self.exploration, strict=True) # The default behavior is to fail noisily. with self.assertRaises(Exception): State.get_by_name(name_2, self.exploration)
def test_get_id_from_name(self): """Test converting state names to ids.""" id_1 = "123" name_1 = "State 1" State.create(self.exploration, name_1, state_id=id_1) self.assertEqual(State._get_id_from_name(name_1, self.exploration), id_1) with self.assertRaises(Exception): State._get_id_from_name("fake_name", self.exploration) self.assertEqual(State._get_id_from_name(feconf.END_DEST, self.exploration), feconf.END_DEST)
def test_exploration_class(self): """Test the Exploration class.""" exploration = Exploration(id='The exploration hash id') # A new exploration should have a default title property. self.assertEqual(exploration.title, 'New exploration') # A new exploration should have a default is_public property. self.assertEqual(exploration.is_public, False) # An Exploration must have properties 'category' and 'init_state' set. with self.assertRaises(BadValueError): exploration.put() exploration.category = 'The category' with self.assertRaises(BadValueError): exploration.put() # The 'init_state' property must be a valid State. with self.assertRaises(BadValueError): exploration.init_state = 'The State' state = State(id='The state hash id') state.put() exploration.init_state = state.key # The 'states' property must be a list. with self.assertRaises(BadValueError): exploration.states = 'A string' # TODO(emersoj): We should put the initial state in the states list. It # should not be empty exploration.states = [] # The 'states property must be a list of State keys. with self.assertRaises(BadValueError): exploration.states = ['A string'] with self.assertRaises(BadValueError): exploration.states = [state] exploration.states = [state.key] # The 'parameters' property must be a list of Parameter objects. with self.assertRaises(BadValueError): exploration.parameters = 'A string' exploration.parameters = [] parameter = Parameter(name='theParameter', obj_type='Int') with self.assertRaises(BadValueError): exploration.parameters = [parameter.key] exploration.parameters = [parameter] # The 'is_public' property must be a boolean. with self.assertRaises(BadValueError): exploration.is_public = 'true' exploration.is_public = True # The 'image_id' property must be a string. image = Image(id='The image') with self.assertRaises(BadValueError): exploration.image_id = image with self.assertRaises(BadValueError): exploration.image_id = image.key exploration.image_id = 'A string' # The 'editors' property must be a list of User objects. with self.assertRaises(BadValueError): exploration.editors = 'A string' with self.assertRaises(BadValueError): exploration.editors = ['A string'] exploration.editors = [] # There must be at least one editor. with self.assertRaises(BadValueError): exploration.put() exploration.editors = [self.user] # Put and Retrieve the exploration. exploration.put() retrieved_exploration = Exploration.get_by_id('The exploration hash id') self.assertEqual(retrieved_exploration.category, 'The category') self.assertEqual(retrieved_exploration.init_state, state.key) self.assertEqual(retrieved_exploration.title, 'New exploration') self.assertEqual(retrieved_exploration.states, [state.key]) self.assertEqual(retrieved_exploration.parameters, [parameter]) self.assertEqual(retrieved_exploration.is_public, True) self.assertEqual(retrieved_exploration.image_id, 'A string') self.assertEqual(retrieved_exploration.editors, [self.user])
def _has_state_named(self, state_name): """Checks if a state with the given name exists in this exploration.""" state = State.query(ancestor=self.key).filter(State.name == state_name).count(limit=1) return bool(state)
def put(self, unused_user, exploration, state): """Saves updates to a state.""" payload = json.loads(self.request.get('payload')) yaml_file = payload.get('yaml_file') if yaml_file and feconf.ALLOW_YAML_FILE_UPLOAD: # The user has uploaded a YAML file. Process only this action. state = State.modify_using_dict( exploration, state, utils.dict_from_yaml(yaml_file)) self.response.write(json.dumps( get_state_for_frontend(state, exploration))) return state_name = payload.get('state_name') param_changes = payload.get('param_changes') interactive_widget = payload.get('interactive_widget') interactive_params = payload.get('interactive_params') interactive_rulesets = payload.get('interactive_rulesets') sticky_interactive_widget = payload.get('sticky_interactive_widget') content = payload.get('content') unresolved_answers = payload.get('unresolved_answers') if 'state_name' in payload: # Replace the state name with this one, after checking validity. if state_name == feconf.END_DEST: raise self.InvalidInputException('Invalid state name: END') exploration.rename_state(state, state_name) if 'param_changes' in payload: state.param_changes = [ ParamChange( name=param_change['name'], values=param_change['values'], obj_type='UnicodeString' ) for param_change in param_changes ] if interactive_widget: state.widget.widget_id = interactive_widget if interactive_params: state.widget.params = interactive_params if sticky_interactive_widget is not None: state.widget.sticky = sticky_interactive_widget if interactive_rulesets: ruleset = interactive_rulesets['submit'] utils.recursively_remove_key(ruleset, u'$$hashKey') state.widget.handlers = [AnswerHandlerInstance( name='submit', rules=[])] # This is part of the state. The rules should be put into it. state_ruleset = state.widget.handlers[0].rules # TODO(yanamal): Do additional calculations here to get the # parameter changes, if necessary. for rule_ind in range(len(ruleset)): rule = ruleset[rule_ind] state_rule = Rule() state_rule.name = rule.get('name') state_rule.inputs = rule.get('inputs') state_rule.dest = rule.get('dest') state_rule.feedback = rule.get('feedback') # Generate the code to be executed. if rule['rule'] == 'Default': # This is the default rule. assert rule_ind == len(ruleset) - 1 state_rule.name = 'Default' state_ruleset.append(state_rule) continue # Normalize the params here, then store them. classifier_func = state_rule.name.replace(' ', '') first_bracket = classifier_func.find('(') mutable_rule = rule['rule'] params = classifier_func[first_bracket + 1: -1].split(',') for index, param in enumerate(params): if param not in rule['inputs']: raise self.InvalidInputException( 'Parameter %s could not be replaced.' % param) typed_object = state.get_typed_object(mutable_rule, param) # TODO(sll): Make the following check more robust. if (not isinstance(rule['inputs'][param], basestring) or '{{' not in rule['inputs'][param] or '}}' not in rule['inputs'][param]): normalized_param = typed_object.normalize( rule['inputs'][param]) else: normalized_param = rule['inputs'][param] if normalized_param is None: raise self.InvalidInputException( '%s has the wrong type. Please replace it with a ' '%s.' % (rule['inputs'][param], typed_object.__name__)) state_rule.inputs[param] = normalized_param state_ruleset.append(state_rule) if content: state.content = [Content(type=item['type'], value=item['value']) for item in content] if 'unresolved_answers' in payload: state.unresolved_answers = {} for answer, count in unresolved_answers.iteritems(): if count > 0: state.unresolved_answers[answer] = count state.put() self.response.write(json.dumps( get_state_for_frontend(state, exploration)))
def post(self, exploration_id, state_id): """Handles feedback interactions with readers.""" values = {'error': []} exploration = Exploration.get(exploration_id) state = State.get(state_id, parent=exploration.key) old_state = state payload = json.loads(self.request.get('payload')) # The 0-based index of the last content block already on the page. block_number = payload.get('block_number') + 1 # The reader's answer. answer = payload.get('answer') # The answer handler (submit, click, etc.) handler = payload.get('handler') params = payload.get('params', {}) # Add the reader's answer to the parameter list. params['answer'] = answer dest_id, feedback, rule, recorded_answer = state.transition( answer, params, handler) if recorded_answer is not None: recorded_answer = json.dumps(recorded_answer) EventHandler.record_rule_hit( exploration_id, state_id, rule, recorded_answer) # Add this answer to the state's 'unresolved answers' list. if recorded_answer not in old_state.unresolved_answers: old_state.unresolved_answers[recorded_answer] = 0 old_state.unresolved_answers[recorded_answer] += 1 # TODO(sll): Make this async? old_state.put() assert dest_id html_output, widget_output = '', [] # TODO(sll): The following is a special-case for multiple choice input, # in which the choice text must be displayed instead of the choice # number. We might need to find a way to do this more generically. if state.widget.widget_id == 'interactive-MultipleChoiceInput': answer = state.widget.params['choices'][int(answer)] # Append reader's answer. values['reader_html'] = feconf.JINJA_ENV.get_template( 'reader/reader_response.html').render({'response': answer}) if dest_id == feconf.END_DEST: # This leads to a FINISHED state. if feedback: html_output, widget_output = self._append_feedback( feedback, html_output, widget_output, block_number, params) EventHandler.record_exploration_completed(exploration_id) else: state = State.get(dest_id, parent=exploration.key) EventHandler.record_state_hit(exploration_id, dest_id) if feedback: html_output, widget_output = self._append_feedback( feedback, html_output, widget_output, block_number, params) # Populate new parameters. params = get_params(state, existing_params=params) # Append text for the new state only if the new and old states # differ. if old_state.id != state.id: state_html, state_widgets = parse_content_into_html( state.content, block_number, params) # Separate text for the new state and feedback for the old state # by an additional line. if state_html and feedback: html_output += '<br>' html_output += state_html widget_output += state_widgets if state.widget.widget_id in DEFAULT_ANSWERS: values['default_answer'] = DEFAULT_ANSWERS[state.widget.widget_id] values.update({ 'exploration_id': exploration.id, 'state_id': state.id, 'oppia_html': html_output, 'widgets': widget_output, 'block_number': block_number, 'params': params, 'finished': (dest_id == feconf.END_DEST), }) if dest_id != feconf.END_DEST: if state.widget.sticky and ( state.widget.widget_id == old_state.widget.widget_id): values['interactive_widget_html'] = '' values['sticky_interactive_widget'] = True else: values['interactive_widget_html'] = ( InteractiveWidget.get_raw_code( state.widget.widget_id, params=utils.parse_dict_with_params( state.widget.params, params) ) ) else: values['interactive_widget_html'] = '' self.render_json(values)