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