blob: 10fbdc85a8fa3c806acf9f548f709c101a2f5137 [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"""Servlets for issue tracker configuration.
7
8These classes implement the Statuses, Labels and fields, Components, Rules, and
9Views subtabs under the Process tab. Unlike most servlet modules, this single
10file holds a base class and several related servlet classes.
11"""
12from __future__ import print_function
13from __future__ import division
14from __future__ import absolute_import
15
16import collections
17import itertools
18import logging
19import time
20
21import ezt
22
23from features import filterrules_helpers
24from features import filterrules_views
25from features import savedqueries_helpers
26from framework import authdata
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020027from framework import flaskservlet
Copybara854996b2021-09-07 19:36:02 +000028from framework import framework_bizobj
29from framework import framework_constants
30from framework import framework_helpers
31from framework import framework_views
32from framework import monorailrequest
33from framework import permissions
34from framework import servlet
35from framework import urls
36from proto import tracker_pb2
37from tracker import field_helpers
38from tracker import tracker_bizobj
39from tracker import tracker_constants
40from tracker import tracker_helpers
41from tracker import tracker_views
42
43
44class IssueAdminBase(servlet.Servlet):
45 """Base class for servlets allowing project owners to configure tracker."""
46
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020047 _MAIN_TAB_MODE = flaskservlet.FlaskServlet.MAIN_TAB_PROCESS
Copybara854996b2021-09-07 19:36:02 +000048 _PROCESS_SUBTAB = None # specified in subclasses
49
50 def GatherPageData(self, mr):
51 """Build up a dictionary of data values to use when rendering the page.
52
53 Args:
54 mr: commonly used info parsed from the request.
55
56 Returns:
57 Dict of values used by EZT for rendering the page.
58 """
59 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
60 config_view = tracker_views.ConfigView(mr, self.services, config,
61 template=None, load_all_templates=True)
62 open_text, closed_text = tracker_views.StatusDefsAsText(config)
63 labels_text = tracker_views.LabelDefsAsText(config)
64
65 return {
66 'admin_tab_mode': self._PROCESS_SUBTAB,
67 'config': config_view,
68 'open_text': open_text,
69 'closed_text': closed_text,
70 'labels_text': labels_text,
71 }
72
73 def ProcessFormData(self, mr, post_data):
74 """Validate and store the contents of the issues tracker admin page.
75
76 Args:
77 mr: commonly used info parsed from the request.
78 post_data: HTML form data from the request.
79
80 Returns:
81 String URL to redirect the user to, or None if response was already sent.
82 """
83 page_url = self.ProcessSubtabForm(post_data, mr)
84
85 if page_url:
86 return framework_helpers.FormatAbsoluteURL(
87 mr, page_url, saved=1, ts=int(time.time()))
88
89
90class AdminStatuses(IssueAdminBase):
91 """Servlet allowing project owners to configure well-known statuses."""
92
93 _PAGE_TEMPLATE = 'tracker/admin-statuses-page.ezt'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020094 _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_STATUSES
Copybara854996b2021-09-07 19:36:02 +000095
96 def ProcessSubtabForm(self, post_data, mr):
97 """Process the status definition section of the admin page.
98
99 Args:
100 post_data: HTML form data for the HTTP request being processed.
101 mr: commonly used info parsed from the request.
102
103 Returns:
104 The URL of the page to show after processing.
105 """
106 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
107 raise permissions.PermissionException(
108 'Only project owners may edit the status definitions')
109
110 wks_open_text = post_data.get('predefinedopen', '')
111 wks_open_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(
112 wks_open_text)
113 wks_open_tuples = [
114 (status.lstrip('#'), docstring.strip(), True, status.startswith('#'))
115 for status, docstring in wks_open_matches]
116 if not wks_open_tuples:
117 mr.errors.open_statuses = 'A project cannot have zero open statuses'
118
119 wks_closed_text = post_data.get('predefinedclosed', '')
120 wks_closed_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(
121 wks_closed_text)
122 wks_closed_tuples = [
123 (status.lstrip('#'), docstring.strip(), False, status.startswith('#'))
124 for status, docstring in wks_closed_matches]
125 if not wks_closed_tuples:
126 mr.errors.closed_statuses = 'A project cannot have zero closed statuses'
127
128 statuses_offer_merge_text = post_data.get('statuses_offer_merge', '')
129 statuses_offer_merge = framework_constants.IDENTIFIER_RE.findall(
130 statuses_offer_merge_text)
131
132 if mr.errors.AnyErrors():
133 self.PleaseCorrect(
134 mr, open_text=wks_open_text, closed_text=wks_closed_text)
135 return
136
137 self.services.config.UpdateConfig(
138 mr.cnxn, mr.project, statuses_offer_merge=statuses_offer_merge,
139 well_known_statuses=wks_open_tuples + wks_closed_tuples)
140
141 # TODO(jrobbins): define a "strict" mode that affects only statuses.
142
143 return urls.ADMIN_STATUSES
144
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200145 # def GetAdminStatusesPage(self, **kwargs):
146 # return self.handler(**kwargs)
147
148 # def PostAdminStatusesPage(self, **kwargs):
149 # return self.handler(**kwargs)
150
Copybara854996b2021-09-07 19:36:02 +0000151
152class AdminLabels(IssueAdminBase):
153 """Servlet allowing project owners to labels and fields."""
154
155 _PAGE_TEMPLATE = 'tracker/admin-labels-page.ezt'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200156 _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_LABELS
Copybara854996b2021-09-07 19:36:02 +0000157
158 def GatherPageData(self, mr):
159 """Build up a dictionary of data values to use when rendering the page.
160
161 Args:
162 mr: commonly used info parsed from the request.
163
164 Returns:
165 Dict of values used by EZT for rendering the page.
166 """
167 page_data = super(AdminLabels, self).GatherPageData(mr)
168 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
169 field_def_views = [
170 tracker_views.FieldDefView(fd, config)
171 # TODO(jrobbins): future field-level view restrictions.
172 for fd in config.field_defs
173 if not fd.is_deleted]
174 page_data.update({
175 'field_defs': field_def_views,
176 })
177 return page_data
178
179 def ProcessSubtabForm(self, post_data, mr):
180 """Process changes to labels and custom field definitions.
181
182 Args:
183 post_data: HTML form data for the HTTP request being processed.
184 mr: commonly used info parsed from the request.
185
186 Returns:
187 The URL of the page to show after processing.
188 """
189 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
190 raise permissions.PermissionException(
191 'Only project owners may edit the label definitions')
192
193 wkl_text = post_data.get('predefinedlabels', '')
194 wkl_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(wkl_text)
195 wkl_tuples = [
196 (label.lstrip('#'), docstring.strip(), label.startswith('#'))
197 for label, docstring in wkl_matches]
198 if not wkl_tuples:
199 mr.errors.label_defs = 'A project cannot have zero labels'
200 label_counter = collections.Counter(wkl[0].lower() for wkl in wkl_tuples)
201 for lab, count in label_counter.items():
202 if count > 1:
203 mr.errors.label_defs = 'Duplicate label: %s' % lab
204
205 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
206 field_names = [fd.field_name for fd in config.field_defs
207 if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE
208 and not fd.is_deleted]
209 masked_labels = tracker_helpers.LabelsMaskedByFields(config, field_names)
210 field_names_lower = [field_name.lower() for field_name in field_names]
211 for wkl in wkl_tuples:
212 conflict = tracker_bizobj.LabelIsMaskedByField(wkl[0], field_names_lower)
213 if conflict:
214 mr.errors.label_defs = (
215 'Label "%s" should be defined in enum "%s"' % (wkl[0], conflict))
216 wkl_tuples.extend([
217 (masked.name, masked.docstring, False) for masked in masked_labels])
218
219 excl_prefix_text = post_data.get('excl_prefixes', '')
220 excl_prefixes = framework_constants.IDENTIFIER_RE.findall(excl_prefix_text)
221
222 if mr.errors.AnyErrors():
223 self.PleaseCorrect(mr, labels_text=wkl_text)
224 return
225
226 self.services.config.UpdateConfig(
227 mr.cnxn, mr.project,
228 well_known_labels=wkl_tuples, excl_label_prefixes=excl_prefixes)
229
230 # TODO(jrobbins): define a "strict" mode that affects only labels.
231
232 return urls.ADMIN_LABELS
233
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200234 # def GetAdminLabelsPage(self, **kwargs):
235 # return self.handler(**kwargs)
236
237 # def PostAdminLabelsPage(self, **kwargs):
238 # return self.handler(**kwargs)
239
Copybara854996b2021-09-07 19:36:02 +0000240
241class AdminTemplates(IssueAdminBase):
242 """Servlet allowing project owners to configure templates."""
243
244 _PAGE_TEMPLATE = 'tracker/admin-templates-page.ezt'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200245 _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_TEMPLATES
Copybara854996b2021-09-07 19:36:02 +0000246
247 def GatherPageData(self, mr):
248 """Build up a dictionary of data values to use when rendering the page.
249
250 Args:
251 mr: commonly used info parsed from the request.
252
253 Returns:
254 Dict of values used by EZT for rendering the page.
255 """
256 return super(AdminTemplates, self).GatherPageData(mr)
257
258 def ProcessSubtabForm(self, post_data, mr):
259 """Process changes to new issue templates.
260
261 Args:
262 post_data: HTML form data for the HTTP request being processed.
263 mr: commonly used info parsed from the request.
264
265 Returns:
266 The URL of the page to show after processing.
267 """
268 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
269 raise permissions.PermissionException(
270 'Only project owners may edit the default templates')
271
272 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
273
274 templates = self.services.template.GetProjectTemplates(mr.cnxn,
275 config.project_id)
276 default_template_id_for_developers, default_template_id_for_users = (
277 self._ParseDefaultTemplateSelections(post_data, templates))
278 if default_template_id_for_developers or default_template_id_for_users:
279 self.services.config.UpdateConfig(
280 mr.cnxn, mr.project,
281 default_template_for_developers=default_template_id_for_developers,
282 default_template_for_users=default_template_id_for_users)
283
284 return urls.ADMIN_TEMPLATES
285
286 def _ParseDefaultTemplateSelections(self, post_data, templates):
287 """Parse the input for the default templates to offer users."""
288 def GetSelectedTemplateID(name):
289 """Find the ID of the template specified in post_data[name]."""
290 if name not in post_data:
291 return None
292 selected_template_name = post_data[name]
293 for template in templates:
294 if selected_template_name == template.name:
295 return template.template_id
296
297 logging.error('User somehow selected an invalid template: %r',
298 selected_template_name)
299 return None
300
301 return (GetSelectedTemplateID('default_template_for_developers'),
302 GetSelectedTemplateID('default_template_for_users'))
303
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200304 # def GetAdminTemplatesPage(self, **kwargs):
305 # return self.handler(**kwargs)
306
307 # def PostAdminTemplatesPage(self, **kwargs):
308 # return self.handler(**kwargs)
309
Copybara854996b2021-09-07 19:36:02 +0000310
311class AdminComponents(IssueAdminBase):
312 """Servlet allowing project owners to view the list of components."""
313
314 _PAGE_TEMPLATE = 'tracker/admin-components-page.ezt'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200315 _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_COMPONENTS
Copybara854996b2021-09-07 19:36:02 +0000316
317 def GatherPageData(self, mr):
318 """Build up a dictionary of data values to use when rendering the page.
319
320 Args:
321 mr: commonly used info parsed from the request.
322
323 Returns:
324 Dict of values used by EZT for rendering the page.
325 """
326 page_data = super(AdminComponents, self).GatherPageData(mr)
327 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
328 users_by_id = framework_views.MakeAllUserViews(
329 mr.cnxn, self.services.user,
330 *[list(cd.admin_ids) + list(cd.cc_ids)
331 for cd in config.component_defs])
332 framework_views.RevealAllEmailsToMembers(
333 mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
334 component_def_views = [
335 tracker_views.ComponentDefView(mr.cnxn, self.services, cd, users_by_id)
336 # TODO(jrobbins): future component-level view restrictions.
337 for cd in config.component_defs]
338 for cd in component_def_views:
339 if mr.auth.email in [user.email for user in cd.admins]:
340 cd.classes += 'myadmin '
341 if mr.auth.email in [user.email for user in cd.cc]:
342 cd.classes += 'mycc '
343
344 page_data.update({
345 'component_defs': component_def_views,
346 'failed_perm': mr.GetParam('failed_perm'),
347 'failed_subcomp': mr.GetParam('failed_subcomp'),
348 'failed_templ': mr.GetParam('failed_templ'),
349 })
350 return page_data
351
352 def _GetComponentDefs(self, _mr, post_data, config):
353 """Get the config and component definitions from the request."""
354 component_defs = []
355 component_paths = post_data.get('delete_components').split(',')
356 for component_path in component_paths:
357 component_def = tracker_bizobj.FindComponentDef(component_path, config)
358 component_defs.append(component_def)
359 return component_defs
360
361 def _ProcessDeleteComponent(self, mr, component_def):
362 """Delete the specified component and its references."""
363 self.services.issue.DeleteComponentReferences(
364 mr.cnxn, component_def.component_id)
365 self.services.config.DeleteComponentDef(
366 mr.cnxn, mr.project_id, component_def.component_id)
367
368 def ProcessFormData(self, mr, post_data):
369 """Processes a POST command to delete components.
370
371 Args:
372 mr: commonly used info parsed from the request.
373 post_data: HTML form data from the request.
374
375 Returns:
376 String URL to redirect the user to, or None if response was already sent.
377 """
378 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
379 component_defs = self._GetComponentDefs(mr, post_data, config)
380 # Reverse the component_defs so that we start deleting from subcomponents.
381 component_defs.reverse()
382
383 # Collect errors.
384 perm_errors = []
385 subcomponents_errors = []
386 templates_errors = []
387 # Collect successes.
388 deleted_components = []
389
390 for component_def in component_defs:
391 allow_edit = permissions.CanEditComponentDef(
392 mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
393 if not allow_edit:
394 perm_errors.append(component_def.path)
395
396 subcomponents = tracker_bizobj.FindDescendantComponents(
397 config, component_def)
398 if subcomponents:
399 subcomponents_errors.append(component_def.path)
400
401 templates = self.services.template.TemplatesWithComponent(
402 mr.cnxn, component_def.component_id)
403 if templates:
404 templates_errors.append(component_def.path)
405
406 allow_delete = allow_edit and not subcomponents and not templates
407 if allow_delete:
408 self._ProcessDeleteComponent(mr, component_def)
409 deleted_components.append(component_def.path)
410 # Refresh project config after the component deletion.
411 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
412
413 return framework_helpers.FormatAbsoluteURL(
414 mr, urls.ADMIN_COMPONENTS, ts=int(time.time()),
415 failed_perm=','.join(perm_errors),
416 failed_subcomp=','.join(subcomponents_errors),
417 failed_templ=','.join(templates_errors),
418 deleted=','.join(deleted_components))
419
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200420 # def GetAdminComponentsPage(self, **kwargs):
421 # return self.handler(**kwargs)
422
423 # def PostAdminComponentsPage(self, **kwargs):
424 # return self.handler(**kwargs)
425
Copybara854996b2021-09-07 19:36:02 +0000426
427class AdminViews(IssueAdminBase):
428 """Servlet for project owners to set default columns, axes, and sorting."""
429
430 _PAGE_TEMPLATE = 'tracker/admin-views-page.ezt'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200431 _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_VIEWS
Copybara854996b2021-09-07 19:36:02 +0000432
433 def GatherPageData(self, mr):
434 """Build up a dictionary of data values to use when rendering the page.
435
436 Args:
437 mr: commonly used info parsed from the request.
438
439 Returns:
440 Dict of values used by EZT for rendering the page.
441 """
442 page_data = super(AdminViews, self).GatherPageData(mr)
443 with mr.profiler.Phase('getting canned queries'):
444 canned_queries = self.services.features.GetCannedQueriesByProjectID(
445 mr.cnxn, mr.project_id)
446 canned_query_views = [
447 savedqueries_helpers.SavedQueryView(sq, idx + 1, None, None)
448 for idx, sq in enumerate(canned_queries)]
449
450 page_data.update({
451 'canned_queries': canned_query_views,
452 'new_query_indexes': list(range(
453 len(canned_queries) + 1, savedqueries_helpers.MAX_QUERIES + 1)),
454 'issue_notify': mr.project.issue_notify_address,
455 'max_queries': savedqueries_helpers.MAX_QUERIES,
456 })
457 return page_data
458
459 def ProcessSubtabForm(self, post_data, mr):
460 """Process the Views subtab.
461
462 Args:
463 post_data: HTML form data for the HTTP request being processed.
464 mr: commonly used info parsed from the request.
465
466 Returns:
467 The URL of the page to show after processing.
468 """
469 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
470 raise permissions.PermissionException(
471 'Only project owners may edit the default views')
472 existing_queries = savedqueries_helpers.ParseSavedQueries(
473 mr.cnxn, post_data, self.services.project)
474 added_queries = savedqueries_helpers.ParseSavedQueries(
475 mr.cnxn, post_data, self.services.project, prefix='new_')
476 canned_queries = existing_queries + added_queries
477
478 list_prefs = _ParseListPreferences(post_data)
479
480 if mr.errors.AnyErrors():
481 self.PleaseCorrect(mr)
482 return
483
484 self.services.config.UpdateConfig(
485 mr.cnxn, mr.project, list_prefs=list_prefs)
486 self.services.features.UpdateCannedQueries(
487 mr.cnxn, mr.project_id, canned_queries)
488
489 return urls.ADMIN_VIEWS
490
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200491 # def GetAdminViewsPage(self, **kwargs):
492 # return self.handler(**kwargs)
493
494 # def PostAdminViewsPage(self, **kwargs):
495 # return self.handler(**kwargs)
496
Copybara854996b2021-09-07 19:36:02 +0000497
498def _ParseListPreferences(post_data):
499 """Parse the part of a project admin form about artifact list preferences."""
500 default_col_spec = ''
501 if 'default_col_spec' in post_data:
502 default_col_spec = post_data['default_col_spec']
503 # Don't allow empty colum spec
504 if not default_col_spec:
505 default_col_spec = tracker_constants.DEFAULT_COL_SPEC
506 col_spec_words = monorailrequest.ParseColSpec(
507 default_col_spec, max_parts=framework_constants.MAX_COL_PARTS)
508 col_spec = ' '.join(word for word in col_spec_words)
509
510 default_sort_spec = ''
511 if 'default_sort_spec' in post_data:
512 default_sort_spec = post_data['default_sort_spec']
513 sort_spec_words = monorailrequest.ParseColSpec(default_sort_spec)
514 sort_spec = ' '.join(sort_spec_words)
515
516 x_attr_str = ''
517 if 'default_x_attr' in post_data:
518 x_attr_str = post_data['default_x_attr']
519 x_attr_words = monorailrequest.ParseColSpec(x_attr_str)
520 x_attr = ''
521 if x_attr_words:
522 x_attr = x_attr_words[0]
523
524 y_attr_str = ''
525 if 'default_y_attr' in post_data:
526 y_attr_str = post_data['default_y_attr']
527 y_attr_words = monorailrequest.ParseColSpec(y_attr_str)
528 y_attr = ''
529 if y_attr_words:
530 y_attr = y_attr_words[0]
531
532 member_default_query = ''
533 if 'member_default_query' in post_data:
534 member_default_query = post_data['member_default_query']
535
536 return col_spec, sort_spec, x_attr, y_attr, member_default_query
537
538
539class AdminRules(IssueAdminBase):
540 """Servlet allowing project owners to configure filter rules."""
541
542 _PAGE_TEMPLATE = 'tracker/admin-rules-page.ezt'
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200543 _PROCESS_SUBTAB = flaskservlet.FlaskServlet.PROCESS_TAB_RULES
Copybara854996b2021-09-07 19:36:02 +0000544
545 def AssertBasePermission(self, mr):
546 """Check whether the user has any permission to visit this page.
547
548 Args:
549 mr: commonly used info parsed from the request.
550 """
551 super(AdminRules, self).AssertBasePermission(mr)
552 if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
553 raise permissions.PermissionException(
554 'User is not allowed to administer this project')
555
556 def GatherPageData(self, mr):
557 """Build up a dictionary of data values to use when rendering the page.
558
559 Args:
560 mr: commonly used info parsed from the request.
561
562 Returns:
563 Dict of values used by EZT for rendering the page.
564 """
565 page_data = super(AdminRules, self).GatherPageData(mr)
566 rules = self.services.features.GetFilterRules(
567 mr.cnxn, mr.project_id)
568 users_by_id = framework_views.MakeAllUserViews(
569 mr.cnxn, self.services.user,
570 [rule.default_owner_id for rule in rules],
571 *[rule.add_cc_ids for rule in rules])
572 framework_views.RevealAllEmailsToMembers(
573 mr.cnxn, self.services, mr.auth, users_by_id, mr.project)
574 rule_views = [filterrules_views.RuleView(rule, users_by_id)
575 for rule in rules]
576
577 for idx, rule_view in enumerate(rule_views):
578 rule_view.idx = idx + 1 # EZT has no loop index, so we set idx.
579
580 page_data.update({
581 'rules': rule_views,
582 'new_rule_indexes': (
583 list(range(len(rules) + 1, filterrules_helpers.MAX_RULES + 1))),
584 'max_rules': filterrules_helpers.MAX_RULES,
585 })
586 return page_data
587
588 def ProcessSubtabForm(self, post_data, mr):
589 """Process the Rules subtab.
590
591 Args:
592 post_data: HTML form data for the HTTP request being processed.
593 mr: commonly used info parsed from the request.
594
595 Returns:
596 The URL of the page to show after processing.
597 """
598 old_rules = self.services.features.GetFilterRules(mr.cnxn, mr.project_id)
599 rules = filterrules_helpers.ParseRules(
600 mr.cnxn, post_data, self.services.user, mr.errors)
601 new_rules = filterrules_helpers.ParseRules(
602 mr.cnxn, post_data, self.services.user, mr.errors, prefix='new_')
603 rules.extend(new_rules)
604
605 if mr.errors.AnyErrors():
606 self.PleaseCorrect(mr)
607 return
608
609 config = self.services.features.UpdateFilterRules(
610 mr.cnxn, mr.project_id, rules)
611
612 if old_rules != rules:
613 logging.info('recomputing derived fields')
614 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
615 filterrules_helpers.RecomputeAllDerivedFields(
616 mr.cnxn, self.services, mr.project, config)
617
618 return urls.ADMIN_RULES
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200619
620 # def GetAdminRulesPage(self, **kwargs):
621 # return self.handler(**kwargs)
622
623 # def PostAdminRulesPage(self, **kwargs):
624 # return self.handler(**kwargs)