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