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

Side by Side Diff: appengine/monorail/tracker/issuelist.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/issueimport.py ('k') | appengine/monorail/tracker/issuelistcsv.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 """Implementation of the issue list feature of the Monorail Issue Tracker.
7
8 Summary of page classes:
9 IssueList: Shows a table of issue that satisfy search criteria.
10 """
11
12 import logging
13 from third_party import ezt
14
15 import settings
16 from framework import framework_constants
17 from framework import framework_helpers
18 from framework import framework_views
19 from framework import grid_view_helpers
20 from framework import permissions
21 from framework import servlet
22 from framework import sql
23 from framework import table_view_helpers
24 from framework import template_helpers
25 from framework import urls
26 from framework import xsrf
27 from search import frontendsearchpipeline
28 from search import searchpipeline
29 from search import query2ast
30 from services import issue_svc
31 from tracker import tablecell
32 from tracker import tracker_bizobj
33 from tracker import tracker_constants
34 from tracker import tracker_helpers
35 from tracker import tracker_views
36
37
38 class IssueList(servlet.Servlet):
39 """IssueList shows a page with a list of issues (search results).
40
41 The issue list is actually a table with a configurable set of columns
42 that can be edited by the user.
43 """
44
45 _PAGE_TEMPLATE = 'tracker/issue-list-page.ezt'
46 _ELIMINATE_BLANK_LINES = True
47 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
48 _DEFAULT_RESULTS_PER_PAGE = tracker_constants.DEFAULT_RESULTS_PER_PAGE
49
50 def GatherPageData(self, mr):
51 """Build up a dictionary of data values to use when rendering the page.
52
53 Args:
54 mr: commonly used info parsed from the request.
55
56 Returns:
57 Dict of values used by EZT for rendering the page.
58 """
59 # Check if the user's query is just the ID of an existing issue.
60 # TODO(jrobbins): consider implementing this for cross-project search.
61 if mr.project and tracker_constants.JUMP_RE.match(mr.query):
62 local_id = int(mr.query)
63 try:
64 _issue = self.services.issue.GetIssueByLocalID(
65 mr.cnxn, mr.project_id, local_id) # does it exist?
66 url = framework_helpers.FormatAbsoluteURL(
67 mr, urls.ISSUE_DETAIL, id=local_id)
68 self.redirect(url, abort=True) # Jump to specified issue.
69 except issue_svc.NoSuchIssueException:
70 pass # The user is searching for a number that is not an issue ID.
71
72 with self.profiler.Phase('finishing config work'):
73 if mr.project_id:
74 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
75 else:
76 config = tracker_bizobj.MakeDefaultProjectIssueConfig(None)
77
78 with self.profiler.Phase('starting frontend search pipeline'):
79 pipeline = frontendsearchpipeline.FrontendSearchPipeline(
80 mr, self.services, self.profiler, self._DEFAULT_RESULTS_PER_PAGE)
81
82 # Perform promises that require authentication information.
83 with self.profiler.Phase('getting stars'):
84 starred_iid_set = _GetStarredIssues(
85 mr.cnxn, mr.auth.user_id, self.services)
86
87 with self.profiler.Phase('computing col_spec'):
88 mr.ComputeColSpec(config)
89
90 if not mr.errors.AnyErrors():
91 pipeline.SearchForIIDs()
92 pipeline.MergeAndSortIssues()
93 pipeline.Paginate()
94
95 with self.profiler.Phase('publishing emails'):
96 framework_views.RevealAllEmailsToMembers(mr, pipeline.users_by_id)
97 # TODO(jrobbins): get the configs for all result issues and
98 # harmonize them to get field defs including restrictions.
99
100 with self.profiler.Phase('getting related issues'):
101 related_iids = set()
102 if pipeline.grid_mode:
103 results_needing_related = pipeline.allowed_results or []
104 else:
105 results_needing_related = pipeline.visible_results or []
106 lower_cols = mr.col_spec.lower().split()
107 for issue in results_needing_related:
108 if 'blockedon' in lower_cols:
109 related_iids.update(issue.blocked_on_iids)
110 if 'blocking' in lower_cols:
111 related_iids.update(issue.blocking_iids)
112 if 'mergedinto' in lower_cols:
113 related_iids.add(issue.merged_into)
114 related_issues_list = self.services.issue.GetIssues(
115 mr.cnxn, list(related_iids))
116 related_issues = {issue.issue_id: issue for issue in related_issues_list}
117
118 with self.profiler.Phase('building table/grid'):
119 if pipeline.grid_mode:
120 page_data = self.GetGridViewData(
121 mr, pipeline.allowed_results or [], config, pipeline.users_by_id,
122 starred_iid_set, pipeline.grid_limited)
123 else:
124 page_data = self.GetTableViewData(
125 mr, pipeline.visible_results or [], config, pipeline.users_by_id,
126 starred_iid_set, related_issues)
127
128 # We show a special message when no query will every produce any results
129 # because the project has no issues in it.
130 with self.profiler.Phase('starting stars promise'):
131 if mr.project_id:
132 project_has_any_issues = (
133 pipeline.allowed_results or
134 self.services.issue.GetHighestLocalID(mr.cnxn, mr.project_id) != 0)
135 else:
136 project_has_any_issues = True # Message only applies in a project.
137
138 with self.profiler.Phase('making page perms'):
139 page_perms = self.MakePagePerms(
140 mr, None,
141 permissions.SET_STAR,
142 permissions.CREATE_ISSUE,
143 permissions.EDIT_ISSUE)
144
145 # Update page data with variables that are shared between list and
146 # grid view.
147 page_data.update({
148 'issue_tab_mode': 'issueList',
149 'pagination': pipeline.pagination,
150 'is_cross_project': ezt.boolean(len(pipeline.query_project_ids) != 1),
151 'project_has_any_issues': ezt.boolean(project_has_any_issues),
152 'colspec': mr.col_spec,
153 'page_perms': page_perms,
154 'grid_mode': ezt.boolean(pipeline.grid_mode),
155 'panel_id': mr.panel_id,
156 'set_star_token': xsrf.GenerateToken(
157 mr.auth.user_id, '/p/%s%s.do' % (
158 mr.project_name, urls.ISSUE_SETSTAR_JSON)),
159 'is_missing_shards': ezt.boolean(len(pipeline.error_responses)),
160 'missing_shard_count': len(pipeline.error_responses),
161 })
162
163 return page_data
164
165 def GetGridViewData(
166 self, mr, results, config, users_by_id, starred_iid_set, grid_limited):
167 """EZT template values to render a Grid View of issues.
168
169 Args:
170 mr: commonly used info parsed from the request.
171 results: The Issue PBs that are the search results to be displayed.
172 config: The ProjectConfig PB for the project this view is in.
173 users_by_id: A dictionary {user_id: user_view,...} for all the users
174 involved in results.
175 starred_iid_set: Set of issues that the user has starred.
176 grid_limited: True if the results were limited to fit within the grid.
177
178 Returns:
179 Dictionary for EZT template rendering of the Grid View.
180 """
181 # We need ordered_columns because EZT loops have no loop-counter available.
182 # And, we use column number in the Javascript to hide/show columns.
183 columns = mr.col_spec.split()
184 ordered_columns = [template_helpers.EZTItem(col_index=i, name=col)
185 for i, col in enumerate(columns)]
186 unshown_columns = table_view_helpers.ComputeUnshownColumns(
187 results, columns, config, tracker_constants.OTHER_BUILT_IN_COLS)
188
189 grid_x_attr = (mr.x or config.default_x_attr or '--').lower()
190 grid_y_attr = (mr.y or config.default_y_attr or '--').lower()
191 all_label_values = {}
192 for art in results:
193 all_label_values[art.local_id] = (
194 grid_view_helpers.MakeLabelValuesDict(art))
195
196 if grid_x_attr == '--':
197 grid_x_headings = ['All']
198 else:
199 grid_x_items = table_view_helpers.ExtractUniqueValues(
200 [grid_x_attr], results, users_by_id, config)
201 grid_x_headings = grid_x_items[0].filter_values
202 if grid_view_helpers.AnyArtifactHasNoAttr(
203 results, grid_x_attr, users_by_id, all_label_values, config):
204 grid_x_headings.append(framework_constants.NO_VALUES)
205 grid_x_headings = grid_view_helpers.SortGridHeadings(
206 grid_x_attr, grid_x_headings, users_by_id, config,
207 tracker_helpers.SORTABLE_FIELDS)
208
209 if grid_y_attr == '--':
210 grid_y_headings = ['All']
211 else:
212 grid_y_items = table_view_helpers.ExtractUniqueValues(
213 [grid_y_attr], results, users_by_id, config)
214 grid_y_headings = grid_y_items[0].filter_values
215 if grid_view_helpers.AnyArtifactHasNoAttr(
216 results, grid_y_attr, users_by_id, all_label_values, config):
217 grid_y_headings.append(framework_constants.NO_VALUES)
218 grid_y_headings = grid_view_helpers.SortGridHeadings(
219 grid_y_attr, grid_y_headings, users_by_id, config,
220 tracker_helpers.SORTABLE_FIELDS)
221
222 logging.info('grid_x_headings = %s', grid_x_headings)
223 logging.info('grid_y_headings = %s', grid_y_headings)
224 grid_data = _MakeGridData(
225 results, mr.auth.user_id,
226 starred_iid_set, grid_x_attr, grid_x_headings,
227 grid_y_attr, grid_y_headings, users_by_id, all_label_values,
228 config)
229
230 grid_axis_choice_dict = {}
231 for oc in ordered_columns:
232 grid_axis_choice_dict[oc.name] = True
233 for uc in unshown_columns:
234 grid_axis_choice_dict[uc] = True
235 for bad_axis in tracker_constants.NOT_USED_IN_GRID_AXES:
236 if bad_axis in grid_axis_choice_dict:
237 del grid_axis_choice_dict[bad_axis]
238 grid_axis_choices = grid_axis_choice_dict.keys()
239 grid_axis_choices.sort()
240
241 grid_cell_mode = mr.cells
242 if len(results) > settings.max_tiles_in_grid and mr.cells == 'tiles':
243 grid_cell_mode = 'ids'
244
245 grid_view_data = {
246 'grid_limited': ezt.boolean(grid_limited),
247 'grid_shown': len(results),
248 'grid_x_headings': grid_x_headings,
249 'grid_y_headings': grid_y_headings,
250 'grid_data': grid_data,
251 'grid_axis_choices': grid_axis_choices,
252 'grid_cell_mode': grid_cell_mode,
253 'results': results, # Really only useful in if-any.
254 }
255 return grid_view_data
256
257 def GetCellFactories(self):
258 return tablecell.CELL_FACTORIES
259
260 def GetTableViewData(
261 self, mr, results, config, users_by_id, starred_iid_set, related_issues):
262 """EZT template values to render a Table View of issues.
263
264 Args:
265 mr: commonly used info parsed from the request.
266 results: list of Issue PBs for the search results to be displayed.
267 config: The ProjectIssueConfig PB for the current project.
268 users_by_id: A dictionary {user_id: UserView} for all the users
269 involved in results.
270 starred_iid_set: Set of issues that the user has starred.
271 related_issues: dict {issue_id: issue} of pre-fetched related issues.
272
273 Returns:
274 Dictionary of page data for rendering of the Table View.
275 """
276 # We need ordered_columns because EZT loops have no loop-counter available.
277 # And, we use column number in the Javascript to hide/show columns.
278 columns = mr.col_spec.split()
279 ordered_columns = [template_helpers.EZTItem(col_index=i, name=col)
280 for i, col in enumerate(columns)]
281 unshown_columns = table_view_helpers.ComputeUnshownColumns(
282 results, columns, config, tracker_constants.OTHER_BUILT_IN_COLS)
283
284 lower_columns = mr.col_spec.lower().split()
285 lower_group_by = mr.group_by_spec.lower().split()
286 table_data = _MakeTableData(
287 results, mr.auth.user_id,
288 starred_iid_set, lower_columns, lower_group_by,
289 users_by_id, self.GetCellFactories(), related_issues, config)
290
291 # Used to offer easy filtering of each unique value in each column.
292 column_values = table_view_helpers.ExtractUniqueValues(
293 lower_columns, results, users_by_id, config)
294
295 table_view_data = {
296 'table_data': table_data,
297 'column_values': column_values,
298 # Put ordered_columns inside a list of exactly 1 panel so that
299 # it can work the same as the dashboard initial panel list headers.
300 'panels': [template_helpers.EZTItem(ordered_columns=ordered_columns)],
301 'unshown_columns': unshown_columns,
302 'cursor': mr.cursor or mr.preview,
303 'preview': mr.preview,
304 'default_colspec': tracker_constants.DEFAULT_COL_SPEC,
305 'default_results_per_page': tracker_constants.DEFAULT_RESULTS_PER_PAGE,
306 'csv_link': framework_helpers.FormatURL(mr, 'csv'),
307 'preview_on_hover': ezt.boolean(
308 _ShouldPreviewOnHover(mr.auth.user_pb)),
309 }
310 return table_view_data
311
312 def GatherHelpData(self, mr, page_data):
313 """Return a dict of values to drive on-page user help.
314
315 Args:
316 mr: common information parsed from the HTTP request.
317 page_data: Dictionary of base and page template data.
318
319 Returns:
320 A dict of values to drive on-page user help, to be added to page_data.
321 """
322 cue = None
323 dismissed = []
324 if mr.auth.user_pb:
325 dismissed = mr.auth.user_pb.dismissed_cues
326
327 if mr.project_id:
328 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
329 else:
330 config = tracker_bizobj.MakeDefaultProjectIssueConfig(None)
331
332 try:
333 _query_ast, is_fulltext_query = searchpipeline.ParseQuery(
334 mr, config, self.services)
335 except query2ast.InvalidQueryError:
336 is_fulltext_query = False
337
338 if mr.mode == 'grid' and mr.cells == 'tiles':
339 if len(page_data.get('results', [])) > settings.max_tiles_in_grid:
340 if 'showing_ids_instead_of_tiles' not in dismissed:
341 cue = 'showing_ids_instead_of_tiles'
342
343 if self.CheckPerm(mr, permissions.EDIT_ISSUE):
344 if ('italics_mean_derived' not in dismissed and
345 _AnyDerivedValues(page_data.get('table_data', []))):
346 cue = 'italics_mean_derived'
347 elif 'dit_keystrokes' not in dismissed and mr.mode != 'grid':
348 cue = 'dit_keystrokes'
349 elif 'stale_fulltext' not in dismissed and is_fulltext_query:
350 cue = 'stale_fulltext'
351
352 return {
353 'cue': cue,
354 }
355
356
357 def _AnyDerivedValues(table_data):
358 """Return True if any value in the given table_data was derived."""
359 for row in table_data:
360 for cell in row.cells:
361 for item in cell.values:
362 if item.is_derived:
363 return True
364
365 return False
366
367
368 def _MakeTableData(
369 visible_results, logged_in_user_id, starred_iid_set,
370 lower_columns, lower_group_by, users_by_id, cell_factories,
371 related_issues, config):
372 """Return a list of list row objects for display by EZT."""
373 table_data = table_view_helpers.MakeTableData(
374 visible_results, logged_in_user_id, starred_iid_set,
375 lower_columns, lower_group_by, users_by_id, cell_factories,
376 lambda issue: issue.issue_id, related_issues, config)
377
378 for row, art in zip(table_data, visible_results):
379 row.local_id = art.local_id
380 row.project_name = art.project_name
381 row.issue_ref = '%s:%d' % (art.project_name, art.local_id)
382 row.issue_url = tracker_helpers.FormatRelativeIssueURL(
383 art.project_name, urls.ISSUE_DETAIL, id=art.local_id)
384
385 return table_data
386
387
388 def _MakeGridData(
389 allowed_results, _logged_in_user_id, starred_iid_set, x_attr,
390 grid_col_values, y_attr, grid_row_values, users_by_id, all_label_values,
391 config):
392 """Return all data needed for EZT to render the body of the grid view."""
393
394 def IssueViewFactory(issue):
395 return template_helpers.EZTItem(
396 summary=issue.summary, local_id=issue.local_id, issue_id=issue.issue_id,
397 status=issue.status or issue.derived_status, starred=None)
398
399 grid_data = grid_view_helpers.MakeGridData(
400 allowed_results, x_attr, grid_col_values, y_attr, grid_row_values,
401 users_by_id, IssueViewFactory, all_label_values, config)
402 for grid_row in grid_data:
403 for grid_cell in grid_row.cells_in_row:
404 for tile in grid_cell.tiles:
405 if tile.issue_id in starred_iid_set:
406 tile.starred = ezt.boolean(True)
407
408 return grid_data
409
410
411 def _GetStarredIssues(cnxn, logged_in_user_id, services):
412 """Get the set of issues that the logged in user has starred."""
413 starred_iids = services.issue_star.LookupStarredItemIDs(
414 cnxn, logged_in_user_id)
415 return set(starred_iids)
416
417
418 def _ShouldPreviewOnHover(user):
419 """Return true if we should show the issue preview when the user hovers.
420
421 Args:
422 user: User PB for the currently signed in user.
423
424 Returns:
425 True if the preview (peek) should open on hover over the issue ID.
426 """
427 return settings.enable_quick_edit and user.preview_on_hover
OLDNEW
« no previous file with comments | « appengine/monorail/tracker/issueimport.py ('k') | appengine/monorail/tracker/issuelistcsv.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698