Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 1 | # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 | # Use of this source code is governed 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 hotlistissues page and related forms.""" |
| 7 | from __future__ import print_function |
| 8 | from __future__ import division |
| 9 | from __future__ import absolute_import |
| 10 | |
| 11 | import logging |
| 12 | import ezt |
| 13 | |
| 14 | import settings |
| 15 | import time |
| 16 | import re |
| 17 | |
| 18 | from businesslogic import work_env |
| 19 | from features import features_bizobj |
| 20 | from features import features_constants |
| 21 | from features import hotlist_helpers |
| 22 | from framework import exceptions |
| 23 | from framework import servlet |
| 24 | from framework import sorting |
| 25 | from framework import permissions |
| 26 | from framework import framework_helpers |
| 27 | from framework import paginate |
| 28 | from framework import framework_constants |
| 29 | from framework import framework_views |
| 30 | from framework import grid_view_helpers |
| 31 | from framework import template_helpers |
| 32 | from framework import timestr |
| 33 | from framework import urls |
| 34 | from framework import xsrf |
| 35 | from services import features_svc |
| 36 | from tracker import tracker_bizobj |
| 37 | |
| 38 | _INITIAL_ADD_ISSUES_MESSAGE = 'projectname:localID, projectname:localID, etc.' |
| 39 | _MSG_INVALID_ISSUES_INPUT = ( |
| 40 | 'Please follow project_name:issue_id, project_name:issue_id..') |
| 41 | _MSG_ISSUES_NOT_FOUND = 'One or more of your issues were not found.' |
| 42 | _MSG_ISSUES_NOT_VIEWABLE = 'You lack permission to view one or more issues.' |
| 43 | |
| 44 | |
| 45 | class HotlistIssues(servlet.Servlet): |
| 46 | """HotlistIssues is a page that shows the issues of one hotlist.""" |
| 47 | |
| 48 | _PAGE_TEMPLATE = 'features/hotlist-issues-page.ezt' |
| 49 | _MAIN_TAB_MODE = servlet.Servlet.HOTLIST_TAB_ISSUES |
| 50 | |
| 51 | def AssertBasePermission(self, mr): |
| 52 | """Check that the user has permission to even visit this page.""" |
| 53 | super(HotlistIssues, self).AssertBasePermission(mr) |
| 54 | try: |
| 55 | hotlist = self._GetHotlist(mr) |
| 56 | except features_svc.NoSuchHotlistException: |
| 57 | return |
| 58 | permit_view = permissions.CanViewHotlist( |
| 59 | mr.auth.effective_ids, mr.perms, hotlist) |
| 60 | if not permit_view: |
| 61 | raise permissions.PermissionException( |
| 62 | 'User is not allowed to view this hotlist') |
| 63 | |
| 64 | def GatherPageData(self, mr): |
| 65 | """Build up a dictionary of data values to use when rendering the page. |
| 66 | |
| 67 | Args: |
| 68 | mr: commonly usef info parsed from the request. |
| 69 | |
| 70 | Returns: |
| 71 | Dict of values used by EZT for rendering the page. |
| 72 | """ |
| 73 | with mr.profiler.Phase('getting hotlist'): |
| 74 | if mr.hotlist_id is None: |
| 75 | self.abort(404, 'no hotlist specified') |
| 76 | if mr.auth.user_id: |
| 77 | self.services.user.AddVisitedHotlist( |
| 78 | mr.cnxn, mr.auth.user_id, mr.hotlist_id) |
| 79 | |
| 80 | if mr.mode == 'grid': |
| 81 | page_data = self.GetGridViewData(mr) |
| 82 | else: |
| 83 | page_data = self.GetTableViewData(mr) |
| 84 | |
| 85 | with mr.profiler.Phase('making page perms'): |
| 86 | owner_permissions = permissions.CanAdministerHotlist( |
| 87 | mr.auth.effective_ids, mr.perms, mr.hotlist) |
| 88 | editor_permissions = permissions.CanEditHotlist( |
| 89 | mr.auth.effective_ids, mr.perms, mr.hotlist) |
| 90 | # TODO(jojwang): each issue should have an individual |
| 91 | # SetStar status based on its project to indicate whether or not |
| 92 | # the star icon should be shown to the user. |
| 93 | page_perms = template_helpers.EZTItem( |
| 94 | EditIssue=None, SetStar=mr.auth.user_id) |
| 95 | |
| 96 | allow_rerank = (not mr.group_by_spec and mr.sort_spec.startswith( |
| 97 | 'rank') and (owner_permissions or editor_permissions)) |
| 98 | |
| 99 | user_hotlists = self.services.features.GetHotlistsByUserID( |
| 100 | mr.cnxn, mr.auth.user_id) |
| 101 | try: |
| 102 | user_hotlists.remove(self.services.features.GetHotlist( |
| 103 | mr.cnxn, mr.hotlist_id)) |
| 104 | except ValueError: |
| 105 | pass |
| 106 | |
| 107 | new_ui_url = '%s/%s/issues' % (urls.HOTLISTS, mr.hotlist_id) |
| 108 | |
| 109 | # Note: The HotlistView is created and returned in servlet.py |
| 110 | page_data.update( |
| 111 | { |
| 112 | 'owner_permissions': |
| 113 | ezt.boolean(owner_permissions), |
| 114 | 'editor_permissions': |
| 115 | ezt.boolean(editor_permissions), |
| 116 | 'issue_tab_mode': |
| 117 | 'issueList', |
| 118 | 'grid_mode': |
| 119 | ezt.boolean(mr.mode == 'grid'), |
| 120 | 'list_mode': |
| 121 | ezt.boolean(mr.mode == 'list'), |
| 122 | 'chart_mode': |
| 123 | ezt.boolean(mr.mode == 'chart'), |
| 124 | 'page_perms': |
| 125 | page_perms, |
| 126 | 'colspec': |
| 127 | mr.col_spec, |
| 128 | # monorail:6336, used in <ezt-show-columns-connector> |
| 129 | 'phasespec': |
| 130 | "", |
| 131 | 'allow_rerank': |
| 132 | ezt.boolean(allow_rerank), |
| 133 | 'csv_link': |
| 134 | framework_helpers.FormatURL( |
| 135 | [ |
| 136 | (name, mr.GetParam(name)) |
| 137 | for name in framework_helpers.RECOGNIZED_PARAMS |
| 138 | ], |
| 139 | '%d/csv' % mr.hotlist_id, |
| 140 | num=100), |
| 141 | 'is_hotlist': |
| 142 | ezt.boolean(True), |
| 143 | 'col_spec': |
| 144 | mr.col_spec.lower(), |
| 145 | 'viewing_user_page': |
| 146 | ezt.boolean(True), |
| 147 | # for update-issues-hotlists-dialog in |
| 148 | # issue-list-controls-top. |
| 149 | 'user_issue_hotlists': [], |
| 150 | 'user_remaining_hotlists': |
| 151 | user_hotlists, |
| 152 | 'new_ui_url': |
| 153 | new_ui_url, |
| 154 | }) |
| 155 | return page_data |
| 156 | # TODO(jojwang): implement peek issue on hover, implement starring issues |
| 157 | |
| 158 | def _GetHotlist(self, mr): |
| 159 | """Retrieve the current hotlist.""" |
| 160 | if mr.hotlist_id is None: |
| 161 | return None |
| 162 | try: |
| 163 | hotlist = self.services.features.GetHotlist(mr.cnxn, mr.hotlist_id) |
| 164 | except features_svc.NoSuchHotlistException: |
| 165 | self.abort(404, 'hotlist not found') |
| 166 | return hotlist |
| 167 | |
| 168 | def GetTableViewData(self, mr): |
| 169 | """EZT template values to render a Table View of issues. |
| 170 | |
| 171 | Args: |
| 172 | mr: commonly used info parsed from the request. |
| 173 | |
| 174 | Returns: |
| 175 | Dictionary of page data for rendering of the Table View. |
| 176 | """ |
| 177 | table_data, table_related_dict = hotlist_helpers.CreateHotlistTableData( |
| 178 | mr, mr.hotlist.items, self.services) |
| 179 | columns = mr.col_spec.split() |
| 180 | ordered_columns = [template_helpers.EZTItem(col_index=i, name=col) |
| 181 | for i, col in enumerate(columns)] |
| 182 | table_view_data = { |
| 183 | 'table_data': table_data, |
| 184 | 'panels': [template_helpers.EZTItem(ordered_columns=ordered_columns)], |
| 185 | 'cursor': mr.cursor or mr.preview, |
| 186 | 'preview': mr.preview, |
| 187 | 'default_colspec': features_constants.DEFAULT_COL_SPEC, |
| 188 | 'default_results_per_page': 10, |
| 189 | 'preview_on_hover': ( |
| 190 | settings.enable_quick_edit and mr.auth.user_pb.preview_on_hover), |
| 191 | # token must be generated using url with userid to accommodate |
| 192 | # multiple urls for one hotlist |
| 193 | 'edit_hotlist_token': xsrf.GenerateToken( |
| 194 | mr.auth.user_id, |
| 195 | hotlist_helpers.GetURLOfHotlist( |
| 196 | mr.cnxn, mr.hotlist, self.services.user, |
| 197 | url_for_token=True) + '.do'), |
| 198 | 'add_local_ids': '', |
| 199 | 'placeholder': _INITIAL_ADD_ISSUES_MESSAGE, |
| 200 | 'add_issues_selected': ezt.boolean(False), |
| 201 | 'col_spec': '' |
| 202 | } |
| 203 | table_view_data.update(table_related_dict) |
| 204 | |
| 205 | return table_view_data |
| 206 | |
| 207 | def ProcessFormData(self, mr, post_data): |
| 208 | if not permissions.CanEditHotlist( |
| 209 | mr.auth.effective_ids, mr.perms, mr.hotlist): |
| 210 | raise permissions.PermissionException( |
| 211 | 'User is not allowed to edit this hotlist.') |
| 212 | |
| 213 | hotlist_view_url = hotlist_helpers.GetURLOfHotlist( |
| 214 | mr.cnxn, mr.hotlist, self.services.user) |
| 215 | current_col_spec = post_data.get('current_col_spec') |
| 216 | default_url = framework_helpers.FormatAbsoluteURL( |
| 217 | mr, hotlist_view_url, |
| 218 | include_project=False, colspec=current_col_spec) |
| 219 | sorting.InvalidateArtValuesKeys( |
| 220 | mr.cnxn, |
| 221 | [hotlist_item.issue_id for hotlist_item |
| 222 | in mr.hotlist.items]) |
| 223 | |
| 224 | if post_data.get('remove') == 'true': |
| 225 | project_and_local_ids = post_data.get('remove_local_ids') |
| 226 | else: |
| 227 | project_and_local_ids = post_data.get('add_local_ids') |
| 228 | if not project_and_local_ids: |
| 229 | return default_url |
| 230 | |
| 231 | selected_iids = [] |
| 232 | if project_and_local_ids: |
| 233 | pattern = re.compile(features_constants.ISSUE_INPUT_REGEX) |
| 234 | if pattern.match(project_and_local_ids): |
| 235 | issue_refs_tuples = [(pair.split(':')[0].strip(), |
| 236 | int(pair.split(':')[1].strip())) |
| 237 | for pair in project_and_local_ids.split(',') |
| 238 | if pair.strip()] |
| 239 | project_names = {project_name for (project_name, _) in |
| 240 | issue_refs_tuples} |
| 241 | projects_dict = self.services.project.GetProjectsByName( |
| 242 | mr.cnxn, project_names) |
| 243 | selected_iids, _misses = self.services.issue.ResolveIssueRefs( |
| 244 | mr.cnxn, projects_dict, mr.project_name, issue_refs_tuples) |
| 245 | if (not selected_iids) or len(issue_refs_tuples) > len(selected_iids): |
| 246 | mr.errors.issues = _MSG_ISSUES_NOT_FOUND |
| 247 | # TODO(jojwang): give issues that were not found. |
| 248 | else: |
| 249 | mr.errors.issues = _MSG_INVALID_ISSUES_INPUT |
| 250 | |
| 251 | try: |
| 252 | with work_env.WorkEnv(mr, self.services) as we: |
| 253 | we.GetIssuesDict(selected_iids) |
| 254 | except exceptions.NoSuchIssueException: |
| 255 | mr.errors.issues = _MSG_ISSUES_NOT_FOUND |
| 256 | except permissions.PermissionException: |
| 257 | mr.errors.issues = _MSG_ISSUES_NOT_VIEWABLE |
| 258 | |
| 259 | # TODO(jojwang): fix: when there are errors, hidden column come back on |
| 260 | # the .do page but go away once the errors are fixed and the form |
| 261 | # is submitted again |
| 262 | if mr.errors.AnyErrors(): |
| 263 | self.PleaseCorrect( |
| 264 | mr, add_local_ids=project_and_local_ids, |
| 265 | add_issues_selected=ezt.boolean(True), col_spec=current_col_spec) |
| 266 | |
| 267 | else: |
| 268 | with work_env.WorkEnv(mr, self.services) as we: |
| 269 | if post_data.get('remove') == 'true': |
| 270 | we.RemoveIssuesFromHotlists([mr.hotlist_id], selected_iids) |
| 271 | else: |
| 272 | we.AddIssuesToHotlists([mr.hotlist_id], selected_iids, '') |
| 273 | return framework_helpers.FormatAbsoluteURL( |
| 274 | mr, hotlist_view_url, saved=1, ts=int(time.time()), |
| 275 | include_project=False, colspec=current_col_spec) |
| 276 | |
| 277 | def GetGridViewData(self, mr): |
| 278 | """EZT template values to render a Table View of issues. |
| 279 | |
| 280 | Args: |
| 281 | mr: commonly used info parsed from the request. |
| 282 | |
| 283 | Returns: |
| 284 | Dictionary of page data for rendering of the Table View. |
| 285 | """ |
| 286 | mr.ComputeColSpec(mr.hotlist) |
| 287 | starred_iid_set = set(self.services.issue_star.LookupStarredItemIDs( |
| 288 | mr.cnxn, mr.auth.user_id)) |
| 289 | issues_list = self.services.issue.GetIssues( |
| 290 | mr.cnxn, |
| 291 | [hotlist_issue.issue_id for hotlist_issue |
| 292 | in mr.hotlist.items]) |
| 293 | allowed_issues = hotlist_helpers.FilterIssues( |
| 294 | mr.cnxn, mr.auth, mr.can, issues_list, self.services) |
| 295 | issue_and_hotlist_users = tracker_bizobj.UsersInvolvedInIssues( |
| 296 | allowed_issues or []).union(features_bizobj.UsersInvolvedInHotlists( |
| 297 | [mr.hotlist])) |
| 298 | users_by_id = framework_views.MakeAllUserViews( |
| 299 | mr.cnxn, self.services.user, |
| 300 | issue_and_hotlist_users) |
| 301 | hotlist_issues_project_ids = hotlist_helpers.GetAllProjectsOfIssues( |
| 302 | [issue for issue in issues_list]) |
| 303 | config_list = hotlist_helpers.GetAllConfigsOfProjects( |
| 304 | mr.cnxn, hotlist_issues_project_ids, self.services) |
| 305 | harmonized_config = tracker_bizobj.HarmonizeConfigs(config_list) |
| 306 | limit = settings.max_issues_in_grid |
| 307 | grid_limited = len(allowed_issues) > limit |
| 308 | lower_cols = mr.col_spec.lower().split() |
| 309 | grid_x = (mr.x or harmonized_config.default_x_attr or '--').lower() |
| 310 | grid_y = (mr.y or harmonized_config.default_y_attr or '--').lower() |
| 311 | lower_cols.append(grid_x) |
| 312 | lower_cols.append(grid_y) |
| 313 | related_iids = set() |
| 314 | for issue in allowed_issues: |
| 315 | if 'blockedon' in lower_cols: |
| 316 | related_iids.update(issue.blocked_on_iids) |
| 317 | if 'blocking' in lower_cols: |
| 318 | related_iids.update(issue.blocking_iids) |
| 319 | if 'mergedinto' in lower_cols: |
| 320 | related_iids.add(issue.merged_into) |
| 321 | related_issues_list = self.services.issue.GetIssues( |
| 322 | mr.cnxn, list(related_iids)) |
| 323 | related_issues = {issue.issue_id: issue for issue in related_issues_list} |
| 324 | |
| 325 | hotlist_context_dict = { |
| 326 | hotlist_issue.issue_id: {'adder_id': hotlist_issue.adder_id, |
| 327 | 'date_added': timestr.FormatRelativeDate( |
| 328 | hotlist_issue.date_added), |
| 329 | 'note': hotlist_issue.note} |
| 330 | for hotlist_issue in mr.hotlist.items} |
| 331 | |
| 332 | grid_view_data = grid_view_helpers.GetGridViewData( |
| 333 | mr, allowed_issues, harmonized_config, |
| 334 | users_by_id, starred_iid_set, grid_limited, related_issues, |
| 335 | hotlist_context_dict=hotlist_context_dict) |
| 336 | |
| 337 | url_params = [(name, mr.GetParam(name)) for name in |
| 338 | framework_helpers.RECOGNIZED_PARAMS] |
| 339 | # We are passing in None for the project_name in ArtifactPagination |
| 340 | # because we are not operating under any project. |
| 341 | grid_view_data.update({'pagination': paginate.ArtifactPagination( |
| 342 | allowed_issues, |
| 343 | mr.GetPositiveIntParam( |
| 344 | 'num', features_constants.DEFAULT_RESULTS_PER_PAGE), |
| 345 | mr.GetPositiveIntParam('start'), None, |
| 346 | urls.HOTLIST_ISSUES, total_count=len(allowed_issues), |
| 347 | url_params=url_params)}) |
| 348 | |
| 349 | return grid_view_data |