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

Unified 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 side-by-side diff with in-line comments
Download patch
Index: appengine/monorail/tracker/issueadmin.py
diff --git a/appengine/monorail/tracker/issueadmin.py b/appengine/monorail/tracker/issueadmin.py
new file mode 100644
index 0000000000000000000000000000000000000000..2fb367f9a800d78386964f96d362bd814c3e0236
--- /dev/null
+++ b/appengine/monorail/tracker/issueadmin.py
@@ -0,0 +1,656 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is govered by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Servlets for issue tracker configuration.
+
+These classes implement the Statuses, Labels and fields, Components, Rules, and
+Views subtabs under the Process tab. Unlike most servlet modules, this single
+file holds a base class and several related servlet classes.
+"""
+
+import collections
+import itertools
+import logging
+import time
+
+from features import filterrules_helpers
+from features import filterrules_views
+from features import savedqueries_helpers
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import monorailrequest
+from framework import permissions
+from framework import servlet
+from framework import urls
+from tracker import field_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+
+class IssueAdminBase(servlet.Servlet):
+ """Base class for servlets allowing project owners to configure tracker."""
+
+ _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
+ _PROCESS_SUBTAB = None # specified in subclasses
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ config_view = tracker_views.ConfigView(mr, self.services, config)
+ return {
+ 'admin_tab_mode': self._PROCESS_SUBTAB,
+ 'config': config_view,
+ }
+
+ def ProcessFormData(self, mr, post_data):
+ """Validate and store the contents of the issues tracker admin page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ post_data: HTML form data from the request.
+
+ Returns:
+ String URL to redirect the user to, or None if response was already sent.
+ """
+ page_url = self.ProcessSubtabForm(post_data, mr)
+
+ if mr.errors.AnyErrors():
+ self.PleaseCorrect(mr) # TODO(jrobbins): echo more user-entered text.
+ else:
+ return framework_helpers.FormatAbsoluteURL(
+ mr, page_url, saved=1, ts=int(time.time()))
+
+
+class AdminStatuses(IssueAdminBase):
+ """Servlet allowing project owners to configure well-known statuses."""
+
+ _PAGE_TEMPLATE = 'tracker/admin-statuses-page.ezt'
+ _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_STATUSES
+
+ def ProcessSubtabForm(self, post_data, mr):
+ """Process the status definition section of the admin page.
+
+ Args:
+ post_data: HTML form data for the HTTP request being processed.
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ The URL of the page to show after processing.
+ """
+ wks_open_text = post_data.get('predefinedopen', '')
+ wks_open_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(
+ wks_open_text)
+ wks_open_tuples = [
+ (status.lstrip('#'), docstring.strip(), True, status.startswith('#'))
+ for status, docstring in wks_open_matches]
+
+ wks_closed_text = post_data.get('predefinedclosed', '')
+ wks_closed_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(
+ wks_closed_text)
+ wks_closed_tuples = [
+ (status.lstrip('#'), docstring.strip(), False, status.startswith('#'))
+ for status, docstring in wks_closed_matches]
+
+ statuses_offer_merge_text = post_data.get('statuses_offer_merge', '')
+ statuses_offer_merge = framework_constants.IDENTIFIER_RE.findall(
+ statuses_offer_merge_text)
+
+ if not mr.errors.AnyErrors():
+ self.services.config.UpdateConfig(
+ mr.cnxn, mr.project, statuses_offer_merge=statuses_offer_merge,
+ well_known_statuses=wks_open_tuples + wks_closed_tuples)
+
+ # TODO(jrobbins): define a "strict" mode that affects only statuses.
+
+ return urls.ADMIN_STATUSES
+
+
+class AdminLabels(IssueAdminBase):
+ """Servlet allowing project owners to labels and fields."""
+
+ _PAGE_TEMPLATE = 'tracker/admin-labels-page.ezt'
+ _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_LABELS
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ page_data = super(AdminLabels, self).GatherPageData(mr)
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ field_def_views = [
+ tracker_views.FieldDefView(fd, config)
+ # TODO(jrobbins): future field-level view restrictions.
+ for fd in config.field_defs
+ if not fd.is_deleted]
+ page_data.update({
+ 'field_defs': field_def_views,
+ })
+ return page_data
+
+ def ProcessSubtabForm(self, post_data, mr):
+ """Process changes to labels and custom field definitions.
+
+ Args:
+ post_data: HTML form data for the HTTP request being processed.
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ The URL of the page to show after processing.
+ """
+ wkl_text = post_data.get('predefinedlabels', '')
+ wkl_matches = framework_constants.IDENTIFIER_DOCSTRING_RE.findall(wkl_text)
+ wkl_tuples = [
+ (label.lstrip('#'), docstring.strip(), label.startswith('#'))
+ for label, docstring in wkl_matches]
+
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ field_names = [fd.field_name for fd in config.field_defs
+ if not fd.is_deleted]
+ masked_labels = tracker_helpers.LabelsMaskedByFields(config, field_names)
+ wkl_tuples.extend([
+ (masked.name, masked.docstring, False) for masked in masked_labels])
+
+ excl_prefix_text = post_data.get('excl_prefixes', '')
+ excl_prefixes = framework_constants.IDENTIFIER_RE.findall(excl_prefix_text)
+
+ if not mr.errors.AnyErrors():
+ self.services.config.UpdateConfig(
+ mr.cnxn, mr.project,
+ well_known_labels=wkl_tuples, excl_label_prefixes=excl_prefixes)
+
+ # TODO(jrobbins): define a "strict" mode that affects only labels.
+
+ return urls.ADMIN_LABELS
+
+
+class AdminTemplates(IssueAdminBase):
+ """Servlet allowing project owners to configure templates."""
+
+ _PAGE_TEMPLATE = 'tracker/admin-templates-page.ezt'
+ _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_TEMPLATES
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ page_data = super(AdminTemplates, self).GatherPageData(mr)
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ field_views = [
+ tracker_views.MakeFieldValueView(fd, config, [], [], [], {})
+ # TODO(jrobbins): field-level view restrictions, display options
+ for fd in config.field_defs
+ if not fd.is_deleted]
+
+ page_data.update({
+ 'fields': field_views,
+ })
+ return page_data
+
+ def ProcessSubtabForm(self, post_data, mr):
+ """Process changes to new issue templates.
+
+ Args:
+ post_data: HTML form data for the HTTP request being processed.
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ The URL of the page to show after processing.
+ """
+ templates = self._ParseAllTemplates(post_data, mr)
+
+ default_template_id_for_developers = None
+ default_template_id_for_users = None
+ if self.CheckPerm(mr, permissions.EDIT_PROJECT):
+ default_template_id_for_developers, default_template_id_for_users = (
+ self._ParseDefaultTemplateSelections(post_data, templates))
+
+ if not mr.errors.AnyErrors():
+ self.services.config.UpdateConfig(
+ mr.cnxn, mr.project, templates=templates,
+ default_template_for_developers=default_template_id_for_developers,
+ default_template_for_users=default_template_id_for_users)
+
+ params = '';
+ if post_data.get('current_template_index'):
+ params = '?tindex=' + post_data['current_template_index']
+ return urls.ADMIN_TEMPLATES + params
+
+ def _ParseAllTemplates(self, post_data, mr):
+ """Iterate over the post_data and parse all templates in it."""
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ orig_templates = {tmpl.template_id: tmpl for tmpl in config.templates}
+
+ templates = []
+ for i in itertools.count():
+ if ('template_id_%s' % i) not in post_data:
+ break
+ template_id = int(post_data['template_id_%s' % i])
+ orig_template = orig_templates.get(template_id)
+ new_template = self._ParseTemplate(
+ post_data, mr, i, orig_template, config)
+ if new_template:
+ templates.append(new_template)
+
+ return templates
+
+ def _ParseTemplate(self, post_data, mr, i, orig_template, config):
+ """Parse an issue template. Return orig_template if cannot edit."""
+ if not self._CanEditTemplate(mr, orig_template):
+ return orig_template
+
+ name = post_data['name_%s' % i]
+ if name == tracker_constants.DELETED_TEMPLATE_NAME:
+ return None
+
+ members_only = False
+ if ('members_only_%s' % i) in post_data:
+ members_only = (
+ post_data['members_only_%s' % i] == 'yes')
+ summary = ''
+ if ('summary_%s' % i) in post_data:
+ summary = post_data['summary_%s' % i]
+ summary_must_be_edited = False
+ if ('summary_must_be_edited_%s' % i) in post_data:
+ summary_must_be_edited = (
+ post_data['summary_must_be_edited_%s' % i] == 'yes')
+ content = ''
+ if ('content_%s' % i) in post_data:
+ content = post_data['content_%s' % i]
+ # wrap="hard" has no effect on the content because we copy it to
+ # a hidden form field before submission. So, server-side word wrap.
+ content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75)
+ status = ''
+ if ('status_%s' % i) in post_data:
+ status = post_data['status_%s' % i]
+ owner_id = 0
+ if ('owner_%s' % i) in post_data:
+ owner = post_data['owner_%s' % i]
+ if owner:
+ user_id = self.services.user.LookupUserID(mr.cnxn, owner)
+ auth = monorailrequest.AuthData.FromUserID(
+ mr.cnxn, user_id, self.services)
+ if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids):
+ owner_id = user_id
+
+ labels = post_data.getall('label_%s' % i)
+ labels_remove = []
+
+ field_val_strs = collections.defaultdict(list)
+ for fd in config.field_defs:
+ field_value_key = 'field_value_%d_%d' % (i, fd.field_id)
+ if post_data.get(field_value_key):
+ field_val_strs[fd.field_id].append(post_data[field_value_key])
+
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels, labels_remove, field_val_strs, {}, config)
+ field_values = field_helpers.ParseFieldValues(
+ mr.cnxn, self.services.user, field_val_strs, config)
+ for fv in field_values:
+ logging.info('field_value is %r: %r',
+ fv.field_id, tracker_bizobj.GetFieldValue(fv, {}))
+
+ admin_ids = []
+ if ('admin_names_%s' % i) in post_data:
+ admin_ids, _admin_str = tracker_helpers.ParseAdminUsers(
+ mr.cnxn, post_data['admin_names_%s' % i], self.services.user)
+
+ component_ids = []
+ if ('components_%s' % i) in post_data:
+ component_paths = []
+ for component_path in post_data['components_%s' % i].split(','):
+ if component_path.strip() not in component_paths:
+ component_paths.append(component_path.strip())
+ component_ids = tracker_helpers.LookupComponentIDs(
+ component_paths, config, mr.errors)
+
+ owner_defaults_to_member = False
+ if ('owner_defaults_to_member_%s' % i) in post_data:
+ owner_defaults_to_member = (
+ post_data['owner_defaults_to_member_%s' % i] == 'yes')
+
+ component_required = False
+ if ('component_required_%s' % i) in post_data:
+ component_required = post_data['component_required_%s' % i] == 'yes'
+
+ template = tracker_bizobj.MakeIssueTemplate(
+ name, summary, status, owner_id,
+ content, labels, field_values, admin_ids, component_ids,
+ summary_must_be_edited=summary_must_be_edited,
+ owner_defaults_to_member=owner_defaults_to_member,
+ component_required=component_required,
+ members_only=members_only)
+ template_id = int(post_data['template_id_%s' % i])
+ if template_id: # new templates have ID 0, so leave that None in PB.
+ template.template_id = template_id
+ logging.info('template is %r', template)
+
+ return template
+
+ def _CanEditTemplate(self, mr, template):
+ """Return True if the user is allowed to edit this template."""
+ if self.CheckPerm(mr, permissions.EDIT_PROJECT):
+ return True
+
+ if template and not mr.auth.effective_ids.isdisjoint(template.admin_ids):
+ return True
+
+ return False
+
+ def _ParseDefaultTemplateSelections(self, post_data, templates):
+ """Parse the input for the default templates to offer users."""
+ def GetSelectedTemplateID(name):
+ """Find the ID of the template specified in post_data[name]."""
+ if name not in post_data:
+ return None
+ selected_template_name = post_data[name]
+ for template in templates:
+ if selected_template_name == template.name:
+ return template.template_id
+
+ logging.error('User somehow selected an invalid template: %r',
+ selected_template_name)
+ return None
+
+ return (GetSelectedTemplateID('default_template_for_developers'),
+ GetSelectedTemplateID('default_template_for_users'))
+
+
+class AdminComponents(IssueAdminBase):
+ """Servlet allowing project owners to view the list of components."""
+
+ _PAGE_TEMPLATE = 'tracker/admin-components-page.ezt'
+ _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_COMPONENTS
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ page_data = super(AdminComponents, self).GatherPageData(mr)
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ users_by_id = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user,
+ *[list(cd.admin_ids) + list(cd.cc_ids)
+ for cd in config.component_defs])
+ framework_views.RevealAllEmailsToMembers(mr, users_by_id)
+ component_def_views = [
+ tracker_views.ComponentDefView(cd, users_by_id)
+ # TODO(jrobbins): future component-level view restrictions.
+ for cd in config.component_defs]
+ for cd in component_def_views:
+ if mr.auth.email in [user.email for user in cd.admins]:
+ cd.classes += 'myadmin '
+ if mr.auth.email in [user.email for user in cd.cc]:
+ cd.classes += 'mycc '
+
+ page_data.update({
+ 'component_defs': component_def_views,
+ 'failed_perm': mr.GetParam('failed_perm'),
+ 'failed_subcomp': mr.GetParam('failed_subcomp'),
+ 'failed_templ': mr.GetParam('failed_templ'),
+ })
+ return page_data
+
+ def _GetComponentDefs(self, _mr, post_data, config):
+ """Get the config and component definitions from the request."""
+ component_defs = []
+ component_paths = post_data.get('delete_components').split(',')
+ for component_path in component_paths:
+ component_def = tracker_bizobj.FindComponentDef(component_path, config)
+ component_defs.append(component_def)
+ return component_defs
+
+ def _ProcessDeleteComponent(self, mr, component_def):
+ """Delete the specified component and its references."""
+ self.services.issue.DeleteComponentReferences(
+ mr.cnxn, component_def.component_id)
+ self.services.config.DeleteComponentDef(
+ mr.cnxn, mr.project_id, component_def.component_id)
+
+ def ProcessFormData(self, mr, post_data):
+ """Processes a POST command to delete components.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ post_data: HTML form data from the request.
+
+ Returns:
+ String URL to redirect the user to, or None if response was already sent.
+ """
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ component_defs = self._GetComponentDefs(mr, post_data, config)
+ # Reverse the component_defs so that we start deleting from subcomponents.
+ component_defs.reverse()
+
+ # Collect errors.
+ perm_errors = []
+ subcomponents_errors = []
+ templates_errors = []
+ # Collect successes.
+ deleted_components = []
+
+ for component_def in component_defs:
+ allow_edit = permissions.CanEditComponentDef(
+ mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
+ if not allow_edit:
+ perm_errors.append(component_def.path)
+
+ subcomponents = tracker_bizobj.FindDescendantComponents(
+ config, component_def)
+ if subcomponents:
+ subcomponents_errors.append(component_def.path)
+
+ templates = self.services.config.TemplatesWithComponent(
+ mr.cnxn, component_def.component_id, config)
+ if templates:
+ templates_errors.append(component_def.path)
+
+ allow_delete = allow_edit and not subcomponents and not templates
+ if allow_delete:
+ self._ProcessDeleteComponent(mr, component_def)
+ deleted_components.append(component_def.path)
+ # Refresh project config after the component deletion.
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+
+ return framework_helpers.FormatAbsoluteURL(
+ mr, urls.ADMIN_COMPONENTS, ts=int(time.time()),
+ failed_perm=','.join(perm_errors),
+ failed_subcomp=','.join(subcomponents_errors),
+ failed_templ=','.join(templates_errors),
+ deleted=','.join(deleted_components))
+
+
+class AdminViews(IssueAdminBase):
+ """Servlet for project owners to set default columns, axes, and sorting."""
+
+ _PAGE_TEMPLATE = 'tracker/admin-views-page.ezt'
+ _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_VIEWS
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ page_data = super(AdminViews, self).GatherPageData(mr)
+ with self.profiler.Phase('getting canned queries'):
+ canned_queries = self.services.features.GetCannedQueriesByProjectID(
+ mr.cnxn, mr.project_id)
+
+ page_data.update({
+ 'new_query_indexes': range(
+ len(canned_queries) + 1, savedqueries_helpers.MAX_QUERIES + 1),
+ 'issue_notify': mr.project.issue_notify_address,
+ 'max_queries': savedqueries_helpers.MAX_QUERIES,
+ })
+ return page_data
+
+ def ProcessSubtabForm(self, post_data, mr):
+ """Process the Views subtab.
+
+ Args:
+ post_data: HTML form data for the HTTP request being processed.
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ The URL of the page to show after processing.
+ """
+ existing_queries = savedqueries_helpers.ParseSavedQueries(
+ mr.cnxn, post_data, self.services.project)
+ added_queries = savedqueries_helpers.ParseSavedQueries(
+ mr.cnxn, post_data, self.services.project, prefix='new_')
+ canned_queries = existing_queries + added_queries
+
+ list_prefs = _ParseListPreferences(post_data)
+
+ if not mr.errors.AnyErrors():
+ self.services.config.UpdateConfig(
+ mr.cnxn, mr.project, list_prefs=list_prefs)
+ self.services.features.UpdateCannedQueries(
+ mr.cnxn, mr.project_id, canned_queries)
+
+ return urls.ADMIN_VIEWS
+
+
+def _ParseListPreferences(post_data):
+ """Parse the part of a project admin form about artifact list preferences."""
+ default_col_spec = ''
+ if 'default_col_spec' in post_data:
+ default_col_spec = post_data['default_col_spec']
+ # Don't allow empty colum spec
+ if not default_col_spec:
+ default_col_spec = tracker_constants.DEFAULT_COL_SPEC
+ col_spec_words = monorailrequest.ParseColSpec(default_col_spec)
+ col_spec = ' '.join(word for word in col_spec_words)
+
+ default_sort_spec = ''
+ if 'default_sort_spec' in post_data:
+ default_sort_spec = post_data['default_sort_spec']
+ sort_spec_words = monorailrequest.ParseColSpec(default_sort_spec)
+ sort_spec = ' '.join(sort_spec_words)
+
+ x_attr_str = ''
+ if 'default_x_attr' in post_data:
+ x_attr_str = post_data['default_x_attr']
+ x_attr_words = monorailrequest.ParseColSpec(x_attr_str)
+ x_attr = ''
+ if x_attr_words:
+ x_attr = x_attr_words[0]
+
+ y_attr_str = ''
+ if 'default_y_attr' in post_data:
+ y_attr_str = post_data['default_y_attr']
+ y_attr_words = monorailrequest.ParseColSpec(y_attr_str)
+ y_attr = ''
+ if y_attr_words:
+ y_attr = y_attr_words[0]
+
+ return col_spec, sort_spec, x_attr, y_attr
+
+
+class AdminRules(IssueAdminBase):
+ """Servlet allowing project owners to configure filter rules."""
+
+ _PAGE_TEMPLATE = 'tracker/admin-rules-page.ezt'
+ _PROCESS_SUBTAB = servlet.Servlet.PROCESS_TAB_RULES
+
+ def AssertBasePermission(self, mr):
+ """Check whether the user has any permission to visit this page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+ """
+ super(AdminRules, self).AssertBasePermission(mr)
+ if not self.CheckPerm(mr, permissions.EDIT_PROJECT):
+ raise permissions.PermissionException(
+ 'User is not allowed to administer this project')
+
+ def GatherPageData(self, mr):
+ """Build up a dictionary of data values to use when rendering the page.
+
+ Args:
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ Dict of values used by EZT for rendering the page.
+ """
+ page_data = super(AdminRules, self).GatherPageData(mr)
+ rules = self.services.features.GetFilterRules(
+ mr.cnxn, mr.project_id)
+ users_by_id = framework_views.MakeAllUserViews(
+ mr.cnxn, self.services.user,
+ [rule.default_owner_id for rule in rules],
+ *[rule.add_cc_ids for rule in rules])
+ framework_views.RevealAllEmailsToMembers(mr, users_by_id)
+ rule_views = [filterrules_views.RuleView(rule, users_by_id)
+ for rule in rules]
+
+ for idx, rule_view in enumerate(rule_views):
+ rule_view.idx = idx + 1 # EZT has no loop index, so we set idx.
+
+ page_data.update({
+ 'rules': rule_views,
+ 'new_rule_indexes': (
+ range(len(rules) + 1, filterrules_helpers.MAX_RULES + 1)),
+ 'max_rules': filterrules_helpers.MAX_RULES,
+ })
+ return page_data
+
+ def ProcessSubtabForm(self, post_data, mr):
+ """Process the Rules subtab.
+
+ Args:
+ post_data: HTML form data for the HTTP request being processed.
+ mr: commonly used info parsed from the request.
+
+ Returns:
+ The URL of the page to show after processing.
+ """
+ old_rules = self.services.features.GetFilterRules(mr.cnxn, mr.project_id)
+ rules = filterrules_helpers.ParseRules(
+ mr.cnxn, post_data, self.services.user, mr.errors)
+ new_rules = filterrules_helpers.ParseRules(
+ mr.cnxn, post_data, self.services.user, mr.errors, prefix='new_')
+ rules.extend(new_rules)
+
+ if not mr.errors.AnyErrors():
+ config = self.services.features.UpdateFilterRules(
+ mr.cnxn, mr.project_id, rules)
+
+ if old_rules != rules:
+ logging.info('recomputing derived fields')
+ config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
+ filterrules_helpers.RecomputeAllDerivedFields(
+ mr.cnxn, self.services, mr.project, config)
+
+ return urls.ADMIN_RULES
« 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