Merge lp:~sinzui/launchpad/person-owned-teams into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 16363
Proposed branch: lp:~sinzui/launchpad/person-owned-teams
Merge into: lp:launchpad
Diff against target: 394 lines (+216/-5)
10 files modified
lib/lp/registry/browser/configure.zcml (+6/-0)
lib/lp/registry/browser/person.py (+20/-1)
lib/lp/registry/browser/tests/test_person.py (+43/-0)
lib/lp/registry/interfaces/person.py (+13/-1)
lib/lp/registry/model/person.py (+12/-0)
lib/lp/registry/templates/person-owned-teams.pt (+70/-0)
lib/lp/registry/templates/person-related-software-navlinks.pt (+4/-0)
lib/lp/registry/templates/product-portlet-requires-subscription.pt (+3/-3)
lib/lp/registry/tests/test_person.py (+44/-0)
lib/lp/soyuz/stories/soyuz/xx-person-packages.txt (+1/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/person-owned-teams
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+139091@code.launchpad.net

Commit message

List teams a person owns.

Description of the change

Team owners are not necessarily team members. All code that lists teams
for people are based on membership. There is no way to discover which
teams a person owns. This is a problem for cases where a person leaves
an organisation -- the team owner does not show up when auditing,
and owners can add themselves back to the team to gain team privileges.

RULES

    Pre-implementation: no one
    * Add a method to IPerson to get the teams a person owns. The
      method must take a user argument to filter out private teams the
      observing user cannot see.
    * Add a related teams page to the related packages/projects group
      of pages. The page must batch the teams.

QA

    * Visit https://qastaging.launchpad.net/~
    * Verify there is a link to owned teams.
    * View the link's page.
    * Verify the page lists the teams.

    * Run this script:
    {{{
    from launchpadlib.launchpad import Launchpad
    lp = Launchpad.login_with(
        'testing', service_root='https://api.qastaging.launchpad.net',
        version='devel')
    person = lp.people['sinzui']
    teams = person.getOwnedTeams()
    for team in teams:
        print team.name

    lp = Launchpad.login_anonymously(
        'testing', service_root='https://api.qastaging.launchpad.net',
        version='devel')
    person = lp.people['sinzui']
    teams = person.getOwnedTeams()
    for team in teams:
        print team.name
    }}}

LINT

    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_person.py
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py
    lib/lp/registry/templates/person-owned-teams.pt
    lib/lp/registry/templates/person-related-software-navlinks.pt
    lib/lp/registry/tests/test_person.py

LoC

    I have more than 10,000 lines of credit this week.

TEST

    ./bin/test -vc -t OwnedTeams lp.registry

IMPLEMENTATION

I added getOwnedTeams to IPerson that return the teams for a Person that
a user is permitted to see. I exported it to use the requesting user as
the user passed to the method.
    lib/lp/registry/interfaces/person.py
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_person.py

I added PersonOwnedTeamsView to the related-software group of pages. I
then added a link to the IPersonRelatedSoftwareMenu to complete the
integration.
    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_person.py
    lib/lp/registry/templates/person-owned-teams.pt
    lib/lp/registry/templates/person-related-software-navlinks.pt

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

This branch looks good. I had two small observations:

A tad more commentary in PersonOwnedTeamsViewTestCase would be good.

It looks like line 267 of the diff should be dedented two spaces.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2012-12-07 19:59:37 +0000
+++ lib/lp/registry/browser/configure.zcml 2012-12-11 19:03:23 +0000
@@ -909,6 +909,12 @@
909 template="../templates/person-related-projects.pt"/>909 template="../templates/person-related-projects.pt"/>
910 <browser:page910 <browser:page
911 for="lp.registry.interfaces.person.IPerson"911 for="lp.registry.interfaces.person.IPerson"
912 permission="zope.Public"
913 class="lp.registry.browser.person.PersonOwnedTeamsView"
914 name="+owned-teams"
915 template="../templates/person-owned-teams.pt"/>
916 <browser:page
917 for="lp.registry.interfaces.person.IPerson"
912 class="lp.registry.browser.person.PersonRelatedSoftwareView"918 class="lp.registry.browser.person.PersonRelatedSoftwareView"
913 permission="zope.Public"919 permission="zope.Public"
914 name="+related-software-navlinks"920 name="+related-software-navlinks"
915921
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2012-12-07 19:59:53 +0000
+++ lib/lp/registry/browser/person.py 2012-12-11 19:03:23 +0000
@@ -34,6 +34,7 @@
34 'PersonNavigation',34 'PersonNavigation',
35 'PersonOAuthTokensView',35 'PersonOAuthTokensView',
36 'PersonOverviewMenu',36 'PersonOverviewMenu',
37 'PersonOwnedTeamsView',
37 'PersonRdfContentsView',38 'PersonRdfContentsView',
38 'PersonRdfView',39 'PersonRdfView',
39 'PersonRelatedSoftwareView',40 'PersonRelatedSoftwareView',
@@ -632,6 +633,11 @@
632 enabled = bool(self.person.getAffiliatedPillars(user))633 enabled = bool(self.person.getAffiliatedPillars(user))
633 return Link(target, text, enabled=enabled, icon='info')634 return Link(target, text, enabled=enabled, icon='info')
634635
636 def owned_teams(self):
637 target = '+owned-teams'
638 text = 'Owned teams'
639 return Link(target, text, icon='info')
640
635 def subscriptions(self):641 def subscriptions(self):
636 target = '+subscriptions'642 target = '+subscriptions'
637 text = 'Direct subscriptions'643 text = 'Direct subscriptions'
@@ -694,6 +700,7 @@
694 'activate_ppa',700 'activate_ppa',
695 'maintained',701 'maintained',
696 'manage_vouchers',702 'manage_vouchers',
703 'owned_teams',
697 'synchronised',704 'synchronised',
698 'view_ppa_subscriptions',705 'view_ppa_subscriptions',
699 'ppa',706 'ppa',
@@ -837,7 +844,7 @@
837 usedfor = IPersonRelatedSoftwareMenu844 usedfor = IPersonRelatedSoftwareMenu
838 facet = 'overview'845 facet = 'overview'
839 links = ('related_software_summary', 'maintained', 'uploaded', 'ppa',846 links = ('related_software_summary', 'maintained', 'uploaded', 'ppa',
840 'synchronised', 'projects')847 'synchronised', 'projects', 'owned_teams')
841848
842 @property849 @property
843 def person(self):850 def person(self):
@@ -3778,6 +3785,18 @@
3778 return "Related projects"3785 return "Related projects"
37793786
37803787
3788class PersonOwnedTeamsView(PersonRelatedSoftwareView):
3789 """View for +owned-teams."""
3790 page_title = "Owned teams"
3791
3792 def initialize(self):
3793 """Set up the batch navigation."""
3794 self.batchnav = BatchNavigator(
3795 self.context.getOwnedTeams(self.user), self.request)
3796 self.batchnav.setHeadings('team', 'teams')
3797 self.batch = list(self.batchnav.currentBatch())
3798
3799
3781class PersonOAuthTokensView(LaunchpadView):3800class PersonOAuthTokensView(LaunchpadView):
3782 """Where users can see/revoke their non-expired access tokens."""3801 """Where users can see/revoke their non-expired access tokens."""
37833802
37843803
=== modified file 'lib/lp/registry/browser/tests/test_person.py'
--- lib/lp/registry/browser/tests/test_person.py 2012-12-10 13:43:47 +0000
+++ lib/lp/registry/browser/tests/test_person.py 2012-12-11 19:03:23 +0000
@@ -79,6 +79,7 @@
79from lp.testing.matchers import HasQueryCount79from lp.testing.matchers import HasQueryCount
80from lp.testing.pages import (80from lp.testing.pages import (
81 extract_text,81 extract_text,
82 find_tag_by_id,
82 setupBrowserForUser,83 setupBrowserForUser,
83 )84 )
84from lp.testing.views import (85from lp.testing.views import (
@@ -970,6 +971,48 @@
970 self.view.max_results_to_display)971 self.view.max_results_to_display)
971972
972973
974class PersonOwnedTeamsViewTestCase(TestCaseWithFactory):
975 """Test +owned-teams view."""
976
977 layer = DatabaseFunctionalLayer
978
979 def test_properties(self):
980 # The batch is created when the view is initialized.
981 owner = self.factory.makePerson()
982 team = self.factory.makeTeam(owner=owner)
983 view = create_initialized_view(owner, '+owned-teams')
984 self.assertEqual('Owned teams', view.page_title)
985 self.assertEqual('team', view.batchnav._singular_heading)
986 self.assertEqual([team], view.batch)
987
988 def test_page_text_with_teams(self):
989 # When the person owns teams, the page shows a a listing
990 # table. There is always a link to the team participation page.
991 owner = self.factory.makePerson(name='snarf')
992 self.factory.makeTeam(owner=owner, name='pting')
993 with person_logged_in(owner):
994 view = create_initialized_view(
995 owner, '+owned-teams', principal=owner)
996 markup = view()
997 soup = find_tag_by_id(markup, 'maincontent')
998 participation_link = 'http://launchpad.dev/~snarf/+participation'
999 self.assertIsNotNone(soup.find('a', {'href': participation_link}))
1000 self.assertIsNotNone(soup.find('table', {'id': 'owned-teams'}))
1001 self.assertIsNotNone(soup.find('a', {'href': '/~pting'}))
1002 self.assertIsNotNone(soup.find('table', {'class': 'upper-batch-nav'}))
1003 self.assertIsNotNone(soup.find('table', {'class': 'lower-batch-nav'}))
1004
1005 def test_page_text_without_teams(self):
1006 # When the person does not own teams, the page states the case.
1007 owner = self.factory.makePerson(name='pting')
1008 with person_logged_in(owner):
1009 view = create_initialized_view(
1010 owner, '+owned-teams', principal=owner)
1011 markup = view()
1012 soup = find_tag_by_id(markup, 'maincontent')
1013 self.assertIsNotNone(soup.find('p', {'id': 'no-teams'}))
1014
1015
973class TestPersonSynchronisedPackagesView(TestCaseWithFactory):1016class TestPersonSynchronisedPackagesView(TestCaseWithFactory):
974 """Test the synchronised packages view."""1017 """Test the synchronised packages view."""
9751018
9761019
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2012-11-19 21:59:03 +0000
+++ lib/lp/registry/interfaces/person.py 2012-12-11 19:03:23 +0000
@@ -1301,6 +1301,17 @@
1301 The person's membership may be direct or indirect.1301 The person's membership may be direct or indirect.
1302 """1302 """
13031303
1304 @call_with(user=REQUEST_USER)
1305 @operation_returns_collection_of(Interface) # Really ITeam.
1306 @export_read_operation()
1307 @operation_for_version("devel")
1308 def getOwnedTeams(user=None):
1309 """Return the teams that this person owns.
1310
1311 The iterator includes the teams that the user owns, but it not
1312 a member of.
1313 """
1314
1304 def getAdministratedTeams():1315 def getAdministratedTeams():
1305 """Return the teams that this person/team is an administrator of.1316 """Return the teams that this person/team is an administrator of.
13061317
@@ -1308,7 +1319,6 @@
1308 member with admin privilege, or member of a team with such1319 member with admin privilege, or member of a team with such
1309 privileges. It excludes teams which have been merged.1320 privileges. It excludes teams which have been merged.
1310 """1321 """
1311
1312 def getTeamAdminsEmailAddresses():1322 def getTeamAdminsEmailAddresses():
1313 """Return a set containing the email addresses of all administrators1323 """Return a set containing the email addresses of all administrators
1314 of this team.1324 of this team.
@@ -2547,6 +2557,8 @@
2547# 'lazr.webservice.exported')['return_type'].value_type.schema = IPerson2557# 'lazr.webservice.exported')['return_type'].value_type.schema = IPerson
2548IPersonViewRestricted['getMembersByStatus'].queryTaggedValue(2558IPersonViewRestricted['getMembersByStatus'].queryTaggedValue(
2549 LAZR_WEBSERVICE_EXPORTED)['return_type'].value_type.schema = IPerson2559 LAZR_WEBSERVICE_EXPORTED)['return_type'].value_type.schema = IPerson
2560IPersonViewRestricted['getOwnedTeams'].queryTaggedValue(
2561 LAZR_WEBSERVICE_EXPORTED)['return_type'].value_type.schema = ITeam
25502562
2551# Fix schema of ITeamMembership fields. Has to be done here because of2563# Fix schema of ITeamMembership fields. Has to be done here because of
2552# circular dependencies.2564# circular dependencies.
25532565
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2012-12-07 10:04:07 +0000
+++ lib/lp/registry/model/person.py 2012-12-11 19:03:23 +0000
@@ -1765,6 +1765,18 @@
1765 tm.setExpirationDate(expires, reviewer)1765 tm.setExpirationDate(expires, reviewer)
1766 tm.setStatus(status, reviewer, comment=comment)1766 tm.setStatus(status, reviewer, comment=comment)
17671767
1768 def getOwnedTeams(self, user=None):
1769 """See `IPerson`."""
1770 query = And(
1771 get_person_visibility_terms(user),
1772 Person.teamowner == self.id,
1773 Person.merged == None)
1774 store = IStore(Person)
1775 results = store.find(
1776 Person, query).order_by(
1777 Upper(Person.displayname), Upper(Person.name))
1778 return results
1779
1768 @cachedproperty1780 @cachedproperty
1769 def administrated_teams(self):1781 def administrated_teams(self):
1770 return list(self.getAdministratedTeams())1782 return list(self.getAdministratedTeams())
17711783
=== added file 'lib/lp/registry/templates/person-owned-teams.pt'
--- lib/lp/registry/templates/person-owned-teams.pt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/templates/person-owned-teams.pt 2012-12-11 19:03:23 +0000
@@ -0,0 +1,70 @@
1<html
2 xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:tal="http://xml.zope.org/namespaces/tal"
4 xmlns:metal="http://xml.zope.org/namespaces/metal"
5 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
6 metal:use-macro="view/macro:page/main_only"
7 i18n:domain="launchpad"
8>
9
10<body>
11
12<div metal:fill-slot="heading">
13 <h1 tal:content="view/page_title"/>
14</div>
15
16<div metal:fill-slot="main">
17 <div class="top-portlet">
18 <tal:navlinks replace="structure context/@@+related-software-navlinks"/>
19 </div>
20
21
22 <div id="teams" class="top-portlet">
23 <p>
24 Team owners are not always team members. The
25 <a tal:attributes="href context/menu:overview/memberships/fmt:url">team
26 participation</a> page shows the teams that
27 <tal:name replace="context/displayname" /> is a member of.
28 </p>
29
30 <tal:navigation_top
31 replace="structure view/batchnav/@@+navigation-links-upper" />
32
33 <tal:maintained-packages define="teams view/batch">
34
35 <table id="owned-teams" class="listing"
36 tal:condition="teams">
37 <cols><col width="30%" /><col width="auto" /></cols>
38
39 <thead>
40 <tr>
41 <th>Name</th>
42 <th>Summary</th>
43 </tr>
44 </thead>
45
46 <tbody>
47 <tr tal:repeat="team teams">
48 <td>
49 <a tal:replace="structure team/fmt:link" />
50 </td>
51 <td>
52 <tal:summary replace="team/description/fmt:shorten/60"/>
53 </td>
54 </tr>
55 </tbody>
56 </table>
57
58 <tal:navigation_bottom
59 replace="structure view/batchnav/@@+navigation-links-lower" />
60
61 <p id="no-teams"
62 tal:condition="not: teams">
63 <span tal:replace="context/displayname">Foo Bar</span>
64 doesn't own any teams.
65 </p>
66 </tal:maintained-packages>
67 </div>
68</div>
69</body>
70</html>
071
=== modified file 'lib/lp/registry/templates/person-related-software-navlinks.pt'
--- lib/lp/registry/templates/person-related-software-navlinks.pt 2011-09-23 07:49:54 +0000
+++ lib/lp/registry/templates/person-related-software-navlinks.pt 2012-12-11 19:03:23 +0000
@@ -29,6 +29,10 @@
29 tal:define="link view/menu:navigation/projects"29 tal:define="link view/menu:navigation/projects"
30 tal:condition="link/enabled"30 tal:condition="link/enabled"
31 tal:content="structure link/fmt:link" />31 tal:content="structure link/fmt:link" />
32 <li
33 tal:define="link view/menu:navigation/owned_teams"
34 tal:condition="link/enabled"
35 tal:content="structure link/fmt:link" />
32 </ul>36 </ul>
3337
34</tal:root>38</tal:root>
3539
=== modified file 'lib/lp/registry/templates/product-portlet-requires-subscription.pt'
--- lib/lp/registry/templates/product-portlet-requires-subscription.pt 2012-12-07 19:45:34 +0000
+++ lib/lp/registry/templates/product-portlet-requires-subscription.pt 2012-12-11 19:03:23 +0000
@@ -21,7 +21,7 @@
21 <span21 <span
22 tal:define="user_menu view/user/menu:overview"22 tal:define="user_menu view/user/menu:overview"
23 tal:condition="context/qualifies_for_free_hosting">23 tal:condition="context/qualifies_for_free_hosting">
24 <br />Purchase a commercial subcription from your24 <br />Purchase a commercial subscription from your
25 <a tal:replace="structure user_menu/manage_vouchers/fmt:link" />25 <a tal:replace="structure user_menu/manage_vouchers/fmt:link" />
26 page.26 page.
27 </span>27 </span>
@@ -36,7 +36,7 @@
3636
37 <ul class="bulleted" style="clear:left;">37 <ul class="bulleted" style="clear:left;">
38 <li tal:define="user_menu view/user/menu:overview">38 <li tal:define="user_menu view/user/menu:overview">
39 Purchase a commercial subcription from your39 Purchase a commercial subscription from your
40 <a tal:replace="structure user_menu/manage_vouchers/fmt:link" />40 <a tal:replace="structure user_menu/manage_vouchers/fmt:link" />
41 page.41 page.
42 </li>42 </li>
@@ -45,7 +45,7 @@
45 <a class="sprite maybe"45 <a class="sprite maybe"
46 tal:define="config modules/lp.services.config/config"46 tal:define="config modules/lp.services.config/config"
47 tal:attributes="href config/commercial/licensing_policy_url"47 tal:attributes="href config/commercial/licensing_policy_url"
48 >Launchpad's licensing policies</a>48 >Launchpad's licensing policies</a>.
49 </li>49 </li>
50 </ul>50 </ul>
51 </tal:proprietary>51 </tal:proprietary>
5252
=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py 2012-11-26 08:33:03 +0000
+++ lib/lp/registry/tests/test_person.py 2012-12-11 19:03:23 +0000
@@ -70,6 +70,7 @@
70from lp.soyuz.enums import ArchivePurpose70from lp.soyuz.enums import ArchivePurpose
71from lp.testing import (71from lp.testing import (
72 celebrity_logged_in,72 celebrity_logged_in,
73 launchpadlib_for,
73 login,74 login,
74 login_person,75 login_person,
75 logout,76 logout,
@@ -266,6 +267,49 @@
266 retrieved_members = sorted(list(self.a_team.all_members_prepopulated))267 retrieved_members = sorted(list(self.a_team.all_members_prepopulated))
267 self.assertEqual(expected_members, retrieved_members)268 self.assertEqual(expected_members, retrieved_members)
268269
270 def test_getOwnedTeams(self):
271 # The iterator contains the teams that person owns, regardless of
272 # membership.
273 owner = self.a_team.teamowner
274 with person_logged_in(owner):
275 owner.leave(self.a_team)
276 results = list(owner.getOwnedTeams(self.user))
277 self.assertEqual([self.a_team], results)
278
279 def test_getOwnedTeams_visibility(self):
280 # The iterator contains the teams that the user can see.
281 owner = self.a_team.teamowner
282 p_team = self.factory.makeTeam(
283 name='p', owner=owner, visibility=PersonVisibility.PRIVATE)
284 results = list(owner.getOwnedTeams(self.user))
285 self.assertEqual([self.a_team], results)
286 results = list(owner.getOwnedTeams(owner))
287 self.assertEqual([self.a_team, p_team], results)
288
289 def test_getOwnedTeams_webservice(self):
290 # The user in the interaction is used as the user arg.
291 owner = self.a_team.teamowner
292 self.factory.makeTeam(
293 name='p', owner=owner, visibility=PersonVisibility.PRIVATE)
294 owner_name = owner.name
295 lp = launchpadlib_for('test', person=self.user)
296 lp_owner = lp.people[owner_name]
297 results = lp_owner.getOwnedTeams()
298 self.assertEqual(['a'], [t.name for t in results])
299
300 def test_getOwnedTeams_webservice_anonymous(self):
301 # The user in the interaction is used as the user arg.
302 # Anonymous scripts also do not reveal private teams.
303 owner = self.a_team.teamowner
304 self.factory.makeTeam(
305 name='p', owner=owner, visibility=PersonVisibility.PRIVATE)
306 owner_name = owner.name
307 logout()
308 lp = launchpadlib_for('test', person=None)
309 lp_owner = lp.people[owner_name]
310 results = lp_owner.getOwnedTeams()
311 self.assertEqual(['a'], [t.name for t in results])
312
269 def test_administrated_teams(self):313 def test_administrated_teams(self):
270 # The property Person.administrated_teams is a cached copy of314 # The property Person.administrated_teams is a cached copy of
271 # the result of Person.getAdministratedTeams().315 # the result of Person.getAdministratedTeams().
272316
=== modified file 'lib/lp/soyuz/stories/soyuz/xx-person-packages.txt'
--- lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2012-11-19 07:31:45 +0000
+++ lib/lp/soyuz/stories/soyuz/xx-person-packages.txt 2012-12-11 19:03:23 +0000
@@ -20,6 +20,7 @@
20 Uploaded packages20 Uploaded packages
21 Related PPA packages21 Related PPA packages
22 Related projects22 Related projects
23 Owned teams
2324
24 >>> print browser.getLink("Maintained packages").url25 >>> print browser.getLink("Maintained packages").url
25 http://launchpad.dev/~mark/+maintained-packages26 http://launchpad.dev/~mark/+maintained-packages