def ConvertIssueApprovalsTemplate(self, mc, request): """Update an issue's existing approvals structure to match the one of the given template.""" if not request.issue_ref.local_id or not request.issue_ref.project_name: raise exceptions.InputException('Param `issue_ref.local_id` empty') if not request.template_name: raise exceptions.InputException('Param `template_name` empty') project, issue, config = self._GetProjectIssueAndConfig( mc, request.issue_ref, use_cache=False) with work_env.WorkEnv(mc, self.services) as we: we.ConvertIssueApprovalsTemplate(config, issue, request.template_name, request.comment_content, send_email=request.send_email) related_refs = we.GetRelatedIssueRefs([issue]) with mc.profiler.Phase('making user views'): users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues( [issue]) users_by_id = framework_views.MakeAllUserViews( mc.cnxn, self.services.user, users_involved_in_issue) framework_views.RevealAllEmailsToMembers(mc.auth, project, users_by_id) with mc.profiler.Phase('converting to response objects'): response = issues_pb2.ConvertIssueApprovalsTemplateResponse() response.issue.CopyFrom( converters.ConvertIssue(issue, users_by_id, related_refs, config)) return response
def HandleRequest(self, mr): """Process the task to process an issue date action. Args: mr: common information parsed from the HTTP request. Returns: Results dictionary in JSON format which is useful just for debugging. The main goal is the side-effect of sending emails. """ issue_id = mr.GetPositiveIntParam('issue_id') hostport = framework_helpers.GetHostPort() issue = self.services.issue.GetIssue(mr.cnxn, issue_id) project = self.services.project.GetProject(mr.cnxn, issue.project_id) config = self.services.config.GetProjectConfig(mr.cnxn, issue.project_id) pings = self._CalculateIssuePings(issue, config) if not pings: logging.warning('Issue %r has no dates to ping afterall?', issue_id) return comment = self._CreatePingComment(mr.cnxn, issue, pings, hostport) users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, tracker_bizobj.UsersInvolvedInIssues([issue]), [comment.user_id]) logging.info('users_by_id is %r', users_by_id) tasks = self._MakeEmailTasks(mr.cnxn, issue, project, config, comment, hostport, users_by_id, pings) notified = notify_helpers.AddAllEmailTasks(tasks) return { 'notified': notified, }
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 testUsersInvolvedInIssues_Normal(self): issue1 = tracker_pb2.Issue( reporter_id=111L, owner_id=222L, cc_ids=[222L, 333L]) issue2 = tracker_pb2.Issue( reporter_id=333L, owner_id=444L, derived_cc_ids=[222L, 444L]) issue2.field_values = [tracker_pb2.FieldValue(user_id=555L)] self.assertEqual( set([0L, 111L, 222L, 333L, 444L, 555L]), tracker_bizobj.UsersInvolvedInIssues([issue1, issue2]))
def HandleRequest(self, mr): """Build up a dictionary of data values to use when rendering the page. Args: mr: commonly used info parsed from the request. Returns: Dict of values used by EZT for rendering the page. """ if not mr.start and not mr.num: issues = self.services.issue.GetAllIssuesInProject( mr.cnxn, mr.project.project_id) else: local_id_range = range(mr.start, mr.start + mr.num) issues = self.services.issue.GetIssuesByLocalIDs( mr.cnxn, mr.project.project_id, local_id_range) user_id_set = tracker_bizobj.UsersInvolvedInIssues(issues) comments_dict = self.services.issue.GetCommentsForIssues( mr.cnxn, [issue.issue_id for issue in issues]) for comment_list in comments_dict.itervalues(): user_id_set.update( tracker_bizobj.UsersInvolvedInCommentList(comment_list)) starrers_dict = self.services.issue_star.LookupItemsStarrers( mr.cnxn, [issue.issue_id for issue in issues]) for starrer_id_list in starrers_dict.itervalues(): user_id_set.update(starrer_id_list) # The value 0 indicates "no user", e.g., that an issue has no owner. # We don't need to create a User row to represent that. user_id_set.discard(0) email_dict = self.services.user.LookupUserEmails(mr.cnxn, user_id_set) issues_json = [ self._MakeIssueJSON(mr, issue, email_dict, comments_dict.get(issue.issue_id, []), starrers_dict.get(issue.issue_id, [])) for issue in issues if not issue.deleted ] json_data = { 'metadata': { 'version': 1, 'when': int(time.time()), 'who': mr.auth.email, 'project': mr.project_name, 'start': mr.start, 'num': mr.num, }, 'issues': issues_json, # This list could be derived from the 'issues', but we provide it for # ease of processing. 'emails': email_dict.values(), } return json_data
def ListHotlistIssues(self, mc, request): """Get the issues on the specified hotlist.""" # TODO(ehmaldonado): This probably doesn't work, since we need to check # the permissions for each issue in their own project, and we're not doing # that. hotlist_id = converters.IngestHotlistRef(mc.cnxn, self.services.user, self.services.features, request.hotlist_ref) with work_env.WorkEnv(mc, self.services) as we: hotlist_items = we.GetHotlist(hotlist_id).items issue_ids = [item.issue_id for item in hotlist_items] issues = we.GetIssuesDict(issue_ids) projects = we.GetProjectsByName( [issue.project_name for issue in issues.values()]) configs = we.GetProjectConfigs( [project.project_id for project in projects.values()]) configs = { project.project_name: configs[project.project_id] for project in projects.values() } related_refs = we.GetRelatedIssueRefs(iter(issues.values())) with mc.profiler.Phase('making user views'): users_involved = set(item.adder_id for item in hotlist_items) users_involved.update( tracker_bizobj.UsersInvolvedInIssues(iter(issues.values()))) users_by_id = framework_views.MakeAllUserViews( mc.cnxn, self.services.user, users_involved) framework_views.RevealAllEmailsToMembers(mc.auth, None, users_by_id) hotlist_items = [ hotlist_item for hotlist_item in hotlist_items if hotlist_item.issue_id in issues ] start, max_items = converters.IngestPagination(request.pagination) pagination = paginate.ArtifactPagination(hotlist_items, max_items, start, None, None) result = features_pb2.ListHotlistIssuesResponse(items=[ converters.ConvertHotlistItem(hotlist_item, issues, users_by_id, related_refs, configs) for hotlist_item in pagination.visible_results ]) return result
def ListReferencedIssues(self, mc, request): """Return the specified issues in a response proto.""" if not request.issue_refs: return issues_pb2.ListReferencedIssuesResponse() for issue_ref in request.issue_refs: if not issue_ref.project_name: raise exceptions.InputException( 'Param `project_name` required.') if not issue_ref.local_id: raise exceptions.InputException('Param `local_id` required.') default_project_name = request.issue_refs[0].project_name ref_tuples = [(ref.project_name, ref.local_id) for ref in request.issue_refs] with work_env.WorkEnv(mc, self.services) as we: open_issues, closed_issues = we.ListReferencedIssues( ref_tuples, default_project_name) all_issues = open_issues + closed_issues all_project_ids = [issue.project_id for issue in all_issues] related_refs = we.GetRelatedIssueRefs(all_issues) configs = we.GetProjectConfigs(all_project_ids) with mc.profiler.Phase('making user views'): users_involved = tracker_bizobj.UsersInvolvedInIssues(all_issues) users_by_id = framework_views.MakeAllUserViews( mc.cnxn, self.services.user, users_involved) framework_views.RevealAllEmailsToMembers(mc.auth, None, users_by_id) with mc.profiler.Phase('converting to response objects'): converted_open_issues = [ converters.ConvertIssue(issue, users_by_id, related_refs, configs[issue.project_id]) for issue in open_issues ] converted_closed_issues = [ converters.ConvertIssue(issue, users_by_id, related_refs, configs[issue.project_id]) for issue in closed_issues ] response = issues_pb2.ListReferencedIssuesResponse( open_refs=converted_open_issues, closed_refs=converted_closed_issues) return response
def GetIssue(self, mc, request): """Return the specified issue in a response proto.""" issue_ref = request.issue_ref project, issue, config = self._GetProjectIssueAndConfig( mc, issue_ref, view_deleted=True, issue_required=False) # Code for getting where a moved issue was moved to. if issue is None: moved_to_ref = self.services.issue.GetCurrentLocationOfMovedIssue( mc.cnxn, project.project_id, issue_ref.local_id) moved_to_project_id, moved_to_id = moved_to_ref moved_to_project_name = None if moved_to_project_id is not None: with work_env.WorkEnv(mc, self.services) as we: moved_to_project = we.GetProject(moved_to_project_id) moved_to_project_name = moved_to_project.project_name return issues_pb2.IssueResponse( moved_to_ref=converters.ConvertIssueRef(( moved_to_project_name, moved_to_id))) raise exceptions.NoSuchIssueException() if issue.deleted: return issues_pb2.IssueResponse(issue=issue_objects_pb2.Issue( is_deleted=True)) with work_env.WorkEnv(mc, self.services) as we: related_refs = we.GetRelatedIssueRefs([issue]) with mc.profiler.Phase('making user views'): users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues( [issue]) users_by_id = framework_views.MakeAllUserViews( mc.cnxn, self.services.user, users_involved_in_issue) framework_views.RevealAllEmailsToMembers(mc.auth, project, users_by_id) with mc.profiler.Phase('converting to response objects'): response = issues_pb2.IssueResponse() response.issue.CopyFrom( converters.ConvertIssue(issue, users_by_id, related_refs, config)) return response
def _IndexIssueBatch(cnxn, issues, user_service, issue_service, config_dict): """Internal method to (re)index the given batch of issues. Args: cnxn: connection to SQL database. issues: list of Issue PBs to index. user_service: interface to user data storage. issue_service: interface to issue data storage. config_dict: dict {project_id: config} for all the projects that the given issues are in. """ user_ids = tracker_bizobj.UsersInvolvedInIssues(issues) comments_dict = issue_service.GetCommentsForIssues( cnxn, [issue.issue_id for issue in issues]) for comments in comments_dict.values(): user_ids.update([ic.user_id for ic in comments]) users_by_id = framework_views.MakeAllUserViews(cnxn, user_service, user_ids) _CreateIssueSearchDocuments(issues, comments_dict, users_by_id, config_dict)
def MakeViewsForUsersInIssues(cnxn, issue_list, user_service, omit_ids=None): """Lookup all the users involved in any of the given issues. Args: cnxn: connection to SQL database. issue_list: list of Issue PBs from a result query. user_service: Connection to User backend storage. omit_ids: a list of user_ids to omit, e.g., because we already have them. Returns: A dictionary {user_id: user_view,...} for all the users involved in the given issues. """ issue_participant_id_set = tracker_bizobj.UsersInvolvedInIssues(issue_list) if omit_ids: issue_participant_id_set.difference_update(omit_ids) # TODO(jrobbins): consider caching View objects as well. users_by_id = framework_views.MakeAllUserViews(cnxn, user_service, issue_participant_id_set) return users_by_id
def UpdateIssue(self, mc, request): """Apply a delta and comment to the specified issue, then return it.""" project, issue, config = self._GetProjectIssueAndConfig( mc, request.issue_ref, use_cache=False) with work_env.WorkEnv(mc, self.services) as we: if request.HasField('delta'): delta = converters.IngestIssueDelta(mc.cnxn, self.services, request.delta, config, issue.phases) else: delta = tracker_pb2.IssueDelta() # No changes specified. attachments = converters.IngestAttachmentUploads(request.uploads) we.UpdateIssue(issue, delta, request.comment_content, send_email=request.send_email, attachments=attachments, is_description=request.is_description, kept_attachments=list(request.kept_attachments)) related_refs = we.GetRelatedIssueRefs([issue]) with mc.profiler.Phase('making user views'): users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues( [issue]) users_by_id = framework_views.MakeAllUserViews( mc.cnxn, self.services.user, users_involved_in_issue) framework_views.RevealAllEmailsToMembers(mc.auth, project, users_by_id) with mc.profiler.Phase('converting to response objects'): response = issues_pb2.IssueResponse() response.issue.CopyFrom( converters.ConvertIssue(issue, users_by_id, related_refs, config)) return response
def GetGridViewData(self, mr): """EZT template values to render a Table View of issues. Args: mr: commonly used info parsed from the request. Returns: Dictionary of page data for rendering of the Table View. """ mr.ComputeColSpec(mr.hotlist) starred_iid_set = set( self.services.issue_star.LookupStarredItemIDs( mr.cnxn, mr.auth.user_id)) issues_list = self.services.issue.GetIssues( mr.cnxn, [hotlist_issue.issue_id for hotlist_issue in mr.hotlist.items]) allowed_issues = hotlist_helpers.FilterIssues(mr, issues_list, self.services) issue_and_hotlist_users = tracker_bizobj.UsersInvolvedInIssues( allowed_issues or []).union(features_bizobj.UsersInvolvedInHotlists([mr.hotlist])) users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, issue_and_hotlist_users) hotlist_issues_project_ids = hotlist_helpers.GetAllProjectsOfIssues( [issue for issue in issues_list]) config_list = hotlist_helpers.GetAllConfigsOfProjects( mr.cnxn, hotlist_issues_project_ids, self.services) harmonized_config = tracker_bizobj.HarmonizeConfigs(config_list) limit = settings.max_issues_in_grid grid_limited = len(allowed_issues) > limit lower_cols = mr.col_spec.lower().split() grid_x = (mr.x or harmonized_config.default_x_attr or '--').lower() grid_y = (mr.y or harmonized_config.default_y_attr or '--').lower() lower_cols.append(grid_x) lower_cols.append(grid_y) related_iids = set() for issue in allowed_issues: if 'blockedon' in lower_cols: related_iids.update(issue.blocked_on_iids) if 'blocking' in lower_cols: related_iids.update(issue.blocking_iids) if 'mergedinto' in lower_cols: related_iids.add(issue.merged_into) related_issues_list = self.services.issue.GetIssues( mr.cnxn, list(related_iids)) related_issues = { issue.issue_id: issue for issue in related_issues_list } hotlist_context_dict = { hotlist_issue.issue_id: { 'adder_id': hotlist_issue.adder_id, 'date_added': timestr.FormatRelativeDate(hotlist_issue.date_added), 'note': hotlist_issue.note } for hotlist_issue in mr.hotlist.items } grid_view_data = grid_view_helpers.GetGridViewData( mr, allowed_issues, harmonized_config, users_by_id, starred_iid_set, grid_limited, related_issues, hotlist_context_dict=hotlist_context_dict) url_params = [(name, mr.GetParam(name)) for name in framework_helpers.RECOGNIZED_PARAMS] # We are passing in None for the project_name in ArtifactPagination # because we are not operating under any project. grid_view_data.update({ 'pagination': paginate.ArtifactPagination( allowed_issues, mr.GetPositiveIntParam( 'num', features_constants.DEFAULT_RESULTS_PER_PAGE), mr.GetPositiveIntParam('start'), None, urls.HOTLIST_ISSUES, total_count=len(allowed_issues), url_params=url_params) }) return grid_view_data
def HandleRequest(self, mr): """Process the task to notify users after an issue blocking change. Args: mr: common information parsed from the HTTP request. Returns: Results dictionary in JSON format which is useful just for debugging. The main goal is the side-effect of sending emails. """ issue_ids = mr.GetIntListParam('issue_ids') hostport = mr.GetParam('hostport') if not issue_ids: return { 'params': {}, 'notified': [], 'message': 'Cannot proceed without a valid issue IDs.', } old_owner_ids = mr.GetIntListParam('old_owner_ids') comment_text = mr.GetParam('comment_text') commenter_id = mr.GetPositiveIntParam('commenter_id') amendments = mr.GetParam('amendments') send_email = bool(mr.GetIntParam('send_email')) params = dict( issue_ids=issue_ids, commenter_id=commenter_id, hostport=hostport, old_owner_ids=old_owner_ids, comment_text=comment_text, send_email=send_email, amendments=amendments) logging.info('bulk edit params are %r', params) issues = self.services.issue.GetIssues(mr.cnxn, issue_ids) # TODO(jrobbins): For cross-project bulk edits, prefetch all relevant # projects and configs and pass a dict of them to subroutines. For # now, all issue must be in the same project. project_id = issues[0].project_id project = self.services.project.GetProject(mr.cnxn, project_id) config = self.services.config.GetProjectConfig(mr.cnxn, project_id) issues = [issue for issue in issues if not issue.is_spam] anon_perms = permissions.GetPermissions(None, set(), project) users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, [commenter_id]) ids_in_issues = {} starrers = {} non_private_issues = [] for issue, old_owner_id in zip(issues, old_owner_ids): # TODO(jrobbins): use issue_id consistently rather than local_id. starrers[issue.local_id] = self.services.issue_star.LookupItemStarrers( mr.cnxn, issue.issue_id) named_ids = set() # users named in user-value fields that notify. for fd in config.field_defs: named_ids.update(notify_reasons.ComputeNamedUserIDsToNotify( issue.field_values, fd)) direct, indirect = self.services.usergroup.ExpandAnyUserGroups( mr.cnxn, list(issue.cc_ids) + list(issue.derived_cc_ids) + [issue.owner_id, old_owner_id, issue.derived_owner_id] + list(named_ids)) ids_in_issues[issue.local_id] = set(starrers[issue.local_id]) ids_in_issues[issue.local_id].update(direct) ids_in_issues[issue.local_id].update(indirect) ids_in_issue_needing_views = ( ids_in_issues[issue.local_id] | tracker_bizobj.UsersInvolvedInIssues([issue])) new_ids_in_issue = [user_id for user_id in ids_in_issue_needing_views if user_id not in users_by_id] users_by_id.update( framework_views.MakeAllUserViews( mr.cnxn, self.services.user, new_ids_in_issue)) anon_can_view = permissions.CanViewIssue( set(), anon_perms, project, issue) if anon_can_view: non_private_issues.append(issue) commenter_view = users_by_id[commenter_id] omit_addrs = {commenter_view.email} tasks = [] if send_email: email_tasks = self._BulkEditEmailTasks( mr.cnxn, issues, old_owner_ids, omit_addrs, project, non_private_issues, users_by_id, ids_in_issues, starrers, commenter_view, hostport, comment_text, amendments, config) tasks = email_tasks notified = notify_helpers.AddAllEmailTasks(tasks) return { 'params': params, 'notified': notified, }
def testUsersInvolvedInIssues_Empty(self): self.assertEqual(set(), tracker_bizobj.UsersInvolvedInIssues([]))
def GatherPageData(self, mr): """Build up a dictionary of data values to use when rendering the page. Args: mr: commonly used info parsed from the request. Returns: Dict of values used by EZT for rendering the page. """ if mr.local_id is None: self.abort(404, 'no issue specified') with work_env.WorkEnv(mr, self.services) as we: # Signed in users could edit the issue, so it must be fresh. use_cache = not mr.auth.user_id issue = we.GetIssueByLocalID( mr.project_id, mr.local_id, use_cache=use_cache) # We give no explanation of missing issues on the peek page. if issue.deleted: self.abort(404, 'issue not found') star_cnxn = sql.MonorailConnection() star_promise = framework_helpers.Promise( we.IsIssueStarred, issue, cnxn=star_cnxn) config = we.GetProjectConfig(mr.project_id) comments = we.ListIssueComments(issue) descriptions, visible_comments, cmnt_pagination = PaginateComments( mr, issue, comments, config, self.services) with mr.profiler.Phase('making user proxies'): involved_user_ids = tracker_bizobj.UsersInvolvedInIssues([issue]) group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups( mr.cnxn, involved_user_ids) comment_user_ids = tracker_bizobj.UsersInvolvedInCommentList( descriptions + visible_comments) users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, involved_user_ids, comment_user_ids, group_ids=group_ids) framework_views.RevealAllEmailsToMembers(mr.auth, mr.project, users_by_id) (issue_view, description_views, comment_views) = self._MakeIssueAndCommentViews( mr, issue, users_by_id, descriptions, visible_comments, config, issue_reporters=[], comment_reporters=[]) with mr.profiler.Phase('getting starring info'): starred = star_promise.WaitAndGetValue() star_cnxn.Close() permit_edit = permissions.CanEditIssue( mr.auth.effective_ids, mr.perms, mr.project, issue) mr.ComputeColSpec(config) restrict_to_known = config.restrict_to_known page_perms = self.MakePagePerms( mr, issue, permissions.CREATE_ISSUE, permissions.SET_STAR, permissions.EDIT_ISSUE, permissions.EDIT_ISSUE_SUMMARY, permissions.EDIT_ISSUE_STATUS, permissions.EDIT_ISSUE_OWNER, permissions.EDIT_ISSUE_CC, permissions.DELETE_ISSUE, permissions.ADD_ISSUE_COMMENT, permissions.DELETE_OWN, permissions.DELETE_ANY, permissions.VIEW_INBOUND_MESSAGES) page_perms.EditIssue = ezt.boolean(permit_edit) prevent_restriction_removal = ( mr.project.only_owners_remove_restrictions and not framework_bizobj.UserOwnsProject( mr.project, mr.auth.effective_ids)) cmd_slots, default_slot_num = self.services.features.GetRecentCommands( mr.cnxn, mr.auth.user_id, mr.project_id) cmd_slot_views = [ template_helpers.EZTItem( slot_num=slot_num, command=command, comment=comment) for slot_num, command, comment in cmd_slots] previous_locations = self.GetPreviousLocations(mr, issue) return { 'issue_tab_mode': 'issueDetail', 'issue': issue_view, 'description': description_views, 'comments': comment_views, 'labels': issue.labels, 'num_detail_rows': len(comment_views) + 4, 'noisy': ezt.boolean(tracker_helpers.IsNoisy( len(comment_views), issue.star_count)), 'cmnt_pagination': cmnt_pagination, 'colspec': mr.col_spec, 'searchtip': 'You can jump to any issue by number', 'starred': ezt.boolean(starred), 'pagegen': str(int(time.time() * 1000000)), 'restrict_to_known': ezt.boolean(restrict_to_known), 'prevent_restriction_removal': ezt.boolean( prevent_restriction_removal), 'statuses_offer_merge': config.statuses_offer_merge, 'page_perms': page_perms, 'cmd_slots': cmd_slot_views, 'default_slot_num': default_slot_num, 'quick_edit_submit_url': tracker_helpers.FormatRelativeIssueURL( issue.project_name, urls.ISSUE_PEEK + '.do', id=issue.local_id), 'previous_locations': previous_locations, # for template issue-meta-part shared by issuedetail servlet 'user_remaining_hotlists': [], 'user_issue_hotlists': [], 'involved_users_issue_hotlists': [], 'remaining_issue_hotlists': [], }
def HandleRequest(self, mr): """Process the task to notify users after an issue change. Args: mr: common information parsed from the HTTP request. Returns: Results dictionary in JSON format which is useful just for debugging. The main goal is the side-effect of sending emails. """ issue_id = mr.GetPositiveIntParam('issue_id') if not issue_id: return { 'params': {}, 'notified': [], 'message': 'Cannot proceed without a valid issue ID.', } commenter_id = mr.GetPositiveIntParam('commenter_id') seq_num = mr.seq omit_ids = [commenter_id] hostport = mr.GetParam('hostport') old_owner_id = mr.GetPositiveIntParam('old_owner_id') send_email = bool(mr.GetIntParam('send_email')) comment_id = mr.GetPositiveIntParam('comment_id') params = dict( issue_id=issue_id, commenter_id=commenter_id, seq_num=seq_num, hostport=hostport, old_owner_id=old_owner_id, omit_ids=omit_ids, send_email=send_email, comment_id=comment_id) logging.info('issue change params are %r', params) # TODO(jrobbins): Re-enable the issue cache for notifications after # the stale issue defect (monorail:2514) is 100% resolved. issue = self.services.issue.GetIssue(mr.cnxn, issue_id, use_cache=False) project = self.services.project.GetProject(mr.cnxn, issue.project_id) config = self.services.config.GetProjectConfig(mr.cnxn, issue.project_id) if issue.is_spam: # Don't send email for spam issues. return { 'params': params, 'notified': [], } all_comments = self.services.issue.GetCommentsForIssue( mr.cnxn, issue.issue_id) if comment_id: logging.info('Looking up comment by comment_id') for c in all_comments: if c.id == comment_id: comment = c logging.info('Comment was found by comment_id') break else: raise ValueError('Comment %r was not found' % comment_id) else: logging.info('Looking up comment by seq_num') comment = all_comments[seq_num] # Only issues that any contributor could view sent to mailing lists. contributor_could_view = permissions.CanViewIssue( set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project, issue) starrer_ids = self.services.issue_star.LookupItemStarrers( mr.cnxn, issue.issue_id) users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, tracker_bizobj.UsersInvolvedInIssues([issue]), [old_owner_id], tracker_bizobj.UsersInvolvedInComment(comment), issue.cc_ids, issue.derived_cc_ids, starrer_ids, omit_ids) # Make followup tasks to send emails tasks = [] if send_email: tasks = self._MakeEmailTasks( mr.cnxn, project, issue, config, old_owner_id, users_by_id, all_comments, comment, starrer_ids, contributor_could_view, hostport, omit_ids, mr.perms) notified = notify_helpers.AddAllEmailTasks(tasks) return { 'params': params, 'notified': notified, }
def HandleRequest(self, mr): """Build up a dictionary of data values to use when rendering the page. Args: mr: commonly used info parsed from the request. Returns: Dict of values used by EZT for rendering the page. """ if mr.query or mr.can != 1: with work_env.WorkEnv(mr, self.services) as we: url_params = [] pipeline = we.ListIssues(mr.query, [mr.project.project_name], mr.auth.user_id, mr.num, mr.start, url_params, mr.can, mr.group_by_spec, mr.sort_spec, False) issues = pipeline.allowed_results # no user query and mr.can == 1 (we want all issues) elif not mr.start and not mr.num: issues = self.services.issue.GetAllIssuesInProject( mr.cnxn, mr.project.project_id) else: local_id_range = list(range(mr.start, mr.start + mr.num)) issues = self.services.issue.GetIssuesByLocalIDs( mr.cnxn, mr.project.project_id, local_id_range) user_id_set = tracker_bizobj.UsersInvolvedInIssues(issues) comments_dict = self.services.issue.GetCommentsForIssues( mr.cnxn, [issue.issue_id for issue in issues]) for comment_list in comments_dict.values(): user_id_set.update( tracker_bizobj.UsersInvolvedInCommentList(comment_list)) starrers_dict = self.services.issue_star.LookupItemsStarrers( mr.cnxn, [issue.issue_id for issue in issues]) for starrer_id_list in starrers_dict.values(): user_id_set.update(starrer_id_list) # The value 0 indicates "no user", e.g., that an issue has no owner. # We don't need to create a User row to represent that. user_id_set.discard(0) email_dict = self.services.user.LookupUserEmails(mr.cnxn, user_id_set, ignore_missed=True) issues_json = [ self._MakeIssueJSON(mr, issue, email_dict, comments_dict.get(issue.issue_id, []), starrers_dict.get(issue.issue_id, [])) for issue in issues if not issue.deleted ] json_data = { 'metadata': { 'version': 1, 'when': int(time.time()), 'who': mr.auth.email, 'project': mr.project_name, 'start': mr.start, 'num': mr.num, }, 'issues': issues_json, # This list could be derived from the 'issues', but we provide it for # ease of processing. 'emails': list(email_dict.values()), } return json_data
def GetSortedHotlistIssues(mr, hotlist_issues, issues_list, harmonized_config, profiler, services): with profiler.Phase('Checking issue permissions and getting ranks'): allowed_issues = FilterIssues(mr, issues_list, services) allowed_iids = [issue.issue_id for issue in allowed_issues] # The values for issues in a hotlist are specific to the hotlist # (rank, adder, added) without invalidating the keys, an issue will retain # the rank value it has in one hotlist when navigating to another hotlist. sorting.InvalidateArtValuesKeys( mr.cnxn, [issue.issue_id for issue in allowed_issues]) sorted_ranks = sorted([ hotlist_issue.rank for hotlist_issue in hotlist_issues if hotlist_issue.issue_id in allowed_iids ]) friendly_ranks = { rank: friendly for friendly, rank in enumerate(sorted_ranks, 1) } issue_adders = framework_views.MakeAllUserViews( mr.cnxn, services.user, [hotlist_issue.adder_id for hotlist_issue in hotlist_issues]) hotlist_issues_context = { hotlist_issue.issue_id: { 'issue_rank': friendly_ranks[hotlist_issue.rank], 'adder_id': hotlist_issue.adder_id, 'date_added': timestr.FormatAbsoluteDate(hotlist_issue.date_added), 'note': hotlist_issue.note } for hotlist_issue in hotlist_issues if hotlist_issue.issue_id in allowed_iids } with profiler.Phase('Making user views'): issues_users_by_id = framework_views.MakeAllUserViews( mr.cnxn, services.user, tracker_bizobj.UsersInvolvedInIssues(allowed_issues or [])) issues_users_by_id.update(issue_adders) with profiler.Phase('Sorting issues'): sortable_fields = tracker_helpers.SORTABLE_FIELDS.copy() sortable_fields.update({ 'rank': lambda issue: hotlist_issues_context[issue.issue_id]['issue_rank'], 'adder': lambda issue: hotlist_issues_context[issue.issue_id]['adder_id'], 'added': lambda issue: hotlist_issues_context[issue.issue_id]['date_added'], 'note': lambda issue: hotlist_issues_context[issue.issue_id]['note'] }) sortable_postproc = tracker_helpers.SORTABLE_FIELDS_POSTPROCESSORS.copy( ) sortable_postproc.update({ 'adder': lambda user_view: user_view.email, }) if not mr.sort_spec: mr.sort_spec = 'rank' sorted_issues = sorting.SortArtifacts(mr, allowed_issues, harmonized_config, sortable_fields, sortable_postproc, users_by_id=issues_users_by_id, tie_breakers=['rank', 'id']) return sorted_issues, hotlist_issues_context, issues_users_by_id