def test_get_suggestions_list(self): self.login(self.OWNER_EMAIL) suggestions = self.get_json( feconf.CREATOR_DASHBOARD_DATA_URL)['created_suggestions_list'] self.assertEqual(suggestions, []) change_dict = { 'cmd': 'edit_state_property', 'property_name': 'content', 'state_name': 'Introduction', 'new_value': '' } self.save_new_default_exploration('exploration_id', self.owner_id) suggestion_services.create_suggestion('edit_exploration_state_content', 'exploration', 'exploration_id', 1, self.owner_id, change_dict, '') suggestions = self.get_json( feconf.CREATOR_DASHBOARD_DATA_URL)['created_suggestions_list'][0] change_dict['old_value'] = {'content_id': 'content', 'html': ''} self.assertEqual(suggestions['change'], change_dict) # Test to check if suggestions populate old value of the change. self.assertEqual(suggestions['change']['old_value']['content_id'], 'content') self.logout()
def test_reject_suggestion_handled_suggestion_failure(self): with self.swap(feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.generate_thread_id): with self.swap(exp_services, 'get_exploration_by_id', self.mock_get_exploration_by_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.target_id, self.target_version_at_submission, self.author_id, self.change, 'test description', self.reviewer_id) suggestion = suggestion_services.get_suggestion_by_id( self.suggestion_id) suggestion.status = suggestion_models.STATUS_ACCEPTED suggestion_services._update_suggestion(suggestion) # pylint: disable=protected-access with self.assertRaisesRegexp( Exception, 'The suggestion has already been accepted/rejected.'): suggestion_services.reject_suggestion(suggestion, self.reviewer_id, 'reject review message') suggestion = suggestion_services.get_suggestion_by_id( self.suggestion_id) self.assertEqual(suggestion.status, suggestion_models.STATUS_ACCEPTED) suggestion.status = suggestion_models.STATUS_REJECTED suggestion_services._update_suggestion(suggestion) # pylint: disable=protected-access with self.assertRaisesRegexp( Exception, 'The suggestion has already been accepted/rejected.'): suggestion_services.reject_suggestion(suggestion, self.reviewer_id, 'reject review message') suggestion = suggestion_services.get_suggestion_by_id( self.suggestion_id) self.assertEqual(suggestion.status, suggestion_models.STATUS_REJECTED)
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)
def test_reject_suggestion_successfully(self): with self.swap(feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_thread_id): with self.swap(exp_fetchers, 'get_exploration_by_id', self.mock_get_exploration_by_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.target_id, self.target_version_at_submission, self.author_id, self.change, 'test description', self.reviewer_id) suggestion = suggestion_services.get_suggestion_by_id( self.suggestion_id) suggestion_services.reject_suggestion(suggestion, self.reviewer_id, 'reject review message') suggestion = suggestion_services.get_suggestion_by_id( self.suggestion_id) self.assertEqual(suggestion.status, suggestion_models.STATUS_REJECTED) self.assertEqual(suggestion.final_reviewer_id, self.reviewer_id) thread_messages = feedback_services.get_messages(self.THREAD_ID) last_message = thread_messages[len(thread_messages) - 1] self.assertEqual(last_message.text, 'reject review message')
def test_post_feedback_threads_with_updated_suggestion_status_raises_400( self): self.login(self.OWNER_EMAIL_1) csrf_token = self.get_new_csrf_token() new_content = state_domain.SubtitledHtml( 'content', '<p>new content html</p>').to_dict() change = { 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY, 'property_name': exp_domain.STATE_PROPERTY_CONTENT, 'state_name': 'Welcome!', 'new_value': new_content } suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.EXP_ID, 1, self.owner_id_1, change, 'sample description', None) thread_id = suggestion_services.query_suggestions([ ('author_id', self.owner_id_1), ('target_id', self.EXP_ID) ])[0].suggestion_id thread_url = '%s/%s' % (feconf.FEEDBACK_THREAD_URL_PREFIX, thread_id) response = self.post_json(thread_url, { 'text': 'Message 1', 'updated_subject': None, 'updated_status': 'open' }, csrf_token=csrf_token, expected_status_int=400) self.assertEqual( response['error'], 'Suggestion thread status cannot be changed manually.') self.logout()
def test_handler_does_not_return_in_review_content(self): change_dict = { 'cmd': 'add_written_translation', 'state_name': 'Introduction', 'content_id': 'content', 'language_code': 'hi', 'content_html': '', 'translation_html': '<p>Translation for content.</p>', 'data_format': 'html' } suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT, feconf.ENTITY_TYPE_EXPLORATION, '0', 1, self.owner_id, change_dict, 'description') output = self.get_json('/gettranslatabletexthandler', params={ 'language_code': 'hi', 'exp_id': '0' }) expected_output = { 'version': 1, 'state_names_to_content_id_mapping': { 'End State': { 'content': { 'content': '', 'data_format': 'html', 'content_type': 'content', 'interaction_id': None, 'rule_type': None } } } } self.assertEqual(output, expected_output)
def test_create_and_accept_suggestion(self): with self.swap(feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.generate_thread_id): with self.swap(feconf, 'ENABLE_GENERALIZED_FEEDBACK_THREADS', True): 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) with self.swap(feconf, 'ENABLE_GENERALIZED_FEEDBACK_THREADS', True): suggestion_services.accept_suggestion(suggestion, self.reviewer_id, self.COMMIT_MESSAGE, None) exploration = exp_services.get_exploration_by_id(self.EXP_ID) self.assertEqual(exploration.states['State 1'].content.html, 'new content') self.assertEqual(suggestion.status, suggestion_models.STATUS_ACCEPTED)
def _create_translation_suggestion(self): """Creates a translation suggestion.""" add_translation_change_dict = { 'cmd': exp_domain.CMD_ADD_TRANSLATION, 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': feconf.DEFAULT_NEW_STATE_CONTENT_ID, 'language_code': self.language_code, 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, 'translation_html': self.default_translation_html } return suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_TRANSLATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.target_id, feconf.CURRENT_STATE_SCHEMA_VERSION, self.author_id, add_translation_change_dict, 'test description')
def _create_translation_suggestion_with_language_code(self, language_code): """Creates a translation suggestion in the given language_code.""" add_translation_change_dict = { 'cmd': exp_domain.CMD_ADD_TRANSLATION, 'state_name': feconf.DEFAULT_INIT_STATE_NAME, 'content_id': feconf.DEFAULT_NEW_STATE_CONTENT_ID, 'language_code': language_code, 'content_html': feconf.DEFAULT_INIT_STATE_CONTENT_STR, 'translation_html': '<p>This is the translated content.</p>' } return suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT, feconf.ENTITY_TYPE_EXPLORATION, self.target_id, feconf.CURRENT_STATE_SCHEMA_VERSION, self.author_id, add_translation_change_dict, 'test description')
def post(self): """Handles POST requests.""" if (self.normalized_payload.get('suggestion_type') == feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT): raise self.InvalidInputException( 'Content suggestion submissions are no longer supported.') suggestion = suggestion_services.create_suggestion( self.normalized_payload.get('suggestion_type'), self.normalized_payload.get('target_type'), self.normalized_payload.get('target_id'), self.normalized_payload.get('target_version_at_submission'), self.user_id, self.normalized_payload.get('change'), self.normalized_payload.get('description')) suggestion_change = suggestion.change if ( suggestion_change.cmd == 'add_written_translation' and suggestion_change.data_format in ( state_domain.WrittenTranslation .DATA_FORMAT_SET_OF_NORMALIZED_STRING, state_domain.WrittenTranslation .DATA_FORMAT_SET_OF_UNICODE_STRING ) ): self.render_json(self.values) return # Images for question suggestions are already stored in the server # before actually the question is submitted. Therefore no need of # uploading images when the suggestion type is 'add_question'. But this # is not good, since when the user cancels a question suggestion after # adding an image, there is no method to remove the uploaded image. # See more - https://github.com/oppia/oppia/issues/14298 if self.normalized_payload.get( 'suggestion_type') != (feconf.SUGGESTION_TYPE_ADD_QUESTION): _upload_suggestion_images( self.normalized_payload.get('files'), suggestion, suggestion.get_new_image_filenames_added_in_suggestion()) self.render_json(self.values)
def _create_question_suggestion(self): """Creates a question suggestion.""" add_question_change_dict = { 'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION, 'question_dict': { 'question_state_data': self._create_valid_question_data('default_state').to_dict(), 'language_code': constants.DEFAULT_LANGUAGE_CODE, 'question_state_data_schema_version': (feconf.CURRENT_STATE_SCHEMA_VERSION), 'linked_skill_ids': ['skill_1'], 'inapplicable_skill_misconception_ids': ['skillid12345-1'] }, 'skill_id': self.skill_id, 'skill_difficulty': 0.3 } return suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_ADD_QUESTION, feconf.ENTITY_TYPE_SKILL, self.skill_id, feconf.CURRENT_STATE_SCHEMA_VERSION, self.author_id, add_question_change_dict, 'test description')
def test_fixes_invalid_unicode_translations(self): exp_id = 'EXP_ID' exploration = exp_domain.Exploration.create_default_exploration( exp_id, title='title', category='category', language_code='bn') self.set_interaction_for_state( exploration.states[exploration.init_state_name], 'Continue') exp_services.save_new_exploration(self.albert_id, exploration) add_translation_change_dict = { 'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION, 'state_name': 'Introduction', 'content_id': 'ca_buttonText_0', 'language_code': 'bn', 'content_html': 'Continue', 'translation_html': '<p>চালিয়ে যান</p>', 'data_format': 'html' } invalid_suggestion = suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT, feconf.ENTITY_TYPE_EXPLORATION, exp_id, exploration.version, self.albert_id, add_translation_change_dict, 'test description') self.assertEqual(invalid_suggestion.change.data_format, 'html') self.assertEqual(invalid_suggestion.change.translation_html, '<p>চালিয়ে যান</p>') expected_output = [ u'[u\'UPDATED\', [u\'%s | ca_buttonText_0\']]' % invalid_suggestion.suggestion_id, u'[u\'PROCESSED\', 1]' ] self._run_job_and_verify_output(expected_output) valid_suggestion = suggestion_services.get_suggestion_by_id( invalid_suggestion.suggestion_id) self.assertEqual(valid_suggestion.change.data_format, 'unicode') self.assertEqual(valid_suggestion.change.translation_html, 'চালিয়ে যান')
def _create_translation_suggestion(self, language_code): """Creates a translation suggestion.""" add_translation_change_dict = { 'cmd': exp_domain.CMD_ADD_WRITTEN_TRANSLATION, 'state_name': 'state_1', 'content_id': 'content', 'language_code': language_code, 'content_html': '<p>This is html to translate.</p>', 'translation_html': '<p>This is translated html.</p>', 'data_format': 'html' } with self.swap(exp_fetchers, 'get_exploration_by_id', self.mock_get_exploration_by_id): with self.swap(exp_domain.Exploration, 'get_content_html', self.MockExploration.get_content_html): translation_suggestion = ( suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_TRANSLATE_CONTENT, feconf.ENTITY_TYPE_EXPLORATION, self.target_id, self.target_version_at_submission, self.author_id, add_translation_change_dict, 'test description')) return translation_suggestion
def test_migration_job_does_not_convert_up_to_date_suggestion(self): suggestion_change = { 'cmd': (question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION), 'question_dict': { 'question_state_data': self._create_valid_question_data('default_state').to_dict(), 'language_code': 'en', 'question_state_data_schema_version': (feconf.CURRENT_STATE_SCHEMA_VERSION), 'linked_skill_ids': [self.skill_id], 'inapplicable_skill_misconception_ids': [] }, 'skill_id': self.skill_id, 'skill_difficulty': 0.3 } suggestion = suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_ADD_QUESTION, feconf.ENTITY_TYPE_SKILL, self.skill_id, 1, self.albert_id, suggestion_change, 'test description') self.assertEqual( suggestion.change. question_dict['question_state_data_schema_version'], feconf.CURRENT_STATE_SCHEMA_VERSION) expected_output = [u'[u\'SUCCESS\', 1]'] self._run_job_and_verify_output(expected_output) updated_suggestion = suggestion_services.get_suggestion_by_id( suggestion.suggestion_id) self.assertEqual( updated_suggestion.change. question_dict['question_state_data_schema_version'], feconf.CURRENT_STATE_SCHEMA_VERSION)
def post(self): """Handles POST requests.""" if (self.payload.get('suggestion_type') == feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT): raise self.InvalidInputException( 'Content suggestion submissions are no longer supported.') try: suggestion = suggestion_services.create_suggestion( self.payload.get('suggestion_type'), self.payload.get('target_type'), self.payload.get('target_id'), self.payload.get('target_version_at_submission'), self.user_id, self.payload.get('change'), self.payload.get('description')) except utils.ValidationError as e: raise self.InvalidInputException(e) suggestion_change = suggestion.change if (suggestion_change.cmd == 'add_written_translation' and (suggestion_change.data_format == state_domain. WrittenTranslation.DATA_FORMAT_SET_OF_NORMALIZED_STRING or suggestion_change.data_format == state_domain. WrittenTranslation.DATA_FORMAT_SET_OF_UNICODE_STRING)): self.render_json(self.values) return # TODO(#10513) : Find a way to save the images before the suggestion is # created. suggestion_image_context = suggestion.image_context new_image_filenames = ( suggestion.get_new_image_filenames_added_in_suggestion()) for filename in new_image_filenames: image = self.request.get(filename) if not image: logging.exception( 'Image not provided for file with name %s when the ' ' suggestion with target id %s was created.' % (filename, suggestion.target_id)) raise self.InvalidInputException( 'No image data provided for file with name %s.' % (filename)) try: file_format = ( image_validation_services.validate_image_and_filename( image, filename)) except utils.ValidationError as e: raise self.InvalidInputException('%s' % (e)) image_is_compressible = (file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS) fs_services.save_original_and_compressed_versions_of_image( filename, suggestion_image_context, suggestion.target_id, image, 'image', image_is_compressible) target_entity_html_list = suggestion.get_target_entity_html_strings() target_image_filenames = ( html_cleaner.get_image_filenames_from_html_strings( target_entity_html_list)) fs_services.copy_images(suggestion.target_type, suggestion.target_id, suggestion_image_context, suggestion.target_id, target_image_filenames) self.render_json(self.values)
def test_email_is_sent_when_suggestion_created(self): """Tests SuggestionEmailHandler functionality.""" user_id_b = self.user_id_b class MockActivityRights: def __init__(self, exploration_id, owner_ids, editor_ids, voice_artist_ids, viewer_ids, community_owned=False, cloned_from=None, status=True, viewable_if_private=False, first_published_msec=None): # User B ID hardcoded into owner_ids to get email_manager # to send email to user B to test functionality. self.id = exploration_id self.getLintToShutUp = owner_ids self.editor_ids = editor_ids self.voice_artist_ids = voice_artist_ids self.viewer_ids = viewer_ids self.community_owned = community_owned self.cloned_from = cloned_from self.status = status self.viewable_if_private = viewable_if_private self.first_published_msec = first_published_msec self.owner_ids = [user_id_b] email_user_b = self.swap(rights_domain, 'ActivityRights', MockActivityRights) with email_user_b, self.can_send_feedback_email_ctx: with self.can_send_emails_ctx: change = { 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY, 'property_name': exp_domain.STATE_PROPERTY_CONTENT, 'state_name': 'state_1', 'new_value': 'new suggestion content' } # Create suggestion from user A to user B. suggestion_services.create_suggestion( feconf.SUGGESTION_TYPE_EDIT_STATE_CONTENT, feconf.ENTITY_TYPE_EXPLORATION, self.exploration.id, 1, self.user_id_a, change, 'test description') threadlist = feedback_services.get_all_threads( feconf.ENTITY_TYPE_EXPLORATION, self.exploration.id, True) thread_id = threadlist[0].id # Enqueue and send suggestion email task. payload = { 'exploration_id': self.exploration.id, 'thread_id': thread_id } messages = self._get_all_sent_email_messages() self.assertEqual(len(messages), 0) taskqueue_services.enqueue_task( feconf.TASK_URL_SUGGESTION_EMAILS, payload, 0) self.process_and_flush_pending_tasks() # Check that user B received message. messages = self._get_sent_email_messages(self.USER_B_EMAIL) self.assertEqual(len(messages), 1) # Check that user B received correct message. expected_message = ( 'Hi userB,\nuserA has submitted a new suggestion' ' for your Oppia exploration, "Title".\nYou can' ' accept or reject this suggestion by visiting' ' the feedback page for your exploration.\n\nTha' 'nks!\n- The Oppia Team\n\nYou can change your' ' email preferences via the Preferences page.') self.assertEqual(messages[0].body, expected_message)
def test_for_html_in_suggestion_edit_content_with_math_rte(self): """Checks that correct number of hints are tabulated when there is single exploration. """ html_content = ( '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a' 'mp;quot;+,-,-,+&quot;"></oppia-noninteractive-math>') state_dict = { 'classifier_model_id': None, 'content': { 'content_id': 'content', 'html': html_content }, 'interaction': { 'answer_groups': [], 'confirmed_unclassified_answers': [], 'customization_args': {}, 'default_outcome': { 'dest': 'Introduction', 'feedback': { 'content_id': 'default_outcome', 'html': html_content }, 'labelled_as_correct': False, 'param_changes': [], 'refresher_exploration_id': None, 'missing_prerequisite_skill_id': None }, 'hints': [], 'id': None, 'solution': None, }, 'param_changes': [], 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': {}, 'default_outcome': {} } }, 'solicit_answer_details': False, 'written_translations': { 'translations_mapping': { 'content': {}, 'default_outcome': {} } } } states = { 'Introduction': state_dict } exploration = ( exp_domain.Exploration( 'exp1', feconf.DEFAULT_EXPLORATION_TITLE, feconf.DEFAULT_EXPLORATION_CATEGORY, feconf.DEFAULT_EXPLORATION_OBJECTIVE, constants.DEFAULT_LANGUAGE_CODE, [], '', '', feconf.CURRENT_STATE_SCHEMA_VERSION, feconf.DEFAULT_INIT_STATE_NAME, states, {}, [], 0, False, False)) exp_services.save_new_exploration(self.author_id, exploration) change_dict = { 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY, 'property_name': exp_domain.STATE_PROPERTY_CONTENT, 'state_name': 'Introduction', 'new_value': { 'content_id': 'content', 'html': 'suggestion content' }, 'old_value': { 'content_id': 'content', 'html': html_content } } with self.swap( feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_exploration_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.target_id, self.target_version_at_submission, self.author_id, change_dict, 'test description') job_id = ( suggestion_jobs_one_off. SuggestionMathRteAuditOneOffJob.create_new()) suggestion_jobs_one_off.SuggestionMathRteAuditOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() actual_output = ( suggestion_jobs_one_off. SuggestionMathRteAuditOneOffJob.get_output(job_id)) self.assertRegexpMatches( python_utils.UNICODE(actual_output), '1 suggestions have Math components in them')
def test_for_html_in_suggestion_with_no_math_rte(self): html_content = '<p>This has no Math components</p>' answer_group = { 'outcome': { 'dest': None, 'feedback': { 'content_id': 'feedback_1', 'html': html_content }, 'labelled_as_correct': True, 'param_changes': [], 'refresher_exploration_id': None, 'missing_prerequisite_skill_id': None }, 'rule_specs': [{ 'inputs': { 'x': 0 }, 'rule_type': 'Equals' }], 'training_data': [], 'tagged_skill_misconception_id': None } question_state_dict_without_math = { 'content': { 'content_id': 'content_1', 'html': 'Question 1' }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'content_1': {}, 'feedback_1': {}, 'feedback_2': {}, 'hint_1': {}, 'solution': {} } }, 'written_translations': { 'translations_mapping': { 'content_1': {}, 'feedback_1': {}, 'feedback_2': {}, 'hint_1': {}, 'solution': { 'en': { 'html': html_content, 'needs_update': True } } } }, 'interaction': { 'answer_groups': [answer_group], 'confirmed_unclassified_answers': [], 'customization_args': { 'choices': { 'value': [html_content] }, 'showChoicesInShuffledOrder': { 'value': True } }, 'default_outcome': { 'dest': None, 'feedback': { 'content_id': 'feedback_2', 'html': html_content }, 'param_changes': [], 'refresher_exploration_id': None, 'labelled_as_correct': True, 'missing_prerequisite_skill_id': None }, 'hints': [{ 'hint_content': { 'content_id': 'hint_1', 'html': 'Hint 1' } }], 'solution': { 'answer_is_exclusive': False, 'correct_answer': 0, 'explanation': { 'content_id': 'solution', 'html': '<p>This is a solution.</p>' } }, 'id': 'MultipleChoiceInput' }, 'param_changes': [], 'solicit_answer_details': False, 'classifier_model_id': None } suggestion_dict_without_math = { 'suggestion_id': 'skill2.thread1', 'suggestion_type': suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, 'target_type': suggestion_models.TARGET_TYPE_SKILL, 'target_id': 'skill2', 'target_version_at_submission': 1, 'status': suggestion_models.STATUS_ACCEPTED, 'author_name': 'author', 'final_reviewer_id': self.reviewer_id, 'change': { 'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION, 'question_dict': { 'question_state_data': question_state_dict_without_math, 'language_code': 'en', 'question_state_data_schema_version': ( feconf.CURRENT_STATE_SCHEMA_VERSION), 'linked_skill_ids': ['skill_2'] }, 'skill_id': 'skill_2', 'skill_difficulty': 0.3, }, 'score_category': 'question.skill1', 'last_updated': utils.get_time_in_millisecs(self.fake_date) } with self.swap( feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_skill_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, suggestion_models.TARGET_TYPE_SKILL, 'skill1', feconf.CURRENT_STATE_SCHEMA_VERSION, self.author_id, suggestion_dict_without_math['change'], 'test description') job_id = ( suggestion_jobs_one_off. SuggestionMathRteAuditOneOffJob.create_new()) suggestion_jobs_one_off.SuggestionMathRteAuditOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() actual_output = ( suggestion_jobs_one_off. SuggestionMathRteAuditOneOffJob.get_output(job_id)) self.assertEqual(len(actual_output), 0)
def test_cron_mail_reviewers_in_rotation_handler(self): self.login(self.ADMIN_EMAIL, is_super_admin=True) reviewer_ids = [] score_categories = [] def _mock_send_mail_to_notify_users_to_review(reviewer_id, score_category): """Mocks email_manager.send_mail_to_notify_users_to_review() as its not possible to send mail with self.testapp_swap, i.e with the URLs defined in main_cron. """ reviewer_ids.append(reviewer_id) score_categories.append(score_category) send_mail_to_notify_users_to_review_swap = self.swap( email_manager, 'send_mail_to_notify_users_to_review', _mock_send_mail_to_notify_users_to_review) self.save_new_valid_exploration('exp_id', self.admin_id, title='A title', category='Algebra') new_content = state_domain.SubtitledHtml( 'content', '<p>new suggestion content</p>').to_dict() change = { 'cmd': 'edit_state_property', 'property_name': 'content', 'state_name': 'Introduction', 'new_value': new_content } suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, 'exp_id', 1, feconf.SYSTEM_COMMITTER_ID, change, 'change title', self.admin_id) exploration = exp_services.get_exploration_by_id('exp_id') self.assertEqual(exploration.states['Introduction'].content.to_dict(), { 'content_id': 'content', 'html': '' }) suggestion = suggestion_services.query_suggestions([ ('author_id', feconf.SYSTEM_COMMITTER_ID), ('target_id', 'exp_id') ])[0] suggestion_services.accept_suggestion( suggestion, self.admin_id, suggestion_models.DEFAULT_SUGGESTION_ACCEPT_MESSAGE, None) exploration = exp_services.get_exploration_by_id('exp_id') self.assertEqual(exploration.states['Introduction'].content.to_dict(), { 'content_id': 'content', 'html': '<p>new suggestion content</p>' }) send_suggestion_review_related_emails_swap = self.swap( feconf, 'SEND_SUGGESTION_REVIEW_RELATED_EMAILS', True) with self.testapp_swap, send_suggestion_review_related_emails_swap, ( send_mail_to_notify_users_to_review_swap): self.get_html_response('/cron/suggestions/notify_reviewers') self.assertEqual(reviewer_ids, [None]) self.assertEqual(score_categories, ['content.Algebra'])
def test_for_html_in_suggestions_with_math_rte(self): """Checks that correct number of hints are tabulated when there is single exploration. """ html_content = ( '<p>Value</p><oppia-noninteractive-math math_content-with-value=' '"{&quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &' 'amp;quot;svg_filename&quot;: &quot;&quot;}"></oppia' '-noninteractive-math>') state_dict = { 'classifier_model_id': None, 'content': { 'content_id': 'content', 'html': html_content }, 'interaction': { 'answer_groups': [], 'confirmed_unclassified_answers': [], 'customization_args': {}, 'default_outcome': { 'dest': 'Introduction', 'feedback': { 'content_id': 'default_outcome', 'html': html_content }, 'labelled_as_correct': False, 'param_changes': [], 'refresher_exploration_id': None, 'missing_prerequisite_skill_id': None }, 'hints': [], 'id': None, 'solution': None, }, 'param_changes': [], 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': {}, 'default_outcome': {} } }, 'solicit_answer_details': False, 'written_translations': { 'translations_mapping': { 'content': {}, 'default_outcome': {} } } } states = { 'Introduction': state_dict } exploration = ( exp_domain.Exploration( 'exp1', feconf.DEFAULT_EXPLORATION_TITLE, feconf.DEFAULT_EXPLORATION_CATEGORY, feconf.DEFAULT_EXPLORATION_OBJECTIVE, constants.DEFAULT_LANGUAGE_CODE, [], '', '', feconf.CURRENT_STATE_SCHEMA_VERSION, feconf.DEFAULT_INIT_STATE_NAME, states, {}, [], 0, False, False)) exp_services.save_new_exploration(self.author_id, exploration) add_translation_change_dict = { 'cmd': 'add_translation', 'state_name': 'Introduction', 'content_id': 'content', 'language_code': 'hi', 'content_html': html_content, 'translation_html': html_content } with self.swap( feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_exploration_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_TRANSLATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.target_id, self.target_version_at_submission, self.author_id, add_translation_change_dict, 'test description') answer_group = { 'outcome': { 'dest': None, 'feedback': { 'content_id': 'feedback_1', 'html': html_content }, 'labelled_as_correct': True, 'param_changes': [], 'refresher_exploration_id': None, 'missing_prerequisite_skill_id': None }, 'rule_specs': [{ 'inputs': { 'x': 0 }, 'rule_type': 'Equals' }], 'training_data': [], 'tagged_skill_misconception_id': None } question_state_dict_with_math = { 'content': { 'content_id': 'content_1', 'html': 'Question 1' }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'content_1': {}, 'feedback_1': {}, 'feedback_2': {}, 'hint_1': {}, 'solution': {} } }, 'written_translations': { 'translations_mapping': { 'content_1': {}, 'feedback_1': {}, 'feedback_2': {}, 'hint_1': {}, 'solution': { 'en': { 'html': html_content, 'needs_update': True } } } }, 'interaction': { 'answer_groups': [answer_group], 'confirmed_unclassified_answers': [], 'customization_args': { 'choices': { 'value': [html_content] }, 'showChoicesInShuffledOrder': { 'value': True } }, 'default_outcome': { 'dest': None, 'feedback': { 'content_id': 'feedback_2', 'html': html_content }, 'param_changes': [], 'refresher_exploration_id': None, 'labelled_as_correct': True, 'missing_prerequisite_skill_id': None }, 'hints': [{ 'hint_content': { 'content_id': 'hint_1', 'html': 'Hint 1' } }], 'solution': { 'answer_is_exclusive': False, 'correct_answer': 0, 'explanation': { 'content_id': 'solution', 'html': '<p>This is a solution.</p>' } }, 'id': 'MultipleChoiceInput' }, 'param_changes': [], 'solicit_answer_details': False, 'classifier_model_id': None } suggestion_dict_with_math = { 'suggestion_id': 'skill2.thread1', 'suggestion_type': suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, 'target_type': suggestion_models.TARGET_TYPE_SKILL, 'target_id': 'skill2', 'target_version_at_submission': 1, 'status': suggestion_models.STATUS_ACCEPTED, 'author_name': 'author', 'final_reviewer_id': self.reviewer_id, 'change': { 'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION, 'question_dict': { 'question_state_data': question_state_dict_with_math, 'language_code': 'en', 'question_state_data_schema_version': ( feconf.CURRENT_STATE_SCHEMA_VERSION), 'linked_skill_ids': ['skill_2'] }, 'skill_id': 'skill_2', 'skill_difficulty': 0.3, }, 'score_category': 'question.skill1', 'last_updated': utils.get_time_in_millisecs(self.fake_date) } with self.swap( feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_skill_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, suggestion_models.TARGET_TYPE_SKILL, 'skill1', feconf.CURRENT_STATE_SCHEMA_VERSION, self.author_id, suggestion_dict_with_math['change'], 'test description') job_id = ( suggestion_jobs_one_off. SuggestionMathRteAuditOneOffJob.create_new()) suggestion_jobs_one_off.SuggestionMathRteAuditOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() actual_output = ( suggestion_jobs_one_off. SuggestionMathRteAuditOneOffJob.get_output(job_id)) self.assertRegexpMatches( python_utils.UNICODE(actual_output), '2 suggestions have Math components in them')
def test_skip_migration_for_suggestion_with_new_math_schema(self): """Tests that a suggestion already having the new math_schema is not migrated. """ expected_html_content = ( '<p>Value</p><oppia-noninteractive-math math_content-with-value=' '"{&quot;raw_latex&quot;: &quot;+,-,-,+&quot;, &' 'amp;quot;svg_filename&quot;: &quot;&quot;}"></oppia' '-noninteractive-math>') state_dict = { 'classifier_model_id': None, 'content': { 'content_id': 'content', 'html': expected_html_content }, 'interaction': { 'answer_groups': [], 'confirmed_unclassified_answers': [], 'customization_args': {}, 'default_outcome': { 'dest': 'Introduction', 'feedback': { 'content_id': 'default_outcome', 'html': expected_html_content }, 'labelled_as_correct': False, 'param_changes': [], 'refresher_exploration_id': None, 'missing_prerequisite_skill_id': None }, 'hints': [], 'id': None, 'solution': None, }, 'param_changes': [], 'recorded_voiceovers': { 'voiceovers_mapping': { 'content': {}, 'default_outcome': {} } }, 'solicit_answer_details': False, 'written_translations': { 'translations_mapping': { 'content': {}, 'default_outcome': {} } } } states = { 'Introduction': state_dict } exploration = ( exp_domain.Exploration( 'exp1', feconf.DEFAULT_EXPLORATION_TITLE, 'Algebra', feconf.DEFAULT_EXPLORATION_OBJECTIVE, constants.DEFAULT_LANGUAGE_CODE, [], '', '', feconf.CURRENT_STATE_SCHEMA_VERSION, feconf.DEFAULT_INIT_STATE_NAME, states, {}, [], 0, False, False)) exp_services.save_new_exploration(self.author_id, exploration) change_dict = { 'cmd': exp_domain.CMD_EDIT_STATE_PROPERTY, 'property_name': exp_domain.STATE_PROPERTY_CONTENT, 'state_name': 'Introduction', 'new_value': { 'content_id': 'content', 'html': 'new suggestion' }, 'old_value': { 'content_id': 'content', 'html': expected_html_content } } with self.swap( feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_exploration_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT, suggestion_models.TARGET_TYPE_EXPLORATION, self.target_id, self.target_version_at_submission, self.author_id, change_dict, 'test description') job_id = ( suggestion_jobs_one_off. SuggestionMathMigrationOneOffJob.create_new()) suggestion_jobs_one_off.SuggestionMathMigrationOneOffJob.enqueue(job_id) self.process_and_flush_pending_tasks() actual_output = ( suggestion_jobs_one_off. SuggestionMathMigrationOneOffJob.get_output(job_id)) self.assertEqual(len(actual_output), 0)
def test_migration_skips_suggestions_failing_validation(self): html_content = ( '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a' 'mp;quot;+,-,-,+&quot;"></oppia-noninteractive-math>') answer_group = { 'outcome': { 'dest': None, 'feedback': { 'content_id': 'feedback_1', 'html': html_content }, 'labelled_as_correct': True, 'param_changes': [], 'refresher_exploration_id': None, 'missing_prerequisite_skill_id': None }, 'rule_specs': [{ 'inputs': { 'x': 0 }, 'rule_type': 'Equals' }], 'training_data': [], 'tagged_skill_misconception_id': None } question_state_dict = { 'content': { 'content_id': 'content_1', 'html': 'Question 1' }, 'recorded_voiceovers': { 'voiceovers_mapping': { 'content_1': {}, 'feedback_1': {}, 'feedback_2': {}, 'hint_1': {}, 'solution': {} } }, 'written_translations': { 'translations_mapping': { 'content_1': {}, 'feedback_1': {}, 'feedback_2': {}, 'hint_1': {}, 'solution': {} } }, 'interaction': { 'answer_groups': [answer_group], 'confirmed_unclassified_answers': [], 'customization_args': { 'choices': { 'value': ['option 1'] }, 'showChoicesInShuffledOrder': { 'value': True } }, 'default_outcome': { 'dest': None, 'feedback': { 'content_id': 'feedback_2', 'html': 'Correct Answer' }, 'param_changes': [], 'refresher_exploration_id': None, 'labelled_as_correct': True, 'missing_prerequisite_skill_id': None }, 'hints': [{ 'hint_content': { 'content_id': 'hint_1', 'html': 'Hint 1' } }], 'solution': { 'answer_is_exclusive': False, 'correct_answer': 0, 'explanation': { 'content_id': 'solution', 'html': '<p>This is a solution.</p>' } }, 'id': 'MultipleChoiceInput' }, 'param_changes': [], 'solicit_answer_details': False, 'classifier_model_id': None } suggestion_dict = { 'suggestion_id': 'skill1.thread1', 'suggestion_type': suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, 'target_type': suggestion_models.TARGET_TYPE_SKILL, 'target_id': 'skill1', 'target_version_at_submission': 1, 'status': suggestion_models.STATUS_ACCEPTED, 'author_name': 'author', 'final_reviewer_id': self.reviewer_id, 'change': { 'cmd': question_domain.CMD_CREATE_NEW_FULLY_SPECIFIED_QUESTION, 'question_dict': { 'question_state_data': question_state_dict, 'language_code': 'en', 'question_state_data_schema_version': ( feconf.CURRENT_STATE_SCHEMA_VERSION), 'linked_skill_ids': ['skill_1'] }, 'skill_id': 'skill_1', 'skill_difficulty': 0.3, }, 'score_category': 'question.skill1', 'last_updated': utils.get_time_in_millisecs(self.fake_date) } with self.swap( feedback_models.GeneralFeedbackThreadModel, 'generate_new_thread_id', self.mock_generate_new_skill_thread_id): suggestion_services.create_suggestion( suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, suggestion_models.TARGET_TYPE_SKILL, 'skill1', feconf.CURRENT_STATE_SCHEMA_VERSION, self.author_id, suggestion_dict['change'], 'test description') def _mock_get_suggestion_by_id(unused_suggestion_id): """Mocks get_suggestion_by_id().""" return 'invalid_suggestion' get_suggestion_by_id_swap = ( self.swap( suggestion_services, 'get_suggestion_by_id', _mock_get_suggestion_by_id)) with get_suggestion_by_id_swap: job_id = ( suggestion_jobs_one_off.SuggestionMathMigrationOneOffJob. create_new()) ( suggestion_jobs_one_off. SuggestionMathMigrationOneOffJob.enqueue(job_id)) self.process_and_flush_pending_tasks() actual_output = ( suggestion_jobs_one_off.SuggestionMathMigrationOneOffJob. get_output(job_id)) expected_output = ( u'[u\'validation_error\', [u"Suggestion skill1.thread1 failed v' + 'alidation: \'unicode\' object has no attribute \'validate\'"]]') self.assertEqual(actual_output, [expected_output])