core/domain/exp_domain_test.py
# coding: utf-8
#
# Copyright 2014 The Oppia Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for exploration domain objects and methods defined on them."""
from __future__ import annotations
import copy
import os
from core import feconf
from core import utils
from core.constants import constants
from core.domain import exp_domain
from core.domain import exp_fetchers
from core.domain import exp_services
from core.domain import exp_services_test
from core.domain import param_domain
from core.domain import rights_manager
from core.domain import state_domain
from core.domain import translation_domain
from core.platform import models
from core.tests import test_utils
from typing import Dict, Final, List, Tuple, Union, cast
MYPY = False
if MYPY: # pragma: no cover
from mypy_imports import exp_models
(exp_models,) = models.Registry.import_models([models.Names.EXPLORATION])
class ExplorationChangeTests(test_utils.GenericTestBase):
def test_exp_change_object_with_missing_cmd(self) -> None:
with self.assertRaisesRegex(
utils.ValidationError, 'Missing cmd key in change dict'):
exp_domain.ExplorationChange({'invalid': 'data'})
def test_exp_change_object_with_invalid_cmd(self) -> None:
with self.assertRaisesRegex(
utils.ValidationError, 'Command invalid is not allowed'):
exp_domain.ExplorationChange({'cmd': 'invalid'})
def test_exp_change_object_with_deprecated_cmd(self) -> None:
with self.assertRaisesRegex(
utils.DeprecatedCommandError, 'Command clone is deprecated'):
exp_domain.ExplorationChange({
'cmd': 'clone',
'property_name': 'content',
'old_value': 'old_value'
})
def test_exp_change_object_with_deprecated_cmd_argument(self) -> None:
with self.assertRaisesRegex(
utils.DeprecatedCommandError,
'Value for property_name in cmd edit_state_property: '
'fallbacks is deprecated'):
exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'fallbacks',
'new_value': 'foo',
})
def test_exp_change_object_with_missing_attribute_in_cmd(self) -> None:
with self.assertRaisesRegex(
utils.ValidationError, (
'The following required attributes are missing: '
'new_value')):
exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'content',
'old_value': 'old_value'
})
def test_exp_change_object_with_extra_attribute_in_cmd(self) -> None:
with self.assertRaisesRegex(
utils.ValidationError, (
'The following extra attributes are present: invalid')):
exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'old_state_name',
'new_state_name': 'new_state_name',
'invalid': 'invalid'
})
def test_exp_change_object_with_invalid_exploration_property(self) -> None:
with self.assertRaisesRegex(
utils.ValidationError, (
'Value for property_name in cmd edit_exploration_property: '
'invalid is not allowed')):
exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'invalid',
'old_value': 'old_value',
'new_value': 'new_value',
})
def test_exp_change_object_with_invalid_state_property(self) -> None:
with self.assertRaisesRegex(
utils.ValidationError, (
'Value for property_name in cmd edit_state_property: '
'invalid is not allowed')):
exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'state_name',
'property_name': 'invalid',
'old_value': 'old_value',
'new_value': 'new_value',
})
def test_exp_change_object_with_create_new(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'create_new',
'category': 'category',
'title': 'title'
})
self.assertEqual(exp_change_object.cmd, 'create_new')
self.assertEqual(exp_change_object.category, 'category')
self.assertEqual(exp_change_object.title, 'title')
def test_exp_change_object_with_add_state(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'state_name',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
})
self.assertEqual(exp_change_object.cmd, 'add_state')
self.assertEqual(exp_change_object.state_name, 'state_name')
def test_exp_change_object_with_rename_state(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'old_state_name',
'new_state_name': 'new_state_name'
})
self.assertEqual(exp_change_object.cmd, 'rename_state')
self.assertEqual(exp_change_object.old_state_name, 'old_state_name')
self.assertEqual(exp_change_object.new_state_name, 'new_state_name')
def test_exp_change_object_with_delete_state(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'state_name',
})
self.assertEqual(exp_change_object.cmd, 'delete_state')
self.assertEqual(exp_change_object.state_name, 'state_name')
def test_exp_change_object_with_edit_state_property(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'state_name',
'property_name': 'content',
'new_value': 'new_value',
'old_value': 'old_value'
})
self.assertEqual(exp_change_object.cmd, 'edit_state_property')
self.assertEqual(exp_change_object.state_name, 'state_name')
self.assertEqual(exp_change_object.property_name, 'content')
self.assertEqual(exp_change_object.new_value, 'new_value')
self.assertEqual(exp_change_object.old_value, 'old_value')
def test_exp_change_object_with_edit_exploration_property(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'title',
'new_value': 'new_value',
'old_value': 'old_value'
})
self.assertEqual(exp_change_object.cmd, 'edit_exploration_property')
self.assertEqual(exp_change_object.property_name, 'title')
self.assertEqual(exp_change_object.new_value, 'new_value')
self.assertEqual(exp_change_object.old_value, 'old_value')
def test_exp_change_object_with_migrate_states_schema_to_latest_version(
self
) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': 'migrate_states_schema_to_latest_version',
'from_version': 'from_version',
'to_version': 'to_version',
})
self.assertEqual(
exp_change_object.cmd, 'migrate_states_schema_to_latest_version')
self.assertEqual(exp_change_object.from_version, 'from_version')
self.assertEqual(exp_change_object.to_version, 'to_version')
def test_exp_change_object_with_revert_commit(self) -> None:
exp_change_object = exp_domain.ExplorationChange({
'cmd': exp_models.ExplorationModel.CMD_REVERT_COMMIT,
'version_number': 'version_number'
})
self.assertEqual(
exp_change_object.cmd,
exp_models.ExplorationModel.CMD_REVERT_COMMIT)
self.assertEqual(exp_change_object.version_number, 'version_number')
def test_to_dict(self) -> None:
exp_change_dict = {
'cmd': 'create_new',
'title': 'title',
'category': 'category'
}
exp_change_object = exp_domain.ExplorationChange(exp_change_dict)
self.assertEqual(exp_change_object.to_dict(), exp_change_dict)
class ExplorationVersionsDiffDomainUnitTests(test_utils.GenericTestBase):
"""Test the exploration versions difference domain object."""
def setUp(self) -> None:
super().setUp()
self.exp_id = 'exp_id1'
test_exp_filepath = os.path.join(
feconf.TESTS_DATA_DIR, 'string_classifier_test.yaml')
yaml_content = utils.get_file_contents(test_exp_filepath)
assets_list: List[Tuple[str, bytes]] = []
exp_services.save_new_exploration_from_yaml_and_assets(
feconf.SYSTEM_COMMITTER_ID, yaml_content, self.exp_id,
assets_list)
self.exploration = exp_fetchers.get_exploration_by_id(self.exp_id)
def test_correct_creation_of_version_diffs(self) -> None:
# Rename a state.
self.exploration.rename_state('Home', 'Renamed state')
change_list = [exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'Home',
'new_state_name': 'Renamed state'
})]
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
self.assertEqual(exp_versions_diff.added_state_names, [])
self.assertEqual(exp_versions_diff.deleted_state_names, [])
self.assertEqual(
exp_versions_diff.old_to_new_state_names, {
'Home': 'Renamed state'
})
self.exploration.version += 1
# Add a state.
self.exploration.add_states(['New state'])
self.exploration.states['New state'] = copy.deepcopy(
self.exploration.states['Renamed state'])
change_list = [exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'New state',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
})]
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
self.assertEqual(exp_versions_diff.added_state_names, ['New state'])
self.assertEqual(exp_versions_diff.deleted_state_names, [])
self.assertEqual(exp_versions_diff.old_to_new_state_names, {})
self.exploration.version += 1
# Delete state.
self.exploration.delete_state('New state')
change_list = [exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'New state'
})]
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
self.assertEqual(exp_versions_diff.added_state_names, [])
self.assertEqual(exp_versions_diff.deleted_state_names, ['New state'])
self.assertEqual(exp_versions_diff.old_to_new_state_names, {})
self.exploration.version += 1
# Test addition and multiple renames.
self.exploration.add_states(['New state'])
self.exploration.states['New state'] = copy.deepcopy(
self.exploration.states['Renamed state'])
self.exploration.rename_state('New state', 'New state2')
self.exploration.rename_state('New state2', 'New state3')
change_list = [exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'New state',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'New state',
'new_state_name': 'New state2'
}), exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'New state2',
'new_state_name': 'New state3'
})]
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
self.assertEqual(exp_versions_diff.added_state_names, ['New state3'])
self.assertEqual(exp_versions_diff.deleted_state_names, [])
self.assertEqual(exp_versions_diff.old_to_new_state_names, {})
self.exploration.version += 1
# Test addition, rename and deletion.
self.exploration.add_states(['New state 2'])
self.exploration.rename_state('New state 2', 'Renamed state 2')
self.exploration.delete_state('Renamed state 2')
change_list = [exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'New state 2',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'New state 2',
'new_state_name': 'Renamed state 2'
}), exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'Renamed state 2'
})]
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
self.assertEqual(exp_versions_diff.added_state_names, [])
self.assertEqual(exp_versions_diff.deleted_state_names, [])
self.assertEqual(exp_versions_diff.old_to_new_state_names, {})
self.exploration.version += 1
# Test multiple renames and deletion.
self.exploration.rename_state('New state3', 'Renamed state 3')
self.exploration.rename_state('Renamed state 3', 'Renamed state 4')
self.exploration.delete_state('Renamed state 4')
change_list = [exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'New state3',
'new_state_name': 'Renamed state 3'
}), exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'Renamed state 3',
'new_state_name': 'Renamed state 4'
}), exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'Renamed state 4'
})]
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
self.assertEqual(exp_versions_diff.added_state_names, [])
self.assertEqual(
exp_versions_diff.deleted_state_names, ['New state3'])
self.assertEqual(exp_versions_diff.old_to_new_state_names, {})
self.exploration.version += 1
def test_cannot_create_exploration_change_with_invalid_change_dict(
self
) -> None:
with self.assertRaisesRegex(
Exception, 'Missing cmd key in change dict'):
exp_domain.ExplorationChange({
'invalid_cmd': 'invalid'
})
def test_cannot_create_exploration_change_with_invalid_cmd(self) -> None:
with self.assertRaisesRegex(
Exception, 'Command invalid_cmd is not allowed'):
exp_domain.ExplorationChange({
'cmd': 'invalid_cmd'
})
def test_cannot_create_exploration_change_with_invalid_state_property(
self
) -> None:
exp_change = exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': exp_domain.STATE_PROPERTY_INTERACTION_ID,
'state_name': '',
'new_value': ''
})
self.assertTrue(isinstance(exp_change, exp_domain.ExplorationChange))
with self.assertRaisesRegex(
Exception,
'Value for property_name in cmd edit_state_property: '
'invalid_property is not allowed'):
exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY,
'property_name': 'invalid_property',
'state_name': '',
'new_value': ''
})
def test_cannot_create_exploration_change_with_invalid_exploration_property(
self
) -> None:
exp_change = exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_EXPLORATION_PROPERTY,
'property_name': 'title',
'new_value': ''
})
self.assertTrue(isinstance(exp_change, exp_domain.ExplorationChange))
with self.assertRaisesRegex(
Exception,
'Value for property_name in cmd edit_exploration_property: '
'invalid_property is not allowed'):
exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_EXPLORATION_PROPERTY,
'property_name': 'invalid_property',
'new_value': ''
})
def test_revert_exploration_commit(self) -> None:
exp_change = exp_domain.ExplorationChange({
'cmd': exp_models.ExplorationModel.CMD_REVERT_COMMIT,
'version_number': 1
})
self.assertEqual(exp_change.version_number, 1)
exp_change = exp_domain.ExplorationChange({
'cmd': exp_models.ExplorationModel.CMD_REVERT_COMMIT,
'version_number': 2
})
self.assertEqual(exp_change.version_number, 2)
class ExpVersionReferenceTests(test_utils.GenericTestBase):
def test_create_exp_version_reference_object(self) -> None:
exp_version_reference = exp_domain.ExpVersionReference('exp_id', 1)
self.assertEqual(
exp_version_reference.to_dict(), {
'exp_id': 'exp_id',
'version': 1
})
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exp_version(self) -> None:
with self.assertRaisesRegex(
Exception,
'Expected version to be an int, received invalid_version'):
exp_domain.ExpVersionReference('exp_id', 'invalid_version') # type: ignore[arg-type]
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exp_id(self) -> None:
with self.assertRaisesRegex(
Exception, 'Expected exp_id to be a str, received 0'):
exp_domain.ExpVersionReference(0, 1) # type: ignore[arg-type]
class TransientCheckpointUrlTests(test_utils.GenericTestBase):
"""Testing TransientCheckpointUrl domain object."""
def setUp(self) -> None:
super().setUp()
self.transient_checkpoint_url = exp_domain.TransientCheckpointUrl(
'exp_id', 'frcs_name', 1, 'mrrcs_name', 1)
def test_initialization(self) -> None:
"""Testing init method."""
self.assertEqual(self.transient_checkpoint_url.exploration_id, 'exp_id')
self.assertEqual(
self.transient_checkpoint_url
.furthest_reached_checkpoint_state_name,
'frcs_name')
self.assertEqual(
self.transient_checkpoint_url.
furthest_reached_checkpoint_exp_version, 1)
self.assertEqual(
self.transient_checkpoint_url
.most_recently_reached_checkpoint_state_name, 'mrrcs_name')
self.assertEqual(
self.transient_checkpoint_url
.most_recently_reached_checkpoint_exp_version, 1)
def test_to_dict(self) -> None:
logged_out_learner_progress_dict = {
'exploration_id': 'exploration_id',
'furthest_reached_checkpoint_exp_version': 1,
'furthest_reached_checkpoint_state_name': (
'furthest_reached_checkpoint_state_name'),
'most_recently_reached_checkpoint_exp_version': 1,
'most_recently_reached_checkpoint_state_name': (
'most_recently_reached_checkpoint_state_name')
}
logged_out_learner_progress_object = exp_domain.TransientCheckpointUrl(
'exploration_id',
'furthest_reached_checkpoint_state_name', 1,
'most_recently_reached_checkpoint_state_name', 1
)
self.assertEqual(
logged_out_learner_progress_object.to_dict(),
logged_out_learner_progress_dict)
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_exploration_id_incorrect_type(self) -> None:
self.transient_checkpoint_url.exploration_id = 5 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected exploration_id to be a str'
):
self.transient_checkpoint_url.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_furthest_reached_checkpoint_state_name_incorrect_type(
self
) -> None:
self.transient_checkpoint_url.furthest_reached_checkpoint_state_name = 5 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected furthest_reached_checkpoint_state_name to be a str'
):
self.transient_checkpoint_url.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_furthest_reached_checkpoint_exp_version_incorrect_type(
self
) -> None:
self.transient_checkpoint_url.furthest_reached_checkpoint_exp_version = 'invalid_version' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected furthest_reached_checkpoint_exp_version to be an int'
):
self.transient_checkpoint_url.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_most_recently_reached_checkpoint_state_name_incorrect_type(
self
) -> None:
self.transient_checkpoint_url.most_recently_reached_checkpoint_state_name = 5 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected most_recently_reached_checkpoint_state_name to be a str'
):
self.transient_checkpoint_url.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_most_recently_reached_checkpoint_exp_version_incorrect_type(
self
) -> None:
self.transient_checkpoint_url.most_recently_reached_checkpoint_exp_version = 'invalid_version' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected most_recently_reached_checkpoint_exp_version to be an int'
):
self.transient_checkpoint_url.validate()
class ExplorationCheckpointsUnitTests(test_utils.GenericTestBase):
"""Test checkpoints validations in an exploration. """
def setUp(self) -> None:
super().setUp()
self.exploration = (
exp_domain.Exploration.create_default_exploration('eid'))
self.content_id_generator = translation_domain.ContentIdGenerator(
self.exploration.next_content_id_index
)
self.new_state = state_domain.State.create_default_state(
'Introduction',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME),
is_initial_state=True)
self.set_interaction_for_state(
self.new_state, 'TextInput', self.content_id_generator)
self.exploration.init_state_name = 'Introduction'
self.exploration.states = {
self.exploration.init_state_name: self.new_state
}
self.set_interaction_for_state(
self.exploration.states[self.exploration.init_state_name],
'TextInput', self.content_id_generator)
self.init_state = (
self.exploration.states[self.exploration.init_state_name])
self.end_state = state_domain.State.create_default_state(
'End',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
self.end_state, 'EndExploration', self.content_id_generator)
self.end_state.update_interaction_default_outcome(None)
self.exploration.next_content_id_index = (
self.content_id_generator.next_content_id_index)
def test_init_state_with_card_is_checkpoint_false_is_invalid(self) -> None:
self.init_state.update_card_is_checkpoint(False)
with self.assertRaisesRegex(
Exception, 'Expected card_is_checkpoint of first state to '
'be True but found it to be False'):
self.exploration.validate(strict=True)
self.init_state.update_card_is_checkpoint(True)
def test_end_state_with_card_is_checkpoint_true_is_invalid(self) -> None:
default_outcome = self.init_state.interaction.default_outcome
# Ruling out the possibility of None for mypy type checking.
assert default_outcome is not None
default_outcome.dest = self.exploration.init_state_name
self.init_state.update_interaction_default_outcome(default_outcome)
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'End': self.end_state
}
self.end_state.update_card_is_checkpoint(True)
with self.assertRaisesRegex(
Exception, 'Expected card_is_checkpoint of terminal state '
'to be False but found it to be True'):
self.exploration.validate(strict=True)
self.end_state.update_card_is_checkpoint(False)
def test_init_state_checkpoint_with_end_exp_interaction_is_valid(
self
) -> None:
self.exploration.init_state_name = 'End'
self.exploration.states = {
self.exploration.init_state_name: self.end_state
}
self.exploration.objective = 'Objective'
self.exploration.title = 'Title'
self.exploration.category = 'Category'
self.end_state.update_card_is_checkpoint(True)
self.exploration.validate(strict=True)
self.end_state.update_card_is_checkpoint(False)
def test_checkpoint_count_with_count_outside_range_is_invalid(self) -> None:
self.exploration.init_state_name = 'Introduction'
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'End': self.end_state
}
for i in range(8):
self.exploration.add_states(['State%s' % i])
self.exploration.states['State%s' % i].card_is_checkpoint = True
self.set_interaction_for_state(
self.exploration.states['State%s' % i],
'Continue', self.content_id_generator)
with self.assertRaisesRegex(
Exception, 'Expected checkpoint count to be between 1 and 8 '
'inclusive but found it to be 9'
):
self.exploration.validate(strict=True)
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'End': self.end_state
}
def test_bypassable_state_with_card_is_checkpoint_true_is_invalid(
self
) -> None:
# Note: In the graphs below, states with the * symbol are checkpoints.
# Exploration to test a checkpoint state which has no outcome.
# ┌────────────────┐
# │ Introduction* │
# └──┬───────────┬─┘
# │ │
# │ │
# ┌────────┴──┐ ┌─┴─────────┐
# │ Second* │ │ Third │
# └───────────┘ └─┬─────────┘
# │
# ┌─────────────┴─┐
# │ End │
# └───────────────┘.
second_state = state_domain.State.create_default_state(
'Second',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
second_state, 'TextInput', self.content_id_generator)
third_state = state_domain.State.create_default_state(
'Third',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
third_state, 'TextInput', self.content_id_generator)
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'End': self.end_state,
'Second': second_state,
'Third': third_state,
}
# Answer group dicts to connect init_state to second_state and
# third_state.
init_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'Second', None, state_domain.SubtitledHtml(
self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': (
self.content_id_generator.generate(
translation_domain.ContentType.RULE,
extra_prefix='input')),
'normalizedStrSet': ['Test0']
}
})
],
[],
None
), state_domain.AnswerGroup(
state_domain.Outcome(
'Third', None, state_domain.SubtitledHtml(
self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': (
self.content_id_generator.generate(
translation_domain.ContentType.RULE,
extra_prefix='input')),
'normalizedStrSet': ['Test1']
}
})
],
[],
None
)
]
# Answer group dict to connect third_state to end_state.
third_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'End', None, state_domain.SubtitledHtml(
self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': (
self.content_id_generator.generate(
translation_domain.ContentType.RULE,
extra_prefix='input')),
'normalizedStrSet': ['Test0']
}
})
],
[],
None
)
]
self.init_state.update_interaction_answer_groups(
init_state_answer_groups)
third_state.update_interaction_answer_groups(
third_state_answer_groups)
self.exploration.next_content_id_index = (
self.content_id_generator.next_content_id_index)
# The exploration can be completed via third_state. Hence, making
# second_state a checkpoint raises a validation error.
second_state.card_is_checkpoint = True
with self.assertRaisesRegex(
Exception, 'Cannot make Second a checkpoint as it is'
' bypassable'
):
self.exploration.validate(strict=True)
second_state.card_is_checkpoint = False
# Exploration to test a checkpoint state when the state in the other
# path has no outcome.
# ┌────────────────┐
# │ Introduction* │
# └──┬───────────┬─┘
# │ │
# │ │
# ┌────────┴──┐ ┌─┴─────────┐
# │ Second* │ │ Third │
# └────────┬──┘ └───────────┘
# │
# ┌─┴─────────────┐
# │ End │
# └───────────────┘.
# Answer group dicts to connect second_state to end_state.
second_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'End', None, state_domain.SubtitledHtml(
'feedback_0', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_0',
'normalizedStrSet': ['Test0']
}
})
],
[],
None
)
]
second_state.update_interaction_answer_groups(
second_state_answer_groups)
# Reset the answer group dicts of third_state.
third_state.update_interaction_answer_groups([])
# As second_state is now connected to end_state and third_state has no
# outcome, second_state has become non-bypassable.
second_state.update_card_is_checkpoint(True)
self.exploration.validate()
# Reset the exploration.
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'End': self.end_state
}
# Exploration to test a bypassable state.
# ┌────────────────┐
# │ Introduction* │
# └─┬─────┬──────┬─┘
# ┌───────────┐ │ │ │ ┌────────────┐
# │ A ├────┘ │ └─────┤ C │
# └────┬──────┘ │ └─────┬──────┘
# │ ┌────┴─────┐ │
# │ │ B │ │
# │ └──┬───────┘ │
# └─────────┐ │ │
# ┌──────┴─────┴─┐ ┌─────────────┘
# │ D* │ │
# └─────────────┬┘ │
# │ │
# ┌──┴─────┴──┐
# │ End │
# └───────────┘.
a_state = state_domain.State.create_default_state(
'A',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
a_state, 'TextInput', self.content_id_generator)
b_state = state_domain.State.create_default_state(
'B',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
b_state, 'TextInput', self.content_id_generator)
c_state = state_domain.State.create_default_state(
'C',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
c_state, 'TextInput', self.content_id_generator)
d_state = state_domain.State.create_default_state(
'D',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
d_state, 'TextInput', self.content_id_generator)
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'A': a_state,
'B': b_state,
'C': c_state,
'D': d_state,
'End': self.end_state
}
# Answer group dicts to connect init_state to a_state, b_state and
# c_state.
init_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'A', None, state_domain.SubtitledHtml(
'feedback_0', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_0',
'normalizedStrSet': ['Test0']
}
})
],
[],
None
), state_domain.AnswerGroup(
state_domain.Outcome(
'B', None, state_domain.SubtitledHtml(
'feedback_1', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_1',
'normalizedStrSet': ['Test1']
}
})
],
[],
None
), state_domain.AnswerGroup(
state_domain.Outcome(
'C', None, state_domain.SubtitledHtml(
'feedback_2', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_2',
'normalizedStrSet': ['Test2']
}
})
],
[],
None
)
]
# Answer group dict to connect a_state and b_state to d_state.
a_and_b_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'D', None, state_domain.SubtitledHtml(
'feedback_0', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_0',
'normalizedStrSet': ['Test0']
}
})
],
[],
None
)
]
# Answer group dict to connect c_state and d_state to end_state.
c_and_d_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'End', None, state_domain.SubtitledHtml(
'feedback_0', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_0',
'normalizedStrSet': ['Test0']
}
})
],
[],
None
)
]
self.init_state.update_interaction_answer_groups(
init_state_answer_groups)
a_state.update_interaction_answer_groups(
a_and_b_state_answer_groups)
b_state.update_interaction_answer_groups(
a_and_b_state_answer_groups)
c_state.update_interaction_answer_groups(
c_and_d_state_answer_groups)
d_state.update_interaction_answer_groups(
c_and_d_state_answer_groups)
# As a user can complete the exploration by going through c_state,
# d_state becomes bypassable. Hence, making d_state a checkpoint raises
# validation error.
d_state.update_card_is_checkpoint(True)
self.exploration.update_next_content_id_index(
self.content_id_generator.next_content_id_index)
with self.assertRaisesRegex(
Exception, 'Cannot make D a checkpoint as it is bypassable'
):
self.exploration.validate(strict=True)
d_state.update_card_is_checkpoint(False)
# Modifying the graph to make D non-bypassable.
# ┌────────────────┐
# │ Introduction* │
# └─┬─────┬──────┬─┘
# ┌───────────┐ │ │ │ ┌────────────┐
# │ A ├────┘ │ └─────┤ C │
# └────┬──────┘ │ └──────┬─────┘
# │ ┌────┴─────┐ │
# │ │ B │ │
# │ └────┬─────┘ │
# │ │ │
# │ ┌──────┴───────┐ │
# └──────────┤ D* ├───────────┘
# └──────┬───────┘
# │
# ┌─────┴─────┐
# │ End │
# └───────────┘.
# Answer group dict to connect c_state to d_state. Hence, making d_state
# non-bypassable.
c_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'D', None, state_domain.SubtitledHtml(
'feedback_0', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_0',
'normalizedStrSet': ['Test0']
}
})
],
[],
None
)
]
c_state.update_interaction_answer_groups(
c_state_answer_groups)
d_state.update_card_is_checkpoint(True)
self.exploration.validate()
# Modifying the graph to add another EndExploration state.
# ┌────────────────┐
# │ Introduction* │
# └─┬─────┬──────┬─┘
# ┌───────────┐ │ │ │ ┌────────────┐
# │ A ├────┘ │ └─────┤ C │
# └────┬──────┘ │ └──────┬───┬─┘
# │ ┌────┴─────┐ │ │
# │ │ B │ │ │
# │ └────┬─────┘ │ │
# │ │ │ │
# │ ┌──────┴───────┐ │ │
# └──────────┤ D* ├───────────┘ │
# └──────┬───────┘ │
# │ │
# ┌─────┴─────┐ ┌─────┴─────┐
# │ End │ │ End 2 │
# └───────────┘ └───────────┘.
new_end_state = state_domain.State.create_default_state(
'End 2',
self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
self.set_interaction_for_state(
new_end_state, 'EndExploration', self.content_id_generator)
new_end_state.update_interaction_default_outcome(None)
self.exploration.states = {
self.exploration.init_state_name: self.new_state,
'A': a_state,
'B': b_state,
'C': c_state,
'D': d_state,
'End': self.end_state,
'End 2': new_end_state
}
# Answer group dicts to connect c_state to d_state and new_end_state,
# making d_state bypassable.
c_state_answer_groups = [
state_domain.AnswerGroup(
state_domain.Outcome(
'D', None, state_domain.SubtitledHtml(
'feedback_0', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_0',
'normalizedStrSet': ['Test0']
}
})
],
[],
None
), state_domain.AnswerGroup(
state_domain.Outcome(
'End 2', None, state_domain.SubtitledHtml(
'feedback_1', '<p>Feedback</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_1',
'normalizedStrSet': ['Test1']
}
})
],
[],
None
)
]
c_state.update_interaction_answer_groups(
c_state_answer_groups)
with self.assertRaisesRegex(
Exception, 'Cannot make D a checkpoint as it is bypassable'
):
self.exploration.validate(strict=True)
d_state.update_card_is_checkpoint(False)
class ExplorationDomainUnitTests(test_utils.GenericTestBase):
"""Test the exploration domain object."""
def setUp(self) -> None:
super().setUp()
translation_dict = {
'content_id_3': translation_domain.TranslatedContent(
'My name is Nikhil.',
translation_domain.TranslatableContentFormat.HTML,
True
)
}
self.dummy_entity_translations = translation_domain.EntityTranslation(
'exp_id', feconf.TranslatableEntityType.EXPLORATION, 1, 'en',
translation_dict)
self.new_exploration = (
exp_domain.Exploration.create_default_exploration('test_id'))
self.content_id_generator = translation_domain.ContentIdGenerator(
self.new_exploration.next_content_id_index
)
self.state = self.new_exploration.states['Introduction']
self.set_interaction_for_state(
self.state, 'Continue', self.content_id_generator)
def test_image_rte_tag(self) -> None:
"""Validate image tag."""
self.state.content.html = (
'<oppia-noninteractive-image></oppia-noninteractive-image>')
self._assert_validation_error(
self.new_exploration, 'Image tag does not have \'alt-with-value\' '
'attribute.')
self.state.content.html = (
'<oppia-noninteractive-image alt-with-value=""Image"" '
'caption-with-value=\"&quot;aaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
'aaaaaa&quot;\"></oppia-noninteractive-image>')
self._assert_validation_error(
self.new_exploration, 'Image tag \'caption-with-value\' attribute '
'should not be greater than 500 characters.')
self.state.content.html = (
'<oppia-noninteractive-image alt-with-value=""Image"">'
'</oppia-noninteractive-image>')
self._assert_validation_error(
self.new_exploration, 'Image tag does not have \'caption-with'
'-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-image filepath-with-value="""'
'" caption-with-value="""" alt-with-value=""'
'Image""></oppia-noninteractive-image>')
self._assert_validation_error(
self.new_exploration, 'Image tag \'filepath-with-value\' attribute '
'should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-image caption-with-value="""" '
'alt-with-value=""Image""></oppia-noninteractive-image>')
self._assert_validation_error(
self.new_exploration, 'Image tag does not have \'filepath-with'
'-value\' attribute.')
def test_skill_review_rte_tag(self) -> None:
"""Validate SkillReview tag."""
self.state.content.html = (
'<oppia-noninteractive-skillreview skill_id-with-value='
'\"&quot;&quot;\" ></oppia-noninteractive-skillreview>'
)
self._assert_validation_error(
self.new_exploration, 'SkillReview tag does not have \'text-with'
'-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-skillreview skill_id-with-value='
'\"&quot;&quot;\" text-with-value=\"&quot;'
'&quot;\"></oppia-noninteractive-skillreview>'
)
self._assert_validation_error(
self.new_exploration, 'SkillReview tag \'text-with-value\' '
'attribute should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-skillreview text-with-value=\"&quot;'
'text&quot;\"></oppia-noninteractive-skillreview>'
)
self._assert_validation_error(
self.new_exploration, 'SkillReview tag does not have '
'\'skill_id-with-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-skillreview skill_id-with-value='
'\"&quot;&quot;\" text-with-value=\"&quot;'
'text&quot;\"></oppia-noninteractive-skillreview>'
)
self._assert_validation_error(
self.new_exploration, 'SkillReview tag \'skill_id-with-value\' '
'attribute should not be empty.')
def test_video_rte_tag(self) -> None:
"""Validate Video tag."""
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'end-with-value=\"11\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag does not have \'start-with'
'-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'end-with-value=\"11\" start-with-value=\"\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag \'start-with-value\' attribute '
'should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'start-with-value=\"13\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag does not have \'end-with-value\' '
'attribute.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'end-with-value=\"\" start-with-value=\"13\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag \'end-with-value\' attribute '
'should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'end-with-value=\"11\" start-with-value=\"13\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Start value should not be greater than End '
'value in Video tag.')
self.state.content.html = (
'<oppia-noninteractive-video '
'end-with-value=\"11\" start-with-value=\"9\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag does not have \'autoplay-with'
'-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"not valid\" '
'end-with-value=\"11\" start-with-value=\"9\"'
' video_id-with-value=\"&quot;Ntcw0H0hwPU&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag \'autoplay-with-value\' attribute '
'should be a boolean value.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'end-with-value=\"11\" start-with-value=\"9\">'
'</oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag does not have \'video_id-with'
'-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-video autoplay-with-value=\"true\" '
'end-with-value=\"11\" start-with-value=\"9\"'
' video_id-with-value=\"&quot;&'
'quot;\"></oppia-noninteractive-video>')
self._assert_validation_error(
self.new_exploration, 'Video tag \'video_id-with-value\' attribute '
'should not be empty.')
def test_link_rte_tag(self) -> None:
"""Validate Link tag."""
self.state.content.html = (
'<oppia-noninteractive-link '
'url-with-value=\"&quot;http://www.example.com&quot;\">'
'</oppia-noninteractive-link>'
)
self._assert_validation_error(
self.new_exploration, 'Link tag does not have \'text-with-value\' '
'attribute.')
self.state.content.html = (
'<oppia-noninteractive-link'
' text-with-value=\"&quot;something&quot;\">'
'</oppia-noninteractive-link>'
)
self._assert_validation_error(
self.new_exploration, 'Link tag does not have \'url-with-value\' '
'attribute.')
self.state.content.html = (
'<oppia-noninteractive-link'
' text-with-value=\"&quot;something&quot;\"'
' url-with-value=\"\"></oppia-noninteractive-link>'
)
self._assert_validation_error(
self.new_exploration, 'Link tag \'url-with-value\' attribute '
'should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-link text-with-value="&quot;Google'
'&quot;" url-with-value="&quot;http://www.google.com&'
'quot;"></oppia-noninteractive-link>'
)
self._assert_validation_error(
self.new_exploration, (
'Link should be prefix with acceptable schemas '
'which are \\[\'https\', \'\']')
)
def test_math_rte_tag(self) -> None:
"""Validate Math tag."""
self.state.content.html = (
'<oppia-noninteractive-math></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag does not have '
'\'math_content-with-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-math'
' math_content-with-value=\"\"></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag \'math_content-with-value\' '
'attribute should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-math math_content-with-value='
'\"{&quot;svg_filename&quot;:&quot;'
'mathImg.svgas&quot;}\"></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag does not have \'raw_latex-with'
'-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-math math_content-with-value='
'\"{&quot;raw_latex&quot;:&quot;'
'&quot;,&quot;svg_filename&quot;:&quot;'
'mathImg.svgas&quot;}\"></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag \'raw_latex-with-value\' attribute '
'should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-math math_content-with-value='
'\"{&quot;raw_latex&quot;:&quot;not empty'
'&quot;}\"></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag does not have '
'\'svg_filename-with-value\' attribute.')
self.state.content.html = (
'<oppia-noninteractive-math math_content-with-value='
'\"{&quot;raw_latex&quot;:&quot;something'
'&quot;,&quot;svg_filename&quot;:&quot;'
'&quot;}\"></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag \'svg_filename-with-value\' '
'attribute should not be empty.')
self.state.content.html = (
'<oppia-noninteractive-math math_content-with-value='
'\"{&quot;raw_latex&quot;:&quot;something'
'&quot;,&quot;svg_filename&quot;:&quot;'
'image.png&quot;}\"></oppia-noninteractive-math>'
)
self._assert_validation_error(
self.new_exploration, 'Math tag \'svg_filename-with-value\' '
'attribute should have svg extension.')
def test_tabs_rte_tag(self) -> None:
"""Validate Tabs tag."""
self.state.content.html = (
'<oppia-noninteractive-tabs tab_contents-with-value=\'[]\'>'
'</oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'No tabs are present inside the tabs tag.')
self.state.content.html = (
'<oppia-noninteractive-tabs></oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'No content attribute is present inside '
'the tabs tag.')
self.state.content.html = (
'<oppia-noninteractive-tabs tab_contents-with-value=\'[{&quot;'
'&quot;:&quot;Hint introduction&quot;,&quot;content'
'&quot;:&quot;&lt;p&gt;hint&lt;/p&gt;&'
'quot;}]\'></oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'No title attribute is present inside '
'the tabs tag.')
self.state.content.html = (
'<oppia-noninteractive-tabs tab_contents-with-value=\'[{&quot;'
'title&quot;:&quot;&quot;,&quot;content&quot;:'
'&quot;&lt;p&gt;hint&lt;/p&gt;&quot;}]\'>'
'</oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'title present inside tabs tag is empty.')
self.state.content.html = (
'<oppia-noninteractive-tabs tab_contents-with-value=\'[{&quot;'
'title&quot;:&quot;Hint introduction&quot;,&quot;'
'&quot;:&quot;&lt;p&gt;hint&lt;/p&gt;&'
'quot;}]\'></oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'No content attribute is present inside '
'the tabs tag.')
self.state.content.html = (
'<oppia-noninteractive-tabs tab_contents-with-value=\'[{&quot;'
'title&quot;:&quot;Hint introduction&quot;,&quot;'
'content&quot;:&quot;&lt;p&gt;&lt;/p&gt;'
'&quot;}]\'></oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'content present inside tabs tag is empty.')
self.state.content.html = (
'<oppia-noninteractive-tabs tab_contents-with-value=\'[{&quot;'
'title&quot;:&quot;Hint introduction&quot;,&quot;'
'content&quot;:&quot;&lt;p&gt;&lt;oppia-'
'noninteractive-tabs&gt;&lt;/oppia-noninteractive-tabs'
'&gt;&lt;/p&gt;&quot;}]\'>'
'</oppia-noninteractive-tabs>'
)
self._assert_validation_error(
self.new_exploration, 'Tabs tag should not be present inside '
'another Tabs or Collapsible tag.')
def test_collapsible_rte_tag(self) -> None:
"""Validate Collapsible tag."""
self.state.content.html = (
'<oppia-noninteractive-collapsible '
'content-with-value=\'&quot;&quot;\' heading-with-value='
'\'&quot;&quot;\'></oppia-noninteractive-collapsible>'
)
self._assert_validation_error(
self.new_exploration, 'No collapsible content is present '
'inside the tag.')
self.state.content.html = (
'<oppia-noninteractive-collapsible heading-with-value='
'\'&quot;head&quot;\'></oppia-noninteractive-collapsible>'
)
self._assert_validation_error(
self.new_exploration, 'No content attribute present in '
'collapsible tag.')
self.state.content.html = (
'<oppia-noninteractive-collapsible content-with-value='
'\'&quot;Content&quot;\' heading-with-value='
'\'&quot;&quot;\'></oppia-noninteractive-collapsible>'
)
self._assert_validation_error(
self.new_exploration, 'Heading attribute inside the collapsible '
'tag is empty.')
self.state.content.html = (
'<oppia-noninteractive-collapsible content-with-value=\'&'
'quot;Content&quot;\'></oppia-noninteractive-collapsible>'
)
self._assert_validation_error(
self.new_exploration, 'No heading attribute present in '
'collapsible tag.')
self.state.content.html = (
'<oppia-noninteractive-collapsible content-with-value='
'\'&quot;<oppia-noninteractive-collapsible>'
'</oppia-noninteractive-collapsible>&quot;\' heading-with-value'
'=\'&quot;heading&quot;\'>'
'</oppia-noninteractive-collapsible>'
)
self._assert_validation_error(
self.new_exploration, 'Collapsible tag should not be present '
'inside another Tabs or Collapsible tag.')
self.state.content.html = 'Valid content'
def test_continue_interaction(self) -> None:
"""Tests Continue interaction."""
self.set_interaction_for_state(
self.state, 'Continue', self.content_id_generator)
# Here we use cast because we are narrowing down the type from various
# customization args value types to 'SubtitledUnicode' type, and this
# is done because here we are accessing 'buttontext' key from continue
# customization arg whose value is always of SubtitledUnicode type.
subtitled_unicode_continue_ca_arg = cast(
state_domain.SubtitledUnicode,
self.state.interaction.customization_args[
'buttonText'
].value
)
subtitled_unicode_continue_ca_arg.unicode_str = (
'Continueeeeeeeeeeeeeeeeee'
)
self._assert_validation_error(
self.new_exploration, (
'The `continue` interaction text length should be atmost '
'20 characters.')
)
def test_end_interaction(self) -> None:
"""Tests End interaction."""
self.set_interaction_for_state(
self.state, 'EndExploration', self.content_id_generator)
self.state.interaction.customization_args[
'recommendedExplorationIds'].value = ['id1', 'id2', 'id3', 'id4']
self.state.update_interaction_default_outcome(None)
self._assert_validation_error(
self.new_exploration, (
'The total number of recommended explorations inside End '
'interaction should be atmost 3.')
)
def test_numeric_interaction(self) -> None:
"""Tests Numeric interaction."""
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
self.state, 'NumericInput', content_id_generator)
test_ans_group_for_numeric_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 7
}
},
{
'rule_type': 'IsInclusivelyBetween',
'inputs': {
'a': 3,
'b': 5
}
},
{
'rule_type': 'IsWithinTolerance',
'inputs': {
'x': 1,
'tol': -1
}
},
{
'rule_type': 'IsInclusivelyBetween',
'inputs': {
'a': 8,
'b': 8
}
},
{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 7
}
},
{
'rule_type': 'IsGreaterThanOrEqualTo',
'inputs': {
'x': 10
}
},
{
'rule_type': 'IsGreaterThanOrEqualTo',
'inputs': {
'x': 15
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
self.state.interaction.answer_groups = (
test_ans_group_for_numeric_interaction)
with self.assertRaisesRegex(
utils.ValidationError, 'Rule \'1\' from answer group \'0\' will '
'never be matched because it is made redundant by the above rules'
):
self.new_exploration.validate(strict=True)
rule_specs = self.state.interaction.answer_groups[0].rule_specs
rule_specs.remove(rule_specs[1])
self._assert_validation_error(
self.new_exploration, 'The rule \'1\' of answer group \'0\' having '
'rule type \'IsWithinTolerance\' have \'tol\' value less than or '
'equal to zero in NumericInput interaction.')
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' '
'having rule type \'IsInclusivelyBetween\' have `a` value greater '
'than `b` value in NumericInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' of '
'NumericInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule \'2\' from answer group \'0\' will '
'never be matched because it is made redundant by the above rules'
):
self.new_exploration.validate(strict=True)
def test_fraction_interaction(self) -> None:
"""Tests Fraction interaction."""
state = self.new_exploration.states['Introduction']
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
state, 'FractionInput', content_id_generator)
test_ans_group_for_fraction_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': 'HasFractionalPartExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 2,
'denominator': 3
}
}
},
{
'rule_type': 'HasFractionalPartExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 2,
'denominator': 3
}
}
},
{
'rule_type': 'HasFractionalPartExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 4,
'denominator': 6
}
}
},
{
'rule_type': 'HasFractionalPartExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 1,
'numerator': 3,
'denominator': 2
}
}
},
{
'rule_type': 'HasFractionalPartExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 3,
'denominator': 2
}
}
},
{
'rule_type': 'IsExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 2,
'numerator': 2,
'denominator': 3
}
}
},
{
'rule_type': 'IsGreaterThan',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 10,
'denominator': 3
}
}
},
{
'rule_type': 'IsExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 27,
'denominator': 2
}
}
},
{
'rule_type': 'HasDenominatorEqualTo',
'inputs': {
'x': 4
}
},
{
'rule_type': 'HasFractionalPartExactlyEqualTo',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 9,
'denominator': 4
}
}
},
{
'rule_type': 'IsLessThan',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 7,
'denominator': 2
}
}
},
{
'rule_type': 'IsLessThan',
'inputs': {
'f': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 5,
'denominator': 2
}
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
state.interaction.answer_groups = (
test_ans_group_for_fraction_interaction)
state.interaction.customization_args[
'allowNonzeroIntegerPart'].value = False
state.interaction.customization_args[
'allowImproperFraction'].value = False
state.interaction.customization_args[
'requireSimplestForm'].value = True
rule_specs = state.interaction.answer_groups[0].rule_specs
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' of '
'FractionInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' do '
'not have value in simple form '
'in FractionInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' do '
'not have value in proper fraction '
'in FractionInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' do '
'not have value in proper fraction '
'in FractionInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
state.interaction.customization_args[
'allowImproperFraction'].value = True
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' has '
'non zero integer part in FractionInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule \'2\' from answer group \'0\' of '
'FractionInput interaction will never be matched because it is '
'made redundant by the above rules'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
self._assert_validation_error(
self.new_exploration, 'Rule \'3\' from answer group \'0\' of '
'FractionInput interaction having rule type HasFractionalPart'
'ExactlyEqualTo will never be matched because it is '
'made redundant by the above rules')
rule_specs.remove(rule_specs[1])
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule \'3\' from answer group \'0\' of '
'FractionInput interaction will never be matched because it is '
'made redundant by the above rules'
):
self.new_exploration.validate(strict=True)
def test_number_with_units_interaction(self) -> None:
"""Tests NumberWithUnits interaction."""
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
self.state, 'NumberWithUnits', content_id_generator)
test_ans_group_for_number_with_units_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': 'IsEquivalentTo',
'inputs': {
'f': {
'type': 'real',
'real': 2,
'fraction': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 0,
'denominator': 1
},
'units': [
{
'unit': 'km',
'exponent': 1
},
{
'unit': 'hr',
'exponent': -1
}
]
}
}
},
{
'rule_type': 'IsEqualTo',
'inputs': {
'f': {
'type': 'real',
'real': 2,
'fraction': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 0,
'denominator': 1
},
'units': [
{
'unit': 'km',
'exponent': 1
},
{
'unit': 'hr',
'exponent': -1
}
]
}
}
},
{
'rule_type': 'IsEquivalentTo',
'inputs': {
'f': {
'type': 'real',
'real': 2,
'fraction': {
'isNegative': False,
'wholeNumber': 0,
'numerator': 0,
'denominator': 1
},
'units': [
{
'unit': 'km',
'exponent': 1
},
{
'unit': 'hr',
'exponent': -1
}
]
}
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
self.state.update_interaction_answer_groups(
test_ans_group_for_number_with_units_interaction)
rule_specs = self.state.interaction.answer_groups[0].rule_specs
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' has '
'rule type equal is coming after rule type equivalent having '
'same value in FractionInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' of '
'NumberWithUnitsInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
def test_multiple_choice_interaction(self) -> None:
"""Tests MultipleChoice interaction."""
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
self.state, 'MultipleChoiceInput', content_id_generator)
test_ans_group_for_multiple_choice_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': 'Equals',
'inputs': {
'x': 0
}
},
{
'rule_type': 'Equals',
'inputs': {
'x': 0
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
self.state.update_interaction_answer_groups(
test_ans_group_for_multiple_choice_interaction)
rule_specs = self.state.interaction.answer_groups[0].rule_specs
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>'),
state_domain.SubtitledHtml('ca_choices_1', '<p>2</p>'),
state_domain.SubtitledHtml('ca_choices_2', '<p>3</p>')
]
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' of '
'MultipleChoiceInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
self.state.interaction.customization_args[
'choices'].value[2].html = '<p>2</p>'
def test_item_selection_choice_interaction(self) -> None:
"""Tests ItemSelection interaction."""
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
self.state, 'ItemSelectionInput', content_id_generator)
self.state.interaction.customization_args[
'minAllowableSelectionCount'].value = 1
self.state.interaction.customization_args[
'maxAllowableSelectionCount'].value = 3
test_ans_group_for_item_selection_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': 'Equals',
'inputs': {
'x': ['ca_choices_0', 'ca_choices_1', 'ca_choices_2']
}
},
{
'rule_type': 'Equals',
'inputs': {
'x': ['ca_choices_0', 'ca_choices_1', 'ca_choices_2']
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
self.state.update_interaction_answer_groups(
test_ans_group_for_item_selection_interaction)
rule_specs = self.state.interaction.answer_groups[0].rule_specs
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>'),
state_domain.SubtitledHtml('ca_choices_1', '<p>2</p>'),
state_domain.SubtitledHtml('ca_choices_2', '<p>3</p>')
]
self.state.interaction.customization_args[
'minAllowableSelectionCount'].value = 3
self.state.interaction.customization_args[
'maxAllowableSelectionCount'].value = 1
self._assert_validation_error(
self.new_exploration, 'Min value which is 3 is greater than max '
'value which is 1 in ItemSelectionInput interaction.'
)
self.state.interaction.customization_args[
'minAllowableSelectionCount'].value = 4
self.state.interaction.customization_args[
'maxAllowableSelectionCount'].value = 4
self._assert_validation_error(
self.new_exploration, 'Number of choices which is 3 is lesser '
'than the min value selection which is 4 in ItemSelectionInput '
'interaction.')
self.state.interaction.customization_args[
'minAllowableSelectionCount'].value = 1
self.state.interaction.customization_args[
'maxAllowableSelectionCount'].value = 3
with self.assertRaisesRegex(
utils.ValidationError, 'The rule 1 of answer group 0 of '
'ItemSelectionInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
self.state.interaction.customization_args[
'minAllowableSelectionCount'].value = 1
self.state.interaction.customization_args[
'maxAllowableSelectionCount'].value = 2
with self.assertRaisesRegex(
utils.ValidationError, 'Selected wrong number of choices in rule '
'\'0\' of answer group \'0\'. 3 were selected, it is either less '
'than 1 or greater than 2 in ItemSelectionInput interaction.'
):
self.new_exploration.validate(strict=True)
self.state.interaction.customization_args[
'minAllowableSelectionCount'].value = 1
self.state.interaction.customization_args[
'maxAllowableSelectionCount'].value = 3
def test_drag_and_drop_interaction(self) -> None:
"""Tests DragAndDrop interaction."""
self.state.recorded_voiceovers.add_content_id_for_voiceover(
'ca_choices_2')
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
self.state, 'DragAndDropSortInput', content_id_generator)
empty_list: List[str] = []
test_ans_group_for_drag_and_drop_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': (
'IsEqualToOrderingWithOneItemAtIncorrectPosition'),
'inputs': {
'x': [
[
'ca_choices_0'
],
[
'ca_choices_1', 'ca_choices_2'
],
[
'ca_choices_3'
]
]
}
},
{
'rule_type': 'IsEqualToOrdering',
'inputs': {
'x': [
[
'ca_choices_0'
],
[
'ca_choices_1', 'ca_choices_2'
],
[
'ca_choices_3'
]
]
}
},
{
'rule_type': 'HasElementXAtPositionY',
'inputs': {
'x': 'ca_choices_0',
'y': 4
}
},
{
'rule_type': 'IsEqualToOrdering',
'inputs': {
'x': [
[
'ca_choices_3'
],
[
'ca_choices_0', 'ca_choices_1', 'ca_choices_2'
]
]
}
},
{
'rule_type': 'HasElementXBeforeElementY',
'inputs': {
'x': 'ca_choices_0',
'y': 'ca_choices_0'
}
},
{
'rule_type': 'IsEqualToOrdering',
'inputs': {
'x': empty_list
}
},
{
'rule_type': 'HasElementXAtPositionY',
'inputs': {
'x': 'ca_choices_0',
'y': 1
}
},
{
'rule_type': 'IsEqualToOrdering',
'inputs': {
'x': [
[
'ca_choices_0'
],
[
'ca_choices_1', 'ca_choices_2'
],
[
'ca_choices_3'
]
]
}
},
{
'rule_type': (
'IsEqualToOrderingWithOneItemAtIncorrectPosition'),
'inputs': {
'x': [
[
'ca_choices_1', 'ca_choices_3'
],
[
'ca_choices_0'
],
[
'ca_choices_2'
]
]
}
},
{
'rule_type': 'IsEqualToOrdering',
'inputs': {
'x': [
[
'ca_choices_1'
],
[
'ca_choices_0'
],
[
'ca_choices_2', 'ca_choices_3'
]
]
}
},
{
'rule_type': 'IsEqualToOrdering',
'inputs': {
'x': [
[
'ca_choices_3'
],
[
'ca_choices_2'
],
[
'ca_choices_1'
],
[
'ca_choices_0'
]
]
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
self.state.interaction.answer_groups = (
test_ans_group_for_drag_and_drop_interaction)
rule_specs = self.state.interaction.answer_groups[0].rule_specs
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>')
]
self._assert_validation_error(
self.new_exploration, (
'There should be atleast 2 values inside DragAndDrop '
'interaction.')
)
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>'),
state_domain.SubtitledHtml('ca_choices_1', '<p> </p>'),
state_domain.SubtitledHtml('ca_choices_2', '')
]
self.state.interaction.customization_args[
'allowMultipleItemsInSamePosition'].value = False
self._assert_validation_error(
self.new_exploration, 'Choices should be non empty.'
)
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>'),
state_domain.SubtitledHtml('ca_choices_1', '<p>2</p>'),
state_domain.SubtitledHtml('ca_choices_2', '')
]
self._assert_validation_error(
self.new_exploration, 'Choices should be non empty.'
)
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>'),
state_domain.SubtitledHtml('ca_choices_1', '<p>2</p>'),
state_domain.SubtitledHtml('ca_choices_2', '<p>2</p>')
]
self._assert_validation_error(
self.new_exploration, 'Choices should be unique.'
)
self.state.interaction.customization_args['choices'].value = [
state_domain.SubtitledHtml('ca_choices_0', '<p>1</p>'),
state_domain.SubtitledHtml('ca_choices_1', '<p>2</p>'),
state_domain.SubtitledHtml('ca_choices_2', '<p>3</p>')
]
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'0\' of answer group \'0\' '
'having rule type - IsEqualToOrderingWithOneItemAtIncorrectPosition'
' should not be there when the multiple items in same position '
'setting is turned off in DragAndDropSortInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'0\' of answer group \'0\' '
'have multiple items at same place when multiple items in same '
'position settings is turned off in DragAndDropSortInput '
'interaction.'
):
self.new_exploration.validate(strict=True)
self.state.interaction.customization_args[
'allowMultipleItemsInSamePosition'].value = True
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'3\' of answer group \'0\', '
'the value 1 and value 2 cannot be same when rule type is '
'HasElementXBeforeElementY of DragAndDropSortInput interaction.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
rule_specs.remove(rule_specs[1])
rule_specs.remove(rule_specs[1])
self._assert_validation_error(
self.new_exploration, 'The rule \'1\'of answer group \'0\', '
'having rule type IsEqualToOrdering should not have empty values.')
rule_specs.remove(rule_specs[1])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'2\' of answer group \'0\' of '
'DragAndDropInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - 1 of answer group 0 '
'will never be match because it is made redundant by the '
'HasElementXAtPositionY rule above.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - 1 of answer group 0 will never '
'be match because it is made redundant by the '
'IsEqualToOrderingWithOneItemAtIncorrectPosition rule above.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[1])
def test_text_interaction(self) -> None:
"""Tests Text interaction."""
self.state.recorded_voiceovers.add_content_id_for_voiceover(
'feedback_0')
self.state.recorded_voiceovers.add_content_id_for_voiceover(
'rule_input_27')
self.state.recorded_voiceovers.add_content_id_for_voiceover(
'ca_choices_0')
self.state.recorded_voiceovers.add_content_id_for_voiceover(
'ca_choices_1')
self.state.recorded_voiceovers.add_content_id_for_voiceover(
'ca_choices_2')
content_id_generator = translation_domain.ContentIdGenerator()
self.set_interaction_for_state(
self.state, 'TextInput', content_id_generator)
test_ans_group_for_text_interaction = [
state_domain.AnswerGroup.from_dict({
'rule_specs': [
{
'rule_type': 'Contains',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'hello',
'abc',
'def'
]
}
}
},
{
'rule_type': 'Contains',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'helloooooo'
]
}
}
},
{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'exci'
]
}
}
},
{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'excitement'
]
}
}
},
{
'rule_type': 'Contains',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'he'
]
}
}
},
{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'hello'
]
}
}
},
{
'rule_type': 'Contains',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'he'
]
}
}
},
{
'rule_type': 'Equals',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'hello'
]
}
}
},
{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'he'
]
}
}
},
{
'rule_type': 'Equals',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'hello'
]
}
}
},
{
'rule_type': 'Equals',
'inputs': {
'x': {
'contentId': 'rule_input_27',
'normalizedStrSet': [
'hello'
]
}
}
}
],
'outcome': {
'dest': 'EXP_1_STATE_1',
'feedback': {
'content_id': 'feedback_0',
'html': '<p>good</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest_if_really_stuck': None
},
'training_data': [],
'tagged_skill_misconception_id': None
})
]
self.state.interaction.answer_groups = (
test_ans_group_for_text_interaction)
rule_specs = self.state.interaction.answer_groups[0].rule_specs
self.state.interaction.customization_args['rows'].value = 15
with self.assertRaisesRegex(
utils.ValidationError, 'Rows value in Text interaction should '
'be between 1 and 10.'
):
self.new_exploration.validate()
self.state.interaction.customization_args['rows'].value = 5
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - \'1\' of answer group - \'0\' '
'having rule type \'Contains\' will never be matched because it '
'is made redundant by the above \'contains\' rule.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - \'1\' of answer group - \'0\' '
'having rule type \'StartsWith\' will never be matched because it '
'is made redundant by the above \'StartsWith\' rule.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - \'1\' of answer group - \'0\' '
'having rule type \'StartsWith\' will never be matched because it '
'is made redundant by the above \'contains\' rule.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - \'1\' of answer group - \'0\' '
'having rule type \'Equals\' will never be matched because it '
'is made redundant by the above \'contains\' rule.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'Rule - \'1\' of answer group - \'0\' '
'having rule type \'Equals\' will never be matched because it '
'is made redundant by the above \'StartsWith\' rule.'
):
self.new_exploration.validate(strict=True)
rule_specs.remove(rule_specs[0])
with self.assertRaisesRegex(
utils.ValidationError, 'The rule \'1\' of answer group \'0\' of '
'TextInput interaction is already present.'
):
self.new_exploration.validate(strict=True)
# TODO(bhenning): The validation tests below should be split into separate
# unit tests. Also, all validation errors should be covered in the tests.
def test_validation(self) -> None:
"""Test validation of explorations."""
exploration = exp_domain.Exploration.create_default_exploration('eid')
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
exploration.init_state_name = ''
exploration.states = {}
exploration.title = 'Hello #'
self._assert_validation_error(exploration, 'Invalid character #')
exploration.title = 'Title'
exploration.category = 'Category'
# Note: If '/' ever becomes a valid state name, ensure that the rule
# editor frontend tenplate is fixed -- it currently uses '/' as a
# sentinel for an invalid state name.
bad_state = state_domain.State.create_default_state(
'/',
content_id_generator.generate(
translation_domain.ContentType.CONTENT),
content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
)
exploration.states = {'/': bad_state}
self._assert_validation_error(
exploration, 'Invalid character / in a state name')
new_state = state_domain.State.create_default_state(
'ABC',
content_id_generator.generate(
translation_domain.ContentType.CONTENT),
content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME))
self.set_interaction_for_state(
new_state, 'TextInput', content_id_generator)
second_state = state_domain.State.create_default_state(
'BCD',
content_id_generator.generate(
translation_domain.ContentType.CONTENT),
content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME))
self.set_interaction_for_state(
second_state, 'TextInput', content_id_generator)
# The 'states' property must be a non-empty dict of states.
exploration.states = {}
self._assert_validation_error(
exploration, 'exploration has no states')
exploration.states = {'A string #': new_state}
self._assert_validation_error(
exploration, 'Invalid character # in a state name')
exploration.states = {'A string _': new_state}
self._assert_validation_error(
exploration, 'Invalid character _ in a state name')
exploration.states = {
'ABC': new_state,
'BCD': second_state
}
self._assert_validation_error(
exploration, 'has no initial state name')
exploration.init_state_name = 'initname'
self._assert_validation_error(
exploration,
r'There is no state in \[\'ABC\'\, \'BCD\'\] corresponding to '
'the exploration\'s initial state name initname.')
# Test whether a default outcome to a non-existing state is invalid.
exploration.states = {
exploration.init_state_name: new_state,
'BCD': second_state
}
exploration.update_next_content_id_index(
content_id_generator.next_content_id_index)
self._assert_validation_error(
exploration, 'destination ABC is not a valid')
# Restore a valid exploration.
init_state = exploration.states[exploration.init_state_name]
default_outcome = init_state.interaction.default_outcome
# Ruling out the possibility of None for mypy type checking.
assert default_outcome is not None
default_outcome.dest = exploration.init_state_name
init_state.update_interaction_default_outcome(default_outcome)
init_state.update_card_is_checkpoint(True)
exploration.validate()
# Ensure an invalid destination can also be detected for answer groups.
# Note: The state must keep its default_outcome, otherwise it will
# trigger a validation error for non-terminal states needing to have a
# default outcome. To validate the outcome of the answer group, this
# default outcome must point to a valid state.
init_state = exploration.states[exploration.init_state_name]
default_outcome = init_state.interaction.default_outcome
# Ruling out the possibility of None for mypy type checking.
assert default_outcome is not None
default_outcome.dest = exploration.init_state_name
old_answer_groups: List[state_domain.AnswerGroupDict] = [
{
'outcome': {
'dest': exploration.init_state_name,
'dest_if_really_stuck': None,
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None
},
'rule_specs': [{
'inputs': {
'x': {
'contentId': 'rule_input_Equals',
'normalizedStrSet': ['Test']
}
},
'rule_type': 'Contains'
}],
'training_data': [],
'tagged_skill_misconception_id': None
}
]
new_answer_groups = [
state_domain.AnswerGroup.from_dict(answer_group)
for answer_group in old_answer_groups
]
init_state.update_interaction_answer_groups(new_answer_groups)
exploration.validate()
interaction = init_state.interaction
answer_groups = interaction.answer_groups
answer_group = answer_groups[0]
default_outcome.dest_if_really_stuck = 'ABD'
self._assert_validation_error(
exploration, 'The destination for the stuck learner '
'ABD is not a valid state')
default_outcome.dest_if_really_stuck = None
answer_group.outcome.dest = 'DEF'
self._assert_validation_error(
exploration, 'destination DEF is not a valid')
answer_group.outcome.dest = exploration.init_state_name
answer_group.outcome.dest_if_really_stuck = 'XYZ'
self._assert_validation_error(
exploration, 'The destination for the stuck learner '
'XYZ is not a valid state')
answer_group.outcome.dest_if_really_stuck = None
# Restore a valid exploration.
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
new_answer_groups = [
state_domain.AnswerGroup.from_dict(answer_groups)
for answer_groups in old_answer_groups
]
init_state.update_interaction_answer_groups(new_answer_groups)
answer_groups = interaction.answer_groups
answer_group = answer_groups[0]
answer_group.outcome.dest = exploration.init_state_name
exploration.validate()
# Validate RuleSpec.
rule_spec = answer_group.rule_specs[0]
rule_spec.inputs = {}
self._assert_validation_error(
exploration, 'RuleSpec \'Contains\' is missing inputs')
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
rule_spec.inputs = 'Inputs string' # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected inputs to be a dict')
rule_spec.inputs = {'x': 'Test'}
rule_spec.rule_type = 'FakeRuleType'
self._assert_validation_error(exploration, 'Unrecognized rule type')
rule_spec.inputs = {'x': {
'contentId': 'rule_input_Equals',
'normalizedStrSet': 15
}}
rule_spec.rule_type = 'Contains'
with self.assertRaisesRegex(
AssertionError, 'Expected list, received 15'
):
exploration.validate()
self.set_interaction_for_state(
exploration.states[exploration.init_state_name],
'PencilCodeEditor', content_id_generator)
temp_rule = old_answer_groups[0]['rule_specs'][0]
old_answer_groups[0]['rule_specs'][0] = {
'rule_type': 'ErrorContains',
'inputs': {'x': '{{ExampleParam}}'}
}
new_answer_groups = [
state_domain.AnswerGroup.from_dict(answer_group)
for answer_group in old_answer_groups
]
init_state.update_interaction_answer_groups(new_answer_groups)
old_answer_groups[0]['rule_specs'][0] = temp_rule
self._assert_validation_error(
exploration,
'RuleSpec \'ErrorContains\' has an input with name \'x\' which '
'refers to an unknown parameter within the exploration: '
'ExampleParam')
# Restore a valid exploration.
exploration.param_specs['ExampleParam'] = param_domain.ParamSpec(
'UnicodeString')
exploration.validate()
# Validate Outcome.
outcome = init_state.interaction.answer_groups[0].outcome
destination = exploration.init_state_name
outcome.dest = None
self._assert_validation_error(
exploration, 'Every outcome should have a destination.')
outcome.dest = destination
default_outcome = init_state.interaction.default_outcome
# Ruling out the possibility of None for mypy type checking.
assert default_outcome is not None
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
default_outcome.dest_if_really_stuck = 20 # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected dest_if_really_stuck to be a string')
default_outcome.dest_if_really_stuck = None
# Try setting the outcome destination to something other than a string.
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.dest = 15 # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected outcome dest to be a string')
outcome.dest = destination
outcome.feedback = state_domain.SubtitledHtml('feedback_1', '')
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.labelled_as_correct = 'hello' # type: ignore[assignment]
self._assert_validation_error(
exploration, 'The "labelled_as_correct" field should be a boolean')
# Test that labelled_as_correct must be False for self-loops, and that
# this causes a strict validation failure but not a normal validation
# failure.
outcome.labelled_as_correct = True
with self.assertRaisesRegex(
Exception, 'is labelled correct but is a self-loop.'
):
exploration.validate(strict=True)
exploration.validate()
outcome.labelled_as_correct = False
exploration.validate()
# Try setting the outcome destination if stuck to something other
# than a string.
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.dest_if_really_stuck = 30 # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected dest_if_really_stuck to be a string')
outcome.dest_if_really_stuck = 'BCD'
outcome.dest = 'BCD'
# Test that no destination for the stuck learner is specified when
# the outcome is labelled correct.
outcome.labelled_as_correct = True
with self.assertRaisesRegex(
Exception, 'The outcome for the state is labelled '
'correct but a destination for the stuck learner '
'is specified.'
):
exploration.validate(strict=True)
exploration.validate()
outcome.labelled_as_correct = False
exploration.validate()
outcome.dest = destination
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.param_changes = 'Changes' # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected outcome param_changes to be a list')
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.param_changes = [param_domain.ParamChange(
0, 'generator_id', {})] # type: ignore[arg-type]
self._assert_validation_error(
exploration,
'Expected param_change name to be a string, received 0')
outcome.param_changes = []
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.refresher_exploration_id = 12345 # type: ignore[assignment]
self._assert_validation_error(
exploration,
'Expected outcome refresher_exploration_id to be a string')
outcome.refresher_exploration_id = None
exploration.validate()
outcome.refresher_exploration_id = 'valid_string'
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
outcome.missing_prerequisite_skill_id = 12345 # type: ignore[assignment]
self._assert_validation_error(
exploration,
'Expected outcome missing_prerequisite_skill_id to be a string')
outcome.missing_prerequisite_skill_id = None
exploration.validate()
outcome.missing_prerequisite_skill_id = 'valid_string'
exploration.validate()
# Test that refresher_exploration_id must be None for non-self-loops.
new_state_name = 'New state'
exploration.add_states([new_state_name])
outcome.dest = new_state_name
outcome.refresher_exploration_id = 'another_string'
self._assert_validation_error(
exploration,
'has a refresher exploration ID, but is not a self-loop')
outcome.refresher_exploration_id = None
exploration.validate()
exploration.delete_state(new_state_name)
# Validate InteractionInstance.
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
interaction.id = 15 # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected interaction id to be a string')
interaction.id = 'SomeInteractionTypeThatDoesNotExist'
self._assert_validation_error(exploration, 'Invalid interaction id')
interaction.id = 'PencilCodeEditor'
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
new_answer_groups = [
state_domain.AnswerGroup.from_dict(answer_group)
for answer_group in old_answer_groups
]
init_state.update_interaction_answer_groups(new_answer_groups)
valid_text_input_cust_args = init_state.interaction.customization_args
rule_spec.inputs = {'x': {
'contentId': 'rule_input_Equals',
'normalizedStrSet': ['Test']
}}
rule_spec.rule_type = 'Contains'
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
interaction.customization_args = [] # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected customization args to be a dict')
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
interaction.customization_args = {15: ''} # type: ignore[dict-item]
self._assert_validation_error(
exploration,
(
'Expected customization arg value to be a '
'InteractionCustomizationArg'
)
)
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
interaction.customization_args = {
15: state_domain.InteractionCustomizationArg('', { # type: ignore[dict-item, no-untyped-call]
'type': 'unicode'
})
}
self._assert_validation_error(
exploration, 'Invalid customization arg name')
interaction.customization_args = valid_text_input_cust_args
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
interaction.answer_groups = {} # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected answer groups to be a list')
new_answer_groups = [
state_domain.AnswerGroup.from_dict(answer_group)
for answer_group in old_answer_groups
]
init_state.update_interaction_answer_groups(new_answer_groups)
self.set_interaction_for_state(
init_state, 'EndExploration', content_id_generator)
self._assert_validation_error(
exploration,
'Terminal interactions must not have a default outcome.')
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
init_state.update_interaction_default_outcome(None)
self._assert_validation_error(
exploration,
'Non-terminal interactions must have a default outcome.')
self.set_interaction_for_state(
init_state, 'EndExploration', content_id_generator
)
init_state.interaction.answer_groups = answer_groups
self._assert_validation_error(
exploration,
'Terminal interactions must not have any answer groups.')
init_state.interaction.answer_groups = []
self.set_interaction_for_state(
init_state, 'Continue', content_id_generator)
init_state.interaction.answer_groups = answer_groups
init_state.update_interaction_default_outcome(default_outcome)
self._assert_validation_error(
exploration,
'Linear interactions must not have any answer groups.')
exploration.update_next_content_id_index(
content_id_generator.next_content_id_index)
# A terminal interaction without a default outcome or answer group is
# valid. This resets the exploration back to a valid state.
init_state.interaction.answer_groups = []
exploration.validate()
# Restore a valid exploration.
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
init_state.update_interaction_answer_groups(answer_groups)
init_state.update_interaction_default_outcome(default_outcome)
exploration.update_next_content_id_index(
content_id_generator.next_content_id_index)
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
interaction.hints = {} # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected hints to be a list')
interaction.hints = []
# Validate AnswerGroup.
state_answer_group = state_domain.AnswerGroup(
state_domain.Outcome(
exploration.init_state_name, None, state_domain.SubtitledHtml(
'feedback_1', 'Feedback'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_Contains',
'normalizedStrSet': ['Test']
}
})
],
[],
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally
# test wrong inputs that we can normally catch by typing.
1 # type: ignore[arg-type]
)
init_state.update_interaction_answer_groups([state_answer_group])
self._assert_validation_error(
exploration,
'Expected tagged skill misconception id to be None, received 1')
with self.assertRaisesRegex(
Exception,
'Expected tagged skill misconception id to be None, received 1'
):
exploration.init_state.validate(
exploration.param_specs,
allow_null_interaction=False,
tagged_skill_misconception_id_required=False)
state_answer_group = state_domain.AnswerGroup(
state_domain.Outcome(
exploration.init_state_name, None, state_domain.SubtitledHtml(
'feedback_1', 'Feedback'),
False, [], None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_Contains',
'normalizedStrSet': ['Test']
}
})
],
[],
'invalid_tagged_skill_misconception_id'
)
init_state.update_interaction_answer_groups([state_answer_group])
self._assert_validation_error(
exploration,
'Expected tagged skill misconception id to be None, received '
'invalid_tagged_skill_misconception_id')
with self.assertRaisesRegex(
Exception,
'Expected tagged skill misconception id to be None, received '
'invalid_tagged_skill_misconception_id'
):
exploration.init_state.validate(
exploration.param_specs,
allow_null_interaction=False,
tagged_skill_misconception_id_required=False)
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
init_state.interaction.answer_groups[0].rule_specs = {} # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected answer group rules to be a list')
first_answer_group = init_state.interaction.answer_groups[0]
first_answer_group.tagged_skill_misconception_id = None
first_answer_group.rule_specs = []
self._assert_validation_error(
exploration,
'There must be at least one rule for each answer group.')
with self.assertRaisesRegex(
Exception,
'There must be at least one rule for each answer group.'
):
exploration.init_state.validate(
exploration.param_specs,
allow_null_interaction=False,
tagged_skill_misconception_id_required=False)
exploration.states = {
exploration.init_state_name: (
state_domain.State.create_default_state(
exploration.init_state_name,
content_id_generator.generate(
translation_domain.ContentType.CONTENT),
content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME),
is_initial_state=True))
}
self.set_interaction_for_state(
exploration.states[exploration.init_state_name], 'TextInput',
content_id_generator)
exploration.update_next_content_id_index(
content_id_generator.next_content_id_index)
exploration.validate()
exploration.language_code = 'fake_code'
self._assert_validation_error(exploration, 'Invalid language_code')
exploration.language_code = 'English'
self._assert_validation_error(exploration, 'Invalid language_code')
exploration.language_code = 'en'
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
exploration.param_specs = 'A string' # type: ignore[assignment]
self._assert_validation_error(exploration, 'param_specs to be a dict')
exploration.param_specs = {
'@': param_domain.ParamSpec.from_dict({
'obj_type': 'UnicodeString'
})
}
self._assert_validation_error(
exploration, 'Only parameter names with characters')
exploration.param_specs = {
'notAParamSpec': param_domain.ParamSpec.from_dict(
{'obj_type': 'UnicodeString'})
}
exploration.validate()
def test_tag_validation(self) -> None:
"""Test validation of exploration tags."""
exploration = exp_domain.Exploration.create_default_exploration('eid')
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
exploration.objective = 'Objective'
init_state = exploration.states[exploration.init_state_name]
self.set_interaction_for_state(
init_state, 'EndExploration', content_id_generator)
init_state.update_interaction_default_outcome(None)
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
exploration.tags = 'this should be a list' # type: ignore[assignment]
self._assert_validation_error(
exploration, 'Expected \'tags\' to be a list')
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
exploration.tags = [123] # type: ignore[list-item]
self._assert_validation_error(exploration, 'to be a string')
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
exploration.tags = ['abc', 123] # type: ignore[list-item]
self._assert_validation_error(exploration, 'to be a string')
exploration.tags = ['']
self._assert_validation_error(exploration, 'Tags should be non-empty')
exploration.tags = ['123']
self._assert_validation_error(
exploration, 'should only contain lowercase letters and spaces')
exploration.tags = ['ABC']
self._assert_validation_error(
exploration, 'should only contain lowercase letters and spaces')
exploration.tags = [' a b']
self._assert_validation_error(
exploration, 'Tags should not start or end with whitespace')
exploration.tags = ['a b ']
self._assert_validation_error(
exploration, 'Tags should not start or end with whitespace')
exploration.tags = ['a b']
self._assert_validation_error(
exploration, 'Adjacent whitespace in tags should be collapsed')
exploration.tags = ['abc', 'abc']
self._assert_validation_error(
exploration, 'Some tags duplicate each other')
exploration.tags = ['computer science', 'analysis', 'a b c']
exploration.validate()
def test_title_category_and_objective_validation(self) -> None:
"""Test that titles, categories and objectives are validated only in
'strict' mode.
"""
self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration = exp_fetchers.get_exploration_by_id('exp_id')
exploration.validate()
with self.assertRaisesRegex(
utils.ValidationError, 'title must be specified'
):
exploration.validate(strict=True)
exploration.title = 'A title'
with self.assertRaisesRegex(
utils.ValidationError, 'category must be specified'
):
exploration.validate(strict=True)
exploration.category = 'A category'
with self.assertRaisesRegex(
utils.ValidationError, 'objective must be specified'
):
exploration.validate(strict=True)
exploration.objective = 'An objective'
exploration.validate(strict=True)
def test_get_trainable_states_dict(self) -> None:
"""Test the get_trainable_states_dict() method."""
exp_id = 'exp_id1'
test_exp_filepath = os.path.join(
feconf.TESTS_DATA_DIR, 'string_classifier_test.yaml')
yaml_content = utils.get_file_contents(test_exp_filepath)
assets_list: List[Tuple[str, bytes]] = []
exp_services.save_new_exploration_from_yaml_and_assets(
feconf.SYSTEM_COMMITTER_ID, yaml_content, exp_id,
assets_list)
exploration_model = exp_models.ExplorationModel.get(
exp_id, strict=True)
old_states = exp_fetchers.get_exploration_from_model(
exploration_model).states
exploration = exp_fetchers.get_exploration_by_id(exp_id)
# Rename a state to add it in unchanged answer group.
exploration.rename_state('Home', 'Renamed state')
change_list = [exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'Home',
'new_state_name': 'Renamed state'
})]
expected_dict = {
'state_names_with_changed_answer_groups': [],
'state_names_with_unchanged_answer_groups': ['Renamed state']
}
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
actual_dict = exploration.get_trainable_states_dict(
old_states, exp_versions_diff)
self.assertEqual(actual_dict, expected_dict)
# Modify answer groups to trigger change in answer groups.
state = exploration.states['Renamed state']
exploration.states['Renamed state'].interaction.answer_groups.insert(
3, state.interaction.answer_groups[3])
answer_groups = []
for answer_group in state.interaction.answer_groups:
answer_groups.append(answer_group.to_dict())
change_list = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Renamed state',
'property_name': 'answer_groups',
'new_value': answer_groups
})]
expected_dict = {
'state_names_with_changed_answer_groups': ['Renamed state'],
'state_names_with_unchanged_answer_groups': []
}
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
actual_dict = exploration.get_trainable_states_dict(
old_states, exp_versions_diff)
self.assertEqual(actual_dict, expected_dict)
# Add new state to trigger change in answer groups.
exploration.add_states(['New state'])
exploration.states['New state'] = copy.deepcopy(
exploration.states['Renamed state'])
change_list = [exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'New state',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
})]
expected_dict = {
'state_names_with_changed_answer_groups': [
'Renamed state', 'New state'],
'state_names_with_unchanged_answer_groups': []
}
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
actual_dict = exploration.get_trainable_states_dict(
old_states, exp_versions_diff)
self.assertEqual(actual_dict, expected_dict)
# Delete state.
exploration.delete_state('New state')
change_list = [exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'New state'
})]
expected_dict = {
'state_names_with_changed_answer_groups': ['Renamed state'],
'state_names_with_unchanged_answer_groups': []
}
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
actual_dict = exploration.get_trainable_states_dict(
old_states, exp_versions_diff)
self.assertEqual(actual_dict, expected_dict)
# Test addition and multiple renames.
exploration.add_states(['New state'])
exploration.states['New state'] = copy.deepcopy(
exploration.states['Renamed state'])
exploration.rename_state('New state', 'New state2')
exploration.rename_state('New state2', 'New state3')
change_list = [exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'New state',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'New state',
'new_state_name': 'New state2'
}), exp_domain.ExplorationChange({
'cmd': 'rename_state',
'old_state_name': 'New state2',
'new_state_name': 'New state3'
})]
expected_dict = {
'state_names_with_changed_answer_groups': [
'Renamed state', 'New state3'
],
'state_names_with_unchanged_answer_groups': []
}
exp_versions_diff = exp_domain.ExplorationVersionsDiff(change_list)
actual_dict = exploration.get_trainable_states_dict(
old_states, exp_versions_diff)
self.assertEqual(actual_dict, expected_dict)
def test_get_metadata(self) -> None:
exploration = exp_domain.Exploration.create_default_exploration('0')
actual_metadata_dict = exploration.get_metadata().to_dict()
expected_metadata_dict = {
'title': exploration.title,
'category': exploration.category,
'objective': exploration.objective,
'language_code': exploration.language_code,
'tags': exploration.tags,
'blurb': exploration.blurb,
'author_notes': exploration.author_notes,
'states_schema_version': exploration.states_schema_version,
'init_state_name': exploration.init_state_name,
'param_specs': {},
'param_changes': [],
'auto_tts_enabled': exploration.auto_tts_enabled,
'edits_allowed': exploration.edits_allowed
}
self.assertEqual(actual_metadata_dict, expected_metadata_dict)
def test_get_content_with_correct_state_name_returns_html(self) -> None:
exploration = exp_domain.Exploration.create_default_exploration('0')
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
init_state = exploration.states[exploration.init_state_name]
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
hints_list = [
state_domain.Hint(
state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
)
]
init_state.update_interaction_hints(hints_list)
self.assertEqual(
exploration.get_content_html(exploration.init_state_name, 'hint_1'),
'<p>hint one</p>')
hints_list[0].hint_content.html = '<p>Changed hint one</p>'
init_state.update_interaction_hints(hints_list)
self.assertEqual(
exploration.get_content_html(exploration.init_state_name, 'hint_1'),
'<p>Changed hint one</p>')
def test_get_content_with_incorrect_state_name_raise_error(self) -> None:
exploration = exp_domain.Exploration.create_default_exploration('0')
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
init_state = exploration.states[exploration.init_state_name]
self.set_interaction_for_state(
init_state, 'TextInput', content_id_generator)
hints_list = [
state_domain.Hint(
state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
)
]
init_state.update_interaction_hints(hints_list)
self.assertEqual(
exploration.get_content_html(exploration.init_state_name, 'hint_1'),
'<p>hint one</p>')
with self.assertRaisesRegex(
ValueError, 'State Invalid state does not exist'):
exploration.get_content_html('Invalid state', 'hint_1')
def test_is_demo_property(self) -> None:
"""Test the is_demo property."""
demo = exp_domain.Exploration.create_default_exploration('0')
self.assertEqual(demo.is_demo, True)
notdemo1 = exp_domain.Exploration.create_default_exploration('a')
self.assertEqual(notdemo1.is_demo, False)
notdemo2 = exp_domain.Exploration.create_default_exploration('abcd')
self.assertEqual(notdemo2.is_demo, False)
def test_has_state_name(self) -> None:
"""Test for has_state_name."""
demo = exp_domain.Exploration.create_default_exploration('0')
state_names = list(demo.states.keys())
self.assertEqual(state_names, ['Introduction'])
self.assertEqual(demo.has_state_name('Introduction'), True)
self.assertEqual(demo.has_state_name('Fake state name'), False)
def test_get_interaction_id_by_state_name(self) -> None:
"""Test for get_interaction_id_by_state_name."""
demo = exp_domain.Exploration.create_default_exploration('0')
self.assertEqual(
demo.get_interaction_id_by_state_name('Introduction'), None)
def test_exploration_export_import(self) -> None:
"""Test that to_dict and from_dict preserve all data within an
exploration.
"""
demo = exp_domain.Exploration.create_default_exploration('0')
demo_dict = demo.to_dict()
exp_from_dict = exp_domain.Exploration.from_dict(demo_dict)
self.assertEqual(exp_from_dict.to_dict(), demo_dict)
def test_interaction_with_none_id_is_not_terminal(self) -> None:
"""Test that an interaction with an id of None leads to is_terminal
being false.
"""
# Default exploration has a default interaction with an ID of None.
demo = exp_domain.Exploration.create_default_exploration('0')
init_state = demo.states[feconf.DEFAULT_INIT_STATE_NAME]
self.assertFalse(init_state.interaction.is_terminal)
def test_cannot_create_demo_exp_with_invalid_param_changes(self) -> None:
demo_exp = exp_domain.Exploration.create_default_exploration('0')
content_id_generator = translation_domain.ContentIdGenerator(
demo_exp.next_content_id_index
)
demo_dict = demo_exp.to_dict()
new_state = state_domain.State.create_default_state(
'new_state_name',
content_id_generator.generate(
translation_domain.ContentType.CONTENT
),
content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME
))
new_state.param_changes = [param_domain.ParamChange.from_dict({
'customization_args': {
'list_of_values': ['1', '2'], 'parse_with_jinja': False
},
'name': 'myParam',
'generator_id': 'RandomSelector'
})]
demo_dict['states']['new_state_name'] = new_state.to_dict()
demo_dict['param_specs'] = {
'ParamSpec': {'obj_type': 'UnicodeString'}
}
with self.assertRaisesRegex(
Exception,
'Parameter myParam was used in a state but not '
'declared in the exploration param_specs.'):
exp_domain.Exploration.from_dict(demo_dict)
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_category(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.category = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected category to be a string, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_objective(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.objective = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected objective to be a string, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_blurb(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.blurb = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected blurb to be a string, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_language_code(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.language_code = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected language_code to be a string, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_author_notes(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.author_notes = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected author_notes to be a string, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_states(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.states = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected states to be a dict, received 1'):
exploration.validate()
def test_validate_exploration_outcome_dest(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
# Ruling out the possibility of None for mypy type checking.
assert exploration.init_state.interaction.default_outcome is not None
exploration.init_state.interaction.default_outcome.dest = None
with self.assertRaisesRegex(
Exception, 'Every outcome should have a destination.'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_outcome_dest_type(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
# Ruling out the possibility of None for mypy type checking.
assert exploration.init_state.interaction.default_outcome is not None
exploration.init_state.interaction.default_outcome.dest = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected outcome dest to be a string, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_states_schema_version(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.states_schema_version = None # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'This exploration has no states schema version.'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_auto_tts_enabled(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.auto_tts_enabled = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected auto_tts_enabled to be a bool, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_next_content_id_index(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.next_content_id_index = '5' # type: ignore[assignment]
with self.assertRaisesRegex(
Exception,
'Expected next_content_id_index to be an int, received 5'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_edits_allowed(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.edits_allowed = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception,
'Expected edits_allowed to be a bool, received 1'):
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validate_exploration_param_specs(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.param_specs = {
1: param_domain.ParamSpec.from_dict( # type: ignore[dict-item]
{'obj_type': 'UnicodeString'})
}
with self.assertRaisesRegex(
Exception, 'Expected parameter name to be a string, received 1'):
exploration.validate()
def test_validate_exploration_param_changes_type(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
exploration.param_changes = 1 # type: ignore[assignment]
with self.assertRaisesRegex(
Exception, 'Expected param_changes to be a list, received 1'):
exploration.validate()
def test_validate_exploration_param_name(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.param_changes = [param_domain.ParamChange.from_dict({
'customization_args': {
'list_of_values': ['1', '2'], 'parse_with_jinja': False
},
'name': 'invalid',
'generator_id': 'RandomSelector'
})]
with self.assertRaisesRegex(
Exception,
'No parameter named \'invalid\' exists in this '
'exploration'):
exploration.validate()
def test_validate_exploration_reserved_param_name(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.param_changes = [param_domain.ParamChange.from_dict({
'customization_args': {
'list_of_values': ['1', '2'], 'parse_with_jinja': False
},
'name': 'all',
'generator_id': 'RandomSelector'
})]
with self.assertRaisesRegex(
Exception,
'The exploration-level parameter with name \'all\' is '
'reserved. Please choose a different name.'):
exploration.validate()
def test_validate_exploration_is_non_self_loop(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
exploration.add_states(['DEF'])
default_outcome = state_domain.Outcome(
'DEF', None, state_domain.SubtitledHtml(
'default_outcome', '<p>Default outcome for state1</p>'),
False, [], 'refresher_exploration_id', None,
)
exploration.init_state.update_interaction_default_outcome(
default_outcome
)
with self.assertRaisesRegex(
Exception,
'The default outcome for state Introduction has a refresher '
'exploration ID, but is not a self-loop.'):
exploration.validate()
def test_validate_exploration_answer_group_parameter(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='', category='',
objective='', end_state_name='End')
exploration.validate()
param_changes = [param_domain.ParamChange(
'ParamChange', 'RandomSelector', {
'list_of_values': ['1', '2'], 'parse_with_jinja': False
}
)]
state_answer_group = state_domain.AnswerGroup(
state_domain.Outcome(
exploration.init_state_name, None, state_domain.SubtitledHtml(
'feedback_1', 'Feedback'),
False, param_changes, None, None),
[
state_domain.RuleSpec(
'Contains',
{
'x':
{
'contentId': 'rule_input_Equals',
'normalizedStrSet': ['Test']
}
})
],
[],
None
)
exploration.init_state.update_interaction_answer_groups(
[state_answer_group])
with self.assertRaisesRegex(
Exception,
'The parameter ParamChange was used in an answer group, '
'but it does not exist in this exploration'):
exploration.validate()
def test_verify_all_states_reachable(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'owner_id')
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
exploration.validate()
exploration.add_states(['End', 'Stuck State'])
end_state = exploration.states['End']
init_state = exploration.states['Introduction']
stuck_state = exploration.states['Stuck State']
state_default_outcome = state_domain.Outcome(
'Introduction', 'Stuck State', state_domain.SubtitledHtml(
'default_outcome_1', '<p>Default outcome for State1</p>'),
False, [], None, None
)
init_state.update_interaction_default_outcome(state_default_outcome)
self.set_interaction_for_state(
stuck_state, 'TextInput', content_id_generator)
self.set_interaction_for_state(
end_state, 'EndExploration', content_id_generator)
end_state.update_interaction_default_outcome(None)
with self.assertRaisesRegex(
Exception,
'Please fix the following issues before saving this exploration: '
'1. The following states are not reachable from the initial state: '
'End 2. It is impossible to complete the exploration from the '
'following states: Introduction, Stuck State'):
exploration.validate(strict=True)
def test_update_init_state_name_with_invalid_state(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='title', category='category',
objective='objective', end_state_name='End')
exploration.update_init_state_name('End')
self.assertEqual(exploration.init_state_name, 'End')
with self.assertRaisesRegex(
Exception,
'Invalid new initial state name: invalid_state;'):
exploration.update_init_state_name('invalid_state')
def test_rename_state_with_invalid_state(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='title', category='category',
objective='objective', end_state_name='End')
self.assertTrue(exploration.states.get('End'))
self.assertFalse(exploration.states.get('new state name'))
exploration.rename_state('End', 'new state name')
self.assertFalse(exploration.states.get('End'))
self.assertTrue(exploration.states.get('new state name'))
with self.assertRaisesRegex(
Exception, 'State invalid_state does not exist'):
exploration.rename_state('invalid_state', 'new state name')
def test_default_outcome_is_labelled_incorrect_for_self_loop(self) -> None:
exploration = self.save_new_valid_exploration(
'exp_id', 'user@example.com', title='title', category='category',
objective='objective', end_state_name='End')
exploration.validate(strict=True)
# Ruling out the possibility of None for mypy type checking.
assert (
exploration.init_state.interaction.default_outcome is not None
)
(
exploration.init_state.interaction.default_outcome
.labelled_as_correct) = True
(
exploration.init_state.interaction.default_outcome
.dest) = exploration.init_state_name
with self.assertRaisesRegex(
Exception,
'The default outcome for state Introduction is labelled '
'correct but is a self-loop'):
exploration.validate(strict=True)
def test_serialize_and_deserialize_returns_unchanged_exploration(
self
) -> None:
"""Checks that serializing and then deserializing a default exploration
works as intended by leaving the exploration unchanged.
"""
exploration = exp_domain.Exploration.create_default_exploration('eid')
self.assertEqual(
exploration.to_dict(),
exp_domain.Exploration.deserialize(
exploration.serialize()).to_dict())
def test_get_all_translatable_content_for_exp(self) -> None:
"""Get all translatable fields from exploration."""
exploration = exp_domain.Exploration.create_default_exploration(
'exp_id')
exploration.add_states(['State1'])
state = exploration.states['State1']
state_content_dict: state_domain.SubtitledHtmlDict = {
'content_id': 'content_0',
'html': '<p>state content html</p>'
}
state_answer_group = [state_domain.AnswerGroup(
state_domain.Outcome(
exploration.init_state_name, None, state_domain.SubtitledHtml(
'feedback_1', '<p>state outcome html</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Equals', {
'x': {
'contentId': 'rule_input_Equals',
'normalizedStrSet': ['Test']
}})
],
[],
None
)]
state_default_outcome = state_domain.Outcome(
'State1', None, state_domain.SubtitledHtml(
'default_outcome', '<p>Default outcome for State1</p>'),
False, [], None, None
)
state_hint_list = [
state_domain.Hint(
state_domain.SubtitledHtml(
'hint_1', '<p>Hello, this is html1 for state1</p>'
)
),
state_domain.Hint(
state_domain.SubtitledHtml(
'hint_2', '<p>Hello, this is html2 for state1</p>'
)
),
]
state_solution_dict: state_domain.SolutionDict = {
'answer_is_exclusive': True,
'correct_answer': 'Answer1',
'explanation': {
'content_id': 'solution',
'html': '<p>This is solution for state1</p>'
}
}
state_interaction_cust_args: Dict[
str, Dict[str, Union[state_domain.SubtitledUnicodeDict, int]]
] = {
'placeholder': {
'value': {
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'rows': {'value': 1},
'catchMisspellings': {'value': False}
}
state.update_content(
state_domain.SubtitledHtml.from_dict(state_content_dict))
state.update_interaction_id('TextInput')
state.update_interaction_customization_args(state_interaction_cust_args)
state.update_interaction_answer_groups(
state_answer_group)
state.update_interaction_default_outcome(state_default_outcome)
state.update_interaction_hints(state_hint_list)
# Ruling out the possibility of None for mypy type checking.
assert state.interaction.id is not None
solution = state_domain.Solution.from_dict(
state.interaction.id, state_solution_dict)
state.update_interaction_solution(solution)
translatable_contents = [
translatable_content.content_value
for translatable_content in
exploration.get_all_contents_which_need_translations(
self.dummy_entity_translations).values()
]
self.assertItemsEqual(
translatable_contents,
[
'<p>state outcome html</p>',
'<p>Default outcome for State1</p>',
'<p>Hello, this is html1 for state1</p>',
['Test'],
'<p>Hello, this is html2 for state1</p>',
'<p>This is solution for state1</p>',
'<p>state content html</p>'
])
class ExplorationSummaryTests(test_utils.GenericTestBase):
def setUp(self) -> None:
super().setUp()
self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME)
self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL)
exploration = exp_domain.Exploration.create_default_exploration('eid')
exp_services.save_new_exploration(self.owner_id, exploration)
self.exp_summary = exp_fetchers.get_exploration_summary_by_id('eid')
self.exp_summary.editor_ids = ['editor_id']
self.exp_summary.voice_artist_ids = ['voice_artist_id']
self.exp_summary.viewer_ids = ['viewer_id']
self.exp_summary.contributor_ids = ['contributor_id']
def test_validation_passes_with_valid_properties(self) -> None:
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_title(self) -> None:
self.exp_summary.title = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected title to be a string, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_category(self) -> None:
self.exp_summary.category = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected category to be a string, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_objective(self) -> None:
self.exp_summary.objective = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected objective to be a string, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_language_code(self) -> None:
self.exp_summary.language_code = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected language_code to be a string, received 0'):
self.exp_summary.validate()
def test_validation_fails_with_unallowed_language_code(self) -> None:
self.exp_summary.language_code = 'invalid'
with self.assertRaisesRegex(
utils.ValidationError, 'Invalid language_code: invalid'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_tags(self) -> None:
self.exp_summary.tags = 'tags' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected \'tags\' to be a list, received tags'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_tag_in_tags(self) -> None:
self.exp_summary.tags = ['tag', 2] # type: ignore[list-item]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected each tag in \'tags\' to be a string, received \'2\''):
self.exp_summary.validate()
def test_validation_fails_with_empty_tag_in_tags(self) -> None:
self.exp_summary.tags = ['', 'abc']
with self.assertRaisesRegex(
utils.ValidationError, 'Tags should be non-empty'):
self.exp_summary.validate()
def test_validation_fails_with_unallowed_characters_in_tag(self) -> None:
self.exp_summary.tags = ['123', 'abc']
with self.assertRaisesRegex(
utils.ValidationError, (
'Tags should only contain lowercase '
'letters and spaces, received \'123\'')):
self.exp_summary.validate()
def test_validation_fails_with_whitespace_in_tag_start(self) -> None:
self.exp_summary.tags = [' ab', 'abc']
with self.assertRaisesRegex(
utils.ValidationError,
'Tags should not start or end with whitespace, received \' ab\''):
self.exp_summary.validate()
def test_validation_fails_with_whitespace_in_tag_end(self) -> None:
self.exp_summary.tags = ['ab ', 'abc']
with self.assertRaisesRegex(
utils.ValidationError,
'Tags should not start or end with whitespace, received \'ab \''):
self.exp_summary.validate()
def test_validation_fails_with_adjacent_whitespace_in_tag(self) -> None:
self.exp_summary.tags = ['a b', 'abc']
with self.assertRaisesRegex(
utils.ValidationError, (
'Adjacent whitespace in tags should '
'be collapsed, received \'a b\'')):
self.exp_summary.validate()
def test_validation_fails_with_duplicate_tags(self) -> None:
self.exp_summary.tags = ['abc', 'abc', 'ab']
with self.assertRaisesRegex(
utils.ValidationError, 'Some tags duplicate each other'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_rating_type(self) -> None:
self.exp_summary.ratings = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected ratings to be a dict, received 0'):
self.exp_summary.validate()
def test_validation_fails_with_invalid_rating_keys(self) -> None:
self.exp_summary.ratings = {'1': 0, '10': 1}
with self.assertRaisesRegex(
utils.ValidationError,
'Expected ratings to have keys: 1, 2, 3, 4, 5, received 1, 10'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_value_type_for_ratings(self) -> None:
self.exp_summary.ratings = {'1': 0, '2': 'one', '3': 0, '4': 0, '5': 0} # type: ignore[dict-item]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected value to be int, received one'):
self.exp_summary.validate()
def test_validation_fails_with_invalid_value_for_ratings(self) -> None:
self.exp_summary.ratings = {'1': 0, '2': -1, '3': 0, '4': 0, '5': 0}
with self.assertRaisesRegex(
utils.ValidationError,
'Expected value to be non-negative, received -1'):
self.exp_summary.validate()
def test_validation_passes_with_int_scaled_average_rating(self) -> None:
self.exp_summary.scaled_average_rating = 1
self.exp_summary.validate()
self.assertEqual(self.exp_summary.scaled_average_rating, 1)
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_scaled_average_rating(self) -> None:
self.exp_summary.scaled_average_rating = 'one' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected scaled_average_rating to be float, received one'
):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_status(self) -> None:
self.exp_summary.status = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected status to be string, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_community_owned(self) -> None:
self.exp_summary.community_owned = '1' # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected community_owned to be bool, received 1'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_contributors_summary(self) -> None:
self.exp_summary.contributors_summary = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected contributors_summary to be dict, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_owner_ids_type(self) -> None:
self.exp_summary.owner_ids = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError, 'Expected owner_ids to be list, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_owner_id_in_owner_ids(self) -> None:
self.exp_summary.owner_ids = ['1', 2, '3'] # type: ignore[list-item]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected each id in owner_ids to be string, received 2'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_editor_ids_type(self) -> None:
self.exp_summary.editor_ids = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected editor_ids to be list, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_editor_id_in_editor_ids(
self
) -> None:
self.exp_summary.editor_ids = ['1', 2, '3'] # type: ignore[list-item]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected each id in editor_ids to be string, received 2'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_voice_artist_ids_type(self) -> None:
self.exp_summary.voice_artist_ids = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected voice_artist_ids to be list, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_voice_artist_id_in_voice_artists_ids(
self
) -> None:
self.exp_summary.voice_artist_ids = ['1', 2, '3'] # type: ignore[list-item]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected each id in voice_artist_ids to be string, received 2'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_viewer_ids_type(self) -> None:
self.exp_summary.viewer_ids = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected viewer_ids to be list, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_viewer_id_in_viewer_ids(
self
) -> None:
self.exp_summary.viewer_ids = ['1', 2, '3'] # type: ignore[list-item]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected each id in viewer_ids to be string, received 2'):
self.exp_summary.validate()
def test_validation_fails_with_duplicate_user_role(self) -> None:
self.exp_summary.owner_ids = ['1']
self.exp_summary.editor_ids = ['2', '3']
self.exp_summary.voice_artist_ids = ['4']
self.exp_summary.viewer_ids = ['2']
with self.assertRaisesRegex(
utils.ValidationError, (
'Users should not be assigned to multiple roles at once, '
'received users: 1, 2, 3, 4, 2')
):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_contributor_ids_type(self) -> None:
self.exp_summary.contributor_ids = 0 # type: ignore[assignment]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected contributor_ids to be list, received 0'):
self.exp_summary.validate()
# TODO(#13059): Here we use MyPy ignore because after we fully type
# the codebase we plan to get rid of the tests that intentionally test
# wrong inputs that we can normally catch by typing.
def test_validation_fails_with_invalid_contributor_id_in_contributor_ids(
self
) -> None:
self.exp_summary.contributor_ids = ['1', 2, '3'] # type: ignore[list-item]
with self.assertRaisesRegex(
utils.ValidationError,
'Expected each id in contributor_ids to be string, received 2'):
self.exp_summary.validate()
def test_is_private(self) -> None:
self.assertTrue(self.exp_summary.is_private())
self.exp_summary.status = constants.ACTIVITY_STATUS_PUBLIC
self.assertFalse(self.exp_summary.is_private())
def test_is_solely_owned_by_user_one_owner(self) -> None:
self.assertTrue(self.exp_summary.is_solely_owned_by_user(self.owner_id))
self.assertFalse(self.exp_summary.is_solely_owned_by_user('other_id'))
self.exp_summary.owner_ids = ['other_id']
self.assertFalse(
self.exp_summary.is_solely_owned_by_user(self.owner_id))
self.assertTrue(self.exp_summary.is_solely_owned_by_user('other_id'))
def test_is_solely_owned_by_user_multiple_owners(self) -> None:
self.assertTrue(self.exp_summary.is_solely_owned_by_user(self.owner_id))
self.assertFalse(self.exp_summary.is_solely_owned_by_user('other_id'))
self.exp_summary.owner_ids = [self.owner_id, 'other_id']
self.assertFalse(
self.exp_summary.is_solely_owned_by_user(self.owner_id))
self.assertFalse(self.exp_summary.is_solely_owned_by_user('other_id'))
def test_is_solely_owned_by_user_other_users(self) -> None:
self.assertFalse(self.exp_summary.is_solely_owned_by_user('editor_id'))
self.assertFalse(
self.exp_summary.is_solely_owned_by_user('voice_artist_id'))
self.assertFalse(self.exp_summary.is_solely_owned_by_user('viewer_id'))
self.assertFalse(
self.exp_summary.is_solely_owned_by_user('contributor_id'))
def test_add_new_contribution_for_user_adds_user_to_contributors(
self
) -> None:
self.exp_summary.add_contribution_by_user('user_id')
self.assertIn('user_id', self.exp_summary.contributors_summary)
self.assertEqual(self.exp_summary.contributors_summary['user_id'], 1)
self.assertIn('user_id', self.exp_summary.contributor_ids)
def test_add_new_contribution_for_user_increases_score_in_contributors(
self
) -> None:
self.exp_summary.add_contribution_by_user('user_id')
self.exp_summary.add_contribution_by_user('user_id')
self.assertIn('user_id', self.exp_summary.contributors_summary)
self.assertEqual(self.exp_summary.contributors_summary['user_id'], 2)
def test_add_new_contribution_for_user_does_not_add_system_user(
self
) -> None:
self.exp_summary.add_contribution_by_user(
feconf.SYSTEM_COMMITTER_ID)
self.assertNotIn(
feconf.SYSTEM_COMMITTER_ID, self.exp_summary.contributors_summary)
self.assertNotIn(
feconf.SYSTEM_COMMITTER_ID, self.exp_summary.contributor_ids)
class YamlCreationUnitTests(test_utils.GenericTestBase):
"""Test creation of explorations from YAML files."""
SAMPLE_YAML_CONTENT: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: Category
edits_allowed: true
init_state_name: %s
language_code: en
next_content_id_index: 4
objective: ''
param_changes: []
param_specs: {}
schema_version: %d
states:
%s:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args: {}
default_outcome:
dest: %s
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: null
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_0: {}
default_outcome_1: {}
solicit_answer_details: false
New state:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_2
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args: {}
default_outcome:
dest: New state
dest_if_really_stuck: null
feedback:
content_id: default_outcome_3
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: null
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_2: {}
default_outcome_3: {}
solicit_answer_details: false
states_schema_version: %d
tags: []
title: Title
version: 0
""") % (
feconf.DEFAULT_INIT_STATE_NAME,
exp_domain.Exploration.CURRENT_EXP_SCHEMA_VERSION,
feconf.DEFAULT_INIT_STATE_NAME, feconf.DEFAULT_INIT_STATE_NAME,
feconf.CURRENT_STATE_SCHEMA_VERSION)
YAML_CONTENT_INVALID_SCHEMA_VERSION: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 10000
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 10000
tags: []
title: Title
""")
EXP_ID: Final = 'An exploration_id'
def test_creation_with_invalid_yaml_schema_version(self) -> None:
"""Test that a schema version that is too big is detected."""
with self.assertRaisesRegex(
Exception,
'Sorry, we can only process v46 to v[0-9]+ exploration YAML files '
'at present.'):
exp_domain.Exploration.from_yaml(
'bad_exp', self.YAML_CONTENT_INVALID_SCHEMA_VERSION)
def test_yaml_import_and_export(self) -> None:
"""Test the from_yaml() and to_yaml() methods."""
exploration = exp_domain.Exploration.create_default_exploration(
self.EXP_ID, title='Title', category='Category')
exploration.add_states(['New state'])
self.assertEqual(len(exploration.states), 2)
exploration.validate()
yaml_content = exploration.to_yaml()
self.assertEqual(yaml_content, self.SAMPLE_YAML_CONTENT)
exploration2 = exp_domain.Exploration.from_yaml('exp2', yaml_content)
self.assertEqual(len(exploration2.states), 2)
yaml_content_2 = exploration2.to_yaml()
self.assertEqual(yaml_content_2, yaml_content)
with self.assertRaisesRegex(
Exception, 'Please ensure that you are uploading a YAML text file, '
'not a zip file. The YAML parser returned the following error: '):
exp_domain.Exploration.from_yaml('exp3', 'No_initial_state_name')
with self.assertRaisesRegex(
Exception,
'Please ensure that you are uploading a YAML text file, not a zip'
' file. The YAML parser returned the following error: mapping '
'values are not allowed here'):
exp_domain.Exploration.from_yaml(
'exp4', 'Invalid\ninit_state_name:\nMore stuff')
with self.assertRaisesRegex(
Exception,
'Please ensure that you are uploading a YAML text file, not a zip'
' file. The YAML parser returned the following error: while '
'scanning a simple key'):
exp_domain.Exploration.from_yaml(
'exp4', 'State1:\n(\nInvalid yaml')
class SchemaMigrationMethodsUnitTests(test_utils.GenericTestBase):
"""Tests the presence of appropriate schema migration methods in the
Exploration domain object class.
"""
def test_correct_states_schema_conversion_methods_exist(self) -> None:
"""Test that the right states schema conversion methods exist."""
current_states_schema_version = (
feconf.CURRENT_STATE_SCHEMA_VERSION)
for version_num in range(
feconf.EARLIEST_SUPPORTED_STATE_SCHEMA_VERSION,
current_states_schema_version):
self.assertTrue(hasattr(
exp_domain.Exploration,
'_convert_states_v%s_dict_to_v%s_dict' % (
version_num, version_num + 1)))
self.assertFalse(hasattr(
exp_domain.Exploration,
'_convert_states_v%s_dict_to_v%s_dict' % (
current_states_schema_version,
current_states_schema_version + 1)))
def test_correct_exploration_schema_conversion_methods_exist(self) -> None:
"""Test that the right exploration schema conversion methods exist."""
current_exp_schema_version = (
exp_domain.Exploration.CURRENT_EXP_SCHEMA_VERSION)
for version_num in range(
exp_domain.Exploration.EARLIEST_SUPPORTED_EXP_SCHEMA_VERSION,
current_exp_schema_version):
self.assertTrue(hasattr(
exp_domain.Exploration,
'_convert_v%s_dict_to_v%s_dict' % (
version_num, version_num + 1)))
self.assertFalse(hasattr(
exp_domain.Exploration,
'_convert_v%s_dict_to_v%s_dict' % (
current_exp_schema_version, current_exp_schema_version + 1)))
class SchemaMigrationUnitTests(test_utils.GenericTestBase):
"""Test migration methods for yaml content."""
YAML_CONTENT_V46: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 46
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 41
tags: []
title: Title
""")
YAML_CONTENT_V47: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 47
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 42
tags: []
title: Title
""")
YAML_CONTENT_V48: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 48
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 43
tags: []
title: Title
""")
YAML_CONTENT_V49: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 49
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 44
tags: []
title: Title
""")
YAML_CONTENT_V50: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 50
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 45
tags: []
title: Title
""")
YAML_CONTENT_V51: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 51
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 46
tags: []
title: Title
""")
YAML_CONTENT_V52: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 52
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 47
tags: []
title: Title
""")
YAML_CONTENT_V53: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 53
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- InputString
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_2
unicode_str: ''
rows:
value: 1
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 48
tags: []
title: Title
""")
YAML_CONTENT_V54: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 54
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 6
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 49
tags: []
title: Title
""")
YAML_CONTENT_V55: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 55
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 6
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 50
tags: []
title: Title
""")
YAML_CONTENT_V56: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 56
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 6
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: (untitled state)
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
default_outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 51
tags: []
title: Title
""")
YAML_CONTENT_V58: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 58
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 6
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: (untitled state)
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
catchMisspellings:
value: false
default_outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 53
tags: []
title: Title
""")
YAML_CONTENT_V59: Final = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 6
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: False
default_outcome:
dest: (untitled state)
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_2: {}
content: {}
default_outcome: {}
feedback_1: {}
rule_input_3: {}
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
New state:
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_0
unicode_str: ''
rows:
value: 1
catchMisspellings:
value: false
default_outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_0: {}
content: {}
default_outcome: {}
states_schema_version: 55
tags: []
title: Title
""")
_LATEST_YAML_CONTENT: Final = YAML_CONTENT_V59
def test_load_from_v46_with_item_selection_input_interaction(self) -> None:
"""Tests the migration of ItemSelectionInput rule inputs."""
sample_yaml_content: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 46
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- <p>Choice 1</p>
- <p>Choice 2</p>
- <p>Choice Invalid</p>
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_2
html: <p>Choice 1</p>
- content_id: ca_choices_3
html: <p>Choice 2</p>
maxAllowableSelectionCount:
value: 2
minAllowableSelectionCount:
value: 1
default_outcome:
dest: (untitled state)
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution:
answer_is_exclusive: true
correct_answer:
- <p>Choice 1</p>
explanation:
content_id: solution
html: This is <i>solution</i> for state1
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_2: {}
ca_choices_3: {}
content: {}
default_outcome: {}
feedback_1: {}
solution: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_2: {}
ca_choices_3: {}
content: {}
default_outcome: {}
feedback_1: {}
solution: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 41
tags: []
title: Title
""")
latest_sample_yaml_content: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
next_content_id_index: 7
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: ''
interaction:
answer_groups:
- outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_4
- ca_choices_5
- invalid_content_id
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_4
html: <p>Choice 1</p>
- content_id: ca_choices_5
html: <p>Choice 2</p>
maxAllowableSelectionCount:
value: 3
minAllowableSelectionCount:
value: 1
default_outcome:
dest: (untitled state)
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution:
answer_is_exclusive: true
correct_answer:
- ca_choices_4
explanation:
content_id: solution_3
html: This is <i>solution</i> for state1
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_4: {}
ca_choices_5: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solution_3: {}
solicit_answer_details: false
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_6
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_6: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: Title
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content)
self.assertEqual(exploration.to_yaml(), latest_sample_yaml_content)
def test_load_from_v46_with_drag_and_drop_sort_input_interaction(
self
) -> None:
"""Tests the migration of DragAndDropSortInput rule inputs."""
sample_yaml_content: str = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 46
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups:
- outcome:
dest: END
feedback:
content_id: feedback_1
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - <p>Choice 1</p>
- <p>Choice 2</p>
rule_type: IsEqualToOrdering
- inputs:
x:
- - <p>Choice 1</p>
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x: <p>Choice 1</p>
y: 1
rule_type: HasElementXAtPositionY
- inputs:
x: <p>Choice 1</p>
y: <p>Choice 2</p>
rule_type: HasElementXBeforeElementY
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: true
choices:
value:
- content_id: ca_choices_2
html: <p>Choice 1</p>
- content_id: ca_choices_3
html: <p>Choice 2</p>
default_outcome:
dest: (untitled state)
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution:
answer_is_exclusive: true
correct_answer:
- - <p>Choice 1</p>
- <p>Choice 2</p>
explanation:
content_id: solution
html: This is <i>solution</i> for state1
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_2: {}
ca_choices_3: {}
content: {}
default_outcome: {}
feedback_1: {}
solution: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_2: {}
ca_choices_3: {}
content: {}
default_outcome: {}
feedback_1: {}
solution: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 41
tags: []
title: Title
""")
latest_sample_yaml_content: str = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
next_content_id_index: 7
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: ''
interaction:
answer_groups:
- outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>Correct!</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - ca_choices_4
- ca_choices_5
rule_type: IsEqualToOrdering
- inputs:
x:
- - ca_choices_4
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x: ca_choices_4
y: 1
rule_type: HasElementXAtPositionY
- inputs:
x: ca_choices_4
y: ca_choices_5
rule_type: HasElementXBeforeElementY
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: true
choices:
value:
- content_id: ca_choices_4
html: <p>Choice 1</p>
- content_id: ca_choices_5
html: <p>Choice 2</p>
default_outcome:
dest: (untitled state)
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution:
answer_is_exclusive: true
correct_answer:
- - ca_choices_4
- ca_choices_5
explanation:
content_id: solution_3
html: This is <i>solution</i> for state1
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_4: {}
ca_choices_5: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solution_3: {}
solicit_answer_details: false
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_6
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_6: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: Title
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content)
self.assertEqual(exploration.to_yaml(), latest_sample_yaml_content)
def test_load_from_v46_with_invalid_unicode_written_translations(
self
) -> None:
"""Tests the migration of unicode written translations rule inputs."""
sample_yaml_content: str = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 46
states:
(untitled state):
classifier_model_id: null
content:
content_id: content
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText
unicode_str: Continue
default_outcome:
dest: END
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
next_content_id_index: 4
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText: {}
content: {}
default_outcome: {}
feedback_1: {}
solution: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_buttonText:
bn:
data_format: html
needs_update: false
translation: <p>hello</p>
content: {}
default_outcome: {}
feedback_1: {}
solution: {}
END:
classifier_model_id: null
content:
content_id: content
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 41
tags: []
title: Title
""")
latest_sample_yaml_content: str = (
"""author_notes: ''
auto_tts_enabled: true
blurb: ''
category: Category
edits_allowed: true
init_state_name: (untitled state)
language_code: en
next_content_id_index: 4
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
(untitled state):
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: ''
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_2
unicode_str: Continue
default_outcome:
dest: END
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_2: {}
content_0: {}
default_outcome_1: {}
solicit_answer_details: false
END:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_3
html: <p>Congratulations, you have finished!</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_3: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: Title
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content)
self.assertEqual(exploration.to_yaml(), latest_sample_yaml_content)
def test_fixing_invalid_labeled_as_correct_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests if the answer group's destination is state itself then
`labelled_as_correct` should be false. Migrates the invalid data.
"""
sample_yaml_content_for_lab_as_correct: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>fdfdf</p>
labelled_as_correct: true
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 25.0
rule_type: Equals
- inputs:
x: 25.0
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 7
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
default_outcome: {}
feedback_2: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content:
hi:
data_format: html
translation:
- <p>choicewa</p>
needs_update: false
default_outcome: {}
feedback_2: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_lab_as_correct: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 4
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>fdfdf</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 25.0
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: NumericInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_0: {}
default_outcome_1: {}
feedback_2: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_3
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_3: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_lab_as_correct)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_lab_as_correct)
def test_fixing_of_rte_content_by_migrating_to_v_58(
self
) -> None:
"""Tests the fixing of RTE content data from version less than 58."""
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
sample_yaml_content_for_rte: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: '<p>Content of RTE</p>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;" filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-image caption-with-value="&quot;&quot;" filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;" filepath-with-value="&quot;&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-link text-with-value="&quotLink;&quot;" url-with-value="&quot;mailto:example@example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;Google&quot;" url-with-value="&quot;http://www.google.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;&quot;" url-with-value="&quot;&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link url-with-value="&quot;&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;Link value&quot;" url-with-value="&quot;https://www.example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link url-with-value="&quot;https://www.example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;&quot;" url-with-value="&quot;https://www.example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-math></oppia-noninteractive-math>
<oppia-noninteractive-math math_content-with-value=""></oppia-noninteractive-math>
<oppia-noninteractive-math math_content-with-value="{&quot;svg_filename&quot;:&quot;mathImg_20220923_043725_4riv8t66q8_height_3d205_width_1d784_vertical_1d306.svg&quot;}"></oppia-noninteractive-math>
<oppia-noninteractive-math math_content-with-value="{&quot;raw_latex&quot;:&quot;&quot;,&quot;svg_filename&quot;:&quot;mathImg_20220923_043725_4riv8t66q8_height_3d205_width_1d784_vertical_1d306.svg&quot;}"></oppia-noninteractive-math>
<oppia-noninteractive-math math_content-with-value="{&quot;raw_latex&quot;:&quot;\\frac{x}{y}&quot;,&quot;svg_filename&quot;:&quot;mathImg_20220923_043725_4riv8t66q8_height_3d205_width_1d784_vertical_1d306.svg&quot;}"></oppia-noninteractive-math>
<oppia-noninteractive-math math_content-with-value="{&quot;raw_latex&quot;:&quot;\\frac{x}{y}&quot;,&quot;svg_filename&quot;:&quot;&quot;}"></oppia-noninteractive-math>
<oppia-noninteractive-math math_content-with-value="{&quot;raw_latex&quot;:&quot;\\frac{x}{y}&quot;}"></oppia-noninteractive-math>
<oppia-noninteractive-skillreview skill_id-with-value="&quot;skill id&quot;" text-with-value="&quot;concept card&quot;"></oppia-noninteractive-skillreview>
<oppia-noninteractive-skillreview skill_id-with-value="&quot;&quot;" text-with-value="&quot;concept card&quot;"></oppia-noninteractive-skillreview>
<oppia-noninteractive-skillreview skill_id-with-value="&quot;&quot;" text-with-value="&quot;&quot;"></oppia-noninteractive-skillreview>
<oppia-noninteractive-skillreview skill_id-with-value="&quot;&quot;"></oppia-noninteractive-skillreview>
<oppia-noninteractive-skillreview text-with-value="&quot;concept card&quot;"></oppia-noninteractive-skillreview>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0" start-with-value="0" video_id-with-value="&quot;&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0" start-with-value="0"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="5" start-with-value="10" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0" start-with-value="0" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="&quot;&quot;" end-with-value="&quot;&quot;" start-with-value="&quot;&quot;" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&lt;p&gt;You have opened the collapsible block.&lt;/p&gt;&lt;oppia-noninteractive-video _nghost-ovd-c35=\\&quot;\\&quot; autoplay-with-value=\\&quot;true\\&quot; end-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; ng-version=\\&quot;11.2.14\\&quot; start-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; video_id-with-value=\\&quot;&amp;amp;quot;hfnv-dfbv5h&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-video&gt;&quot;\" heading-with-value=\\"&quot;&quot;\\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&lt;oppia-noninteractive-tabs&gt;&lt;/oppia-noninteractive-tabs&gt;&quot;\" heading-with-value=\"&quot;heading&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&lt;p&gt;You have opened the collapsible block.&lt;/p&gt;&lt;oppia-noninteractive-video _nghost-ovd-c35=\\&quot;\\&quot; autoplay-with-value=\\&quot;true\\&quot; end-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; ng-version=\\&quot;11.2.14\\&quot; start-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; video_id-with-value=\\&quot;&amp;amp;quot;hfnv-dfbv5h&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-video&gt;&quot;\" heading-with-value=\"&quot;heading&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&lt;p&gt;You have opened the collapsible block.&lt;/p&gt;&lt;oppia-noninteractive-video _nghost-ovd-c35=\\&quot;\\&quot; autoplay-with-value=\\&quot;true\\&quot; end-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; ng-version=\\&quot;11.2.14\\&quot; start-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; video_id-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-video&gt;&quot;\" heading-with-value=\"&quot;heading&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&lt;oppia-noninteractive-video _nghost-ovd-c35=\\&quot;\\&quot; autoplay-with-value=\\&quot;true\\&quot; end-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; ng-version=\\&quot;11.2.14\\&quot; start-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; video_id-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-video&gt;&quot;\" heading-with-value=\"&quot;heading&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&lt;p&gt;You have opened the collapsible block.&lt;/p&gt;&lt;oppia-noninteractive-video _nghost-ovd-c35=\\&quot;\\&quot; autoplay-with-value=\\&quot;true\\&quot; end-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; ng-version=\\&quot;11.2.14\\&quot; start-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot; video_id-with-value=\\&quot;&amp;amp;quot;hfnv-dfbv5h&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-video&gt;&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible heading-with-value=\"&quot;heading&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value=\"&quot;&quot;\" heading-with-value=\"&quot;heading&quot;\"></oppia-noninteractive-collapsible>
<oppia-noninteractive-tabs tab_contents-with-value=\"[{&quot;title&quot;:&quot;Title1&quot;,&quot;content&quot;:&quot;&lt;p&gt;Content1&lt;/p&gt;&quot;},{&quot;title&quot;:&quot;Title2&quot;,&quot;content&quot;:&quot;&lt;p&gt;Content2&lt;/p&gt;&lt;oppia-noninteractive-image filepath-with-value=\\&quot;&amp;amp;quot;s7TabImage.png&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-image&gt;&quot;}]\"></oppia-noninteractive-tabs>
<oppia-noninteractive-tabs tab_contents-with-value=\"[{&quot;title&quot;:&quot;Title2&quot;,&quot;content&quot;:&quot;&lt;oppia-noninteractive-image filepath-with-value=\\&quot;&amp;amp;quot;&amp;amp;quot;\\&quot;&gt;&lt;/oppia-noninteractive-image&gt;&quot;}]\"></oppia-noninteractive-tabs>
<oppia-noninteractive-tabs></oppia-noninteractive-tabs>
<oppia-noninteractive-tabs tab_contents-with-value=\"[{&quot;title&quot;:&quot;Title2&quot;,&quot;content&quot;:&quot;&lt;oppia-noninteractive-collapsible&gt;&lt;/oppia-noninteractive-collapsible&gt;&quot;}]\"></oppia-noninteractive-tabs>
<oppia-noninteractive-tabs tab_contents-with-value=\"[]\"></oppia-noninteractive-tabs>
<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&quot;, &quot;title&quot;: &quot;Hint introduction&quot;},
{&quot;content&quot;: &quot;&quot;,
&quot;title&quot;: &quot;Hint #1&quot;}, {&quot;content&quot;:
&quot;&quot;, &quot;title&quot;: &quot;Hint
#2&quot;}]"></oppia-noninteractive-tabs>'
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_0
unicode_str: Continueeeeeeeeeeeeeeeeeeeeeee
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_0:
hi:
filename: default_outcome-hi-en-7hl9iw3az8.mp3
file_size_bytes: 37198
needs_update: false
duration_secs: 2.324875
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_buttonText_0:
hi:
data_format: html
translation: '<p><oppia-noninteractive-image caption-with-value="&quot;&quot;" filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image></p>'
needs_update: false
content: {}
default_outcome: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value:
- id1
- id2
- id3
- id4
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
# pylint: disable=anomalous-backslash-in-string
latest_sample_yaml_content_for_rte: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 4
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: '<p>Content of RTE</p>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;"
filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;"
filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;"
filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image>
<oppia-noninteractive-link text-with-value="&quot;Google&quot;" url-with-value="&quot;https://www.google.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;Link value&quot;"
url-with-value="&quot;https://www.example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;https://www.example.com&quot;"
url-with-value="&quot;https://www.example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-link text-with-value="&quot;https://www.example.com&quot;"
url-with-value="&quot;https://www.example.com&quot;"></oppia-noninteractive-link>
<oppia-noninteractive-math math_content-with-value="{&quot;raw_latex&quot;:&quot;\\frac{x}{y}&quot;,&quot;svg_filename&quot;:&quot;mathImg_20220923_043725_4riv8t66q8_height_3d205_width_1d784_vertical_1d306.svg&quot;}"></oppia-noninteractive-math>
<oppia-noninteractive-skillreview skill_id-with-value="&quot;skill id&quot;"
text-with-value="&quot;concept card&quot;"></oppia-noninteractive-skillreview>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0"
start-with-value="0" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0"
start-with-value="0" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0"
start-with-value="0" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video>
<oppia-noninteractive-video autoplay-with-value="false" end-with-value="0"
start-with-value="0" video_id-with-value="&quot;mhlEfHv-LHo&quot;"></oppia-noninteractive-video> <oppia-noninteractive-collapsible
content-with-value="&quot;&lt;p&gt;You have opened the collapsible
block.&lt;/p&gt;&lt;oppia-noninteractive-video _nghost-ovd-c35=\&quot;\&quot;
autoplay-with-value=\&quot;true\&quot; end-with-value=\&quot;0\&quot;
ng-version=\&quot;11.2.14\&quot; start-with-value=\&quot;0\&quot;
video_id-with-value=\&quot;&amp;amp;quot;hfnv-dfbv5h&amp;amp;quot;\&quot;&gt;&lt;/oppia-noninteractive-video&gt;&quot;"
heading-with-value="&quot;heading&quot;"></oppia-noninteractive-collapsible>
<oppia-noninteractive-collapsible content-with-value="&quot;&lt;p&gt;You
have opened the collapsible block.&lt;/p&gt;&quot;" heading-with-value="&quot;heading&quot;"></oppia-noninteractive-collapsible> <oppia-noninteractive-tabs
tab_contents-with-value="[{&quot;title&quot;: &quot;Title1&quot;,
&quot;content&quot;: &quot;&lt;p&gt;Content1&lt;/p&gt;&quot;},
{&quot;title&quot;: &quot;Title2&quot;, &quot;content&quot;:
&quot;&lt;p&gt;Content2&lt;/p&gt;&lt;oppia-noninteractive-image
alt-with-value=\&quot;&amp;amp;quot;&amp;amp;quot;\&quot;
caption-with-value=\&quot;&amp;amp;quot;&amp;amp;quot;\&quot;
filepath-with-value=\&quot;&amp;amp;quot;s7TabImage.png&amp;amp;quot;\&quot;&gt;&lt;/oppia-noninteractive-image&gt;&quot;}]"></oppia-noninteractive-tabs> '
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_2
unicode_str: Continue
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_2:
hi:
duration_secs: 2.324875
file_size_bytes: 37198
filename: default_outcome-hi-en-7hl9iw3az8.mp3
needs_update: true
content_0: {}
default_outcome_1: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_3
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value:
- id1
- id2
- id3
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_3: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_rte)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_rte)
def test_fixing_invalid_continue_and_end_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid continue and end exploration data
from version less than 58.
"""
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
sample_yaml_content_for_cont_and_end_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Continue and End interaction validation</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_0
unicode_str: Continueeeeeeeeeeeeeeeeeeeeeee
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&quot;, &quot;title&quot;: &quot;Hint introduction&quot;},
{&quot;content&quot;: &quot;&lt;p&gt;A noun is a person,
place, or thing. A noun can also be an animal. &lt;/p&gt;&quot;,
&quot;title&quot;: &quot;Hint #1&quot;}, {&quot;content&quot;:
&quot;&lt;p&gt;One of these words is an animal. Which word
is the noun?&lt;/p&gt;&quot;, &quot;title&quot;: &quot;Hint
#2&quot;}]"></oppia-noninteractive-tabs>'
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_0:
hi:
filename: default_outcome-hi-en-7hl9iw3az8.mp3
file_size_bytes: 37198
needs_update: false
duration_secs: 2.324875
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_buttonText_0:
hi:
data_format: html
translation: <p>choicewa</p>
needs_update: false
content: {}
default_outcome: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value:
- id1
- id2
- id3
- id4
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
latest_sample_yaml_content_for_cont_and_end_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 4
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Continue and End interaction validation</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_2
unicode_str: Continue
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&lt;p&gt;A noun is a person, place, or thing. A noun
can also be an animal. &lt;/p&gt;&quot;, &quot;title&quot;:
&quot;Hint #1&quot;}, {&quot;content&quot;: &quot;&lt;p&gt;One
of these words is an animal. Which word is the noun?&lt;/p&gt;&quot;,
&quot;title&quot;: &quot;Hint #2&quot;}]"></oppia-noninteractive-tabs>'
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_2:
hi:
duration_secs: 2.324875
file_size_bytes: 37198
filename: default_outcome-hi-en-7hl9iw3az8.mp3
needs_update: true
content_0: {}
default_outcome_1: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_3
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value:
- id1
- id2
- id3
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_3: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_cont_and_end_interac_1)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_cont_and_end_interac_1)
sample_yaml_content_for_cont_and_end_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: hi
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Continue and End interaction validation</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_0
unicode_str: Continueeeeeeeeeeeeeeeeeeeeeee
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
next_content_id_index: 1
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_0: {}
content: {}
default_outcome: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_buttonText_0: {}
content: {}
default_outcome: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value:
- id1
- id2
- id3
- id4
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_cont_and_end_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: hi
next_content_id_index: 4
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Continue and End interaction validation</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
buttonText:
value:
content_id: ca_buttonText_2
unicode_str: Continue
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: Continue
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_buttonText_2: {}
content_0: {}
default_outcome_1: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_3
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value:
- id1
- id2
- id3
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_3: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_cont_and_end_interac_2)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_cont_and_end_interac_2)
def test_fixing_invalid_numeric_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid NumericInput interaction exploration
data from version less than 58.
"""
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
sample_yaml_content_for_numeric_interac: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_1
html: <p>fdfdf</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 25.0
rule_type: Equals
- inputs:
x: 25.0
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&quot;, &quot;title&quot;: &quot;Hint introduction&quot;},
{&quot;content&quot;: &quot;&lt;p&gt;A noun is a person,
place, or thing. A noun can also be an animal. &lt;/p&gt;&quot;,
&quot;title&quot;: &quot;Hint #1&quot;}, {&quot;content&quot;:
&quot;&lt;p&gt;One of these words is an animal. Which word
is the noun?&lt;/p&gt;&quot;, &quot;title&quot;: &quot;Hint
#2&quot;}]"></oppia-noninteractive-tabs>'
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
a: 18.0
b: 18.0
rule_type: IsInclusivelyBetween
- inputs:
x: 25.0
rule_type: Equals
- inputs:
tol: -5.0
x: 5.0
rule_type: IsWithinTolerance
- inputs:
a: 30.0
b: 39.0
rule_type: IsInclusivelyBetween
- inputs:
a: 17.0
b: 15.0
rule_type: IsInclusivelyBetween
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_3
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 25.0
rule_type: IsLessThanOrEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_4
html: <p>cc</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 15.0
rule_type: IsLessThanOrEqualTo
- inputs:
x: 10.0
rule_type: Equals
- inputs:
x: 5.0
rule_type: IsLessThan
- inputs:
a: 9.0
b: 5.0
rule_type: IsInclusivelyBetween
- inputs:
tol: 2.0
x: 5.0
rule_type: IsWithinTolerance
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_5
html: <p>cv</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 40.0
rule_type: IsGreaterThanOrEqualTo
- inputs:
x: 50.0
rule_type: Equals
- inputs:
x: 40.0
rule_type: IsGreaterThan
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_6
html: <p>vb</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: string
rule_type: IsLessThanOrEqualTo
- inputs:
x: string
rule_type: Equals
- inputs:
x: string
rule_type: IsLessThan
- inputs:
a: string
b: 9.0
rule_type: IsInclusivelyBetween
- inputs:
tol: string
x: 5.0
rule_type: IsWithinTolerance
- inputs:
x: string
rule_type: IsGreaterThanOrEqualTo
- inputs:
x: string
rule_type: IsGreaterThan
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_7
html: <p>vb</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
tol: string
x: 60.0
rule_type: IsWithinTolerance
- inputs:
a: string
b: 10.0
rule_type: IsInclusivelyBetween
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints:
- hint_content:
content_id: hint
html: '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&quot;, &quot;title&quot;: &quot;Hint introduction&quot;},
{&quot;content&quot;: &quot;&lt;p&gt;A noun is a person,
place, or thing. A noun can also be an animal. &lt;/p&gt;&quot;,
&quot;title&quot;: &quot;Hint #1&quot;}, {&quot;content&quot;:
&quot;&lt;p&gt;One of these words is an animal. Which word
is the noun?&lt;/p&gt;&quot;, &quot;title&quot;: &quot;Hint
#2&quot;}]"></oppia-noninteractive-tabs>'
- hint_content:
content_id: hint_2
html: '<oppia-noninteractive-image alt-with-value="&quot;&quot;"
caption-with-value="&quot;&quot;" filepath-with-value="&quot;&quot;"></oppia-noninteractive-image>'
id: NumericInput
solution: null
linked_skill_id: null
next_content_id_index: 7
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
default_outcome: {}
hint: {}
hint_2: {}
feedback_1: {}
feedback_2: {}
feedback_3: {}
feedback_4: {}
feedback_5: {}
feedback_6: {}
feedback_7: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
default_outcome: {}
hint: {}
hint_2: {}
feedback_1: {}
feedback_2: {}
feedback_3: {}
feedback_4: {}
feedback_5: {}
feedback_6: {}
feedback_7: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
latest_sample_yaml_content_for_numeric_interac: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 7
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&lt;p&gt;A noun is a person, place, or thing. A noun
can also be an animal. &lt;/p&gt;&quot;, &quot;title&quot;:
&quot;Hint #1&quot;}, {&quot;content&quot;: &quot;&lt;p&gt;One
of these words is an animal. Which word is the noun?&lt;/p&gt;&quot;,
&quot;title&quot;: &quot;Hint #2&quot;}]"></oppia-noninteractive-tabs>'
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 18.0
rule_type: Equals
- inputs:
x: 25.0
rule_type: Equals
- inputs:
tol: 5.0
x: 5.0
rule_type: IsWithinTolerance
- inputs:
a: 30.0
b: 39.0
rule_type: IsInclusivelyBetween
- inputs:
a: 15.0
b: 17.0
rule_type: IsInclusivelyBetween
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_3
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 25.0
rule_type: IsLessThanOrEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_4
html: <p>cv</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 40.0
rule_type: IsGreaterThanOrEqualTo
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
requireNonnegativeInput:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints:
- hint_content:
content_id: hint_5
html: '<oppia-noninteractive-tabs tab_contents-with-value="[{&quot;content&quot;:
&quot;&lt;p&gt;A noun is a person, place, or thing. A noun
can also be an animal. &lt;/p&gt;&quot;, &quot;title&quot;:
&quot;Hint #1&quot;}, {&quot;content&quot;: &quot;&lt;p&gt;One
of these words is an animal. Which word is the noun?&lt;/p&gt;&quot;,
&quot;title&quot;: &quot;Hint #2&quot;}]"></oppia-noninteractive-tabs>'
id: NumericInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_0: {}
default_outcome_1: {}
feedback_2: {}
feedback_3: {}
feedback_4: {}
hint_5: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_6
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_6: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_numeric_interac)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_numeric_interac)
def test_fixing_invalid_fraction_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid FractionInput interaction exploration
data from version less than 58.
"""
sample_yaml_content_for_fraction_interac: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_8
html: <p>jj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 17
wholeNumber: 0
rule_type: IsExactlyEqualTo
- inputs:
f:
denominator: 3
isNegative: false
numerator: 17
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_9
html: <p>dfd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 17
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_10
html: <p>hj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 11
wholeNumber: 0
rule_type: IsGreaterThan
- inputs:
f:
denominator: 3
isNegative: false
numerator: 14
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_11
html: <p>hj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 11
wholeNumber: 0
rule_type: IsLessThan
- inputs:
f:
denominator: 3
isNegative: false
numerator: 7
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_12
html: <p>ll</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 3
rule_type: HasDenominatorEqualTo
- inputs:
f:
denominator: 3
isNegative: false
numerator: 11
wholeNumber: 0
rule_type: HasFractionalPartExactlyEqualTo
- inputs:
x: string
rule_type: HasDenominatorEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_13
html: <p>hj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 19
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowImproperFraction:
value: true
allowNonzeroIntegerPart:
value: true
customPlaceholder:
value:
content_id: ca_customPlaceholder_7
unicode_str: ''
requireSimplestForm:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: FractionInput
solution: null
linked_skill_id: null
next_content_id_index: 14
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_customPlaceholder_7: {}
content: {}
default_outcome: {}
feedback_10: {}
feedback_11: {}
feedback_12: {}
feedback_13: {}
feedback_8: {}
feedback_9: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_customPlaceholder_7: {}
content: {}
default_outcome: {}
feedback_10: {}
feedback_11: {}
feedback_12: {}
feedback_13: {}
feedback_8: {}
feedback_9: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_fraction_interac: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 8
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>jj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 17
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_3
html: <p>hj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 11
wholeNumber: 0
rule_type: IsGreaterThan
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_4
html: <p>hj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 11
wholeNumber: 0
rule_type: IsLessThan
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_5
html: <p>ll</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 3
rule_type: HasDenominatorEqualTo
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowImproperFraction:
value: true
allowNonzeroIntegerPart:
value: true
customPlaceholder:
value:
content_id: ca_customPlaceholder_6
unicode_str: ''
requireSimplestForm:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: FractionInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_customPlaceholder_6: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
feedback_3: {}
feedback_4: {}
feedback_5: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_7
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_7: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_fraction_interac)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_fraction_interac)
sample_yaml_content_for_fraction_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_8
html: <p>jj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 3
isNegative: false
numerator: 17
wholeNumber: 0
rule_type: IsExactlyEqualTo
- inputs:
f:
denominator: 17
isNegative: false
numerator: 3
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowImproperFraction:
value: false
allowNonzeroIntegerPart:
value: true
customPlaceholder:
value:
content_id: ca_customPlaceholder_7
unicode_str: ''
requireSimplestForm:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: FractionInput
solution: null
linked_skill_id: null
next_content_id_index: 14
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_customPlaceholder_7: {}
content: {}
default_outcome: {}
feedback_10: {}
feedback_11: {}
feedback_12: {}
feedback_8: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_customPlaceholder_7: {}
content: {}
default_outcome: {}
feedback_10: {}
feedback_11: {}
feedback_12: {}
feedback_8: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_fraction_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 5
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>jj</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
f:
denominator: 17
isNegative: false
numerator: 3
wholeNumber: 0
rule_type: IsExactlyEqualTo
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowImproperFraction:
value: false
allowNonzeroIntegerPart:
value: true
customPlaceholder:
value:
content_id: ca_customPlaceholder_3
unicode_str: ''
requireSimplestForm:
value: false
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: FractionInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_customPlaceholder_3: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_4
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_4: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_fraction_interac_2)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_fraction_interac_2)
def test_fixing_invalid_multiple_choice_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid MultipleChoice interaction exploration
data from version less than 58.
"""
sample_yaml_content_for_multiple_choice_interac: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_17
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 0
rule_type: Equals
- inputs:
x: 0
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_18
html: <p>a</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 2
rule_type: Equals
- inputs:
x: 0
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_19
html: <p>aa</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 3
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_13
html: ''
- content_id: ca_choices_14
html: ''
- content_id: ca_choices_15
html: <p>1</p>
- content_id: ca_choices_16
html: <p>1</p>
- content_id: ca_choices_17
html: <p>Choice 2</p>
showChoicesInShuffledOrder:
value: true
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: MultipleChoiceInput
solution: null
linked_skill_id: null
next_content_id_index: 20
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_13:
hi:
filename: default_outcome-hi-en-7hl9iw3az8.mp3
file_size_bytes: 37198
needs_update: false
duration_secs: 2.324875
ca_choices_14: {}
ca_choices_15: {}
ca_choices_16: {}
ca_choices_17: {}
content: {}
default_outcome: {}
feedback_17: {}
feedback_18: {}
feedback_19: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_13:
hi:
data_format: html
translation: <p>choicewa</p>
needs_update: false
ca_choices_14: {}
ca_choices_15: {}
ca_choices_16: {}
ca_choices_17: {}
content: {}
default_outcome: {}
feedback_17: {}
feedback_18: {}
feedback_19: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_multiple_choice_interac: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 8
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 0
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_3
html: <p>a</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: 2
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_4
html: <p>Choice 1</p>
- content_id: ca_choices_5
html: <p>1</p>
- content_id: ca_choices_6
html: <p>Choice 2</p>
showChoicesInShuffledOrder:
value: true
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: MultipleChoiceInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_4:
hi:
duration_secs: 2.324875
file_size_bytes: 37198
filename: default_outcome-hi-en-7hl9iw3az8.mp3
needs_update: true
ca_choices_5: {}
ca_choices_6: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
feedback_3: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_7
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_7: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_multiple_choice_interac)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_multiple_choice_interac)
def test_fixing_invalid_item_selec_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid ItemSelection interaction exploration
data from version less than 58.
"""
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
sample_yaml_content_for_item_selection_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_24
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_20
- ca_choices_21
rule_type: Equals
- inputs:
x:
- ca_choices_22
rule_type: Equals
- inputs:
x:
- ca_choices_20
rule_type: ContainsAtLeastOneOf
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_25
html: <p>gg</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_20
rule_type: Equals
- inputs:
x:
- ca_choices_20
- ca_choices_21
- ca_choices_22
- ca_choices_23
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_20
html: <p>1<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;" filepath-with-value="&quot;&quot;"></oppia-noninteractive-image></p>
- content_id: ca_choices_21
html: <p>2<oppia-noninteractive-image alt-with-value="&quot;&quot;" caption-with-value="&quot;&quot;" filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image></p>
- content_id: ca_choices_22
html: <p>3</p>
- content_id: ca_choices_23
html: <p>4</p>
maxAllowableSelectionCount:
value: 2
minAllowableSelectionCount:
value: 3
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution:
answer_is_exclusive: true
correct_answer:
- ca_choices_20
explanation:
content_id: solution
html: This is <i>solution</i> for state1
linked_skill_id: null
next_content_id_index: 26
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_20: {}
ca_choices_21: {}
ca_choices_22: {}
ca_choices_23: {}
content: {}
default_outcome: {}
solution: {}
feedback_24: {}
feedback_25: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_20: {}
ca_choices_21: {}
ca_choices_22: {}
ca_choices_23: {}
content: {}
default_outcome: {}
solution: {}
feedback_24: {}
feedback_25: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
# pylint: disable=single-line-pragma
# pylint: disable=line-too-long
latest_sample_yaml_content_for_item_selection_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 10
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_5
- ca_choices_6
rule_type: Equals
- inputs:
x:
- ca_choices_5
rule_type: ContainsAtLeastOneOf
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_3
html: <p>gg</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_5
rule_type: Equals
- inputs:
x:
- ca_choices_5
- ca_choices_6
- ca_choices_7
- ca_choices_8
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_5
html: <p>1</p>
- content_id: ca_choices_6
html: <p>2<oppia-noninteractive-image alt-with-value="&quot;&quot;"
caption-with-value="&quot;&quot;" filepath-with-value="&quot;img_20220923_043536_g7mr3k59oa_height_374_width_490.svg&quot;"></oppia-noninteractive-image></p>
- content_id: ca_choices_7
html: <p>3</p>
- content_id: ca_choices_8
html: <p>4</p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 1
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution:
answer_is_exclusive: true
correct_answer:
- ca_choices_5
explanation:
content_id: solution_4
html: This is <i>solution</i> for state1
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_5: {}
ca_choices_6: {}
ca_choices_7: {}
ca_choices_8: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
feedback_3: {}
solution_4: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_9
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_9: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_item_selection_interac_1)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_item_selection_interac_1)
sample_yaml_content_for_item_selection_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_24
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_20
rule_type: Equals
- inputs:
x:
- ca_choices_22
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_25
html: <p>gg</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_22
rule_type: Equals
- inputs:
x:
- ca_choices_20
rule_type: Equals
- inputs:
x:
- ca_choices_21
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_26
html: <p>gg</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_22
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_27
html: <p>gg</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_23
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_20
html: <p>1</p>
- content_id: ca_choices_21
html: <p>2</p>
- content_id: ca_choices_22
html: <p>3</p>
- content_id: ca_choices_23
html: <p> </p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 2
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution:
answer_is_exclusive: true
correct_answer:
- ca_choices_23
explanation:
content_id: solution
html: This is <i>solution</i> for state1
linked_skill_id: null
next_content_id_index: 28
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_20: {}
ca_choices_21: {}
ca_choices_22: {}
ca_choices_23: {}
content: {}
default_outcome: {}
solution: {}
feedback_24: {}
feedback_25: {}
feedback_26: {}
feedback_27: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_20: {}
ca_choices_21: {}
ca_choices_22: {}
ca_choices_23: {}
content: {}
default_outcome: {}
solution: {}
feedback_24: {}
feedback_25: {}
feedback_26: {}
feedback_27: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
version: 0
""")
latest_sample_yaml_content_for_item_selection_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 7
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>gg</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_5
rule_type: Equals
- inputs:
x:
- ca_choices_3
rule_type: Equals
- inputs:
x:
- ca_choices_4
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_3
html: <p>1</p>
- content_id: ca_choices_4
html: <p>2</p>
- content_id: ca_choices_5
html: <p>3</p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 1
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_3: {}
ca_choices_4: {}
ca_choices_5: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_6
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_6: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_item_selection_interac_2)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_item_selection_interac_2)
sample_yaml_content_for_item_selection_interac_3: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_24
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_23
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_25
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_20
- ca_choices_21
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_20
html: <p>1</p>
- content_id: ca_choices_21
html: <p>2</p>
- content_id: ca_choices_22
html: <p>3</p>
- content_id: ca_choices_23
html: <p>4</p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 2
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution: null
linked_skill_id: null
next_content_id_index: 26
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_20: {}
ca_choices_21: {}
ca_choices_22: {}
ca_choices_23: {}
content: {}
default_outcome: {}
feedback_24: {}
feedback_25: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_20: {}
ca_choices_21: {}
ca_choices_22: {}
ca_choices_23: {}
content: {}
default_outcome: {}
feedback_24: {}
feedback_25: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
version: 0
""")
latest_sample_yaml_content_for_item_selection_interac_3: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 8
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_3
- ca_choices_4
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_3
html: <p>1</p>
- content_id: ca_choices_4
html: <p>2</p>
- content_id: ca_choices_5
html: <p>3</p>
- content_id: ca_choices_6
html: <p>4</p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 2
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_3: {}
ca_choices_4: {}
ca_choices_5: {}
ca_choices_6: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_7
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_7: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_item_selection_interac_3)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_item_selection_interac_3)
sample_yaml_content_for_item_selection_interac_4: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_24
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_20
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_20
html: <p>1</p>
- content_id: ca_choices_21
html: <p>2</p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 3
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution: null
linked_skill_id: null
next_content_id_index: 26
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_20: {}
ca_choices_21: {}
content: {}
default_outcome: {}
feedback_24: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_20: {}
ca_choices_21: {}
content: {}
default_outcome: {}
feedback_24: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
version: 0
""")
latest_sample_yaml_content_for_item_selection_interac_4: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 6
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: <p>dff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- ca_choices_3
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
choices:
value:
- content_id: ca_choices_3
html: <p>1</p>
- content_id: ca_choices_4
html: <p>2</p>
maxAllowableSelectionCount:
value: 4
minAllowableSelectionCount:
value: 1
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: ItemSelectionInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_3: {}
ca_choices_4: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_5
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_5: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_item_selection_interac_4)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_item_selection_interac_4)
def test_fixing_invalid_drag_and_drop_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid DragAndDrop interaction exploration
data from version less than 58.
"""
sample_yaml_content_for_drag_and_drop_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_30
html: <p>as</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - ca_choices_26
- ca_choices_27
- - ca_choices_28
- - ca_choices_29
rule_type: IsEqualToOrdering
- inputs:
x:
- - ca_choices_26
- ca_choices_27
- - ca_choices_28
- - ca_choices_29
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_31
html: <p>ff</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: ca_choices_26
y: ca_choices_26
rule_type: HasElementXBeforeElementY
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_32
html: <p>a</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: []
rule_type: IsEqualToOrdering
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_33
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: ca_choices_27
y: 2
rule_type: HasElementXAtPositionY
- inputs:
x:
- - ca_choices_26
- - ca_choices_27
- - ca_choices_28
- - ca_choices_29
rule_type: IsEqualToOrdering
- inputs:
x:
- - ca_choices_26
- []
- - ca_choices_28
- - ca_choices_29
rule_type: IsEqualToOrdering
- inputs:
x:
- - ca_choices_29
- - ca_choices_28
- - ca_choices_27
- - ca_choices_26
rule_type: IsEqualToOrdering
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_33
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: ca_choices_27
y: 4
rule_type: HasElementXAtPositionY
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: false
choices:
value:
- content_id: ca_choices_26
html: <p>1</p>
- content_id: ca_choices_27
html: <p>2</p>
- content_id: ca_choices_28
html: <p>3</p>
- content_id: ca_choices_29
html: <p>4</p>
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution: null
linked_skill_id: null
next_content_id_index: 34
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_26: {}
ca_choices_27: {}
ca_choices_28: {}
ca_choices_29: {}
content: {}
default_outcome: {}
feedback_30: {}
feedback_31: {}
feedback_32: {}
feedback_33: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_26: {}
ca_choices_27: {}
ca_choices_28: {}
ca_choices_29: {}
content: {}
default_outcome: {}
feedback_30: {}
feedback_31: {}
feedback_32: {}
feedback_33: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_drag_and_drop_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 9
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: ca_choices_5
y: 2
rule_type: HasElementXAtPositionY
- inputs:
x:
- - ca_choices_7
- - ca_choices_6
- - ca_choices_5
- - ca_choices_4
rule_type: IsEqualToOrdering
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_3
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x: ca_choices_5
y: 4
rule_type: HasElementXAtPositionY
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: false
choices:
value:
- content_id: ca_choices_4
html: <p>1</p>
- content_id: ca_choices_5
html: <p>2</p>
- content_id: ca_choices_6
html: <p>3</p>
- content_id: ca_choices_7
html: <p>4</p>
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_4: {}
ca_choices_5: {}
ca_choices_6: {}
ca_choices_7: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
feedback_3: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_8
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_8: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_drag_and_drop_interac_1)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_drag_and_drop_interac_1)
sample_yaml_content_for_drag_and_drop_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_33
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - ca_choices_29
- ca_choices_28
- - ca_choices_27
- - ca_choices_26
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x:
- - ca_choices_26
- - ca_choices_27
- - ca_choices_28
- - ca_choices_29
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x:
- - ca_choices_29
- - ca_choices_27
- ca_choices_28
- - ca_choices_26
rule_type: IsEqualToOrdering
- inputs:
x: ca_choices_27
y: 4
rule_type: HasElementXAtPositionY
- inputs:
x:
- - ca_choices_29
- ca_choices_27
- ca_choices_28
- ca_choices_26
rule_type: IsEqualToOrdering
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: true
choices:
value:
- content_id: ca_choices_26
html: <p>1</p>
- content_id: ca_choices_27
html: <p>2</p>
- content_id: ca_choices_28
html: <p>3</p>
- content_id: ca_choices_29
html: <p>4</p>
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution: null
linked_skill_id: null
next_content_id_index: 34
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_26: {}
ca_choices_27: {}
ca_choices_28: {}
ca_choices_29: {}
content: {}
default_outcome: {}
feedback_33: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_26: {}
ca_choices_27: {}
ca_choices_28: {}
ca_choices_29: {}
content: {}
default_outcome: {}
feedback_33: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_drag_and_drop_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 8
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - ca_choices_6
- ca_choices_5
- - ca_choices_4
- - ca_choices_3
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x:
- - ca_choices_3
- - ca_choices_4
- - ca_choices_5
- - ca_choices_6
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x: ca_choices_4
y: 4
rule_type: HasElementXAtPositionY
- inputs:
x:
- - ca_choices_6
- ca_choices_4
- ca_choices_5
- ca_choices_3
rule_type: IsEqualToOrdering
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: true
choices:
value:
- content_id: ca_choices_3
html: <p>1</p>
- content_id: ca_choices_4
html: <p>2</p>
- content_id: ca_choices_5
html: <p>3</p>
- content_id: ca_choices_6
html: <p>4</p>
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_3: {}
ca_choices_4: {}
ca_choices_5: {}
ca_choices_6: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_7
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_7: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_drag_and_drop_interac_2)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_drag_and_drop_interac_2)
sample_yaml_content_for_drag_and_drop_interac_3: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 57
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_33
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - ca_choices_26
- - ca_choices_27
- - ca_choices_28
- - ca_choices_29
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x:
- - ca_choices_29
- - ca_choices_27
- ca_choices_28
- - ca_choices_26
rule_type: IsEqualToOrdering
- inputs:
x: ca_choices_28
y: ca_choices_26
rule_type: HasElementXBeforeElementY
- inputs:
x: ca_choices_26
y: ca_choices_28
rule_type: HasElementXBeforeElementY
- inputs:
x: ca_choices_27
y: 2
rule_type: HasElementXAtPositionY
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: true
choices:
value:
- content_id: ca_choices_26
html: <p></p>
- content_id: ca_choices_27
html: <p> </p>
- content_id: ca_choices_28
html: <p>1</p>
- content_id: ca_choices_29
html: <p>2</p>
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution:
answer_is_exclusive: true
correct_answer:
- - ca_choices_29
- - ca_choices_27
- ca_choices_28
- - ca_choices_26
explanation:
content_id: solution
html: This is <i>solution</i> for state1
linked_skill_id: null
next_content_id_index: 34
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_26: {}
ca_choices_27: {}
ca_choices_28: {}
ca_choices_29: {}
content: {}
solution: {}
default_outcome: {}
feedback_33: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_choices_26: {}
ca_choices_27: {}
ca_choices_28: {}
ca_choices_29: {}
content: {}
solution: {}
default_outcome: {}
feedback_33: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
version: 0
""")
latest_sample_yaml_content_for_drag_and_drop_interac_3: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 7
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
- - ca_choices_4
- - ca_choices_5
rule_type: IsEqualToOrderingWithOneItemAtIncorrectPosition
- inputs:
x:
- - ca_choices_5
- - ca_choices_4
rule_type: IsEqualToOrdering
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
allowMultipleItemsInSamePosition:
value: true
choices:
value:
- content_id: ca_choices_4
html: <p>1</p>
- content_id: ca_choices_5
html: <p>2</p>
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: DragAndDropSortInput
solution:
answer_is_exclusive: true
correct_answer:
- - ca_choices_5
- - ca_choices_4
explanation:
content_id: solution_3
html: This is <i>solution</i> for state1
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_choices_4: {}
ca_choices_5: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
solution_3: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_6
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_6: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_drag_and_drop_interac_3)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_drag_and_drop_interac_3)
def test_fixing_invalid_text_exp_data_by_migrating_to_v58(
self
) -> None:
"""Tests the migration of invalid TextInput interaction exploration
data from version less than 58.
"""
sample_yaml_content_for_text_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 53
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_35
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_36
normalizedStrSet:
- and
- drop
rule_type: Contains
- inputs:
x:
contentId: rule_input_37
normalizedStrSet:
- Draganddrop
rule_type: Contains
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_38
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_39
normalizedStrSet:
- ze
rule_type: StartsWith
- inputs:
x:
contentId: rule_input_40
normalizedStrSet:
- zebra
rule_type: StartsWith
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_41
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_42
normalizedStrSet:
- he
rule_type: Contains
- inputs:
x:
contentId: rule_input_43
normalizedStrSet:
- hello
rule_type: StartsWith
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_44
html: <p>ssd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_45
normalizedStrSet:
- abc
rule_type: Contains
- inputs:
x:
contentId: rule_input_46
normalizedStrSet:
- abcd
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_47
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_48
normalizedStrSet:
- dog
rule_type: StartsWith
- inputs:
x:
contentId: rule_input_49
normalizedStrSet:
- dogs
rule_type: Equals
- inputs:
x:
contentId: rule_input_50
normalizedStrSet:
- beautiful
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_48
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_51
normalizedStrSet:
- doggies
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_34
unicode_str: ''
rows:
value: 15
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 50
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_34: {}
content: {}
default_outcome: {}
feedback_35: {}
feedback_38: {}
feedback_41: {}
feedback_44: {}
feedback_47: {}
feedback_48: {}
rule_input_36: {}
rule_input_37: {}
rule_input_39: {}
rule_input_40: {}
rule_input_42: {}
rule_input_43: {}
rule_input_45: {}
rule_input_46: {}
rule_input_48: {}
rule_input_49: {}
rule_input_50: {}
rule_input_51: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_34: {}
content: {}
default_outcome: {}
feedback_35: {}
feedback_38: {}
feedback_41: {}
feedback_44: {}
feedback_47: {}
feedback_48: {}
rule_input_36: {}
rule_input_37: {}
rule_input_39: {}
rule_input_40: {}
rule_input_42: {}
rule_input_43: {}
rule_input_45: {}
rule_input_46: {}
rule_input_48: {}
rule_input_49: {}
rule_input_50: {}
rule_input_51: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_text_interac_1: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 15
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- and
- drop
rule_type: Contains
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_4
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_5
normalizedStrSet:
- ze
rule_type: StartsWith
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_6
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_7
normalizedStrSet:
- he
rule_type: Contains
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_8
html: <p>ssd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_9
normalizedStrSet:
- abc
rule_type: Contains
tagged_skill_misconception_id: null
training_data: []
- outcome:
dest: Introduction
dest_if_really_stuck: null
feedback:
content_id: feedback_10
html: <p>sd</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_11
normalizedStrSet:
- dog
rule_type: StartsWith
- inputs:
x:
contentId: rule_input_12
normalizedStrSet:
- beautiful
rule_type: Equals
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
catchMisspellings:
value: false
placeholder:
value:
content_id: ca_placeholder_13
unicode_str: ''
rows:
value: 10
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_13: {}
content_0: {}
default_outcome_1: {}
feedback_10: {}
feedback_2: {}
feedback_4: {}
feedback_6: {}
feedback_8: {}
rule_input_11: {}
rule_input_12: {}
rule_input_3: {}
rule_input_5: {}
rule_input_7: {}
rule_input_9: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_14
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_14: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_text_interac_1)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_text_interac_1)
sample_yaml_content_for_text_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
objective: ''
param_changes: []
param_specs: {}
schema_version: 53
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_35
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_36
normalizedStrSet:
- and
- drop
rule_type: Contains
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
placeholder:
value:
content_id: ca_placeholder_34
unicode_str: ''
rows:
value: 0
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
next_content_id_index: 50
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_34: {}
content: {}
default_outcome: {}
feedback_35: {}
rule_input_36: {}
solicit_answer_details: false
written_translations:
translations_mapping:
ca_placeholder_34: {}
content: {}
default_outcome: {}
feedback_35: {}
rule_input_36: {}
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
next_content_id_index: 0
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content: {}
solicit_answer_details: false
written_translations:
translations_mapping:
content: {}
states_schema_version: 52
tags: []
title: ''
""")
latest_sample_yaml_content_for_text_interac_2: str = (
"""author_notes: ''
auto_tts_enabled: false
blurb: ''
category: ''
edits_allowed: true
init_state_name: Introduction
language_code: en
next_content_id_index: 6
objective: ''
param_changes: []
param_specs: {}
schema_version: 60
states:
Introduction:
card_is_checkpoint: true
classifier_model_id: null
content:
content_id: content_0
html: <p>Numeric interaction validation</p>
interaction:
answer_groups:
- outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: feedback_2
html: ''
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
rule_specs:
- inputs:
x:
contentId: rule_input_3
normalizedStrSet:
- and
- drop
rule_type: Contains
tagged_skill_misconception_id: null
training_data: []
confirmed_unclassified_answers: []
customization_args:
catchMisspellings:
value: false
placeholder:
value:
content_id: ca_placeholder_4
unicode_str: ''
rows:
value: 1
default_outcome:
dest: end
dest_if_really_stuck: null
feedback:
content_id: default_outcome_1
html: <p>df</p>
labelled_as_correct: false
missing_prerequisite_skill_id: null
param_changes: []
refresher_exploration_id: null
hints: []
id: TextInput
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
ca_placeholder_4: {}
content_0: {}
default_outcome_1: {}
feedback_2: {}
rule_input_3: {}
solicit_answer_details: false
end:
card_is_checkpoint: false
classifier_model_id: null
content:
content_id: content_5
html: <p>End interaction</p>
interaction:
answer_groups: []
confirmed_unclassified_answers: []
customization_args:
recommendedExplorationIds:
value: []
default_outcome: null
hints: []
id: EndExploration
solution: null
linked_skill_id: null
param_changes: []
recorded_voiceovers:
voiceovers_mapping:
content_5: {}
solicit_answer_details: false
states_schema_version: 55
tags: []
title: ''
version: 0
""")
exploration = exp_domain.Exploration.from_yaml(
'eid', sample_yaml_content_for_text_interac_2)
self.assertEqual(
exploration.to_yaml(),
latest_sample_yaml_content_for_text_interac_2)
class ConversionUnitTests(test_utils.GenericTestBase):
"""Test conversion methods."""
def test_convert_exploration_to_player_dict(self) -> None:
exp_title = 'Title'
second_state_name = 'first state'
exploration = exp_domain.Exploration.create_default_exploration(
'eid', title=exp_title, category='Category')
exploration.add_states([second_state_name])
def _get_default_state_dict(
content_str: str,
dest_name: str,
is_init_state: bool,
content_id_generator: translation_domain.ContentIdGenerator
) -> state_domain.StateDict:
"""Gets the default state dict of the exploration."""
content_id_for_content = content_id_generator.generate(
translation_domain.ContentType.CONTENT)
content_id_for_default_outcome = content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME)
return {
'linked_skill_id': None,
'classifier_model_id': None,
'content': {
'content_id': content_id_for_content,
'html': content_str,
},
'recorded_voiceovers': {
'voiceovers_mapping': {
content_id_for_content: {},
content_id_for_default_outcome: {}
}
},
'solicit_answer_details': False,
'card_is_checkpoint': is_init_state,
'interaction': {
'answer_groups': [],
'confirmed_unclassified_answers': [],
'customization_args': {},
'default_outcome': {
'dest': dest_name,
'dest_if_really_stuck': None,
'feedback': {
'content_id': content_id_for_default_outcome,
'html': ''
},
'labelled_as_correct': False,
'param_changes': [],
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None
},
'hints': [],
'id': None,
'solution': None,
},
'param_changes': [],
}
content_id_generator = translation_domain.ContentIdGenerator()
self.assertEqual(exploration.to_player_dict(), {
'init_state_name': feconf.DEFAULT_INIT_STATE_NAME,
'title': exp_title,
'objective': feconf.DEFAULT_EXPLORATION_OBJECTIVE,
'states': {
feconf.DEFAULT_INIT_STATE_NAME: _get_default_state_dict(
feconf.DEFAULT_INIT_STATE_CONTENT_STR,
feconf.DEFAULT_INIT_STATE_NAME, True, content_id_generator),
second_state_name: _get_default_state_dict(
'', second_state_name, False, content_id_generator),
},
'param_changes': [],
'param_specs': {},
'language_code': 'en',
'next_content_id_index': content_id_generator.next_content_id_index
})
class StateOperationsUnitTests(test_utils.GenericTestBase):
"""Test methods operating on states."""
def test_delete_state(self) -> None:
"""Test deletion of states."""
exploration = exp_domain.Exploration.create_default_exploration('eid')
exploration.add_states(['first state'])
with self.assertRaisesRegex(
ValueError, 'Cannot delete initial state'
):
exploration.delete_state(exploration.init_state_name)
exploration.add_states(['second state'])
interaction = exploration.states['first state'].interaction
default_outcome_for_first_state = interaction.default_outcome
assert default_outcome_for_first_state is not None
default_outcome_for_first_state.dest_if_really_stuck = 'second state'
exploration.delete_state('second state')
self.assertEqual(
default_outcome_for_first_state.dest_if_really_stuck, 'first state')
with self.assertRaisesRegex(ValueError, 'fake state does not exist'):
exploration.delete_state('fake state')
class HtmlCollectionTests(test_utils.GenericTestBase):
"""Test method to obtain all html strings."""
def test_all_html_strings_are_collected(self) -> None:
exploration = exp_domain.Exploration.create_default_exploration(
'eid', title='title', category='category')
content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
exploration.add_states(['state1', 'state2', 'state3', 'state4'])
state1 = exploration.states['state1']
state2 = exploration.states['state2']
state3 = exploration.states['state3']
state4 = exploration.states['state4']
content1_dict: state_domain.SubtitledHtmlDict = {
'content_id': content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<blockquote>Hello, this is state1</blockquote>'
}
content2_dict: state_domain.SubtitledHtmlDict = {
'content_id': content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<pre>Hello, this is state2</pre>'
}
content3_dict: state_domain.SubtitledHtmlDict = {
'content_id': content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Hello, this is state3</p>'
}
content4_dict: state_domain.SubtitledHtmlDict = {
'content_id': content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Hello, this is state4</p>'
}
state1.update_content(
state_domain.SubtitledHtml.from_dict(content1_dict))
state2.update_content(
state_domain.SubtitledHtml.from_dict(content2_dict))
state3.update_content(
state_domain.SubtitledHtml.from_dict(content3_dict))
state4.update_content(
state_domain.SubtitledHtml.from_dict(content4_dict))
self.set_interaction_for_state(
state1, 'TextInput', content_id_generator)
self.set_interaction_for_state(
state2, 'MultipleChoiceInput', content_id_generator)
self.set_interaction_for_state(
state3, 'ItemSelectionInput', content_id_generator)
self.set_interaction_for_state(
state4, 'DragAndDropSortInput', content_id_generator)
ca_placeholder_value_dict: state_domain.SubtitledUnicodeDict = {
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='placeholder'),
'unicode_str': 'Enter here.'
}
customization_args_dict1: Dict[
str, Dict[str, Union[state_domain.SubtitledUnicodeDict, int]]
] = {
'placeholder': {
'value': ca_placeholder_value_dict
},
'rows': {'value': 1},
'catchMisspellings': {
'value': False
}
}
choices_subtitled_html_dicts: List[state_domain.SubtitledHtmlDict] = [
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value1 for MultipleChoice</p>'
},
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value2 for MultipleChoice</p>'
}
]
customization_args_dict2: Dict[
str, Dict[str, Union[List[state_domain.SubtitledHtmlDict], bool]]
] = {
'choices': {'value': choices_subtitled_html_dicts},
'showChoicesInShuffledOrder': {'value': True}
}
choices_subtitled_html_dicts = [
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value1 for ItemSelection</p>'
},
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value2 for ItemSelection</p>'
},
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value3 for ItemSelection</p>'
}
]
customization_args_dict3: Dict[
str, Dict[str, Union[List[state_domain.SubtitledHtmlDict], int]]
] = {
'choices': {'value': choices_subtitled_html_dicts},
'minAllowableSelectionCount': {'value': 1},
'maxAllowableSelectionCount': {'value': 2}
}
choices_subtitled_html_dicts = [
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value1 for DragAndDropSortInput</p>'
},
{
'content_id': content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='choices'),
'html': '<p>This is value2 for DragAndDropSortInput</p>'
}
]
customization_args_dict4: Dict[
str, Dict[str, Union[List[state_domain.SubtitledHtmlDict], bool]]
] = {
'choices': {'value': choices_subtitled_html_dicts},
'allowMultipleItemsInSamePosition': {'value': True}
}
state1.update_interaction_customization_args(customization_args_dict1)
state2.update_interaction_customization_args(customization_args_dict2)
state3.update_interaction_customization_args(customization_args_dict3)
state4.update_interaction_customization_args(customization_args_dict4)
default_outcome = state_domain.Outcome(
'state2', None, state_domain.SubtitledHtml(
content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME),
'<p>Default outcome for state1</p>'),
False, [], None, None
)
state1.update_interaction_default_outcome(default_outcome)
hint_list2 = [
state_domain.Hint(
state_domain.SubtitledHtml(
content_id_generator.generate(
translation_domain.ContentType.HINT),
'<p>Hello, this is html1 for state2</p>'
)
),
state_domain.Hint(
state_domain.SubtitledHtml(
content_id_generator.generate(
translation_domain.ContentType.HINT),
'<p>Hello, this is html2 for state2</p>'
)
),
]
state2.update_interaction_hints(hint_list2)
solution_dict: state_domain.SolutionDict = {
'answer_is_exclusive': True,
'correct_answer': 'Answer1',
'explanation': {
'content_id': content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>This is solution for state1</p>'
}
}
# Ruling out the possibility of None for mypy type checking.
assert state1.interaction.id is not None
solution = state_domain.Solution.from_dict(
state1.interaction.id, solution_dict)
state1.update_interaction_solution(solution)
state_answer_group_list2 = [
state_domain.AnswerGroup(
state_domain.Outcome(
'state1', None, state_domain.SubtitledHtml(
content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'<p>Outcome2 for state2</p>'
), False, [], None, None),
[
state_domain.RuleSpec(
'Equals',
{
'x': 0
}),
state_domain.RuleSpec(
'Equals',
{
'x': 1
})
],
[],
None),
state_domain.AnswerGroup(
state_domain.Outcome(
'state3', None, state_domain.SubtitledHtml(
content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'<p>Outcome1 for state2</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Equals',
{
'x': 0
})
],
[],
None
)]
state_answer_group_list3 = [state_domain.AnswerGroup(
state_domain.Outcome(
'state1', None, state_domain.SubtitledHtml(
content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'<p>Outcome for state3</p>'),
False, [], None, None),
[
state_domain.RuleSpec(
'Equals',
{
'x': ['ca_choices_0']
}),
state_domain.RuleSpec(
'Equals',
{
'x': ['ca_choices_2']
})
],
[],
None
)]
state2.update_interaction_answer_groups(state_answer_group_list2)
state3.update_interaction_answer_groups(state_answer_group_list3)
expected_html_list = [
'',
'',
'<pre>Hello, this is state2</pre>',
'<p>Outcome1 for state2</p>',
'<p>Outcome2 for state2</p>',
'',
'<p>Hello, this is html1 for state2</p>',
'<p>Hello, this is html2 for state2</p>',
'<p>This is value1 for MultipleChoice</p>',
'<p>This is value2 for MultipleChoice</p>',
'<blockquote>Hello, this is state1</blockquote>',
'<p>Default outcome for state1</p>',
'<p>This is solution for state1</p>',
'<p>Hello, this is state3</p>',
'<p>Outcome for state3</p>',
'',
'<p>This is value1 for ItemSelection</p>',
'<p>This is value2 for ItemSelection</p>',
'<p>This is value3 for ItemSelection</p>',
'<p>Hello, this is state4</p>',
'',
'<p>This is value1 for DragAndDropSortInput</p>',
'<p>This is value2 for DragAndDropSortInput</p>'
]
actual_outcome_list = exploration.get_all_html_content_strings()
self.assertItemsEqual(set(actual_outcome_list), set(expected_html_list))
class ExplorationChangesMergeabilityUnitTests(
exp_services_test.ExplorationServicesUnitTests,
test_utils.EmailTestBase):
"""Test methods related to exploration changes mergeability."""
def setUp(self) -> None:
super().setUp()
exploration = self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
self.content_id_generator = translation_domain.ContentIdGenerator(
exploration.next_content_id_index
)
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
def append_next_content_id_index_change(
self, change_list: List[exp_domain.ExplorationChange]
) -> List[exp_domain.ExplorationChange]:
"""Appends the next_content_id_index change in the change list."""
change_list.append(exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_EXPLORATION_PROPERTY,
'property_name': 'next_content_id_index',
'new_value': self.content_id_generator.next_content_id_index,
'old_value': 0
}))
return change_list
def test_changes_are_mergeable_when_content_changes_do_not_conflict(
self
) -> None:
change_list = [exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_EDIT_EXPLORATION_PROPERTY,
'property_name': 'title',
'new_value': 'First title'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed title.')
test_dict: Dict[str, str] = {}
# Making changes to properties except content.
change_list_2 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': None,
'old_value': 'TextInput'
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': test_dict,
'old_value': {
'placeholder': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': 'Continue',
'old_value': None
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': {
'buttonText': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': 'Continue'
}
}
},
'old_value': test_dict
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Interaction.')
# Changing content of second state.
change_list_3 = [exp_domain.ExplorationChange({
'property_name': 'content',
'state_name': 'End',
'cmd': 'edit_state_property',
'old_value': {
'html': '',
'content_id': 'content_0'
},
'new_value': {
'html': '<p>Congratulations, you have finished!</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT)
}
})]
# Checking that the changes can be applied when
# changing to same version.
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 3, change_list_3)
self.assertEqual(changes_are_mergeable, True)
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, True)
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_3),
'Changed content of End state.')
# Changing content of first state.
change_list_4 = [exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_RENAME_STATE,
'old_state_name': 'Introduction',
'new_state_name': 'Renamed state'
}), exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_RENAME_STATE,
'old_state_name': 'Renamed state',
'new_state_name': 'Renamed state again'
}), exp_domain.ExplorationChange({
'cmd': exp_domain.CMD_RENAME_STATE,
'old_state_name': 'Renamed state again',
'new_state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'property_name': 'content',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'html': '',
'content_id': 'content_0'
},
'new_value': {
'html': '<p>Hello</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT)
}
})]
# Checking for the mergability of the fourth change list.
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_4)
self.assertEqual(changes_are_mergeable, True)
# Checking for the mergability when working on latest version.
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 3, change_list_4)
self.assertEqual(changes_are_mergeable, True)
def test_changes_are_not_mergeable_when_content_changes_conflict(
self
) -> None:
# Making changes to content of the first state.
change_list = [exp_domain.ExplorationChange({
'property_name': 'content',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'html': '',
'content_id': 'content_0'
},
'new_value': {
'html': '<p>Content 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT)
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed Content.')
# Changing content of the same state to check that
# changes are not mergeable.
change_list_2 = [exp_domain.ExplorationChange({
'property_name': 'content',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'html': '',
'content_id': 'content_0'
},
'new_value': {
'html': '<p>Content 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT)
}
})]
# Checking for the mergability of the second change list.
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_2)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_interaction_id_changes_do_not_conflict(
self
) -> None:
# Making changes in the properties which are
# not related to the interaction id.
change_list_2 = [exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>This is the first state.</p>'
},
'state_name': 'Introduction',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
}), exp_domain.ExplorationChange({
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is a first hint.</p>'
}
}],
'state_name': 'Introduction',
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'hints'
}), exp_domain.ExplorationChange({
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is a first hint.</p>'
}
}, {
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is the second hint.</p>'
}
}],
'state_name': 'Introduction',
'old_value': [{
'hint_content': {
'content_id': 'hint_1',
'html': '<p>This is a first hint.</p>'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints'
}), exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Congratulations, you have finished!</p>'
},
'state_name': 'End',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Contents and Hint')
test_dict: Dict[str, str] = {}
# Changes to the properties affected by or affecting
# interaction id and in interaction_id itself.
change_list_3 = [exp_domain.ExplorationChange({
'new_value': None,
'state_name': 'Introduction',
'old_value': 'TextInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'new_value': test_dict,
'state_name': 'Introduction',
'old_value': {
'rows': {
'value': 1
},
'placeholder': {
'value': {
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args'
}), exp_domain.ExplorationChange({
'new_value': 'Continue',
'state_name': 'Introduction',
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'new_value': {
'buttonText': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': 'Continue'
}
}
},
'state_name': 'Introduction',
'old_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_3)
self.assertEqual(changes_are_mergeable, True)
# Creating second exploration to test the scenario
# when changes to same properties are made in two
# different states.
self.save_new_valid_exploration(
self.EXP_1_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_1_ID)
# Using the old change_list_3 here because they already covers
# the changes related to interaction in first state.
exp_services.update_exploration(
self.owner_id, self.EXP_1_ID,
self.append_next_content_id_index_change(change_list_3),
'Changed Interaction')
# Changes related to interaction in the second state
# to check for mergeability.
change_list_4 = [exp_domain.ExplorationChange({
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': None,
'old_value': 'EndExploration',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': test_dict,
'old_value': {
'recommendedExplorationIds': {
'value': []
}
},
'property_name': 'widget_customization_args'
}), exp_domain.ExplorationChange({
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': 'NumericInput',
'old_value': None,
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': {
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'labelled_as_correct': False,
'param_changes': [],
'feedback': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
}
},
'old_value': None,
'property_name': 'default_outcome'
}), exp_domain.ExplorationChange({
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': [{
'outcome': {
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'labelled_as_correct': False,
'param_changes': [],
'feedback': {
'html': '<p>Feedback</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
}
},
'rule_specs': [{
'inputs': {
'x': 60
},
'rule_type': 'IsLessThanOrEqualTo'
}],
'tagged_skill_misconception_id': None,
'training_data': []
}],
'old_value': ['old_value'],
'property_name': 'answer_groups'
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'End',
'property_name': 'solicit_answer_details',
'new_value': True
})]
changes_are_mergeable_1 = exp_services.are_changes_mergeable(
self.EXP_1_ID, 1, change_list_4)
self.assertEqual(changes_are_mergeable_1, True)
def test_changes_are_not_mergeable_when_interaction_id_changes_conflict(
self
) -> None:
test_dict: Dict[str, str] = {}
# Changes to the properties affected by or affecting
# interaction id and in interaction_id itself.
change_list_2 = [exp_domain.ExplorationChange({
'new_value': None,
'state_name': 'Introduction',
'old_value': 'TextInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'new_value': test_dict,
'state_name': 'Introduction',
'old_value': {
'rows': {
'value': 1
},
'placeholder': {
'value': {
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args'
}), exp_domain.ExplorationChange({
'new_value': 'Continue',
'state_name': 'Introduction',
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'new_value': {
'buttonText': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': 'Continue'
}
}
},
'state_name': 'Introduction',
'old_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Contents and Hint')
# Changes to the properties affected by or affecting
# interaction id and in interaction_id itself again
# to check that changes are not mergeable.
change_list_3 = [exp_domain.ExplorationChange({
'new_value': None,
'state_name': 'Introduction',
'old_value': 'TextInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'new_value': test_dict,
'state_name': 'Introduction',
'old_value': {
'rows': {
'value': 1
},
'placeholder': {
'value': {
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args'
}), exp_domain.ExplorationChange({
'new_value': 'Continue',
'state_name': 'Introduction',
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id'
}), exp_domain.ExplorationChange({
'new_value': {
'buttonText': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': 'Continue'
}
}
},
'state_name': 'Introduction',
'old_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args'
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_3)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_customization_args_changes_do_not_conflict( # pylint: disable=line-too-long
self
) -> None:
test_dict: Dict[str, str] = {}
# Changes in the properties which aren't affected by
# customization args or doesn't affects customization_args.
change_list = [exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>This is the first state.</p>'
},
'state_name': 'Introduction',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
}), exp_domain.ExplorationChange({
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is a first hint.</p>'
}
}],
'state_name': 'Introduction',
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'hints'
}), exp_domain.ExplorationChange({
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is a first hint.</p>'
}
}, {
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is the second hint.</p>'
}
}],
'state_name': 'Introduction',
'old_value': [{
'hint_content': {
'content_id': 'hint_1',
'html': '<p>This is a first hint.</p>'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints'
}), exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Congratulations, you have finished!</p>'
},
'state_name': 'End',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed Contents and Hints')
# Changes to the properties affecting customization_args
# or are affected by customization_args in the same state.
# This includes changes related to renaming a state in
# order to check that changes are applied even if states
# are renamed.
change_list_2 = [exp_domain.ExplorationChange({
'cmd': 'rename_state',
'new_state_name': 'Intro-rename',
'old_state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': 'Introduction',
'property_name': 'init_state_name',
'new_value': 'Intro-rename',
'cmd': 'edit_exploration_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': {
'placeholder':
{
'value':
{
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'placeholder':
{
'value':
{
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': 'Placeholder text'
}
},
'rows':
{
'value': 2
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': 'TextInput',
'property_name': 'widget_id',
'new_value': None,
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value':
{
'placeholder':
{
'value':
{
'content_id': 'ca_placeholder_0',
'unicode_str': 'Placeholder text'
}
},
'rows':
{
'value': 2
},
'catchMisspellings': {
'value': False
}
},
'property_name': 'widget_customization_args',
'new_value': test_dict,
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': None,
'property_name': 'widget_id',
'new_value': 'NumericInput',
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value':
{
'requireNonnegativeInput':
{
'value': True
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'requireNonnegativeInput':
{
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': ['old_value'],
'property_name': 'answer_groups',
'new_value':
[
{
'rule_specs':
[
{
'inputs':
{
'x': 50
},
'rule_type': 'IsLessThanOrEqualTo'
}
],
'training_data': [],
'tagged_skill_misconception_id': None,
'outcome':
{
'feedback':
{
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Next</p>'
},
'param_changes': [],
'refresher_exploration_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False
}
}
],
'cmd': 'edit_state_property'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_2)
self.assertEqual(changes_are_mergeable, True)
# Creating second exploration to test the scenario
# when changes to same properties are made in two
# different states.
self.save_new_valid_exploration(
self.EXP_1_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_1_ID)
# Using the old change_list_2 here because they already covers
# the changes related to customization args in first state.
exp_services.update_exploration(
self.owner_id, self.EXP_1_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Interactions and Customization_args in One State')
# Changes to the properties related to the customization args
# in the second state to check for mergeability.
change_list_3 = [exp_domain.ExplorationChange({
'old_value': 'EndExploration',
'state_name': 'End',
'property_name': 'widget_id',
'cmd': 'edit_state_property',
'new_value': None
}), exp_domain.ExplorationChange({
'old_value': {
'recommendedExplorationIds': {
'value': []
}
},
'state_name': 'End',
'property_name': 'widget_customization_args',
'cmd': 'edit_state_property',
'new_value': test_dict
}), exp_domain.ExplorationChange({
'old_value': None,
'state_name': 'End',
'property_name': 'widget_id',
'cmd': 'edit_state_property',
'new_value': 'ItemSelectionInput'
}), exp_domain.ExplorationChange({
'old_value': test_dict,
'state_name': 'End',
'property_name': 'widget_customization_args',
'cmd': 'edit_state_property',
'new_value': {
'minAllowableSelectionCount': {
'value': 1
},
'choices': {
'value': [{
'html': '<p>A</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.RULE)
}, {
'html': '<p>B</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.RULE)
}, {
'html': '<p>C</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.RULE)
}, {
'html': '<p>D</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.RULE)
}]
},
'maxAllowableSelectionCount': {
'value': 1
}
}
}), exp_domain.ExplorationChange({
'old_value': None,
'state_name': 'End',
'property_name': 'default_outcome',
'cmd': 'edit_state_property',
'new_value': {
'refresher_exploration_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'param_changes': [],
'labelled_as_correct': False
}
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'state_name': 'End',
'property_name': 'answer_groups',
'cmd': 'edit_state_property',
'new_value':
[
{
'training_data': [],
'tagged_skill_misconception_id': None,
'outcome':
{
'refresher_exploration_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback':
{
'html': '<p>Good</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'param_changes': [],
'labelled_as_correct': False
},
'rule_specs':
[
{
'rule_type': 'Equals',
'inputs':
{
'x':
[
'ca_choices_1'
]
}
}
]
}
]
})]
changes_are_mergeable_1 = exp_services.are_changes_mergeable(
self.EXP_1_ID, 1, change_list_3)
self.assertEqual(changes_are_mergeable_1, True)
def test_changes_are_not_mergeable_when_customization_args_changes_conflict(
self
) -> None:
test_dict: Dict[str, str] = {}
# Changes in the properties which affected by or affecting
# customization_args.
change_list = [exp_domain.ExplorationChange({
'cmd': 'rename_state',
'new_state_name': 'Intro-rename',
'old_state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': 'Introduction',
'property_name': 'init_state_name',
'new_value': 'Intro-rename',
'cmd': 'edit_exploration_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': {
'placeholder':
{
'value':
{
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'placeholder':
{
'value':
{
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'unicode_str': 'Placeholder text'
}
},
'rows':
{
'value': 2
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': 'TextInput',
'property_name': 'widget_id',
'new_value': None,
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value':
{
'placeholder':
{
'value':
{
'content_id': 'ca_placeholder_0',
'unicode_str': 'Placeholder text'
}
},
'rows':
{
'value': 2
},
'catchMisspellings': {
'value': False
}
},
'property_name': 'widget_customization_args',
'new_value': test_dict,
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': None,
'property_name': 'widget_id',
'new_value': 'NumericInput',
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value':
{
'requireNonnegativeInput':
{
'value': True
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'requireNonnegativeInput':
{
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Intro-rename',
'old_value': ['old_value'],
'property_name': 'answer_groups',
'new_value':
[
{
'rule_specs':
[
{
'inputs':
{
'x': 50
},
'rule_type': 'IsLessThanOrEqualTo'
}
],
'training_data': [],
'tagged_skill_misconception_id': None,
'outcome':
{
'feedback':
{
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Next</p>'
},
'param_changes': [],
'refresher_exploration_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False
}
}
],
'cmd': 'edit_state_property'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed Customization Args and related properties again')
# Changes to the customization_args in same
# state again to check that changes are not mergeable.
change_list_2 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'old_value': {
'placeholder':
{
'value':
{
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'placeholder':
{
'value':
{
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG),
'unicode_str': 'Placeholder text 2.'
}
},
'rows':
{
'value': 2
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property'
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_2)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_answer_groups_changes_do_not_conflict(
self
) -> None:
# Adding answer_groups and solutions to the existing state.
change_list = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'rule_specs': [{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': self.content_id_generator.generate(
translation_domain.ContentType.RULE),
'normalizedStrSet': ['Hello', 'Hola']
}
}
}],
'tagged_skill_misconception_id': None,
'outcome': {
'labelled_as_correct': False,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'refresher_exploration_id': None
},
'training_data': []
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Hint 1.</p>'
}
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'new_value': {
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Explanation.</p>'
},
'answer_is_exclusive': False
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added answer groups and solution')
# Changes to the properties that are not related to
# the answer_groups. These changes are done to check
# when the changes are made in unrelated properties,
# they can be merged easily.
change_list_2 = [exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>This is the first state.</p>'
},
'state_name': 'Introduction',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
}), exp_domain.ExplorationChange({
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}, {
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is a first hint.</p>'
}
}],
'state_name': 'Introduction',
'old_value': [{
'hint_content': {
'content_id': 'hint_3',
'html': '<p>Hint 1.</p>'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints'
}), exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Congratulations, you have finished!</p>'
},
'state_name': 'End',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Contents and Hint')
change_list_3 = [exp_domain.ExplorationChange({
'property_name': 'default_outcome',
'old_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'default_outcome',
'html': ''
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback 1.</p>'
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
}
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, True)
# Changes to the answer_groups and the properties that
# affects or are affected by answer_groups.
change_list_4 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': [
'Hello',
'Hola',
'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': 'solution',
'html': '<p>Explanation.</p>'
}
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}, {
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Oppia', 'GSoC'],
'contentId': 'rule_input_5'
}
},
'rule_type': 'Contains'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Oppia is selected for GSoC.',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': 'solution_5',
'html': '<p>Explanation.</p>'
}
}
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'solicit_answer_details',
'new_value': True
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_4)
self.assertEqual(changes_are_mergeable, True)
# Creating second exploration to test the scenario
# when changes to same properties are made in two
# different states.
self.save_new_valid_exploration(
self.EXP_1_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_1_ID)
# Using the old change_list_2 and change_list_3 here
# because they already covers the changes related to
# the answer_groups in the first state.
exp_services.update_exploration(
self.owner_id, self.EXP_1_ID, change_list_2,
'Added Answer Group and Solution in One state')
exp_services.update_exploration(
self.owner_id, self.EXP_1_ID, change_list_3,
'Changed Answer Groups and Solutions in One State')
test_dict: Dict[str, str] = {}
# Changes to the properties related to the answer_groups
# in the second state to check for mergeability.
change_list_5 = [exp_domain.ExplorationChange({
'old_value': 'EndExploration',
'state_name': 'End',
'property_name': 'widget_id',
'cmd': 'edit_state_property',
'new_value': None
}), exp_domain.ExplorationChange({
'old_value': {
'recommendedExplorationIds': {
'value': []
}
},
'state_name': 'End',
'property_name': 'widget_customization_args',
'cmd': 'edit_state_property',
'new_value': test_dict
}), exp_domain.ExplorationChange({
'old_value': None,
'state_name': 'End',
'property_name': 'widget_id',
'cmd': 'edit_state_property',
'new_value': 'ItemSelectionInput'
}), exp_domain.ExplorationChange({
'old_value': test_dict,
'state_name': 'End',
'property_name': 'widget_customization_args',
'cmd': 'edit_state_property',
'new_value': {
'minAllowableSelectionCount': {
'value': 1
},
'choices': {
'value': [{
'html': '<p>A</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG)
}, {
'html': '<p>B</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG)
}, {
'html': '<p>C</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG)
}, {
'html': '<p>D</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG)
}]
},
'maxAllowableSelectionCount': {
'value': 1
}
}
}), exp_domain.ExplorationChange({
'old_value': None,
'state_name': 'End',
'property_name': 'default_outcome',
'cmd': 'edit_state_property',
'new_value': {
'refresher_exploration_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'param_changes': [],
'labelled_as_correct': False
}
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'state_name': 'End',
'property_name': 'answer_groups',
'cmd': 'edit_state_property',
'new_value': [{
'training_data': [],
'tagged_skill_misconception_id': None,
'outcome': {
'refresher_exploration_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'html': '<p>Good</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'param_changes': [],
'labelled_as_correct': False
},
'rule_specs': [{
'rule_type': 'Equals',
'inputs': {
'x': ['ca_choices_1']
}
}]
}]
})]
changes_are_mergeable_1 = exp_services.are_changes_mergeable(
self.EXP_1_ID, 2, change_list_5)
self.assertEqual(changes_are_mergeable_1, True)
def test_changes_are_not_mergeable_when_answer_groups_changes_conflict(
self
) -> None:
# Adding answer_groups and solutions to the existing state.
change_list = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'rule_specs': [{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_2',
'normalizedStrSet': ['Hello', 'Hola']
}
}
}],
'tagged_skill_misconception_id': None,
'outcome': {
'labelled_as_correct': False,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'refresher_exploration_id': None
},
'training_data': []
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'new_value': {
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
},
'answer_is_exclusive': False
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added answer groups and solution')
# Changes to the answer_groups and the properties that
# affects or are affected by answer_groups.
change_list_2 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': [
'Hello',
'Hola',
'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': 'solution',
'html': '<p>Explanation.</p>'
}
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}, {
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Oppia', 'GSoC'],
'contentId': 'rule_input_5'
}
},
'rule_type': 'Contains'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Oppia is selected for GSoC.',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': 'solution',
'html': '<p>Explanation.</p>'
}
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Answer Groups and related properties')
# Changes to the answer group in same state again
# to check that changes are not mergeable.
change_list_3 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': [
'Hello',
'Hola',
'Hey'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_solutions_changes_do_not_conflict(
self
) -> None:
# Adding new answer_groups and solutions.
change_list = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'rule_specs': [{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_2',
'normalizedStrSet': [
'Hello',
'Hola'
]
}
}
}],
'tagged_skill_misconception_id': None,
'outcome': {
'labelled_as_correct': False,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'refresher_exploration_id': None
},
'training_data': []
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'new_value': {
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
},
'answer_is_exclusive': False
}
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'solicit_answer_details',
'new_value': True
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added answer groups and solution')
# Changes to the properties unrelated to the solutions.
change_list_2 = [exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>This is the first state.</p>'
},
'state_name': 'Introduction',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
}), exp_domain.ExplorationChange({
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}, {
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>This is a first hint.</p>'
}
}],
'state_name': 'Introduction',
'old_value': [{
'hint_content': {
'content_id': 'hint_3',
'html': '<p>Hint 1.</p>'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints'
}), exp_domain.ExplorationChange({
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Congratulations, you have finished!</p>'
},
'state_name': 'End',
'old_value': {
'content_id': 'content_0',
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content'
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'solicit_answer_details',
'new_value': True
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Contents and Hint')
# Changes to the solutions and the properties that affects
# solutions to check for mergeability.
change_list_3 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}, {
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Oppia', 'GSoC'],
'contentId': 'rule_input_5'
}
},
'rule_type': 'Contains'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Oppia is selected for GSoC.',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
}
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'solicit_answer_details',
'new_value': False
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, True)
# Creating second exploration to test the scenario
# when changes to same properties are made in two
# different states.
self.save_new_valid_exploration(
self.EXP_1_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_1_ID)
# Using the old change_list_2 and change_list_3 here
# because they already covers the changes related to
# the solutions in the first state.
exp_services.update_exploration(
self.owner_id, self.EXP_1_ID, change_list_2,
'Added Answer Group and Solution in One state')
exp_services.update_exploration(
self.owner_id, self.EXP_1_ID,
self.append_next_content_id_index_change(change_list_3),
'Changed Answer Groups and Solutions in One State')
test_dict: Dict[str, str] = {}
# Changes to the properties related to the solutions
# in the second state to check for mergeability.
change_list_4 = [exp_domain.ExplorationChange({
'old_value': 'EndExploration',
'new_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': {
'recommendedExplorationIds': {
'value': []
}
},
'new_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': None,
'new_value': 'NumericInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': None,
'new_value': {
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None,
'feedback': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
}
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'new_value': [{
'outcome': {
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None,
'feedback': {
'html': '<p>Good</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
}
},
'training_data': [],
'tagged_skill_misconception_id': None,
'rule_specs': [{
'rule_type': 'IsGreaterThanOrEqualTo',
'inputs': {
'x': 20
}
}]
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'new_value': [{
'hint_content': {
'html': '<p>Hint 1. State 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': None,
'new_value': {
'correct_answer': 30,
'explanation': {
'html': '<p>Explanation.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'answer_is_exclusive': False
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': {
'correct_answer': 30,
'explanation': {
'html': '<p>Explanation.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'answer_is_exclusive': False
},
'new_value': {
'correct_answer': 10,
'explanation': {
'html': '<p>Explanation.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'answer_is_exclusive': False
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'state_name': 'End'
})]
changes_are_mergeable_1 = exp_services.are_changes_mergeable(
self.EXP_1_ID, 2, change_list_4)
self.assertEqual(changes_are_mergeable_1, True)
def test_changes_are_not_mergeable_when_solutions_changes_conflict(
self
) -> None:
# Adding new answer_groups and solutions.
change_list = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'rule_specs': [{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_2',
'normalizedStrSet': [
'Hello',
'Hola'
]
}
}
}],
'tagged_skill_misconception_id': None,
'outcome': {
'labelled_as_correct': False,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'refresher_exploration_id': None
},
'training_data': []
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}]
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'new_value': {
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
},
'answer_is_exclusive': False
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added answer groups and solution')
# Changes to the solutions and the properties that affects
# solutions to check for mergeability.
change_list_2 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': 'solution',
'html': '<p>Explanation.</p>'
}
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}, {
'outcome': {
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Oppia', 'GSoC'],
'contentId': 'rule_input_5'
}
},
'rule_type': 'Contains'
}],
'tagged_skill_misconception_id': None
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': [{
'outcome': {
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'training_data': [],
'rule_specs': [{
'inputs': {
'x': {
'normalizedStrSet': ['Hello', 'Hola', 'Hi'],
'contentId': 'rule_input_2'
}
},
'rule_type': 'StartsWith'
}],
'tagged_skill_misconception_id': None
}]
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Oppia is selected for GSoC.',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hi Aryaman!',
'explanation': {
'content_id': 'solution',
'html': '<p>Explanation.</p>'
}
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed Solutions and affected properties')
# Change to the solution of same state again
# to check that changes are not mergeable.
change_list_3 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': {
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman!',
'explanation': {
'content_id': 'solution',
'html': '<p>Changed Explanation.</p>'
}
}
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_hints_changes_do_not_conflict(
self
) -> None:
change_list = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'property_name': 'hints',
'cmd': 'edit_state_property',
'old_value': ['old_value']
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'explanation': {
'html': '<p>Explanation</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'correct_answer': 'Hello'
},
'property_name': 'solution',
'cmd': 'edit_state_property',
'old_value': None
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added Hint and Solution in Introduction state')
test_dict: Dict[str, str] = {}
# Changes to all state propeties other than the hints.
change_list_2 = [exp_domain.ExplorationChange({
'property_name': 'content',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT)
},
'new_value': {
'html': '<p>Content in Introduction.</p>',
'content_id': 'content_0'
}
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'explanation': {
'html': '<p>Explanation</p>',
'content_id': 'solution'
},
'answer_is_exclusive': False,
'correct_answer': 'Hello'
},
'new_value': {
'explanation': {
'html': '<p>Explanation</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman'
}
}), exp_domain.ExplorationChange({
'property_name': 'widget_id',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': 'TextInput',
'new_value': None
}), exp_domain.ExplorationChange({
'property_name': 'widget_customization_args',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'placeholder': {
'value': {
'content_id': 'ca_placeholder_0',
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'new_value': test_dict
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'explanation': {
'html': '<p>Explanation</p>',
'content_id': 'solution'
},
'answer_is_exclusive': False,
'correct_answer': 'Hello Aryaman'
},
'new_value': None
}), exp_domain.ExplorationChange({
'property_name': 'widget_id',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': None,
'new_value': 'NumericInput'
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'old_value':
{
'requireNonnegativeInput':
{
'value': True
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'requireNonnegativeInput':
{
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'property_name': 'answer_groups',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': ['old_value'],
'new_value': [{
'rule_specs': [{
'inputs': {
'x': 46
},
'rule_type': 'IsLessThanOrEqualTo'
}],
'training_data': [],
'tagged_skill_misconception_id': None,
'outcome': {
'labelled_as_correct': False,
'refresher_exploration_id': None,
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'feedback': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'param_changes': []
}
}]
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': None,
'new_value': {
'explanation': {
'html': '<p>Explanation</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'answer_is_exclusive': False,
'correct_answer': 42
}
}), exp_domain.ExplorationChange({
'property_name': 'content',
'state_name': 'End',
'cmd': 'edit_state_property',
'old_value': {
'html': '',
'content_id': 'content_0'
},
'new_value': {
'html': '<p>Congratulations, you have finished!</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT)
}
}), exp_domain.ExplorationChange({
'property_name': 'title',
'cmd': 'edit_exploration_property',
'old_value': 'A title',
'new_value': 'First Title'
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'old_value': {
'explanation': {
'html': '<p>Explanation</p>',
'content_id': 'solution'
},
'answer_is_exclusive': False,
'correct_answer': 42
},
'new_value': {
'explanation': {
'html': '<p>Explanation</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'answer_is_exclusive': False,
'correct_answer': 40
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Made changes in interaction, contents, solutions, answer_groups in both states') # pylint: disable=line-too-long
# Changes to the old hints and also deleted and added
# new hints to take all the cases to check for mergeability.
change_list_3 = [exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': 'hint_1'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': 'hint_1'
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': 'hint_2'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [{
'hint_content': {
'html': '<p>Changed hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Changed hint 1.</p>',
'content_id': 'hint_1'
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': 'hint_2'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [
{
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}, {
'hint_content': {
'html': '<p>Changed hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}
],
'state_name': 'Introduction'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, True)
def test_changes_are_not_mergeable_when_hints_changes_conflict(
self
) -> None:
change_list = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'property_name': 'hints',
'cmd': 'edit_state_property',
'old_value': ['old_value']
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'explanation': {
'html': '<p>Explanation</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
},
'correct_answer': 'Hello'
},
'property_name': 'solution',
'cmd': 'edit_state_property',
'old_value': None
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added Hint and Solution in Introduction state')
# Changes to the old hints and also deleted and added
# new hints to take all the cases to check for mergeability.
change_list_2 = [exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': 'hint_1'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': 'hint_1'
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [{
'hint_content': {
'html': '<p>Changed hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Changed hint 1.</p>',
'content_id': 'hint_1'
}
}, {
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': 'hint_2'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [
{
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}, {
'hint_content': {
'html': '<p>Changed hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}
],
'state_name': 'Introduction'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changes in the hints again.')
change_list_3 = [exp_domain.ExplorationChange({
'old_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': 'hint_1'
}
}],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [{
'hint_content': {
'html': '<p>Changed Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}],
'state_name': 'Introduction'
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_exploration_properties_changes_do_not_conflict( # pylint: disable=line-too-long
self
) -> None:
test_dict: Dict[str, str] = {}
# Changes to all the properties of both states other than
# exploration properties i.e. title, category, objective etc.
# Also included rename states changes to check that
# renaming states doesn't affect anything.
change_list = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'html': '<p>Content</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'cmd': 'edit_state_property',
'property_name': 'content',
'old_value': {
'html': '',
'content_id': 'content_0'
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [
{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}
],
'cmd': 'edit_state_property',
'property_name': 'hints',
'old_value': ['old_value']
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'old_value': 'TextInput'
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'old_value': {
'rows': {
'value': 1
},
'placeholder': {
'value': {
'unicode_str': '',
'content_id': 'ca_placeholder_0'
}
},
'catchMisspellings': {
'value': False
}
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': 'NumericInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'old_value': None
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'old_value':
{
'requireNonnegativeInput':
{
'value': True
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'requireNonnegativeInput':
{
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [
{
'outcome': {
'refresher_exploration_id': None,
'feedback': {
'html': '<p>Good.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': []
},
'training_data': [],
'rule_specs': [
{
'inputs': {
'x': 50
},
'rule_type': 'IsLessThanOrEqualTo'
}
],
'tagged_skill_misconception_id': None
}
],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': ['old_value']
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'refresher_exploration_id': None,
'feedback': {
'html': '<p>Try Again.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': []
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'old_value': {
'refresher_exploration_id': None,
'feedback': {
'html': '',
'content_id': 'default_outcome'
},
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [
]
}
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'refresher_exploration_id': None,
'feedback': {
'html': '<p>Try Again.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False,
'dest': 'Introduction',
'dest_if_really_stuck': None,
'param_changes': [
]
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'old_value': {
'refresher_exploration_id': None,
'feedback': {
'html': '<p>Try Again.</p>',
'content_id': 'default_outcome'
},
'missing_prerequisite_skill_id': None,
'labelled_as_correct': False,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [
]
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Made changes in interaction, contents, solutions, answer_groups in introduction state.') # pylint: disable=line-too-long
# Changes to properties of second state.
change_list_2 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': {
'answer_is_exclusive': False,
'correct_answer': 25,
'explanation': {
'html': '<p>Explanation.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION)
}
},
'cmd': 'edit_state_property',
'property_name': 'solution',
'old_value': None
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'new_value': [
{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
},
{
'hint_content': {
'html': '<p>Hint 2.</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
}
}
],
'cmd': 'edit_state_property',
'property_name': 'hints',
'old_value': [{
'hint_content': {
'html': '<p>Hint 1.</p>',
'content_id': 'hint_1'
}
}]
}), exp_domain.ExplorationChange({
'state_name': 'End',
'new_value': {
'html': '<p>Congratulations, you have finished!</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT)
},
'cmd': 'edit_state_property',
'property_name': 'content',
'old_value': {
'html': '',
'content_id': 'content_0'
}
}), exp_domain.ExplorationChange({
'new_state_name': 'End-State',
'cmd': 'rename_state',
'old_state_name': 'End'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Made changes in solutions in introduction state and content, state_name in end state.') # pylint: disable=line-too-long
# Changes to the exploration properties to check
# for mergeability.
change_list_3 = [exp_domain.ExplorationChange({
'property_name': 'title',
'cmd': 'edit_exploration_property',
'old_value': 'A title',
'new_value': 'A changed title.'
}), exp_domain.ExplorationChange({
'property_name': 'objective',
'cmd': 'edit_exploration_property',
'old_value': 'An objective',
'new_value': 'A changed objective.'
}), exp_domain.ExplorationChange({
'property_name': 'category',
'cmd': 'edit_exploration_property',
'old_value': 'A category',
'new_value': 'A changed category'
}), exp_domain.ExplorationChange({
'property_name': 'auto_tts_enabled',
'cmd': 'edit_exploration_property',
'old_value': True,
'new_value': False
}), exp_domain.ExplorationChange({
'property_name': 'tags',
'cmd': 'edit_exploration_property',
'old_value': ['old_value'],
'new_value': [
'new'
]
}), exp_domain.ExplorationChange({
'property_name': 'tags',
'cmd': 'edit_exploration_property',
'old_value': [
'new'
],
'new_value': [
'new',
'skill'
]
}), exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'language_code',
'new_value': 'bn',
'old_value': 'en'
}), exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'author_notes',
'new_value': 'author_notes'
}), exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'blurb',
'new_value': 'blurb'
}), exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'init_state_name',
'new_value': 'End',
}), exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'init_state_name',
'new_value': 'Introduction',
}), exp_domain.ExplorationChange({
'cmd': 'edit_exploration_property',
'property_name': 'auto_tts_enabled',
'new_value': False
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'confirmed_unclassified_answers',
'state_name': 'Introduction',
'new_value': ['test']
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'linked_skill_id',
'new_value': 'string_1'
}), exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'card_is_checkpoint',
'new_value': True
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_3)
self.assertEqual(changes_are_mergeable, True)
def test_changes_are_not_mergeable_when_exploration_properties_changes_conflict( # pylint: disable=line-too-long
self
) -> None:
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
# Changes to the exploration properties to check
# for mergeability.
change_list = [exp_domain.ExplorationChange({
'property_name': 'title',
'cmd': 'edit_exploration_property',
'old_value': 'A title',
'new_value': 'A changed title.'
}), exp_domain.ExplorationChange({
'property_name': 'objective',
'cmd': 'edit_exploration_property',
'old_value': 'An objective',
'new_value': 'A changed objective.'
}), exp_domain.ExplorationChange({
'property_name': 'category',
'cmd': 'edit_exploration_property',
'old_value': 'A category',
'new_value': 'A changed category'
}), exp_domain.ExplorationChange({
'property_name': 'auto_tts_enabled',
'cmd': 'edit_exploration_property',
'old_value': True,
'new_value': False
}), exp_domain.ExplorationChange({
'property_name': 'tags',
'cmd': 'edit_exploration_property',
'old_value': ['old_value'],
'new_value': [
'new'
]
}), exp_domain.ExplorationChange({
'property_name': 'tags',
'cmd': 'edit_exploration_property',
'old_value': [
'new'
],
'new_value': [
'new',
'skill'
]
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changes in the Exploration Properties.')
change_list_2 = [exp_domain.ExplorationChange({
'property_name': 'title',
'cmd': 'edit_exploration_property',
'old_value': 'A title',
'new_value': 'A new title.'
}), exp_domain.ExplorationChange({
'property_name': 'objective',
'cmd': 'edit_exploration_property',
'old_value': 'An objective',
'new_value': 'A new objective.'
}), exp_domain.ExplorationChange({
'property_name': 'category',
'cmd': 'edit_exploration_property',
'old_value': 'A category',
'new_value': 'A new category'
}), exp_domain.ExplorationChange({
'property_name': 'auto_tts_enabled',
'cmd': 'edit_exploration_property',
'old_value': True,
'new_value': False
}), exp_domain.ExplorationChange({
'property_name': 'tags',
'cmd': 'edit_exploration_property',
'old_value': ['old_value'],
'new_value': [
'new'
]
}), exp_domain.ExplorationChange({
'property_name': 'tags',
'cmd': 'edit_exploration_property',
'old_value': [
'new'
],
'new_value': [
'new',
'skill'
]
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_2)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_mergeable_when_translations_changes_do_not_conflict(
self
) -> None:
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
# Adding content, feedbacks, solutions so that
# translations can be added later on.
change_list = [exp_domain.ExplorationChange({
'property_name': 'content',
'old_value': {
'content_id': 'content',
'html': ''
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'content_id': 'content',
'html': '<p>First State Content.</p>'
}
}), exp_domain.ExplorationChange({
'property_name': 'widget_customization_args',
'old_value': {
'placeholder': {
'value': {
'unicode_str': '',
'content_id': 'ca_placeholder_0'
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'placeholder': {
'value': {
'unicode_str': 'Placeholder',
'content_id': 'ca_placeholder_0'
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
}
}), exp_domain.ExplorationChange({
'property_name': 'default_outcome',
'old_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'default_outcome',
'html': ''
},
'param_changes': [],
'dest_if_really_stuck': None,
'dest': 'End'
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'default_outcome',
'html': '<p>Feedback 1.</p>'
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
}
}), exp_domain.ExplorationChange({
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': [
{
'hint_content': {
'content_id': 'hint_1',
'html': '<p>Hint 1.</p>'
}
}
]
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'answer_is_exclusive': False,
'explanation': {
'content_id': 'solution',
'html': '<p>Explanation.</p>'
},
'correct_answer': 'Solution'
}
}), exp_domain.ExplorationChange({
'property_name': 'content',
'old_value': {
'content_id': 'content',
'html': ''
},
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': {
'content_id': 'content',
'html': '<p>Second State Content.</p>'
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID, change_list,
'Added various contents.')
change_list_2 = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'old_value': ['old_value'],
'state_name': 'Introduction',
'new_value': [{
'rule_specs': [{
'rule_type': 'StartsWith',
'inputs': {
'x': {
'contentId': 'rule_input_2',
'normalizedStrSet': [
'Hello',
'Hola'
]
}
}
}],
'tagged_skill_misconception_id': None,
'outcome': {
'labelled_as_correct': False,
'feedback': {
'content_id': 'feedback_1',
'html': '<p>Feedback</p>'
},
'missing_prerequisite_skill_id': None,
'dest': 'End',
'dest_if_really_stuck': None,
'param_changes': [],
'refresher_exploration_id': None
},
'training_data': []
}]
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID, change_list_2,
'Added answer group.')
# Adding some translations to the first state.
change_list_3 = [exp_domain.ExplorationChange({
'cmd': 'mark_translations_needs_update',
'content_id': 'content',
}), exp_domain.ExplorationChange({
'cmd': 'mark_translations_needs_update',
'content_id': 'default_outcome'
}), exp_domain.ExplorationChange({
'cmd': 'remove_translations',
'content_id': 'default_outcome'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, False)
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID, change_list_3,
'Added some translations.')
# Adding translations again to the different contents
# of same state to check that they can be merged.
change_list_4 = [exp_domain.ExplorationChange({
'new_state_name': 'Intro-Rename',
'cmd': 'rename_state',
'old_state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'content_id': 'ca_placeholder_0',
'cmd': 'remove_translations'
}), exp_domain.ExplorationChange({
'content_id': 'hint_1',
'cmd': 'remove_translations'
}), exp_domain.ExplorationChange({
'new_state_name': 'Introduction',
'cmd': 'rename_state',
'old_state_name': 'Intro-Rename'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 3, change_list_4)
self.assertEqual(changes_are_mergeable, False)
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID, change_list_4,
'Resname state.')
# Adding translations to the second state to check
# that they can be merged even in the same property.
change_list_5 = [exp_domain.ExplorationChange({
'content_id': 'content',
'cmd': 'mark_translations_needs_update'
})]
changes_are_mergeable_1 = exp_services.are_changes_mergeable(
self.EXP_0_ID, 4, change_list_5)
self.assertEqual(changes_are_mergeable_1, False)
# Add changes to the different content of first state to
# check that translation changes to some properties doesn't
# affects the changes of content of other properties.
change_list_6 = [exp_domain.ExplorationChange({
'old_value': {
'rows': {
'value': 1
},
'placeholder': {
'value': {
'unicode_str': 'Placeholder',
'content_id': 'ca_placeholder_0'
}
},
'catchMisspellings': {
'value': False
}
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': {
'rows': {
'value': 1
},
'placeholder': {
'value': {
'unicode_str': 'Placeholder Changed.',
'content_id': 'ca_placeholder_0'
}
},
'catchMisspellings': {
'value': False
}
}
}), exp_domain.ExplorationChange({
'property_name': 'default_outcome',
'old_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'default_outcome',
'html': 'Feedback 1.'
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'default_outcome',
'html': '<p>Feedback 2.</p>'
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID, change_list_6,
'Changing Customization Args Placeholder in First State.')
changes_are_mergeable_3 = exp_services.are_changes_mergeable(
self.EXP_0_ID, 4, change_list_5)
self.assertEqual(changes_are_mergeable_3, False)
def test_changes_are_mergeable_when_voiceovers_changes_do_not_conflict(
self
) -> None:
# Adding content, feedbacks, solutions so that
# voiceovers can be added later on.
change_list = [exp_domain.ExplorationChange({
'property_name': 'content',
'old_value': None,
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>First State Content.</p>'
}
}), exp_domain.ExplorationChange({
'property_name': 'widget_customization_args',
'old_value': {
'placeholder': {
'value': {
'unicode_str': '',
'content_id': 'cust_arg_1'
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'placeholder': {
'value': {
'unicode_str': 'Placeholder',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='placeholder')
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
}
}), exp_domain.ExplorationChange({
'property_name': 'default_outcome',
'old_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'feedback_5',
'html': ''
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME),
'html': '<p>Feedback 1.</p>'
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
}
}), exp_domain.ExplorationChange({
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': [
{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}
]
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'answer_is_exclusive': False,
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
},
'correct_answer': 'Solution'
}
}), exp_domain.ExplorationChange({
'property_name': 'content',
'old_value': {
'content_id': 'content_6',
'html': ''
},
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Second State Content.</p>'
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added various contents.')
# Adding change to the field which is neither
# affected by nor affects voiceovers.
change_list_2 = [exp_domain.ExplorationChange({
'cmd': 'edit_state_property',
'state_name': 'Introduction',
'property_name': 'card_is_checkpoint',
'new_value': True
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Added single unrelated change.')
# Adding some voiceovers to the first state.
change_list_3 = [exp_domain.ExplorationChange({
'property_name': 'recorded_voiceovers',
'old_value': {
'voiceovers_mapping': {
'hint_1': {},
'default_outcome': {},
'solution': {},
'ca_placeholder_0': {},
'content': {}
}
},
'state_name': 'Introduction',
'new_value': {
'voiceovers_mapping': {
'hint_1': {},
'default_outcome': {},
'solution': {},
'ca_placeholder_0': {},
'content': {
'en': {
'needs_update': False,
'filename': 'content-en-xrss3z3nso.mp3',
'file_size_bytes': 114938,
'duration_secs': 7.183625
}
}
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'property_name': 'recorded_voiceovers',
'old_value': {
'voiceovers_mapping': {
'hint_1': {},
'default_outcome': {},
'solution': {},
'ca_placeholder_0': {},
'content': {
'en': {
'needs_update': False,
'filename': 'content-en-xrss3z3nso.mp3',
'file_size_bytes': 114938,
'duration_secs': 7.183625
}
}
}
},
'state_name': 'Introduction',
'new_value': {
'voiceovers_mapping': {
'hint_8': {},
'default_outcome_7': {},
'solution_9': {},
'ca_placeholder_6': {
'en': {
'needs_update': False,
'filename': 'ca_placeholder_0-en-mfy5l6logg.mp3',
'file_size_bytes': 175542,
'duration_secs': 10.971375
}
},
'content_5': {
'en': {
'needs_update': False,
'filename': 'content-en-xrss3z3nso.mp3',
'file_size_bytes': 114938,
'duration_secs': 7.183625
}
}
}
},
'cmd': 'edit_state_property'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, True)
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_3),
'Added some voiceovers.')
# Adding voiceovers again to the same first state
# to check if they can be applied. They will not
# be mergeable as the changes are in the same property
# i.e. recorded_voiceovers.
change_list_4 = [exp_domain.ExplorationChange({
'property_name': 'recorded_voiceovers',
'cmd': 'edit_state_property',
'old_value': {
'voiceovers_mapping': {
'default_outcome': {},
'solution': {},
'content': {},
'ca_placeholder_0': {},
'hint_1': {}
}
},
'new_value': {
'voiceovers_mapping': {
'default_outcome': {},
'solution': {},
'content': {},
'ca_placeholder_0': {},
'hint_1': {
'en': {
'needs_update': False,
'duration_secs': 30.0669375,
'filename': 'hint_1-en-ajclkw0cnz.mp3',
'file_size_bytes': 481071
}
}
}
},
'state_name': 'Introduction'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 3, change_list_4)
self.assertEqual(changes_are_mergeable, False)
# Adding voiceovers to the second state to check
# if they can be applied. They can be mergead as
# the changes are in the different states.
change_list_5 = [exp_domain.ExplorationChange({
'old_value': {
'voiceovers_mapping': {
'content': {}
}
},
'property_name': 'recorded_voiceovers',
'cmd': 'edit_state_property',
'new_value': {
'voiceovers_mapping': {
'content': {
'en': {
'duration_secs': 10.3183125,
'filename': 'content-en-ar9zhd7edl.mp3',
'file_size_bytes': 165093,
'needs_update': False
}
}
}
},
'state_name': 'End'
})]
changes_are_mergeable_1 = exp_services.are_changes_mergeable(
self.EXP_0_ID, 3, change_list_5)
self.assertEqual(changes_are_mergeable_1, True)
# Changes to the content of first state to check
# that the changes in the contents of first state
# doesn't affects the changes to the voiceovers in
# second state.
change_list_6 = [exp_domain.ExplorationChange({
'state_name': 'Introduction',
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>First State Content.</p>'
},
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Changed First State Content.</p>'
},
'property_name': 'content',
'cmd': 'edit_state_property'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_6),
'Changing Content in First State.')
changes_are_mergeable_3 = exp_services.are_changes_mergeable(
self.EXP_0_ID, 4, change_list_5)
self.assertEqual(changes_are_mergeable_3, True)
# Changes to the content of second state to check that
# the changes to the voiceovers can not be made in
# same state if the property which can be recorded is
# changed.
change_list_6 = [exp_domain.ExplorationChange({
'state_name': 'End',
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Second State Content.</p>'
},
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Changed Second State Content.</p>'
},
'property_name': 'content',
'cmd': 'edit_state_property'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_6),
'Changing Content in Second State.')
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 4, change_list_4)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_not_mergeable_when_voiceovers_changes_conflict(
self
) -> None:
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
# Adding content, feedbacks, solutions so that
# voiceovers can be added later on.
change_list = [exp_domain.ExplorationChange({
'property_name': 'content',
'old_value': {
'content_id': 'content_5',
'html': ''
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>First State Content.</p>'
}
}), exp_domain.ExplorationChange({
'property_name': 'widget_customization_args',
'old_value': {
'placeholder': {
'value': {
'unicode_str': '',
'content_id': 'cust_arg_5'
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'placeholder': {
'value': {
'unicode_str': 'Placeholder',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CUSTOMIZATION_ARG,
extra_prefix='placeholder')
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
}
}), exp_domain.ExplorationChange({
'property_name': 'default_outcome',
'old_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': 'feedback_7',
'html': ''
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
},
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'labelled_as_correct': False,
'missing_prerequisite_skill_id': None,
'refresher_exploration_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.DEFAULT_OUTCOME),
'html': '<p>Feedback 1.</p>'
},
'param_changes': [
],
'dest_if_really_stuck': None,
'dest': 'End'
}
}), exp_domain.ExplorationChange({
'property_name': 'hints',
'old_value': ['old_value'],
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': [
{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.HINT),
'html': '<p>Hint 1.</p>'
}
}
]
}), exp_domain.ExplorationChange({
'property_name': 'solution',
'old_value': None,
'state_name': 'Introduction',
'cmd': 'edit_state_property',
'new_value': {
'answer_is_exclusive': False,
'explanation': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.SOLUTION),
'html': '<p>Explanation.</p>'
},
'correct_answer': 'Solution'
}
}), exp_domain.ExplorationChange({
'property_name': 'content',
'old_value': {
'content_id': 'content',
'html': ''
},
'state_name': 'End',
'cmd': 'edit_state_property',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.CONTENT),
'html': '<p>Second State Content.</p>'
}
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Added various contents.')
# Adding some voiceovers to the first state.
change_list_2 = [exp_domain.ExplorationChange({
'property_name': 'recorded_voiceovers',
'old_value': {
'voiceovers_mapping': {
'hint_1': {},
'default_outcome': {},
'solution': {},
'ca_placeholder_0': {},
'content': {}
}
},
'state_name': 'Introduction',
'new_value': {
'voiceovers_mapping': {
'hint_1': {},
'default_outcome': {},
'solution': {},
'ca_placeholder_0': {},
'content': {
'en': {
'needs_update': False,
'filename': 'content-en-xrss3z3nso.mp3',
'file_size_bytes': 114938,
'duration_secs': 7.183625
}
}
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'property_name': 'recorded_voiceovers',
'old_value': {
'voiceovers_mapping': {
'hint_1': {},
'default_outcome': {},
'solution': {},
'ca_placeholder_0': {},
'content': {
'en': {
'needs_update': False,
'filename': 'content-en-xrss3z3nso.mp3',
'file_size_bytes': 114938,
'duration_secs': 7.183625
}
}
}
},
'state_name': 'Introduction',
'new_value': {
'voiceovers_mapping': {
'hint_8': {},
'default_outcome_7': {},
'solution_9': {},
'ca_placeholder_6': {
'en': {
'needs_update': False,
'filename': 'ca_placeholder_0-en-mfy5l6logg.mp3',
'file_size_bytes': 175542,
'duration_secs': 10.971375
}
},
'content_5': {
'en': {
'needs_update': False,
'filename': 'content-en-xrss3z3nso.mp3',
'file_size_bytes': 114938,
'duration_secs': 7.183625
}
}
}
},
'cmd': 'edit_state_property'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Added some voiceovers.')
# Adding voiceovers again to the same first state
# to check if they can be applied. They will not
# be mergeable as the changes are in the same property
# i.e. recorded_voiceovers.
change_list_3 = [exp_domain.ExplorationChange({
'property_name': 'recorded_voiceovers',
'cmd': 'edit_state_property',
'old_value': {
'voiceovers_mapping': {
'default_outcome': {},
'solution': {},
'content': {},
'ca_placeholder_0': {},
'hint_1': {}
}
},
'new_value': {
'voiceovers_mapping': {
'default_outcome': {},
'solution': {},
'content': {},
'ca_placeholder_0': {},
'hint_1': {
'en': {
'needs_update': False,
'duration_secs': 30.0669375,
'filename': 'hint_1-en-ajclkw0cnz.mp3',
'file_size_bytes': 481071
}
}
}
},
'state_name': 'Introduction'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_mergeable, False)
def test_changes_are_not_mergeable_when_state_added_or_deleted(
self
) -> None:
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
test_dict: Dict[str, str] = {}
# Changes to the various properties of the first and
# second state.
change_list = [exp_domain.ExplorationChange({
'old_value': 'TextInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': None,
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'placeholder': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': test_dict,
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': 'NumericInput',
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'old_value':
{
'requireNonnegativeInput':
{
'value': True
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'requireNonnegativeInput':
{
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'new_value': [
{
'tagged_skill_misconception_id': None,
'rule_specs': [
{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}
],
'training_data': [],
'outcome': {
'param_changes': [],
'dest_if_really_stuck': None,
'dest': 'End',
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}
],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [
{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Hint.</p>'
}
}
],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': 'Congratulations, you have finished!'
},
'cmd': 'edit_state_property',
'property_name': 'content',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>2Congratulations, you have finished!</p>'
},
'state_name': 'End'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed various properties in both states.')
# Change to the unrelated property to check that
# it can be merged.
change_list_2 = [exp_domain.ExplorationChange({
'old_value': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'new_value': {
'html': '<p>Hello Aryaman!</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'state_name': 'Introduction',
'property_name': 'content',
'cmd': 'edit_state_property'
})]
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_2)
self.assertEqual(changes_are_mergeable, True)
# Deleting and Adding states to check that when any
# state is deleted or added, then the changes can not be
# merged.
change_list_3 = [exp_domain.ExplorationChange({
'new_state_name': 'End-State',
'cmd': 'rename_state',
'old_state_name': 'End'
}), exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'End-State'
}), exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'End',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'End',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'new_state_name': 'End-State',
'cmd': 'rename_state',
'old_state_name': 'End'
}), exp_domain.ExplorationChange({
'new_state_name': 'End',
'cmd': 'rename_state',
'old_state_name': 'End-State'
}), exp_domain.ExplorationChange({
'old_value': [{
'tagged_skill_misconception_id': None,
'rule_specs': [{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}],
'training_data': [],
'outcome': {
'param_changes': [],
'dest': 'Introduction',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'new_value': [{
'tagged_skill_misconception_id': None,
'rule_specs': [{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}],
'training_data': [],
'outcome': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'param_changes': [],
'dest': 'Introduction',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'new_value': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': 'End',
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': 'Congratulations, you have finished!'
},
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': 'EndExploration',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': {
'recommendedExplorationIds': {
'value': []
}
},
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'new_value': None,
'state_name': 'End'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_3),
'Added and deleted states.')
# Checking that old changes that could be
# merged previously can not be merged after
# addition or deletion of state.
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_2)
self.assertEqual(changes_are_not_mergeable, False)
def test_changes_are_not_mergeable_when_frontend_version_exceeds_backend_version( # pylint: disable=line-too-long
self
) -> None:
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
test_dict: Dict[str, str] = {}
# Changes to the various properties of the first and
# second state.
change_list = [exp_domain.ExplorationChange({
'old_value': 'TextInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': None,
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'placeholder': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': test_dict,
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': 'NumericInput',
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'new_value': [
{
'tagged_skill_misconception_id': None,
'rule_specs': [
{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}
],
'training_data': [],
'outcome': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}
],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [
{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Hint.</p>'
}
}
],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': 'Congratulations, you have finished!'
},
'cmd': 'edit_state_property',
'property_name': 'content',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>2Congratulations, you have finished!</p>'
},
'state_name': 'End'
})]
# Changes are mergeable when updating the same version.
changes_are_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list)
self.assertEqual(changes_are_mergeable, True)
# Changes are not mergeable when updating from version
# more than that on the backend.
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 3, change_list)
self.assertEqual(changes_are_not_mergeable, False)
def test_email_is_sent_to_admin_in_case_of_adding_deleting_state_changes(
self
) -> None:
self.login(self.OWNER_EMAIL)
with self.swap(feconf, 'CAN_SEND_EMAILS', True):
messages = self._get_sent_email_messages(
feconf.ADMIN_EMAIL_ADDRESS)
self.assertEqual(len(messages), 0)
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
test_dict: Dict[str, str] = {}
# Changes to the various properties of the first and
# second state.
change_list = [exp_domain.ExplorationChange({
'old_value': 'TextInput',
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': None,
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'placeholder': {
'value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'unicode_str': ''
}
},
'rows': {
'value': 1
},
'catchMisspellings': {
'value': False
}
},
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': test_dict,
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': 'NumericInput',
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'state_name': 'Introduction',
'old_value':
{
'requireNonnegativeInput':
{
'value': True
}
},
'property_name': 'widget_customization_args',
'new_value':
{
'requireNonnegativeInput':
{
'value': False
}
},
'cmd': 'edit_state_property'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'new_value': [
{
'tagged_skill_misconception_id': None,
'rule_specs': [
{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}
],
'training_data': [],
'outcome': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': (
self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK
)
),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}
],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': ['old_value'],
'cmd': 'edit_state_property',
'property_name': 'hints',
'new_value': [
{
'hint_content': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>Hint.</p>'
}
}
],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': 'Congratulations, you have finished!'
},
'cmd': 'edit_state_property',
'property_name': 'content',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': '<p>2Congratulations, you have finished!</p>'
},
'state_name': 'End'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed various properties in both states.')
change_list_2 = [exp_domain.ExplorationChange({
'new_state_name': 'End-State',
'cmd': 'rename_state',
'old_state_name': 'End'
}), exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'End-State'
}), exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'End',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'cmd': 'delete_state',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'cmd': 'add_state',
'state_name': 'End',
'content_id_for_state_content': 'content_0',
'content_id_for_default_outcome': 'default_outcome_1'
}), exp_domain.ExplorationChange({
'new_state_name': 'End-State',
'cmd': 'rename_state',
'old_state_name': 'End'
}), exp_domain.ExplorationChange({
'new_state_name': 'End',
'cmd': 'rename_state',
'old_state_name': 'End-State'
}), exp_domain.ExplorationChange({
'old_value': [{
'tagged_skill_misconception_id': None,
'rule_specs': [{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}],
'training_data': [],
'outcome': {
'param_changes': [],
'dest': 'Introduction',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}],
'cmd': 'edit_state_property',
'property_name': 'answer_groups',
'new_value': [{
'tagged_skill_misconception_id': None,
'rule_specs': [{
'rule_type': 'IsLessThanOrEqualTo',
'inputs': {
'x': 50
}
}],
'training_data': [],
'outcome': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
}
}],
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'param_changes': [],
'dest': 'Introduction',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'new_value': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'state_name': 'Introduction'
}), exp_domain.ExplorationChange({
'old_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'cmd': 'edit_state_property',
'property_name': 'content',
'new_value': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': 'Congratulations, you have finished!'
},
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': None,
'cmd': 'edit_state_property',
'property_name': 'widget_id',
'new_value': 'EndExploration',
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': test_dict,
'cmd': 'edit_state_property',
'property_name': 'widget_customization_args',
'new_value': {
'recommendedExplorationIds': {
'value': []
}
},
'state_name': 'End'
}), exp_domain.ExplorationChange({
'old_value': {
'param_changes': [],
'dest': 'End',
'dest_if_really_stuck': None,
'missing_prerequisite_skill_id': None,
'feedback': {
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'html': ''
},
'labelled_as_correct': False,
'refresher_exploration_id': None
},
'cmd': 'edit_state_property',
'property_name': 'default_outcome',
'new_value': None,
'state_name': 'End'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Added and deleted states.')
change_list_3 = [exp_domain.ExplorationChange({
'old_value': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'new_value': {
'html': '<p>Hello Aryaman!</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'state_name': 'Introduction',
'property_name': 'content',
'cmd': 'edit_state_property'
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 1, change_list_3)
self.assertEqual(changes_are_not_mergeable, False)
change_list_3_dict = [change.to_dict() for change in change_list_3]
expected_email_html_body = (
'(Sent from dev-project-id)<br/><br/>'
'Hi Admin,<br><br>'
'Some draft changes were rejected in exploration %s because '
'the changes were conflicting and could not be saved. Please '
'see the rejected change list below:<br>'
'Discarded change list: %s <br><br>'
'Frontend Version: %s<br>'
'Backend Version: %s<br><br>'
'Thanks!' % (self.EXP_0_ID, change_list_3_dict, 1, 3)
)
messages = self._get_sent_email_messages(
feconf.ADMIN_EMAIL_ADDRESS)
self.assertEqual(len(messages), 1)
self.assertEqual(messages[0].html, expected_email_html_body)
def test_email_is_sent_to_admin_in_case_of_state_renames_changes_conflict(
self
) -> None:
self.login(self.OWNER_EMAIL)
with self.swap(feconf, 'CAN_SEND_EMAILS', True):
messages = self._get_sent_email_messages(
feconf.ADMIN_EMAIL_ADDRESS)
self.assertEqual(len(messages), 0)
self.save_new_valid_exploration(
self.EXP_0_ID, self.owner_id, end_state_name='End')
rights_manager.publish_exploration(self.owner, self.EXP_0_ID)
change_list = [exp_domain.ExplorationChange({
'old_value': {
'html': '',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'new_value': {
'html': '<p>End State</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'state_name': 'End',
'property_name': 'content',
'cmd': 'edit_state_property'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list),
'Changed various properties in both states.')
# State name changed.
change_list_2 = [exp_domain.ExplorationChange({
'new_state_name': 'End-State',
'cmd': 'rename_state',
'old_state_name': 'End'
})]
exp_services.update_exploration(
self.owner_id, self.EXP_0_ID,
self.append_next_content_id_index_change(change_list_2),
'Changed various properties in both states.')
change_list_3 = [exp_domain.ExplorationChange({
'old_value': {
'html': 'End State',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'new_value': {
'html': '<p>End State Changed</p>',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK)
},
'state_name': 'End',
'property_name': 'content',
'cmd': 'edit_state_property'
})]
changes_are_not_mergeable = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_3)
self.assertEqual(changes_are_not_mergeable, False)
change_list_3_dict = [change.to_dict() for change in change_list_3]
expected_email_html_body = (
'(Sent from dev-project-id)<br/><br/>'
'Hi Admin,<br><br>'
'Some draft changes were rejected in exploration %s because '
'the changes were conflicting and could not be saved. Please '
'see the rejected change list below:<br>'
'Discarded change list: %s <br><br>'
'Frontend Version: %s<br>'
'Backend Version: %s<br><br>'
'Thanks!' % (self.EXP_0_ID, change_list_3_dict, 2, 3)
)
messages = self._get_sent_email_messages(
feconf.ADMIN_EMAIL_ADDRESS)
self.assertEqual(len(messages), 1)
self.assertEqual(expected_email_html_body, messages[0].html)
# Add a translation after state renames.
change_list_4 = [exp_domain.ExplorationChange({
'content_html': 'N/A',
'translation_html': '<p>State 2 Content Translation.</p>',
'state_name': 'End',
'language_code': 'de',
'content_id': self.content_id_generator.generate(
translation_domain.ContentType.FEEDBACK),
'cmd': 'add_written_translation',
'data_format': 'html'
})]
changes_are_not_mergeable_2 = exp_services.are_changes_mergeable(
self.EXP_0_ID, 2, change_list_4)
self.assertEqual(changes_are_not_mergeable_2, False)
class ExplorationMetadataDomainUnitTests(test_utils.GenericTestBase):
def _require_metadata_properties_to_be_synced(self) -> None:
"""Raises error if there is a new metadata property in the Exploration
object and it is not added in the ExplorationMetadata domain object.
Raises:
Exception. All the metadata properties are not synced.
"""
exploration = exp_domain.Exploration.create_default_exploration('0')
exploration_dict = exploration.to_dict()
for key in exploration_dict:
if (
key not in constants.NON_METADATA_PROPERTIES and
key not in constants.METADATA_PROPERTIES
):
raise Exception(
'Looks like a new property %s was added to the Exploration'
' domain object. Please include this property in '
'constants.METADATA_PROPERTIES if you want to use this '
'as a metadata property. Otherwise, add this in the '
'constants.NON_METADATA_PROPERTIES if you don\'t want '
'to use this as a metadata property.' % (key)
)
exploration_metadata = exploration.get_metadata()
exploration_metadata_dict = exploration_metadata.to_dict()
for metadata_property in constants.METADATA_PROPERTIES:
if metadata_property not in exploration_metadata_dict:
raise Exception(
'A new metadata property %s was added to the Exploration '
'domain object but not included in the '
'ExplorationMetadata domain object. Please include this '
'new property in the ExplorationMetadata domain object '
'also.' % (metadata_property)
)
def test_exploration_metadata_gets_created(self) -> None:
exploration = exp_domain.Exploration.create_default_exploration('0')
exploration.update_param_specs({
'ExampleParamOne': (
param_domain.ParamSpec('UnicodeString').to_dict())
})
exploration.update_param_changes([
param_domain.ParamChange(
'ParamChange', 'RandomSelector', {
'list_of_values': ['3', '4'],
'parse_with_jinja': True
}
),
param_domain.ParamChange(
'ParamChange', 'RandomSelector', {
'list_of_values': ['5', '6'],
'parse_with_jinja': True
}
)
])
actual_metadata_dict = exp_domain.ExplorationMetadata(
exploration.title, exploration. category, exploration.objective,
exploration.language_code, exploration.tags, exploration.blurb,
exploration.author_notes, exploration.states_schema_version,
exploration.init_state_name, exploration.param_specs,
exploration.param_changes, exploration.auto_tts_enabled,
exploration.edits_allowed
).to_dict()
expected_metadata_dict = {
'title': exploration.title,
'category': exploration.category,
'objective': exploration.objective,
'language_code': exploration.language_code,
'tags': exploration.tags,
'blurb': exploration.blurb,
'author_notes': exploration.author_notes,
'states_schema_version': exploration.states_schema_version,
'init_state_name': exploration.init_state_name,
'param_specs': {
'ExampleParamOne': (
param_domain.ParamSpec('UnicodeString').to_dict())
},
'param_changes': [
param_domain.ParamChange(
'ParamChange', 'RandomSelector', {
'list_of_values': ['3', '4'],
'parse_with_jinja': True
}
).to_dict(),
param_domain.ParamChange(
'ParamChange', 'RandomSelector', {
'list_of_values': ['5', '6'],
'parse_with_jinja': True
}
).to_dict()
],
'auto_tts_enabled': exploration.auto_tts_enabled,
'edits_allowed': exploration.edits_allowed
}
self.assertEqual(actual_metadata_dict, expected_metadata_dict)
def test_metadata_properties_are_synced(self) -> None:
self._require_metadata_properties_to_be_synced()
swapped_metadata_properties = self.swap(
constants, 'METADATA_PROPERTIES', [
'title', 'category', 'objective', 'language_code',
'blurb', 'author_notes', 'states_schema_version',
'init_state_name', 'param_specs', 'param_changes',
'auto_tts_enabled',
'edits_allowed'
]
)
error_message = (
'Looks like a new property tags was added to the Exploration'
' domain object. Please include this property in '
'constants.METADATA_PROPERTIES if you want to use this '
'as a metadata property. Otherwise, add this in the '
'constants.NON_METADATA_PROPERTIES if you don\'t want '
'to use this as a metadata property.'
)
with swapped_metadata_properties, self.assertRaisesRegex(
Exception, error_message
):
self._require_metadata_properties_to_be_synced()
swapped_metadata_properties = self.swap(
constants, 'METADATA_PROPERTIES', [
'title', 'category', 'objective', 'language_code', 'tags',
'blurb', 'author_notes', 'states_schema_version',
'init_state_name', 'param_specs', 'param_changes',
'auto_tts_enabled', 'edits_allowed', 'new_property'
]
)
error_message = (
'A new metadata property %s was added to the Exploration '
'domain object but not included in the '
'ExplorationMetadata domain object. Please include this '
'new property in the ExplorationMetadata domain object '
'also.' % ('new_property')
)
with swapped_metadata_properties, self.assertRaisesRegex(
Exception, error_message
):
self._require_metadata_properties_to_be_synced()
class MetadataVersionHistoryDomainUnitTests(test_utils.GenericTestBase):
def test_metadata_version_history_gets_created(self) -> None:
expected_dict = {
'last_edited_version_number': 1,
'last_edited_committer_id': 'user_1'
}
actual_dict = exp_domain.MetadataVersionHistory(1, 'user_1').to_dict()
self.assertEqual(expected_dict, actual_dict)
def test_metadata_version_history_gets_created_from_dict(self) -> None:
metadata_version_history_dict: exp_domain.MetadataVersionHistoryDict = {
'last_edited_version_number': 1,
'last_edited_committer_id': 'user_1'
}
metadata_version_history = (
exp_domain.MetadataVersionHistory.from_dict(
metadata_version_history_dict))
self.assertEqual(
metadata_version_history.last_edited_version_number,
metadata_version_history_dict['last_edited_version_number'])
self.assertEqual(
metadata_version_history.last_edited_committer_id,
metadata_version_history_dict['last_edited_committer_id'])
class ExplorationVersionHistoryUnitTests(test_utils.GenericTestBase):
def test_exploration_version_history_gets_created(self) -> None:
state_version_history_dict = {
'state 1': state_domain.StateVersionHistory(
1, 'state 1', 'user1'
).to_dict()
}
metadata_version_history = exp_domain.MetadataVersionHistory(
None, 'user1'
)
expected_dict = {
'exploration_id': 'exp_1',
'exploration_version': 2,
'state_version_history': state_version_history_dict,
'metadata_version_history': metadata_version_history.to_dict(),
'committer_ids': ['user1']
}
actual_dict = exp_domain.ExplorationVersionHistory(
'exp_1', 2, state_version_history_dict,
metadata_version_history.last_edited_version_number,
metadata_version_history.last_edited_committer_id,
['user1']
).to_dict()
self.assertEqual(actual_dict, expected_dict)