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