blob: 35c6a641964315eb80b068e70bb65dde50a0624f [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"""Code to support project and user activies pages."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import logging
12import time
13
14import ezt
15
16from framework import framework_constants
17from framework import framework_helpers
18from framework import framework_views
19from framework import sql
20from framework import template_helpers
21from framework import timestr
22from project import project_views
23from proto import tracker_pb2
24from tracker import tracker_helpers
25from tracker import tracker_views
26
27UPDATES_PER_PAGE = 50
28MAX_UPDATES_PER_PAGE = 200
29
30
31class ActivityView(template_helpers.PBProxy):
32 """EZT-friendly wrapper for Activities."""
33
34 _TITLE_TEMPLATE = template_helpers.MonorailTemplate(
35 framework_constants.TEMPLATE_PATH + 'features/activity-title.ezt',
36 compress_whitespace=True, base_format=ezt.FORMAT_HTML)
37
38 _BODY_TEMPLATE = template_helpers.MonorailTemplate(
39 framework_constants.TEMPLATE_PATH + 'features/activity-body.ezt',
40 compress_whitespace=True, base_format=ezt.FORMAT_HTML)
41
42 def __init__(
43 self, pb, mr, prefetched_issues, users_by_id,
44 prefetched_projects, prefetched_configs,
45 autolink=None, all_ref_artifacts=None, ending=None, highlight=None):
46 """Constructs an ActivityView out of an Activity protocol buffer.
47
48 Args:
49 pb: an IssueComment or Activity protocol buffer.
50 mr: HTTP request info, used by the artifact autolink.
51 prefetched_issues: dictionary of the issues for the comments being shown.
52 users_by_id: dict {user_id: UserView} for all relevant users.
53 prefetched_projects: dict {project_id: project} including all the projects
54 that we might need.
55 prefetched_configs: dict {project_id: config} for those projects.
56 autolink: Autolink instance.
57 all_ref_artifacts: list of all artifacts in the activity stream.
58 ending: ending type for activity titles, 'in_project' or 'by_user'
59 highlight: what to highlight in the middle column on user updates pages
60 i.e. 'project', 'user', or None
61 """
62 template_helpers.PBProxy.__init__(self, pb)
63
64 activity_type = 'ProjectIssueUpdate' # TODO(jrobbins): more types
65
66 self.comment = None
67 self.issue = None
68 self.field_changed = None
69 self.multiple_fields_changed = ezt.boolean(False)
70 self.project = None
71 self.user = None
72 self.timestamp = time.time() # Bogus value makes bad ones highly visible.
73
74 if isinstance(pb, tracker_pb2.IssueComment):
75 self.timestamp = pb.timestamp
76 issue = prefetched_issues[pb.issue_id]
77 if self.timestamp == issue.opened_timestamp:
78 issue_change_id = None # This comment is the description.
79 else:
80 issue_change_id = pb.timestamp # instead of seq num.
81
82 self.comment = tracker_views.IssueCommentView(
83 mr.project_name, pb, users_by_id, autolink,
84 all_ref_artifacts, mr, issue)
85
86 # TODO(jrobbins): pass effective_ids of the commenter so that they
87 # can be identified as a project member or not.
88 config = prefetched_configs[issue.project_id]
89 self.issue = tracker_views.IssueView(issue, users_by_id, config)
90 self.user = self.comment.creator
91 project = prefetched_projects[issue.project_id]
92 self.project_name = project.project_name
93 self.project = project_views.ProjectView(project)
94
95 else:
96 logging.warn('unknown activity object %r', pb)
97
98 nested_page_data = {
99 'activity_type': activity_type,
100 'issue_change_id': issue_change_id,
101 'comment': self.comment,
102 'issue': self.issue,
103 'project': self.project,
104 'user': self.user,
105 'timestamp': self.timestamp,
106 'ending_type': ending,
107 }
108
109 self.escaped_title = self._TITLE_TEMPLATE.GetResponse(
110 nested_page_data).strip()
111 self.escaped_body = self._BODY_TEMPLATE.GetResponse(
112 nested_page_data).strip()
113
114 if autolink is not None and all_ref_artifacts is not None:
115 # TODO(jrobbins): actually parse the comment text. Actually render runs.
116 runs = autolink.MarkupAutolinks(
117 mr, [template_helpers.TextRun(self.escaped_body)], all_ref_artifacts)
118 self.escaped_body = ''.join(run.content for run in runs)
119
120 self.date_bucket, self.date_relative = timestr.GetHumanScaleDate(
121 self.timestamp)
122 time_tuple = time.localtime(self.timestamp)
123 self.date_tooltip = time.asctime(time_tuple)
124
125 # We always highlight the user for starring activities
126 if activity_type.startswith('UserStar'):
127 self.highlight = 'user'
128 else:
129 self.highlight = highlight
130
131
132def GatherUpdatesData(
133 services, mr, project_ids=None, user_ids=None, ending=None,
134 updates_page_url=None, autolink=None, highlight=None):
135 """Gathers and returns updates data.
136
137 Args:
138 services: Connections to backend services.
139 mr: HTTP request info, used by the artifact autolink.
140 project_ids: List of project IDs we want updates for.
141 user_ids: List of user IDs we want updates for.
142 ending: Ending type for activity titles, 'in_project' or 'by_user'.
143 updates_page_url: The URL that will be used to create pagination links from.
144 autolink: Autolink instance.
145 highlight: What to highlight in the middle column on user updates pages
146 i.e. 'project', 'user', or None.
147 """
148 # num should be non-negative number
149 num = mr.GetPositiveIntParam('num', UPDATES_PER_PAGE)
150 num = min(num, MAX_UPDATES_PER_PAGE)
151
152 updates_data = {
153 'no_stars': None,
154 'no_activities': None,
155 'pagination': None,
156 'updates_data': None,
157 'ending_type': ending,
158 }
159
160 if not user_ids and not project_ids:
161 updates_data['no_stars'] = ezt.boolean(True)
162 return updates_data
163
164 ascending = bool(mr.after)
165 with mr.profiler.Phase('get activities'):
166 comments = services.issue.GetIssueActivity(mr.cnxn, num=num,
167 before=mr.before, after=mr.after, project_ids=project_ids,
168 user_ids=user_ids, ascending=ascending)
169 # Filter the comments based on permission to view the issue.
170 # TODO(jrobbins): push permission checking in the query so that
171 # pagination pages never become underfilled, or use backends to shard.
172 # TODO(jrobbins): come back to this when I implement private comments.
173 # TODO(jrobbins): it would be better if we could just get the dict directly.
174 prefetched_issues_list = services.issue.GetIssues(
175 mr.cnxn, {c.issue_id for c in comments})
176 prefetched_issues = {
177 issue.issue_id: issue for issue in prefetched_issues_list}
178 needed_project_ids = {issue.project_id for issue
179 in prefetched_issues_list}
180 prefetched_projects = services.project.GetProjects(
181 mr.cnxn, needed_project_ids)
182 prefetched_configs = services.config.GetProjectConfigs(
183 mr.cnxn, needed_project_ids)
184 viewable_issues_list = tracker_helpers.FilterOutNonViewableIssues(
185 mr.auth.effective_ids, mr.auth.user_pb, prefetched_projects,
186 prefetched_configs, prefetched_issues_list)
187 viewable_iids = {issue.issue_id for issue in viewable_issues_list}
188 comments = [
189 c for c in comments if c.issue_id in viewable_iids]
190 if ascending:
191 comments.reverse()
192
193 amendment_user_ids = []
194 for comment in comments:
195 for amendment in comment.amendments:
196 amendment_user_ids.extend(amendment.added_user_ids)
197 amendment_user_ids.extend(amendment.removed_user_ids)
198
199 users_by_id = framework_views.MakeAllUserViews(
200 mr.cnxn, services.user, [c.user_id for c in comments],
201 amendment_user_ids)
202 framework_views.RevealAllEmailsToMembers(
203 mr.cnxn, services, mr.auth, users_by_id, mr.project)
204
205 num_results_returned = len(comments)
206 displayed_activities = comments[:UPDATES_PER_PAGE]
207
208 if not num_results_returned:
209 updates_data['no_activities'] = ezt.boolean(True)
210 return updates_data
211
212 # Get all referenced artifacts first
213 all_ref_artifacts = None
214 if autolink is not None:
215 content_list = []
216 for activity in comments:
217 content_list.append(activity.content)
218
219 all_ref_artifacts = autolink.GetAllReferencedArtifacts(
220 mr, content_list)
221
222 # Now process content and gather activities
223 today = []
224 yesterday = []
225 pastweek = []
226 pastmonth = []
227 thisyear = []
228 older = []
229
230 with mr.profiler.Phase('rendering activities'):
231 for activity in displayed_activities:
232 entry = ActivityView(
233 activity, mr, prefetched_issues, users_by_id,
234 prefetched_projects, prefetched_configs,
235 autolink=autolink, all_ref_artifacts=all_ref_artifacts, ending=ending,
236 highlight=highlight)
237
238 if entry.date_bucket == 'Today':
239 today.append(entry)
240 elif entry.date_bucket == 'Yesterday':
241 yesterday.append(entry)
242 elif entry.date_bucket == 'Last 7 days':
243 pastweek.append(entry)
244 elif entry.date_bucket == 'Last 30 days':
245 pastmonth.append(entry)
246 elif entry.date_bucket == 'Earlier this year':
247 thisyear.append(entry)
248 elif entry.date_bucket == 'Before this year':
249 older.append(entry)
250
251 new_after = None
252 new_before = None
253 if displayed_activities:
254 new_after = displayed_activities[0].timestamp
255 new_before = displayed_activities[-1].timestamp
256
257 prev_url = None
258 next_url = None
259 if updates_page_url:
260 list_servlet_rel_url = updates_page_url.split('/')[-1]
261 recognized_params = [(name, mr.GetParam(name))
262 for name in framework_helpers.RECOGNIZED_PARAMS]
263 if displayed_activities and (mr.before or mr.after):
264 prev_url = framework_helpers.FormatURL(
265 recognized_params, list_servlet_rel_url, after=new_after)
266 if mr.after or len(comments) > UPDATES_PER_PAGE:
267 next_url = framework_helpers.FormatURL(
268 recognized_params, list_servlet_rel_url, before=new_before)
269
270 if prev_url or next_url:
271 pagination = template_helpers.EZTItem(
272 start=None, last=None, prev_url=prev_url, next_url=next_url,
273 reload_url=None, visible=ezt.boolean(True), total_count=None)
274 else:
275 pagination = None
276
277 updates_data.update({
278 'no_activities': ezt.boolean(False),
279 'pagination': pagination,
280 'updates_data': template_helpers.EZTItem(
281 today=today, yesterday=yesterday, pastweek=pastweek,
282 pastmonth=pastmonth, thisyear=thisyear, older=older),
283 })
284
285 return updates_data