示例#1
0
    def update_platform_parameter(
            cls, name, committer_id, commit_message, new_rule_dicts):
        """Updates the platform parameter with new rules.

        Args:
            name: str. The name of the platform parameter to update.
            committer_id: str. ID of the committer.
            commit_message: str. The commit message.
            new_rule_dicts: list(dist). A list of dict mappings of all fields
                of PlatformParameterRule object.
        """
        param = cls.get_platform_parameter(name)

        # Create a temporary param instance with new rules for validation,
        # if the new rules are invalid, an exception will be raised in
        # validate() method.
        param_dict = param.to_dict()
        param_dict['rules'] = new_rule_dicts
        updated_param = param.from_dict(param_dict)
        updated_param.validate()

        model_instance = cls._to_platform_parameter_model(param)

        new_rules = [
            platform_parameter_domain.PlatformParameterRule.from_dict(rule_dict)
            for rule_dict in new_rule_dicts]
        param.set_rules(new_rules)

        model_instance.rules = [rule.to_dict() for rule in param.rules]
        model_instance.commit(
            committer_id,
            commit_message,
            [{
                'cmd': (
                    platform_parameter_domain
                    .PlatformParameterChange.CMD_EDIT_RULES),
                'new_rules': new_rule_dicts
            }]
        )

        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_PLATFORM_PARAMETER, None, [name])
示例#2
0
def delete_story(committer_id, story_id, force_deletion=False):
    """Deletes the story with the given story_id.

    Args:
        committer_id: str. ID of the committer.
        story_id: str. ID of the story to be deleted.
        force_deletion: bool. If true, the story and its history are fully
            deleted and are unrecoverable. Otherwise, the story and all
            its history are marked as deleted, but the corresponding models are
            still retained in the datastore. This last option is the preferred
            one.
    """

    story_model = story_models.StoryModel.get(story_id, strict=False)
    if story_model is not None:
        story = story_fetchers.get_story_from_model(story_model)
        exp_ids = story.story_contents.get_all_linked_exp_ids()
        story_model.delete(committer_id,
                           feconf.COMMIT_MESSAGE_STORY_DELETED,
                           force_deletion=force_deletion)
        # Reject the suggestions related to the exploration used in
        # the story.
        suggestion_services.auto_reject_translation_suggestions_for_exp_ids(
            exp_ids)

    exploration_context_models = (
        exp_models.ExplorationContextModel.get_all().filter(
            exp_models.ExplorationContextModel.story_id == story_id).fetch())
    exp_models.ExplorationContextModel.delete_multi(exploration_context_models)

    # This must come after the story is retrieved. Otherwise the memcache
    # key will be reinstated.
    caching_services.delete_multi(caching_services.CACHE_NAMESPACE_STORY, None,
                                  [story_id])

    # Delete the summary of the story (regardless of whether
    # force_deletion is True or not).
    delete_story_summary(story_id)

    # Delete the opportunities available.
    opportunity_services.delete_exp_opportunities_corresponding_to_story(
        story_id)
示例#3
0
    def test_delete_multi_returns_true_when_all_ids_exist(self):
        key_value_mapping = {'a': '1', 'b': '2', 'c': '3'}

        self.assertFalse(
            caching_services.delete_multi(
                caching_services.CACHE_NAMESPACE_DEFAULT, None,
                ['a', 'b', 'c']))

        caching_services.set_multi(caching_services.CACHE_NAMESPACE_DEFAULT,
                                   None, key_value_mapping)

        exploration_id = 'id'
        default_exploration = (
            exp_domain.Exploration.create_default_exploration(
                'exp_id_1', title='A title', category='A category'))
        caching_services.set_multi(
            caching_services.CACHE_NAMESPACE_EXPLORATION, '0',
            {exploration_id: default_exploration})

        self.assertTrue(
            caching_services.delete_multi(
                caching_services.CACHE_NAMESPACE_DEFAULT, None, []))

        self.assertTrue(
            caching_services.delete_multi(
                caching_services.CACHE_NAMESPACE_DEFAULT, None,
                ['a', 'b', 'c']))

        self.assertGreater(
            caching_services.get_multi(
                caching_services.CACHE_NAMESPACE_EXPLORATION, '0',
                [exploration_id]), 0)

        self.assertTrue(
            caching_services.delete_multi(
                caching_services.CACHE_NAMESPACE_EXPLORATION, '0',
                [exploration_id]))

        self.assertEqual(
            caching_services.get_multi(
                caching_services.CACHE_NAMESPACE_EXPLORATION, '0',
                [exploration_id]), {})
    def test_delete_multi_returns_false_when_sub_namespace_incorrect(
            self) -> None:
        key_value_mapping = {'a': '1', 'b': '2', 'c': '3'}

        caching_services.set_multi(caching_services.CACHE_NAMESPACE_DEFAULT,
                                   None, key_value_mapping)

        self.assertFalse(
            caching_services.delete_multi(
                caching_services.CACHE_NAMESPACE_DEFAULT,
                'invalid_sub_namespace', ['a', 'b', 'c']))
    def test_delete_multi_returns_false_when_not_all_ids_exist(self) -> None:
        """Tests that deleting keys that don't exist returns False."""
        key_value_mapping = {'a': '1', 'b': '2', 'c': '3'}

        caching_services.set_multi(caching_services.CACHE_NAMESPACE_DEFAULT,
                                   None, key_value_mapping)

        self.assertFalse(
            caching_services.delete_multi(
                caching_services.CACHE_NAMESPACE_DEFAULT, None,
                ['a', 'e', 'f']))
示例#6
0
    def setUp(self):
        super(PlatformFeaturesEvaluationHandlerTest, self).setUp()

        self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME)
        self.user_id = self.get_user_id_from_email(self.OWNER_EMAIL)

        self.original_registry = registry.Registry.parameter_registry
        self.original_feature_list = feature_services.ALL_FEATURES_LIST
        self.original_feature_name_set = feature_services.ALL_FEATURES_NAMES_SET

        param_names = ['parameter_a', 'parameter_b']
        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_PLATFORM_PARAMETER, None,
            param_names)

        registry.Registry.parameter_registry.clear()
        self.dev_feature = registry.Registry.create_platform_parameter(
            'parameter_a',
            'parameter for test',
            'bool',
            is_feature=True,
            feature_stage=param_domain.FEATURE_STAGES.dev)
        self.prod_feature = registry.Registry.create_platform_parameter(
            'parameter_b',
            'parameter for test',
            'bool',
            is_feature=True,
            feature_stage=param_domain.FEATURE_STAGES.prod)
        registry.Registry.update_platform_parameter(
            self.prod_feature.name, self.user_id, 'edit rules', [{
                'filters': [{
                    'type':
                    'server_mode',
                    'conditions': [['=', param_domain.SERVER_MODES.dev]]
                }],
                'value_when_matched':
                True
            }])

        feature_services.ALL_FEATURES_LIST = param_names
        feature_services.ALL_FEATURES_NAMES_SET = set(param_names)
示例#7
0
    def test_regeneration_job_with_no_exp_model_for_some_topics(self):
        exp_models.ExplorationModel.get('0').delete(self.owner_id,
                                                    'Delete exploration',
                                                    force_deletion=True)
        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_EXPLORATION, None, ['0'])

        exp_opp_summary_model_regen_job_class = (
            opportunity_jobs_one_off.
            ExplorationOpportunitySummaryModelRegenerationJob)

        all_opportunity_models = list(
            opportunity_models.ExplorationOpportunitySummaryModel.get_all())

        self.assertEqual(len(all_opportunity_models), 2)

        job_id = exp_opp_summary_model_regen_job_class.create_new()
        exp_opp_summary_model_regen_job_class.enqueue(job_id)

        self.assertEqual(
            self.count_jobs_in_mapreduce_taskqueue(
                taskqueue_services.QUEUE_NAME_ONE_OFF_JOBS), 1)
        self.process_and_flush_pending_mapreduce_tasks()

        output = exp_opp_summary_model_regen_job_class.get_output(job_id)
        expected = [[
            'FAILED (1)',
            [
                'Failed to regenerate opportunities for topic id: topic1, '
                'missing_exp_with_ids: [u\'0\'], '
                'missing_story_with_ids: []'
            ]
        ], ['SUCCESS', 1]]

        self.assertEqual(expected, [ast.literal_eval(x) for x in output])

        all_opportunity_models = list(
            opportunity_models.ExplorationOpportunitySummaryModel.get_all())
        self.assertEqual(len(all_opportunity_models), 1)
示例#8
0
    def test_migration_job_detects_invalid_exploration(self):
        exploration = exp_domain.Exploration.create_default_exploration(
            self.VALID_EXP_ID, title='title', category='category')
        exp_services.save_new_exploration(self.albert_id, exploration)

        exploration_model = exp_models.ExplorationModel.get(self.VALID_EXP_ID)
        exploration_model.language_code = 'invalid_language_code'
        exploration_model.commit(self.albert_id, 'Changed language_code.', [])
        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_EXPLORATION, None,
            [self.VALID_EXP_ID])

        job_id = exp_jobs_one_off.ExpSnapshotsMigrationJob.create_new()
        exp_jobs_one_off.ExpSnapshotsMigrationJob.enqueue(job_id)
        self.process_and_flush_pending_mapreduce_tasks()

        actual_output = (
            exp_jobs_one_off.ExpSnapshotsMigrationJob.get_output(job_id))
        expected_output_message = (
            '[u\'INFO - Exploration %s-1 failed non-strict validation\', '
            '[u\'Invalid language_code: invalid_language_code\']]' %
            self.VALID_EXP_ID)
        self.assertIn(expected_output_message, actual_output)
示例#9
0
def delete_collections(committer_id, collection_ids, force_deletion=False):
    """Deletes the collections with the given collection_ids.

    IMPORTANT: Callers of this function should ensure that committer_id has
    permissions to delete this collection, prior to calling this function.

    Args:
        committer_id: str. ID of the committer.
        collection_ids: list(str). IDs of the collections to be deleted.
        force_deletion: bool. If true, the collections and its histories are
            fully deleted and are unrecoverable. Otherwise, the collections and
            all its histories are marked as deleted, but the corresponding
            models are still retained in the datastore.
    """
    collection_models.CollectionRightsModel.delete_multi(
        collection_ids, committer_id, '', force_deletion=force_deletion)
    collection_models.CollectionModel.delete_multi(
        collection_ids,
        committer_id,
        feconf.COMMIT_MESSAGE_EXPLORATION_DELETED,
        force_deletion=force_deletion)

    # This must come after the collection is retrieved. Otherwise the memcache
    # key will be reinstated.
    caching_services.delete_multi(caching_services.CACHE_NAMESPACE_COLLECTION,
                                  None, collection_ids)

    # Delete the collection from search.
    search_services.delete_collections_from_search_index(collection_ids)

    # Delete the summary of the collection (regardless of whether
    # force_deletion is True or not).
    delete_collection_summaries(collection_ids)

    # Remove the collection from the featured activity list, if necessary.
    activity_services.remove_featured_activities(
        constants.ACTIVITY_TYPE_COLLECTION, collection_ids)
示例#10
0
def _save_topic(committer_id, topic, commit_message, change_list):
    """Validates a topic and commits it to persistent storage. If
    successful, increments the version number of the incoming topic domain
    object by 1.

    Args:
        committer_id: str. ID of the given committer.
        topic: Topic. The topic domain object to be saved.
        commit_message: str. The commit message.
        change_list: list(TopicChange). List of changes applied to a topic.

    Raises:
        Exception. Received invalid change list.
        Exception. The topic model and the incoming topic domain
            object have different version numbers.
    """
    if not change_list:
        raise Exception(
            'Unexpected error: received an invalid change list when trying to '
            'save topic %s: %s' % (topic.id, change_list))
    topic_rights = topic_fetchers.get_topic_rights(topic.id, strict=False)
    topic.validate(strict=topic_rights.topic_is_published)

    topic_model = topic_models.TopicModel.get(topic.id, strict=False)

    # Topic model cannot be None as topic is passed as parameter here and that
    # is only possible if a topic model with that topic id exists. Also this is
    # a private function and so it cannot be called independently with any
    # topic object.
    if topic.version > topic_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of topic '
            'from version %s. Please reload the page and try again.' %
            (topic_model.version, topic.version))
    elif topic.version < topic_model.version:
        raise Exception(
            'Trying to update version %s of topic from version %s, '
            'which is too old. Please reload the page and try again.' %
            (topic_model.version, topic.version))

    topic_model.description = topic.description
    topic_model.name = topic.name
    topic_model.canonical_name = topic.canonical_name
    topic_model.abbreviated_name = topic.abbreviated_name
    topic_model.url_fragment = topic.url_fragment
    topic_model.thumbnail_bg_color = topic.thumbnail_bg_color
    topic_model.thumbnail_filename = topic.thumbnail_filename
    topic_model.canonical_story_references = [
        reference.to_dict() for reference in topic.canonical_story_references
    ]
    topic_model.additional_story_references = [
        reference.to_dict() for reference in topic.additional_story_references
    ]
    topic_model.uncategorized_skill_ids = topic.uncategorized_skill_ids
    topic_model.subtopics = [
        subtopic.to_dict() for subtopic in topic.subtopics
    ]
    topic_model.subtopic_schema_version = topic.subtopic_schema_version
    topic_model.story_reference_schema_version = (
        topic.story_reference_schema_version)
    topic_model.next_subtopic_id = topic.next_subtopic_id
    topic_model.language_code = topic.language_code
    topic_model.meta_tag_content = topic.meta_tag_content
    topic_model.practice_tab_is_displayed = topic.practice_tab_is_displayed
    topic_model.page_title_fragment_for_web = topic.page_title_fragment_for_web
    change_dicts = [change.to_dict() for change in change_list]
    topic_model.commit(committer_id, commit_message, change_dicts)
    caching_services.delete_multi(caching_services.CACHE_NAMESPACE_TOPIC, None,
                                  [topic.id])
    topic.version += 1
    def setUp(self) -> None:
        super(PlatformFeatureServiceTest, self).setUp()

        self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME)
        self.user_id = self.get_user_id_from_email(self.OWNER_EMAIL)  # type: ignore[no-untyped-call]

        registry.Registry.parameter_registry.clear()
        # Parameter names that might be used in following tests.
        param_names = ['feature_a', 'feature_b']
        param_name_enums = [ParamNames.FEATURE_A, ParamNames.FEATURE_B]
        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_PLATFORM_PARAMETER, None,
            param_names)

        self.dev_feature = registry.Registry.create_feature_flag(
            ParamNames.FEATURE_A, 'a feature in dev stage',
            FeatureStages.DEV)
        self.prod_feature = registry.Registry.create_feature_flag(
            ParamNames.FEATURE_B, 'a feature in prod stage',
            FeatureStages.PROD)
        registry.Registry.update_platform_parameter(
            self.dev_feature.name, self.user_id, 'edit rules',
            [
                platform_parameter_domain.PlatformParameterRule.from_dict({
                    'filters': [
                        {
                            'type': 'server_mode',
                            'conditions': [
                                ['=', ServerMode.DEV.value]
                            ]
                        }
                    ],
                    'value_when_matched': True
                })
            ]
        )

        registry.Registry.update_platform_parameter(
            self.prod_feature.name, self.user_id, 'edit rules',
            [
                platform_parameter_domain.PlatformParameterRule.from_dict({
                    'filters': [
                        {
                            'type': 'server_mode',
                            'conditions': [
                                ['=', ServerMode.DEV.value],
                                ['=', ServerMode.TEST.value],
                                ['=', ServerMode.PROD.value]
                            ]
                        }
                    ],
                    'value_when_matched': True
                })
            ]
        )

        # Replace feature lists with mocked names.
        self.original_feature_list = feature_services.ALL_FEATURES_LIST
        self.original_feature_name_set = (
            feature_services.ALL_FEATURES_NAMES_SET
        )
        # The expected type of 'ALL_FEATURES_LIST' is a list of 'PARAM_NAMES'
        # Enum, but here for testing purposes we are providing a list of
        # 'ParamNames' enums, which causes MyPy to throw an 'Incompatible types
        # in assignment' error. Thus to avoid the error, we used ignore here.
        feature_services.ALL_FEATURES_LIST = param_name_enums  # type: ignore[assignment]
        feature_services.ALL_FEATURES_NAMES_SET = set(param_names)
示例#12
0
def _save_story(
        committer_id, story, commit_message, change_list, story_is_published):
    """Validates a story and commits it to persistent storage. If
    successful, increments the version number of the incoming story domain
    object by 1.

    Args:
        committer_id: str. ID of the given committer.
        story: Story. The story domain object to be saved.
        commit_message: str. The commit message.
        change_list: list(StoryChange). List of changes applied to a story.
        story_is_published: bool. Whether the supplied story is published.

    Raises:
        ValidationError. An invalid exploration was referenced in the
            story.
        Exception. The story model and the incoming story domain
            object have different version numbers.
    """
    if not change_list:
        raise Exception(
            'Unexpected error: received an invalid change list when trying to '
            'save story %s: %s' % (story.id, change_list))

    story.validate()
    validate_prerequisite_skills_in_story_contents(
        story.corresponding_topic_id, story.story_contents)

    if story_is_published:
        exp_ids = []
        for node in story.story_contents.nodes:
            if not node.exploration_id:
                raise Exception(
                    'Story node with id %s does not contain an '
                    'exploration id.' % node.id)
            exp_ids.append(node.exploration_id)

        validate_explorations_for_story(exp_ids, True)

    # Story model cannot be None as story is passed as parameter here and that
    # is only possible if a story model with that story id exists. Also this is
    # a private function and so it cannot be called independently with any
    # story object.
    story_model = story_models.StoryModel.get(story.id)
    if story.version > story_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of story '
            'from version %s. Please reload the page and try again.'
            % (story_model.version, story.version))
    elif story.version < story_model.version:
        raise Exception(
            'Trying to update version %s of story from version %s, '
            'which is too old. Please reload the page and try again.'
            % (story_model.version, story.version))

    story_model.description = story.description
    story_model.title = story.title
    story_model.thumbnail_bg_color = story.thumbnail_bg_color
    story_model.thumbnail_filename = story.thumbnail_filename
    story_model.notes = story.notes
    story_model.language_code = story.language_code
    story_model.story_contents_schema_version = (
        story.story_contents_schema_version)
    story_model.story_contents = story.story_contents.to_dict()
    story_model.corresponding_topic_id = story.corresponding_topic_id
    story_model.version = story.version
    story_model.url_fragment = story.url_fragment
    story_model.meta_tag_content = story.meta_tag_content
    change_dicts = [change.to_dict() for change in change_list]
    story_model.commit(committer_id, commit_message, change_dicts)
    caching_services.delete_multi(
        caching_services.CACHE_NAMESPACE_STORY, None, [story.id])
    story.version += 1
示例#13
0
def _save_collection(committer_id, collection, commit_message, change_list):
    """Validates a collection and commits it to persistent storage. If
    successful, increments the version number of the incoming collection domain
    object by 1.

    Args:
        committer_id: str. ID of the given committer.
        collection: Collection. The collection domain object to be saved.
        commit_message: str. The commit message.
        change_list: list(dict). List of changes applied to a collection. Each
            entry in change_list is a dict that represents a CollectionChange.

    Raises:
        ValidationError. An invalid exploration was referenced in the
            collection.
        Exception. The collection model and the incoming collection domain
            object have different version numbers.
    """
    if not change_list:
        raise Exception(
            'Unexpected error: received an invalid change list when trying to '
            'save collection %s: %s' % (collection.id, change_list))

    collection_rights = rights_manager.get_collection_rights(collection.id)
    if collection_rights.status != rights_domain.ACTIVITY_STATUS_PRIVATE:
        collection.validate(strict=True)
    else:
        collection.validate(strict=False)

    # Validate that all explorations referenced by the collection exist.
    exp_ids = collection.exploration_ids
    exp_summaries = (
        exp_fetchers.get_exploration_summaries_matching_ids(exp_ids))
    exp_summaries_dict = {
        exp_id: exp_summaries[ind]
        for (ind, exp_id) in enumerate(exp_ids)
    }
    for collection_node in collection.nodes:
        if not exp_summaries_dict[collection_node.exploration_id]:
            raise utils.ValidationError(
                'Expected collection to only reference valid explorations, '
                'but found an exploration with ID: %s (was it deleted?)' %
                collection_node.exploration_id)

    # Ensure no explorations are being added that are 'below' the public status
    # of this collection. If the collection is private, it can have both
    # private and public explorations. If it's public, it can only have public
    # explorations.
    # TODO(bhenning): Ensure the latter is enforced above when trying to
    # publish a collection.
    if rights_manager.is_collection_public(collection.id):
        validate_exps_in_collection_are_public(collection)

    # Collection model cannot be none as we are passing the collection as a
    # parameter and also this function is called by update_collection which only
    # works if the collection is put into the datastore.
    collection_model = collection_models.CollectionModel.get(collection.id,
                                                             strict=False)
    if collection.version > collection_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of collection '
            'from version %s. Please reload the page and try again.' %
            (collection_model.version, collection.version))
    elif collection.version < collection_model.version:
        raise Exception(
            'Trying to update version %s of collection from version %s, '
            'which is too old. Please reload the page and try again.' %
            (collection_model.version, collection.version))

    collection_model.category = collection.category
    collection_model.title = collection.title
    collection_model.objective = collection.objective
    collection_model.language_code = collection.language_code
    collection_model.tags = collection.tags
    collection_model.schema_version = collection.schema_version
    collection_model.collection_contents = {
        'nodes':
        [collection_node.to_dict() for collection_node in collection.nodes]
    }
    collection_model.node_count = len(
        collection_model.collection_contents['nodes'])
    collection_model.commit(committer_id, commit_message, change_list)
    caching_services.delete_multi(caching_services.CACHE_NAMESPACE_COLLECTION,
                                  None, [collection.id])
    index_collections_given_ids([collection.id])

    collection.version += 1
示例#14
0
    def test_migration_then_reversion_maintains_valid_exploration(self):
        """This integration test simulates the behavior of the domain layer
        prior to the introduction of a states schema. In particular, it deals
        with an exploration that was created before any states schema
        migrations occur. The exploration is constructed using multiple change
        lists, then a migration is run. The test thereafter tests if
        reverting to a version prior to the migration still maintains a valid
        exploration. It tests both the exploration domain object and the
        exploration model stored in the datastore for validity.
        Note: It is important to distinguish between when the test is testing
        the exploration domain versus its model. It is operating at the domain
        layer when using exp_fetchers.get_exploration_by_id. Otherwise, it
        loads the model explicitly using exp_models.ExplorationModel.get and
        then converts it to an exploration domain object for validation using
        exp_fetchers.get_exploration_from_model. This is NOT the same process
        as exp_fetchers.get_exploration_by_id as it skips many steps which
        include the conversion pipeline (which is crucial to this test).
        """
        exp_id = 'exp_id2'
        end_state_name = 'End'

        # Create an exploration with an old states schema version.
        swap_states_schema_41 = self.swap(feconf,
                                          'CURRENT_STATE_SCHEMA_VERSION', 41)
        swap_exp_schema_46 = self.swap(exp_domain.Exploration,
                                       'CURRENT_EXP_SCHEMA_VERSION', 46)
        with swap_states_schema_41, swap_exp_schema_46:
            self.save_new_valid_exploration(exp_id,
                                            self.albert_id,
                                            title='Old Title',
                                            end_state_name=end_state_name)
        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_EXPLORATION, None, [exp_id])

        # Load the exploration without using the conversion pipeline. All of
        # these changes are to happen on an exploration with states schema
        # version 41.
        exploration_model = exp_models.ExplorationModel.get(exp_id,
                                                            strict=True,
                                                            version=None)

        # In version 1, the title was 'Old title'.
        # In version 2, the title becomes 'New title'.
        exploration_model.title = 'New title'
        exploration_model.commit(self.albert_id, 'Changed title.', [])

        # Version 2 of exploration.
        exploration_model = exp_models.ExplorationModel.get(exp_id,
                                                            strict=True,
                                                            version=None)

        # Store state id mapping model for new exploration.
        exp_fetchers.get_exploration_from_model(exploration_model)

        # In version 3, a new state is added.
        exploration_model.states['New state'] = {
            'solicit_answer_details': False,
            'written_translations': {
                'translations_mapping': {
                    'content': {},
                    'default_outcome': {},
                    'ca_placeholder_0': {},
                }
            },
            'recorded_voiceovers': {
                'voiceovers_mapping': {
                    'content': {},
                    'default_outcome': {},
                    'ca_placeholder_0': {},
                }
            },
            'param_changes': [],
            'classifier_model_id': None,
            'content': {
                'content_id': 'content',
                'html': '<p>Unicode Characters ����</p>'
            },
            'next_content_id_index': 5,
            'interaction': {
                'answer_groups': [],
                'confirmed_unclassified_answers': [],
                'customization_args': {
                    'buttonText': {
                        'value': {
                            'content_id': 'ca_placeholder_0',
                            'unicode_str': 'Click me!',
                        },
                    },
                },
                'default_outcome': {
                    'dest': end_state_name,
                    'feedback': {
                        'content_id': 'default_outcome',
                        'html': '',
                    },
                    'labelled_as_correct': False,
                    'missing_prerequisite_skill_id': None,
                    'param_changes': [],
                    'refresher_exploration_id': None,
                },
                'hints': [],
                'id': 'Continue',
                'solution': None,
            },
        }

        # Properly link in the new state to avoid an invalid exploration.
        init_state = exploration_model.states[feconf.DEFAULT_INIT_STATE_NAME]
        init_state['interaction']['default_outcome']['dest'] = 'New state'

        exploration_model.commit('committer_id_v3', 'Added new state', [])

        # Version 3 of exploration.
        exploration_model = exp_models.ExplorationModel.get(exp_id,
                                                            strict=True,
                                                            version=None)

        # Version 4 is an upgrade based on the migration job.
        commit_cmds = [
            exp_domain.ExplorationChange({
                'cmd':
                exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
                'from_version':
                str(exploration_model.states_schema_version),
                'to_version':
                str(feconf.CURRENT_STATE_SCHEMA_VERSION)
            })
        ]
        exp_services.update_exploration(
            feconf.MIGRATION_BOT_USERNAME, exploration_model.id, commit_cmds,
            'Update exploration states from schema version %d to %d.' %
            (exploration_model.states_schema_version,
             feconf.CURRENT_STATE_SCHEMA_VERSION))

        # Verify the latest version of the exploration has the most up-to-date
        # states schema version.
        exploration_model = exp_models.ExplorationModel.get(exp_id,
                                                            strict=True,
                                                            version=None)
        exploration = exp_fetchers.get_exploration_from_model(
            exploration_model, run_conversion=False)
        self.assertEqual(exploration.states_schema_version,
                         feconf.CURRENT_STATE_SCHEMA_VERSION)

        # The exploration should be valid after conversion.
        exploration.validate(strict=True)

        # Version 5 is a reversion to version 1.
        exp_services.revert_exploration('committer_id_v4', exp_id, 4, 1)

        # The exploration model itself should now be the old version
        # (pre-migration).
        exploration_model = exp_models.ExplorationModel.get(exp_id,
                                                            strict=True,
                                                            version=None)
        self.assertEqual(exploration_model.states_schema_version, 41)

        # The exploration domain object should be updated since it ran through
        # the conversion pipeline.
        exploration = exp_fetchers.get_exploration_by_id(exp_id)

        # The reversion after migration should still be an up-to-date
        # exploration. exp_fetchers.get_exploration_by_id will automatically
        # keep it up-to-date.
        self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML)

        # The exploration should be valid after reversion.
        exploration.validate(strict=True)

        snapshots_metadata = exp_services.get_exploration_snapshots_metadata(
            exp_id)

        # These are used to verify the correct history has been recorded after
        # both migration and reversion.
        commit_dict_5 = {
            'committer_id': 'committer_id_v4',
            'commit_message': 'Reverted exploration to version 1',
            'version_number': 5,
        }
        commit_dict_4 = {
            'committer_id':
            feconf.MIGRATION_BOT_USERNAME,
            'commit_message':
            'Update exploration states from schema version 41 to %d.' %
            feconf.CURRENT_STATE_SCHEMA_VERSION,
            'commit_cmds': [{
                'cmd':
                exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION,
                'from_version':
                '41',
                'to_version':
                str(feconf.CURRENT_STATE_SCHEMA_VERSION)
            }],
            'version_number':
            4,
        }

        # Ensure there have been 5 commits.
        self.assertEqual(len(snapshots_metadata), 5)

        # Ensure the correct commit logs were entered during both migration and
        # reversion. Also, ensure the correct commit command was written during
        # migration.
        # These asserts check whether one dict is subset of the other.
        # The format is assertDictEqual(a, {**a, **b}) where a is the superset
        # and b is the subset.
        self.assertDictEqual(snapshots_metadata[3], {
            **snapshots_metadata[3],
            **commit_dict_4
        })
        self.assertDictEqual(snapshots_metadata[4], {
            **snapshots_metadata[4],
            **commit_dict_5
        })
        self.assertLess(snapshots_metadata[3]['created_on_ms'],
                        snapshots_metadata[4]['created_on_ms'])

        # Ensure that if a converted, then reverted, then converted exploration
        # is saved, it will be the up-to-date version within the datastore.
        exp_services.update_exploration(self.albert_id, exp_id, [],
                                        'Resave after reversion')
        exploration_model = exp_models.ExplorationModel.get(exp_id,
                                                            strict=True,
                                                            version=None)
        exploration = exp_fetchers.get_exploration_from_model(
            exploration_model, run_conversion=False)

        # This exploration should be both up-to-date and valid.
        self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML)
        exploration.validate()
示例#15
0
    def setUp(self):
        super(PlatformFeatureServiceTest, self).setUp()

        self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME)
        self.user_id = self.get_user_id_from_email(self.OWNER_EMAIL)

        registry.Registry.parameter_registry.clear()
        # Parameter names that might be used in following tests.
        param_names = ['feature_a', 'feature_b']
        caching_services.delete_multi(
            caching_services.CACHE_NAMESPACE_PLATFORM_PARAMETER, None,
            param_names)

        self.dev_feature = registry.Registry.create_feature_flag(
            'feature_a', 'a feature in dev stage',
            platform_parameter_domain.FEATURE_STAGES.dev)
        self.prod_feature = registry.Registry.create_feature_flag(
            'feature_b', 'a feature in prod stage',
            platform_parameter_domain.FEATURE_STAGES.prod)

        registry.Registry.update_platform_parameter(
            self.dev_feature.name, self.user_id, 'edit rules',
            [
                {
                    'filters': [
                        {
                            'type': 'server_mode',
                            'conditions': [
                                [
                                    '=',
                                    platform_parameter_domain.SERVER_MODES.dev
                                ]
                            ]
                        }
                    ],
                    'value_when_matched': True
                }
            ]
        )

        registry.Registry.update_platform_parameter(
            self.prod_feature.name, self.user_id, 'edit rules',
            [
                {
                    'filters': [
                        {
                            'type': 'server_mode',
                            'conditions': [
                                [
                                    '=',
                                    platform_parameter_domain.SERVER_MODES.dev
                                ],
                                [
                                    '=',
                                    platform_parameter_domain.SERVER_MODES.test
                                ],
                                [
                                    '=',
                                    platform_parameter_domain.SERVER_MODES.prod
                                ]
                            ]
                        }
                    ],
                    'value_when_matched': True
                }
            ]
        )

        # Replace feature lists with mocked names.
        self.original_feature_list = feature_services.ALL_FEATURES_LIST
        self.original_feature_name_set = (
            feature_services.ALL_FEATURES_NAMES_SET)
        feature_services.ALL_FEATURES_LIST = param_names
        feature_services.ALL_FEATURES_NAMES_SET = set(param_names)
示例#16
0
def _save_skill(committer_id, skill, commit_message, change_list):
    """Validates a skill and commits it to persistent storage. If
    successful, increments the version number of the incoming skill domain
    object by 1.

    Args:
        committer_id: str. ID of the given committer.
        skill: Skill. The skill domain object to be saved.
        commit_message: str. The commit message.
        change_list: list(SkillChange). List of changes applied to a skill.

    Raises:
        Exception. The skill model and the incoming skill domain object have
            different version numbers.
        Exception. Received invalid change list.
    """
    if not change_list:
        raise Exception(
            'Unexpected error: received an invalid change list when trying to '
            'save skill %s: %s' % (skill.id, change_list))
    skill.validate()

    # Skill model cannot be None as skill is passed as parameter here and that
    # is only possible if a skill model with that skill id exists.
    skill_model = skill_models.SkillModel.get(
        skill.id, strict=False)

    if skill.version > skill_model.version:
        raise Exception(
            'Unexpected error: trying to update version %s of skill '
            'from version %s. Please reload the page and try again.'
            % (skill_model.version, skill.version))
    elif skill.version < skill_model.version:
        raise Exception(
            'Trying to update version %s of skill from version %s, '
            'which is too old. Please reload the page and try again.'
            % (skill_model.version, skill.version))

    skill_model.description = skill.description
    skill_model.language_code = skill.language_code
    skill_model.superseding_skill_id = skill.superseding_skill_id
    skill_model.all_questions_merged = skill.all_questions_merged
    skill_model.prerequisite_skill_ids = skill.prerequisite_skill_ids
    skill_model.misconceptions_schema_version = (
        skill.misconceptions_schema_version)
    skill_model.rubric_schema_version = (
        skill.rubric_schema_version)
    skill_model.skill_contents_schema_version = (
        skill.skill_contents_schema_version)
    skill_model.skill_contents = skill.skill_contents.to_dict()
    skill_model.misconceptions = [
        misconception.to_dict() for misconception in skill.misconceptions
    ]
    skill_model.rubrics = [
        rubric.to_dict() for rubric in skill.rubrics
    ]
    skill_model.next_misconception_id = skill.next_misconception_id
    change_dicts = [change.to_dict() for change in change_list]
    skill_model.commit(committer_id, commit_message, change_dicts)
    caching_services.delete_multi(
        caching_services.CACHE_NAMESPACE_SKILL, None, [skill.id])
    skill.version += 1