=== modified file 'lib/lp/registry/browser/person.py' --- lib/lp/registry/browser/person.py 2010-06-04 18:58:48 +0000 +++ lib/lp/registry/browser/person.py 2010-06-08 12:58:26 +0000 @@ -3072,22 +3072,51 @@ def label(self): return 'Team participation for ' + self.context.displayname - @property - def indirect_teams_via(self): - """Information about indirect membership. + def _asParticipation(self, membership, team_path=None): + """Return a dict of participation information for the membership.""" + if team_path is None: + via = None + else: + via = COMMASPACE.join(team.displayname for team in team_path) + team = membership.team + if membership.person == team.teamowner: + role = 'Owner' + elif membership.status == TeamMembershipStatus.ADMIN: + role = 'Admin' + else: + role = 'Member' + if team.mailing_list is not None and team.mailing_list.is_usable: + subscription = team.mailing_list.getSubscription(self.context) + if subscription is None: + subscribed = 'Not subscribed' + else: + subscribed = 'Subscribed' + else: + subscribed = None + return dict( + displayname=team.displayname, team=team, membership=membership, + role=role, via=via, subscribed=subscribed) - :return: A list of dictionaries, where each dictionary has a team in - which the person is an indirect member, and a path to membership in - that team. - :rtype: a list of dictionaries - """ - indirect_teams = [] + @cachedproperty + def active_participations(self): + """Return the participation information for active memberships.""" + participations = [self._asParticipation(membership) + for membership in self.context.myactivememberships + if check_permission('launchpad.View', membership)] + membership_set = getUtility(ITeamMembershipSet) for team in self.context.teams_indirectly_participated_in: - via = COMMASPACE.join(viateam.displayname - for viateam - in self.context.findPathToTeam(team)[:-1]) - indirect_teams.append(dict(team=team, via=via)) - return indirect_teams + # The key points of the path for presentation are: + # [-?] indirect memberships, [-2] direct membership, [-1] team. + team_path = self.context.findPathToTeam(team) + membership = membership_set.getByPersonAndTeam( + team_path[-2], team) + participations.append( + self._asParticipation(membership, team_path=team_path[:-1])) + return sorted(participations, key=itemgetter('displayname')) + + @cachedproperty + def has_participations(self): + return len(self.active_participations) > 0 class EmailAddressVisibleState: === modified file 'lib/lp/registry/browser/tests/test_person_view.py' --- lib/lp/registry/browser/tests/test_person_view.py 2010-05-11 12:31:49 +0000 +++ lib/lp/registry/browser/tests/test_person_view.py 2010-06-08 12:58:26 +0000 @@ -12,12 +12,16 @@ from canonical.launchpad.webapp.interfaces import NotFoundError from lp.registry.interfaces.karma import IKarmaCacheManager from canonical.launchpad.webapp.servers import LaunchpadTestRequest -from canonical.testing import LaunchpadFunctionalLayer, LaunchpadZopelessLayer +from canonical.testing import ( + DatabaseFunctionalLayer, LaunchpadFunctionalLayer, LaunchpadZopelessLayer) from lp.registry.browser.person import PersonEditView, PersonView +from lp.registry.interfaces.person import PersonVisibility +from lp.registry.interfaces.teammembership import TeamMembershipStatus from lp.registry.model.karma import KarmaCategory from lp.soyuz.tests.test_publishing import SoyuzTestPublisher from lp.soyuz.interfaces.archive import ArchiveStatus from lp.testing import TestCaseWithFactory, login_person +from lp.testing.views import create_view class TestPersonViewKarma(TestCaseWithFactory): @@ -112,7 +116,7 @@ def test_viewing_self(self): # If the current user has edit access to the context person then - # the section should always display + # the section should always display. login_person(self.owner) person_view = PersonView(self.owner, LaunchpadTestRequest()) self.failUnless(person_view.should_show_ppa_section) @@ -226,5 +230,112 @@ self.assertFalse(self.view.form_fields['name'].for_display) +class TestPersonParticipationView(TestCaseWithFactory): + + layer = DatabaseFunctionalLayer + + def setUp(self): + super(TestPersonParticipationView, self).setUp() + self.user = self.factory.makePerson() + self.view = create_view(self.user, name='+participation') + + def test__asParticpation_owner(self): + # Team owners have the role of 'Owner'. + self.factory.makeTeam(owner=self.user) + [participation] = self.view.active_participations + self.assertEqual('Owner', participation['role']) + + def test__asParticpation_admin(self): + # Team admins have the role of 'Admin'. + team = self.factory.makeTeam() + login_person(team.teamowner) + team.addMember(self.user, team.teamowner) + for membership in self.user.myactivememberships: + membership.setStatus( + TeamMembershipStatus.ADMIN, team.teamowner) + [participation] = self.view.active_participations + self.assertEqual('Admin', participation['role']) + + def test__asParticpation_member(self): + # The default team role is 'Member'. + team = self.factory.makeTeam() + login_person(team.teamowner) + team.addMember(self.user, team.teamowner) + [participation] = self.view.active_participations + self.assertEqual('Member', participation['role']) + + def test__asParticpation_without_mailing_list(self): + # The default team role is 'Member'. + team = self.factory.makeTeam() + login_person(team.teamowner) + team.addMember(self.user, team.teamowner) + [participation] = self.view.active_participations + self.assertEqual(None, participation['subscribed']) + + def test__asParticpation_unsubscribed_to_mailing_list(self): + # The default team role is 'Member'. + team = self.factory.makeTeam() + self.factory.makeMailingList(team, team.teamowner) + login_person(team.teamowner) + team.addMember(self.user, team.teamowner) + [participation] = self.view.active_participations + self.assertEqual('Not subscribed', participation['subscribed']) + + def test__asParticpation_subscribed_to_mailing_list(self): + # The default team role is 'Member'. + team = self.factory.makeTeam() + mailing_list = self.factory.makeMailingList(team, team.teamowner) + mailing_list.subscribe(self.user) + login_person(team.teamowner) + team.addMember(self.user, team.teamowner) + [participation] = self.view.active_participations + self.assertEqual('Subscribed', participation['subscribed']) + + def test_active_participations_with_private_team(self): + # Users cannot see private teams that they are not members of. + team = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE) + login_person(team.teamowner) + team.addMember(self.user, team.teamowner) + # The team is included in active_participations. + login_person(self.user) + view = create_view( + self.user, name='+participation', principal=self.user) + self.assertEqual(1, len(view.active_participations)) + # The team is not included in active_participations. + observer = self.factory.makePerson() + login_person(observer) + view = create_view( + self.user, name='+participation', principal=observer) + self.assertEqual(0, len(view.active_participations)) + + def test_active_participations_indirect_membership(self): + # Verify the path of indirect membership. + a_team = self.factory.makeTeam(name='a') + b_team = self.factory.makeTeam(name='b', owner=a_team) + c_team = self.factory.makeTeam(name='c', owner=b_team) + login_person(a_team.teamowner) + a_team.addMember(self.user, a_team.teamowner) + transaction.commit() + participations = self.view.active_participations + self.assertEqual(3, len(participations)) + display_names = [ + participation['displayname'] for participation in participations] + self.assertEqual(['A', 'B', 'C'], display_names) + self.assertEqual(None, participations[0]['via']) + self.assertEqual('A', participations[1]['via']) + self.assertEqual('A, B', participations[2]['via']) + + def test_has_participations_false(self): + participations = self.view.active_participations + self.assertEqual(0, len(participations)) + self.assertEqual(False, self.view.has_participations) + + def test_has_participations_true(self): + self.factory.makeTeam(owner=self.user) + participations = self.view.active_participations + self.assertEqual(1, len(participations)) + self.assertEqual(True, self.view.has_participations) + + def test_suite(): return unittest.TestLoader().loadTestsFromName(__name__) === modified file 'lib/lp/registry/stories/team/xx-team-membership.txt' --- lib/lp/registry/stories/team/xx-team-membership.txt 2010-01-15 13:36:09 +0000 +++ lib/lp/registry/stories/team/xx-team-membership.txt 2010-06-08 12:58:26 +0000 @@ -204,62 +204,48 @@ ======================= The team participation page shows the team in which a person is a direct -member, as well as the teams in which they are an indirect member. We will -just make sure that this page is displaying without errors in several cases: - - - for a person with no memberships at all - - for a person with direct memberships only - - for a person with some indirect memberships - - for a team with direct memberships only - -First, Kiko has not joined any teams: - - >>> browser = setupBrowser() - >>> browser.open('http://launchpad.dev/~kiko/+participation') - >>> 'has not yet joined any teams' in browser.contents - True - >>> direct = find_portlet(browser.contents, 'Direct membership') - >>> print direct - None - >>> indirect = find_portlet(browser.contents, 'Indirect membership') - >>> print indirect - None - - -Next, Marilize Coetzee is only a direct member: - - >>> browser.open('http://launchpad.dev/~marilize/+participation') - >>> 'has not yet joined any teams' in browser.contents - False - >>> print extract_text(find_portlet(browser.contents, 'Direct membership')) - Direct membership - ShipIt Administrators - Joined on ... - >>> print find_portlet(browser.contents, 'Indirect membership') - None - - -Next, Sample Person has both direct and indirect memberships: - - >>> browser.open('http://launchpad.dev/~name12/+participation') - >>> 'has not yet joined any teams' in browser.contents - False - >>> direct = find_portlet(browser.contents, 'Direct membership') - >>> 'Landscape Developers' in direct.renderContents() - True - >>> indirect = find_portlet(browser.contents, 'Indirect membership') - >>> 'Ubuntu Gnome Team' in indirect.renderContents() - True - - -The Ubuntu Team is only a direct member of other teams: - - >>> browser.open('http://launchpad.dev/~ubuntu-team/+participation') - >>> 'has not yet joined any teams' in browser.contents - False - >>> direct = find_portlet(browser.contents, 'Direct membership') - >>> 'GuadaMen' in direct.renderContents() - True - >>> indirect = find_portlet(browser.contents, 'Indirect membership') - >>> print indirect - None +member, as well as the teams in which they are an indirect member. + +Kiko has not joined any teams: + + >>> anon_browser.open('http://launchpad.dev/~kiko/+participation') + >>> print extract_text( + ... find_tag_by_id(anon_browser.contents, 'no-participation')) + Christian Reis has not yet joined any teams. + >>> print find_tag_by_id(anon_browser.contents, 'participation') + None + +Sample Person has both direct and indirect memberships: + + >>> anon_browser.open('http://launchpad.dev/~name12/+participation') + >>> content = find_main_content(anon_browser.contents) + >>> print find_tag_by_id(content, 'no-participation') + None + + >>> print extract_text( + ... find_tag_by_id(content, 'participation')) + Team Joined Role Via Mailing List + HWDB Team 2009-07-09 Member — — + Landscape Developers 2006-07-11 Owner — — + Launchpad Users 2008-11-26 Owner — — + Ubuntu Gnome Team — Member Warty Security Team — + Warty Security Team 2007-01-26 Member — — + +User can see links to register teams and change their mailing list +subscriptions on their own participation page. + + >>> print find_tag_by_id(content, 'participation-actions') + None + + >>> user_browser.open('http://launchpad.dev/~no-priv/+participation') + >>> actions = find_tag_by_id( + ... user_browser.contents, 'participation-actions') + >>> print extract_text(actions) + Register a team + Change mailing list subscriptions + + >>> user_browser.getLink('Register a team') + + >>> user_browser.getLink('Change mailing list subscriptions') + + === modified file 'lib/lp/registry/stories/teammembership/xx-private-membership.txt' --- lib/lp/registry/stories/teammembership/xx-private-membership.txt 2010-04-23 13:50:57 +0000 +++ lib/lp/registry/stories/teammembership/xx-private-membership.txt 2010-06-08 12:58:26 +0000 @@ -118,57 +118,6 @@ Other Team -=== Indirect Membership === - -If a person is a member of a public team that is a member of a private -membership team then he is indirectly a member of the private -membership team. The rules for disclosing that information are the -same as for direct membership. - -My Team invites the Launchpad Admins team to join them. - - >>> owner_browser = setupBrowser(auth='Basic owner@canonical.com:test') - >>> owner_browser.open('http://launchpad.dev/~myteam/+addmember') - >>> owner_browser.getControl('New member').value = 'admins' - >>> owner_browser.getControl('Add Member').click() - -Foo Bar accepts the invitation for the Launchpad Admins, which makes -him, and all other admins, an indirect member of My Team. - - >>> admin_browser.open('http://launchpad.dev/~admins/+invitation/myteam') - >>> admin_browser.getControl('Accept').click() - -All Launchpad Admin members are indirectly members of My Team and -their participation is visible to other team members. - - >>> owner_browser.open('http://launchpad.dev/~name16/+participation') - >>> div = find_tag_by_id(owner_browser.contents, 'indirect participation') - >>> a_tags = div.findAll('a') - >>> for a_tag in a_tags: - ... print a_tag.contents - [u'Mailing List Experts'] - [u'My Team'] - -People who are not members of My Team, such as No Privileges Person, -do not see it in the indirect participation list for the members who -are. - - >>> user_browser.open('http://launchpad.dev/~name16/+participation') - >>> div = find_tag_by_id(user_browser.contents, 'indirect participation') - >>> a_tags = div.findAll('a') - >>> for a_tag in a_tags: - ... print a_tag.contents - [u'Mailing List Experts'] - -And anonymous users do not see the membership either. - - >>> anon_browser.open('http://launchpad.dev/~name16/+participation') - >>> div = find_tag_by_id(anon_browser.contents, 'indirect participation') - >>> a_tags = div.findAll('a') - >>> for a_tag in a_tags: - ... print a_tag.contents - [u'Mailing List Experts'] - == Teams with Icons == The person page also shows a list of icons for all the teams that @@ -214,6 +163,7 @@ Even the owner of the team with private membership should not see MyTeam as an option in the +answer-contact form. + >>> owner_browser = setupBrowser(auth='Basic owner@canonical.com:test') >>> owner_browser.open( ... 'http://answers.launchpad.dev/ubuntu/+answer-contact') >>> team_div = find_tag_by_id(owner_browser.contents, === modified file 'lib/lp/registry/templates/person-participation.pt' --- lib/lp/registry/templates/person-participation.pt 2009-09-15 21:16:37 +0000 +++ lib/lp/registry/templates/person-participation.pt 2010-06-08 12:58:26 +0000 @@ -8,55 +8,79 @@ >
-+
Foo Bar is a member of the following teams:
-+ +
Foo Bar has not yet joined any teams.
-Team | +Joined | +Role | +Via | +Mailing List | +
---|---|---|---|---|
+ name + | +
+ |
+ + Member + | +
+ |
+
+ |
+
- | -
-
- Team name
-
-
- Via
- guadamen.
-
- |
-