blob: 177e548b9241d479c9ce51f44c4c2e8bcbc96ba3 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""A servlet for project and component owners to view and edit components."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import logging
11import time
12
13import ezt
14
15from features import filterrules_helpers
16from framework import framework_helpers
17from framework import framework_views
18from framework import permissions
19from framework import servlet
20from framework import timestr
21from framework import urls
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010022from gae_ts_mon import flask_handlers
Copybara854996b2021-09-07 19:36:02 +000023from tracker import component_helpers
24from tracker import tracker_bizobj
25from tracker import tracker_constants
26from tracker import tracker_views
27
28
29class ComponentDetail(servlet.Servlet):
30 """Servlets allowing project owners to view and edit a component."""
31
32 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_PROCESS
33 _PAGE_TEMPLATE = 'tracker/component-detail-page.ezt'
34
35 def _GetComponentDef(self, mr):
36 """Get the config and component definition to be viewed or edited."""
37 if not mr.component_path:
38 self.abort(404, 'component not specified')
39 config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
40 component_def = tracker_bizobj.FindComponentDef(mr.component_path, config)
41 if not component_def:
42 self.abort(404, 'component not found')
43 return config, component_def
44
45 def AssertBasePermission(self, mr):
46 """Check whether the user has any permission to visit this page.
47
48 Args:
49 mr: commonly used info parsed from the request.
50 """
51 super(ComponentDetail, self).AssertBasePermission(mr)
52 _config, component_def = self._GetComponentDef(mr)
53
54 # TODO(jrobbins): optional restrictions on viewing fields by component.
55
56 allow_view = permissions.CanViewComponentDef(
57 mr.auth.effective_ids, mr.perms, mr.project, component_def)
58 if not allow_view:
59 raise permissions.PermissionException(
60 'User is not allowed to view this component')
61
62 def GatherPageData(self, mr):
63 """Build up a dictionary of data values to use when rendering the page.
64
65 Args:
66 mr: commonly used info parsed from the request.
67
68 Returns:
69 Dict of values used by EZT for rendering the page.
70 """
71 config, component_def = self._GetComponentDef(mr)
72 users_by_id = framework_views.MakeAllUserViews(
73 mr.cnxn, self.services.user,
74 component_def.admin_ids, component_def.cc_ids)
75 component_def_view = tracker_views.ComponentDefView(
76 mr.cnxn, self.services, component_def, users_by_id)
77 initial_admins = [users_by_id[uid].email for uid in component_def.admin_ids]
78 initial_cc = [users_by_id[uid].email for uid in component_def.cc_ids]
79 initial_labels = [
80 self.services.config.LookupLabel(mr.cnxn, mr.project_id, label_id)
81 for label_id in component_def.label_ids]
82
83 creator, created = self._GetUserViewAndFormattedTime(
84 mr, component_def.creator_id, component_def.created)
85 modifier, modified = self._GetUserViewAndFormattedTime(
86 mr, component_def.modifier_id, component_def.modified)
87
88 allow_edit = permissions.CanEditComponentDef(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010089 mr, self.services, component_def, config)
Copybara854996b2021-09-07 19:36:02 +000090
91 subcomponents = tracker_bizobj.FindDescendantComponents(
92 config, component_def)
93 templates = self.services.template.TemplatesWithComponent(
94 mr.cnxn, component_def.component_id)
95 allow_delete = allow_edit and not subcomponents and not templates
96
97 return {
98 'admin_tab_mode': servlet.Servlet.PROCESS_TAB_COMPONENTS,
99 'component_def': component_def_view,
100 'initial_leaf_name': component_def_view.leaf_name,
101 'initial_docstring': component_def.docstring,
102 'initial_deprecated': ezt.boolean(component_def.deprecated),
103 'initial_admins': initial_admins,
104 'initial_cc': initial_cc,
105 'initial_labels': initial_labels,
106 'allow_edit': ezt.boolean(allow_edit),
107 'allow_delete': ezt.boolean(allow_delete),
108 'subcomponents': subcomponents,
109 'templates': templates,
110 'creator': creator,
111 'created': created,
112 'modifier': modifier,
113 'modified': modified,
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100114 }
Copybara854996b2021-09-07 19:36:02 +0000115
116 def ProcessFormData(self, mr, post_data):
117 """Validate and store the contents of the issues tracker admin page.
118
119 Args:
120 mr: commonly used info parsed from the request.
121 post_data: HTML form data from the request.
122
123 Returns:
124 String URL to redirect the user to, or None if response was already sent.
125 """
126 config, component_def = self._GetComponentDef(mr)
127 allow_edit = permissions.CanEditComponentDef(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100128 mr, self.services, component_def, config)
Copybara854996b2021-09-07 19:36:02 +0000129 if not allow_edit:
130 raise permissions.PermissionException(
131 'User is not allowed to edit or delete this component')
132
133 if 'deletecomponent' in post_data:
134 allow_delete = not tracker_bizobj.FindDescendantComponents(
135 config, component_def)
136 if not allow_delete:
137 raise permissions.PermissionException(
138 'User tried to delete component that had subcomponents')
139 return self._ProcessDeleteComponent(mr, component_def)
140
141 else:
142 return self._ProcessEditComponent(mr, post_data, config, component_def)
143
144
145 def _ProcessDeleteComponent(self, mr, component_def):
146 """The user wants to delete the specified custom field definition."""
147 self.services.issue.DeleteComponentReferences(
148 mr.cnxn, component_def.component_id)
149 self.services.config.DeleteComponentDef(
150 mr.cnxn, mr.project_id, component_def.component_id)
151 return framework_helpers.FormatAbsoluteURL(
152 mr, urls.ADMIN_COMPONENTS, deleted=1, ts=int(time.time()))
153
154 def _GetUserViewAndFormattedTime(self, mr, user_id, timestamp):
155 formatted_time = (timestr.FormatAbsoluteDate(timestamp)
156 if timestamp else None)
157 user = self.services.user.GetUser(mr.cnxn, user_id) if user_id else None
158 user_view = None
159 if user:
160 user_view = framework_views.UserView(user)
161 viewing_self = mr.auth.user_id == user_id
162 # Do not obscure email if current user is a site admin. Do not obscure
163 # email if current user is the same as the creator. For all other
164 # cases do whatever obscure_email setting for the user is.
165 email_obscured = (not(mr.auth.user_pb.is_site_admin or viewing_self)
166 and user_view.obscure_email)
167 if not email_obscured:
168 user_view.RevealEmail()
169
170 return user_view, formatted_time
171
172 def _ProcessEditComponent(self, mr, post_data, config, component_def):
173 """The user wants to edit this component definition."""
174 parsed = component_helpers.ParseComponentRequest(
175 mr, post_data, self.services)
176
177 if not tracker_constants.COMPONENT_NAME_RE.match(parsed.leaf_name):
178 mr.errors.leaf_name = 'Invalid component name'
179
180 original_path = component_def.path
181 if mr.component_path and '>' in mr.component_path:
182 parent_path = mr.component_path[:mr.component_path.rindex('>')]
183 new_path = '%s>%s' % (parent_path, parsed.leaf_name)
184 else:
185 new_path = parsed.leaf_name
186
187 conflict = tracker_bizobj.FindComponentDef(new_path, config)
188 if conflict and conflict.component_id != component_def.component_id:
189 mr.errors.leaf_name = 'That name is already in use.'
190
191 creator, created = self._GetUserViewAndFormattedTime(
192 mr, component_def.creator_id, component_def.created)
193 modifier, modified = self._GetUserViewAndFormattedTime(
194 mr, component_def.modifier_id, component_def.modified)
195
196 if mr.errors.AnyErrors():
197 self.PleaseCorrect(
198 mr, initial_leaf_name=parsed.leaf_name,
199 initial_docstring=parsed.docstring,
200 initial_deprecated=ezt.boolean(parsed.deprecated),
201 initial_admins=parsed.admin_usernames,
202 initial_cc=parsed.cc_usernames,
203 initial_labels=parsed.label_strs,
204 created=created,
205 creator=creator,
206 modified=modified,
207 modifier=modifier,
208 )
209 return None
210
211 new_modified = int(time.time())
212 new_modifier_id = self.services.user.LookupUserID(
213 mr.cnxn, mr.auth.email, autocreate=False)
214 self.services.config.UpdateComponentDef(
215 mr.cnxn, mr.project_id, component_def.component_id,
216 path=new_path, docstring=parsed.docstring, deprecated=parsed.deprecated,
217 admin_ids=parsed.admin_ids, cc_ids=parsed.cc_ids, modified=new_modified,
218 modifier_id=new_modifier_id, label_ids=parsed.label_ids)
219
220 update_rule = False
221 if new_path != original_path:
222 update_rule = True
223 # If the name changed then update all of its subcomponents as well.
224 subcomponent_ids = tracker_bizobj.FindMatchingComponentIDs(
225 original_path, config, exact=False)
226 for subcomponent_id in subcomponent_ids:
227 if subcomponent_id == component_def.component_id:
228 continue
229 subcomponent_def = tracker_bizobj.FindComponentDefByID(
230 subcomponent_id, config)
231 subcomponent_new_path = subcomponent_def.path.replace(
232 original_path, new_path, 1)
233 self.services.config.UpdateComponentDef(
234 mr.cnxn, mr.project_id, subcomponent_def.component_id,
235 path=subcomponent_new_path)
236
237 if (set(parsed.cc_ids) != set(component_def.cc_ids) or
238 set(parsed.label_ids) != set(component_def.label_ids)):
239 update_rule = True
240 if update_rule:
241 filterrules_helpers.RecomputeAllDerivedFields(
242 mr.cnxn, self.services, mr.project, config)
243
244 return framework_helpers.FormatAbsoluteURL(
245 mr, urls.COMPONENT_DETAIL,
246 component=new_path, saved=1, ts=int(time.time()))
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100247
248 def GetComponentDetailPage(self, **kwargs):
249 return self.handler(**kwargs)
250
251 def PostComponentDetailPage(self, **kwargs):
252 return self.handler(**kwargs)