Beispiel #1
0
  def testIssueViewWithBlocking(self):
    # Treat issues 3 and 4 as visible to the current user.
    view2 = tracker_views.IssueView(
        self.issue2, self.users_by_id, _MakeConfig(),
        open_related={self.issue1.issue_id: self.issue1,
                      self.issue3.issue_id: self.issue3},
        closed_related={self.issue4.issue_id: self.issue4})
    self.assertEqual(['not too long summary', 'sum 3'],
                     [ref.summary for ref in view2.blocked_on])
    self.assertEqual(['not too long summary', 'sum 4',
                      'Issue 5001 in codesite.'],
                     [ref.summary for ref in view2.blocking])

    # Now, treat issues 3 and 4 as not visible to the current user.
    view2 = tracker_views.IssueView(
        self.issue2, self.users_by_id, _MakeConfig(),
        open_related={self.issue1.issue_id: self.issue1}, closed_related={})
    self.assertEqual(['not too long summary'],
                     [ref.summary for ref in view2.blocked_on])
    self.assertEqual(['not too long summary', 'Issue 5001 in codesite.'],
                     [ref.summary for ref in view2.blocking])

    # Treat nothing as visible to the current user. Can still see dangling ref.
    view2 = tracker_views.IssueView(
        self.issue2, self.users_by_id, _MakeConfig(),
        open_related={}, closed_related={})
    self.assertEqual([], view2.blocked_on)
    self.assertEqual(['Issue 5001 in codesite.'],
                     [ref.summary for ref in view2.blocking])
    def testIsOpen(self):
        config = _MakeConfig()
        view1 = tracker_views.IssueView(self.issue1, self.users_by_id, config)
        self.assertEqual(ezt.boolean(True), view1.is_open)

        self.issue1.status = 'Old'
        view1 = tracker_views.IssueView(self.issue1, self.users_by_id, config)
        self.assertEqual(ezt.boolean(False), view1.is_open)
    def testIssueViewWithBlocking(self):
        all_related = {
            self.issue1.issue_id: self.issue1,
            self.issue2.issue_id: self.issue2,
            self.issue3.issue_id: self.issue3,
            self.issue4.issue_id: self.issue4,
        }
        # Treat issues 3 and 4 as visible to the current user.
        view2 = tracker_views.IssueView(
            self.issue2,
            self.users_by_id,
            _MakeConfig(),
            open_related={
                self.issue1.issue_id: self.issue1,
                self.issue3.issue_id: self.issue3
            },
            closed_related={self.issue4.issue_id: self.issue4},
            all_related=all_related)
        self.assertEqual(['not too long summary', 'sum 3'],
                         [irv.summary for irv in view2.blocked_on])
        self.assertEqual(
            ['not too long summary', 'sum 4', 'Issue 5001 in codesite.'],
            [irv.summary for irv in view2.blocking])
        self.assertTrue(view2.multiple_blocked_on)

        # Now, treat issues 3 and 4 as not visible to the current user.
        view2 = tracker_views.IssueView(
            self.issue2,
            self.users_by_id,
            _MakeConfig(),
            open_related={self.issue1.issue_id: self.issue1},
            closed_related={},
            all_related=all_related)
        self.assertEqual(['not too long summary', None],
                         [irv.summary for irv in view2.blocked_on])
        self.assertEqual(
            ['not too long summary', None, 'Issue 5001 in codesite.'],
            [irv.summary for irv in view2.blocking])
        self.assertFalse(view2.multiple_blocked_on)

        # Treat nothing as visible to the current user. Can still see dangling ref.
        dref_blocked_on = tracker_pb2.DanglingIssueRef()
        dref_blocked_on.project = 'codesite'
        dref_blocked_on.issue_id = 4999
        self.issue2.dangling_blocked_on_refs.append(dref_blocked_on)
        view2 = tracker_views.IssueView(
            self.issue2,
            self.users_by_id,
            _MakeConfig(),
            open_related={9999: 'some irrelevant issue'},
            closed_related={},
            all_related=all_related)
        self.assertEqual([None, None, 'Issue 4999 in codesite.'],
                         [irv.summary for irv in view2.blocked_on])
        self.assertEqual([None, None, 'Issue 5001 in codesite.'],
                         [irv.summary for irv in view2.blocking])
        self.assertFalse(view2.multiple_blocked_on)
Beispiel #4
0
    def _MakeEmailTasks(self, cnxn, issue, project, config, comment, hostport,
                        users_by_id, pings):
        """Return a list of dicts for tasks to notify people."""
        detail_url = framework_helpers.IssueCommentURL(
            hostport, project, issue.local_id, seq_num=comment.sequence)
        fields = sorted((field_def for (field_def, _date_value) in pings),
                        key=lambda fd: fd.field_name)
        email_data = {
            'issue': tracker_views.IssueView(issue, users_by_id, config),
            'summary': issue.summary,
            'ping_comment_content': comment.content,
            'detail_url': detail_url,
            'fields': fields,
        }

        # Generate two versions of email body: members version has all
        # full email addresses exposed.
        body_for_non_members = self.email_template.GetResponse(email_data)
        framework_views.RevealAllEmails(users_by_id)
        body_for_members = self.email_template.GetResponse(email_data)
        logging.info('body for non-members is:\n%r' % body_for_non_members)
        logging.info('body for members is:\n%r' % body_for_members)

        contributor_could_view = permissions.CanViewIssue(
            set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project,
            issue)
        # Note: We never notify the reporter of any issue just because they
        # reported it, only if they star it.
        # TODO(jrobbins): add a user preference for notifying starrers.
        starrer_ids = []

        # TODO(jrobbins): consider IsNoisy() when we support notifying starrers.
        group_reason_list = notify_reasons.ComputeGroupReasonList(
            cnxn,
            self.services,
            project,
            issue,
            config,
            users_by_id, [],
            contributor_could_view,
            starrer_ids=starrer_ids,
            commenter_in_project=True)

        commenter_view = users_by_id[comment.user_id]
        email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
            group_reason_list,
            issue,
            body_for_non_members,
            body_for_members,
            project,
            hostport,
            commenter_view,
            detail_url,
            seq_num=comment.sequence,
            subject_prefix='Follow up on issue ',
            compact_subject_prefix='Follow up ')

        return email_tasks
Beispiel #5
0
  def _ProcessUpstreamIssue(
      self, cnxn, upstream_issue, upstream_project, upstream_config,
      issue, omit_ids, hostport, commenter_view):
    """Compute notifications for one upstream issue that is now blocking."""
    upstream_detail_url = framework_helpers.FormatAbsoluteURLForDomain(
        hostport, upstream_issue.project_name, urls.ISSUE_DETAIL,
        id=upstream_issue.local_id)
    logging.info('upstream_detail_url = %r', upstream_detail_url)
    detail_url = framework_helpers.FormatAbsoluteURLForDomain(
        hostport, issue.project_name, urls.ISSUE_DETAIL,
        id=issue.local_id)

    # Only issues that any contributor could view are sent to mailing lists.
    contributor_could_view = permissions.CanViewIssue(
        set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET,
        upstream_project, upstream_issue)

    # Now construct the e-mail to send

    # Note: we purposely do not notify users who starred an issue
    # about changes in blocking.
    users_by_id = framework_views.MakeAllUserViews(
        cnxn, self.services.user,
        tracker_bizobj.UsersInvolvedInIssues([upstream_issue]), omit_ids)

    is_blocking = upstream_issue.issue_id in issue.blocked_on_iids

    email_data = {
        'issue': tracker_views.IssueView(
            upstream_issue, users_by_id, upstream_config),
        'summary': upstream_issue.summary,
        'detail_url': upstream_detail_url,
        'is_blocking': ezt.boolean(is_blocking),
        'downstream_issue_ref': tracker_bizobj.FormatIssueRef(
            (None, issue.local_id)),
        'downstream_issue_url': detail_url,
        }

    # TODO(jrobbins): Generate two versions of email body: members
    # vesion has other member full email addresses exposed.  But, don't
    # expose too many as we iterate through upstream projects.
    body_link_only = self.link_only_email_template.GetResponse(
        {'detail_url': upstream_detail_url, 'was_created': ezt.boolean(False)})
    body = self.email_template.GetResponse(email_data)

    omit_addrs = {users_by_id[omit_id].email for omit_id in omit_ids}

    # Get the transitive set of owners and Cc'd users, and their UserView's.
    # Give each user a bullet-list of all the reasons that apply for that user.
    # Starrers are not notified of blocking changes to reduce noise.
    group_reason_list = notify_reasons.ComputeGroupReasonList(
        cnxn, self.services, upstream_project, upstream_issue,
        upstream_config, users_by_id, omit_addrs, contributor_could_view)
    one_issue_email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
        group_reason_list, upstream_issue, body_link_only, body, body,
        upstream_project, hostport, commenter_view, detail_url)

    return one_issue_email_tasks
Beispiel #6
0
 def testIssueViewWithRestrictions(self):
   view = tracker_views.IssueView(
       self.restricted, self.users_by_id, _MakeConfig())
   self.assertTrue(view.restrictions.has_restrictions)
   self.assertEqual('Commit and MyCustomPerm', view.restrictions.view)
   self.assertEqual('Commit', view.restrictions.add_comment)
   self.assertEqual('Commit', view.restrictions.edit)
   self.assertEqual(['Restrict-Action-NeededPerm'], view.restrictions.other)
   self.assertEqual('Restrict-View-Commit', view.labels[0].name)
   self.assertTrue(view.labels[0].is_restrict)
 def CheckSimpleIssueView(self, config):
     view1 = tracker_views.IssueView(self.issue1, self.users_by_id, config)
     self.assertEqual('not too long summary', view1.summary)
     self.assertEqual('New', view1.status.name)
     self.assertEqual('user 2002', view1.owner)
     self.assertEqual('A', view1.labels[0].name)
     self.assertEqual('B', view1.labels[1].name)
     self.assertEqual('C', view1.derived_labels[0].name)
     self.assertEqual('D', view1.derived_labels[1].name)
     self.assertEqual([], view1.blocked_on)
     self.assertEqual([], view1.blocking)
     detail_url = '/p/%s%s?id=%d' % (
         self.issue1.project_name, urls.ISSUE_DETAIL, self.issue1.local_id)
     self.assertEqual(detail_url, view1.detail_relative_url)
     return view1
Beispiel #8
0
  def _FormatBulkIssues(
      self, issues, users_by_id, commenter_view, hostport, comment_text,
      amendments, config, addr_perm):
    """Format a subject and body for a bulk issue edit."""
    project_name = issues[0].project_name

    any_link_only = False
    issue_views = []
    for issue in issues:
      # TODO(jrobbins): choose config from dict of prefetched configs.
      issue_view = tracker_views.IssueView(issue, users_by_id, config)
      issue_view.link_only = ezt.boolean(False)
      if addr_perm and notify_helpers.ShouldUseLinkOnly(addr_perm, issue):
        issue_view.link_only = ezt.boolean(True)
        any_link_only = True
      issue_views.append(issue_view)

    email_data = {
        'any_link_only': ezt.boolean(any_link_only),
        'hostport': hostport,
        'num_issues': len(issues),
        'issues': issue_views,
        'comment_text': comment_text,
        'commenter': commenter_view,
        'amendments': amendments,
    }

    if len(issues) == 1:
      # TODO(jrobbins): use compact email subject lines based on user pref.
      if addr_perm and notify_helpers.ShouldUseLinkOnly(addr_perm, issues[0]):
        subject = 'issue %s in %s' % (issues[0].local_id, project_name)
      else:
        subject = 'issue %s in %s: %s' % (
            issues[0].local_id, project_name, issues[0].summary)
      # TODO(jrobbins): Look up the sequence number instead and treat this
      # more like an individual change for email threading.  For now, just
      # add "Re:" because bulk edits are always replies.
      subject = 'Re: ' + subject
    else:
      subject = '%d issues changed in %s' % (len(issues), project_name)

    body = self.email_template.GetResponse(email_data)

    return subject, body
Beispiel #9
0
    def _FormatBulkIssues(self,
                          issues,
                          users_by_id,
                          commenter_view,
                          hostport,
                          comment_text,
                          amendments,
                          config,
                          body_type='email'):
        """Format a subject and body for a bulk issue edit."""
        assert body_type in ('email', 'feed')
        project_name = issues[0].project_name

        issue_views = []
        for issue in issues:
            # TODO(jrobbins): choose config from dict of prefetched configs.
            issue_views.append(
                tracker_views.IssueView(issue, users_by_id, config))

        email_data = {
            'hostport': hostport,
            'num_issues': len(issues),
            'issues': issue_views,
            'comment_text': comment_text,
            'commenter': commenter_view,
            'amendments': amendments,
            'body_type': body_type,
        }

        if len(issues) == 1:
            # TODO(jrobbins): use compact email subject lines based on user pref.
            subject = 'issue %s in %s: %s' % (issues[0].local_id, project_name,
                                              issues[0].summary)
            # TODO(jrobbins): Look up the sequence number instead and treat this
            # more like an individual change for email threading.  For now, just
            # add "Re:" because bulk edits are always replies.
            subject = 'Re: ' + subject
        else:
            subject = '%d issues changed in %s' % (len(issues), project_name)

        body = self.email_template.GetResponse(email_data)

        return subject, body
Beispiel #10
0
    def __init__(self,
                 pb,
                 mr,
                 prefetched_issues,
                 users_by_id,
                 prefetched_projects,
                 prefetched_configs,
                 autolink=None,
                 all_ref_artifacts=None,
                 ending=None,
                 highlight=None):
        """Constructs an ActivityView out of an Activity protocol buffer.

    Args:
      pb: an IssueComment or Activity protocol buffer.
      mr: HTTP request info, used by the artifact autolink.
      prefetched_issues: dictionary of the issues for the comments being shown.
      users_by_id: dict {user_id: UserView} for all relevant users.
      prefetched_projects: dict {project_id: project} including all the projects
          that we might need.
      prefetched_configs: dict {project_id: config} for those projects.
      autolink: Autolink instance.
      all_ref_artifacts: list of all artifacts in the activity stream.
      ending: ending type for activity titles, 'in_project' or 'by_user'
      highlight: what to highlight in the middle column on user updates pages
          i.e. 'project', 'user', or None
    """
        template_helpers.PBProxy.__init__(self, pb)

        activity_type = 'ProjectIssueUpdate'  # TODO(jrobbins): more types

        self.comment = None
        self.issue = None
        self.field_changed = None
        self.multiple_fields_changed = ezt.boolean(False)
        self.project = None
        self.user = None
        self.timestamp = time.time(
        )  # Bogus value makes bad ones highly visible.

        if isinstance(pb, tracker_pb2.IssueComment):
            self.timestamp = pb.timestamp
            issue = prefetched_issues[pb.issue_id]
            if self.timestamp == issue.opened_timestamp:
                issue_change_id = None  # This comment is the description.
            else:
                issue_change_id = pb.timestamp  # instead of seq num.

            self.comment = tracker_views.IssueCommentView(
                mr.project_name, pb, users_by_id, autolink, all_ref_artifacts,
                mr, issue)

            # TODO(jrobbins): pass effective_ids of the commenter so that he/she
            # can be identified as a project member or not.
            config = prefetched_configs[issue.project_id]
            self.issue = tracker_views.IssueView(issue, users_by_id, config)
            self.user = self.comment.creator
            project = prefetched_projects[issue.project_id]
            self.project_name = project.project_name
            self.project = project_views.ProjectView(project)

        else:
            logging.warn('unknown activity object %r', pb)

        nested_page_data = {
            'activity_type': activity_type,
            'issue_change_id': issue_change_id,
            'comment': self.comment,
            'issue': self.issue,
            'project': self.project,
            'user': self.user,
            'timestamp': self.timestamp,
            'ending_type': ending,
        }

        self.escaped_title = self._TITLE_TEMPLATE.GetResponse(
            nested_page_data).strip()
        self.escaped_body = self._BODY_TEMPLATE.GetResponse(
            nested_page_data).strip()

        if autolink is not None and all_ref_artifacts is not None:
            # TODO(jrobbins): actually parse the comment text.  Actually render runs.
            runs = autolink.MarkupAutolinks(
                mr, [template_helpers.TextRun(self.escaped_body)],
                all_ref_artifacts)
            self.escaped_body = ''.join(run.content for run in runs)

        self.date_bucket, self.date_relative = timestr.GetHumanScaleDate(
            self.timestamp)
        time_tuple = time.localtime(self.timestamp)
        self.date_tooltip = time.asctime(time_tuple)

        # We always highlight the user for starring activities
        if activity_type.startswith('UserStar'):
            self.highlight = 'user'
        else:
            self.highlight = highlight
Beispiel #11
0
  def _MakeIssueAndCommentViews(
      self, mr, issue, users_by_id, descriptions, comments, config,
      issue_reporters=None, comment_reporters=None):
    """Create view objects that help display parts of an issue.

    Args:
      mr: commonly used info parsed from the request.
      issue: issue PB for the currently viewed issue.
      users_by_id: dictionary of {user_id: UserView,...}.
      descriptions: list of IssueComment PBs for the issue report history.
      comments: list of IssueComment PBs on the current issue.
      issue_reporters: list of user IDs who have flagged the issue as spam.
      comment_reporters: map of comment ID to list of flagging user IDs.
      config: ProjectIssueConfig for the project that contains this issue.

    Returns:
      (issue_view, description_views, comment_views). One IssueView for
      the whole issue, a list of IssueCommentViews for the issue descriptions,
      and then a list of IssueCommentViews for each additional comment.
    """
    with mr.profiler.Phase('getting related issues'):
      open_related, closed_related = (
          tracker_helpers.GetAllowedOpenAndClosedRelatedIssues(
              self.services, mr, issue))
      all_related_iids = list(issue.blocked_on_iids) + list(issue.blocking_iids)
      if issue.merged_into:
        all_related_iids.append(issue.merged_into)
      all_related = self.services.issue.GetIssues(mr.cnxn, all_related_iids)

    with mr.profiler.Phase('making issue view'):
      issue_view = tracker_views.IssueView(
          issue, users_by_id, config,
          open_related=open_related, closed_related=closed_related,
          all_related={rel.issue_id: rel for rel in all_related})

    with mr.profiler.Phase('autolinker object lookup'):
      all_ref_artifacts = None
      if self.services.autolink:
        all_ref_artifacts = self.services.autolink.GetAllReferencedArtifacts(
            mr, [c.content for c in descriptions + comments
                 if not c.deleted_by])

    with mr.profiler.Phase('making comment views'):
      reporter_auth = authdata.AuthData.FromUserID(
          mr.cnxn, descriptions[0].user_id, self.services)
      desc_views = [
          tracker_views.IssueCommentView(
              mr.project_name, d, users_by_id,
              self.services.autolink, all_ref_artifacts, mr,
              issue, effective_ids=reporter_auth.effective_ids)
          for d in descriptions]
      # TODO(jrobbins): get effective_ids of each comment author, but
      # that is too slow right now.
      comment_views = [
          tracker_views.IssueCommentView(
              mr.project_name, c, users_by_id, self.services.autolink,
              all_ref_artifacts, mr, issue)
          for c in comments]

    issue_view.flagged_spam = mr.auth.user_id in issue_reporters
    if comment_reporters is not None:
      for c in comment_views:
        c.flagged_spam = mr.auth.user_id in comment_reporters.get(c.id, [])

    return issue_view, desc_views, comment_views
Beispiel #12
0
  def _MakeEmailTasks(
      self, cnxn, project, issue, config, old_owner_id,
      users_by_id, all_comments, comment, starrer_ids,
      contributor_could_view, hostport, omit_ids, perms):
    """Formulate emails to be sent."""
    detail_url = framework_helpers.IssueCommentURL(
        hostport, project, issue.local_id, seq_num=comment.sequence)

    # TODO(jrobbins): avoid the need to make a MonorailRequest object.
    mr = monorailrequest.MonorailRequest(self.services)
    mr.project_name = project.project_name
    mr.project = project
    mr.perms = perms

    # We do not autolink in the emails, so just use an empty
    # registry of autolink rules.
    # TODO(jrobbins): offer users an HTML email option w/ autolinks.
    autolinker = autolink.Autolink()
    was_created = ezt.boolean(comment.sequence == 0)

    email_data = {
        # Pass open_related and closed_related into this method and to
        # the issue view so that we can show it on new issue email.
        'issue': tracker_views.IssueView(issue, users_by_id, config),
        'summary': issue.summary,
        'comment': tracker_views.IssueCommentView(
            project.project_name, comment, users_by_id,
            autolinker, {}, mr, issue),
        'comment_text': comment.content,
        'detail_url': detail_url,
        'was_created': was_created,
        }

    # Generate three versions of email body: link-only is just the link,
    # non-members see some obscured email addresses, and members version has
    # all full email addresses exposed.
    body_link_only = self.link_only_email_template.GetResponse(
      {'detail_url': detail_url, 'was_created': was_created})
    body_for_non_members = self.email_template.GetResponse(email_data)
    framework_views.RevealAllEmails(users_by_id)
    email_data['comment'] = tracker_views.IssueCommentView(
        project.project_name, comment, users_by_id,
        autolinker, {}, mr, issue)
    body_for_members = self.email_template.GetResponse(email_data)

    logging.info('link-only body is:\n%r' % body_link_only)
    logging.info('body for non-members is:\n%r' % body_for_non_members)
    logging.info('body for members is:\n%r' % body_for_members)

    commenter_email = users_by_id[comment.user_id].email
    omit_addrs = set([commenter_email] +
                     [users_by_id[omit_id].email for omit_id in omit_ids])

    auth = authdata.AuthData.FromUserID(
        cnxn, comment.user_id, self.services)
    commenter_in_project = framework_bizobj.UserIsInProject(
        project, auth.effective_ids)
    noisy = tracker_helpers.IsNoisy(len(all_comments) - 1, len(starrer_ids))

    # Give each user a bullet-list of all the reasons that apply for that user.
    group_reason_list = notify_reasons.ComputeGroupReasonList(
        cnxn, self.services, project, issue, config, users_by_id,
        omit_addrs, contributor_could_view, noisy=noisy,
        starrer_ids=starrer_ids, old_owner_id=old_owner_id,
        commenter_in_project=commenter_in_project)

    commenter_view = users_by_id[comment.user_id]
    detail_url = framework_helpers.FormatAbsoluteURLForDomain(
        hostport, issue.project_name, urls.ISSUE_DETAIL,
        id=issue.local_id)
    email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
        group_reason_list, issue, body_link_only, body_for_non_members,
        body_for_members, project, hostport, commenter_view, detail_url,
        seq_num=comment.sequence)

    return email_tasks
Beispiel #13
0
    def _MakeEmailTasks(self, cnxn, issue, project, config, comment,
                        starrer_ids, hostport, users_by_id, pings):
        """Return a list of dicts for tasks to notify people."""
        detail_url = framework_helpers.IssueCommentURL(
            hostport, project, issue.local_id, seq_num=comment.sequence)
        fields = sorted((field_def for (field_def, _date_value) in pings),
                        key=lambda fd: fd.field_name)
        email_data = {
            'issue': tracker_views.IssueView(issue, users_by_id, config),
            'summary': issue.summary,
            'ping_comment_content': comment.content,
            'detail_url': detail_url,
            'fields': fields,
        }

        # Generate three versions of email body with progressively more info.
        body_link_only = self.link_only_email_template.GetResponse({
            'detail_url':
            detail_url,
            'was_created':
            ezt.boolean(False)
        })
        body_for_non_members = self.email_template.GetResponse(email_data)
        framework_views.RevealAllEmails(users_by_id)
        body_for_members = self.email_template.GetResponse(email_data)
        logging.info('body for non-members is:\n%r' % body_for_non_members)
        logging.info('body for members is:\n%r' % body_for_members)

        contributor_could_view = permissions.CanViewIssue(
            set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project,
            issue)

        group_reason_list = notify_reasons.ComputeGroupReasonList(
            cnxn,
            self.services,
            project,
            issue,
            config,
            users_by_id, [],
            contributor_could_view,
            starrer_ids=starrer_ids,
            commenter_in_project=True,
            include_subscribers=False,
            include_notify_all=False,
            starrer_pref_check_function=lambda u: u.notify_starred_ping)

        commenter_view = users_by_id[comment.user_id]
        email_tasks = notify_helpers.MakeBulletedEmailWorkItems(
            group_reason_list,
            issue,
            body_link_only,
            body_for_non_members,
            body_for_members,
            project,
            hostport,
            commenter_view,
            detail_url,
            seq_num=comment.sequence,
            subject_prefix='Follow up on issue ',
            compact_subject_prefix='Follow up ')

        return email_tasks