Example #1
0
    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
Example #2
0
    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
Example #3
0
    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()
Example #4
0
 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')
Example #5
0
    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)
Example #6
0
    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
Example #7
0
    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)
Example #8
0
    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)
Example #9
0
    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])
Example #10
0
 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)
Example #11
0
    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)))
Example #12
0
    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)