def get_summary_of_exploration(exploration): """Create ExplorationSummary domain object for a given Exploration domain object and return it. """ exp_rights = exp_models.ExplorationRightsModel.get_by_id(exploration.id) exp_summary_model = exp_models.ExpSummaryModel.get_by_id(exploration.id) if exp_summary_model: old_exp_summary = get_exploration_summary_from_model(exp_summary_model) ratings = old_exp_summary.ratings or feconf.get_empty_ratings() else: ratings = feconf.get_empty_ratings() exploration_model_last_updated = exploration.last_updated exploration_model_created_on = exploration.created_on exp_summary = exp_domain.ExplorationSummary( exploration.id, exploration.title, exploration.category, exploration.objective, exploration.language_code, exploration.tags, ratings, exp_rights.status, exp_rights.community_owned, exp_rights.owner_ids, exp_rights.editor_ids, exp_rights.viewer_ids, exploration.version, exploration_model_created_on, exploration_model_last_updated ) return exp_summary
def test_for_featured_explorations(self): """Note that EXP_ID_1 is public, and EXP_ID_2 is publicized. The call to get_featured_explorations() should only return [EXP_ID_2]. """ rights_manager.publicize_exploration(self.admin_id, self.EXP_ID_2) featured_exploration_summaries = ( summary_services.get_featured_exploration_summary_dicts([ feconf.DEFAULT_LANGUAGE_CODE])) expected_summary = { 'status': u'publicized', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/images/subjects/Lightbulb.svg', 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_2, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } self.assertDictContainsSubset( expected_summary, featured_exploration_summaries[0])
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration # EXP_ID_2 -- pubished exploration # EXP_ID_3 -- deleted exploration # Should only return [EXP_ID_2] displayable_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3])) expected_summary = { 'status': u'public', 'thumbnail_bg_color': '#05a69a', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/images/gallery/thumbnails/Lightbulb.svg', 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'human_readable_contributors_summary': {self.ALBERT_NAME: 2}, 'id': self.EXP_ID_2, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'Exploration 2 Albert title', 'num_views': 0, 'objective': u'An objective', 'contributor_names': [self.ALBERT_NAME] } self.assertIn('last_updated_msec', displayable_summaries[0]) self.assertDictContainsSubset(expected_summary, displayable_summaries[0])
def test_get_library_groups(self): """The exploration with id '2' is an exploration in the Mathematics category. The call to get_library_groups() should return the exploration as part of the Mathematics & Statistics group. """ library_groups = summary_services.get_library_groups([]) expected_exploration_summary_dict = { 'category': u'Algorithms', 'community_owned': True, 'id': '2', 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'num_views': 0, 'objective': u'discover the binary search algorithm', 'ratings': feconf.get_empty_ratings(), 'status': u'public', 'tags': [], 'title': u'The Lazy Magician', 'thumbnail_bg_color': '#d0982a', 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Algorithms.svg'), } expected_group = { 'categories': ['Algorithms', 'Computing', 'Programming'], 'header_i18n_id': 'I18N_LIBRARY_GROUPS_COMPUTING', } self.assertEqual(len(library_groups), 1) self.assertDictContainsSubset(expected_group, library_groups[0]) self.assertEqual( len(library_groups[0]['activity_summary_dicts']), 1) actual_exploration_summary_dict = ( library_groups[0]['activity_summary_dicts'][0]) self.assertDictContainsSubset(expected_exploration_summary_dict, ( actual_exploration_summary_dict))
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration # EXP_ID_2 -- pubished exploration # EXP_ID_3 -- deleted exploration # Should only return [EXP_ID_2] displayable_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3])) expected_summary = { 'status': u'public', 'thumbnail_bg_color': '#05a69a', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/images/gallery/thumbnails/Lightbulb.svg', 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'human_readable_contributors_summary': { self.ALBERT_NAME: { 'num_commits': 2, 'profile_picture_data_url': None } }, 'id': self.EXP_ID_2, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'Exploration 2 Albert title', 'num_views': 0, 'objective': u'An objective' } self.assertIn('last_updated_msec', displayable_summaries[0]) self.assertDictContainsSubset(expected_summary, displayable_summaries[0])
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration # EXP_ID_2 -- pubished exploration # EXP_ID_3 -- deleted exploration # Should only return [EXP_ID_2] displayable_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3])) self.assertEqual(len(displayable_summaries), 1) self.assertEqual( displayable_summaries[0]['id'], self.EXP_ID_2) self.assertEqual( displayable_summaries[0]['status'], rights_manager.ACTIVITY_STATUS_PUBLIC) self.assertEqual( displayable_summaries[0]['community_owned'], False) self.assertEqual( displayable_summaries[0]['language_code'], feconf.DEFAULT_LANGUAGE_CODE) self.assertEqual( displayable_summaries[0]['category'], 'A category') self.assertEqual( displayable_summaries[0]['ratings'], feconf.get_empty_ratings()) self.assertEqual( displayable_summaries[0]['title'], 'Exploration 2 Albert title') self.assertEqual( displayable_summaries[0]['contributor_names'], [self.ALBERT_NAME]) self.assertEqual( displayable_summaries[0]['objective'], 'An objective') self.assertEqual(displayable_summaries[0]['num_views'], 0) self.assertIn('last_updated_msec', displayable_summaries[0])
def test_for_featured_explorations(self): """Note that both EXP_ID_1 and EXP_ID_2 are public. However, only EXP_ID_2 is featured, so the call to get_featured_explorations() should only return [EXP_ID_2]. """ activity_services.update_featured_activity_references([ activity_domain.ActivityReference( feconf.ACTIVITY_TYPE_EXPLORATION, self.EXP_ID_2) ]) featured_activity_summaries = ( summary_services.get_featured_activity_summary_dicts([ feconf.DEFAULT_LANGUAGE_CODE])) self.assertEqual(len(featured_activity_summaries), 1) self.assertDictContainsSubset({ 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_2, 'category': 'A category', 'ratings': feconf.get_empty_ratings(), 'title': 'A title', 'num_views': 0, 'objective': 'An objective' }, featured_activity_summaries[0])
def test_for_featured_explorations(self): """Note that EXP_ID_1 is public, and EXP_ID_2 is publicized. The call to get_featured_explorations() should only return [EXP_ID_2]. """ rights_manager.publicize_exploration(self.admin_id, self.EXP_ID_2) featured_exploration_summaries = ( summary_services.get_featured_exploration_summary_dicts( [feconf.DEFAULT_LANGUAGE_CODE])) expected_summary = { 'status': u'publicized', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/images/subjects/Lightbulb.svg', 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_2, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } self.assertDictContainsSubset(expected_summary, featured_exploration_summaries[0])
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration # EXP_ID_2 -- pubished exploration # EXP_ID_3 -- deleted exploration # Should only return [EXP_ID_2] displayable_summaries = summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3] ) expected_summary = { "status": u"public", "thumbnail_bg_color": "#05a69a", "community_owned": False, "tags": [], "thumbnail_icon_url": "/images/gallery/thumbnails/Lightbulb.svg", "language_code": feconf.DEFAULT_LANGUAGE_CODE, "human_readable_contributors_summary": {self.ALBERT_NAME: 2}, "id": self.EXP_ID_2, "category": u"A category", "ratings": feconf.get_empty_ratings(), "title": u"Exploration 2 Albert title", "num_views": 0, "objective": u"An objective", "contributor_names": [self.ALBERT_NAME], } self.assertIn("last_updated_msec", displayable_summaries[0]) self.assertDictContainsSubset(expected_summary, displayable_summaries[0])
def test_get_library_groups(self): """The exploration with id '2' is an exploration in the Mathematics category. The call to get_library_groups() should return the exploration as part of the Mathematics & Statistics group. """ library_groups = summary_services.get_library_groups([]) expected_exploration_summary_dict = { 'category': u'Algorithms', 'community_owned': True, 'id': '2', 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'num_views': 0, 'objective': u'discover the binary search algorithm', 'ratings': feconf.get_empty_ratings(), 'status': u'public', 'tags': [], 'title': u'The Lazy Magician', 'thumbnail_bg_color': '#d0982a', 'thumbnail_icon_url': '/subjects/Algorithms.svg', } expected_group = { 'categories': ['Algorithms', 'Computing', 'Programming'], 'header_i18n_id': 'I18N_LIBRARY_GROUPS_COMPUTING', } self.assertEqual(len(library_groups), 1) self.assertDictContainsSubset(expected_group, library_groups[0]) self.assertEqual( len(library_groups[0]['activity_summary_dicts']), 1) actual_exploration_summary_dict = ( library_groups[0]['activity_summary_dicts'][0]) self.assertDictContainsSubset(expected_exploration_summary_dict, ( actual_exploration_summary_dict))
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration owned by Albert. # EXP_ID_2 -- pubished exploration owned by Albert. # EXP_ID_3 -- deleted exploration. # EXP_ID_5 -- private exploration owned by Bob. # Should only return [EXP_ID_2]. displayable_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3, self.EXP_ID_5])) expected_summary = { 'category': u'A category', 'community_owned': False, 'id': self.EXP_ID_2, 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'num_views': 0, 'objective': u'An objective', 'ratings': feconf.get_empty_ratings(), 'status': 'public', 'tags': [], 'thumbnail_bg_color': '#a33f40', 'thumbnail_icon_url': '/subjects/Lightbulb.svg', 'title': u'Exploration 2 Albert title', } self.assertIn('last_updated_msec', displayable_summaries[0]) self.assertDictContainsSubset( expected_summary, displayable_summaries[0])
def test_for_featured_explorations(self): """Note that both EXP_ID_1 and EXP_ID_2 are public. However, only EXP_ID_2 is featured, so the call to get_featured_explorations() should only return [EXP_ID_2]. """ activity_services.update_featured_activity_references([ activity_domain.ActivityReference( constants.ACTIVITY_TYPE_EXPLORATION, self.EXP_ID_2) ]) featured_activity_summaries = ( summary_services.get_featured_activity_summary_dicts([ constants.DEFAULT_LANGUAGE_CODE])) self.assertEqual(len(featured_activity_summaries), 1) self.assertDictContainsSubset({ 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/subjects/Lightbulb.svg', 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_2, 'category': 'A category', 'ratings': feconf.get_empty_ratings(), 'title': 'A title', 'num_views': 0, 'objective': 'An objective' }, featured_activity_summaries[0])
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration owned by Albert # EXP_ID_2 -- pubished exploration owned by Albert # EXP_ID_3 -- deleted exploration # EXP_ID_5 -- private exploration owned by Bob # Should only return [EXP_ID_2] displayable_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3, self.EXP_ID_5])) expected_summary = { 'category': u'A category', 'community_owned': False, 'id': self.EXP_ID_2, 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'num_views': 0, 'objective': u'An objective', 'ratings': feconf.get_empty_ratings(), 'status': 'public', 'tags': [], 'thumbnail_bg_color': '#a33f40', 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'title': u'Exploration 2 Albert title', } self.assertIn('last_updated_msec', displayable_summaries[0]) self.assertDictContainsSubset(expected_summary, displayable_summaries[0])
def assign_rating_to_exploration(user_id, exploration_id, new_rating): """Records the rating awarded by the user to the exploration in both the user-specific data and exploration summary. This function validates the exploration id but not the user id. Args: user_id: str. The id of the user assigning the rating. exploration_id: str. The id of the exploration that is assigned a rating. new_rating: int. Value of assigned rating, should be between 1 and 5 inclusive. """ if not isinstance(new_rating, int): raise ValueError( 'Expected the rating to be an integer, received %s' % new_rating) if new_rating not in ALLOWED_RATINGS: raise ValueError('Expected a rating 1-5, received %s.' % new_rating) try: exp_fetchers.get_exploration_by_id(exploration_id) except: raise Exception('Invalid exploration id %s' % exploration_id) def _update_user_rating(): """Updates the user rating of the exploration. Returns the old rating before updation. """ exp_user_data_model = user_models.ExplorationUserDataModel.get( user_id, exploration_id) if exp_user_data_model: old_rating = exp_user_data_model.rating else: old_rating = None exp_user_data_model = user_models.ExplorationUserDataModel.create( user_id, exploration_id) exp_user_data_model.rating = new_rating exp_user_data_model.rated_on = datetime.datetime.utcnow() exp_user_data_model.put() return old_rating old_rating = transaction_services.run_in_transaction(_update_user_rating) exploration_summary = exp_fetchers.get_exploration_summary_by_id( exploration_id) if not exploration_summary.ratings: exploration_summary.ratings = feconf.get_empty_ratings() exploration_summary.ratings[str(new_rating)] += 1 if old_rating: exploration_summary.ratings[str(old_rating)] -= 1 event_services.RateExplorationEventHandler.record( exploration_id, user_id, new_rating, old_rating) exploration_summary.scaled_average_rating = ( exp_services.get_scaled_average_rating( exploration_summary.ratings)) exp_services.save_exploration_summary(exploration_summary)
def save_new_exp_with_states_schema_v34(self, exp_id, user_id, states_dict): """Saves a new default exploration with a default version 34 states dictionary. This function should only be used for creating explorations in tests involving migration of datastore explorations that use an old states schema version. Note that it makes an explicit commit to the datastore instead of using the usual functions for updating and creating explorations. This is because the latter approach would result in an exploration with the *current* states schema version. Args: exp_id: str. The exploration ID. user_id: str. The user_id of the creator. states_dict: dict. The dict representation of all the states. """ exp_model = exp_models.ExplorationModel( id=exp_id, category='category', title='title', objective='Old objective', language_code='en', tags=[], blurb='', author_notes='', states_schema_version=34, init_state_name=feconf.DEFAULT_INIT_STATE_NAME, states=states_dict, param_specs={}, param_changes=[] ) rights_manager.create_new_exploration_rights(exp_id, user_id) commit_message = 'New exploration created with title \'title\'.' exp_model.commit( user_id, commit_message, [{ 'cmd': 'create_new', 'title': 'title', 'category': 'category', }]) exp_rights = exp_models.ExplorationRightsModel.get_by_id(exp_id) exp_summary_model = exp_models.ExpSummaryModel( id=exp_id, title='title', category='category', objective='Old objective', language_code='en', tags=[], ratings=feconf.get_empty_ratings(), scaled_average_rating=feconf.EMPTY_SCALED_AVERAGE_RATING, status=exp_rights.status, community_owned=exp_rights.community_owned, owner_ids=exp_rights.owner_ids, contributor_ids=[], contributors_summary={}, ) exp_summary_model.update_timestamps() exp_summary_model.put()
def assign_rating_to_exploration(user_id, exploration_id, new_rating): """Records the rating awarded by the user to the exploration in both the user-specific data and exploration summary. This function validates the exploration id but not the user id. Args: user_id: str. The id of the user assigning the rating. exploration_id: str. The id of the exploration that is assigned a rating. new_rating: int. Value of assigned rating, should be between 1 and 5 inclusive. """ if not isinstance(new_rating, int): raise ValueError( 'Expected the rating to be an integer, received %s' % new_rating) if new_rating not in ALLOWED_RATINGS: raise ValueError('Expected a rating 1-5, received %s.' % new_rating) try: exp_services.get_exploration_by_id(exploration_id) except: raise Exception('Invalid exploration id %s' % exploration_id) def _update_user_rating(): exp_user_data_model = user_models.ExplorationUserDataModel.get( user_id, exploration_id) if exp_user_data_model: old_rating = exp_user_data_model.rating else: old_rating = None exp_user_data_model = user_models.ExplorationUserDataModel.create( user_id, exploration_id) exp_user_data_model.rating = new_rating exp_user_data_model.rated_on = datetime.datetime.utcnow() exp_user_data_model.put() return old_rating old_rating = transaction_services.run_in_transaction(_update_user_rating) exploration_summary = exp_services.get_exploration_summary_by_id( exploration_id) if not exploration_summary.ratings: exploration_summary.ratings = feconf.get_empty_ratings() exploration_summary.ratings[str(new_rating)] += 1 if old_rating: exploration_summary.ratings[str(old_rating)] -= 1 event_services.RateExplorationEventHandler.record( exploration_id, user_id, new_rating, old_rating) exploration_summary.scaled_average_rating = ( exp_services.get_scaled_average_rating( exploration_summary.ratings)) exp_services.save_exploration_summary(exploration_summary)
def assign_rating_to_exploration(user_id, exploration_id, new_rating): """Records the rating awarded by the user to the exploration in both the user-specific data and exploration summary. It validates the exploration id but not the user id. - 'new_rating' should be an integer between 1 and 5 """ if not isinstance(new_rating, int): raise ValueError('Expected the rating to be an integer, received %s' % new_rating) if new_rating not in ALLOWED_RATINGS: raise ValueError('Expected a rating 1-5, received %s.' % new_rating) try: exp_services.get_exploration_by_id(exploration_id) except: raise Exception('Invalid exploration id %s' % exploration_id) def _update_user_rating(): exp_user_data_model = user_models.ExplorationUserDataModel.get( user_id, exploration_id) if exp_user_data_model: old_rating = exp_user_data_model.rating else: old_rating = None exp_user_data_model = user_models.ExplorationUserDataModel.create( user_id, exploration_id) exp_user_data_model.rating = new_rating exp_user_data_model.rated_on = datetime.datetime.utcnow() exp_user_data_model.put() return old_rating old_rating = transaction_services.run_in_transaction(_update_user_rating) exploration_summary = exp_services.get_exploration_summary_by_id( exploration_id) if not exploration_summary.ratings: exploration_summary.ratings = feconf.get_empty_ratings() exploration_summary.ratings[str(new_rating)] += 1 if old_rating: exploration_summary.ratings[str(old_rating)] -= 1 exploration_summary.scaled_average_rating = ( exp_services.get_scaled_average_rating(exploration_summary.ratings)) exp_services.save_exploration_summary(exploration_summary)
def assign_rating(user_id, exploration_id, new_rating): """Records the rating awarded by the user to the exploration in both the user-specific data and exploration summary. It validates the exploration id but not the user id. - 'new_rating' should be an integer between 1 and 5 """ if not isinstance(new_rating, int): raise ValueError( 'Expected the rating to be an integer, received %s' % new_rating) ALLOWED_RATINGS = [1, 2, 3, 4, 5] if new_rating not in ALLOWED_RATINGS: raise ValueError('Expected a rating 1-5, received: %s.' % new_rating) try: exp_services.get_exploration_by_id(exploration_id) except: raise Exception('Invalid exploration id %s' % exploration_id) def _update_user_rating(): exp_user_data_model = user_models.ExplorationUserDataModel.get( user_id, exploration_id) if exp_user_data_model: old_rating = exp_user_data_model.rating else: old_rating = None exp_user_data_model = user_models.ExplorationUserDataModel.create( user_id, exploration_id) exp_user_data_model.rating = new_rating exp_user_data_model.rated_on = datetime.datetime.utcnow() exp_user_data_model.put() return old_rating old_rating = transaction_services.run_in_transaction(_update_user_rating) exploration_summary = exp_services.get_exploration_summary_by_id( exploration_id) if not exploration_summary.ratings: exploration_summary.ratings = feconf.get_empty_ratings() exploration_summary.ratings[str(new_rating)] += 1 if old_rating: exploration_summary.ratings[str(old_rating)] -= 1 exp_services.save_exploration_summary(exploration_summary)
def test_get_displayable_exp_summary_dicts_matching_ids(self): # A list of exp_id's are passed in: # EXP_ID_1 -- private exploration owned by Albert # EXP_ID_2 -- pubished exploration owned by Albert # EXP_ID_3 -- deleted exploration # EXP_ID_5 -- private exploration owned by Bob # Should only return [EXP_ID_2] displayable_summaries = ( summary_services.get_displayable_exp_summary_dicts_matching_ids( [self.EXP_ID_1, self.EXP_ID_2, self.EXP_ID_3, self.EXP_ID_5])) expected_summary = { 'category': u'A category', 'community_owned': False, 'human_readable_contributors_summary': { self.ALBERT_NAME: { 'num_commits': 2, 'profile_picture_data_url': (user_services.DEFAULT_IDENTICON_DATA_URL) } }, 'id': self.EXP_ID_2, 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'num_views': 0, 'objective': u'An objective', 'ratings': feconf.get_empty_ratings(), 'status': 'public', 'tags': [], 'thumbnail_bg_color': '#a33f40', 'thumbnail_icon_url': '/images/subjects/Lightbulb.svg', 'title': u'Exploration 2 Albert title', } self.assertIn('last_updated_msec', displayable_summaries[0]) self.assertDictContainsSubset(expected_summary, displayable_summaries[0])
def _run_batch_job_once_and_verify_output( self, exp_specs, default_title='A title', default_category='A category', default_status=rights_manager.ACTIVITY_STATUS_PUBLIC): """Run batch job for creating exploration summaries once and verify its output. exp_specs is a list of dicts with exploration specifications. Allowed keys are category, status, title. If a key is not specified, the default value is used. """ with self.swap(jobs_registry, 'ONE_OFF_JOB_MANAGERS', self.ONE_OFF_JOB_MANAGERS_FOR_TESTS): default_spec = { 'title': default_title, 'category': default_category, 'status': default_status } self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME) self.login(self.ADMIN_EMAIL) admin_id = self.get_user_id_from_email(self.ADMIN_EMAIL) self.set_admins([self.ADMIN_USERNAME]) admin = user_services.UserActionsInfo(admin_id) # Create and delete an exploration (to make sure job handles # deleted explorations correctly). exp_id = '100' self.save_new_valid_exploration(exp_id, admin_id, title=default_spec['title'], category=default_spec['category']) exploration = exp_services.get_exploration_by_id(exp_id) exp_services.delete_exploration(admin_id, exp_id) # Get dummy explorations. num_exps = len(exp_specs) expected_job_output = {} for ind in range(num_exps): exp_id = str(ind) spec = default_spec spec.update(exp_specs[ind]) self.save_new_valid_exploration(exp_id, admin_id, title=spec['title'], category=spec['category']) exploration = exp_services.get_exploration_by_id(exp_id) # Publish exploration. if spec['status'] == rights_manager.ACTIVITY_STATUS_PUBLIC: rights_manager.publish_exploration(admin, exp_id) # Do not include user_id here, so all explorations are not # editable for now (will be updated depending on user_id # in galleries). exp_rights_model = exp_models.ExplorationRightsModel.get( exp_id) exploration = exp_services.get_exploration_by_id(exp_id) exploration_model_last_updated = exploration.last_updated exploration_model_created_on = exploration.created_on first_published_msec = (exp_rights_model.first_published_msec) # Manually create the expected summary specifying title, # category, etc. expected_job_output[exp_id] = exp_domain.ExplorationSummary( exp_id, spec['title'], spec['category'], exploration.objective, exploration.language_code, exploration.tags, feconf.get_empty_ratings(), feconf.EMPTY_SCALED_AVERAGE_RATING, spec['status'], exp_rights_model.community_owned, exp_rights_model.owner_ids, exp_rights_model.editor_ids, exp_rights_model.translator_ids, exp_rights_model.viewer_ids, [admin_id], {admin_id: 1}, exploration.version, exploration_model_created_on, exploration_model_last_updated, first_published_msec) # Note: Calling constructor for fields that are not required # and have no default value does not work, because # unspecified fields will be empty list in # expected_job_output but will be unspecified in # actual_job_output. if exploration.tags: expected_job_output[exp_id].tags = exploration.tags if exp_rights_model.owner_ids: expected_job_output[exp_id].owner_ids = ( exp_rights_model.owner_ids) if exp_rights_model.editor_ids: expected_job_output[exp_id].editor_ids = ( exp_rights_model.editor_ids) if exp_rights_model.translator_ids: expected_job_output[exp_id].translator_ids = ( exp_rights_model.translator_ids) if exp_rights_model.viewer_ids: expected_job_output[exp_id].viewer_ids = ( exp_rights_model.viewer_ids) if exploration.version: expected_job_output[exp_id].version = (exploration.version) # Run batch job. job_id = ( exp_jobs_one_off.ExpSummariesCreationOneOffJob.create_new()) exp_jobs_one_off.ExpSummariesCreationOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() # Get and check job output. actual_job_output = exp_services.get_all_exploration_summaries() self.assertEqual(actual_job_output.keys(), expected_job_output.keys()) # Note: 'exploration_model_last_updated' is not expected to be the # same, because it is now read from the version model representing # the exploration's history snapshot, and not the ExplorationModel. simple_props = [ 'id', 'title', 'category', 'objective', 'language_code', 'tags', 'ratings', 'status', 'community_owned', 'owner_ids', 'editor_ids', 'translator_ids', 'viewer_ids', 'contributor_ids', 'contributors_summary', 'version', 'exploration_model_created_on' ] for exp_id in actual_job_output: for prop in simple_props: self.assertEqual( getattr(actual_job_output[exp_id], prop), getattr(expected_job_output[exp_id], prop))
def test_for_recently_published_explorations(self): """Tests for recently published explorations.""" recently_published_exploration_summaries = ( summary_services.get_recently_published_exp_summary_dicts( feconf.RECENTLY_PUBLISHED_QUERY_LIMIT_FOR_LIBRARY_PAGE)) test_summary_1 = { 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/subjects/Lightbulb.svg', 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_1, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } test_summary_2 = { 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/subjects/Lightbulb.svg', 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_2, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } test_summary_3 = { 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': '/subjects/Lightbulb.svg', 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_3, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } self.assertDictContainsSubset( test_summary_3, recently_published_exploration_summaries[0]) self.assertDictContainsSubset( test_summary_1, recently_published_exploration_summaries[1]) self.assertDictContainsSubset( test_summary_2, recently_published_exploration_summaries[2]) # Test that editing an exploration does not change its # 'recently-published' status. exp_services.update_exploration( self.albert_id, self.EXP_ID_1, [{ 'cmd': 'edit_exploration_property', 'property_name': 'title', 'new_value': 'New title' }], 'Changed title.') recently_published_exploration_summaries = ( summary_services.get_recently_published_exp_summary_dicts( feconf.RECENTLY_PUBLISHED_QUERY_LIMIT_FOR_LIBRARY_PAGE)) self.assertEqual( recently_published_exploration_summaries[1]['title'], 'New title') self.assertDictContainsSubset( test_summary_3, recently_published_exploration_summaries[0])
def _run_batch_job_once_and_verify_output( self, exp_specs, default_title='A title', default_category='A category', default_status=rights_manager.EXPLORATION_STATUS_PUBLICIZED): """Run batch job for creating exploration summaries once and verify its output. exp_specs is a list of dicts with exploration specifications. Allowed keys are category, status, title. If a key is not specified, the default value is taken. """ from core.domain import exp_services with self.swap(jobs_registry, 'ONE_OFF_JOB_MANAGERS', self.ONE_OFF_JOB_MANAGERS_FOR_TESTS): # default specs default_specs = { 'title': default_title, 'category': default_category, 'status': default_status } self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME) self.login(self.ADMIN_EMAIL) self.ADMIN_ID = self.get_user_id_from_email(self.ADMIN_EMAIL) self.set_admins([self.ADMIN_EMAIL]) # create and delete an exploration (to make sure job handles # deleted explorations correctly) exp_id = '100' self.save_new_valid_exploration(exp_id, self.ADMIN_ID, title=default_specs['title'], category=default_specs['category']) exploration = exp_services.get_exploration_by_id(exp_id) exp_services.delete_exploration(self.ADMIN_ID, exp_id) # get dummy explorations num_exps = len(exp_specs) expected_job_output = {} for ind in range(num_exps): exp_id = str(ind) spec = default_specs spec.update(exp_specs[ind]) self.save_new_valid_exploration(exp_id, self.ADMIN_ID, title=spec['title'], category=spec['category']) exploration = exp_services.get_exploration_by_id(exp_id) # publish or publicize exploration if spec['status'] == rights_manager.EXPLORATION_STATUS_PUBLIC: rights_manager.publish_exploration(self.ADMIN_ID, exp_id) elif (spec['status'] == rights_manager.EXPLORATION_STATUS_PUBLICIZED): rights_manager.publish_exploration(self.ADMIN_ID, exp_id) rights_manager.publicize_exploration(self.ADMIN_ID, exp_id) # do not include user_id here, so all explorations are not # editable for now (will be updated depending on user_id # in galleries) exp_rights_model = exp_models.ExplorationRightsModel.get( exp_id) exploration = exp_services.get_exploration_by_id(exp_id) exploration_model_last_updated = exploration.last_updated exploration_model_created_on = exploration.created_on # manually create the expectated summary specifying title, # category, etc expected_job_output[exp_id] = exp_domain.ExplorationSummary( exp_id, spec['title'], spec['category'], exploration.objective, exploration.language_code, exploration.tags, feconf.get_empty_ratings(), spec['status'], exp_rights_model.community_owned, exp_rights_model.owner_ids, exp_rights_model.editor_ids, exp_rights_model.viewer_ids, exploration.version, exploration_model_created_on, exploration_model_last_updated) # calling constructor for fields that are not required # and have no default value does not work b/c # unspecified fields will be empty list in # expected_job_output but will be unspecified in # actual_job_output if exploration.tags: expected_job_output[exp_id].tags = exploration.tags if exp_rights_model.owner_ids: expected_job_output[exp_id].owner_ids = ( exp_rights_model.owner_ids) if exp_rights_model.editor_ids: expected_job_output[exp_id].editor_ids = ( exp_rights_model.editor_ids) if exp_rights_model.viewer_ids: expected_job_output[exp_id].viewer_ids = ( exp_rights_model.viewer_ids) if exploration.version: expected_job_output[exp_id].version = (exploration.version) # run batch job job_id = exp_jobs.ExpSummariesCreationOneOffJob.create_new() exp_jobs.ExpSummariesCreationOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() # get job output actual_job_output = exp_services.get_all_exploration_summaries() # check job output self.assertEqual(actual_job_output.keys(), expected_job_output.keys()) simple_props = [ 'id', 'title', 'category', 'objective', 'language_code', 'tags', 'ratings', 'status', 'community_owned', 'owner_ids', 'editor_ids', 'viewer_ids', 'version', 'exploration_model_created_on', 'exploration_model_last_updated' ] for exp_id in actual_job_output: for prop in simple_props: self.assertEqual( getattr(actual_job_output[exp_id], prop), getattr(expected_job_output[exp_id], prop))
def test_migration_job_creates_appropriate_classifier_models(self): """Tests that the exploration migration job creates appropriate classifier data models for explorations. """ 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: exp_model = exp_models.ExplorationModel( id=self.NEW_EXP_ID, category='category', title=self.EXP_TITLE, objective='Old objective', language_code='en', tags=[], blurb='', author_notes='', states_schema_version=41, init_state_name=feconf.DEFAULT_INIT_STATE_NAME, states={ 'END': { 'classifier_model_id': None, 'content': { 'content_id': 'content', 'html': 'Congratulations, you have finished!', }, 'interaction': { 'answer_groups': [], 'confirmed_unclassified_answers': [], 'customization_args': { 'recommendedExplorationIds': { 'value': [] }, }, 'default_outcome': None, 'hints': [], 'id': 'EndExploration', 'solution': None, }, 'next_content_id_index': 0, 'param_changes': [], 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': {}, } }, 'solicit_answer_details': False, 'written_translations': { 'translations_mapping': { 'content': {}, } } }, 'Introduction': { 'classifier_model_id': None, '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': None, 'param_changes': [], 'refresher_exploration_id': None, }, 'rule_specs': [{ 'inputs': { 'x': { 'contentId': 'rule_input_3', 'normalizedStrSet': ['InputString'] } }, 'rule_type': 'Equals', }], 'tagged_skill_misconception_id': None, 'training_data': ['answer1', 'answer2', 'answer3'], }], 'confirmed_unclassified_answers': [], 'customization_args': { 'placeholder': { 'value': { 'content_id': 'ca_placeholder_2', 'unicode_str': '', }, }, 'rows': { 'value': 1 }, }, 'default_outcome': { 'dest': 'Introduction', 'feedback': { 'content_id': 'default_outcome', 'html': '' }, 'labelled_as_correct': False, 'missing_prerequisite_skill_id': None, 'param_changes': [], 'refresher_exploration_id': None, }, 'hints': [], 'id': 'TextInput', 'solution': None, }, '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': {}, } }, }, }, param_specs={}, param_changes=[]) rights_manager.create_new_exploration_rights( self.NEW_EXP_ID, self.albert_id) commit_message = ('New exploration created with title \'%s\'.' % self.EXP_TITLE) exp_model.commit(self.albert_id, commit_message, [{ 'cmd': 'create_new', 'title': 'title', 'category': 'category', }]) exp_rights = exp_models.ExplorationRightsModel.get_by_id( self.NEW_EXP_ID) exp_summary_model = exp_models.ExpSummaryModel( id=self.NEW_EXP_ID, title=self.EXP_TITLE, category='category', objective='Old objective', language_code='en', tags=[], ratings=feconf.get_empty_ratings(), scaled_average_rating=feconf.EMPTY_SCALED_AVERAGE_RATING, status=exp_rights.status, community_owned=exp_rights.community_owned, owner_ids=exp_rights.owner_ids, contributor_ids=[], contributors_summary={}) exp_summary_model.update_timestamps() exp_summary_model.put() exploration = exp_fetchers.get_exploration_by_id(self.NEW_EXP_ID) initial_state_name = list(exploration.states.keys())[0] # Store classifier model for the new exploration. classifier_model_id = ( classifier_models.ClassifierTrainingJobModel.create( 'TextClassifier', 'TextInput', self.NEW_EXP_ID, exploration.version, datetime.datetime.utcnow(), {}, initial_state_name, feconf.TRAINING_JOB_STATUS_COMPLETE, 1)) # Store training job model for the classifier model. classifier_models.StateTrainingJobsMappingModel.create( self.NEW_EXP_ID, exploration.version, initial_state_name, {'TextClassifier': classifier_model_id}) # Start migration job on sample exploration. job_id = exp_jobs_one_off.ExplorationMigrationJobManager.create_new() exp_jobs_one_off.ExplorationMigrationJobManager.enqueue(job_id) with self.swap(feconf, 'ENABLE_ML_CLASSIFIERS', True): with self.swap(feconf, 'MIN_TOTAL_TRAINING_EXAMPLES', 2): with self.swap(feconf, 'MIN_ASSIGNED_LABELS', 1): self.process_and_flush_pending_mapreduce_tasks() actual_output = ( exp_jobs_one_off.ExplorationMigrationJobManager.get_output(job_id)) expected_output = ['[u\'SUCCESS\', 1]'] self.assertEqual(actual_output, expected_output) new_exploration = exp_fetchers.get_exploration_by_id(self.NEW_EXP_ID) initial_state_name = list(new_exploration.states.keys())[0] self.assertLess(exploration.version, new_exploration.version) classifier_exp_mapping_model = ( classifier_models.StateTrainingJobsMappingModel.get_models( self.NEW_EXP_ID, new_exploration.version, [initial_state_name]))[0] self.assertEqual( classifier_exp_mapping_model. algorithm_ids_to_job_ids['TextClassifier'], classifier_model_id)
def test_for_recently_published_explorations(self): """ Tests for recently published explorations. """ recently_published_exploration_summaries = ( summary_services.get_recently_published_exp_summary_dicts( feconf.RECENTLY_PUBLISHED_QUERY_LIMIT_FOR_LIBRARY_PAGE)) test_summary_1 = { 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_1, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } test_summary_2 = { 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_2, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } test_summary_3 = { 'status': 'public', 'thumbnail_bg_color': '#a33f40', 'community_owned': False, 'tags': [], 'thumbnail_icon_url': self.get_static_asset_url( '/images/subjects/Lightbulb.svg'), 'language_code': feconf.DEFAULT_LANGUAGE_CODE, 'id': self.EXP_ID_3, 'category': u'A category', 'ratings': feconf.get_empty_ratings(), 'title': u'A title', 'num_views': 0, 'objective': u'An objective' } self.assertDictContainsSubset( test_summary_3, recently_published_exploration_summaries[0]) self.assertDictContainsSubset( test_summary_1, recently_published_exploration_summaries[1]) self.assertDictContainsSubset( test_summary_2, recently_published_exploration_summaries[2]) # Test that editing an exploration does not change its # 'recently-published' status. exp_services.update_exploration( self.albert_id, self.EXP_ID_1, [{ 'cmd': 'edit_exploration_property', 'property_name': 'title', 'new_value': 'New title' }], 'Changed title.') recently_published_exploration_summaries = ( summary_services.get_recently_published_exp_summary_dicts( feconf.RECENTLY_PUBLISHED_QUERY_LIMIT_FOR_LIBRARY_PAGE)) self.assertEqual( recently_published_exploration_summaries[1]['title'], 'New title') self.assertDictContainsSubset( test_summary_3, recently_published_exploration_summaries[0])
def save_new_exp_with_states_schema_v0(self, exp_id, user_id, title): """Saves a new default exploration with a default version 0 states dictionary. This function should only be used for creating explorations in tests involving migration of datastore explorations that use an old states schema version. Note that it makes an explicit commit to the datastore instead of using the usual functions for updating and creating explorations. This is because the latter approach would result in an exploration with the *current* states schema version. Args: exp_id: str. The exploration ID. user_id: str. The user_id of the creator. title: str. The title of the exploration. """ exp_model = exp_models.ExplorationModel( id=exp_id, category='category', title=title, objective='Old objective', language_code='en', tags=[], blurb='', author_notes='', states_schema_version=0, init_state_name=feconf.DEFAULT_INIT_STATE_NAME, states=self.VERSION_0_STATES_DICT, param_specs={}, param_changes=[]) rights_manager.create_new_exploration_rights(exp_id, user_id) commit_message = 'New exploration created with title \'%s\'.' % title exp_model.commit(user_id, commit_message, [{ 'cmd': 'create_new', 'title': 'title', 'category': 'category', }]) exp_rights = exp_models.ExplorationRightsModel.get_by_id(exp_id) exp_summary_model = exp_models.ExpSummaryModel( id=exp_id, title=title, category='category', objective='Old objective', language_code='en', tags=[], ratings=feconf.get_empty_ratings(), scaled_average_rating=feconf.EMPTY_SCALED_AVERAGE_RATING, status=exp_rights.status, community_owned=exp_rights.community_owned, owner_ids=exp_rights.owner_ids, contributor_ids=[], contributors_summary={}, ) exp_summary_model.put() # Note: Also save state id mappping model for new exploration. If not # saved, it may cause errors in test cases. exploration = exp_services.get_exploration_from_model(exp_model) exp_services.create_and_save_state_id_mapping_model(exploration, [])
def _run_batch_job_once_and_verify_output( self, exp_specs, default_title='A title', default_category='A category', default_status=rights_manager.ACTIVITY_STATUS_PUBLICIZED): """Run batch job for creating exploration summaries once and verify its output. exp_specs is a list of dicts with exploration specifications. Allowed keys are category, status, title. If a key is not specified, the default value is used. """ with self.swap( jobs_registry, 'ONE_OFF_JOB_MANAGERS', self.ONE_OFF_JOB_MANAGERS_FOR_TESTS ): default_spec = { 'title': default_title, 'category': default_category, 'status': default_status } self.signup(self.ADMIN_EMAIL, self.ADMIN_USERNAME) self.login(self.ADMIN_EMAIL) admin_id = self.get_user_id_from_email(self.ADMIN_EMAIL) self.set_admins([self.ADMIN_USERNAME]) # Create and delete an exploration (to make sure job handles # deleted explorations correctly). exp_id = '100' self.save_new_valid_exploration( exp_id, admin_id, title=default_spec['title'], category=default_spec['category']) exploration = exp_services.get_exploration_by_id(exp_id) exp_services.delete_exploration(admin_id, exp_id) # Get dummy explorations. num_exps = len(exp_specs) expected_job_output = {} for ind in range(num_exps): exp_id = str(ind) spec = default_spec spec.update(exp_specs[ind]) self.save_new_valid_exploration( exp_id, admin_id, title=spec['title'], category=spec['category']) exploration = exp_services.get_exploration_by_id(exp_id) # Publish or publicize exploration. if spec['status'] == rights_manager.ACTIVITY_STATUS_PUBLIC: rights_manager.publish_exploration(admin_id, exp_id) elif ( spec['status'] == rights_manager.ACTIVITY_STATUS_PUBLICIZED): rights_manager.publish_exploration(admin_id, exp_id) rights_manager.publicize_exploration(admin_id, exp_id) # Do not include user_id here, so all explorations are not # editable for now (will be updated depending on user_id # in galleries) exp_rights_model = exp_models.ExplorationRightsModel.get( exp_id) exploration = exp_services.get_exploration_by_id(exp_id) exploration_model_last_updated = exploration.last_updated exploration_model_created_on = exploration.created_on # Manually create the expected summary specifying title, # category, etc. expected_job_output[exp_id] = exp_domain.ExplorationSummary( exp_id, spec['title'], spec['category'], exploration.objective, exploration.language_code, exploration.tags, feconf.get_empty_ratings(), spec['status'], exp_rights_model.community_owned, exp_rights_model.owner_ids, exp_rights_model.editor_ids, exp_rights_model.viewer_ids, [admin_id], {admin_id: 1}, exploration.version, exploration_model_created_on, exploration_model_last_updated) # Note: Calling constructor for fields that are not required # and have no default value does not work, because # unspecified fields will be empty list in # expected_job_output but will be unspecified in # actual_job_output. if exploration.tags: expected_job_output[exp_id].tags = exploration.tags if exp_rights_model.owner_ids: expected_job_output[exp_id].owner_ids = ( exp_rights_model.owner_ids) if exp_rights_model.editor_ids: expected_job_output[exp_id].editor_ids = ( exp_rights_model.editor_ids) if exp_rights_model.viewer_ids: expected_job_output[exp_id].viewer_ids = ( exp_rights_model.viewer_ids) if exploration.version: expected_job_output[exp_id].version = ( exploration.version) # Run batch job. job_id = ( exp_jobs_one_off.ExpSummariesCreationOneOffJob.create_new()) exp_jobs_one_off.ExpSummariesCreationOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() # Get and check job output. actual_job_output = exp_services.get_all_exploration_summaries() self.assertEqual( actual_job_output.keys(), expected_job_output.keys()) # Note: 'exploration_model_last_updated' is not expected to be the # same, because it is now read from the version model representing # the exploration's history snapshot, and not the ExplorationModel. simple_props = ['id', 'title', 'category', 'objective', 'language_code', 'tags', 'ratings', 'status', 'community_owned', 'owner_ids', 'editor_ids', 'viewer_ids', 'contributor_ids', 'contributors_summary', 'version', 'exploration_model_created_on'] for exp_id in actual_job_output: for prop in simple_props: self.assertEqual( getattr(actual_job_output[exp_id], prop), getattr(expected_job_output[exp_id], prop))