def get(self, exploration_id, escaped_state_name): """Handles GET requests.""" current_exploration = exp_fetchers.get_exploration_by_id( exploration_id) state_name = utils.unescape_encoded_uri_component(escaped_state_name) if state_name not in current_exploration.states: logging.error('Could not find state: %s' % state_name) logging.error('Available states: %s' % (list(current_exploration.states.keys()))) raise self.PageNotFoundException self.render_json({ 'visualizations_info': stats_services.get_visualizations_info( current_exploration.id, state_name, current_exploration.states[state_name].interaction.id), })
def get_state_reference_for_exploration(exp_id, state_name): """Returns the generated state reference for the given exploration id and state name. Args: exp_id: str. ID of the exploration. state_name: str. Name of the state. Returns: str. The generated state reference. """ exploration = exp_fetchers.get_exploration_by_id(exp_id) if not exploration.has_state_name(state_name): raise utils.InvalidInputException( 'No state with the given state name was found in the ' 'exploration with id %s' % exp_id) return (stats_models.LearnerAnswerDetailsModel. get_state_reference_for_exploration(exp_id, state_name))
def _publish_exploration(self, exploration_id): """Publish an exploration. Args: exploration_id: str. Id of the exploration. Raises: InvalidInputException: Given exploration is invalid. """ exploration = exp_fetchers.get_exploration_by_id(exploration_id) try: exploration.validate(strict=True) except utils.ValidationError as e: raise self.InvalidInputException(e) exp_services.publish_exploration_and_update_user_profiles( self.user, exploration_id) exp_services.index_explorations_given_ids([exploration_id])
def post(self): payload = json.loads(self.request.body) user_id = payload['user_id'] reference_dict = payload['reference_dict'] old_status = payload['old_status'] new_status = payload['new_status'] message = feedback_services.get_message(reference_dict['thread_id'], reference_dict['message_id']) exploration = exp_fetchers.get_exploration_by_id( reference_dict['entity_id']) thread = feedback_services.get_thread(reference_dict['thread_id']) text = 'changed status from %s to %s' % (old_status, new_status) subject = 'Oppia thread status change: "%s"' % thread.subject email_manager.send_instant_feedback_message_email( user_id, message.author_id, text, subject, exploration.title, reference_dict['entity_id'], thread.subject)
def test_handle_trainable_states(self): """Test the handle_trainable_states method.""" exploration = exp_fetchers.get_exploration_by_id(self.exp_id) state_names = ['Home'] classifier_services.handle_trainable_states(exploration, state_names) # There should be two jobs (the first job because of the creation of the # exploration) in the data store now. all_jobs = classifier_models.ClassifierTrainingJobModel.get_all() self.assertEqual(all_jobs.count(), 2) for index, job in enumerate(all_jobs): if index == 1: job_id = job.id classifier_training_job = ( classifier_services.get_classifier_training_job_by_id(job_id)) self.assertEqual(classifier_training_job.exp_id, self.exp_id) self.assertEqual(classifier_training_job.state_name, 'Home')
def put(self, exploration_id): """Updates properties of the given exploration.""" exploration = exp_fetchers.get_exploration_by_id(exploration_id) version = self.normalized_payload.get('version') if version > exploration.version: raise base.BaseHandler.InvalidInputException( 'Trying to update version %s of exploration from version %s, ' 'which is not possible. Please reload the page and try again.' % (exploration.version, version)) if not exploration.edits_allowed: raise base.BaseHandler.InvalidInputException( 'This exploration cannot be edited. Please contact the admin.') commit_message = self.normalized_payload.get('commit_message') change_list = self.normalized_payload.get('change_list') changes_are_mergeable = exp_services.are_changes_mergeable( exploration_id, version, change_list) exploration_rights = rights_manager.get_exploration_rights( exploration_id) can_edit = rights_manager.check_can_edit_activity( self.user, exploration_rights) can_voiceover = rights_manager.check_can_voiceover_activity( self.user, exploration_rights) try: if can_edit and changes_are_mergeable: exp_services.update_exploration(self.user_id, exploration_id, change_list, commit_message) elif can_voiceover and changes_are_mergeable: exp_services.update_exploration(self.user_id, exploration_id, change_list, commit_message, is_by_voice_artist=True) except utils.ValidationError as e: raise self.InvalidInputException(e) exploration_data = exp_services.get_user_exploration_data( self.user_id, exploration_id) self.values.update(exploration_data) self.render_json(self.values)
def get(self, exploration_id): """Populates the data on the individual exploration page. Args: exploration_id: str. The ID of the exploration. """ version = self.normalized_request.get('v') exploration = exp_fetchers.get_exploration_by_id( exploration_id, strict=False, version=version) if exploration is None: raise self.PageNotFoundException() exploration_rights = rights_manager.get_exploration_rights( exploration_id, strict=False) user_settings = user_services.get_user_settings(self.user_id) preferred_audio_language_code = None preferred_language_codes = None if user_settings is not None: preferred_audio_language_code = ( user_settings.preferred_audio_language_code) preferred_language_codes = ( user_settings.preferred_language_codes) self.values.update({ 'can_edit': ( rights_manager.check_can_edit_activity( self.user, exploration_rights)), 'exploration': exploration.to_player_dict(), 'exploration_id': exploration_id, 'is_logged_in': bool(self.user_id), 'session_id': utils.generate_new_session_id(), 'version': exploration.version, 'preferred_audio_language_code': preferred_audio_language_code, 'preferred_language_codes': preferred_language_codes, 'auto_tts_enabled': exploration.auto_tts_enabled, 'correctness_feedback_enabled': ( exploration.correctness_feedback_enabled), 'record_playthrough_probability': ( config_domain.RECORD_PLAYTHROUGH_PROBABILITY.value), }) self.render_json(self.values)
def get(self, exploration_id): self.render_json({ 'exploration_id': exploration_id, 'exploration_version': ( exp_fetchers.get_exploration_by_id(exploration_id).version), 'is_improvements_tab_enabled': ( config_domain.IS_IMPROVEMENTS_TAB_ENABLED.value), 'high_bounce_rate_task_state_bounce_rate_creation_threshold': ( config_domain .HIGH_BOUNCE_RATE_TASK_STATE_BOUNCE_RATE_CREATION_THRESHOLD .value), 'high_bounce_rate_task_state_bounce_rate_obsoletion_threshold': ( config_domain .HIGH_BOUNCE_RATE_TASK_STATE_BOUNCE_RATE_OBSOLETION_THRESHOLD .value), 'high_bounce_rate_task_minimum_exploration_starts': ( config_domain.HIGH_BOUNCE_RATE_TASK_MINIMUM_EXPLORATION_STARTS .value), })
def update_opportunity_with_updated_exploration(exp_id): """Updates the opportunities models with the changes made in the exploration. Args: exp_id: str. The exploration id which is also the id of the opportunity model. """ updated_exploration = exp_fetchers.get_exploration_by_id(exp_id) content_count = updated_exploration.get_content_count() translation_counts = updated_exploration.get_translation_counts() complete_translation_language_list = ( updated_exploration.get_languages_with_complete_translation()) model = opportunity_models.ExplorationOpportunitySummaryModel.get(exp_id) exploration_opportunity_summary = ( get_exploration_opportunity_summary_from_model(model)) exploration_opportunity_summary.content_count = content_count exploration_opportunity_summary.translation_counts = translation_counts exploration_opportunity_summary.complete_translation_languages = ( complete_translation_language_list) new_languages_for_voiceover = set( complete_translation_language_list) - set( exploration_opportunity_summary. assigned_voice_artist_in_language_codes) # We only append new languages to need_voice_artist_in_language_codes( # instead of adding all of the complete_translation_language_list), as the # complete translation languages list will be dynamic based on some # content text are changed, where as the voiceover is a long term work and # we can allow a voice_artist to work for an exploration which needs a # little bit update in text translation. need_voice_artist_in_language_codes_set = set( exploration_opportunity_summary.need_voice_artist_in_language_codes) need_voice_artist_in_language_codes_set |= set(new_languages_for_voiceover) exploration_opportunity_summary.need_voice_artist_in_language_codes = list( need_voice_artist_in_language_codes_set) exploration_opportunity_summary.validate() _save_multi_exploration_opportunity_summary( [exploration_opportunity_summary])
def test_can_upload_exploration(self): with self.swap(constants, 'ALLOW_YAML_FILE_UPLOAD', True): self.set_curriculum_admins([self.CURRICULUM_ADMIN_USERNAME]) self.login(self.CURRICULUM_ADMIN_EMAIL, is_super_admin=True) csrf_token = self.get_new_csrf_token() explorations_list = self.get_json( feconf.CREATOR_DASHBOARD_DATA_URL)['explorations_list'] self.assertEqual(explorations_list, []) exp_a_id = self.post_json( '%s?yaml_file=%s' % (feconf.UPLOAD_EXPLORATION_URL, self.SAMPLE_YAML_CONTENT), {}, csrf_token=csrf_token)[creator_dashboard.EXPLORATION_ID_KEY] explorations_list = self.get_json( feconf.CREATOR_DASHBOARD_DATA_URL)['explorations_list'] exploration = exp_fetchers.get_exploration_by_id(exp_a_id) self.assertEqual(explorations_list[0]['id'], exp_a_id) self.assertEqual(exploration.to_yaml(), self.SAMPLE_YAML_CONTENT) self.logout()
def test_interactions_demo_exploration(self): exp_services.load_demo(self._DEMO_EXPLORATION_ID) exploration = exp_fetchers.get_exploration_by_id( self._DEMO_EXPLORATION_ID) all_interaction_ids = set( interaction_registry.Registry.get_all_interaction_ids()) observed_interaction_ids = set() for state in exploration.states.values(): observed_interaction_ids.add(state.interaction.id) missing_interaction_ids = (all_interaction_ids - observed_interaction_ids) self.assertEqual( len(missing_interaction_ids), 0, msg=('Missing interaction IDs in demo exploration: %s' % missing_interaction_ids))
def test_stats_events_successfully_updated(self): all_models = (stats_models.ExplorationStatsModel.get_all()) self.assertEqual(all_models.count(), 0) exp_id = 'eid1' self.save_new_valid_exploration(exp_id, self.OWNER_EMAIL) exploration = exp_fetchers.get_exploration_by_id(exp_id) event_services.StatsEventsHandler.record( exp_id, exploration.version, {'state_stats_mapping': { 'Introduction': {} }}) all_models = stats_models.ExplorationStatsModel.get_all() self.assertEqual(all_models.count(), 1) model = all_models.get() self.assertEqual(model.exp_id, exp_id) self.assertEqual(model.exp_version, exploration.version)
def post(self, exploration_id): """Handles POST requests. Args: exploration_id: str. The ID of the exploration. """ old_state_name = self.payload.get('old_state_name') # The reader's answer. answer = self.payload.get('answer') # Parameters associated with the learner. params = self.payload.get('params', {}) # The version of the exploration. version = self.payload.get('version') if version is None: raise self.InvalidInputException( 'NONE EXP VERSION: Answer Submit') session_id = self.payload.get('session_id') client_time_spent_in_secs = self.payload.get( 'client_time_spent_in_secs') # The answer group and rule spec indexes, which will be used to get # the rule spec string. answer_group_index = self.payload.get('answer_group_index') rule_spec_index = self.payload.get('rule_spec_index') classification_categorization = self.payload.get( 'classification_categorization') exploration = exp_fetchers.get_exploration_by_id( exploration_id, version=version) old_interaction = exploration.states[old_state_name].interaction old_interaction_instance = ( interaction_registry.Registry.get_interaction_by_id( old_interaction.id)) normalized_answer = old_interaction_instance.normalize_answer(answer) event_services.AnswerSubmissionEventHandler.record( exploration_id, version, old_state_name, exploration.states[old_state_name].interaction.id, answer_group_index, rule_spec_index, classification_categorization, session_id, client_time_spent_in_secs, params, normalized_answer) self.render_json({})
def post(self): payload = json.loads(self.request.body) user_id = payload['user_id'] reference_dict = payload['reference_dict'] message = feedback_services.get_message( reference_dict['thread_id'], reference_dict['message_id']) exploration = exp_fetchers.get_exploration_by_id( reference_dict['entity_id']) thread = feedback_services.get_thread(reference_dict['thread_id']) model = email_models.GeneralFeedbackEmailReplyToIdModel.get( user_id, reference_dict['thread_id']) reply_to_id = model.reply_to_id subject = 'New Oppia message in "%s"' % thread.subject email_manager.send_instant_feedback_message_email( user_id, message.author_id, message.text, subject, exploration.title, reference_dict['entity_id'], thread.subject, reply_to_id=reply_to_id)
def put(self, exploration_id): """Updates properties of the given exploration.""" exploration = exp_fetchers.get_exploration_by_id(exploration_id) version = self.payload.get('version') _require_valid_version(version, exploration.version) commit_message = self.payload.get('commit_message') if (commit_message is not None and len(commit_message) > feconf.MAX_COMMIT_MESSAGE_LENGTH): raise self.InvalidInputException( 'Commit messages must be at most %s characters long.' % feconf.MAX_COMMIT_MESSAGE_LENGTH) change_list_dict = self.payload.get('change_list') change_list = [ exp_domain.ExplorationChange(change) for change in change_list_dict ] try: exploration_rights = rights_manager.get_exploration_rights( exploration_id) can_edit = rights_manager.check_can_edit_activity( self.user, exploration_rights) can_voiceover = rights_manager.check_can_voiceover_activity( self.user, exploration_rights) if can_edit: exp_services.update_exploration(self.user_id, exploration_id, change_list, commit_message) elif can_voiceover: exp_services.update_exploration(self.user_id, exploration_id, change_list, commit_message, is_by_voice_artist=True) except utils.ValidationError as e: raise self.InvalidInputException(e) exploration_data = exp_services.get_user_exploration_data( self.user_id, exploration_id) self.values.update(exploration_data) self.render_json(self.values)
def _construct_exploration_suggestions(suggestions): """Returns exploration suggestions with current exploration content. Args: suggestions: list(BaseSuggestion). A list of suggestions. Returns: list(dict). List of suggestion dicts with an additional exploration_content_html field representing the target exploration's current content. """ suggestion_dicts = [] for suggestion in suggestions: exploration = exp_fetchers.get_exploration_by_id(suggestion.target_id) content_html = exploration.get_content_html( suggestion.change.state_name, suggestion.change.content_id) suggestion_dict = suggestion.to_dict() suggestion_dict['exploration_content_html'] = content_html suggestion_dicts.append(suggestion_dict) return suggestion_dicts
def test_interactions_demo_exploration(self): exp_services.load_demo(self._DEMO_EXPLORATION_ID) exploration = exp_fetchers.get_exploration_by_id( self._DEMO_EXPLORATION_ID) all_interaction_ids = set( interaction_registry.Registry.get_all_interaction_ids()) observed_interaction_ids = set() for state in exploration.states.values(): observed_interaction_ids.add(state.interaction.id) missing_interaction_ids = ( all_interaction_ids - observed_interaction_ids) if list(missing_interaction_ids) != ['RatioExpressionInput']: # TODO(#10460): Remove this as soon as the learner's flow is added # for RatioExpressionInput. self.assertEqual(len(missing_interaction_ids), 0, msg=( 'Missing interaction IDs in demo exploration: %s' % missing_interaction_ids))
def test_dependencies_loaded_in_exploration_editor(self): exp_services.load_demo('0') # Register and login as an editor. self.signup(self.EDITOR_EMAIL, self.EDITOR_USERNAME) self.login(self.EDITOR_EMAIL) # Verify that the exploration does not have a Skulpt dependency. exploration = exp_fetchers.get_exploration_by_id('0') interaction_ids = exploration.get_interaction_ids() all_dependency_ids = (interaction_registry.Registry. get_deduplicated_dependency_ids(interaction_ids)) self.assertNotIn('skulpt', all_dependency_ids) # However, Skulpt is loaded in the exploration editor anyway, since # all dependencies are loaded in the exploration editor. response = self.get_html_response('/create/0') response.mustcontain('skulpt') self.logout()
def put(self, exploration_id): """Updates properties of the given exploration.""" exploration = exp_fetchers.get_exploration_by_id(exploration_id) version = self.payload.get('version') _require_valid_version(version, exploration.version) commit_message = self.payload.get('commit_message') change_list_dict = self.payload.get('change_list') change_list = [ exp_domain.ExplorationChange(change) for change in change_list_dict] try: exp_services.update_exploration( self.user_id, exploration_id, change_list, commit_message) except utils.ValidationError as e: raise self.InvalidInputException(e) exploration_data = exp_services.get_user_exploration_data( self.user_id, exploration_id) self.values.update(exploration_data) self.render_json(self.values)
def map(model): if model.draft_change_list is None: return exploration = exp_fetchers.get_exploration_by_id( model.exploration_id, strict=False) if exploration is None: yield ('DISCARDED - Exploration is missing', model.id) elif model.draft_change_list_last_updated.timetuple().tm_year <= 2019: yield ('DISCARDED - Draft is old', model.id) else: return # Discard the draft. model.draft_change_list = None model.draft_change_list_last_updated = None model.draft_change_list_exp_version = None model.update_timestamps() model.put() yield ('SUCCESS - Discarded draft', 1)
def test_loading_old_exploration_does_not_break_domain_object_ctor(self): """This test attempts to load an exploration that is stored in the data store as pre-states schema version 0. The exp_fetchers.get_exploration_by_id function should properly load and convert the exploration without any issues. Structural changes to the states schema will not break the exploration domain class constructor. """ exp_id = 'exp_id3' # Create a exploration with states schema version 0 and an old states # blob. self.save_new_exp_with_states_schema_v0(exp_id, self.albert_id, 'Old Title') # Ensure the exploration was converted. exploration = exp_fetchers.get_exploration_by_id(exp_id) self.assertEqual(exploration.states_schema_version, feconf.CURRENT_STATE_SCHEMA_VERSION) # The converted exploration should be up-to-date and properly # converted. self.assertEqual(exploration.to_yaml(), self.UPGRADED_EXP_YAML)
def test_create_and_accept_suggestion(self): with self.swap(feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.EXP_ID, self.target_version_at_submission, self.author_id, self.change, 'test description', None) suggestion_id = self.THREAD_ID suggestion = suggestion_services.get_suggestion_by_id(suggestion_id) suggestion_services.accept_suggestion(suggestion, self.reviewer_id, self.COMMIT_MESSAGE, None) exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID) self.assertEqual(exploration.states['State 1'].content.html, '<p>new content</p>') self.assertEqual(suggestion.status, suggestion_models.STATUS_ACCEPTED)
def test_interactions_demo_exploration(self): exp_services.load_demo(self._DEMO_EXPLORATION_ID) exploration = exp_fetchers.get_exploration_by_id( self._DEMO_EXPLORATION_ID) all_interaction_ids = set( interaction_registry.Registry.get_all_interaction_ids()) observed_interaction_ids = set() for state in exploration.states.values(): observed_interaction_ids.add(state.interaction.id) missing_interaction_ids = ( all_interaction_ids - observed_interaction_ids) if list(missing_interaction_ids) != ['MathExpressionInput']: # Ignoring the lack of the MathExpressionInput since it is going # to be deprecated and explorations that use it will now be using # one of AlgebraicExpressionInput, NumericExpressionInput, or # MathEquationInput. self.assertEqual(len(missing_interaction_ids), 0, msg=( 'Missing interaction IDs in demo exploration: %s' % missing_interaction_ids))
def map(item): if item.deleted: return # Do not upgrade explorations that fail non-strict validation. old_exploration = exp_fetchers.get_exploration_by_id(item.id) try: old_exploration.validate() except Exception as e: logging.error('Exploration %s failed non-strict validation: %s' % (item.id, e)) return # If the exploration model being stored in the datastore is not the # most up-to-date states schema version, then update it. if (item.states_schema_version != feconf.CURRENT_STATE_SCHEMA_VERSION): # Note: update_exploration does not need to apply a change list in # order to perform a migration. See the related comment in # exp_services.apply_change_list for more information. # # Note: from_version and to_version really should be int, but left # as str to conform with legacy data. commit_cmds = [ exp_domain.ExplorationChange({ 'cmd': exp_domain.CMD_MIGRATE_STATES_SCHEMA_TO_LATEST_VERSION, 'from_version': python_utils.UNICODE(item.states_schema_version), 'to_version': python_utils.UNICODE(feconf.CURRENT_STATE_SCHEMA_VERSION) }) ] exp_services.update_exploration( feconf.MIGRATION_BOT_USERNAME, item.id, commit_cmds, 'Update exploration states from schema version %d to %d.' % (item.states_schema_version, feconf.CURRENT_STATE_SCHEMA_VERSION)) yield ('SUCCESS', item.id)
def post(self): payload = json.loads(self.request.body) user_id = payload['user_id'] reference_dict = payload['reference_dict'] message = feedback_services.get_message( reference_dict['thread_id'], reference_dict['message_id']) exploration = exp_fetchers.get_exploration_by_id( reference_dict['entity_id']) thread = feedback_services.get_thread(reference_dict['thread_id']) feedback_thread_reply_info = ( email_services.get_feedback_thread_reply_info_by_user_and_thread( user_id, reference_dict['thread_id'])) if feedback_thread_reply_info is None: raise self.InvalidInputException( 'Feedback thread for current user and thread_id does not exist') subject = 'New Oppia message in "%s"' % thread.subject email_manager.send_instant_feedback_message_email( user_id, message.author_id, message.text, subject, exploration.title, reference_dict['entity_id'], thread.subject, reply_to_id=feedback_thread_reply_info.reply_to_id) self.render_json({})
def get_text_to_create_voiceover_application(target_type, target_id, language_code): """Returns a text to voiceover for a voiceover application. Args: target_type: str. The string representing the type of the target entity. target_id: str. The ID of the target entity. language_code: str. The language code for the content. Returns: str. The text which can be voiceover for a voiceover application. """ if target_type == feconf.ENTITY_TYPE_EXPLORATION: exploration = exp_fetchers.get_exploration_by_id(target_id) init_state_name = exploration.init_state_name state = exploration.states[init_state_name] if exploration.language_code == language_code: return state.content.html else: return state.written_translations.get_translated_content( state.content.content_id, language_code) else: raise Exception('Invalid target type: %s' % target_type)
def post(self): payload = json.loads(self.request.body) user_id = payload['user_id'] references = feedback_services.get_feedback_message_references(user_id) if not references: # Model may not exist if user has already attended to the feedback. return transaction_services.run_in_transaction( feedback_services.update_feedback_email_retries, user_id) messages = {} for reference in references: message = feedback_services.get_message(reference.thread_id, reference.message_id) exploration = exp_fetchers.get_exploration_by_id( reference.entity_id) message_text = message.text if len(message_text) > 200: message_text = message_text[:200] + '...' if exploration.id in messages: messages[exploration.id]['messages'].append(message_text) else: messages[exploration.id] = { 'title': exploration.title, 'messages': [message_text] } email_manager.send_feedback_message_email(user_id, messages) transaction_services.run_in_transaction( feedback_services.pop_feedback_message_references, user_id, len(references)) self.render_json({})
def get(self): """Handles GET requests.""" language_code = self.request.get('language_code') exp_id = self.request.get('exp_id') if not utils.is_supported_audio_language_code(language_code): raise self.InvalidInputException('Invalid language_code: %s' % ( language_code)) if not opportunity_services.is_exploration_available_for_contribution( exp_id): raise self.InvalidInputException('Invalid exp_id: %s' % exp_id) exp = exp_fetchers.get_exploration_by_id(exp_id) state_names_to_content_id_mapping = exp.get_translatable_text( language_code) self.values = { 'state_names_to_content_id_mapping': ( state_names_to_content_id_mapping), 'version': exp.version } self.render_json(self.values)
def _does_exploration_exist(exploration_id, version, collection_id): """Returns if an exploration exists. Args: exploration_id: str. The ID of the exploration. version: int or None. The version of the exploration. collection_id: str. ID of the collection. Returns: bool. True if the exploration exists False otherwise. """ exploration = exp_fetchers.get_exploration_by_id( exploration_id, strict=False, version=version) if exploration is None: return False if collection_id: collection = collection_services.get_collection_by_id( collection_id, strict=False) if collection is None: return False return True
def test_create_and_reject_suggestion(self): with self.swap(feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.EXP_ID, self.target_version_at_submission, self.author_id, self.change, 'test description', None) suggestion_id = self.THREAD_ID suggestion = suggestion_services.get_suggestion_by_id(suggestion_id) suggestion_services.reject_suggestion(suggestion, self.reviewer_id, 'Reject message') exploration = exp_fetchers.get_exploration_by_id(self.EXP_ID) thread_messages = feedback_services.get_messages(self.THREAD_ID) last_message = thread_messages[len(thread_messages) - 1] self.assertEqual(last_message.text, 'Reject message') self.assertEqual(exploration.states['State 1'].content.html, '<p>old content</p>') self.assertEqual(suggestion.status, suggestion_models.STATUS_REJECTED)