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

Unified 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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « appengine/monorail/tracker/issueimport.py ('k') | appengine/monorail/tracker/issuelistcsv.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: appengine/monorail/tracker/issuelist.py
diff --git a/appengine/monorail/tracker/issuelist.py b/appengine/monorail/tracker/issuelist.py
new file mode 100644
index 0000000000000000000000000000000000000000..293c1da0ce16aac8703d3d0c2916f74acbee381b
--- /dev/null
+++ b/appengine/monorail/tracker/issuelist.py
@@ -0,0 +1,427 @@
+# 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
+
+"""Implementation of the issue list feature of the Monorail Issue Tracker.
+
+Summary of page classes:
+ IssueList: Shows a table of issue that satisfy search criteria.
+"""
+
+import logging
+from third_party import ezt
+
+import settings
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import grid_view_helpers
+from framework import permissions
+from framework import servlet
+from framework import sql
+from framework import table_view_helpers
+from framework import template_helpers
+from framework import urls
+from framework import xsrf
+from search import frontendsearchpipeline
+from search import searchpipeline
+from search import query2ast
+from services import issue_svc
+from tracker import tablecell
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+
+class IssueList(servlet.Servlet):
+ """IssueList shows a page with a list of issues (search results).
+
+ The issue list is actually a table with a configurable set of columns
+ that can be edited by the user.
+ """
+
+ _PAGE_TEMPLATE = 'tracker/issue-list-page.ezt'
+ _ELIMINATE_BLANK_LINES = True
+ _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
+ _DEFAULT_RESULTS_PER_PAGE = tracker_constants.DEFAULT_RESULTS_PER_PAGE
+
+ 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.
+ """
+ # Check if the user's query is just the ID of an existing issue.
+ # TODO(jrobbins): consider implementing this for cross-project search.
+ if mr.project and tracker_constants.JUMP_RE.match(mr.query):
+ local_id = int(mr.query)
+ try:
+ _issue = self.services.issue.GetIssueByLocalID(
+ mr.cnxn, mr.project_id, local_id) # does it exist?
+ url = framework_helpers.FormatAbsoluteURL(
+ mr, urls.ISSUE_DETAIL, id=local_id)
+ self.redirect(url, abort=True) # Jump to specified issue.
+ except issue_svc.NoSuchIssueException:
+ pass # The user is searching for a number that is not an issue ID.
+
+ with self.profiler.Phase('finishing config work'):
+ if mr.project_id:
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ else:
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(None)
+
+ with self.profiler.Phase('starting frontend search pipeline'):
+ pipeline = frontendsearchpipeline.FrontendSearchPipeline(
+ mr, self.services, self.profiler, self._DEFAULT_RESULTS_PER_PAGE)
+
+ # Perform promises that require authentication information.
+ with self.profiler.Phase('getting stars'):
+ starred_iid_set = _GetStarredIssues(
+ mr.cnxn, mr.auth.user_id, self.services)
+
+ with self.profiler.Phase('computing col_spec'):
+ mr.ComputeColSpec(config)
+
+ if not mr.errors.AnyErrors():
+ pipeline.SearchForIIDs()
+ pipeline.MergeAndSortIssues()
+ pipeline.Paginate()
+
+ with self.profiler.Phase('publishing emails'):
+ framework_views.RevealAllEmailsToMembers(mr, pipeline.users_by_id)
+ # TODO(jrobbins): get the configs for all result issues and
+ # harmonize them to get field defs including restrictions.
+
+ with self.profiler.Phase('getting related issues'):
+ related_iids = set()
+ if pipeline.grid_mode:
+ results_needing_related = pipeline.allowed_results or []
+ else:
+ results_needing_related = pipeline.visible_results or []
+ lower_cols = mr.col_spec.lower().split()
+ for issue in results_needing_related:
+ 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}
+
+ with self.profiler.Phase('building table/grid'):
+ if pipeline.grid_mode:
+ page_data = self.GetGridViewData(
+ mr, pipeline.allowed_results or [], config, pipeline.users_by_id,
+ starred_iid_set, pipeline.grid_limited)
+ else:
+ page_data = self.GetTableViewData(
+ mr, pipeline.visible_results or [], config, pipeline.users_by_id,
+ starred_iid_set, related_issues)
+
+ # We show a special message when no query will every produce any results
+ # because the project has no issues in it.
+ with self.profiler.Phase('starting stars promise'):
+ if mr.project_id:
+ project_has_any_issues = (
+ pipeline.allowed_results or
+ self.services.issue.GetHighestLocalID(mr.cnxn, mr.project_id) != 0)
+ else:
+ project_has_any_issues = True # Message only applies in a project.
+
+ with self.profiler.Phase('making page perms'):
+ page_perms = self.MakePagePerms(
+ mr, None,
+ permissions.SET_STAR,
+ permissions.CREATE_ISSUE,
+ permissions.EDIT_ISSUE)
+
+ # Update page data with variables that are shared between list and
+ # grid view.
+ page_data.update({
+ 'issue_tab_mode': 'issueList',
+ 'pagination': pipeline.pagination,
+ 'is_cross_project': ezt.boolean(len(pipeline.query_project_ids) != 1),
+ 'project_has_any_issues': ezt.boolean(project_has_any_issues),
+ 'colspec': mr.col_spec,
+ 'page_perms': page_perms,
+ 'grid_mode': ezt.boolean(pipeline.grid_mode),
+ 'panel_id': mr.panel_id,
+ 'set_star_token': xsrf.GenerateToken(
+ mr.auth.user_id, '/p/%s%s.do' % (
+ mr.project_name, urls.ISSUE_SETSTAR_JSON)),
+ 'is_missing_shards': ezt.boolean(len(pipeline.error_responses)),
+ 'missing_shard_count': len(pipeline.error_responses),
+ })
+
+ return page_data
+
+ def GetGridViewData(
+ self, mr, results, config, users_by_id, starred_iid_set, grid_limited):
+ """EZT template values to render a Grid View of issues.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ results: The Issue PBs that are the search results to be displayed.
+ config: The ProjectConfig PB for the project this view is in.
+ users_by_id: A dictionary {user_id: user_view,...} for all the users
+ involved in results.
+ starred_iid_set: Set of issues that the user has starred.
+ grid_limited: True if the results were limited to fit within the grid.
+
+ Returns:
+ Dictionary for EZT template rendering of the Grid View.
+ """
+ # We need ordered_columns because EZT loops have no loop-counter available.
+ # And, we use column number in the Javascript to hide/show columns.
+ columns = mr.col_spec.split()
+ ordered_columns = [template_helpers.EZTItem(col_index=i, name=col)
+ for i, col in enumerate(columns)]
+ unshown_columns = table_view_helpers.ComputeUnshownColumns(
+ results, columns, config, tracker_constants.OTHER_BUILT_IN_COLS)
+
+ grid_x_attr = (mr.x or config.default_x_attr or '--').lower()
+ grid_y_attr = (mr.y or config.default_y_attr or '--').lower()
+ all_label_values = {}
+ for art in results:
+ all_label_values[art.local_id] = (
+ grid_view_helpers.MakeLabelValuesDict(art))
+
+ if grid_x_attr == '--':
+ grid_x_headings = ['All']
+ else:
+ grid_x_items = table_view_helpers.ExtractUniqueValues(
+ [grid_x_attr], results, users_by_id, config)
+ grid_x_headings = grid_x_items[0].filter_values
+ if grid_view_helpers.AnyArtifactHasNoAttr(
+ results, grid_x_attr, users_by_id, all_label_values, config):
+ grid_x_headings.append(framework_constants.NO_VALUES)
+ grid_x_headings = grid_view_helpers.SortGridHeadings(
+ grid_x_attr, grid_x_headings, users_by_id, config,
+ tracker_helpers.SORTABLE_FIELDS)
+
+ if grid_y_attr == '--':
+ grid_y_headings = ['All']
+ else:
+ grid_y_items = table_view_helpers.ExtractUniqueValues(
+ [grid_y_attr], results, users_by_id, config)
+ grid_y_headings = grid_y_items[0].filter_values
+ if grid_view_helpers.AnyArtifactHasNoAttr(
+ results, grid_y_attr, users_by_id, all_label_values, config):
+ grid_y_headings.append(framework_constants.NO_VALUES)
+ grid_y_headings = grid_view_helpers.SortGridHeadings(
+ grid_y_attr, grid_y_headings, users_by_id, config,
+ tracker_helpers.SORTABLE_FIELDS)
+
+ logging.info('grid_x_headings = %s', grid_x_headings)
+ logging.info('grid_y_headings = %s', grid_y_headings)
+ grid_data = _MakeGridData(
+ results, mr.auth.user_id,
+ starred_iid_set, grid_x_attr, grid_x_headings,
+ grid_y_attr, grid_y_headings, users_by_id, all_label_values,
+ config)
+
+ grid_axis_choice_dict = {}
+ for oc in ordered_columns:
+ grid_axis_choice_dict[oc.name] = True
+ for uc in unshown_columns:
+ grid_axis_choice_dict[uc] = True
+ for bad_axis in tracker_constants.NOT_USED_IN_GRID_AXES:
+ if bad_axis in grid_axis_choice_dict:
+ del grid_axis_choice_dict[bad_axis]
+ grid_axis_choices = grid_axis_choice_dict.keys()
+ grid_axis_choices.sort()
+
+ grid_cell_mode = mr.cells
+ if len(results) > settings.max_tiles_in_grid and mr.cells == 'tiles':
+ grid_cell_mode = 'ids'
+
+ grid_view_data = {
+ 'grid_limited': ezt.boolean(grid_limited),
+ 'grid_shown': len(results),
+ 'grid_x_headings': grid_x_headings,
+ 'grid_y_headings': grid_y_headings,
+ 'grid_data': grid_data,
+ 'grid_axis_choices': grid_axis_choices,
+ 'grid_cell_mode': grid_cell_mode,
+ 'results': results, # Really only useful in if-any.
+ }
+ return grid_view_data
+
+ def GetCellFactories(self):
+ return tablecell.CELL_FACTORIES
+
+ def GetTableViewData(
+ self, mr, results, config, users_by_id, starred_iid_set, related_issues):
+ """EZT template values to render a Table View of issues.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ results: list of Issue PBs for the search results to be displayed.
+ config: The ProjectIssueConfig PB for the current project.
+ users_by_id: A dictionary {user_id: UserView} for all the users
+ involved in results.
+ starred_iid_set: Set of issues that the user has starred.
+ related_issues: dict {issue_id: issue} of pre-fetched related issues.
+
+ Returns:
+ Dictionary of page data for rendering of the Table View.
+ """
+ # We need ordered_columns because EZT loops have no loop-counter available.
+ # And, we use column number in the Javascript to hide/show columns.
+ columns = mr.col_spec.split()
+ ordered_columns = [template_helpers.EZTItem(col_index=i, name=col)
+ for i, col in enumerate(columns)]
+ unshown_columns = table_view_helpers.ComputeUnshownColumns(
+ results, columns, config, tracker_constants.OTHER_BUILT_IN_COLS)
+
+ lower_columns = mr.col_spec.lower().split()
+ lower_group_by = mr.group_by_spec.lower().split()
+ table_data = _MakeTableData(
+ results, mr.auth.user_id,
+ starred_iid_set, lower_columns, lower_group_by,
+ users_by_id, self.GetCellFactories(), related_issues, config)
+
+ # Used to offer easy filtering of each unique value in each column.
+ column_values = table_view_helpers.ExtractUniqueValues(
+ lower_columns, results, users_by_id, config)
+
+ table_view_data = {
+ 'table_data': table_data,
+ 'column_values': column_values,
+ # Put ordered_columns inside a list of exactly 1 panel so that
+ # it can work the same as the dashboard initial panel list headers.
+ 'panels': [template_helpers.EZTItem(ordered_columns=ordered_columns)],
+ 'unshown_columns': unshown_columns,
+ 'cursor': mr.cursor or mr.preview,
+ 'preview': mr.preview,
+ 'default_colspec': tracker_constants.DEFAULT_COL_SPEC,
+ 'default_results_per_page': tracker_constants.DEFAULT_RESULTS_PER_PAGE,
+ 'csv_link': framework_helpers.FormatURL(mr, 'csv'),
+ 'preview_on_hover': ezt.boolean(
+ _ShouldPreviewOnHover(mr.auth.user_pb)),
+ }
+ return table_view_data
+
+ def GatherHelpData(self, mr, page_data):
+ """Return a dict of values to drive on-page user help.
+
+ Args:
+ mr: common information parsed from the HTTP request.
+ page_data: Dictionary of base and page template data.
+
+ Returns:
+ A dict of values to drive on-page user help, to be added to page_data.
+ """
+ cue = None
+ dismissed = []
+ if mr.auth.user_pb:
+ dismissed = mr.auth.user_pb.dismissed_cues
+
+ if mr.project_id:
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ else:
+ config = tracker_bizobj.MakeDefaultProjectIssueConfig(None)
+
+ try:
+ _query_ast, is_fulltext_query = searchpipeline.ParseQuery(
+ mr, config, self.services)
+ except query2ast.InvalidQueryError:
+ is_fulltext_query = False
+
+ if mr.mode == 'grid' and mr.cells == 'tiles':
+ if len(page_data.get('results', [])) > settings.max_tiles_in_grid:
+ if 'showing_ids_instead_of_tiles' not in dismissed:
+ cue = 'showing_ids_instead_of_tiles'
+
+ if self.CheckPerm(mr, permissions.EDIT_ISSUE):
+ if ('italics_mean_derived' not in dismissed and
+ _AnyDerivedValues(page_data.get('table_data', []))):
+ cue = 'italics_mean_derived'
+ elif 'dit_keystrokes' not in dismissed and mr.mode != 'grid':
+ cue = 'dit_keystrokes'
+ elif 'stale_fulltext' not in dismissed and is_fulltext_query:
+ cue = 'stale_fulltext'
+
+ return {
+ 'cue': cue,
+ }
+
+
+def _AnyDerivedValues(table_data):
+ """Return True if any value in the given table_data was derived."""
+ for row in table_data:
+ for cell in row.cells:
+ for item in cell.values:
+ if item.is_derived:
+ return True
+
+ return False
+
+
+def _MakeTableData(
+ visible_results, logged_in_user_id, starred_iid_set,
+ lower_columns, lower_group_by, users_by_id, cell_factories,
+ related_issues, config):
+ """Return a list of list row objects for display by EZT."""
+ table_data = table_view_helpers.MakeTableData(
+ visible_results, logged_in_user_id, starred_iid_set,
+ lower_columns, lower_group_by, users_by_id, cell_factories,
+ lambda issue: issue.issue_id, related_issues, config)
+
+ for row, art in zip(table_data, visible_results):
+ row.local_id = art.local_id
+ row.project_name = art.project_name
+ row.issue_ref = '%s:%d' % (art.project_name, art.local_id)
+ row.issue_url = tracker_helpers.FormatRelativeIssueURL(
+ art.project_name, urls.ISSUE_DETAIL, id=art.local_id)
+
+ return table_data
+
+
+def _MakeGridData(
+ allowed_results, _logged_in_user_id, starred_iid_set, x_attr,
+ grid_col_values, y_attr, grid_row_values, users_by_id, all_label_values,
+ config):
+ """Return all data needed for EZT to render the body of the grid view."""
+
+ def IssueViewFactory(issue):
+ return template_helpers.EZTItem(
+ summary=issue.summary, local_id=issue.local_id, issue_id=issue.issue_id,
+ status=issue.status or issue.derived_status, starred=None)
+
+ grid_data = grid_view_helpers.MakeGridData(
+ allowed_results, x_attr, grid_col_values, y_attr, grid_row_values,
+ users_by_id, IssueViewFactory, all_label_values, config)
+ for grid_row in grid_data:
+ for grid_cell in grid_row.cells_in_row:
+ for tile in grid_cell.tiles:
+ if tile.issue_id in starred_iid_set:
+ tile.starred = ezt.boolean(True)
+
+ return grid_data
+
+
+def _GetStarredIssues(cnxn, logged_in_user_id, services):
+ """Get the set of issues that the logged in user has starred."""
+ starred_iids = services.issue_star.LookupStarredItemIDs(
+ cnxn, logged_in_user_id)
+ return set(starred_iids)
+
+
+def _ShouldPreviewOnHover(user):
+ """Return true if we should show the issue preview when the user hovers.
+
+ Args:
+ user: User PB for the currently signed in user.
+
+ Returns:
+ True if the preview (peek) should open on hover over the issue ID.
+ """
+ return settings.enable_quick_edit and user.preview_on_hover
« 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