blob: 6fe70187a5506604d97354a91465f7e800a677bc [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"""Servlet to export a range of issues in JSON format.
6"""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
Copybara854996b2021-09-07 19:36:02 +000011import time
12
Copybara854996b2021-09-07 19:36:02 +000013from businesslogic import work_env
14from features import savedqueries_helpers
Copybara854996b2021-09-07 19:36:02 +000015from framework import jsonfeed
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010016from framework import permissions
Copybara854996b2021-09-07 19:36:02 +000017from framework import servlet
18from tracker import tracker_bizobj
19
20
21class IssueExport(servlet.Servlet):
22 """IssueExportControls let's an admin choose how to export issues."""
23
24 _PAGE_TEMPLATE = 'tracker/issue-export-page.ezt'
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010025 _MAIN_TAB_MODE = servlet.Servlet.MAIN_TAB_ISSUES
Copybara854996b2021-09-07 19:36:02 +000026
27 def AssertBasePermission(self, mr):
28 """Make sure that the logged in user has permission to view this page."""
29 super(IssueExport, self).AssertBasePermission(mr)
30 if not mr.auth.user_pb.is_site_admin:
31 raise permissions.PermissionException(
32 'Only site admins may export issues')
33
34 def GatherPageData(self, mr):
35 """Build up a dictionary of data values to use when rendering the page."""
36
37 canned_query_views = []
38 if mr.project_id:
39 with mr.profiler.Phase('getting canned queries'):
40 canned_queries = self.services.features.GetCannedQueriesByProjectID(
41 mr.cnxn, mr.project_id)
42 canned_query_views = [
43 savedqueries_helpers.SavedQueryView(sq, idx + 1, None, None)
44 for idx, sq in enumerate(canned_queries)
45 ]
46
47 saved_query_views = []
48 if mr.auth.user_id and self.services.features:
49 with mr.profiler.Phase('getting saved queries'):
50 saved_queries = self.services.features.GetSavedQueriesByUserID(
51 mr.cnxn, mr.me_user_id)
52 saved_query_views = [
53 savedqueries_helpers.SavedQueryView(sq, idx + 1, None, None)
54 for idx, sq in enumerate(saved_queries)
55 if
56 (mr.project_id in sq.executes_in_project_ids or not mr.project_id)
57 ]
58
59 return {
60 'issue_tab_mode': None,
61 'initial_start': mr.start,
62 'initial_num': mr.num,
63 'page_perms': self.MakePagePerms(mr, None, permissions.CREATE_ISSUE),
64 'canned_queries': canned_query_views,
65 'saved_queries': saved_query_views,
66 }
67
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010068 def GetIssueExport(self, **kwargs):
69 return self.handler(**kwargs)
Copybara854996b2021-09-07 19:36:02 +000070
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020071
Copybara854996b2021-09-07 19:36:02 +000072class IssueExportJSON(jsonfeed.JsonFeed):
73 """IssueExport shows a range of issues in JSON format."""
74
75 # Pretty-print the JSON output.
76 JSON_INDENT = 4
77
78 def AssertBasePermission(self, mr):
79 """Make sure that the logged in user has permission to view this page."""
80 super(IssueExportJSON, self).AssertBasePermission(mr)
81 if not mr.auth.user_pb.is_site_admin:
82 raise permissions.PermissionException(
83 'Only site admins may export issues')
84
85 def HandleRequest(self, mr):
86 """Build up a dictionary of data values to use when rendering the page.
87
88 Args:
89 mr: commonly used info parsed from the request.
90
91 Returns:
92 Dict of values used by EZT for rendering the page.
93 """
94 if mr.query or mr.can != 1:
95 with work_env.WorkEnv(mr, self.services) as we:
96 pipeline = we.ListIssues(
97 mr.query, [mr.project.project_name], mr.auth.user_id, mr.num,
98 mr.start, mr.can, mr.group_by_spec, mr.sort_spec, False)
99 issues = pipeline.allowed_results
100 # no user query and mr.can == 1 (we want all issues)
101 elif not mr.start and not mr.num:
102 issues = self.services.issue.GetAllIssuesInProject(
103 mr.cnxn, mr.project.project_id)
104 else:
105 local_id_range = list(range(mr.start, mr.start + mr.num))
106 issues = self.services.issue.GetIssuesByLocalIDs(
107 mr.cnxn, mr.project.project_id, local_id_range)
108
109 user_id_set = tracker_bizobj.UsersInvolvedInIssues(issues)
110
111 comments_dict = self.services.issue.GetCommentsForIssues(
112 mr.cnxn, [issue.issue_id for issue in issues])
113 for comment_list in comments_dict.values():
114 user_id_set.update(
115 tracker_bizobj.UsersInvolvedInCommentList(comment_list))
116
117 starrers_dict = self.services.issue_star.LookupItemsStarrers(
118 mr.cnxn, [issue.issue_id for issue in issues])
119 for starrer_id_list in starrers_dict.values():
120 user_id_set.update(starrer_id_list)
121
122 # The value 0 indicates "no user", e.g., that an issue has no owner.
123 # We don't need to create a User row to represent that.
124 user_id_set.discard(0)
125 email_dict = self.services.user.LookupUserEmails(
126 mr.cnxn, user_id_set, ignore_missed=True)
127
128 issues_json = [
129 self._MakeIssueJSON(
130 mr, issue, email_dict,
131 comments_dict.get(issue.issue_id, []),
132 starrers_dict.get(issue.issue_id, []))
133 for issue in issues if not issue.deleted]
134
135 json_data = {
136 'metadata': {
137 'version': 1,
138 'when': int(time.time()),
139 'who': mr.auth.email,
140 'project': mr.project_name,
141 'start': mr.start,
142 'num': mr.num,
143 },
144 'issues': issues_json,
145 # This list could be derived from the 'issues', but we provide it for
146 # ease of processing.
147 'emails': list(email_dict.values()),
148 }
149 return json_data
150
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100151 def _MakeAmendmentJSON(self, amendment, email_dict, comp_dict):
Copybara854996b2021-09-07 19:36:02 +0000152 amendment_json = {
153 'field': amendment.field.name,
154 }
155 if amendment.custom_field_name:
156 amendment_json.update({'custom_field_name': amendment.custom_field_name})
157 if amendment.newvalue:
158 amendment_json.update({'new_value': amendment.newvalue})
159 if amendment.added_user_ids:
160 amendment_json.update(
161 {'added_emails': [email_dict.get(user_id)
162 for user_id in amendment.added_user_ids]})
163 if amendment.removed_user_ids:
164 amendment_json.update(
165 {'removed_emails': [email_dict.get(user_id)
166 for user_id in amendment.removed_user_ids]})
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100167 if amendment.added_component_ids:
168 amendment_json.update(
169 {
170 'added_components':
171 [
172 comp_dict.get(component_id).path.lower()
173 for component_id in amendment.added_component_ids
174 ]
175 })
176 if amendment.removed_component_ids:
177 amendment_json.update(
178 {
179 'removed_components':
180 [
181 comp_dict.get(component_id).path.lower()
182 for component_id in amendment.removed_component_ids
183 ]
184 })
Copybara854996b2021-09-07 19:36:02 +0000185 return amendment_json
186
187 def _MakeAttachmentJSON(self, attachment):
188 if attachment.deleted:
189 return None
190 attachment_json = {
191 'name': attachment.filename,
192 'size': attachment.filesize,
193 'mimetype': attachment.mimetype,
194 'gcs_object_id': attachment.gcs_object_id,
195 }
196 return attachment_json
197
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100198 def _MakeCommentJSON(self, comment, email_dict, comp_dict):
Copybara854996b2021-09-07 19:36:02 +0000199 if comment.deleted_by:
200 return None
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100201 amendments = [
202 self._MakeAmendmentJSON(a, email_dict, comp_dict)
203 for a in comment.amendments
204 ]
Copybara854996b2021-09-07 19:36:02 +0000205 attachments = [self._MakeAttachmentJSON(a)
206 for a in comment.attachments]
207 comment_json = {
208 'timestamp': comment.timestamp,
209 'commenter': email_dict.get(comment.user_id),
210 'content': comment.content,
211 'amendments': [a for a in amendments if a],
212 'attachments': [a for a in attachments if a],
213 'description_num': comment.description_num
214 }
215 return comment_json
216
217 def _MakePhaseJSON(self, phase):
218 return {'id': phase.phase_id, 'name': phase.name, 'rank': phase.rank}
219
220 def _MakeFieldValueJSON(self, field, fd_dict, email_dict, phase_dict):
221 fd = fd_dict.get(field.field_id)
222 field_value_json = {
223 'field': fd.field_name,
224 'phase': phase_dict.get(field.phase_id),
225 }
226 approval_fd = fd_dict.get(fd.approval_id)
227 if approval_fd:
228 field_value_json['approval'] = approval_fd.field_name
229
230 if field.int_value:
231 field_value_json['int_value'] = field.int_value
232 if field.str_value:
233 field_value_json['str_value'] = field.str_value
234 if field.user_id:
235 field_value_json['user_value'] = email_dict.get(field.user_id)
236 if field.date_value:
237 field_value_json['date_value'] = field.date_value
238 return field_value_json
239
240 def _MakeApprovalValueJSON(
241 self, approval_value, fd_dict, email_dict, phase_dict):
242 av_json = {
243 'approval': fd_dict.get(approval_value.approval_id).field_name,
244 'status': approval_value.status.name,
245 'setter': email_dict.get(approval_value.setter_id),
246 'set_on': approval_value.set_on,
247 'approvers': [email_dict.get(approver_id) for
248 approver_id in approval_value.approver_ids],
249 'phase': phase_dict.get(approval_value.phase_id),
250 }
251 return av_json
252
253 def _MakeIssueJSON(
254 self, mr, issue, email_dict, comment_list, starrer_id_list):
255 """Return a dict of info about the issue and its comments."""
256 descriptions = [c for c in comment_list if c.is_description]
257 for i, d in enumerate(descriptions):
258 d.description_num = str(i+1)
Copybara854996b2021-09-07 19:36:02 +0000259 phase_dict = {phase.phase_id: phase.name for phase in issue.phases}
260 config = self.services.config.GetProjectConfig(
261 mr.cnxn, mr.project.project_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100262 comp_dict = {comp.component_id: comp for comp in config.component_defs}
263 comments = [
264 self._MakeCommentJSON(c, email_dict, comp_dict) for c in comment_list
265 ]
Copybara854996b2021-09-07 19:36:02 +0000266 fd_dict = {fd.field_id: fd for fd in config.field_defs}
267 issue_json = {
268 'local_id': issue.local_id,
269 'reporter': email_dict.get(issue.reporter_id),
270 'summary': issue.summary,
271 'owner': email_dict.get(issue.owner_id),
272 'status': issue.status,
273 'cc': [email_dict[cc_id] for cc_id in issue.cc_ids],
274 'labels': issue.labels,
275 'phases': [self._MakePhaseJSON(phase) for phase in issue.phases],
276 'fields': [
277 self._MakeFieldValueJSON(field, fd_dict, email_dict, phase_dict)
278 for field in issue.field_values],
279 'approvals': [self._MakeApprovalValueJSON(
280 approval, fd_dict, email_dict, phase_dict)
281 for approval in issue.approval_values],
282 'starrers': [email_dict[starrer] for starrer in starrer_id_list],
283 'comments': [c for c in comments if c],
284 'opened': issue.opened_timestamp,
285 'modified': issue.modified_timestamp,
286 'closed': issue.closed_timestamp,
287 }
288 # TODO(http://crbug.com/monorail/7217): Export cross-project references.
289 if issue.blocked_on_iids:
290 issue_json['blocked_on'] = [i.local_id for i in
291 self.services.issue.GetIssues(mr.cnxn, issue.blocked_on_iids)
292 if i.project_id == mr.project.project_id]
293 if issue.blocking_iids:
294 issue_json['blocking'] = [i.local_id for i in
295 self.services.issue.GetIssues(mr.cnxn, issue.blocking_iids)
296 if i.project_id == mr.project.project_id]
297 if issue.merged_into:
298 merge = self.services.issue.GetIssue(mr.cnxn, issue.merged_into)
299 if merge.project_id == mr.project.project_id:
300 issue_json['merged_into'] = merge.local_id
301 return issue_json
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200302
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100303 def GetIssueExportJSON(self, **kwargs):
304 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200305
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100306 def PostIssueExportJSON(self, **kwargs):
307 return self.handler(**kwargs)