Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/tracker/componentdetail.py b/tracker/componentdetail.py
new file mode 100644
index 0000000..01f2469
--- /dev/null
+++ b/tracker/componentdetail.py
@@ -0,0 +1,246 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed 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."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+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(
+ mr.cnxn, self.services, 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]
+ initial_labels = [
+ self.services.config.LookupLabel(mr.cnxn, mr.project_id, label_id)
+ for label_id in component_def.label_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.template.TemplatesWithComponent(
+ mr.cnxn, component_def.component_id)
+ 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,
+ 'initial_labels': initial_labels,
+ '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)
+ 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)
+
+ 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,
+ initial_labels=parsed.label_strs,
+ 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, label_ids=parsed.label_ids)
+
+ 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) or
+ set(parsed.label_ids) != set(component_def.label_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()))