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

Side by Side 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 unified diff | Download patch
« no previous file with comments | « appengine/monorail/tracker/issueoriginal.py ('k') | appengine/monorail/tracker/issuereindex.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2016 The Chromium Authors. All rights reserved.
2 # Use of this source code is govered by a BSD-style
3 # license that can be found in the LICENSE file or at
4 # https://developers.google.com/open-source/licenses/bsd
5
6 """Classes that implement the issue peek page and related forms."""
7
8 import logging
9 import time
10 from third_party import ezt
11
12 import settings
13 from features import commands
14 from features import notify
15 from framework import framework_bizobj
16 from framework import framework_constants
17 from framework import framework_helpers
18 from framework import framework_views
19 from framework import monorailrequest
20 from framework import paginate
21 from framework import permissions
22 from framework import servlet
23 from framework import sql
24 from framework import template_helpers
25 from framework import urls
26 from framework import xsrf
27 from services import issue_svc
28 from tracker import tracker_bizobj
29 from tracker import tracker_constants
30 from tracker import tracker_helpers
31 from tracker import tracker_views
32
33
34 class IssuePeek(servlet.Servlet):
35 """IssuePeek is a page that shows the details of one issue."""
36
37 _PAGE_TEMPLATE = 'tracker/issue-peek-ajah.ezt'
38 _ALLOW_VIEWING_DELETED = False
39
40 def AssertBasePermission(self, mr):
41 """Check that the user has permission to even visit this page."""
42 super(IssuePeek, self).AssertBasePermission(mr)
43 try:
44 issue = self._GetIssue(mr)
45 except issue_svc.NoSuchIssueException:
46 return
47 if not issue:
48 return
49 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
50 granted_perms = tracker_bizobj.GetGrantedPerms(
51 issue, mr.auth.effective_ids, config)
52 permit_view = permissions.CanViewIssue(
53 mr.auth.effective_ids, mr.perms, mr.project, issue,
54 allow_viewing_deleted=self._ALLOW_VIEWING_DELETED,
55 granted_perms=granted_perms)
56 if not permit_view:
57 raise permissions.PermissionException(
58 'User is not allowed to view this issue')
59
60 def _GetIssue(self, mr):
61 """Retrieve the current issue."""
62 if mr.local_id is None:
63 return None # GatherPageData will detect the same condition.
64 issue = self.services.issue.GetIssueByLocalID(
65 mr.cnxn, mr.project_id, mr.local_id)
66 return issue
67
68 def GatherPageData(self, mr):
69 """Build up a dictionary of data values to use when rendering the page.
70
71 Args:
72 mr: commonly used info parsed from the request.
73
74 Returns:
75 Dict of values used by EZT for rendering the page.
76 """
77 if mr.local_id is None:
78 self.abort(404, 'no issue specified')
79 with self.profiler.Phase('finishing getting issue'):
80 issue = self._GetIssue(mr)
81 if issue is None:
82 self.abort(404, 'issue not found')
83
84 # We give no explanation of missing issues on the peek page.
85 if issue is None or issue.deleted:
86 self.abort(404, 'issue not found')
87
88 star_cnxn = sql.MonorailConnection()
89 star_promise = framework_helpers.Promise(
90 self.services.issue_star.IsItemStarredBy, star_cnxn,
91 issue.issue_id, mr.auth.user_id)
92
93 with self.profiler.Phase('getting project issue config'):
94 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
95
96 with self.profiler.Phase('finishing getting comments'):
97 comments = self.services.issue.GetCommentsForIssue(
98 mr.cnxn, issue.issue_id)
99
100 description, visible_comments, cmnt_pagination = PaginateComments(
101 mr, issue, comments, config)
102
103 with self.profiler.Phase('making user proxies'):
104 users_by_id = framework_views.MakeAllUserViews(
105 mr.cnxn, self.services.user,
106 tracker_bizobj.UsersInvolvedInIssues([issue]),
107 tracker_bizobj.UsersInvolvedInCommentList(
108 [description] + visible_comments))
109 framework_views.RevealAllEmailsToMembers(mr, users_by_id)
110
111 (issue_view, description_view,
112 comment_views) = self._MakeIssueAndCommentViews(
113 mr, issue, users_by_id, description, visible_comments, config)
114
115 with self.profiler.Phase('getting starring info'):
116 starred = star_promise.WaitAndGetValue()
117 star_cnxn.Close()
118 permit_edit = permissions.CanEditIssue(
119 mr.auth.effective_ids, mr.perms, mr.project, issue)
120
121 mr.ComputeColSpec(config)
122 restrict_to_known = config.restrict_to_known
123
124 page_perms = self.MakePagePerms(
125 mr, issue,
126 permissions.CREATE_ISSUE,
127 permissions.SET_STAR,
128 permissions.EDIT_ISSUE,
129 permissions.EDIT_ISSUE_SUMMARY,
130 permissions.EDIT_ISSUE_STATUS,
131 permissions.EDIT_ISSUE_OWNER,
132 permissions.EDIT_ISSUE_CC,
133 permissions.DELETE_ISSUE,
134 permissions.ADD_ISSUE_COMMENT,
135 permissions.DELETE_OWN,
136 permissions.DELETE_ANY,
137 permissions.VIEW_INBOUND_MESSAGES)
138 page_perms.EditIssue = ezt.boolean(permit_edit)
139
140 prevent_restriction_removal = (
141 mr.project.only_owners_remove_restrictions and
142 not framework_bizobj.UserOwnsProject(
143 mr.project, mr.auth.effective_ids))
144
145 cmd_slots, default_slot_num = self.services.features.GetRecentCommands(
146 mr.cnxn, mr.auth.user_id, mr.project_id)
147 cmd_slot_views = [
148 template_helpers.EZTItem(
149 slot_num=slot_num, command=command, comment=comment)
150 for slot_num, command, comment in cmd_slots]
151
152 previous_locations = self.GetPreviousLocations(mr, issue)
153
154 return {
155 'issue_tab_mode': 'issueDetail',
156 'issue': issue_view,
157 'description': description_view,
158 'comments': comment_views,
159 'labels': issue.labels,
160 'num_detail_rows': len(comment_views) + 4,
161 'noisy': ezt.boolean(tracker_helpers.IsNoisy(
162 len(comment_views), issue.star_count)),
163
164 'cmnt_pagination': cmnt_pagination,
165 'colspec': mr.col_spec,
166 'searchtip': 'You can jump to any issue by number',
167 'starred': ezt.boolean(starred),
168
169 'pagegen': str(long(time.time() * 1000000)),
170 'set_star_token': xsrf.GenerateToken(
171 mr.auth.user_id, '/p/%s%s' % ( # Note: no .do suffix.
172 mr.project_name, urls.ISSUE_SETSTAR_JSON)),
173
174 'restrict_to_known': ezt.boolean(restrict_to_known),
175 'prevent_restriction_removal': ezt.boolean(
176 prevent_restriction_removal),
177
178 'statuses_offer_merge': config.statuses_offer_merge,
179 'page_perms': page_perms,
180 'cmd_slots': cmd_slot_views,
181 'default_slot_num': default_slot_num,
182 'quick_edit_submit_url': tracker_helpers.FormatRelativeIssueURL(
183 issue.project_name, urls.ISSUE_PEEK + '.do', id=issue.local_id),
184 'previous_locations': previous_locations,
185 }
186
187 def GetPreviousLocations(self, mr, issue):
188 """Return a list of previous locations of the current issue."""
189 previous_location_ids = self.services.issue.GetPreviousLocations(
190 mr.cnxn, issue)
191 previous_locations = []
192 for old_pid, old_id in previous_location_ids:
193 old_project = self.services.project.GetProject(mr.cnxn, old_pid)
194 previous_locations.append(
195 template_helpers.EZTItem(
196 project_name=old_project.project_name, local_id=old_id))
197
198 return previous_locations
199
200 def _MakeIssueAndCommentViews(
201 self, mr, issue, users_by_id, initial_description, comments, config,
202 issue_reporters=None, comment_reporters=None):
203 """Create view objects that help display parts of an issue.
204
205 Args:
206 mr: commonly used info parsed from the request.
207 issue: issue PB for the currently viewed issue.
208 users_by_id: dictionary of {user_id: UserView,...}.
209 initial_description: IssueComment for the initial issue report.
210 comments: list of IssueComment PBs on the current issue.
211 issue_reporters: list of user IDs who have flagged the issue as spam.
212 comment_reporters: map of comment ID to list of flagging user IDs.
213 config: ProjectIssueConfig for the project that contains this issue.
214
215 Returns:
216 (issue_view, description_view, comment_views). One IssueView for
217 the whole issue, one IssueCommentView for the initial description,
218 and then a list of IssueCommentView's for each additional comment.
219 """
220 with self.profiler.Phase('getting related issues'):
221 open_related, closed_related = (
222 tracker_helpers.GetAllowedOpenAndClosedRelatedIssues(
223 self.services, mr, issue))
224 all_related_iids = list(issue.blocked_on_iids) + list(issue.blocking_iids)
225 if issue.merged_into:
226 all_related_iids.append(issue.merged_into)
227 all_related = self.services.issue.GetIssues(mr.cnxn, all_related_iids)
228
229 with self.profiler.Phase('making issue view'):
230 issue_view = tracker_views.IssueView(
231 issue, users_by_id, config,
232 open_related=open_related, closed_related=closed_related,
233 all_related={rel.issue_id: rel for rel in all_related})
234
235 with self.profiler.Phase('autolinker object lookup'):
236 all_ref_artifacts = self.services.autolink.GetAllReferencedArtifacts(
237 mr, [c.content for c in [initial_description] + comments])
238
239 with self.profiler.Phase('making comment views'):
240 reporter_auth = monorailrequest.AuthData.FromUserID(
241 mr.cnxn, initial_description.user_id, self.services)
242 desc_view = tracker_views.IssueCommentView(
243 mr.project_name, initial_description, users_by_id,
244 self.services.autolink, all_ref_artifacts, mr,
245 issue, effective_ids=reporter_auth.effective_ids)
246 # TODO(jrobbins): get effective_ids of each comment author, but
247 # that is too slow right now.
248 comment_views = [
249 tracker_views.IssueCommentView(
250 mr.project_name, c, users_by_id, self.services.autolink,
251 all_ref_artifacts, mr, issue)
252 for c in comments]
253
254 issue_view.flagged_spam = mr.auth.user_id in issue_reporters
255 if comment_reporters is not None:
256 for c in comment_views:
257 c.flagged_spam = mr.auth.user_id in comment_reporters.get(c.id, [])
258
259 return issue_view, desc_view, comment_views
260
261 def ProcessFormData(self, mr, post_data):
262 """Process the posted issue update form.
263
264 Args:
265 mr: commonly used info parsed from the request.
266 post_data: HTML form data from the request.
267
268 Returns:
269 String URL to redirect the user to, or None if response was already sent.
270 """
271 cmd = post_data.get('cmd', '')
272 send_email = 'send_email' in post_data
273 comment = post_data.get('comment', '')
274 slot_used = int(post_data.get('slot_used', 1))
275 page_generation_time = long(post_data['pagegen'])
276 issue = self._GetIssue(mr)
277 old_owner_id = tracker_bizobj.GetOwnerId(issue)
278 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
279
280 summary, status, owner_id, cc_ids, labels = commands.ParseQuickEditCommand(
281 mr.cnxn, cmd, issue, config, mr.auth.user_id, self.services)
282 component_ids = issue.component_ids # TODO(jrobbins): component commands
283 field_values = issue.field_values # TODO(jrobbins): edit custom fields
284
285 permit_edit = permissions.CanEditIssue(
286 mr.auth.effective_ids, mr.perms, mr.project, issue)
287 if not permit_edit:
288 raise permissions.PermissionException(
289 'User is not allowed to edit this issue')
290
291 amendments, _ = self.services.issue.ApplyIssueComment(
292 mr.cnxn, self.services, mr.auth.user_id,
293 mr.project_id, mr.local_id, summary, status, owner_id, cc_ids,
294 labels, field_values, component_ids, issue.blocked_on_iids,
295 issue.blocking_iids, issue.dangling_blocked_on_refs,
296 issue.dangling_blocking_refs, issue.merged_into,
297 page_gen_ts=page_generation_time, comment=comment)
298 self.services.project.UpdateRecentActivity(
299 mr.cnxn, mr.project.project_id)
300
301 if send_email:
302 if amendments or comment.strip():
303 cmnts = self.services.issue.GetCommentsForIssue(
304 mr.cnxn, issue.issue_id)
305 notify.PrepareAndSendIssueChangeNotification(
306 mr.project_id, mr.local_id, mr.request.host,
307 mr.auth.user_id, len(cmnts) - 1,
308 send_email=send_email, old_owner_id=old_owner_id)
309
310 # TODO(jrobbins): allow issue merge via quick-edit.
311
312 self.services.features.StoreRecentCommand(
313 mr.cnxn, mr.auth.user_id, mr.project_id, slot_used, cmd, comment)
314
315 # TODO(jrobbins): this is very similar to a block of code in issuebulkedit.
316 mr.can = int(post_data['can'])
317 mr.query = post_data.get('q', '')
318 mr.col_spec = post_data.get('colspec', '')
319 mr.sort_spec = post_data.get('sort', '')
320 mr.group_by_spec = post_data.get('groupby', '')
321 mr.start = int(post_data['start'])
322 mr.num = int(post_data['num'])
323 preview_issue_ref_str = '%s:%d' % (issue.project_name, issue.local_id)
324 return tracker_helpers.FormatIssueListURL(
325 mr, config, preview=preview_issue_ref_str, updated=mr.local_id,
326 ts=int(time.time()))
327
328
329 def PaginateComments(mr, issue, issuecomment_list, config):
330 """Filter and paginate the IssueComment PBs for the given issue.
331
332 Unlike most pagination, this one starts at the end of the whole
333 list so it shows only the most recent comments. The user can use
334 the "Older" and "Newer" links to page through older comments.
335
336 Args:
337 mr: common info parsed from the HTTP request.
338 issue: Issue PB for the issue being viewed.
339 issuecomment_list: list of IssueComment PBs for the viewed issue,
340 the zeroth item in this list is the initial issue description.
341 config: ProjectIssueConfig for the project that contains this issue.
342
343 Returns:
344 A tuple (description, visible_comments, pagination), where description
345 is the IssueComment for the initial issue description, visible_comments
346 is a list of IssueComment PBs for the comments that should be displayed
347 on the current pagination page, and pagination is a VirtualPagination
348 object that keeps track of the Older and Newer links.
349 """
350 if not issuecomment_list:
351 return None, [], None
352
353 description = issuecomment_list[0]
354 comments = issuecomment_list[1:]
355 allowed_comments = []
356 restrictions = permissions.GetRestrictions(issue)
357 granted_perms = tracker_bizobj.GetGrantedPerms(
358 issue, mr.auth.effective_ids, config)
359 for c in comments:
360 can_delete = permissions.CanDelete(
361 mr.auth.user_id, mr.auth.effective_ids, mr.perms, c.deleted_by,
362 c.user_id, mr.project, restrictions, granted_perms=granted_perms)
363 if can_delete or not c.deleted_by:
364 allowed_comments.append(c)
365
366 pagination_url = '%s?id=%d' % (urls.ISSUE_DETAIL, issue.local_id)
367 pagination = paginate.VirtualPagination(
368 mr, len(allowed_comments),
369 framework_constants.DEFAULT_COMMENTS_PER_PAGE,
370 list_page_url=pagination_url,
371 count_up=False, start_param='cstart', num_param='cnum',
372 max_num=settings.max_comments_per_page)
373 if pagination.last == 1 and pagination.start == len(allowed_comments):
374 pagination.visible = ezt.boolean(False)
375 visible_comments = allowed_comments[
376 pagination.last - 1:pagination.start]
377
378 return description, visible_comments, pagination
OLDNEW
« 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