blob: 8743772f77c10a0c145324dbb0d877d28a166d97 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# 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."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import logging
12import ezt
13
14import settings
15import time
16import re
17
18from businesslogic import work_env
19from features import features_bizobj
20from features import features_constants
21from features import hotlist_helpers
22from framework import exceptions
23from framework import servlet
24from framework import sorting
25from framework import permissions
26from framework import framework_helpers
27from framework import paginate
28from framework import framework_constants
29from framework import framework_views
30from framework import grid_view_helpers
31from framework import template_helpers
32from framework import timestr
33from framework import urls
34from framework import xsrf
35from services import features_svc
36from 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
45class 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