Merge lp:~twom/launchpad/git-permissions-activity-view into lp:launchpad

Proposed by Tom Wardill
Status: Merged
Merged at revision: 18817
Proposed branch: lp:~twom/launchpad/git-permissions-activity-view
Merge into: lp:launchpad
Diff against target: 297 lines (+214/-2)
6 files modified
lib/lp/code/browser/configure.zcml (+6/-0)
lib/lp/code/browser/gitrepository.py (+27/-1)
lib/lp/code/browser/tests/test_gitrepository.py (+74/-0)
lib/lp/code/interfaces/gitrepository.py (+8/-0)
lib/lp/code/model/gitrepository.py (+20/-1)
lib/lp/code/templates/gitrepository-activity.pt (+79/-0)
To merge this branch: bzr merge lp:~twom/launchpad/git-permissions-activity-view
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+358375@code.launchpad.net

Commit message

Add log page for GitActivity

Description of the change

Add a page for viewing the GitActivity records for a repository.
* Add link to sidebar of GitRepository index
* Precache the granter/grantee details on loading the page
* Add tests for rendering and precaching
* Format the raw output to something more human friendly

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Needs Fixing
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/code/browser/configure.zcml'
2--- lib/lp/code/browser/configure.zcml 2018-08-29 15:38:17 +0000
3+++ lib/lp/code/browser/configure.zcml 2018-11-08 16:01:42 +0000
4@@ -877,6 +877,12 @@
5 template="../templates/gitrepository-delete.pt"/>
6 <browser:page
7 for="lp.code.interfaces.gitrepository.IGitRepository"
8+ class="lp.code.browser.gitrepository.GitRepositoryActivityView"
9+ permission="launchpad.Edit"
10+ name="+activity"
11+ template="../templates/gitrepository-activity.pt" />
12+ <browser:page
13+ for="lp.code.interfaces.gitrepository.IGitRepository"
14 class="lp.code.browser.gitsubscription.GitSubscriptionAddView"
15 permission="launchpad.AnyPerson"
16 name="+subscribe"
17
18=== modified file 'lib/lp/code/browser/gitrepository.py'
19--- lib/lp/code/browser/gitrepository.py 2018-08-31 13:36:38 +0000
20+++ lib/lp/code/browser/gitrepository.py 2018-11-08 16:01:42 +0000
21@@ -8,6 +8,7 @@
22 __all__ = [
23 'GitRefBatchNavigator',
24 'GitRepositoriesBreadcrumb',
25+ 'GitRepositoryActivityView',
26 'GitRepositoryBreadcrumb',
27 'GitRepositoryContextMenu',
28 'GitRepositoryDeletionView',
29@@ -210,7 +211,7 @@
30 usedfor = IGitRepository
31 facet = "branches"
32 title = "Edit Git repository"
33- links = ["edit", "reviewer", "webhooks", "delete"]
34+ links = ["edit", "reviewer", "webhooks", "activity", "delete"]
35
36 @enabled_with_permission("launchpad.Edit")
37 def edit(self):
38@@ -230,6 +231,11 @@
39 enabled=bool(getFeatureFlag('webhooks.new.enabled')))
40
41 @enabled_with_permission("launchpad.Edit")
42+ def activity(self):
43+ text = "View activity"
44+ return Link("+activity", text, icon="info")
45+
46+ @enabled_with_permission("launchpad.Edit")
47 def delete(self):
48 text = "Delete repository"
49 return Link("+delete", text, icon="trash-icon")
50@@ -771,3 +777,23 @@
51 @property
52 def cancel_url(self):
53 return canonical_url(self.context)
54+
55+
56+class GitRepositoryActivityView(LaunchpadView):
57+
58+ page_title = "Activity"
59+
60+ @property
61+ def label(self):
62+ return "Activity log for %s" % self.context.display_name
63+
64+ def displayPermissions(self, values):
65+ """Assemble a human readable list from the permissions changes."""
66+ permissions = []
67+ if values.get('can_create'):
68+ permissions.append('create')
69+ if values.get('can_push'):
70+ permissions.append('push')
71+ if values.get('can_force_push'):
72+ permissions.append('force-push')
73+ return ', '.join(permissions)
74
75=== modified file 'lib/lp/code/browser/tests/test_gitrepository.py'
76--- lib/lp/code/browser/tests/test_gitrepository.py 2018-09-11 15:04:43 +0000
77+++ lib/lp/code/browser/tests/test_gitrepository.py 2018-11-08 16:01:42 +0000
78@@ -1154,3 +1154,77 @@
79 self.assertEqual(
80 ["Repository %s deleted." % name],
81 get_feedback_messages(browser.contents))
82+
83+
84+class TestGitRepositoryActivityView(BrowserTestCase):
85+
86+ layer = DatabaseFunctionalLayer
87+
88+ def test_render(self):
89+ requester = self.factory.makePerson()
90+ repository = removeSecurityProxy(
91+ self.factory.makeGitRepository(owner=requester))
92+
93+ rule = self.factory.makeGitRule(repository)
94+ self.factory.makeGitRuleGrant(
95+ rule=rule, grantee=requester, can_push=True, can_create=True)
96+
97+ browser = self.getViewBrowser(
98+ repository, "+activity", rootsite="code",
99+ user=repository.owner)
100+
101+ table = find_tag_by_id(browser.contents, "activity-listing")
102+ date = repository.getActivity()[0].date_changed.strftime(
103+ '%Y-%m-%d %T')
104+ person_name = repository.owner.display_name
105+ self.assertThat(extract_text(table), DocTestMatches(dedent("""
106+ Date
107+ Author
108+ Target
109+ What changed
110+ Old value
111+ New value
112+ {date}
113+ {person_name}
114+ {person_name}
115+ Added access grant
116+ Pattern: refs/heads/*
117+ Permissions:
118+ create, push
119+ {date}
120+ {person_name}
121+ Added access rule
122+ Pattern: refs/heads/*
123+ Rule position: 0
124+ """.format(date=date, person_name=person_name)),
125+ flags=doctest.NORMALIZE_WHITESPACE))
126+
127+ def test_activity_query_count(self):
128+ requester = self.factory.makePerson()
129+ repository = removeSecurityProxy(
130+ self.factory.makeGitRepository(owner=requester))
131+ rule = self.factory.makeGitRule(repository)
132+
133+ grantees = iter([self.factory.makePerson() for _ in range(4)])
134+
135+ def login_and_view():
136+ browser = self.getViewBrowser(
137+ repository,
138+ "+activity",
139+ rootsite="code",
140+ user=repository.owner)
141+ self.assertIsNotNone(
142+ find_tag_by_id(browser.contents, 'activity-listing'))
143+
144+ def create_activity():
145+ self.factory.makeGitRuleGrant(
146+ rule=rule,
147+ grantee=next(grantees),
148+ can_push=True,
149+ can_create=True)
150+
151+ recorder1, recorder2 = record_two_runs(
152+ login_and_view,
153+ create_activity,
154+ 2)
155+ self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
156
157=== modified file 'lib/lp/code/interfaces/gitrepository.py'
158--- lib/lp/code/interfaces/gitrepository.py 2018-10-23 16:15:37 +0000
159+++ lib/lp/code/interfaces/gitrepository.py 2018-11-08 16:01:42 +0000
160@@ -639,6 +639,14 @@
161 :return: A `ResultSet` of `IGitActivity`.
162 """
163
164+ def getPrecachedActivity(changed_after=None):
165+ """Activity log entries are preloaded.
166+
167+ :param changed_after: If supplied, only return entries for changes
168+ made after this date.
169+ :return: A `ResultSet` of `IGitActivity`.
170+ """
171+
172
173 class IGitRepositoryModerateAttributes(Interface):
174 """IGitRepository attributes that can be edited by more than one community.
175
176=== modified file 'lib/lp/code/model/gitrepository.py'
177--- lib/lp/code/model/gitrepository.py 2018-10-22 16:24:46 +0000
178+++ lib/lp/code/model/gitrepository.py 2018-11-08 16:01:42 +0000
179@@ -137,7 +137,10 @@
180 from lp.registry.interfaces.distributionsourcepackage import (
181 IDistributionSourcePackage,
182 )
183-from lp.registry.interfaces.person import IPerson
184+from lp.registry.interfaces.person import (
185+ IPerson,
186+ IPersonSet,
187+ )
188 from lp.registry.interfaces.product import IProduct
189 from lp.registry.interfaces.role import IHasOwner
190 from lp.registry.interfaces.sharingjob import (
191@@ -1287,6 +1290,22 @@
192 return Store.of(self).find(GitActivity, *clauses).order_by(
193 Desc(GitActivity.date_changed), Desc(GitActivity.id))
194
195+ def getPrecachedActivity(self, **kwargs):
196+
197+ def preloadDataForActivities(activities):
198+ # Utility to load related data for a list of GitActivity
199+ person_ids = set()
200+ for activity in activities:
201+ person_ids.add(activity.changer_id)
202+ person_ids.add(activity.changee_id)
203+ list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
204+ person_ids, need_validity=True))
205+ return activities
206+
207+ results = self.getActivity(**kwargs)
208+ return DecoratedResultSet(
209+ results, pre_iter_hook=preloadDataForActivities)
210+
211 def canBeDeleted(self):
212 """See `IGitRepository`."""
213 # Can't delete if the repository is associated with anything.
214
215=== added file 'lib/lp/code/templates/gitrepository-activity.pt'
216--- lib/lp/code/templates/gitrepository-activity.pt 1970-01-01 00:00:00 +0000
217+++ lib/lp/code/templates/gitrepository-activity.pt 2018-11-08 16:01:42 +0000
218@@ -0,0 +1,79 @@
219+<html
220+ xmlns="http://www.w3.org/1999/xhtml"
221+ xmlns:tal="http://xml.zope.org/namespaces/tal"
222+ xmlns:metal="http://xml.zope.org/namespaces/metal"
223+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
224+ metal:use-macro="view/macro:page/main_only" i18n:domain="launchpad">
225+
226+<body>
227+
228+ <metal:macros fill-slot="bogus">
229+ <metal:macro define-macro="activity-value">
230+ <ul tal:condition="value">
231+ <li><strong>Pattern:</strong> <span tal:content="value/ref_pattern" /></li>
232+ <tal:comment condition="nothing">
233+ Rule changes
234+ </tal:comment>
235+ <li tal:condition="python: 'position' in value"><strong>Rule position:</strong> <span tal:content="value/position" /></li>
236+ <tal:comment condition="nothing">
237+ Grant changes
238+ </tal:comment>
239+ <li tal:condition="python: 'position' not in value">
240+ <strong>Permissions:</strong>
241+ <span metal:define-slot="permissions"></span>
242+ </li>
243+ </ul>
244+ </metal:macro>
245+ </metal:macros>
246+
247+ <div metal:fill-slot="main">
248+ <div class="yui-g">
249+ <div class="top-portlet">
250+ <table id="activity-listing" class="listing">
251+ <thead>
252+ <tr>
253+ <th>Date</th>
254+ <th>Author</th>
255+ <th>Target</th>
256+ <th>What changed</th>
257+ <th>Old value</th>
258+ <th>New value</th>
259+ </tr>
260+ </thead>
261+ <tbody>
262+ <tr tal:repeat="log context/getPrecachedActivity">
263+ <tal:comment condition="nothing">
264+ XXX: Gavin Panella 2009-08-12 bug=412963: Using strftime()
265+ here because fmt:datetime changes timezone, even though we
266+ always want to show only UTC.
267+ </tal:comment>
268+ <td tal:content="python:log.date_changed.strftime('%Y-%m-%d %T')">
269+ 2004-09-24 12:04:43
270+ </td>
271+ <td tal:content="structure log/changer/fmt:link">Changer</td>
272+ <td tal:content="structure log/changee/fmt:link">Changee</td>
273+ <td tal:content="log/what_changed/title">description</td>
274+ <td>
275+ <tal:activity define="value log/old_value">
276+ <metal:logs use-macro="template/macros/activity-value">
277+ <span metal:fill-slot="permissions" tal:content="python:view.displayPermissions(log.old_value)"></span>
278+ </metal:logs>
279+ </tal:activity>
280+ </td>
281+ <td>
282+ <tal:activity define="value log/new_value">
283+ <metal:logs use-macro="template/macros/activity-value">
284+ <span metal:fill-slot="permissions" tal:content="python:view.displayPermissions(log.new_value)"></span>
285+ </metal:logs>
286+ </tal:activity>
287+ </td>
288+ </tr>
289+ </tbody>
290+ </table>
291+ </div>
292+ </div>
293+ </div>
294+
295+</body>
296+
297+</html>