Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/features/activities.py b/features/activities.py
new file mode 100644
index 0000000..35c6a64
--- /dev/null
+++ b/features/activities.py
@@ -0,0 +1,285 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file or at
+# https://developers.google.com/open-source/licenses/bsd
+
+"""Code to support project and user activies pages."""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import time
+
+import ezt
+
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import sql
+from framework import template_helpers
+from framework import timestr
+from project import project_views
+from proto import tracker_pb2
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+UPDATES_PER_PAGE = 50
+MAX_UPDATES_PER_PAGE = 200
+
+
+class ActivityView(template_helpers.PBProxy):
+ """EZT-friendly wrapper for Activities."""
+
+ _TITLE_TEMPLATE = template_helpers.MonorailTemplate(
+ framework_constants.TEMPLATE_PATH + 'features/activity-title.ezt',
+ compress_whitespace=True, base_format=ezt.FORMAT_HTML)
+
+ _BODY_TEMPLATE = template_helpers.MonorailTemplate(
+ framework_constants.TEMPLATE_PATH + 'features/activity-body.ezt',
+ compress_whitespace=True, base_format=ezt.FORMAT_HTML)
+
+ def __init__(
+ self, pb, mr, prefetched_issues, users_by_id,
+ prefetched_projects, prefetched_configs,
+ autolink=None, all_ref_artifacts=None, ending=None, highlight=None):
+ """Constructs an ActivityView out of an Activity protocol buffer.
+
+ Args:
+ pb: an IssueComment or Activity protocol buffer.
+ mr: HTTP request info, used by the artifact autolink.
+ prefetched_issues: dictionary of the issues for the comments being shown.
+ users_by_id: dict {user_id: UserView} for all relevant users.
+ prefetched_projects: dict {project_id: project} including all the projects
+ that we might need.
+ prefetched_configs: dict {project_id: config} for those projects.
+ autolink: Autolink instance.
+ all_ref_artifacts: list of all artifacts in the activity stream.
+ ending: ending type for activity titles, 'in_project' or 'by_user'
+ highlight: what to highlight in the middle column on user updates pages
+ i.e. 'project', 'user', or None
+ """
+ template_helpers.PBProxy.__init__(self, pb)
+
+ activity_type = 'ProjectIssueUpdate' # TODO(jrobbins): more types
+
+ self.comment = None
+ self.issue = None
+ self.field_changed = None
+ self.multiple_fields_changed = ezt.boolean(False)
+ self.project = None
+ self.user = None
+ self.timestamp = time.time() # Bogus value makes bad ones highly visible.
+
+ if isinstance(pb, tracker_pb2.IssueComment):
+ self.timestamp = pb.timestamp
+ issue = prefetched_issues[pb.issue_id]
+ if self.timestamp == issue.opened_timestamp:
+ issue_change_id = None # This comment is the description.
+ else:
+ issue_change_id = pb.timestamp # instead of seq num.
+
+ self.comment = tracker_views.IssueCommentView(
+ mr.project_name, pb, users_by_id, autolink,
+ all_ref_artifacts, mr, issue)
+
+ # TODO(jrobbins): pass effective_ids of the commenter so that they
+ # can be identified as a project member or not.
+ config = prefetched_configs[issue.project_id]
+ self.issue = tracker_views.IssueView(issue, users_by_id, config)
+ self.user = self.comment.creator
+ project = prefetched_projects[issue.project_id]
+ self.project_name = project.project_name
+ self.project = project_views.ProjectView(project)
+
+ else:
+ logging.warn('unknown activity object %r', pb)
+
+ nested_page_data = {
+ 'activity_type': activity_type,
+ 'issue_change_id': issue_change_id,
+ 'comment': self.comment,
+ 'issue': self.issue,
+ 'project': self.project,
+ 'user': self.user,
+ 'timestamp': self.timestamp,
+ 'ending_type': ending,
+ }
+
+ self.escaped_title = self._TITLE_TEMPLATE.GetResponse(
+ nested_page_data).strip()
+ self.escaped_body = self._BODY_TEMPLATE.GetResponse(
+ nested_page_data).strip()
+
+ if autolink is not None and all_ref_artifacts is not None:
+ # TODO(jrobbins): actually parse the comment text. Actually render runs.
+ runs = autolink.MarkupAutolinks(
+ mr, [template_helpers.TextRun(self.escaped_body)], all_ref_artifacts)
+ self.escaped_body = ''.join(run.content for run in runs)
+
+ self.date_bucket, self.date_relative = timestr.GetHumanScaleDate(
+ self.timestamp)
+ time_tuple = time.localtime(self.timestamp)
+ self.date_tooltip = time.asctime(time_tuple)
+
+ # We always highlight the user for starring activities
+ if activity_type.startswith('UserStar'):
+ self.highlight = 'user'
+ else:
+ self.highlight = highlight
+
+
+def GatherUpdatesData(
+ services, mr, project_ids=None, user_ids=None, ending=None,
+ updates_page_url=None, autolink=None, highlight=None):
+ """Gathers and returns updates data.
+
+ Args:
+ services: Connections to backend services.
+ mr: HTTP request info, used by the artifact autolink.
+ project_ids: List of project IDs we want updates for.
+ user_ids: List of user IDs we want updates for.
+ ending: Ending type for activity titles, 'in_project' or 'by_user'.
+ updates_page_url: The URL that will be used to create pagination links from.
+ autolink: Autolink instance.
+ highlight: What to highlight in the middle column on user updates pages
+ i.e. 'project', 'user', or None.
+ """
+ # num should be non-negative number
+ num = mr.GetPositiveIntParam('num', UPDATES_PER_PAGE)
+ num = min(num, MAX_UPDATES_PER_PAGE)
+
+ updates_data = {
+ 'no_stars': None,
+ 'no_activities': None,
+ 'pagination': None,
+ 'updates_data': None,
+ 'ending_type': ending,
+ }
+
+ if not user_ids and not project_ids:
+ updates_data['no_stars'] = ezt.boolean(True)
+ return updates_data
+
+ ascending = bool(mr.after)
+ with mr.profiler.Phase('get activities'):
+ comments = services.issue.GetIssueActivity(mr.cnxn, num=num,
+ before=mr.before, after=mr.after, project_ids=project_ids,
+ user_ids=user_ids, ascending=ascending)
+ # Filter the comments based on permission to view the issue.
+ # TODO(jrobbins): push permission checking in the query so that
+ # pagination pages never become underfilled, or use backends to shard.
+ # TODO(jrobbins): come back to this when I implement private comments.
+ # TODO(jrobbins): it would be better if we could just get the dict directly.
+ prefetched_issues_list = services.issue.GetIssues(
+ mr.cnxn, {c.issue_id for c in comments})
+ prefetched_issues = {
+ issue.issue_id: issue for issue in prefetched_issues_list}
+ needed_project_ids = {issue.project_id for issue
+ in prefetched_issues_list}
+ prefetched_projects = services.project.GetProjects(
+ mr.cnxn, needed_project_ids)
+ prefetched_configs = services.config.GetProjectConfigs(
+ mr.cnxn, needed_project_ids)
+ viewable_issues_list = tracker_helpers.FilterOutNonViewableIssues(
+ mr.auth.effective_ids, mr.auth.user_pb, prefetched_projects,
+ prefetched_configs, prefetched_issues_list)
+ viewable_iids = {issue.issue_id for issue in viewable_issues_list}
+ comments = [
+ c for c in comments if c.issue_id in viewable_iids]
+ if ascending:
+ comments.reverse()
+
+ amendment_user_ids = []
+ for comment in comments:
+ for amendment in comment.amendments:
+ amendment_user_ids.extend(amendment.added_user_ids)
+ amendment_user_ids.extend(amendment.removed_user_ids)
+
+ users_by_id = framework_views.MakeAllUserViews(
+ mr.cnxn, services.user, [c.user_id for c in comments],
+ amendment_user_ids)
+ framework_views.RevealAllEmailsToMembers(
+ mr.cnxn, services, mr.auth, users_by_id, mr.project)
+
+ num_results_returned = len(comments)
+ displayed_activities = comments[:UPDATES_PER_PAGE]
+
+ if not num_results_returned:
+ updates_data['no_activities'] = ezt.boolean(True)
+ return updates_data
+
+ # Get all referenced artifacts first
+ all_ref_artifacts = None
+ if autolink is not None:
+ content_list = []
+ for activity in comments:
+ content_list.append(activity.content)
+
+ all_ref_artifacts = autolink.GetAllReferencedArtifacts(
+ mr, content_list)
+
+ # Now process content and gather activities
+ today = []
+ yesterday = []
+ pastweek = []
+ pastmonth = []
+ thisyear = []
+ older = []
+
+ with mr.profiler.Phase('rendering activities'):
+ for activity in displayed_activities:
+ entry = ActivityView(
+ activity, mr, prefetched_issues, users_by_id,
+ prefetched_projects, prefetched_configs,
+ autolink=autolink, all_ref_artifacts=all_ref_artifacts, ending=ending,
+ highlight=highlight)
+
+ if entry.date_bucket == 'Today':
+ today.append(entry)
+ elif entry.date_bucket == 'Yesterday':
+ yesterday.append(entry)
+ elif entry.date_bucket == 'Last 7 days':
+ pastweek.append(entry)
+ elif entry.date_bucket == 'Last 30 days':
+ pastmonth.append(entry)
+ elif entry.date_bucket == 'Earlier this year':
+ thisyear.append(entry)
+ elif entry.date_bucket == 'Before this year':
+ older.append(entry)
+
+ new_after = None
+ new_before = None
+ if displayed_activities:
+ new_after = displayed_activities[0].timestamp
+ new_before = displayed_activities[-1].timestamp
+
+ prev_url = None
+ next_url = None
+ if updates_page_url:
+ list_servlet_rel_url = updates_page_url.split('/')[-1]
+ recognized_params = [(name, mr.GetParam(name))
+ for name in framework_helpers.RECOGNIZED_PARAMS]
+ if displayed_activities and (mr.before or mr.after):
+ prev_url = framework_helpers.FormatURL(
+ recognized_params, list_servlet_rel_url, after=new_after)
+ if mr.after or len(comments) > UPDATES_PER_PAGE:
+ next_url = framework_helpers.FormatURL(
+ recognized_params, list_servlet_rel_url, before=new_before)
+
+ if prev_url or next_url:
+ pagination = template_helpers.EZTItem(
+ start=None, last=None, prev_url=prev_url, next_url=next_url,
+ reload_url=None, visible=ezt.boolean(True), total_count=None)
+ else:
+ pagination = None
+
+ updates_data.update({
+ 'no_activities': ezt.boolean(False),
+ 'pagination': pagination,
+ 'updates_data': template_helpers.EZTItem(
+ today=today, yesterday=yesterday, pastweek=pastweek,
+ pastmonth=pastmonth, thisyear=thisyear, older=older),
+ })
+
+ return updates_data