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