def test_get_all_threads(self): # Create an anonymous feedback thread expected_thread_dict = { 'status': u'open', 'state_name': u'a_state_name', 'summary': None, 'original_author_username': None, 'subject': u'a subject' } feedback_services.create_thread( self.EXP_ID, expected_thread_dict['state_name'], None, expected_thread_dict['subject'], 'not used here') threads = feedback_services.get_all_threads(self.EXP_ID, False) self.assertEqual(1, len(threads)) self.assertDictContainsSubset(expected_thread_dict, threads[0].to_dict()) # Viewer creates feedback thread expected_thread_dict = { 'status': u'open', 'state_name': u'a_state_name_second', 'summary': None, 'original_author_username': self.VIEWER_USERNAME, 'subject': u'a subject second' } feedback_services.create_thread( self.EXP_ID, expected_thread_dict['state_name'], self.viewer_id, expected_thread_dict['subject'], 'not used here') threads = feedback_services.get_all_threads(self.EXP_ID, False) self.assertEqual(2, len(threads)) self.assertDictContainsSubset(expected_thread_dict, threads[1].to_dict())
def test_feedback_ids(self): """Test various conventions for thread and message ids.""" exp_id = '0' feedback_services.create_thread( exp_id, 'a_state_name', None, 'a subject', 'some text') threadlist = feedback_services.get_all_threads(exp_id, False) self.assertEqual(len(threadlist), 1) thread_id = threadlist[0].get_thread_id() # The thread id should not have any full stops. self.assertNotIn('.', thread_id) messages = feedback_services.get_messages(exp_id, thread_id) self.assertEqual(len(messages), 1) message_id = messages[0].message_id self.assertTrue(isinstance(message_id, int)) # Retrieve the message instance from the storage layer. datastore_id = feedback_models.FeedbackMessageModel.get_messages( exp_id, thread_id)[0].id full_thread_id = (feedback_models.FeedbackThreadModel .generate_full_thread_id(exp_id, thread_id)) # The message id should be prefixed with the full thread id and a full # stop, followed by the message id. self.assertEqual( datastore_id, '%s.%s' % (full_thread_id, message_id))
def test_that_correct_emails_are_sent_for_multiple_feedback(self): expected_email_html_body = ( 'Hi editor,<br>' '<br>' 'You\'ve received 1 new message on your Oppia explorations:<br>' '<ul><li>Title: some text<br></li>' '<li>Title: more text<br></li></ul>' 'You can view and reply to your messages from your ' '<a href="https://www.oppia.org/dashboard">dashboard</a>.' '<br>' 'Thanks, and happy teaching!<br>' '<br>' 'Best wishes,<br>' 'The Oppia Team<br>' '<br>' 'You can change your email preferences via the ' '<a href="https://www.example.com">Preferences</a> page.') expected_email_text_body = ( 'Hi editor,\n' '\n' 'You\'ve received 1 new message on your Oppia explorations:\n' '- Title: some text\n' '- Title: more text\n' 'You can view and reply to your messages from your dashboard.' '\n' 'Thanks, and happy teaching!\n' '\n' 'Best wishes,\n' 'The Oppia Team\n' '\n' 'You can change your email preferences via the Preferences page.') with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.new_user_id, 'a subject', 'some text') threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() feedback_services.create_message( self.exploration.id, thread_id, self.new_user_id, feedback_models.STATUS_CHOICES_OPEN, 'subject', 'more text') messagelist = feedback_services.get_messages( self.exploration.id, thread_id) self.assertEqual(len(messagelist), 2) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 1) self.assertEqual( messages[0].html.decode(), expected_email_html_body) self.assertEqual( messages[0].body.decode(), expected_email_text_body)
def test_thread_closed_job_running(self): with self.swap( jobs_registry, "ALL_CONTINUOUS_COMPUTATION_MANAGERS", self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS ): # Create test objects. exp_id = "eid" self.save_new_valid_exploration(exp_id, "owner") # Trigger thread creation events. self.process_and_flush_pending_tasks() feedback_services.create_thread(exp_id, "a_state_name", None, "a subject", "some text") self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics(exp_id), {"num_open_threads": 1, "num_total_threads": 1}, ) # Trigger close event. threadlist = feedback_services.get_threadlist(exp_id) thread_id = threadlist[0]["thread_id"] feedback_services.create_message( thread_id, "author", feedback_models.STATUS_CHOICES_FIXED, None, "some text" ) self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics(exp_id), {"num_open_threads": 0, "num_total_threads": 1}, )
def test_realtime_with_batch_computation(self): with self.swap( jobs_registry, "ALL_CONTINUOUS_COMPUTATION_MANAGERS", self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS ): # Create test objects. user_id = "uid" exp_id = "eid" self.save_new_valid_exploration(exp_id, "owner") feedback_services.create_thread(exp_id, "a_state_name", None, "a subject", "some text") # Start job. self.process_and_flush_pending_tasks() ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() # Stop job. ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics(exp_id), {"num_open_threads": 1, "num_total_threads": 1}, ) # Create another thread but don't start job. feedback_services.create_thread(exp_id, "a_state_name", None, "a subject", "some text") self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics(exp_id), {"num_open_threads": 2, "num_total_threads": 2}, )
def test_feedback_ids(self): """Test various conventions for thread and message ids.""" EXP_ID = '0' feedback_services.create_thread( EXP_ID, 'a_state_name', None, 'a subject', 'some text') threadlist = feedback_services.get_threadlist(EXP_ID) self.assertEqual(len(threadlist), 1) thread_id = threadlist[0]['thread_id'] # The thread id should be prefixed with the exploration id and a full # stop. self.assertTrue(thread_id.startswith('%s.' % EXP_ID)) # The rest of the thread id should not have any full stops. self.assertNotIn('.', thread_id[len(EXP_ID) + 1:]) messages = feedback_services.get_messages(threadlist[0]['thread_id']) self.assertEqual(len(messages), 1) message_id = messages[0]['message_id'] self.assertTrue(isinstance(message_id, int)) # Retrieve the message instance from the storage layer. datastore_id = feedback_models.FeedbackMessageModel.get_messages( thread_id)[0].id # The message id should be prefixed with the thread id and a full stop, # followed by the message id. self.assertEqual( datastore_id, '%s.%s' % (thread_id, message_id))
def test_send_feedback_message_email(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.user_id_a, 'a subject', 'some text') threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() messagelist = feedback_services.get_messages( self.exploration.id, thread_id) self.assertEqual(len(messagelist), 1) expected_feedback_message_dict = { 'exploration_id': self.exploration.id, 'thread_id': thread_id, 'message_id': messagelist[0].message_id } # There are two jobs in the taskqueue: one for the realtime event # associated with creating a thread, and one for sending the email. self.assertEqual(self.count_jobs_in_taskqueue(), 2) model = feedback_models.UnsentFeedbackEmailModel.get(self.editor_id) self.assertEqual(len(model.feedback_message_references), 1) self.assertDictEqual( model.feedback_message_references[0], expected_feedback_message_dict) self.assertEqual(model.retries, 0)
def test_status_of_newly_created_thread_is_open(self): exp_id = '0' feedback_services.create_thread( exp_id, 'a_state_name', None, 'a subject', 'some text') threadlist = feedback_services.get_all_threads(exp_id, False) thread_status = threadlist[0].status self.assertEqual(thread_status, feedback_models.STATUS_CHOICES_OPEN)
def test_status_of_newly_created_thread_is_open(self): EXP_ID = '0' feedback_services.create_thread( EXP_ID, 'a_state_name', None, 'a subject', 'some text') threadlist = feedback_services.get_threadlist(EXP_ID) thread_status = threadlist[0]['status'] self.assertEqual(thread_status, feedback_models.STATUS_CHOICES_OPEN)
def test_posting_to_feedback_thread_results_in_subscription(self): # The viewer posts a message to the thread. message_text = 'text' feedback_services.create_thread( 'exp_id', 'state_name', self.viewer_id, 'subject', message_text) thread_ids_subscribed_to = self._get_thread_ids_subscribed_to( self.viewer_id) self.assertEqual(len(thread_ids_subscribed_to), 1) full_thread_id = thread_ids_subscribed_to[0] thread_id = ( feedback_domain.FeedbackThread.get_thread_id_from_full_thread_id( full_thread_id)) self.assertEqual( feedback_services.get_messages('exp_id', thread_id)[0].text, message_text) # The editor posts a follow-up message to the thread. new_message_text = 'new text' feedback_services.create_message( 'exp_id', thread_id, self.editor_id, '', '', new_message_text) # The viewer and editor are now both subscribed to the thread. self.assertEqual( self._get_thread_ids_subscribed_to(self.viewer_id), [full_thread_id]) self.assertEqual( self._get_thread_ids_subscribed_to(self.editor_id), [full_thread_id])
def test_get_total_open_threads_for_multiple_explorations(self): feedback_services.create_thread( self.EXP_ID_1, self.EXPECTED_THREAD_DICT['state_name'], None, self.EXPECTED_THREAD_DICT['subject'], 'not used here') feedback_services.create_thread( self.EXP_ID_2, self.EXPECTED_THREAD_DICT['state_name'], None, self.EXPECTED_THREAD_DICT['subject'], 'not used here') threads_exp_1 = feedback_services.get_all_threads(self.EXP_ID_1, False) self.assertEqual(1, len(threads_exp_1)) threads_exp_2 = feedback_services.get_all_threads(self.EXP_ID_2, False) self.assertEqual(1, len(threads_exp_2)) def _close_thread(exp_id, thread_id): thread = (feedback_models.FeedbackThreadModel. get_by_exp_and_thread_id(exp_id, thread_id)) thread.status = feedback_models.STATUS_CHOICES_FIXED thread.put() _close_thread(self.EXP_ID_1, threads_exp_1[0].get_thread_id()) self.assertEqual( len(feedback_services.get_closed_threads(self.EXP_ID_1, False)), 1) self._run_computation() self.assertEqual(feedback_services.get_total_open_threads( feedback_services.get_thread_analytics_multi( [self.EXP_ID_1, self.EXP_ID_2])), 1)
def test_realtime_with_batch_computation(self): with self._get_swap_context(): # Create test objects. user_id = 'uid' exp_id = 'eid' self.save_new_valid_exploration(exp_id, 'owner') feedback_services.create_thread( exp_id, 'a_state_name', None, 'a subject', 'some text') # Start job. self.process_and_flush_pending_tasks() ModifiedFeedbackAnalyticsAggregator.start_computation() self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() # Stop job. ModifiedFeedbackAnalyticsAggregator.stop_computation(user_id) self.assertEqual(self.count_jobs_in_taskqueue(), 0) self._flush_tasks_and_check_analytics(exp_id, { 'num_open_threads': 1, 'num_total_threads': 1, }) # Create another thread but don't start job. feedback_services.create_thread( exp_id, 'a_state_name', None, 'a subject', 'some text') self._flush_tasks_and_check_analytics(exp_id, { 'num_open_threads': 2, 'num_total_threads': 2, })
def test_thread_closed_job_running(self): with self._get_swap_context(): # Create test objects. exp_id = 'eid' self.save_new_valid_exploration(exp_id, 'owner') # Trigger thread creation events. self.process_and_flush_pending_tasks() feedback_services.create_thread( exp_id, 'a_state_name', None, 'a subject', 'some text') self._flush_tasks_and_check_analytics(exp_id, { 'num_open_threads': 1, 'num_total_threads': 1, }) # Trigger close event. threadlist = feedback_services.get_all_threads(exp_id, False) thread_id = threadlist[0]['thread_id'] feedback_services.create_message( exp_id, thread_id, 'author', feedback_models.STATUS_CHOICES_FIXED, None, 'some text') self._flush_tasks_and_check_analytics(exp_id, { 'num_open_threads': 0, 'num_total_threads': 1, })
def test_making_feedback_thread_does_not_subscribe_to_exploration(self): with self._get_test_context(): self.signup(USER_A_EMAIL, USER_A_USERNAME) user_a_id = self.get_user_id_from_email(USER_A_EMAIL) self.signup(USER_B_EMAIL, USER_B_USERNAME) user_b_id = self.get_user_id_from_email(USER_B_EMAIL) # User A creates an exploration. self.save_new_valid_exploration( EXP_ID, user_a_id, title=EXP_TITLE, category='Category') exp_last_updated_ms = ( self._get_most_recent_exp_snapshot_created_on_ms(EXP_ID)) # User B starts a feedback thread. feedback_services.create_thread( EXP_ID, None, user_b_id, FEEDBACK_THREAD_SUBJECT, 'text') thread_id = feedback_services.get_all_threads( EXP_ID, False)[0].get_thread_id() message = feedback_services.get_messages( EXP_ID, thread_id)[0] ModifiedRecentUpdatesAggregator.start_computation() self.assertEqual( self.count_jobs_in_taskqueue( queue_name=taskqueue_services.QUEUE_NAME_DEFAULT), 1) self.process_and_flush_pending_tasks() recent_notifications_for_user_a = ( ModifiedRecentUpdatesAggregator.get_recent_notifications( user_a_id)[1]) recent_notifications_for_user_b = ( ModifiedRecentUpdatesAggregator.get_recent_notifications( user_b_id)[1]) expected_thread_notification = { 'activity_id': EXP_ID, 'activity_title': EXP_TITLE, 'author_id': user_b_id, 'last_updated_ms': utils.get_time_in_millisecs( message.created_on), 'subject': FEEDBACK_THREAD_SUBJECT, 'type': feconf.UPDATE_TYPE_FEEDBACK_MESSAGE, } expected_creation_notification = ( self._get_expected_activity_created_dict( user_a_id, EXP_ID, EXP_TITLE, 'exploration', feconf.UPDATE_TYPE_EXPLORATION_COMMIT, exp_last_updated_ms)) # User A sees A's commit and B's feedback thread. self.assertEqual(recent_notifications_for_user_a, [ expected_thread_notification, expected_creation_notification ]) # User B sees only her feedback thread, but no commits. self.assertEqual(recent_notifications_for_user_b, [ expected_thread_notification, ])
def test_multiple_exploration_commits_and_feedback_messages(self): with self._get_test_context(): self.signup(self.EDITOR_EMAIL, self.EDITOR_USERNAME) editor_id = self.get_user_id_from_email(self.EDITOR_EMAIL) # User creates an exploration. self.save_new_valid_exploration( EXP_1_ID, editor_id, title=EXP_1_TITLE, category='Category') exp1_last_updated_ms = ( self._get_most_recent_exp_snapshot_created_on_ms(EXP_1_ID)) # User gives feedback on it. feedback_services.create_thread( EXP_1_ID, None, editor_id, FEEDBACK_THREAD_SUBJECT, 'text') thread_id = feedback_services.get_all_threads( EXP_1_ID, False)[0].get_thread_id() message = feedback_services.get_messages(EXP_1_ID, thread_id)[0] # User creates another exploration. self.save_new_valid_exploration( EXP_2_ID, editor_id, title=EXP_2_TITLE, category='Category') exp2_last_updated_ms = ( self._get_most_recent_exp_snapshot_created_on_ms(EXP_2_ID)) ModifiedRecentUpdatesAggregator.start_computation() self.assertEqual( self.count_jobs_in_taskqueue( queue_name=taskqueue_services.QUEUE_NAME_DEFAULT), 1) self.process_and_flush_pending_tasks() recent_notifications = ( ModifiedRecentUpdatesAggregator.get_recent_notifications( editor_id)[1]) self.assertEqual([( self._get_expected_activity_created_dict( editor_id, EXP_2_ID, EXP_2_TITLE, 'exploration', feconf.UPDATE_TYPE_EXPLORATION_COMMIT, exp2_last_updated_ms) ), { 'activity_id': EXP_1_ID, 'activity_title': EXP_1_TITLE, 'author_id': editor_id, 'last_updated_ms': utils.get_time_in_millisecs( message.created_on), 'subject': FEEDBACK_THREAD_SUBJECT, 'type': feconf.UPDATE_TYPE_FEEDBACK_MESSAGE, }, ( self._get_expected_activity_created_dict( editor_id, EXP_1_ID, EXP_1_TITLE, 'exploration', feconf.UPDATE_TYPE_EXPLORATION_COMMIT, exp1_last_updated_ms) )], recent_notifications)
def test_get_threads_single_exploration(self): threads = feedback_services.get_threads(self.EXP_ID_1) self.assertEqual(len(threads), 0) feedback_services.create_thread( self.EXP_ID_1, self.EXPECTED_THREAD_DICT['state_name'], None, self.EXPECTED_THREAD_DICT['subject'], 'not used here') threads = feedback_services.get_threads(self.EXP_ID_1) self.assertEqual(1, len(threads)) self.assertDictContainsSubset(self.EXPECTED_THREAD_DICT, threads[0].to_dict())
def test_get_total_open_threads_for_single_exploration(self): feedback_services.create_thread( self.EXP_ID_1, self.EXPECTED_THREAD_DICT['state_name'], None, self.EXPECTED_THREAD_DICT['subject'], 'not used here') threads = feedback_services.get_all_threads(self.EXP_ID_1, False) self.assertEqual(1, len(threads)) self._run_computation() self.assertEqual(feedback_services.get_total_open_threads( feedback_services.get_thread_analytics_multi([self.EXP_ID_1])), 1)
def post(self, exploration_id): subject = self.payload.get("subject") if not subject: raise self.InvalidInputException("A thread subject must be specified.") text = self.payload.get("text") if not text: raise self.InvalidInputException("Text for the first message in the thread must be specified.") feedback_services.create_thread(exploration_id, self.payload.get("state_name"), self.user_id, subject, text) self.render_json(self.values)
def test_that_email_are_not_sent_to_author_himself(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.editor_id, 'a subject', 'A message') # Note: the job in the taskqueue represents the realtime # event emitted by create_thread(). self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 0)
def test_get_total_open_threads_before_job_run(self): self.assertEqual(feedback_services.get_total_open_threads( feedback_services.get_thread_analytics_multi([self.EXP_ID_1])), 0) feedback_services.create_thread( self.EXP_ID_1, self.EXPECTED_THREAD_DICT['state_name'], None, self.EXPECTED_THREAD_DICT['subject'], 'not used here') threads = feedback_services.get_all_threads(self.EXP_ID_1, False) self.assertEqual(1, len(threads)) self.assertEqual(feedback_services.get_total_open_threads( feedback_services.get_thread_analytics_multi([self.EXP_ID_1])), 0)
def test_multiple_threads_multiple_exp(self): with self.swap( jobs_registry, "ALL_CONTINUOUS_COMPUTATION_MANAGERS", self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS ): # Create test objects. exp_id_1 = "eid1" exp_id_2 = "eid2" self.save_new_valid_exploration(exp_id_1, "owner") self.save_new_valid_exploration(exp_id_2, "owner") # Trigger thread creation events. self.process_and_flush_pending_tasks() feedback_services.create_thread(exp_id_1, "a_state_name", None, "a subject", "some text") feedback_services.create_thread(exp_id_1, "a_state_name", None, "a subject", "some text") feedback_services.create_thread(exp_id_2, "a_state_name", None, "a subject", "some text") feedback_services.create_thread(exp_id_2, "a_state_name", None, "a subject", "some text") self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics(exp_id_1), {"num_open_threads": 2, "num_total_threads": 2}, ) self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics(exp_id_2), {"num_open_threads": 2, "num_total_threads": 2}, )
def test_that_emails_are_sent_for_feedback_message(self): expected_email_html_body = ( 'Hi newuser,<br><br>' 'New update to thread "a subject" on ' '<a href="https://www.oppia.org/A">Title</a>:<br>' '<ul><li>editor: editor message<br></li></ul>' '(You received this message because you are a ' 'participant in this thread.)<br><br>' 'Best wishes,<br>' 'The Oppia team<br>' '<br>' 'You can change your email preferences via the ' '<a href="https://www.example.com">Preferences</a> page.') expected_email_text_body = ( 'Hi newuser,\n' '\n' 'New update to thread "a subject" on Title:\n' '- editor: editor message\n' '(You received this message because you are a' ' participant in this thread.)\n' '\n' 'Best wishes,\n' 'The Oppia team\n' '\n' 'You can change your email preferences via the Preferences page.') with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.new_user_id, 'a subject', 'some text') self.process_and_flush_pending_tasks() threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() feedback_services.create_message( self.exploration.id, thread_id, self.editor_id, None, None, 'editor message') self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.NEW_USER_EMAIL) self.assertEqual(len(messages), 1) self.assertEqual( messages[0].html.decode(), expected_email_html_body) self.assertEqual( messages[0].body.decode(), expected_email_text_body)
def post(self, exploration_id): """Handles POST requests.""" state_name = self.payload.get('state_name') subject = self.payload.get('subject', 'Feedback from a learner') feedback = self.payload.get('feedback') include_author = self.payload.get('include_author') feedback_services.create_thread( exploration_id, state_name, self.user_id if include_author else None, subject, feedback) self.render_json(self.values)
def test_multiple_threads_multiple_exp(self): with self._get_swap_context(): # Create test objects. exp_id_1 = 'eid1' exp_id_2 = 'eid2' self.save_new_valid_exploration(exp_id_1, 'owner') self.save_new_valid_exploration(exp_id_2, 'owner') # Trigger thread creation events. self.process_and_flush_pending_tasks() feedback_services.create_thread( exp_id_1, 'a_state_name', None, 'a subject', 'some text') feedback_services.create_thread( exp_id_1, 'a_state_name', None, 'a subject', 'some text') feedback_services.create_thread( exp_id_2, 'a_state_name', None, 'a subject', 'some text') feedback_services.create_thread( exp_id_2, 'a_state_name', None, 'a subject', 'some text') self._flush_tasks_and_check_analytics(exp_id_1, { 'num_open_threads': 2, 'num_total_threads': 2, }) self._flush_tasks_and_check_analytics(exp_id_2, { 'num_open_threads': 2, 'num_total_threads': 2, })
def test_get_thread_analytics_multi(self): with self._get_swap_context(): exp_id_1 = 'eid1' exp_id_2 = 'eid2' self.save_new_valid_exploration(exp_id_1, 'owner') self.save_new_valid_exploration(exp_id_2, 'owner') initial_feedback_threads = ( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics_multi( [exp_id_1, exp_id_2])) self.assertEqual(len(initial_feedback_threads), 2) self.assertEqual(initial_feedback_threads[0].to_dict(), { 'num_open_threads': 0, 'num_total_threads': 0, }) self.assertEqual(initial_feedback_threads[1].to_dict(), { 'num_open_threads': 0, 'num_total_threads': 0, }) feedback_services.create_thread( exp_id_1, None, 'owner', 'subject', 'text') self.process_and_flush_pending_tasks() feedback_threads = ( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics_multi( [exp_id_1, exp_id_2])) self.assertEqual(len(feedback_threads), 2) self.assertEqual(feedback_threads[0].to_dict(), { 'num_open_threads': 1, 'num_total_threads': 1, }) self.assertEqual(feedback_threads[1].to_dict(), { 'num_open_threads': 0, 'num_total_threads': 0, }) self._run_job() feedback_threads_after_running_job = ( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics_multi( [exp_id_1, exp_id_2])) self.assertEqual(len(feedback_threads_after_running_job), 2) self.assertEqual(feedback_threads_after_running_job[0].to_dict(), { 'num_open_threads': 1, 'num_total_threads': 1, }) self.assertEqual(feedback_threads_after_running_job[1].to_dict(), { 'num_open_threads': 0, 'num_total_threads': 0, })
def test_email_is_not_sent_if_recipient_has_declined_such_emails(self): user_services.update_email_preferences( self.editor_id, True, False, False) with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.user_id_a, 'a subject', 'some text') # Note: the job in the taskqueue represents the realtime # event emitted by create_thread(). self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 0)
def post(self, exploration_id): """Handles POST requests. Args: exploration_id: str. The ID of the exploration. """ state_name = self.payload.get('state_name') subject = self.payload.get('subject', 'Feedback from a learner') feedback = self.payload.get('feedback') include_author = self.payload.get('include_author') feedback_services.create_thread( feconf.ENTITY_TYPE_EXPLORATION, exploration_id, state_name, self.user_id if include_author else None, subject, feedback) self.render_json(self.values)
def test_single_thread_single_exp(self): with self._get_swap_context(): # Create test objects. exp_id = 'eid' self.save_new_valid_exploration(exp_id, 'owner') # Trigger thread creation event. self.process_and_flush_pending_tasks() feedback_services.create_thread('exploration', exp_id, None, 'a subject', 'some text') self._flush_tasks_and_check_analytics(exp_id, { 'num_open_threads': 1, 'num_total_threads': 1, })
def post(self, exploration_id): subject = self.payload.get('subject') if not subject: raise self.InvalidInputException( 'A thread subject must be specified.') text = self.payload.get('text') if not text: raise self.InvalidInputException( 'Text for the first message in the thread must be specified.') feedback_services.create_thread(feconf.ENTITY_TYPE_EXPLORATION, exploration_id, self.user_id, subject, text) self.render_json(self.values)
def test_get_total_open_threads_before_job_run(self): self.assertEqual(feedback_services.get_total_open_threads( feedback_services.get_thread_analytics_multi([self.EXP_ID_1])), 0) feedback_services.create_thread( 'exploration', self.EXP_ID_1, None, self.EXPECTED_THREAD_DICT['subject'], 'not used here') threads = feedback_services.get_all_threads( 'exploration', self.EXP_ID_1, False) self.assertEqual(1, len(threads)) self.assertEqual(feedback_services.get_total_open_threads( feedback_services.get_thread_analytics_multi( [self.EXP_ID_1])), 0)
def test_message_count(self): """Test if the job returns the correct message count.""" feedback_services.create_thread( self.EXP_ID_1, self.EXPECTED_THREAD_DICT['state_name'], self.user_id, self.EXPECTED_THREAD_DICT['subject'], 'not used here') feedback_services.create_thread( self.EXP_ID_2, self.EXPECTED_THREAD_DICT['state_name'], self.user_id, self.EXPECTED_THREAD_DICT['subject'], 'not used here') thread_ids = subscription_services.get_all_threads_subscribed_to( self.user_id) self._run_one_off_job() thread_summaries, _ = feedback_services.get_thread_summaries( self.user_id, thread_ids) # Check that the first message has only one message. self.assertEqual(thread_summaries[0]['total_message_count'], 1) # Check that the second message has only one message. self.assertEqual(thread_summaries[1]['total_message_count'], 1) feedback_services.create_message(self.EXP_ID_1, thread_ids[0].split('.')[1], self.user_id, None, None, 'editor message') self._run_one_off_job() thread_summaries, _ = feedback_services.get_thread_summaries( self.user_id, thread_ids) # Check that the first message has two messages. self.assertEqual(thread_summaries[0]['total_message_count'], 2) # Get the first message so that we can delete it and check the error # case. first_message_model = (feedback_models.FeedbackMessageModel.get( self.EXP_ID_1, thread_ids[0].split('.')[1], 0)) first_message_model.delete() output = self._run_one_off_job() # Check if the quantities have the correct values. self.assertEqual(output[0][1]['message_count'], 1) self.assertEqual(output[0][1]['next_message_id'], 2)
def test_email_sent_to_moderator_after_flag(self): """Tests Flagged Exploration Email Handler.""" def fake_get_user_ids_by_role(_): """Replaces get_user_ids_by_role for testing purposes.""" return [self.moderator_id] get_moderator_id_as_list = self.swap(user_services, 'get_user_ids_by_role', fake_get_user_ids_by_role) with self.can_send_feedback_email_ctx, self.can_send_emails_ctx: with get_moderator_id_as_list: # Create thread. feedback_services.create_thread(feconf.ENTITY_TYPE_EXPLORATION, self.exploration.id, self.user_id_a, 'bad subject', 'bad text') # User B reports thread, sends email. payload = { 'exploration_id': self.exploration.id, 'report_text': 'He said a bad word :-( ', 'reporter_id': self.user_id_b } taskqueue_services.enqueue_email_task( feconf.TASK_URL_FLAG_EXPLORATION_EMAILS, payload, 0) # Ensure moderator has no messages sent to him yet. messages = self.mail_stub.get_sent_messages( to=self.MODERATOR_EMAIL) self.assertEqual(len(messages), 0) # Invoke Flag Exploration Email Handler. self.process_and_flush_pending_tasks() # Ensure moderator has 1 email now. messages = self.mail_stub.get_sent_messages( to=self.MODERATOR_EMAIL) self.assertEqual(len(messages), 1) # Ensure moderator has received correct email. expected_message = ( 'Hello Moderator,\nuserB has flagged exploration "Title"' ' on the following grounds: \nHe said a bad word :-( ' ' .\nYou can modify the exploration by clicking here' '.\n\nThanks!\n- The Oppia Team\n\nYou can change your' ' email preferences via the Preferences page.') self.assertEqual(messages[0].body.decode(), expected_message)
def test_thread_closed_status_changed(self): with self.swap( jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS', self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS): # Create test objects. exp_id = 'eid' self.save_new_valid_exploration(exp_id, 'owner') # Trigger thread creation events. self.process_and_flush_pending_tasks() feedback_services.create_thread( exp_id, 'a_state_name', None, 'a subject', 'some text') self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( exp_id), { 'num_open_threads': 1, 'num_total_threads': 1, }) # Trigger close event. threadlist = feedback_services.get_threadlist(exp_id) thread_id = threadlist[0]['thread_id'] feedback_services.create_message(thread_id, 'author', feedback_models.STATUS_CHOICES_FIXED, None, 'some text') self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( exp_id), { 'num_open_threads': 0, 'num_total_threads': 1, }) # Trigger thread status change event. threadlist = feedback_services.get_threadlist(exp_id) thread_id = threadlist[0]['thread_id'] feedback_services.create_message(thread_id, 'author', feedback_models.STATUS_CHOICES_IGNORED, None, 'some text') self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( exp_id), { 'num_open_threads': 0, 'num_total_threads': 1, })
def test_get_all_messages(self) -> None: thread_id = feedback_services.create_thread( # type: ignore[no-untyped-call] 'exploration', '0', None, 'subject 1', 'text 1') feedback_services.create_message( # type: ignore[no-untyped-call] thread_id, None, 'open', 'subject 2', 'text 2') model = feedback_models.GeneralFeedbackMessageModel.get(thread_id, 0) # Ruling out the possibility of None for mypy type checking. assert model is not None self.assertEqual(model.entity_type, 'exploration') all_messages = ( feedback_models.GeneralFeedbackMessageModel.get_all_messages( 2, None)) self.assertEqual(len(all_messages[0]), 2) self.assertEqual(all_messages[0][0].thread_id, thread_id) self.assertEqual(all_messages[0][0].entity_id, '0') self.assertEqual(all_messages[0][0].entity_type, 'exploration') self.assertEqual(all_messages[0][0].text, 'text 2') self.assertEqual(all_messages[0][0].updated_subject, 'subject 2') self.assertEqual(all_messages[0][1].thread_id, thread_id) self.assertEqual(all_messages[0][1].entity_id, '0') self.assertEqual(all_messages[0][1].entity_type, 'exploration') self.assertEqual(all_messages[0][1].text, 'text 1') self.assertEqual(all_messages[0][1].updated_subject, 'subject 1')
def test_cannot_access_suggestion_to_topic_handler(self): self.login(self.ADMIN_EMAIL) thread_id = feedback_services.create_thread( suggestion_models.TARGET_TYPE_QUESTION, self.topic_id, self.author_id, 'description', '', has_suggestion=True) response = self.get_html_response(feconf.CREATOR_DASHBOARD_URL) csrf_token = self.get_csrf_token_from_response(response) with self.swap(constants, 'ENABLE_NEW_STRUCTURE_PLAYERS', False): self.put_json('%s/topic/%s/%s' % (feconf.SUGGESTION_ACTION_URL_PREFIX, self.topic_id, thread_id), { 'action': u'reject', 'review_message': u'Rejected!' }, csrf_token=csrf_token, expected_status_int=404) self.logout()
def test_that_emails_are_not_sent_if_service_is_disabled(self): cannot_send_emails_ctx = self.swap( feconf, 'CAN_SEND_EMAILS', False) cannot_send_feedback_message_email_ctx = self.swap( feconf, 'CAN_SEND_FEEDBACK_MESSAGE_EMAILS', False) with cannot_send_emails_ctx, cannot_send_feedback_message_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.user_id_a, 'a subject', 'some text') # Note: the job in the taskqueue represents the realtime # event emitted by create_thread(). self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 0)
def setUp(self): super(GeneralFeedbackThreadUserModelValidatorTests, self).setUp() self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME) self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL) exp = exp_domain.Exploration.create_default_exploration( '0', title='title 0', category='Art', ) exp_services.save_new_exploration(self.owner_id, exp) self.thread_id = feedback_services.create_thread('exploration', '0', self.owner_id, 'Subject', 'Text', has_suggestion=False) self.model_instance = ( feedback_models.GeneralFeedbackThreadUserModel.get_by_id( '%s.%s' % (self.owner_id, self.thread_id))) self.job_class = (prod_validation_jobs_one_off. GeneralFeedbackThreadUserModelAuditOneOffJob)
def create_suggestion(suggestion_type, target_type, target_id, target_version_at_submission, author_id, change, description, final_reviewer_id): """Creates a new SuggestionModel and the corresponding FeedbackThread. Args: suggestion_type: str. The type of the suggestion. target_type: str. The target entity being edited. (The above 2 parameters should be one of the constants defined in storage/suggestion/gae_models.py.) target_id: str. The ID of the target entity being suggested to. target_version_at_submission: int. The version number of the target entity at the time of creation of the suggestion. author_id: str. The ID of the user who submitted the suggestion. change: dict. The details of the suggestion. description: str. The description of the changes provided by the author. final_reviewer_id: str|None. The ID of the reviewer who has accepted/rejected the suggestion. """ if description is None: description = DEFAULT_SUGGESTION_THREAD_SUBJECT thread_id = feedback_services.create_thread( target_type, target_id, author_id, description, DEFAULT_SUGGESTION_THREAD_INITIAL_MESSAGE, has_suggestion=True) status = suggestion_models.STATUS_IN_REVIEW if target_type == suggestion_models.TARGET_TYPE_EXPLORATION: exploration = exp_fetchers.get_exploration_by_id(target_id) if suggestion_type == suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT: score_category = (suggestion_models.SCORE_TYPE_CONTENT + suggestion_models.SCORE_CATEGORY_DELIMITER + exploration.category) elif suggestion_type == suggestion_models.SUGGESTION_TYPE_TRANSLATE_CONTENT: score_category = (suggestion_models.SCORE_TYPE_TRANSLATION + suggestion_models.SCORE_CATEGORY_DELIMITER + exploration.category) content_html = exploration.get_content_html(change['state_name'], change['content_id']) if content_html != change['content_html']: raise Exception( 'The given content_html does not match the content of the ' 'exploration.') elif suggestion_type == suggestion_models.SUGGESTION_TYPE_ADD_QUESTION: score_category = (suggestion_models.SCORE_TYPE_QUESTION + suggestion_models.SCORE_CATEGORY_DELIMITER + target_id) else: raise Exception('Invalid suggestion type %s' % suggestion_type) suggestion_models.GeneralSuggestionModel.create( suggestion_type, target_type, target_id, target_version_at_submission, status, author_id, final_reviewer_id, change, score_category, thread_id)
def setUp(self): super(UnsentFeedbackEmailModelValidatorTests, self).setUp() self.signup(self.OWNER_EMAIL, self.OWNER_USERNAME) self.owner_id = self.get_user_id_from_email(self.OWNER_EMAIL) exp = exp_domain.Exploration.create_default_exploration( '0', title='title 0', category='Art', ) exp_services.save_new_exploration(self.owner_id, exp) self.thread_id = feedback_services.create_thread('exploration', '0', self.owner_id, 'Subject', 'Text', has_suggestion=False) feedback_message_references = [{ 'entity_type': 'exploration', 'entity_id': '0', 'thread_id': self.thread_id, 'message_id': 0 }] self.model_instance = feedback_models.UnsentFeedbackEmailModel( id=self.owner_id, feedback_message_references=feedback_message_references, retries=1) self.model_instance.update_timestamps() self.model_instance.put() self.job_class = (prod_validation_jobs_one_off. UnsentFeedbackEmailModelAuditOneOffJob)
def test_get_all_messages(self): thread_id = feedback_services.create_thread('exploration', '0', None, 'subject 1', 'text 1') feedback_services.create_message(thread_id, None, 'open', 'subject 2', 'text 2') model = feedback_models.GeneralFeedbackMessageModel.get(thread_id, 0) self.assertEqual(model.entity_type, 'exploration') all_messages = ( feedback_models.GeneralFeedbackMessageModel.get_all_messages( 2, None)) self.assertEqual(len(all_messages[0]), 2) self.assertEqual(all_messages[0][0].thread_id, thread_id) self.assertEqual(all_messages[0][0].entity_id, '0') self.assertEqual(all_messages[0][0].entity_type, 'exploration') self.assertEqual(all_messages[0][0].text, 'text 2') self.assertEqual(all_messages[0][0].updated_subject, 'subject 2') self.assertEqual(all_messages[0][1].thread_id, thread_id) self.assertEqual(all_messages[0][1].entity_id, '0') self.assertEqual(all_messages[0][1].entity_type, 'exploration') self.assertEqual(all_messages[0][1].text, 'text 1') self.assertEqual(all_messages[0][1].updated_subject, 'subject 1')
def test_that_emails_are_sent_for_feedback_message(self): expected_email_html_body = ( 'Hi newuser,<br><br>' 'New update to thread "a subject" on ' '<a href="https://www.oppia.org/create/A#/feedback">Title</a>:<br>' '<ul><li>editor: editor message<br></li></ul>' '(You received this message because you are a ' 'participant in this thread.)<br><br>' 'Best wishes,<br>' 'The Oppia team<br>' '<br>' 'You can change your email preferences via the ' '<a href="https://www.example.com">Preferences</a> page.') expected_email_text_body = ( 'Hi newuser,\n' '\n' 'New update to thread "a subject" on Title:\n' '- editor: editor message\n' '(You received this message because you are a' ' participant in this thread.)\n' '\n' 'Best wishes,\n' 'The Oppia team\n' '\n' 'You can change your email preferences via the Preferences page.') with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( 'exploration', self.exploration.id, self.new_user_id, 'a subject', 'some text') self.process_and_flush_pending_tasks() threadlist = feedback_services.get_all_threads( 'exploration', self.exploration.id, False) thread_id = threadlist[0].id feedback_services.create_message( thread_id, self.editor_id, None, None, 'editor message') self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.NEW_USER_EMAIL) self.assertEqual(len(messages), 1) self.assertEqual( messages[0].html.decode(), expected_email_html_body) self.assertEqual( messages[0].body.decode(), expected_email_text_body)
def test_add_new_feedback_message(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( 'exploration', self.exploration.id, self.user_id_a, 'a subject', 'some text') threadlist = feedback_services.get_all_threads( 'exploration', self.exploration.id, False) thread_id = threadlist[0].id feedback_services.create_message( thread_id, self.user_id_a, None, None, 'editor message') # There are two jobs in the taskqueue: one for the realtime # event associated with creating a thread, and one for sending # the email. self.assertEqual( self.count_jobs_in_taskqueue( taskqueue_services.QUEUE_NAME_EVENTS), 1) self.assertEqual( self.count_jobs_in_taskqueue( taskqueue_services.QUEUE_NAME_EMAILS), 1) messagelist = feedback_services.get_messages(thread_id) self.assertEqual(len(messagelist), 2) expected_feedback_message_dict1 = { 'entity_type': 'exploration', 'entity_id': self.exploration.id, 'thread_id': thread_id, 'message_id': messagelist[0].message_id } expected_feedback_message_dict2 = { 'entity_type': 'exploration', 'entity_id': self.exploration.id, 'thread_id': thread_id, 'message_id': messagelist[1].message_id } model = feedback_models.UnsentFeedbackEmailModel.get(self.editor_id) self.assertEqual(len(model.feedback_message_references), 2) self.assertDictEqual( model.feedback_message_references[0], expected_feedback_message_dict1) self.assertDictEqual( model.feedback_message_references[1], expected_feedback_message_dict2) self.assertEqual(model.retries, 0)
def test_that_emails_are_sent_for_registered_user(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.user_id_a, 'a subject', 'some text') # There are two jobs in the taskqueue: one for the realtime event # associated with creating a thread, and one for sending the email. self.assertEqual(self.count_jobs_in_taskqueue(), 2) tasks = self.get_pending_tasks() self.assertEqual( tasks[0].url, feconf.TASK_URL_FEEDBACK_MESSAGE_EMAILS) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 1)
def test_posting_to_feedback_thread_results_in_subscription(self): # The viewer posts a message to the thread. MESSAGE_TEXT = "text" feedback_services.create_thread("exp_id", "state_name", self.viewer_id, "subject", MESSAGE_TEXT) thread_ids_subscribed_to = self._get_thread_ids_subscribed_to(self.viewer_id) self.assertEqual(len(thread_ids_subscribed_to), 1) thread_id = thread_ids_subscribed_to[0] self.assertEqual(feedback_services.get_messages(thread_id)[0]["text"], MESSAGE_TEXT) # The editor posts a follow-up message to the thread. NEW_MESSAGE_TEXT = "new text" feedback_services.create_message(thread_id, self.editor_id, "", "", NEW_MESSAGE_TEXT) # The viewer and editor are now both subscribed to the thread. self.assertEqual(self._get_thread_ids_subscribed_to(self.viewer_id), [thread_id]) self.assertEqual(self._get_thread_ids_subscribed_to(self.editor_id), [thread_id])
def test_that_reply_emails_are_added_to_thread(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: # Create thread. feedback_services.create_thread( self.exploration.id, 'a_state_name', self.user_id_a, 'a subject', 'some text') threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() # Create another message. feedback_services.create_message( self.exploration.id, thread_id, self.user_id_b, None, None, 'user b message') # Check that there are 2 messages in thread. messages = feedback_services.get_messages( self.exploration.id, thread_id) self.assertEqual(len(messages), 2) # Check that received_via_email is set to False. self.assertFalse(messages[0].received_via_email) # Get reply_to id for user A. model = email_models.FeedbackEmailReplyToIdModel.get( self.user_id_a, self.exploration.id, thread_id) recipient_email = 'reply+%s@%s' % ( model.reply_to_id, feconf.INCOMING_EMAILS_DOMAIN_NAME) # Send email to Oppia. self.post_email( str(recipient_email), self.USER_A_EMAIL, 'feedback email reply', 'New reply') # Check that new message is added. messages = feedback_services.get_messages( self.exploration.id, thread_id) self.assertEqual(len(messages), 3) # Check content of message is correct. msg = messages[-1] self.assertEqual(msg.text, 'New reply') self.assertEqual(msg.author_id, self.user_id_a) self.assertTrue(msg.received_via_email)
def test_email_is_not_sent_recipient_has_muted_this_exploration(self): user_services.set_email_preferences_for_exploration( self.editor_id, self.exploration.id, mute_feedback_notifications=True) with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( 'exploration', self.exploration.id, self.user_id_a, 'a subject', 'some text') # Note: the job in the taskqueue represents the realtime # event emitted by create_thread(). self.assertEqual( self.count_jobs_in_taskqueue( taskqueue_services.QUEUE_NAME_EVENTS), 1) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 0)
def test_validate_score_category_for_question_suggestion(self): rubrics = [ skill_domain.Rubric(constants.SKILL_DIFFICULTIES[0], ['Explanation 1']), skill_domain.Rubric(constants.SKILL_DIFFICULTIES[1], ['Explanation 2']), skill_domain.Rubric(constants.SKILL_DIFFICULTIES[2], ['Explanation 3']) ] skill = skill_domain.Skill.create_default_skill( '0', 'skill_description', rubrics) skill_services.save_new_skill(self.owner_id, skill) 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': ['0'], 'inapplicable_skill_misconception_ids': ['skillid12345-0'] }, 'skill_id': '0', 'skill_difficulty': 0.3, } score_category = (suggestion_models.SCORE_TYPE_QUESTION + suggestion_models.SCORE_CATEGORY_DELIMITER + 'invalid_sub_category') thread_id = feedback_services.create_thread('skill', '0', self.owner_id, 'description', 'suggestion', has_suggestion=True) suggestion_models.GeneralSuggestionModel.create( suggestion_models.SUGGESTION_TYPE_ADD_QUESTION, suggestion_models.TARGET_TYPE_SKILL, '0', 1, suggestion_models.STATUS_ACCEPTED, self.owner_id, self.admin_id, change, score_category, thread_id, 'en') model_instance = ( suggestion_models.GeneralSuggestionModel.get_by_id(thread_id)) expected_output = [ (u'[u\'failed validation check for score category check of ' 'GeneralSuggestionModel\', [u\'Entity id %s: Score category' ' question.invalid_sub_category is invalid\']]') % (model_instance.id), u'[u\'fully-validated GeneralSuggestionModel\', 1]' ] self.run_job_and_check_output(expected_output, sort=True, literal_eval=False)
def test_that_emails_are_not_sent_to_anonymous_user(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: # Create thread as anonoymous user. feedback_services.create_thread( self.exploration.id, 'a_state_name', None, 'a subject', 'some text') self.process_and_flush_pending_tasks() threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() feedback_services.create_message( self.exploration.id, thread_id, self.editor_id, feedback_models.STATUS_CHOICES_FIXED, None, 'editor message') self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages() self.assertEqual(len(messages), 0)
def test_that_email_is_sent_for_reply_on_feedback(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.user_id_a, 'a subject', 'A message') # There are two jobs in the taskqueue: one for the realtime event # associated with creating a thread, and one for sending the email. self.assertEqual(self.count_jobs_in_taskqueue(), 2) self.process_and_flush_pending_tasks() threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() feedback_services.create_message( self.exploration.id, thread_id, self.editor_id, None, None, 'editor message') self.assertEqual(self.count_jobs_in_taskqueue(), 1) self.process_and_flush_pending_tasks()
def test_single_thread_single_exp(self): with self.swap(jobs_registry, 'ALL_CONTINUOUS_COMPUTATION_MANAGERS', self.ALL_CONTINUOUS_COMPUTATION_MANAGERS_FOR_TESTS): # Create test objects. exp_id = 'eid' self.save_new_valid_exploration(exp_id, 'owner') # Trigger thread creation event. self.process_and_flush_pending_tasks() feedback_services.create_thread(exp_id, 'a_state_name', None, 'a subject', 'some text') self.process_and_flush_pending_tasks() self.assertEqual( ModifiedFeedbackAnalyticsAggregator.get_thread_analytics( exp_id), { 'num_open_threads': 1, 'num_total_threads': 1, })
def create_suggestion( suggestion_type, target_type, target_id, target_version_at_submission, author_id, change_cmd, description, assigned_reviewer_id, final_reviewer_id): """Creates a new SuggestionModel and the corresponding FeedbackThread. Args: suggestion_type: str. The type of the suggestion. target_type: str. The target entity being edited. (The above 2 parameters should be one of the constants defined in storage/suggestion/gae_models.py.) target_id: str. The ID of the target entity being suggested to. target_version_at_submission: int. The version number of the target entity at the time of creation of the suggestion. author_id: str. The ID of the user who submitted the suggestion. change_cmd: dict. The details of the suggestion. description: str. The description of the changes provided by the author. assigned_reviewer_id: str|None. The ID of the user assigned to review the suggestion. final_reviewer_id: str|None. The ID of the reviewer who has accepted/rejected the suggestion. """ # TODO(nithesh): Remove the check for target type once the feedback threads # are generalised for all types of entities. As at the moment feedback # threads can only be linked to explorations, we have this check. # This will be completed as a part of milestone 2 of the generalised review # system project. if target_type == suggestion_models.TARGET_TYPE_EXPLORATION: thread_id = feedback_services.create_thread( target_id, None, author_id, description, DEFAULT_SUGGESTION_THREAD_SUBJECT) # This line and the if..else will be removed after the feedback thread # migration is complete and the IDs for both models match. thread_id = suggestion_models.TARGET_TYPE_EXPLORATION + '.' + thread_id else: raise Exception('Feedback threads can only be linked to explorations') status = ( suggestion_models.STATUS_IN_REVIEW if assigned_reviewer_id else suggestion_models.STATUS_RECEIVED) if target_type == suggestion_models.TARGET_TYPE_EXPLORATION: exploration = exp_services.get_exploration_by_id(target_id) if suggestion_type == suggestion_models.SUGGESTION_TYPE_EDIT_STATE_CONTENT: score_category = ( suggestion_models.SCORE_TYPE_CONTENT + suggestion_models.SCORE_CATEGORY_DELIMITER + exploration.category) suggestion_models.GeneralSuggestionModel.create( suggestion_type, target_type, target_id, target_version_at_submission, status, author_id, assigned_reviewer_id, final_reviewer_id, change_cmd, score_category, thread_id)
def test_thread_closed_status_changed(self): with self._get_swap_context(): # Create test objects. exp_id = 'eid' self.save_new_valid_exploration(exp_id, self.owner_id) # Trigger thread creation events. self.process_and_flush_pending_mapreduce_tasks() feedback_services.create_thread( 'exploration', exp_id, None, 'a subject', 'some text') self._flush_tasks_and_check_analytics( exp_id, { 'num_open_threads': 1, 'num_total_threads': 1, }) # Trigger close event. threadlist = feedback_services.get_all_threads( 'exploration', exp_id, False) thread_id = threadlist[0].id feedback_services.create_message( thread_id, self.owner_id, feedback_models.STATUS_CHOICES_FIXED, None, 'some text') self._flush_tasks_and_check_analytics( exp_id, { 'num_open_threads': 0, 'num_total_threads': 1, }) # Trigger thread status change event. threadlist = feedback_services.get_all_threads( 'exploration', exp_id, False) thread_id = threadlist[0].id feedback_services.create_message( thread_id, self.owner_id, feedback_models.STATUS_CHOICES_IGNORED, None, 'some text') self._flush_tasks_and_check_analytics( exp_id, { 'num_open_threads': 0, 'num_total_threads': 1, })
def test_get_num_threads_after_creating_feedback_analytics(self): self.login(self.OWNER_EMAIL) self.get_json('%s/%s' % (feconf.FEEDBACK_STATS_URL_PREFIX, self.exp_id), expected_status_int=404) self.save_new_valid_exploration(self.exp_id, self.owner_id, title='Exploration title', category='Architecture', language_code='en') response = self.get_json( '%s/%s' % (feconf.FEEDBACK_STATS_URL_PREFIX, self.exp_id)) self.assertEqual(response['num_total_threads'], 0) self.assertEqual(response['num_open_threads'], 0) feedback_services.create_thread('exploration', self.exp_id, self.owner_id, 'subject', 'text') feedback_analytics_aggregator_swap = self.swap( feedback_jobs_continuous, 'FeedbackAnalyticsAggregator', MockFeedbackAnalyticsAggregator) with feedback_analytics_aggregator_swap: (feedback_jobs_continuous.FeedbackAnalyticsAggregator. start_computation()) self.assertEqual( self.count_jobs_in_mapreduce_taskqueue( taskqueue_services.QUEUE_NAME_CONTINUOUS_JOBS), 1) self.process_and_flush_pending_mapreduce_tasks() self.process_and_flush_pending_tasks() response = self.get_json( '%s/%s' % (feconf.FEEDBACK_STATS_URL_PREFIX, self.exp_id)) self.assertEqual(response['num_total_threads'], 2) self.assertEqual(response['num_open_threads'], 2) self.logout()
def test_email_sent_when_status_changed(self): """Tests Feedback Thread Status Change Email Handler.""" with self.can_send_feedback_email_ctx, self.can_send_emails_ctx: # Create thread. feedback_services.create_thread( feconf.ENTITY_TYPE_EXPLORATION, self.exploration.id, self.user_id_a, 'a subject', 'some text') threadlist = feedback_services.get_all_threads( feconf.ENTITY_TYPE_EXPLORATION, self.exploration.id, False) thread_id = threadlist[0].id # User B creates message with status change. feedback_services.create_message( thread_id, self.user_id_b, feedback_models.STATUS_CHOICES_FIXED, None, 'user b message') # Ensure user A has no messages sent to him yet. messages = self.mail_stub.get_sent_messages( to=self.USER_A_EMAIL) self.assertEqual(len(messages), 0) # Invoke feedback status change email handler. self.process_and_flush_pending_tasks() # Check that user A has 2 emails sent to him. # 1 instant feedback message email and 1 status change. messages = self.mail_stub.get_sent_messages( to=self.USER_A_EMAIL) self.assertEqual(len(messages), 2) # Check that user A has right email sent to him. expected_message = ( 'Hi userA,\n\nNew update to thread "a subject" on Title:\n-' ' userB: changed status from open to fixed\n(You received' ' this message because you are a participant in this thread' '.)\n\nBest wishes,\nThe Oppia team\n\nYou can change your' ' email preferences via the Preferences page.') status_change_email = messages[0] self.assertEqual( status_change_email.body.decode(), expected_message)
def test_that_emails_are_not_sent_if_already_seen(self): with self.can_send_emails_ctx, self.can_send_feedback_email_ctx: feedback_services.create_thread( self.exploration.id, 'a_state_name', self.new_user_id, 'a subject', 'some text') threadlist = feedback_services.get_all_threads( self.exploration.id, False) thread_id = threadlist[0].get_thread_id() self.login(self.EDITOR_EMAIL) csrf_token = self.get_csrf_token_from_response( self.testapp.get('/create/%s' % self.exploration.id)) self.post_json('%s' % feconf.FEEDBACK_THREAD_VIEW_EVENT_URL, { 'exploration_id': self.exploration.id, 'thread_id': thread_id}, csrf_token) self.process_and_flush_pending_tasks() messages = self.mail_stub.get_sent_messages(to=self.EDITOR_EMAIL) self.assertEqual(len(messages), 0)
def test_feedback_thread_subscription(self): user_b_subscriptions_model = user_models.UserSubscriptionsModel.get( self.user_b_id, strict=False) user_c_subscriptions_model = user_models.UserSubscriptionsModel.get( self.user_c_id, strict=False) self.assertEqual(user_b_subscriptions_model, None) self.assertEqual(user_c_subscriptions_model, None) with self.swap( subscription_services, 'subscribe_to_thread', self._null_fn ), self.swap( subscription_services, 'subscribe_to_exploration', self._null_fn ): # User B starts a feedback thread. feedback_services.create_thread( self.EXP_ID_1, None, self.user_b_id, 'subject', 'text') # User C adds to that thread. thread_id = feedback_services.get_all_threads( self.EXP_ID_1, False)[0].get_thread_id() feedback_services.create_message( self.EXP_ID_1, thread_id, self.user_c_id, None, None, 'more text') self._run_one_off_job() # Both users are subscribed to the feedback thread. user_b_subscriptions_model = user_models.UserSubscriptionsModel.get( self.user_b_id) user_c_subscriptions_model = user_models.UserSubscriptionsModel.get( self.user_c_id) self.assertEqual(user_b_subscriptions_model.activity_ids, []) self.assertEqual(user_c_subscriptions_model.activity_ids, []) full_thread_id = ( feedback_models.FeedbackThreadModel.generate_full_thread_id( self.EXP_ID_1, thread_id)) self.assertEqual( user_b_subscriptions_model.feedback_thread_ids, [full_thread_id]) self.assertEqual( user_c_subscriptions_model.feedback_thread_ids, [full_thread_id])