Example #1
0
    def post(self, reply_to_id):
        incoming_mail = mail.InboundEmailMessage(self.request.body)
        if feconf.ENABLE_GENERALIZED_FEEDBACK_THREADS:
            model = (email_models.GeneralFeedbackEmailReplyToIdModel.
                     get_by_reply_to_id(reply_to_id))
        else:
            model = email_models.FeedbackEmailReplyToIdModel.get_by_reply_to_id(
                reply_to_id)

        if model is None:
            raise self.PageNotFoundException

        user_id = model.user_id
        thread_id = model.thread_id

        # Get text message from email.
        msg = list(
            incoming_mail.bodies(content_type='text/plain'))[0][1].decode()

        # Add new feedback message to thread.
        feedback_services.create_message(thread_id,
                                         user_id,
                                         None,
                                         None,
                                         msg,
                                         received_via_email=True)
        self.render_json({})
Example #2
0
    def test_instant_feedback_reply_email_when_feedback_is_none(self):
        """Tests Instant feedback message handler."""
        with self.can_send_feedback_email_ctx, self.can_send_emails_ctx:
            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
            # Create reply message.
            feedback_services.create_message(
                thread_id, self.user_id_b, None, None, 'user b message')
            # Get all messages in the thread.
            messages = feedback_services.get_messages(thread_id)
            # Make sure there are only 2 messages in thread.
            self.assertEqual(len(messages), 2)

            # Ensure that user A has no emails sent yet.
            messages = self._get_sent_email_messages(
                self.USER_A_EMAIL)
            self.assertEqual(len(messages), 0)

            email_reply_to_id_model = (
                email_models.GeneralFeedbackEmailReplyToIdModel.get(
                    self.user_id_a, thread_id, strict=False))
            email_reply_to_id_model.delete()

            raises_feedback_thread_does_not_exist = self.assertRaisesRegexp(
                Exception,
                'Feedback thread for current user and thread_id does not exist'
            )
            with raises_feedback_thread_does_not_exist:
                # Invoke InstantFeedbackMessageEmail which sends
                # instantFeedbackMessage.
                self.process_and_flush_pending_tasks()
Example #3
0
    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')
Example #4
0
 def test_create_message_fails_if_invalid_thread_id(self):
     exp_id = '0'
     with self.assertRaises(
         feedback_models.FeedbackMessageModel.EntityNotFoundError
         ):
         feedback_services.create_message(
             exp_id, 'invalid_thread_id', 'user_id', None, None, 'Hello')
Example #5
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_threadlist(
                self.EXP_ID_1)[0]['thread_id']
            feedback_services.create_message(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, [])
        self.assertEqual(user_b_subscriptions_model.feedback_thread_ids,
                         [thread_id])
        self.assertEqual(user_c_subscriptions_model.feedback_thread_ids,
                         [thread_id])
Example #6
0
    def post(self, reply_to_id):
        incoming_mail = mail.InboundEmailMessage(self.request.body)
        model = email_models.FeedbackEmailReplyToIdModel.get_by_reply_to_id(
            reply_to_id)

        if model is None:
            raise self.PageNotFoundException

        user_id = model.user_id
        exploration_id = model.exploration_id
        thread_id = model.thread_id

        # Get text message from email.
        msg = list(
            incoming_mail.bodies(content_type='text/plain'))[0][1].decode()

        # Add new feedback message to thread.
        feedback_services.create_message(exploration_id,
                                         thread_id,
                                         user_id,
                                         None,
                                         None,
                                         msg,
                                         received_via_email=True)
        self.render_json({})
    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,
                    })
Example #8
0
    def test_that_reply_to_id_is_created(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', 'A message')
            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_b, None, None, 'user b message')
            # Check that reply_to id is created for user A.
            queried_object = (
                email_services
                .get_feedback_thread_reply_info_by_user_and_thread_ids(
                    self.user_id_a, thread_id))
            self.assertEqual(queried_object.user_id, self.user_id_a)
            self.assertEqual(queried_object.thread_id, thread_id)

            feedback_services.create_message(
                thread_id, self.user_id_a, None, None, 'user a message')
            # Check that reply_to id is created for user B.
            queried_object = (
                email_services
                .get_feedback_thread_reply_info_by_user_and_thread_ids(
                    self.user_id_b, thread_id))
            self.assertEqual(queried_object.user_id, self.user_id_b)
            self.assertEqual(queried_object.thread_id, thread_id)
def accept_suggestion(suggestion, reviewer_id, commit_message, review_message):
    """Accepts the given suggestion after validating it.

    Args:
        suggestion: Suggestion. The suggestion to be accepted.
        reviewer_id: str. The ID of the reviewer accepting the suggestion.
        commit_message: str. The commit message.
        review_message: str. The message provided by the reviewer while
            accepting the suggestion.

    Raises:
        Exception: The suggestion is already handled.
        Exception: The suggestion is not valid.
        Exception: The commit message is empty.
    """
    if suggestion.is_handled:
        raise Exception('The suggestion has already been accepted/rejected.')
    if not commit_message or not commit_message.strip():
        raise Exception('Commit message cannot be empty.')

    suggestion.pre_accept_validate()

    author_name = user_services.get_username(suggestion.author_id)
    commit_message = get_commit_message_for_suggestion(author_name,
                                                       commit_message)
    mark_review_completed(suggestion, suggestion_models.STATUS_ACCEPTED,
                          reviewer_id)
    suggestion.accept(commit_message)

    feedback_services.create_message(
        get_thread_id_from_suggestion_id(suggestion.suggestion_id),
        reviewer_id, feedback_models.STATUS_CHOICES_FIXED, None,
        review_message)
Example #10
0
    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(
            feconf.ENTITY_TYPE_EXPLORATION, 'exp_id',
            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_get_thread_summaries_load_test(self):
        # The speed of fetching the summaries of 100 threads having 5 messages
        # should be less than 1.7 second. In reality, the time taken to fetch
        # all the summaries is less than 0.2s. However since it seems to take
        # longer on Travis, the constant has been set to 1.7s.
        # Create 100 threads.
        for _ in range(100):
            feedback_services.create_thread(
                feconf.ENTITY_TYPE_EXPLORATION, self.EXP_ID_1,
                self.EXPECTED_THREAD_DICT['state_name'],
                self.user_id, self.EXPECTED_THREAD_DICT['subject'],
                'not used here')
        threadlist = feedback_services.get_all_threads(
            feconf.ENTITY_TYPE_EXPLORATION, self.EXP_ID_1, False)

        thread_ids = []
        for thread in threadlist:
            thread_ids.append(thread.id)
            # Create 5 messages in each thread.
            for _ in range(5):
                feedback_services.create_message(
                    thread.id, self.user_id, None, None, 'editor message')

        start = time.time()
        # Fetch the summaries of all the threads.
        feedback_services.get_thread_summaries(self.user_id, thread_ids)
        elapsed_time = time.time() - start
        print "Time for fetching all the thread summaries -", elapsed_time
        self.assertLessEqual(elapsed_time, 1.7)
Example #12
0
    def test_that_email_is_sent_for_changing_status_of_thread(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', '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(
                    taskqueue_services.QUEUE_NAME_EMAILS), 1)
            self.assertEqual(
                self.count_jobs_in_taskqueue(
                    taskqueue_services.QUEUE_NAME_EVENTS), 1)
            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,
                feedback_models.STATUS_CHOICES_FIXED, None, '')
            # There are two jobs in the taskqueue: one for the realtime
            # event associated with changing subject of thread, and one for
            # sending the email.
            self.assertEqual(
                self.count_jobs_in_taskqueue(
                    taskqueue_services.QUEUE_NAME_EMAILS), 1)
            self.assertEqual(
                self.count_jobs_in_taskqueue(
                    taskqueue_services.QUEUE_NAME_EVENTS), 1)
            self.process_and_flush_pending_tasks()
Example #13
0
    def test_that_reply_to_id_is_created(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', 'A message')
            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_b, None, None, 'user b message')
            # Check that reply_to id is created for user A.
            model = email_models.GeneralFeedbackEmailReplyToIdModel.get(
                self.user_id_a, thread_id)
            cross_model = (
                email_models.GeneralFeedbackEmailReplyToIdModel
                .get_by_reply_to_id(model.reply_to_id))
            self.assertEqual(model, cross_model)
            self.assertEqual(cross_model.user_id, self.user_id_a)

            feedback_services.create_message(
                thread_id, self.user_id_a, None, None, 'user a message')
            # Check that reply_to id is created for user B.
            model = email_models.GeneralFeedbackEmailReplyToIdModel.get(
                self.user_id_b, thread_id)
            cross_model = (
                email_models.GeneralFeedbackEmailReplyToIdModel
                .get_by_reply_to_id(model.reply_to_id))
            self.assertEqual(model, cross_model)
            self.assertEqual(cross_model.user_id, self.user_id_b)
Example #14
0
    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')
Example #15
0
    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, 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,
                })
    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,
            })
Example #17
0
    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])
Example #18
0
    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},
            )
Example #19
0
 def test_create_message_fails_if_invalid_thread_id(self):
     exp_id = '0'
     with self.assertRaises(
         feedback_models.FeedbackMessageModel.EntityNotFoundError
         ):
         feedback_services.create_message(
             exp_id, 'invalid_thread_id', 'user_id', None, None, 'Hello')
Example #20
0
    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)
Example #21
0
def reject_suggestion(suggestion, reviewer_id, review_message):
    """Rejects the suggestion.

     Args:
        suggestion: Suggestion. The suggestion to be rejected.
        reviewer_id: str. The ID of the reviewer rejecting the suggestion.
        review_message: str. The message provided by the reviewer while
            rejecting the suggestion.

    Raises:
        Exception: The suggestion is already handled.
    """
    if suggestion.is_handled:
        raise Exception('The suggestion has already been accepted/rejected.')
    if not review_message:
        raise Exception('Review message cannot be empty.')
    mark_review_completed(
        suggestion, suggestion_models.STATUS_REJECTED, reviewer_id)


    thread_id = suggestion.suggestion_id
    if not constants.ENABLE_GENERALIZED_FEEDBACK_THREADS:
        thread_id = thread_id[thread_id.find('.') + 1:]
    feedback_services.create_message(
        thread_id, reviewer_id, feedback_models.STATUS_CHOICES_IGNORED,
        None, review_message)
Example #22
0
def resubmit_rejected_suggestion(suggestion, summary_message, author_id):
    """Resubmit a rejected suggestion.

    Args:
        suggestion: Suggestion. The rejected suggestion.
        summary_message: str. The message provided by the author to
            summarize new suggestion.
        author_id: str. The ID of the author creating the suggestion.

    Raises:
        Exception: The summary message is empty.
        Exception: The suggestion has not been handled yet.
        Exception: The suggestion has already been accepted.
    """
    if not summary_message:
        raise Exception('Summary message cannot be empty.')
    if not suggestion.is_handled:
        raise Exception('The suggestion is not yet handled.')
    if suggestion.status == suggestion_models.STATUS_ACCEPTED:
        raise Exception('The suggestion was accepted. '
                        'Only rejected suggestions can be resubmitted.')

    suggestion.status = suggestion_models.STATUS_IN_REVIEW
    _update_suggestion(suggestion)

    thread_id = suggestion.suggestion_id
    feedback_services.create_message(thread_id, author_id,
                                     feedback_models.STATUS_CHOICES_OPEN, None,
                                     summary_message)
Example #23
0
def resubmit_rejected_suggestion(suggestion_id, summary_message, author_id,
                                 change):
    """Resubmit a rejected suggestion with the given suggestion_id.

    Args:
        suggestion_id: str. The id of the rejected suggestion.
        summary_message: str. The message provided by the author to
            summarize new suggestion.
        author_id: str. The ID of the author creating the suggestion.
        change: ExplorationChange. The new change to apply to the suggestion.

    Raises:
        Exception. The summary message is empty.
        Exception. The suggestion has not been handled yet.
        Exception. The suggestion has already been accepted.
    """
    suggestion = get_suggestion_by_id(suggestion_id)
    if not summary_message:
        raise Exception('Summary message cannot be empty.')
    if not suggestion.is_handled:
        raise Exception('The suggestion with id %s is not yet handled.' %
                        (suggestion_id))
    if suggestion.status == suggestion_models.STATUS_ACCEPTED:
        raise Exception('The suggestion with id %s was accepted. '
                        'Only rejected suggestions can be resubmitted.' %
                        (suggestion_id))

    suggestion.pre_update_validate(change)
    suggestion.change = change
    suggestion.set_suggestion_status_to_in_review()
    _update_suggestion(suggestion)

    feedback_services.create_message(suggestion_id, author_id,
                                     feedback_models.STATUS_CHOICES_OPEN, None,
                                     summary_message)
Example #24
0
def accept_suggestion(suggestion, reviewer_id, commit_message, review_message):
    """Accepts the given suggestion after validating it.

    Args:
        suggestion: Suggestion. The suggestion to be accepted.
        reviewer_id: str. The ID of the reviewer accepting the suggestion.
        commit_message: str. The commit message.
        review_message: str. The message provided by the reviewer while
            accepting the suggestion.

    Raises:
        Exception. The suggestion is already handled.
        Exception. The suggestion is not valid.
        Exception. The commit message is empty.
    """
    if suggestion.is_handled:
        raise Exception(
            'The suggestion with id %s has already been accepted/rejected.' %
            (suggestion.suggestion_id))
    if not commit_message or not commit_message.strip():
        raise Exception('Commit message cannot be empty.')
    suggestion.pre_accept_validate()
    html_string = ''.join(suggestion.get_all_html_content_strings())
    error_list = (
        html_validation_service.
        validate_math_tags_in_html_with_attribute_math_content(html_string))
    if len(error_list) > 0:
        raise Exception(
            'Invalid math tags found in the suggestion with id %s.' %
            (suggestion.suggestion_id))

    author_name = user_services.get_username(suggestion.author_id)
    commit_message = get_commit_message_for_suggestion(author_name,
                                                       commit_message)
    mark_review_completed(suggestion, suggestion_models.STATUS_ACCEPTED,
                          reviewer_id)
    suggestion.accept(commit_message)
    thread_id = suggestion.suggestion_id
    feedback_services.create_message(thread_id, reviewer_id,
                                     feedback_models.STATUS_CHOICES_FIXED,
                                     None, review_message)

    if feconf.ENABLE_RECORDING_OF_SCORES:
        increment_score_for_user(
            suggestion.author_id, suggestion.score_category,
            suggestion_models.INCREMENT_SCORE_OF_AUTHOR_BY)
        if feconf.SEND_SUGGESTION_REVIEW_RELATED_EMAILS:
            scores = get_all_scores_of_user(suggestion.author_id)
            if (suggestion.score_category in scores
                    and scores[suggestion.score_category] >=
                    feconf.MINIMUM_SCORE_REQUIRED_TO_REVIEW):
                if not check_if_email_has_been_sent_to_user(
                        suggestion.author_id, suggestion.score_category):
                    email_manager.send_mail_to_onboard_new_reviewers(
                        suggestion.author_id, suggestion.score_category)
                    mark_email_has_been_sent_to_user(suggestion.author_id,
                                                     suggestion.score_category)
Example #25
0
    def post(self, exploration_id, thread_id):
        text = self.payload.get("text")
        updated_status = self.payload.get("updated_status")
        if not text and not updated_status:
            raise self.InvalidInputException("Text for the message must be specified.")

        feedback_services.create_message(
            thread_id, self.user_id, updated_status, self.payload.get("updated_subject"), text
        )
        self.render_json(self.values)
Example #26
0
    def post(self, exploration_id, thread_id):  # pylint: disable=unused-argument
        text = self.payload.get('text')
        updated_status = self.payload.get('updated_status')
        if not text and not updated_status:
            raise self.InvalidInputException(
                'Text for the message must be specified.')

        feedback_services.create_message(thread_id, self.user_id,
                                         updated_status,
                                         self.payload.get('updated_subject'),
                                         text)
        self.render_json(self.values)
Example #27
0
    def test_export_data_nontrivial(self):
        # Setup test variables.
        test_export_thread_type = 'exploration'
        test_export_thread_id = 'export_thread_1'
        test_export_updated_status = 'open'
        test_export_updated_subject = 'export_subject_1'
        test_export_text = 'Export test text.'
        test_export_received_via_email = False

        self.signup('*****@*****.**', 'exportAuthor1')
        test_export_author_id = (
            self.get_user_id_from_email('*****@*****.**'))

        thread_id = feedback_services.create_thread(
            test_export_thread_type,
            test_export_thread_id,
            test_export_author_id,
            test_export_updated_subject,
            test_export_text
        )

        feedback_services.create_message(
            thread_id,
            test_export_author_id,
            test_export_updated_status,
            test_export_updated_subject,
            test_export_text
        )

        user_data = (
            feedback_models.GeneralFeedbackMessageModel
            .export_data(test_export_author_id))

        test_data = {
            thread_id + '.0': {
                'thread_id': thread_id,
                'message_id': 0,
                'updated_status': test_export_updated_status,
                'updated_subject': test_export_updated_subject,
                'text': test_export_text,
                'received_via_email': test_export_received_via_email
            },
            thread_id + '.1': {
                'thread_id': thread_id,
                'message_id': 1,
                'updated_status': test_export_updated_status,
                'updated_subject': test_export_updated_subject,
                'text': test_export_text,
                'received_via_email': test_export_received_via_email
            }
        }

        self.assertEqual(test_data, user_data)
    def _create_message(self, thread_id, author_id, text):
        """Helper wrapper for feedback_services.create_message which only
        exposes arguments relevant to the cache.

        Args:
            thread_id: str. ID of the thread to which this message should be
                appened to.
            author_id: str|None. ID of the user which created this message, or
                None if the author was anonymous (not logged in).
            text: str. Content of the first message in the thread (allowed to be
                an empty string).
        """
        feedback_services.create_message(thread_id, author_id, None, None, text)
Example #29
0
def accept_suggestion(suggestion, reviewer_id, commit_message, review_message):
    """Accepts the given suggestion after validating it.

    Args:
        suggestion: Suggestion. The suggestion to be accepted.
        reviewer_id: str. The ID of the reviewer accepting the suggestion.
        commit_message: str. The commit message.
        review_message: str. The message provided by the reviewer while
            accepting the suggestion.

    Raises:
        Exception: The suggestion is already handled.
        Exception: The suggestion is not valid.
        Exception: The commit message is empty.
    """
    if suggestion.is_handled:
        raise Exception('The suggestion has already been accepted/rejected.')
    if not commit_message or not commit_message.strip():
        raise Exception('Commit message cannot be empty.')

    suggestion.pre_accept_validate()

    author_name = user_services.get_username(suggestion.author_id)
    commit_message = get_commit_message_for_suggestion(
        author_name, commit_message)
    mark_review_completed(
        suggestion, suggestion_models.STATUS_ACCEPTED, reviewer_id)
    suggestion.accept(commit_message)
    thread_id = suggestion.suggestion_id
    if not constants.ENABLE_GENERALIZED_FEEDBACK_THREADS:
        thread_id = thread_id[thread_id.find('.') + 1:]
    feedback_services.create_message(
        thread_id, reviewer_id, feedback_models.STATUS_CHOICES_FIXED,
        None, review_message)

    if feconf.ENABLE_RECORDING_OF_SCORES:
        increment_score_for_user(
            suggestion.author_id, suggestion.score_category,
            suggestion_models.INCREMENT_SCORE_OF_AUTHOR_BY)
        if feconf.SEND_SUGGESTION_REVIEW_RELATED_EMAILS:
            scores = get_all_scores_of_user(suggestion.author_id)
            if (
                    suggestion.score_category in scores and
                    scores[suggestion.score_category] >=
                    feconf.MINIMUM_SCORE_REQUIRED_TO_REVIEW):
                if check_if_email_has_been_sent_to_user(
                        suggestion.author_id, suggestion.score_category):
                    email_manager.send_mail_to_onboard_new_reviewers(
                        suggestion.author_id, suggestion.score_category)
                    mark_email_has_been_sent_to_user(
                        suggestion.author_id, suggestion.score_category)
Example #30
0
    def post(self, exploration_id, thread_id):  # pylint: disable=unused-argument
        text = self.payload.get('text')
        updated_status = self.payload.get('updated_status')
        if not text and not updated_status:
            raise self.InvalidInputException(
                'Text for the message must be specified.')

        feedback_services.create_message(
            thread_id,
            self.user_id,
            updated_status,
            self.payload.get('updated_subject'),
            text)
        self.render_json(self.values)
Example #31
0
    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)
Example #32
0
    def post(self, thread_id):
        suggestion = suggestion_services.get_suggestion_by_id(thread_id)
        text = self.payload.get('text')
        updated_status = self.payload.get('updated_status')
        if not text and not updated_status:
            raise self.InvalidInputException(
                'Text for the message must be specified.')
        if suggestion and updated_status:
            raise self.InvalidInputException(
                'Suggestion thread status cannot be changed manually.')

        feedback_services.create_message(
            thread_id, self.user_id, updated_status,
            self.payload.get('updated_subject'), text)
        self.render_json(self.values)
Example #33
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)
Example #34
0
    def post(self, thread_id):
        suggestion = suggestion_services.get_suggestion_by_id(thread_id)
        text = self.normalized_payload.get('text')
        updated_status = self.normalized_payload.get('updated_status')
        updated_subject = self.normalized_payload.get('updated_subject')

        if suggestion and updated_status:
            raise self.InvalidInputException(
                'Suggestion thread status cannot be changed manually.')

        messages = feedback_services.get_messages(thread_id)
        new_message = feedback_services.create_message(thread_id, self.user_id,
                                                       updated_status,
                                                       updated_subject, text)

        # Currently we are manually adding new message to the messages list as
        # the feedback_services.get_messages is not returning a correct list of
        # messages after adding new message model to the datastore because of an
        # unknown reason.
        messages.append(new_message)
        message_dict = [message.to_dict() for message in messages]

        self.render_json({
            'messages':
            replace_user_id_with_username_in_dict(
                message_dict, [('author_id', 'author_username')])
        })
Example #35
0
    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_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_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_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])
    def test_add_new_feedback_message(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()

            feedback_services.create_message(
                self.exploration.id, 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(), 2)

            messagelist = feedback_services.get_messages(
                self.exploration.id, thread_id)
            self.assertEqual(len(messagelist), 2)

            expected_feedback_message_dict1 = {
                'exploration_id': self.exploration.id,
                'thread_id': thread_id,
                'message_id': messagelist[0].message_id
            }
            expected_feedback_message_dict2 = {
                'exploration_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)
Example #41
0
    def post(self, reply_to_id):
        incoming_mail = mail.InboundEmailMessage(self.request.body)
        model = email_models.FeedbackEmailReplyToIdModel.get_by_reply_to_id(
            reply_to_id)

        if model is None:
            raise self.PageNotFoundException

        user_id = model.user_id
        exploration_id = model.exploration_id
        thread_id = model.thread_id

        # Get text message from email.
        msg = list(
            incoming_mail.bodies(content_type='text/plain'))[0][1].decode()

        # Add new feedback message to thread.
        feedback_services.create_message(
            exploration_id, thread_id, user_id, None, None, msg,
            received_via_email=True)
        self.render_json({})