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 |