| 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
|
|
|