def get(self, exploration_id): """Gets the data for the exploration overview page.""" exploration = exp_services.get_exploration_by_id(exploration_id) state_list = {} for state_id in exploration.state_ids: state_list[state_id] = exp_services.export_state_to_verbose_dict( exploration_id, state_id) self.values.update({ 'exploration_id': exploration_id, 'init_state_id': exploration.init_state_id, 'is_public': exploration.is_public, 'image_id': exploration.image_id, 'category': exploration.category, 'title': exploration.title, 'editors': exploration.editor_ids, 'states': state_list, 'param_changes': exploration.param_change_dicts, 'param_specs': exploration.param_specs_dict, 'version': exploration.version, # Add information about the most recent versions. 'snapshots': exp_services.get_exploration_snapshots_metadata( exploration_id, DEFAULT_NUM_SNAPSHOTS), # Add information for the exploration statistics page. 'num_visits': stats_services.get_exploration_visit_count( exploration_id), 'num_completions': stats_services.get_exploration_completed_count( exploration_id), 'state_stats': stats_services.get_state_stats_for_exploration( exploration_id), 'imp': stats_services.get_top_improvable_states( [exploration_id], 10), }) self.render_json(self.values)
def test_record_commit_message(self): """Check published explorations record commit messages.""" rights_manager.publish_exploration(self.OWNER_ID, self.EXP_ID) exp_services.update_exploration( self.OWNER_ID, self.EXP_ID, _get_change_list( self.init_state_name, 'widget_sticky', False), 'A message') self.assertEqual( exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 1)[0]['commit_message'], 'A message')
def get(self, exploration_id): """Handles GET requests.""" snapshots = exp_services.get_exploration_snapshots_metadata( exploration_id, DEFAULT_NUM_SNAPSHOTS) # Patch `snapshots` to use the editor's display name. for snapshot in snapshots: if snapshot['committer_id'] != 'admin': snapshot['committer_id'] = user_services.get_username( snapshot['committer_id']) self.render_json({ 'snapshots': snapshots, })
def test_versioning_with_reverting(self): exploration = self.save_new_valid_exploration( self.EXP_ID, self.OWNER_ID) # In version 1, the title was 'A title'. # In version 2, the title becomes 'V2 title'. exploration.title = 'V2 title' exp_services._save_exploration( self.OWNER_ID, exploration, 'Changed title.', []) # In version 3, a new state is added. exploration = exp_services.get_exploration_by_id(self.EXP_ID) exploration.add_states(['New state']) exp_services._save_exploration( 'committer_id_v3', exploration, 'Added new state', []) # It is not possible to revert from anything other than the most # current version. with self.assertRaisesRegexp(Exception, 'too old'): exp_services.revert_exploration( 'committer_id_v4', self.EXP_ID, 2, 1) # Version 4 is a reversion to version 1. exp_services.revert_exploration('committer_id_v4', self.EXP_ID, 3, 1) exploration = exp_services.get_exploration_by_id(self.EXP_ID) self.assertEqual(exploration.title, 'A title') self.assertEqual(len(exploration.states), 1) self.assertEqual(exploration.version, 4) snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 5) commit_dict_4 = { 'committer_id': 'committer_id_v4', 'commit_message': 'Reverted exploration to version 1', 'version_number': 4, } commit_dict_3 = { 'committer_id': 'committer_id_v3', 'commit_message': 'Added new state', 'version_number': 3, } self.assertEqual(len(snapshots_metadata), 4) self.assertDictContainsSubset( commit_dict_4, snapshots_metadata[0]) self.assertDictContainsSubset(commit_dict_3, snapshots_metadata[1]) self.assertGreaterEqual( snapshots_metadata[0]['created_on'], snapshots_metadata[1]['created_on'])
def get(self, exploration_id): """Handles GET requests.""" try: snapshots = exp_services.get_exploration_snapshots_metadata( exploration_id, DEFAULT_NUM_SNAPSHOTS) except: raise self.PageNotFoundException # Patch `snapshots` to use the editor's display name. for snapshot in snapshots: if snapshot['committer_id'] != feconf.ADMIN_COMMITTER_ID: snapshot['committer_id'] = user_services.get_username( snapshot['committer_id']) self.render_json({ 'snapshots': snapshots, })
def get(self, exploration_id): """Handles GET requests.""" try: snapshots = exp_services.get_exploration_snapshots_metadata( exploration_id) except: raise self.PageNotFoundException # Patch `snapshots` to use the editor's display name. for snapshot in snapshots: if snapshot['committer_id'] != feconf.ADMIN_COMMITTER_ID: snapshot['committer_id'] = user_services.get_username( snapshot['committer_id']) self.render_json({ 'snapshots': snapshots, })
def get(self, exploration_id): """Handles GET requests.""" snapshots = exp_services.get_exploration_snapshots_metadata( exploration_id) # Patch `snapshots` to use the editor's display name. snapshots_committer_ids = [ snapshot['committer_id'] for snapshot in snapshots ] committer_usernames = user_services.get_usernames( snapshots_committer_ids) for index, snapshot in enumerate(snapshots): snapshot['committer_id'] = committer_usernames[index] self.render_json({ 'snapshots': snapshots, })
def _get_most_recent_exp_snapshot_created_on_ms(self, exp_id): most_recent_snapshot = exp_services.get_exploration_snapshots_metadata( exp_id)[-1] return most_recent_snapshot['created_on_ms']
def test_get_exploration_snapshots_metadata(self): v1_exploration = self.save_new_valid_exploration( self.EXP_ID, self.OWNER_ID) snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 3) self.assertEqual(len(snapshots_metadata), 1) self.assertDictContainsSubset({ 'commit_cmds': [{ 'cmd': 'create_new', 'title': 'A title', 'category': 'A category', }], 'committer_id': self.OWNER_ID, 'commit_message': ( 'New exploration created with title \'A title\'.'), 'commit_type': 'create', 'version_number': 1 }, snapshots_metadata[0]) self.assertIn('created_on', snapshots_metadata[0]) # Publish the exploration. This does not affect the exploration version # history. rights_manager.publish_exploration(self.OWNER_ID, self.EXP_ID) snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 3) self.assertEqual(len(snapshots_metadata), 1) self.assertDictContainsSubset({ 'commit_cmds': [{ 'cmd': 'create_new', 'title': 'A title', 'category': 'A category' }], 'committer_id': self.OWNER_ID, 'commit_message': ( 'New exploration created with title \'A title\'.'), 'commit_type': 'create', 'version_number': 1 }, snapshots_metadata[0]) self.assertIn('created_on', snapshots_metadata[0]) # Modify the exploration. This affects the exploration version history. change_list = [{ 'cmd': 'edit_exploration_property', 'property_name': 'title', 'new_value': 'First title' }] exp_services.update_exploration( self.OWNER_ID, self.EXP_ID, change_list, 'Changed title.') snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 3) self.assertEqual(len(snapshots_metadata), 2) self.assertIn('created_on', snapshots_metadata[0]) self.assertDictContainsSubset({ 'commit_cmds': change_list, 'committer_id': self.OWNER_ID, 'commit_message': 'Changed title.', 'commit_type': 'edit', 'version_number': 2, }, snapshots_metadata[0]) self.assertDictContainsSubset({ 'commit_cmds': [{ 'cmd': 'create_new', 'title': 'A title', 'category': 'A category' }], 'committer_id': self.OWNER_ID, 'commit_message': ( 'New exploration created with title \'A title\'.'), 'commit_type': 'create', 'version_number': 1 }, snapshots_metadata[1]) self.assertGreaterEqual( snapshots_metadata[0]['created_on'], snapshots_metadata[1]['created_on']) # Using the old version of the exploration should raise an error. with self.assertRaisesRegexp(Exception, 'version 1, which is too old'): exp_services._save_exploration( 'committer_id_2', v1_exploration, '', []) # Another person modifies the exploration. new_change_list = [{ 'cmd': 'edit_exploration_property', 'property_name': 'title', 'new_value': 'New title' }] exp_services.update_exploration( 'committer_id_2', self.EXP_ID, new_change_list, 'Second commit.') snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 5) self.assertEqual(len(snapshots_metadata), 3) self.assertDictContainsSubset({ 'commit_cmds': new_change_list, 'committer_id': 'committer_id_2', 'commit_message': 'Second commit.', 'commit_type': 'edit', 'version_number': 3, }, snapshots_metadata[0]) self.assertDictContainsSubset({ 'commit_cmds': change_list, 'committer_id': self.OWNER_ID, 'commit_message': 'Changed title.', 'commit_type': 'edit', 'version_number': 2, }, snapshots_metadata[1]) self.assertDictContainsSubset({ 'commit_cmds': [{ 'cmd': 'create_new', 'title': 'A title', 'category': 'A category' }], 'committer_id': self.OWNER_ID, 'commit_message': ( 'New exploration created with title \'A title\'.'), 'commit_type': 'create', 'version_number': 1 }, snapshots_metadata[2]) self.assertGreaterEqual( snapshots_metadata[0]['created_on'], snapshots_metadata[1]['created_on'])
def test_versioning_with_add_and_delete_states(self): exploration = self.save_new_valid_exploration( self.EXP_ID, self.OWNER_ID) exploration.title = 'First title' exp_services._save_exploration( self.OWNER_ID, exploration, 'Changed title.', []) commit_dict_2 = { 'committer_id': self.OWNER_ID, 'commit_message': 'Changed title.', 'version_number': 2, } snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 5) self.assertEqual(len(snapshots_metadata), 2) exploration = exp_services.get_exploration_by_id(self.EXP_ID) exploration.add_states(['New state']) exp_services._save_exploration( 'committer_id_2', exploration, 'Added new state', []) commit_dict_3 = { 'committer_id': 'committer_id_2', 'commit_message': 'Added new state', 'version_number': 3, } snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 5) self.assertEqual(len(snapshots_metadata), 3) self.assertDictContainsSubset( commit_dict_3, snapshots_metadata[0]) self.assertDictContainsSubset(commit_dict_2, snapshots_metadata[1]) self.assertGreaterEqual( snapshots_metadata[0]['created_on'], snapshots_metadata[1]['created_on']) # Perform an invalid action: delete a state that does not exist. This # should not create a new version. with self.assertRaisesRegexp(ValueError, 'does not exist'): exploration.delete_state('invalid_state_name') # Now delete the new state. exploration.delete_state('New state') exp_services._save_exploration( 'committer_id_3', exploration, 'Deleted state: New state', []) commit_dict_4 = { 'committer_id': 'committer_id_3', 'commit_message': 'Deleted state: New state', 'version_number': 4, } snapshots_metadata = exp_services.get_exploration_snapshots_metadata( self.EXP_ID, 5) self.assertEqual(len(snapshots_metadata), 4) self.assertDictContainsSubset(commit_dict_4, snapshots_metadata[0]) self.assertDictContainsSubset(commit_dict_3, snapshots_metadata[1]) self.assertDictContainsSubset(commit_dict_2, snapshots_metadata[2]) self.assertGreaterEqual( snapshots_metadata[0]['created_on'], snapshots_metadata[1]['created_on']) self.assertGreaterEqual( snapshots_metadata[1]['created_on'], snapshots_metadata[2]['created_on']) # The final exploration should have exactly one state. exploration = exp_services.get_exploration_by_id(self.EXP_ID) self.assertEqual(len(exploration.states), 1)
def test_migration_then_reversion_maintains_valid_exploration(self): """This integration test simulates the behavior of the domain layer prior to the introduction of a states schema. In particular, it deals with an exploration that was created before any states schema migrations occur. The exploration is constructed using multiple change lists, then a migration job is run. The test thereafter tests if reverting to a version prior to the migration still maintains a valid exploration. It tests both the exploration domain object and the exploration model stored in the datastore for validity. Note: It is important to distinguish between when the test is testing the exploration domain versus its model. It is operating at the domain layer when using exp_fetchers.get_exploration_by_id. Otherwise, it loads the model explicitly using exp_models.ExplorationModel.get and then converts it to an exploration domain object for validation using exp_fetchers.get_exploration_from_model. This is NOT the same process as exp_fetchers.get_exploration_by_id as it skips many steps which include the conversion pipeline (which is crucial to this test). """ exp_id = 'exp_id2' # Create a exploration with states schema version 0. self.save_new_exp_with_states_schema_v0(exp_id, self.albert_id, 'Old Title') # Load the exploration without using the conversion pipeline. All of # these changes are to happen on an exploration with states schema # version 0. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) # In version 1, the title was 'Old title'. # In version 2, the title becomes 'New title'. exploration_model.title = 'New title' exploration_model.commit(self.albert_id, 'Changed title.', []) # Version 2 of exploration. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) # Store state id mapping model for new exploration. exploration = exp_fetchers.get_exploration_from_model( exploration_model) # In version 3, a new state is added. new_state = copy.deepcopy( self.VERSION_0_STATES_DICT[feconf.DEFAULT_INIT_STATE_NAME]) new_state['interaction']['id'] = 'TextInput' exploration_model.states['New state'] = new_state # Properly link in the new state to avoid an invalid exploration. init_state = exploration_model.states[feconf.DEFAULT_INIT_STATE_NAME] init_handler = init_state['interaction']['handlers'][0] init_handler['rule_specs'][0]['dest'] = 'New state' exploration_model.commit('committer_id_v3', 'Added new state', []) # Version 3 of exploration. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) # Store state id mapping model for new exploration. exploration = exp_fetchers.get_exploration_from_model( exploration_model) # Version 4 is an upgrade based on the migration job. # Start migration job on sample exploration. job_id = exp_jobs_one_off.ExplorationMigrationJobManager.create_new() exp_jobs_one_off.ExplorationMigrationJobManager.enqueue(job_id) self.process_and_flush_pending_tasks() # Verify the latest version of the exploration has the most up-to-date # states schema version. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) exploration = exp_fetchers.get_exploration_from_model( exploration_model, run_conversion=False) self.assertEqual(exploration.states_schema_version, feconf.CURRENT_STATE_SCHEMA_VERSION) # The exploration should be valid after conversion. exploration.validate(strict=True) # Version 5 is a reversion to version 1. exp_services.revert_exploration('committer_id_v4', exp_id, 4, 1) # The exploration model itself should now be the old version # (pre-migration). exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) self.assertEqual(exploration_model.states_schema_version, 0) # The exploration domain object should be updated since it ran through # the conversion pipeline. exploration = exp_fetchers.get_exploration_by_id(exp_id) # The reversion after migration should still be an up-to-date # exploration. exp_fetchers.get_exploration_by_id will automatically # keep it up-to-date. self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML) # The exploration should be valid after reversion. exploration.validate(strict=True) snapshots_metadata = exp_services.get_exploration_snapshots_metadata( exp_id) # These are used to verify the correct history has been recorded after # both migration and reversion. commit_dict_5 = { 'committer_id': 'committer_id_v4', 'commit_message': 'Reverted exploration to version 1', 'version_number': 5, } commit_dict_4 = { 'committer_id': feconf.MIGRATION_BOT_USERNAME, 'commit_message': 'Update exploration states from schema version 0 to %d.' % feconf.CURRENT_STATE_SCHEMA_VERSION, 'commit_cmds': [{ 'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION, 'from_version': '0', 'to_version': python_utils.UNICODE(feconf.CURRENT_STATE_SCHEMA_VERSION) }], 'version_number': 4, } # Ensure there have been 5 commits. self.assertEqual(len(snapshots_metadata), 5) # Ensure the correct commit logs were entered during both migration and # reversion. Also, ensure the correct commit command was written during # migration. self.assertDictContainsSubset(commit_dict_4, snapshots_metadata[3]) self.assertDictContainsSubset(commit_dict_5, snapshots_metadata[4]) self.assertLess(snapshots_metadata[3]['created_on_ms'], snapshots_metadata[4]['created_on_ms']) # Ensure that if a converted, then reverted, then converted exploration # is saved, it will be the up-to-date version within the datastore. exp_services.update_exploration(self.albert_id, exp_id, [], 'Resave after reversion') exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) exploration = exp_fetchers.get_exploration_from_model( exploration_model, run_conversion=False) # This exploration should be both up-to-date and valid. self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML) exploration.validate(strict=True)
def test_migration_then_reversion_maintains_valid_exploration(self): """This integration test simulates the behavior of the domain layer prior to the introduction of a states schema. In particular, it deals with an exploration that was created before any states schema migrations occur. The exploration is constructed using multiple change lists, then a migration is run. The test thereafter tests if reverting to a version prior to the migration still maintains a valid exploration. It tests both the exploration domain object and the exploration model stored in the datastore for validity. Note: It is important to distinguish between when the test is testing the exploration domain versus its model. It is operating at the domain layer when using exp_fetchers.get_exploration_by_id. Otherwise, it loads the model explicitly using exp_models.ExplorationModel.get and then converts it to an exploration domain object for validation using exp_fetchers.get_exploration_from_model. This is NOT the same process as exp_fetchers.get_exploration_by_id as it skips many steps which include the conversion pipeline (which is crucial to this test). """ exp_id = 'exp_id2' end_state_name = 'End' # Create an exploration with an old states schema version. swap_states_schema_41 = self.swap(feconf, 'CURRENT_STATE_SCHEMA_VERSION', 41) swap_exp_schema_46 = self.swap(exp_domain.Exploration, 'CURRENT_EXP_SCHEMA_VERSION', 46) with swap_states_schema_41, swap_exp_schema_46: self.save_new_valid_exploration(exp_id, self.albert_id, title='Old Title', end_state_name=end_state_name) caching_services.delete_multi( caching_services.CACHE_NAMESPACE_EXPLORATION, None, [exp_id]) # Load the exploration without using the conversion pipeline. All of # these changes are to happen on an exploration with states schema # version 41. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) # In version 1, the title was 'Old title'. # In version 2, the title becomes 'New title'. exploration_model.title = 'New title' exploration_model.commit(self.albert_id, 'Changed title.', []) # Version 2 of exploration. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) # Store state id mapping model for new exploration. exp_fetchers.get_exploration_from_model(exploration_model) # In version 3, a new state is added. exploration_model.states['New state'] = { 'solicit_answer_details': False, 'written_translations': { 'translations_mapping': { 'content': {}, 'default_outcome': {}, 'ca_placeholder_0': {}, } }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': {}, 'default_outcome': {}, 'ca_placeholder_0': {}, } }, 'param_changes': [], 'classifier_model_id': None, 'content': { 'content_id': 'content', 'html': '<p>Unicode Characters ����</p>' }, 'next_content_id_index': 5, 'interaction': { 'answer_groups': [], 'confirmed_unclassified_answers': [], 'customization_args': { 'buttonText': { 'value': { 'content_id': 'ca_placeholder_0', 'unicode_str': 'Click me!', }, }, }, 'default_outcome': { 'dest': end_state_name, 'feedback': { 'content_id': 'default_outcome', 'html': '', }, 'labelled_as_correct': False, 'missing_prerequisite_skill_id': None, 'param_changes': [], 'refresher_exploration_id': None, }, 'hints': [], 'id': 'Continue', 'solution': None, }, } # Properly link in the new state to avoid an invalid exploration. init_state = exploration_model.states[feconf.DEFAULT_INIT_STATE_NAME] init_state['interaction']['default_outcome']['dest'] = 'New state' exploration_model.commit('committer_id_v3', 'Added new state', []) # Version 3 of exploration. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) # Version 4 is an upgrade based on the migration job. commit_cmds = [ exp_domain.ExplorationChange({ 'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION, 'from_version': str(exploration_model.states_schema_version), 'to_version': str(feconf.CURRENT_STATE_SCHEMA_VERSION) }) ] exp_services.update_exploration( feconf.MIGRATION_BOT_USERNAME, exploration_model.id, commit_cmds, 'Update exploration states from schema version %d to %d.' % (exploration_model.states_schema_version, feconf.CURRENT_STATE_SCHEMA_VERSION)) # Verify the latest version of the exploration has the most up-to-date # states schema version. exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) exploration = exp_fetchers.get_exploration_from_model( exploration_model, run_conversion=False) self.assertEqual(exploration.states_schema_version, feconf.CURRENT_STATE_SCHEMA_VERSION) # The exploration should be valid after conversion. exploration.validate(strict=True) # Version 5 is a reversion to version 1. exp_services.revert_exploration('committer_id_v4', exp_id, 4, 1) # The exploration model itself should now be the old version # (pre-migration). exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) self.assertEqual(exploration_model.states_schema_version, 41) # The exploration domain object should be updated since it ran through # the conversion pipeline. exploration = exp_fetchers.get_exploration_by_id(exp_id) # The reversion after migration should still be an up-to-date # exploration. exp_fetchers.get_exploration_by_id will automatically # keep it up-to-date. self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML) # The exploration should be valid after reversion. exploration.validate(strict=True) snapshots_metadata = exp_services.get_exploration_snapshots_metadata( exp_id) # These are used to verify the correct history has been recorded after # both migration and reversion. commit_dict_5 = { 'committer_id': 'committer_id_v4', 'commit_message': 'Reverted exploration to version 1', 'version_number': 5, } commit_dict_4 = { 'committer_id': feconf.MIGRATION_BOT_USERNAME, 'commit_message': 'Update exploration states from schema version 41 to %d.' % feconf.CURRENT_STATE_SCHEMA_VERSION, 'commit_cmds': [{ 'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION, 'from_version': '41', 'to_version': str(feconf.CURRENT_STATE_SCHEMA_VERSION) }], 'version_number': 4, } # Ensure there have been 5 commits. self.assertEqual(len(snapshots_metadata), 5) # Ensure the correct commit logs were entered during both migration and # reversion. Also, ensure the correct commit command was written during # migration. # These asserts check whether one dict is subset of the other. # The format is assertDictEqual(a, {**a, **b}) where a is the superset # and b is the subset. self.assertDictEqual(snapshots_metadata[3], { **snapshots_metadata[3], **commit_dict_4 }) self.assertDictEqual(snapshots_metadata[4], { **snapshots_metadata[4], **commit_dict_5 }) self.assertLess(snapshots_metadata[3]['created_on_ms'], snapshots_metadata[4]['created_on_ms']) # Ensure that if a converted, then reverted, then converted exploration # is saved, it will be the up-to-date version within the datastore. exp_services.update_exploration(self.albert_id, exp_id, [], 'Resave after reversion') exploration_model = exp_models.ExplorationModel.get(exp_id, strict=True, version=None) exploration = exp_fetchers.get_exploration_from_model( exploration_model, run_conversion=False) # This exploration should be both up-to-date and valid. self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML) exploration.validate()