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