Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # 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. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Classes that implement the issue bulk edit page and related forms. |
| 6 | |
| 7 | Summary of classes: |
| 8 | IssueBulkEdit: Show a form for editing multiple issues and allow the |
| 9 | user to update them all at once. |
| 10 | """ |
| 11 | from __future__ import print_function |
| 12 | from __future__ import division |
| 13 | from __future__ import absolute_import |
| 14 | |
| 15 | import collections |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 16 | from six.moves import http_client |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 17 | import itertools |
| 18 | import logging |
| 19 | import time |
| 20 | |
| 21 | import ezt |
| 22 | |
| 23 | from features import filterrules_helpers |
| 24 | from features import send_notifications |
| 25 | from framework import exceptions |
| 26 | from framework import framework_constants |
| 27 | from framework import framework_views |
| 28 | from framework import permissions |
| 29 | from framework import servlet |
| 30 | from framework import template_helpers |
| 31 | from services import tracker_fulltext |
| 32 | from tracker import field_helpers |
| 33 | from tracker import tracker_bizobj |
| 34 | from tracker import tracker_constants |
| 35 | from tracker import tracker_helpers |
| 36 | from tracker import tracker_views |
| 37 | |
| 38 | |
| 39 | class IssueBulkEdit(servlet.Servlet): |
| 40 | """IssueBulkEdit lists multiple issues and allows an edit to all of them.""" |
| 41 | |
| 42 | _PAGE_TEMPLATE = 'tracker/issue-bulk-edit-page.ezt' |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 43 | _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 44 | _SECONDS_OVERHEAD = 4 |
| 45 | _SECONDS_PER_UPDATE = 0.12 |
| 46 | _SLOWNESS_THRESHOLD = 10 |
| 47 | |
| 48 | def AssertBasePermission(self, mr): |
| 49 | """Check whether the user has any permission to visit this page. |
| 50 | |
| 51 | Args: |
| 52 | mr: commonly used info parsed from the request. |
| 53 | |
| 54 | Raises: |
| 55 | PermissionException: if the user is not allowed to enter an issue. |
| 56 | """ |
| 57 | super(IssueBulkEdit, self).AssertBasePermission(mr) |
| 58 | can_edit = self.CheckPerm(mr, permissions.EDIT_ISSUE) |
| 59 | can_comment = self.CheckPerm(mr, permissions.ADD_ISSUE_COMMENT) |
| 60 | if not (can_edit and can_comment): |
| 61 | raise permissions.PermissionException('bulk edit forbidden') |
| 62 | |
| 63 | def GatherPageData(self, mr): |
| 64 | """Build up a dictionary of data values to use when rendering the page. |
| 65 | |
| 66 | Args: |
| 67 | mr: commonly used info parsed from the request. |
| 68 | |
| 69 | Returns: |
| 70 | Dict of values used by EZT for rendering the page. |
| 71 | """ |
| 72 | with mr.profiler.Phase('getting issues'): |
| 73 | if not mr.local_id_list: |
| 74 | raise exceptions.InputException() |
| 75 | requested_issues = self.services.issue.GetIssuesByLocalIDs( |
| 76 | mr.cnxn, mr.project_id, sorted(mr.local_id_list)) |
| 77 | |
| 78 | with mr.profiler.Phase('filtering issues'): |
| 79 | # TODO(jrobbins): filter out issues that the user cannot edit and |
| 80 | # provide that as feedback rather than just siliently ignoring them. |
| 81 | open_issues, closed_issues = ( |
| 82 | tracker_helpers.GetAllowedOpenedAndClosedIssues( |
| 83 | mr, [issue.issue_id for issue in requested_issues], |
| 84 | self.services)) |
| 85 | issues = open_issues + closed_issues |
| 86 | |
| 87 | if not issues: |
| 88 | self.abort(404, 'no issues found') |
| 89 | |
| 90 | config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 91 | type_label_set = { |
| 92 | lab.lower() for lab in issues[0].labels |
| 93 | if lab.lower().startswith('type-')} |
| 94 | for issue in issues[1:]: |
| 95 | new_type_set = { |
| 96 | lab.lower() for lab in issue.labels |
| 97 | if lab.lower().startswith('type-')} |
| 98 | type_label_set &= new_type_set |
| 99 | |
| 100 | issue_phases = list( |
| 101 | itertools.chain.from_iterable(issue.phases for issue in issues)) |
| 102 | |
| 103 | field_views = tracker_views.MakeAllFieldValueViews( |
| 104 | config, type_label_set, [], [], {}, phases=issue_phases) |
| 105 | for fv in field_views: |
| 106 | # Explicitly set all field views to not required. We do not want to force |
| 107 | # users to have to set it for issues missing required fields. |
| 108 | # See https://bugs.chromium.org/p/monorail/issues/detail?id=500 for more |
| 109 | # details. |
| 110 | fv.field_def.is_required_bool = None |
| 111 | |
| 112 | if permissions.CanEditValueForFieldDef( |
| 113 | mr.auth.effective_ids, mr.perms, mr.project, fv.field_def.field_def): |
| 114 | fv.is_editable = ezt.boolean(True) |
| 115 | else: |
| 116 | fv.is_editable = ezt.boolean(False) |
| 117 | |
| 118 | with mr.profiler.Phase('making issue proxies'): |
| 119 | issue_views = [ |
| 120 | template_helpers.EZTItem( |
| 121 | local_id=issue.local_id, summary=issue.summary, |
| 122 | closed=ezt.boolean(issue in closed_issues)) |
| 123 | for issue in issues] |
| 124 | |
| 125 | num_seconds = (int(len(issue_views) * self._SECONDS_PER_UPDATE) + |
| 126 | self._SECONDS_OVERHEAD) |
| 127 | |
| 128 | page_perms = self.MakePagePerms( |
| 129 | mr, None, |
| 130 | permissions.CREATE_ISSUE, |
| 131 | permissions.DELETE_ISSUE) |
| 132 | |
| 133 | return { |
| 134 | 'issue_tab_mode': 'issueBulkEdit', |
| 135 | 'issues': issue_views, |
| 136 | 'local_ids_str': ','.join([str(issue.local_id) for issue in issues]), |
| 137 | 'num_issues': len(issue_views), |
| 138 | 'show_progress': ezt.boolean(num_seconds > self._SLOWNESS_THRESHOLD), |
| 139 | 'num_seconds': num_seconds, |
| 140 | |
| 141 | 'initial_blocked_on': '', |
| 142 | 'initial_blocking': '', |
| 143 | 'initial_comment': '', |
| 144 | 'initial_status': '', |
| 145 | 'initial_owner': '', |
| 146 | 'initial_merge_into': '', |
| 147 | 'initial_cc': '', |
| 148 | 'initial_components': '', |
| 149 | 'labels': [], |
| 150 | 'fields': field_views, |
| 151 | |
| 152 | 'restrict_to_known': ezt.boolean(config.restrict_to_known), |
| 153 | 'page_perms': page_perms, |
| 154 | 'statuses_offer_merge': config.statuses_offer_merge, |
| 155 | 'issue_phase_names': list( |
| 156 | {phase.name.lower() for phase in issue_phases}), |
| 157 | } |
| 158 | |
| 159 | def ProcessFormData(self, mr, post_data): |
| 160 | # (...) -> str |
| 161 | """Process the posted issue update form. |
| 162 | |
| 163 | Args: |
| 164 | mr: commonly used info parsed from the request. |
| 165 | post_data: HTML form data from the request. |
| 166 | |
| 167 | Returns: |
| 168 | String URL to redirect the user to after processing. |
| 169 | """ |
| 170 | if not mr.local_id_list: |
| 171 | logging.info('missing issue local IDs, probably tampered') |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 172 | self.response.status_code = http_client.BAD_REQUEST |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 173 | return |
| 174 | |
| 175 | # Check that the user is logged in; anon users cannot update issues. |
| 176 | if not mr.auth.user_id: |
| 177 | logging.info('user was not logged in, cannot update issue') |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 178 | self.response.status_code = http_client.BAD_REQUEST |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 179 | return |
| 180 | |
| 181 | # Check that the user has permission to add a comment, and to enter |
| 182 | # metadata if they are trying to do that. |
| 183 | if not self.CheckPerm(mr, permissions.ADD_ISSUE_COMMENT): |
| 184 | logging.info('user has no permission to add issue comment') |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 185 | self.response.status_code = http_client.BAD_REQUEST |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 186 | return |
| 187 | |
| 188 | if not self.CheckPerm(mr, permissions.EDIT_ISSUE): |
| 189 | logging.info('user has no permission to edit issue metadata') |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 190 | self.response.status_code = http_client.BAD_REQUEST |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 191 | return |
| 192 | |
| 193 | move_to = post_data.get('move_to', '').lower() |
| 194 | if move_to and not self.CheckPerm(mr, permissions.DELETE_ISSUE): |
| 195 | logging.info('user has no permission to move issue') |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 196 | self.response.status_code = http_client.BAD_REQUEST |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 197 | return |
| 198 | |
| 199 | config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 200 | |
| 201 | parsed = tracker_helpers.ParseIssueRequest( |
| 202 | mr.cnxn, post_data, self.services, mr.errors, mr.project_name) |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 203 | |
| 204 | field_helpers.ValidateLabels( |
| 205 | mr.cnxn, |
| 206 | self.services, |
| 207 | mr.project_id, |
| 208 | parsed.labels, |
| 209 | ezt_errors=mr.errors) |
| 210 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 211 | bounce_labels = ( |
| 212 | parsed.labels[:] + |
| 213 | ['-%s' % lr for lr in parsed.labels_remove]) |
| 214 | bounce_fields = tracker_views.MakeBounceFieldValueViews( |
| 215 | parsed.fields.vals, parsed.fields.phase_vals, config) |
| 216 | field_helpers.ShiftEnumFieldsIntoLabels( |
| 217 | parsed.labels, parsed.labels_remove, |
| 218 | parsed.fields.vals, parsed.fields.vals_remove, |
| 219 | config) |
| 220 | issue_list = self.services.issue.GetIssuesByLocalIDs( |
| 221 | mr.cnxn, mr.project_id, mr.local_id_list) |
| 222 | issue_phases = list( |
| 223 | itertools.chain.from_iterable(issue.phases for issue in issue_list)) |
| 224 | phase_ids_by_name = collections.defaultdict(set) |
| 225 | for phase in issue_phases: |
| 226 | phase_ids_by_name[phase.name.lower()].add(phase.phase_id) |
| 227 | # Note: Not all parsed phase field values will be applicable to every issue. |
| 228 | # tracker_bizobj.ApplyFieldValueChanges will take care of not adding |
| 229 | # phase field values to issues that don't contain the correct phase. |
| 230 | field_vals = field_helpers.ParseFieldValues( |
| 231 | mr.cnxn, self.services.user, parsed.fields.vals, |
| 232 | parsed.fields.phase_vals, config, |
| 233 | phase_ids_by_name=phase_ids_by_name) |
| 234 | field_vals_remove = field_helpers.ParseFieldValues( |
| 235 | mr.cnxn, self.services.user, parsed.fields.vals_remove, |
| 236 | parsed.fields.phase_vals_remove, config, |
| 237 | phase_ids_by_name=phase_ids_by_name) |
| 238 | |
| 239 | field_helpers.AssertCustomFieldsEditPerms( |
| 240 | mr, config, field_vals, field_vals_remove, parsed.fields.fields_clear, |
| 241 | parsed.labels, parsed.labels_remove) |
| 242 | field_helpers.ValidateCustomFields( |
| 243 | mr.cnxn, self.services, field_vals, config, mr.project, |
| 244 | ezt_errors=mr.errors) |
| 245 | |
| 246 | # Treat status '' as no change and explicit 'clear' as clearing the status. |
| 247 | status = parsed.status |
| 248 | if status == '': |
| 249 | status = None |
| 250 | if post_data.get('op_statusenter') == 'clear': |
| 251 | status = '' |
| 252 | |
| 253 | reporter_id = mr.auth.user_id |
| 254 | logging.info('bulk edit request by %s', reporter_id) |
| 255 | |
| 256 | if parsed.users.owner_id is None: |
| 257 | mr.errors.owner = 'Invalid owner username' |
| 258 | else: |
| 259 | valid, msg = tracker_helpers.IsValidIssueOwner( |
| 260 | mr.cnxn, mr.project, parsed.users.owner_id, self.services) |
| 261 | if not valid: |
| 262 | mr.errors.owner = msg |
| 263 | |
| 264 | if (status in config.statuses_offer_merge and |
| 265 | not post_data.get('merge_into')): |
| 266 | mr.errors.merge_into_id = 'Please enter a valid issue ID' |
| 267 | |
| 268 | move_to_project = None |
| 269 | if move_to: |
| 270 | if mr.project_name == move_to: |
| 271 | mr.errors.move_to = 'The issues are already in project ' + move_to |
| 272 | else: |
| 273 | move_to_project = self.services.project.GetProjectByName( |
| 274 | mr.cnxn, move_to) |
| 275 | if not move_to_project: |
| 276 | mr.errors.move_to = 'No such project: ' + move_to |
| 277 | |
| 278 | # Treat owner '' as no change, and explicit 'clear' as NO_USER_SPECIFIED |
| 279 | owner_id = parsed.users.owner_id |
| 280 | if parsed.users.owner_username == '': |
| 281 | owner_id = None |
| 282 | if post_data.get('op_ownerenter') == 'clear': |
| 283 | owner_id = framework_constants.NO_USER_SPECIFIED |
| 284 | |
| 285 | comp_ids = tracker_helpers.LookupComponentIDs( |
| 286 | parsed.components.paths, config, mr.errors) |
| 287 | comp_ids_remove = tracker_helpers.LookupComponentIDs( |
| 288 | parsed.components.paths_remove, config, mr.errors) |
| 289 | if post_data.get('op_componententer') == 'remove': |
| 290 | comp_ids, comp_ids_remove = comp_ids_remove, comp_ids |
| 291 | |
| 292 | cc_ids, cc_ids_remove = parsed.users.cc_ids, parsed.users.cc_ids_remove |
| 293 | if post_data.get('op_memberenter') == 'remove': |
| 294 | cc_ids, cc_ids_remove = parsed.users.cc_ids_remove, parsed.users.cc_ids |
| 295 | |
| 296 | issue_list_iids = {issue.issue_id for issue in issue_list} |
| 297 | if post_data.get('op_blockedonenter') == 'append': |
| 298 | if issue_list_iids.intersection(parsed.blocked_on.iids): |
| 299 | mr.errors.blocked_on = 'Cannot block an issue on itself.' |
| 300 | blocked_on_add = parsed.blocked_on.iids |
| 301 | blocked_on_remove = [] |
| 302 | else: |
| 303 | blocked_on_add = [] |
| 304 | blocked_on_remove = parsed.blocked_on.iids |
| 305 | if post_data.get('op_blockingenter') == 'append': |
| 306 | if issue_list_iids.intersection(parsed.blocking.iids): |
| 307 | mr.errors.blocking = 'Cannot block an issue on itself.' |
| 308 | blocking_add = parsed.blocking.iids |
| 309 | blocking_remove = [] |
| 310 | else: |
| 311 | blocking_add = [] |
| 312 | blocking_remove = parsed.blocking.iids |
| 313 | |
| 314 | if len(parsed.comment) > tracker_constants.MAX_COMMENT_CHARS: |
| 315 | mr.errors.comment = 'Comment is too long.' |
| 316 | |
| 317 | iids_actually_changed = [] |
| 318 | old_owner_ids = [] |
| 319 | combined_amendments = [] |
| 320 | merge_into_issue = None |
| 321 | new_starrers = set() |
| 322 | |
| 323 | if not mr.errors.AnyErrors(): |
| 324 | # Because we will modify issues, load from DB rather than cache. |
| 325 | issue_list = self.services.issue.GetIssuesByLocalIDs( |
| 326 | mr.cnxn, mr.project_id, mr.local_id_list, use_cache=False) |
| 327 | |
| 328 | # Skip any individual issues that the user is not allowed to edit. |
| 329 | editable_issues = [ |
| 330 | issue for issue in issue_list |
| 331 | if permissions.CanEditIssue( |
| 332 | mr.auth.effective_ids, mr.perms, mr.project, issue)] |
| 333 | |
| 334 | # Skip any restrict issues that cannot be moved |
| 335 | if move_to: |
| 336 | editable_issues = [ |
| 337 | issue for issue in editable_issues |
| 338 | if not permissions.GetRestrictions(issue)] |
| 339 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 340 | # Check that we can modify issues we want to block with. |
| 341 | if post_data.get('blocked_on'): |
| 342 | for issue_ref in post_data.get('blocked_on').split(','): |
| 343 | if not issue_ref: |
| 344 | continue |
| 345 | project_name, iid = tracker_bizobj.ParseIssueRef(issue_ref) |
| 346 | project_name = project_name or mr.project_name |
| 347 | project = self.services.project.GetProjectByName( |
| 348 | mr.cnxn, project_name) |
| 349 | issue = self.services.issue.GetIssueByLocalID( |
| 350 | mr.cnxn, project.project_id, iid, use_cache=False) |
| 351 | if not self._CheckEditIssuePermissions(mr, project, issue): |
| 352 | mr.errors.blocked_on = 'Target issue %s cannot be modified' % (iid) |
| 353 | break |
| 354 | |
| 355 | if post_data.get('blocking'): |
| 356 | for issue_ref in post_data.get('blocking').split(','): |
| 357 | if not issue_ref: |
| 358 | continue |
| 359 | project_name, iid = tracker_bizobj.ParseIssueRef(issue_ref) |
| 360 | project_name = project_name or mr.project_name |
| 361 | project = self.services.project.GetProjectByName( |
| 362 | mr.cnxn, project_name) |
| 363 | issue = self.services.issue.GetIssueByLocalID( |
| 364 | mr.cnxn, project.project_id, iid, use_cache=False) |
| 365 | if not self._CheckEditIssuePermissions(mr, project, issue): |
| 366 | mr.errors.blocking = 'Target issue %s cannot be modified' % (iid) |
| 367 | break |
| 368 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 369 | # If 'Duplicate' status is specified ensure there are no permission issues |
| 370 | # with the issue we want to merge with. |
| 371 | if post_data.get('merge_into'): |
| 372 | for issue in editable_issues: |
| 373 | _, merge_into_issue = tracker_helpers.ParseMergeFields( |
| 374 | mr.cnxn, self.services, mr.project_name, post_data, parsed.status, |
| 375 | config, issue, mr.errors) |
| 376 | if merge_into_issue: |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 377 | project = self.services.project.GetProjectByName( |
| 378 | mr.cnxn, issue.project_name) |
| 379 | if not self._CheckEditIssuePermissions(mr, project, |
| 380 | merge_into_issue): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 381 | mr.errors.merge_into_id = 'Target issue %s cannot be modified' % ( |
| 382 | merge_into_issue.local_id) |
| 383 | break |
| 384 | |
| 385 | # Update the new_starrers set. |
| 386 | new_starrers.update(tracker_helpers.GetNewIssueStarrers( |
| 387 | mr.cnxn, self.services, [issue.issue_id], |
| 388 | merge_into_issue.issue_id)) |
| 389 | |
| 390 | # Proceed with amendments only if there are no reported errors. |
| 391 | if not mr.errors.AnyErrors(): |
| 392 | # Sort the issues: we want them in this order so that the |
| 393 | # corresponding old_owner_id are found in the same order. |
| 394 | editable_issues.sort(key=lambda issue: issue.local_id) |
| 395 | |
| 396 | iids_to_invalidate = set() |
| 397 | rules = self.services.features.GetFilterRules( |
| 398 | mr.cnxn, config.project_id) |
| 399 | predicate_asts = filterrules_helpers.ParsePredicateASTs( |
| 400 | rules, config, []) |
| 401 | for issue in editable_issues: |
| 402 | old_owner_id = tracker_bizobj.GetOwnerId(issue) |
| 403 | merge_into_iid = ( |
| 404 | merge_into_issue.issue_id if merge_into_issue else None) |
| 405 | |
| 406 | delta = tracker_bizobj.MakeIssueDelta( |
| 407 | status, owner_id, cc_ids, cc_ids_remove, comp_ids, comp_ids_remove, |
| 408 | parsed.labels, parsed.labels_remove, field_vals, field_vals_remove, |
| 409 | parsed.fields.fields_clear, blocked_on_add, blocked_on_remove, |
| 410 | blocking_add, blocking_remove, merge_into_iid, None) |
| 411 | amendments, _ = self.services.issue.DeltaUpdateIssue( |
| 412 | mr.cnxn, self.services, mr.auth.user_id, mr.project_id, config, |
| 413 | issue, delta, comment=parsed.comment, |
| 414 | iids_to_invalidate=iids_to_invalidate, rules=rules, |
| 415 | predicate_asts=predicate_asts) |
| 416 | |
| 417 | if amendments or parsed.comment: # Avoid empty comments. |
| 418 | iids_actually_changed.append(issue.issue_id) |
| 419 | old_owner_ids.append(old_owner_id) |
| 420 | combined_amendments.extend(amendments) |
| 421 | |
| 422 | self.services.issue.InvalidateIIDs(mr.cnxn, iids_to_invalidate) |
| 423 | self.services.project.UpdateRecentActivity( |
| 424 | mr.cnxn, mr.project.project_id) |
| 425 | |
| 426 | # Add new_starrers and new CCs to merge_into_issue. |
| 427 | if merge_into_issue: |
| 428 | merge_into_project = self.services.project.GetProjectByName( |
| 429 | mr.cnxn, merge_into_issue.project_name) |
| 430 | tracker_helpers.AddIssueStarrers( |
| 431 | mr.cnxn, self.services, mr, merge_into_issue.issue_id, |
| 432 | merge_into_project, new_starrers) |
| 433 | # Load target issue again to get the updated star count. |
| 434 | merge_into_issue = self.services.issue.GetIssue( |
| 435 | mr.cnxn, merge_into_issue.issue_id, use_cache=False) |
| 436 | tracker_helpers.MergeCCsAndAddCommentMultipleIssues( |
| 437 | self.services, mr, editable_issues, merge_into_issue) |
| 438 | |
| 439 | if move_to and editable_issues: |
| 440 | tracker_fulltext.UnindexIssues( |
| 441 | [issue.issue_id for issue in editable_issues]) |
| 442 | for issue in editable_issues: |
| 443 | old_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id) |
| 444 | moved_back_iids = self.services.issue.MoveIssues( |
| 445 | mr.cnxn, move_to_project, [issue], self.services.user) |
| 446 | new_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id) |
| 447 | if issue.issue_id in moved_back_iids: |
| 448 | content = 'Moved %s back to %s again.' % ( |
| 449 | old_text_ref, new_text_ref) |
| 450 | else: |
| 451 | content = 'Moved %s to now be %s.' % (old_text_ref, new_text_ref) |
| 452 | self.services.issue.CreateIssueComment( |
| 453 | mr.cnxn, issue, mr.auth.user_id, content, amendments=[ |
| 454 | tracker_bizobj.MakeProjectAmendment( |
| 455 | move_to_project.project_name)]) |
| 456 | |
| 457 | send_email = 'send_email' in post_data |
| 458 | |
| 459 | users_by_id = framework_views.MakeAllUserViews( |
| 460 | mr.cnxn, self.services.user, |
| 461 | [owner_id], cc_ids, cc_ids_remove, old_owner_ids, |
| 462 | tracker_bizobj.UsersInvolvedInAmendments(combined_amendments)) |
| 463 | if move_to and editable_issues: |
| 464 | iids_actually_changed = [ |
| 465 | issue.issue_id for issue in editable_issues] |
| 466 | |
| 467 | send_notifications.SendIssueBulkChangeNotification( |
| 468 | iids_actually_changed, mr.request.host, |
| 469 | old_owner_ids, parsed.comment, |
| 470 | reporter_id, combined_amendments, send_email, users_by_id) |
| 471 | |
| 472 | if mr.errors.AnyErrors(): |
| 473 | bounce_cc_parts = ( |
| 474 | parsed.users.cc_usernames + |
| 475 | ['-%s' % ccur for ccur in parsed.users.cc_usernames_remove]) |
| 476 | self.PleaseCorrect( |
| 477 | mr, initial_status=parsed.status, |
| 478 | initial_owner=parsed.users.owner_username, |
| 479 | initial_merge_into=post_data.get('merge_into', 0), |
| 480 | initial_cc=', '.join(bounce_cc_parts), |
| 481 | initial_comment=parsed.comment, |
| 482 | initial_components=parsed.components.entered_str, |
| 483 | labels=bounce_labels, |
| 484 | fields=bounce_fields) |
| 485 | return |
| 486 | |
| 487 | with mr.profiler.Phase('reindexing issues'): |
| 488 | logging.info('starting reindexing') |
| 489 | start = time.time() |
| 490 | # Get the updated issues and index them |
| 491 | issue_list = self.services.issue.GetIssuesByLocalIDs( |
| 492 | mr.cnxn, mr.project_id, mr.local_id_list) |
| 493 | tracker_fulltext.IndexIssues( |
| 494 | mr.cnxn, issue_list, self.services.user, self.services.issue, |
| 495 | self.services.config) |
| 496 | logging.info('reindexing %d issues took %s sec', |
| 497 | len(issue_list), time.time() - start) |
| 498 | |
| 499 | # TODO(jrobbins): These could be put into the form action attribute. |
| 500 | mr.can = int(post_data['can']) |
| 501 | mr.query = post_data['q'] |
| 502 | mr.col_spec = post_data['colspec'] |
| 503 | mr.sort_spec = post_data['sort'] |
| 504 | mr.group_by_spec = post_data['groupby'] |
| 505 | mr.start = int(post_data['start']) |
| 506 | mr.num = int(post_data['num']) |
| 507 | |
| 508 | # TODO(jrobbins): implement bulk=N param for a better confirmation alert. |
| 509 | return tracker_helpers.FormatIssueListURL( |
| 510 | mr, config, saved=len(mr.local_id_list), ts=int(time.time())) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 511 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 512 | def GetIssueBulkEdit(self, **kwargs): |
| 513 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 514 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 515 | def PostIssueBulkEdit(self, **kwargs): |
| 516 | return self.handler(**kwargs) |
| 517 | |
| 518 | def _CheckEditIssuePermissions(self, mr, project, issue): |
| 519 | config = self.services.config.GetProjectConfig(mr.cnxn, project.project_id) |
| 520 | granted_perms = tracker_bizobj.GetGrantedPerms( |
| 521 | issue, mr.auth.effective_ids, config) |
| 522 | return tracker_helpers.CanEditProjectIssue( |
| 523 | mr, project, issue, granted_perms) |