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)
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
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
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
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
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
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
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
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
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