OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered 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 """Servlets for issue tracker configuration. |
| 7 |
| 8 These classes implement the Statuses, Labels and fields, Components, Rules, and |
| 9 Views subtabs under the Process tab. Unlike most servlet modules, this single |
| 10 file holds a base class and several related servlet classes. |
| 11 """ |
| 12 |
| 13 import collections |
| 14 import itertools |
| 15 import logging |
| 16 import time |
| 17 |
| 18 from features import filterrules_helpers |
| 19 from features import filterrules_views |
| 20 from features import savedqueries_helpers |
| 21 from framework import framework_bizobj |
| 22 from framework import framework_constants |
| 23 from framework import framework_helpers |
| 24 from framework import framework_views |
| 25 from framework import monorailrequest |
| 26 from framework import permissions |
| 27 from framework import servlet |
| 28 from framework import urls |
| 29 from tracker import field_helpers |
| 30 from tracker import tracker_bizobj |
| 31 from tracker import tracker_constants |
| 32 from tracker import tracker_helpers |
| 33 from tracker import tracker_views |
| 34 |
| 35 |
| 36 class IssueAdminBase(servlet.Servlet): |
| 37 """Base class for servlets allowing project owners to configure tracker.""" |
| 38 |
| 39 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS |
| 40 _PROCESS_SUBTAB = None # specified in subclasses |
| 41 |
| 42 def GatherPageData(self, mr): |
| 43 """Build up a dictionary of data values to use when rendering the page. |
| 44 |
| 45 Args: |
| 46 mr: commonly used info parsed from the request. |
| 47 |
| 48 Returns: |
| 49 Dict of values used by EZT for rendering the page. |
| 50 """ |
| 51 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 52 config_view = tracker_views.ConfigView(mr, self.services, config) |
| 53 return { |
| 54 'admin_tab_mode': self._PROCESS_SUBTAB, |
| 55 'config': config_view, |
| 56 } |
| 57 |
| 58 def ProcessFormData(self, mr, post_data): |
| 59 """Validate and store the contents of the issues tracker admin page. |
| 60 |
| 61 Args: |
| 62 mr: commonly used info parsed from the request. |
| 63 post_data: HTML form data from the request. |
| 64 |
| 65 Returns: |
| 66 String URL to redirect the user to, or None if response was already sent. |
| 67 """ |
| 68 page_url = self.ProcessSubtabForm(post_data, mr) |
| 69 |
| 70 if mr.errors.AnyErrors(): |
| 71 self.PleaseCorrect(mr) # TODO(jrobbins): echo more user-entered text. |
| 72 else: |
| 73 return framework_helpers.FormatAbsoluteURL( |
| 74 mr, page_url, saved=1, ts=int(time.time())) |
| 75 |
| 76 |
| 77 class AdminStatuses(IssueAdminBase): |
| 78 """Servlet allowing project owners to configure well-known statuses.""" |
| 79 |
| 80 _PAGE_TEMPLATE = 'tracker/admin-statuses-page.ezt' |
| 81 _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_STATUSES |
| 82 |
| 83 def ProcessSubtabForm(self, post_data, mr): |
| 84 """Process the status definition section of the admin page. |
| 85 |
| 86 Args: |
| 87 post_data: HTML form data for the HTTP request being processed. |
| 88 mr: commonly used info parsed from the request. |
| 89 |
| 90 Returns: |
| 91 The URL of the page to show after processing. |
| 92 """ |
| 93 wks_open_text = post_data.get('predefinedopen', '') |
| 94 wks_open_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall( |
| 95 wks_open_text) |
| 96 wks_open_tuples = [ |
| 97 (status.lstrip('#'), docstring.strip(), True, status.startswith('#')) |
| 98 for status, docstring in wks_open_matches] |
| 99 |
| 100 wks_closed_text = post_data.get('predefinedclosed', '') |
| 101 wks_closed_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall( |
| 102 wks_closed_text) |
| 103 wks_closed_tuples = [ |
| 104 (status.lstrip('#'), docstring.strip(), False, status.startswith('#')) |
| 105 for status, docstring in wks_closed_matches] |
| 106 |
| 107 statuses_offer_merge_text = post_data.get('statuses_offer_merge', '') |
| 108 statuses_offer_merge = framework_constants.IDENTIFIER_RE.findall( |
| 109 statuses_offer_merge_text) |
| 110 |
| 111 if not mr.errors.AnyErrors(): |
| 112 self.services.config.UpdateConfig( |
| 113 mr.cnxn, mr.project, statuses_offer_merge=statuses_offer_merge, |
| 114 well_known_statuses=wks_open_tuples + wks_closed_tuples) |
| 115 |
| 116 # TODO(jrobbins): define a "strict" mode that affects only statuses. |
| 117 |
| 118 return urls.ADMIN_STATUSES |
| 119 |
| 120 |
| 121 class AdminLabels(IssueAdminBase): |
| 122 """Servlet allowing project owners to labels and fields.""" |
| 123 |
| 124 _PAGE_TEMPLATE = 'tracker/admin-labels-page.ezt' |
| 125 _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_LABELS |
| 126 |
| 127 def GatherPageData(self, mr): |
| 128 """Build up a dictionary of data values to use when rendering the page. |
| 129 |
| 130 Args: |
| 131 mr: commonly used info parsed from the request. |
| 132 |
| 133 Returns: |
| 134 Dict of values used by EZT for rendering the page. |
| 135 """ |
| 136 page_data = super(AdminLabels, self).GatherPageData(mr) |
| 137 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 138 field_def_views = [ |
| 139 tracker_views.FieldDefView(fd, config) |
| 140 # TODO(jrobbins): future field-level view restrictions. |
| 141 for fd in config.field_defs |
| 142 if not fd.is_deleted] |
| 143 page_data.update({ |
| 144 'field_defs': field_def_views, |
| 145 }) |
| 146 return page_data |
| 147 |
| 148 def ProcessSubtabForm(self, post_data, mr): |
| 149 """Process changes to labels and custom field definitions. |
| 150 |
| 151 Args: |
| 152 post_data: HTML form data for the HTTP request being processed. |
| 153 mr: commonly used info parsed from the request. |
| 154 |
| 155 Returns: |
| 156 The URL of the page to show after processing. |
| 157 """ |
| 158 wkl_text = post_data.get('predefinedlabels', '') |
| 159 wkl_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(wkl_text) |
| 160 wkl_tuples = [ |
| 161 (label.lstrip('#'), docstring.strip(), label.startswith('#')) |
| 162 for label, docstring in wkl_matches] |
| 163 |
| 164 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 165 field_names = [fd.field_name for fd in config.field_defs |
| 166 if not fd.is_deleted] |
| 167 masked_labels = tracker_helpers.LabelsMaskedByFields(config, field_names) |
| 168 wkl_tuples.extend([ |
| 169 (masked.name, masked.docstring, False) for masked in masked_labels]) |
| 170 |
| 171 excl_prefix_text = post_data.get('excl_prefixes', '') |
| 172 excl_prefixes = framework_constants.IDENTIFIER_RE.findall(excl_prefix_text) |
| 173 |
| 174 if not mr.errors.AnyErrors(): |
| 175 self.services.config.UpdateConfig( |
| 176 mr.cnxn, mr.project, |
| 177 well_known_labels=wkl_tuples, excl_label_prefixes=excl_prefixes) |
| 178 |
| 179 # TODO(jrobbins): define a "strict" mode that affects only labels. |
| 180 |
| 181 return urls.ADMIN_LABELS |
| 182 |
| 183 |
| 184 class AdminTemplates(IssueAdminBase): |
| 185 """Servlet allowing project owners to configure templates.""" |
| 186 |
| 187 _PAGE_TEMPLATE = 'tracker/admin-templates-page.ezt' |
| 188 _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES |
| 189 |
| 190 def GatherPageData(self, mr): |
| 191 """Build up a dictionary of data values to use when rendering the page. |
| 192 |
| 193 Args: |
| 194 mr: commonly used info parsed from the request. |
| 195 |
| 196 Returns: |
| 197 Dict of values used by EZT for rendering the page. |
| 198 """ |
| 199 page_data = super(AdminTemplates, self).GatherPageData(mr) |
| 200 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 201 field_views = [ |
| 202 tracker_views.MakeFieldValueView(fd, config, [], [], [], {}) |
| 203 # TODO(jrobbins): field-level view restrictions, display options |
| 204 for fd in config.field_defs |
| 205 if not fd.is_deleted] |
| 206 |
| 207 page_data.update({ |
| 208 'fields': field_views, |
| 209 }) |
| 210 return page_data |
| 211 |
| 212 def ProcessSubtabForm(self, post_data, mr): |
| 213 """Process changes to new issue templates. |
| 214 |
| 215 Args: |
| 216 post_data: HTML form data for the HTTP request being processed. |
| 217 mr: commonly used info parsed from the request. |
| 218 |
| 219 Returns: |
| 220 The URL of the page to show after processing. |
| 221 """ |
| 222 templates = self._ParseAllTemplates(post_data, mr) |
| 223 |
| 224 default_template_id_for_developers = None |
| 225 default_template_id_for_users = None |
| 226 if self.CheckPerm(mr, permissions.EDIT_PROJECT): |
| 227 default_template_id_for_developers, default_template_id_for_users = ( |
| 228 self._ParseDefaultTemplateSelections(post_data, templates)) |
| 229 |
| 230 if not mr.errors.AnyErrors(): |
| 231 self.services.config.UpdateConfig( |
| 232 mr.cnxn, mr.project, templates=templates, |
| 233 default_template_for_developers=default_template_id_for_developers, |
| 234 default_template_for_users=default_template_id_for_users) |
| 235 |
| 236 params = ''; |
| 237 if post_data.get('current_template_index'): |
| 238 params = '?tindex=' + post_data['current_template_index'] |
| 239 return urls.ADMIN_TEMPLATES + params |
| 240 |
| 241 def _ParseAllTemplates(self, post_data, mr): |
| 242 """Iterate over the post_data and parse all templates in it.""" |
| 243 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 244 orig_templates = {tmpl.template_id: tmpl for tmpl in config.templates} |
| 245 |
| 246 templates = [] |
| 247 for i in itertools.count(): |
| 248 if ('template_id_%s' % i) not in post_data: |
| 249 break |
| 250 template_id = int(post_data['template_id_%s' % i]) |
| 251 orig_template = orig_templates.get(template_id) |
| 252 new_template = self._ParseTemplate( |
| 253 post_data, mr, i, orig_template, config) |
| 254 if new_template: |
| 255 templates.append(new_template) |
| 256 |
| 257 return templates |
| 258 |
| 259 def _ParseTemplate(self, post_data, mr, i, orig_template, config): |
| 260 """Parse an issue template. Return orig_template if cannot edit.""" |
| 261 if not self._CanEditTemplate(mr, orig_template): |
| 262 return orig_template |
| 263 |
| 264 name = post_data['name_%s' % i] |
| 265 if name == tracker_constants.DELETED_TEMPLATE_NAME: |
| 266 return None |
| 267 |
| 268 members_only = False |
| 269 if ('members_only_%s' % i) in post_data: |
| 270 members_only = ( |
| 271 post_data['members_only_%s' % i] == 'yes') |
| 272 summary = '' |
| 273 if ('summary_%s' % i) in post_data: |
| 274 summary = post_data['summary_%s' % i] |
| 275 summary_must_be_edited = False |
| 276 if ('summary_must_be_edited_%s' % i) in post_data: |
| 277 summary_must_be_edited = ( |
| 278 post_data['summary_must_be_edited_%s' % i] == 'yes') |
| 279 content = '' |
| 280 if ('content_%s' % i) in post_data: |
| 281 content = post_data['content_%s' % i] |
| 282 # wrap="hard" has no effect on the content because we copy it to |
| 283 # a hidden form field before submission. So, server-side word wrap. |
| 284 content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75) |
| 285 status = '' |
| 286 if ('status_%s' % i) in post_data: |
| 287 status = post_data['status_%s' % i] |
| 288 owner_id = 0 |
| 289 if ('owner_%s' % i) in post_data: |
| 290 owner = post_data['owner_%s' % i] |
| 291 if owner: |
| 292 user_id = self.services.user.LookupUserID(mr.cnxn, owner) |
| 293 auth = monorailrequest.AuthData.FromUserID( |
| 294 mr.cnxn, user_id, self.services) |
| 295 if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids): |
| 296 owner_id = user_id |
| 297 |
| 298 labels = post_data.getall('label_%s' % i) |
| 299 labels_remove = [] |
| 300 |
| 301 field_val_strs = collections.defaultdict(list) |
| 302 for fd in config.field_defs: |
| 303 field_value_key = 'field_value_%d_%d' % (i, fd.field_id) |
| 304 if post_data.get(field_value_key): |
| 305 field_val_strs[fd.field_id].append(post_data[field_value_key]) |
| 306 |
| 307 field_helpers.ShiftEnumFieldsIntoLabels( |
| 308 labels, labels_remove, field_val_strs, {}, config) |
| 309 field_values = field_helpers.ParseFieldValues( |
| 310 mr.cnxn, self.services.user, field_val_strs, config) |
| 311 for fv in field_values: |
| 312 logging.info('field_value is %r: %r', |
| 313 fv.field_id, tracker_bizobj.GetFieldValue(fv, {})) |
| 314 |
| 315 admin_ids = [] |
| 316 if ('admin_names_%s' % i) in post_data: |
| 317 admin_ids, _admin_str = tracker_helpers.ParseAdminUsers( |
| 318 mr.cnxn, post_data['admin_names_%s' % i], self.services.user) |
| 319 |
| 320 component_ids = [] |
| 321 if ('components_%s' % i) in post_data: |
| 322 component_paths = [] |
| 323 for component_path in post_data['components_%s' % i].split(','): |
| 324 if component_path.strip() not in component_paths: |
| 325 component_paths.append(component_path.strip()) |
| 326 component_ids = tracker_helpers.LookupComponentIDs( |
| 327 component_paths, config, mr.errors) |
| 328 |
| 329 owner_defaults_to_member = False |
| 330 if ('owner_defaults_to_member_%s' % i) in post_data: |
| 331 owner_defaults_to_member = ( |
| 332 post_data['owner_defaults_to_member_%s' % i] == 'yes') |
| 333 |
| 334 component_required = False |
| 335 if ('component_required_%s' % i) in post_data: |
| 336 component_required = post_data['component_required_%s' % i] == 'yes' |
| 337 |
| 338 template = tracker_bizobj.MakeIssueTemplate( |
| 339 name, summary, status, owner_id, |
| 340 content, labels, field_values, admin_ids, component_ids, |
| 341 summary_must_be_edited=summary_must_be_edited, |
| 342 owner_defaults_to_member=owner_defaults_to_member, |
| 343 component_required=component_required, |
| 344 members_only=members_only) |
| 345 template_id = int(post_data['template_id_%s' % i]) |
| 346 if template_id: # new templates have ID 0, so leave that None in PB. |
| 347 template.template_id = template_id |
| 348 logging.info('template is %r', template) |
| 349 |
| 350 return template |
| 351 |
| 352 def _CanEditTemplate(self, mr, template): |
| 353 """Return True if the user is allowed to edit this template.""" |
| 354 if self.CheckPerm(mr, permissions.EDIT_PROJECT): |
| 355 return True |
| 356 |
| 357 if template and not mr.auth.effective_ids.isdisjoint(template.admin_ids): |
| 358 return True |
| 359 |
| 360 return False |
| 361 |
| 362 def _ParseDefaultTemplateSelections(self, post_data, templates): |
| 363 """Parse the input for the default templates to offer users.""" |
| 364 def GetSelectedTemplateID(name): |
| 365 """Find the ID of the template specified in post_data[name].""" |
| 366 if name not in post_data: |
| 367 return None |
| 368 selected_template_name = post_data[name] |
| 369 for template in templates: |
| 370 if selected_template_name == template.name: |
| 371 return template.template_id |
| 372 |
| 373 logging.error('User somehow selected an invalid template: %r', |
| 374 selected_template_name) |
| 375 return None |
| 376 |
| 377 return (GetSelectedTemplateID('default_template_for_developers'), |
| 378 GetSelectedTemplateID('default_template_for_users')) |
| 379 |
| 380 |
| 381 class AdminComponents(IssueAdminBase): |
| 382 """Servlet allowing project owners to view the list of components.""" |
| 383 |
| 384 _PAGE_TEMPLATE = 'tracker/admin-components-page.ezt' |
| 385 _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_COMPONENTS |
| 386 |
| 387 def GatherPageData(self, mr): |
| 388 """Build up a dictionary of data values to use when rendering the page. |
| 389 |
| 390 Args: |
| 391 mr: commonly used info parsed from the request. |
| 392 |
| 393 Returns: |
| 394 Dict of values used by EZT for rendering the page. |
| 395 """ |
| 396 page_data = super(AdminComponents, self).GatherPageData(mr) |
| 397 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 398 users_by_id = framework_views.MakeAllUserViews( |
| 399 mr.cnxn, self.services.user, |
| 400 *[list(cd.admin_ids) + list(cd.cc_ids) |
| 401 for cd in config.component_defs]) |
| 402 framework_views.RevealAllEmailsToMembers(mr, users_by_id) |
| 403 component_def_views = [ |
| 404 tracker_views.ComponentDefView(cd, users_by_id) |
| 405 # TODO(jrobbins): future component-level view restrictions. |
| 406 for cd in config.component_defs] |
| 407 for cd in component_def_views: |
| 408 if mr.auth.email in [user.email for user in cd.admins]: |
| 409 cd.classes += 'myadmin ' |
| 410 if mr.auth.email in [user.email for user in cd.cc]: |
| 411 cd.classes += 'mycc ' |
| 412 |
| 413 page_data.update({ |
| 414 'component_defs': component_def_views, |
| 415 'failed_perm': mr.GetParam('failed_perm'), |
| 416 'failed_subcomp': mr.GetParam('failed_subcomp'), |
| 417 'failed_templ': mr.GetParam('failed_templ'), |
| 418 }) |
| 419 return page_data |
| 420 |
| 421 def _GetComponentDefs(self, _mr, post_data, config): |
| 422 """Get the config and component definitions from the request.""" |
| 423 component_defs = [] |
| 424 component_paths = post_data.get('delete_components').split(',') |
| 425 for component_path in component_paths: |
| 426 component_def = tracker_bizobj.FindComponentDef(component_path, config) |
| 427 component_defs.append(component_def) |
| 428 return component_defs |
| 429 |
| 430 def _ProcessDeleteComponent(self, mr, component_def): |
| 431 """Delete the specified component and its references.""" |
| 432 self.services.issue.DeleteComponentReferences( |
| 433 mr.cnxn, component_def.component_id) |
| 434 self.services.config.DeleteComponentDef( |
| 435 mr.cnxn, mr.project_id, component_def.component_id) |
| 436 |
| 437 def ProcessFormData(self, mr, post_data): |
| 438 """Processes a POST command to delete components. |
| 439 |
| 440 Args: |
| 441 mr: commonly used info parsed from the request. |
| 442 post_data: HTML form data from the request. |
| 443 |
| 444 Returns: |
| 445 String URL to redirect the user to, or None if response was already sent. |
| 446 """ |
| 447 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 448 component_defs = self._GetComponentDefs(mr, post_data, config) |
| 449 # Reverse the component_defs so that we start deleting from subcomponents. |
| 450 component_defs.reverse() |
| 451 |
| 452 # Collect errors. |
| 453 perm_errors = [] |
| 454 subcomponents_errors = [] |
| 455 templates_errors = [] |
| 456 # Collect successes. |
| 457 deleted_components = [] |
| 458 |
| 459 for component_def in component_defs: |
| 460 allow_edit = permissions.CanEditComponentDef( |
| 461 mr.auth.effective_ids, mr.perms, mr.project, component_def, config) |
| 462 if not allow_edit: |
| 463 perm_errors.append(component_def.path) |
| 464 |
| 465 subcomponents = tracker_bizobj.FindDescendantComponents( |
| 466 config, component_def) |
| 467 if subcomponents: |
| 468 subcomponents_errors.append(component_def.path) |
| 469 |
| 470 templates = self.services.config.TemplatesWithComponent( |
| 471 mr.cnxn, component_def.component_id, config) |
| 472 if templates: |
| 473 templates_errors.append(component_def.path) |
| 474 |
| 475 allow_delete = allow_edit and not subcomponents and not templates |
| 476 if allow_delete: |
| 477 self._ProcessDeleteComponent(mr, component_def) |
| 478 deleted_components.append(component_def.path) |
| 479 # Refresh project config after the component deletion. |
| 480 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 481 |
| 482 return framework_helpers.FormatAbsoluteURL( |
| 483 mr, urls.ADMIN_COMPONENTS, ts=int(time.time()), |
| 484 failed_perm=','.join(perm_errors), |
| 485 failed_subcomp=','.join(subcomponents_errors), |
| 486 failed_templ=','.join(templates_errors), |
| 487 deleted=','.join(deleted_components)) |
| 488 |
| 489 |
| 490 class AdminViews(IssueAdminBase): |
| 491 """Servlet for project owners to set default columns, axes, and sorting.""" |
| 492 |
| 493 _PAGE_TEMPLATE = 'tracker/admin-views-page.ezt' |
| 494 _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_VIEWS |
| 495 |
| 496 def GatherPageData(self, mr): |
| 497 """Build up a dictionary of data values to use when rendering the page. |
| 498 |
| 499 Args: |
| 500 mr: commonly used info parsed from the request. |
| 501 |
| 502 Returns: |
| 503 Dict of values used by EZT for rendering the page. |
| 504 """ |
| 505 page_data = super(AdminViews, self).GatherPageData(mr) |
| 506 with self.profiler.Phase('getting canned queries'): |
| 507 canned_queries = self.services.features.GetCannedQueriesByProjectID( |
| 508 mr.cnxn, mr.project_id) |
| 509 |
| 510 page_data.update({ |
| 511 'new_query_indexes': range( |
| 512 len(canned_queries) + 1, savedqueries_helpers.MAX_QUERIES + 1), |
| 513 'issue_notify': mr.project.issue_notify_address, |
| 514 'max_queries': savedqueries_helpers.MAX_QUERIES, |
| 515 }) |
| 516 return page_data |
| 517 |
| 518 def ProcessSubtabForm(self, post_data, mr): |
| 519 """Process the Views subtab. |
| 520 |
| 521 Args: |
| 522 post_data: HTML form data for the HTTP request being processed. |
| 523 mr: commonly used info parsed from the request. |
| 524 |
| 525 Returns: |
| 526 The URL of the page to show after processing. |
| 527 """ |
| 528 existing_queries = savedqueries_helpers.ParseSavedQueries( |
| 529 mr.cnxn, post_data, self.services.project) |
| 530 added_queries = savedqueries_helpers.ParseSavedQueries( |
| 531 mr.cnxn, post_data, self.services.project, prefix='new_') |
| 532 canned_queries = existing_queries + added_queries |
| 533 |
| 534 list_prefs = _ParseListPreferences(post_data) |
| 535 |
| 536 if not mr.errors.AnyErrors(): |
| 537 self.services.config.UpdateConfig( |
| 538 mr.cnxn, mr.project, list_prefs=list_prefs) |
| 539 self.services.features.UpdateCannedQueries( |
| 540 mr.cnxn, mr.project_id, canned_queries) |
| 541 |
| 542 return urls.ADMIN_VIEWS |
| 543 |
| 544 |
| 545 def _ParseListPreferences(post_data): |
| 546 """Parse the part of a project admin form about artifact list preferences.""" |
| 547 default_col_spec = '' |
| 548 if 'default_col_spec' in post_data: |
| 549 default_col_spec = post_data['default_col_spec'] |
| 550 # Don't allow empty colum spec |
| 551 if not default_col_spec: |
| 552 default_col_spec = tracker_constants.DEFAULT_COL_SPEC |
| 553 col_spec_words = monorailrequest.ParseColSpec(default_col_spec) |
| 554 col_spec = ' '.join(word for word in col_spec_words) |
| 555 |
| 556 default_sort_spec = '' |
| 557 if 'default_sort_spec' in post_data: |
| 558 default_sort_spec = post_data['default_sort_spec'] |
| 559 sort_spec_words = monorailrequest.ParseColSpec(default_sort_spec) |
| 560 sort_spec = ' '.join(sort_spec_words) |
| 561 |
| 562 x_attr_str = '' |
| 563 if 'default_x_attr' in post_data: |
| 564 x_attr_str = post_data['default_x_attr'] |
| 565 x_attr_words = monorailrequest.ParseColSpec(x_attr_str) |
| 566 x_attr = '' |
| 567 if x_attr_words: |
| 568 x_attr = x_attr_words[0] |
| 569 |
| 570 y_attr_str = '' |
| 571 if 'default_y_attr' in post_data: |
| 572 y_attr_str = post_data['default_y_attr'] |
| 573 y_attr_words = monorailrequest.ParseColSpec(y_attr_str) |
| 574 y_attr = '' |
| 575 if y_attr_words: |
| 576 y_attr = y_attr_words[0] |
| 577 |
| 578 return col_spec, sort_spec, x_attr, y_attr |
| 579 |
| 580 |
| 581 class AdminRules(IssueAdminBase): |
| 582 """Servlet allowing project owners to configure filter rules.""" |
| 583 |
| 584 _PAGE_TEMPLATE = 'tracker/admin-rules-page.ezt' |
| 585 _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_RULES |
| 586 |
| 587 def AssertBasePermission(self, mr): |
| 588 """Check whether the user has any permission to visit this page. |
| 589 |
| 590 Args: |
| 591 mr: commonly used info parsed from the request. |
| 592 """ |
| 593 super(AdminRules, self).AssertBasePermission(mr) |
| 594 if not self.CheckPerm(mr, permissions.EDIT_PROJECT): |
| 595 raise permissions.PermissionException( |
| 596 'User is not allowed to administer this project') |
| 597 |
| 598 def GatherPageData(self, mr): |
| 599 """Build up a dictionary of data values to use when rendering the page. |
| 600 |
| 601 Args: |
| 602 mr: commonly used info parsed from the request. |
| 603 |
| 604 Returns: |
| 605 Dict of values used by EZT for rendering the page. |
| 606 """ |
| 607 page_data = super(AdminRules, self).GatherPageData(mr) |
| 608 rules = self.services.features.GetFilterRules( |
| 609 mr.cnxn, mr.project_id) |
| 610 users_by_id = framework_views.MakeAllUserViews( |
| 611 mr.cnxn, self.services.user, |
| 612 [rule.default_owner_id for rule in rules], |
| 613 *[rule.add_cc_ids for rule in rules]) |
| 614 framework_views.RevealAllEmailsToMembers(mr, users_by_id) |
| 615 rule_views = [filterrules_views.RuleView(rule, users_by_id) |
| 616 for rule in rules] |
| 617 |
| 618 for idx, rule_view in enumerate(rule_views): |
| 619 rule_view.idx = idx + 1 # EZT has no loop index, so we set idx. |
| 620 |
| 621 page_data.update({ |
| 622 'rules': rule_views, |
| 623 'new_rule_indexes': ( |
| 624 range(len(rules) + 1, filterrules_helpers.MAX_RULES + 1)), |
| 625 'max_rules': filterrules_helpers.MAX_RULES, |
| 626 }) |
| 627 return page_data |
| 628 |
| 629 def ProcessSubtabForm(self, post_data, mr): |
| 630 """Process the Rules subtab. |
| 631 |
| 632 Args: |
| 633 post_data: HTML form data for the HTTP request being processed. |
| 634 mr: commonly used info parsed from the request. |
| 635 |
| 636 Returns: |
| 637 The URL of the page to show after processing. |
| 638 """ |
| 639 old_rules = self.services.features.GetFilterRules(mr.cnxn, mr.project_id) |
| 640 rules = filterrules_helpers.ParseRules( |
| 641 mr.cnxn, post_data, self.services.user, mr.errors) |
| 642 new_rules = filterrules_helpers.ParseRules( |
| 643 mr.cnxn, post_data, self.services.user, mr.errors, prefix='new_') |
| 644 rules.extend(new_rules) |
| 645 |
| 646 if not mr.errors.AnyErrors(): |
| 647 config = self.services.features.UpdateFilterRules( |
| 648 mr.cnxn, mr.project_id, rules) |
| 649 |
| 650 if old_rules != rules: |
| 651 logging.info('recomputing derived fields') |
| 652 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) |
| 653 filterrules_helpers.RecomputeAllDerivedFields( |
| 654 mr.cnxn, self.services, mr.project, config) |
| 655 |
| 656 return urls.ADMIN_RULES |
OLD | NEW |