Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(249)

Side by Side Diff: appengine/monorail/tracker/issueadmin.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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
OLDNEW
« no previous file with comments | « appengine/monorail/tracker/issue-bulk-change-notification-email.ezt ('k') | appengine/monorail/tracker/issueadvsearch.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698