Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1566)

Unified Diff: appengine/monorail/tracker/issuepeek.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « appengine/monorail/tracker/issueoriginal.py ('k') | appengine/monorail/tracker/issuereindex.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: appengine/monorail/tracker/issuepeek.py
diff --git a/appengine/monorail/tracker/issuepeek.py b/appengine/monorail/tracker/issuepeek.py
new file mode 100644
index 0000000000000000000000000000000000000000..24d8a902ca9164e0bc75330dcd3319d1053ac162
--- /dev/null
+++ b/appengine/monorail/tracker/issuepeek.py
@@ -0,0 +1,378 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is govered by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Classes that implement the issue peek page and related forms."""
+
+import logging
+import time
+from third_party import ezt
+
+import settings
+from features import commands
+from features import notify
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import monorailrequest
+from framework import paginate
+from framework import permissions
+from framework import servlet
+from framework import sql
+from framework import template_helpers
+from framework import urls
+from framework import xsrf
+from services import issue_svc
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+
+class IssuePeek(servlet.Servlet):
+ """IssuePeek is a page that shows the details of one issue."""
+
+ _PAGE_TEMPLATE = 'tracker/issue-peek-ajah.ezt'
+ _ALLOW_VIEWING_DELETED = False
+
+ def AssertBasePermission(self, mr):
+ """Check that the user has permission to even visit this page."""
+ super(IssuePeek, self).AssertBasePermission(mr)
+ try:
+ issue = self._GetIssue(mr)
+ except issue_svc.NoSuchIssueException:
+ return
+ if not issue:
+ return
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ granted_perms = tracker_bizobj.GetGrantedPerms(
+ issue, mr.auth.effective_ids, config)
+ permit_view = permissions.CanViewIssue(
+ mr.auth.effective_ids, mr.perms, mr.project, issue,
+ allow_viewing_deleted=self._ALLOW_VIEWING_DELETED,
+ granted_perms=granted_perms)
+ if not permit_view:
+ raise permissions.PermissionException(
+ 'User is not allowed to view this issue')
+
+ def _GetIssue(self, mr):
+ """Retrieve the current issue."""
+ if mr.local_id is None:
+ return None # GatherPageData will detect the same condition.
+ issue = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, mr.project_id, mr.local_id)
+ return issue
+
+ 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 self.profiler.Phase('finishing getting issue'):
+ issue = self._GetIssue(mr)
+ if issue is None:
+ self.abort(404, 'issue not found')
+
+ # We give no explanation of missing issues on the peek page.
+ if issue is None or issue.deleted:
+ self.abort(404, 'issue not found')
+
+ star_cnxn = sql.MonorailConnection()
+ star_promise = framework_helpers.Promise(
+ self.services.issue_star.IsItemStarredBy, star_cnxn,
+ issue.issue_id, mr.auth.user_id)
+
+ with self.profiler.Phase('getting project issue config'):
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+
+ with self.profiler.Phase('finishing getting comments'):
+ comments = self.services.issue.GetCommentsForIssue(
+ mr.cnxn, issue.issue_id)
+
+ description, visible_comments, cmnt_pagination = PaginateComments(
+ mr, issue, comments, config)
+
+ with self.profiler.Phase('making user proxies'):
+ users_by_id = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user,
+ tracker_bizobj.UsersInvolvedInIssues([issue]),
+ tracker_bizobj.UsersInvolvedInCommentList(
+ [description] + visible_comments))
+ framework_views.RevealAllEmailsToMembers(mr, users_by_id)
+
+ (issue_view, description_view,
+ comment_views) = self._MakeIssueAndCommentViews(
+ mr, issue, users_by_id, description, visible_comments, config)
+
+ with self.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_view,
+ '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(long(time.time() * 1000000)),
+ 'set_star_token': xsrf.GenerateToken(
+ mr.auth.user_id, '/p/%s%s' % ( # Note: no .do suffix.
+ mr.project_name, urls.ISSUE_SETSTAR_JSON)),
+
+ '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,
+ }
+
+ def GetPreviousLocations(self, mr, issue):
+ """Return a list of previous locations of the current issue."""
+ previous_location_ids = self.services.issue.GetPreviousLocations(
+ mr.cnxn, issue)
+ previous_locations = []
+ for old_pid, old_id in previous_location_ids:
+ old_project = self.services.project.GetProject(mr.cnxn, old_pid)
+ previous_locations.append(
+ template_helpers.EZTItem(
+ project_name=old_project.project_name, local_id=old_id))
+
+ return previous_locations
+
+ def _MakeIssueAndCommentViews(
+ self, mr, issue, users_by_id, initial_description, 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,...}.
+ initial_description: IssueComment for the initial issue report.
+ 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_view, comment_views). One IssueView for
+ the whole issue, one IssueCommentView for the initial description,
+ and then a list of IssueCommentView's for each additional comment.
+ """
+ with self.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 self.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 self.profiler.Phase('autolinker object lookup'):
+ all_ref_artifacts = self.services.autolink.GetAllReferencedArtifacts(
+ mr, [c.content for c in [initial_description] + comments])
+
+ with self.profiler.Phase('making comment views'):
+ reporter_auth = monorailrequest.AuthData.FromUserID(
+ mr.cnxn, initial_description.user_id, self.services)
+ desc_view = tracker_views.IssueCommentView(
+ mr.project_name, initial_description, users_by_id,
+ self.services.autolink, all_ref_artifacts, mr,
+ issue, effective_ids=reporter_auth.effective_ids)
+ # 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_view, comment_views
+
+ def ProcessFormData(self, mr, post_data):
+ """Process the posted issue update form.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ post_data: HTML form data from the request.
+
+ Returns:
+ String URL to redirect the user to, or None if response was already sent.
+ """
+ cmd = post_data.get('cmd', '')
+ send_email = 'send_email' in post_data
+ comment = post_data.get('comment', '')
+ slot_used = int(post_data.get('slot_used', 1))
+ page_generation_time = long(post_data['pagegen'])
+ issue = self._GetIssue(mr)
+ old_owner_id = tracker_bizobj.GetOwnerId(issue)
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+
+ summary, status, owner_id, cc_ids, labels = commands.ParseQuickEditCommand(
+ mr.cnxn, cmd, issue, config, mr.auth.user_id, self.services)
+ component_ids = issue.component_ids # TODO(jrobbins): component commands
+ field_values = issue.field_values # TODO(jrobbins): edit custom fields
+
+ permit_edit = permissions.CanEditIssue(
+ mr.auth.effective_ids, mr.perms, mr.project, issue)
+ if not permit_edit:
+ raise permissions.PermissionException(
+ 'User is not allowed to edit this issue')
+
+ amendments, _ = self.services.issue.ApplyIssueComment(
+ mr.cnxn, self.services, mr.auth.user_id,
+ mr.project_id, mr.local_id, summary, status, owner_id, cc_ids,
+ labels, field_values, component_ids, issue.blocked_on_iids,
+ issue.blocking_iids, issue.dangling_blocked_on_refs,
+ issue.dangling_blocking_refs, issue.merged_into,
+ page_gen_ts=page_generation_time, comment=comment)
+ self.services.project.UpdateRecentActivity(
+ mr.cnxn, mr.project.project_id)
+
+ if send_email:
+ if amendments or comment.strip():
+ cmnts = self.services.issue.GetCommentsForIssue(
+ mr.cnxn, issue.issue_id)
+ notify.PrepareAndSendIssueChangeNotification(
+ mr.project_id, mr.local_id, mr.request.host,
+ mr.auth.user_id, len(cmnts) - 1,
+ send_email=send_email, old_owner_id=old_owner_id)
+
+ # TODO(jrobbins): allow issue merge via quick-edit.
+
+ self.services.features.StoreRecentCommand(
+ mr.cnxn, mr.auth.user_id, mr.project_id, slot_used, cmd, comment)
+
+ # TODO(jrobbins): this is very similar to a block of code in issuebulkedit.
+ mr.can = int(post_data['can'])
+ mr.query = post_data.get('q', '')
+ mr.col_spec = post_data.get('colspec', '')
+ mr.sort_spec = post_data.get('sort', '')
+ mr.group_by_spec = post_data.get('groupby', '')
+ mr.start = int(post_data['start'])
+ mr.num = int(post_data['num'])
+ preview_issue_ref_str = '%s:%d' % (issue.project_name, issue.local_id)
+ return tracker_helpers.FormatIssueListURL(
+ mr, config, preview=preview_issue_ref_str, updated=mr.local_id,
+ ts=int(time.time()))
+
+
+def PaginateComments(mr, issue, issuecomment_list, config):
+ """Filter and paginate the IssueComment PBs for the given issue.
+
+ Unlike most pagination, this one starts at the end of the whole
+ list so it shows only the most recent comments. The user can use
+ the "Older" and "Newer" links to page through older comments.
+
+ Args:
+ mr: common info parsed from the HTTP request.
+ issue: Issue PB for the issue being viewed.
+ issuecomment_list: list of IssueComment PBs for the viewed issue,
+ the zeroth item in this list is the initial issue description.
+ config: ProjectIssueConfig for the project that contains this issue.
+
+ Returns:
+ A tuple (description, visible_comments, pagination), where description
+ is the IssueComment for the initial issue description, visible_comments
+ is a list of IssueComment PBs for the comments that should be displayed
+ on the current pagination page, and pagination is a VirtualPagination
+ object that keeps track of the Older and Newer links.
+ """
+ if not issuecomment_list:
+ return None, [], None
+
+ description = issuecomment_list[0]
+ comments = issuecomment_list[1:]
+ allowed_comments = []
+ restrictions = permissions.GetRestrictions(issue)
+ granted_perms = tracker_bizobj.GetGrantedPerms(
+ issue, mr.auth.effective_ids, config)
+ for c in comments:
+ can_delete = permissions.CanDelete(
+ mr.auth.user_id, mr.auth.effective_ids, mr.perms, c.deleted_by,
+ c.user_id, mr.project, restrictions, granted_perms=granted_perms)
+ if can_delete or not c.deleted_by:
+ allowed_comments.append(c)
+
+ pagination_url = '%s?id=%d' % (urls.ISSUE_DETAIL, issue.local_id)
+ pagination = paginate.VirtualPagination(
+ mr, len(allowed_comments),
+ framework_constants.DEFAULT_COMMENTS_PER_PAGE,
+ list_page_url=pagination_url,
+ count_up=False, start_param='cstart', num_param='cnum',
+ max_num=settings.max_comments_per_page)
+ if pagination.last == 1 and pagination.start == len(allowed_comments):
+ pagination.visible = ezt.boolean(False)
+ visible_comments = allowed_comments[
+ pagination.last - 1:pagination.start]
+
+ return description, visible_comments, pagination
« no previous file with comments | « appengine/monorail/tracker/issueoriginal.py ('k') | appengine/monorail/tracker/issuereindex.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698