| Index: appengine/monorail/tracker/componentdetail.py
|
| diff --git a/appengine/monorail/tracker/componentdetail.py b/appengine/monorail/tracker/componentdetail.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..751f7ec739c59481d18f6bc22db2a27a72d8bf9e
|
| --- /dev/null
|
| +++ b/appengine/monorail/tracker/componentdetail.py
|
| @@ -0,0 +1,238 @@
|
| +# 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
|
| +
|
| +"""A servlet for project and component owners to view and edit components."""
|
| +
|
| +import logging
|
| +import time
|
| +
|
| +from third_party import ezt
|
| +
|
| +from features import filterrules_helpers
|
| +from framework import framework_helpers
|
| +from framework import framework_views
|
| +from framework import permissions
|
| +from framework import servlet
|
| +from framework import timestr
|
| +from framework import urls
|
| +from tracker import component_helpers
|
| +from tracker import tracker_bizobj
|
| +from tracker import tracker_constants
|
| +from tracker import tracker_views
|
| +
|
| +
|
| +class ComponentDetail(servlet.Servlet):
|
| + """Servlets allowing project owners to view and edit a component."""
|
| +
|
| + _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
|
| + _PAGE_TEMPLATE = 'tracker/component-detail-page.ezt'
|
| +
|
| + def _GetComponentDef(self, mr):
|
| + """Get the config and component definition to be viewed or edited."""
|
| + if not mr.component_path:
|
| + self.abort(404, 'component not specified')
|
| + config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
|
| + component_def = tracker_bizobj.FindComponentDef(mr.component_path, config)
|
| + if not component_def:
|
| + self.abort(404, 'component not found')
|
| + return config, component_def
|
| +
|
| + 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(ComponentDetail, self).AssertBasePermission(mr)
|
| + _config, component_def = self._GetComponentDef(mr)
|
| +
|
| + # TODO(jrobbins): optional restrictions on viewing fields by component.
|
| +
|
| + allow_view = permissions.CanViewComponentDef(
|
| + mr.auth.effective_ids, mr.perms, mr.project, component_def)
|
| + if not allow_view:
|
| + raise permissions.PermissionException(
|
| + 'User is not allowed to view this component')
|
| +
|
| + 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, component_def = self._GetComponentDef(mr)
|
| + users_by_id = framework_views.MakeAllUserViews(
|
| + mr.cnxn, self.services.user,
|
| + component_def.admin_ids, component_def.cc_ids)
|
| + component_def_view = tracker_views.ComponentDefView(
|
| + component_def, users_by_id)
|
| + initial_admins = [users_by_id[uid].email for uid in component_def.admin_ids]
|
| + initial_cc = [users_by_id[uid].email for uid in component_def.cc_ids]
|
| +
|
| + creator, created = self._GetUserViewAndFormattedTime(
|
| + mr, component_def.creator_id, component_def.created)
|
| + modifier, modified = self._GetUserViewAndFormattedTime(
|
| + mr, component_def.modifier_id, component_def.modified)
|
| +
|
| + allow_edit = permissions.CanEditComponentDef(
|
| + mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
|
| +
|
| + subcomponents = tracker_bizobj.FindDescendantComponents(
|
| + config, component_def)
|
| + templates = self.services.config.TemplatesWithComponent(
|
| + mr.cnxn, component_def.component_id, config)
|
| + allow_delete = allow_edit and not subcomponents and not templates
|
| +
|
| + return {
|
| + 'admin_tab_mode': servlet.Servlet.PROCESS_TAB_COMPONENTS,
|
| + 'component_def': component_def_view,
|
| + 'initial_leaf_name': component_def_view.leaf_name,
|
| + 'initial_docstring': component_def.docstring,
|
| + 'initial_deprecated': ezt.boolean(component_def.deprecated),
|
| + 'initial_admins': initial_admins,
|
| + 'initial_cc': initial_cc,
|
| + 'allow_edit': ezt.boolean(allow_edit),
|
| + 'allow_delete': ezt.boolean(allow_delete),
|
| + 'subcomponents': subcomponents,
|
| + 'templates': templates,
|
| + 'creator': creator,
|
| + 'created': created,
|
| + 'modifier': modifier,
|
| + 'modified': modified,
|
| + }
|
| +
|
| + 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.
|
| + """
|
| + config, component_def = self._GetComponentDef(mr)
|
| + allow_edit = permissions.CanEditComponentDef(
|
| + mr.auth.effective_ids, mr.perms, mr.project, component_def, config)
|
| + if not allow_edit:
|
| + raise permissions.PermissionException(
|
| + 'User is not allowed to edit or delete this component')
|
| +
|
| + if 'deletecomponent' in post_data:
|
| + allow_delete = not tracker_bizobj.FindDescendantComponents(
|
| + config, component_def)
|
| + if not allow_delete:
|
| + raise permissions.PermissionException(
|
| + 'User tried to delete component that had subcomponents')
|
| + return self._ProcessDeleteComponent(mr, component_def)
|
| +
|
| + else:
|
| + return self._ProcessEditComponent(mr, post_data, config, component_def)
|
| +
|
| +
|
| + def _ProcessDeleteComponent(self, mr, component_def):
|
| + """The user wants to delete the specified custom field definition."""
|
| + self.services.issue.DeleteComponentReferences(
|
| + mr.cnxn, component_def.component_id)
|
| + self.services.config.DeleteComponentDef(
|
| + mr.cnxn, mr.project_id, component_def.component_id)
|
| + return framework_helpers.FormatAbsoluteURL(
|
| + mr, urls.ADMIN_COMPONENTS, deleted=1, ts=int(time.time()))
|
| +
|
| + def _GetUserViewAndFormattedTime(self, mr, user_id, timestamp):
|
| + formatted_time = (timestr.FormatAbsoluteDate(timestamp)
|
| + if timestamp else None)
|
| + user = self.services.user.GetUser(mr.cnxn, user_id) if user_id else None
|
| + user_view = None
|
| + if user:
|
| + user_view = framework_views.UserView(
|
| + user_id, user.email, user.obscure_email)
|
| + viewing_self = mr.auth.user_id == user_id
|
| + # Do not obscure email if current user is a site admin. Do not obscure
|
| + # email if current user is the same as the creator. For all other
|
| + # cases do whatever obscure_email setting for the user is.
|
| + email_obscured = (not(mr.auth.user_pb.is_site_admin or viewing_self)
|
| + and user_view.obscure_email)
|
| + if not email_obscured:
|
| + user_view.RevealEmail()
|
| +
|
| + return user_view, formatted_time
|
| +
|
| + def _ProcessEditComponent(self, mr, post_data, config, component_def):
|
| + """The user wants to edit this component definition."""
|
| + parsed = component_helpers.ParseComponentRequest(
|
| + mr, post_data, self.services.user)
|
| +
|
| + if not tracker_constants.COMPONENT_NAME_RE.match(parsed.leaf_name):
|
| + mr.errors.leaf_name = 'Invalid component name'
|
| +
|
| + original_path = component_def.path
|
| + if mr.component_path and '>' in mr.component_path:
|
| + parent_path = mr.component_path[:mr.component_path.rindex('>')]
|
| + new_path = '%s>%s' % (parent_path, parsed.leaf_name)
|
| + else:
|
| + new_path = parsed.leaf_name
|
| +
|
| + conflict = tracker_bizobj.FindComponentDef(new_path, config)
|
| + if conflict and conflict.component_id != component_def.component_id:
|
| + mr.errors.leaf_name = 'That name is already in use.'
|
| +
|
| + creator, created = self._GetUserViewAndFormattedTime(
|
| + mr, component_def.creator_id, component_def.created)
|
| + modifier, modified = self._GetUserViewAndFormattedTime(
|
| + mr, component_def.modifier_id, component_def.modified)
|
| +
|
| + if mr.errors.AnyErrors():
|
| + self.PleaseCorrect(
|
| + mr, initial_leaf_name=parsed.leaf_name,
|
| + initial_docstring=parsed.docstring,
|
| + initial_deprecated=ezt.boolean(parsed.deprecated),
|
| + initial_admins=parsed.admin_usernames,
|
| + initial_cc=parsed.cc_usernames,
|
| + created=created,
|
| + creator=creator,
|
| + modified=modified,
|
| + modifier=modifier,
|
| + )
|
| + return None
|
| +
|
| + new_modified = int(time.time())
|
| + new_modifier_id = self.services.user.LookupUserID(
|
| + mr.cnxn, mr.auth.email, autocreate=False)
|
| + self.services.config.UpdateComponentDef(
|
| + mr.cnxn, mr.project_id, component_def.component_id,
|
| + path=new_path, docstring=parsed.docstring, deprecated=parsed.deprecated,
|
| + admin_ids=parsed.admin_ids, cc_ids=parsed.cc_ids, modified=new_modified,
|
| + modifier_id=new_modifier_id)
|
| +
|
| + update_rule = False
|
| + if new_path != original_path:
|
| + update_rule = True
|
| + # If the name changed then update all of its subcomponents as well.
|
| + subcomponent_ids = tracker_bizobj.FindMatchingComponentIDs(
|
| + original_path, config, exact=False)
|
| + for subcomponent_id in subcomponent_ids:
|
| + if subcomponent_id == component_def.component_id:
|
| + continue
|
| + subcomponent_def = tracker_bizobj.FindComponentDefByID(
|
| + subcomponent_id, config)
|
| + subcomponent_new_path = subcomponent_def.path.replace(
|
| + original_path, new_path, 1)
|
| + self.services.config.UpdateComponentDef(
|
| + mr.cnxn, mr.project_id, subcomponent_def.component_id,
|
| + path=subcomponent_new_path)
|
| +
|
| + if set(parsed.cc_ids) != set(component_def.cc_ids):
|
| + update_rule = True
|
| + if update_rule:
|
| + filterrules_helpers.RecomputeAllDerivedFields(
|
| + mr.cnxn, self.services, mr.project, config)
|
| +
|
| + return framework_helpers.FormatAbsoluteURL(
|
| + mr, urls.COMPONENT_DETAIL,
|
| + component=new_path, saved=1, ts=int(time.time()))
|
|
|