← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/no-team-admin-restrictions into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/no-team-admin-restrictions into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #206058 in Launchpad itself: "Team Administrators cannot change their membership expiration date"
  https://bugs.launchpad.net/launchpad/+bug/206058

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/no-team-admin-restrictions/+merge/84571

This branch removes a longstanding restriction preventing team admins from promoting other members to admins or changing their own expiration date. It confuses people (eg. bug #206058), is fairly pointless (admins can do just about any other damage to the team), and is easy to socially engineer your way around (teams are frequently handed over to their admins upon request when their owner is inactive).

sinzui says we should do it, and lifeless says he has no intrinsic objection.
-- 
https://code.launchpad.net/~wgrant/launchpad/no-team-admin-restrictions/+merge/84571
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/no-team-admin-restrictions into lp:launchpad.
=== modified file 'lib/lp/registry/browser/teammembership.py'
--- lib/lp/registry/browser/teammembership.py	2011-07-28 14:19:11 +0000
+++ lib/lp/registry/browser/teammembership.py	2011-12-06 06:50:30 +0000
@@ -15,7 +15,6 @@
 import pytz
 from zope.app.form import CustomWidgetFactory
 from zope.app.form.interfaces import InputErrors
-from zope.component import getUtility
 from zope.formlib import form
 from zope.schema import Date
 
@@ -26,7 +25,6 @@
     )
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from lp.app.errors import UnexpectedFormData
-from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.app.widgets.date import DateWidget
 from lp.registry.interfaces.teammembership import TeamMembershipStatus
 
@@ -96,13 +94,6 @@
         return '%s member %s' % (prefix, self.context.person.displayname)
 
     # Boolean helpers
-    def userIsTeamOwnerOrLPAdmin(self):
-        return (self.user.inTeam(self.context.team.teamowner) or
-                self.user.inTeam(getUtility(ILaunchpadCelebrities).admin))
-
-    def allowChangeAdmin(self):
-        return self.userIsTeamOwnerOrLPAdmin() or self.isAdmin()
-
     def isActive(self):
         return self.context.status in [TeamMembershipStatus.APPROVED,
                                        TeamMembershipStatus.ADMIN]
@@ -225,12 +216,7 @@
                 context.status == TeamMembershipStatus.ADMIN):
                 new_status = TeamMembershipStatus.APPROVED
             elif (form.get('admin') == "yes" and
-                  context.status == TeamMembershipStatus.APPROVED
-                  # XXX: salgado 2005-03-15: The clause below is a hack
-                  # to make sure only the teamowner can promote a given
-                  # member to admin, while we don't have a specific
-                  # permission setup for this.
-                  and self.userIsTeamOwnerOrLPAdmin()):
+                  context.status == TeamMembershipStatus.APPROVED):
                 new_status = TeamMembershipStatus.ADMIN
             else:
                 # No status change will happen

=== modified file 'lib/lp/registry/doc/teammembership-email-notification.txt'
--- lib/lp/registry/doc/teammembership-email-notification.txt	2011-11-15 01:01:55 +0000
+++ lib/lp/registry/doc/teammembership-email-notification.txt	2011-12-06 06:50:30 +0000
@@ -1,10 +1,11 @@
-= Mail notifications for membership changes =
+Mail notifications for membership changes
+=========================================
 
-Whenever a membership status is changed, we should notify the team admins and
-the member whose membership changed. There's a few cases where we might want
-to notify only the team admins, but in most of the cases we'll be sending two
-similar (but not identical) notifications: one for all team admins and another
-for the member.
+Whenever a membership status is changed, we should notify the team
+admins and the member whose membership changed. There's a few cases
+where we might want to notify only the team admins, but in most of the
+cases we'll be sending two similar (but not identical) notifications:
+one for all team admins and another for the member.
 
     >>> def by_to_addrs(a, b):
     ...     return cmp(a[1], b[1])
@@ -12,7 +13,7 @@
     >>> def setStatus(membership, status, reviewer=None, comment=None,
     ...               silent=False):
     ...     """Set the status of the given membership.
-    ...
+    >>> 
     ...     Also sets the reviewer and comment, calling flush_database_updates
     ...     and transaction.commit after, to ensure the changes are flushed to
     ...     the database.
@@ -47,7 +48,6 @@
     >>> from lp.testing.sampledata import ADMIN_EMAIL
     >>> admin_person = personset.getByEmail(ADMIN_EMAIL)
 
-
 In open teams joining and leaving the team generates no notifications.
 
     >>> login_person(admin_person)
@@ -58,15 +58,16 @@
     >>> membership = membershipset.getByPersonAndTeam(new_person, open_team)
     >>> membership.status.title
     'Approved'
+
     >>> run_mail_jobs()
     >>> len(stub.test_emails) - base_mails
     0
+
     >>> new_person.leave(open_team)
     >>> run_mail_jobs()
     >>> len(stub.test_emails) - base_mails
     0
 
-
 Now Robert Collins proposes himself as a member of the Ubuntu Team. This
 generates a notification email only to Ubuntu Team administrators.
 
@@ -80,6 +81,7 @@
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     5
+
     >>> print_distinct_emails(include_reply_to=True)
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -108,11 +110,12 @@
     You received this email because you are the owner of the Ubuntu Team team.
     ----------------------------------------
 
-Declining a proposed member should generate notifications for both the member
-and each of the team's admins.
+Declining a proposed member should generate notifications for both the
+member and each of the team's admins.
 
     # Need to be logged in as a team admin to be able to change memberships of
     # that team.
+
     >>> login('mark@xxxxxxxxxxx')
     >>> setStatus(membership, TeamMembershipStatus.DECLINED, reviewer=mark)
 
@@ -153,6 +156,7 @@
 
     # Remove notification of daf's membership pending approval from
     # stub.test_emails
+
     >>> transaction.commit()
     >>> dummy = pop_notifications()
 
@@ -162,6 +166,7 @@
     >>> stub.test_emails.sort(by_to_addrs)
     >>> len(stub.test_emails)
     6
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -196,6 +201,7 @@
     >>> stub.test_emails.sort(by_to_addrs)
     >>> len(stub.test_emails)
     6
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -216,15 +222,17 @@
     <http://launchpad.dev/~ubuntu-team>
     ----------------------------------------
 
-Team admins can propose their teams using the join() method as well, but in
-that case we'll use the requester's (the person proposing the team as the
-other's member) email address in the 'Reply-To' header of the message sent.
+Team admins can propose their teams using the join() method as well, but
+in that case we'll use the requester's (the person proposing the team as
+the other's member) email address in the 'Reply-To' header of the
+message sent.
 
     >>> admins = personset.getByName('admins')
     >>> admins.join(ubuntu_team, requester=mark)
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     5
+
     >>> print_distinct_emails(include_reply_to=True)
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -254,11 +262,12 @@
     ----------------------------------------
 
 
-== Adding new members ==
+Adding new members
+------------------
 
 When a person is added as a member of a team by one of that team's
-administrators, an email is sent to all team administrators and to the new
-member.
+administrators, an email is sent to all team administrators and to the
+new member.
 
     >>> cprov = personset.getByName('cprov')
     >>> marilize = personset.getByName('marilize')
@@ -269,6 +278,7 @@
 
     >>> len(stub.test_emails)
     6
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: marilize@xxxxxxx
@@ -307,17 +317,19 @@
     You received this email because you are the owner of the Ubuntu Team team.
     ----------------------------------------
 
-By default, if the newly added member is actually a team, we'll only send
-an invitation to the team's admins, telling them that the membership will
-only be activated if they accept the invitation.
+By default, if the newly added member is actually a team, we'll only
+send an invitation to the team's admins, telling them that the
+membership will only be activated if they accept the invitation.
 
     >>> mirror_admins = personset.getByName('ubuntu-mirror-admins')
     >>> mirror_admins.getTeamAdminsEmailAddresses()
     ['mark@xxxxxxxxxxx']
+
     >>> ignored = ubuntu_team.addMember(mirror_admins, reviewer=cprov)
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     1
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: mark@xxxxxxxxxxx
@@ -336,8 +348,9 @@
     The Launchpad team
     ----------------------------------------
 
-If one of the admins accept the invitation, then a notification is sent to the
-team which just became a member and to the admins of the hosting team.
+If one of the admins accept the invitation, then a notification is sent
+to the team which just became a member and to the admins of the hosting
+team.
 
     >>> comment = "Of course I want to be part of ubuntu!"
     >>> mirror_admins.acceptInvitationToBeMemberOf(ubuntu_team, comment)
@@ -346,6 +359,7 @@
 
     >>> len(stub.test_emails)
     6
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -369,6 +383,7 @@
 
     # Reset stub.test_emails as we don't care about the notification triggered
     # by the addMember() call.
+
     >>> transaction.commit()
     >>> stub.test_emails = []
 
@@ -379,6 +394,7 @@
 
     >>> len(stub.test_emails)
     7
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -404,6 +420,7 @@
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     5
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: foo.bar@xxxxxxxxxxxxx
@@ -437,60 +454,24 @@
     ----------------------------------------
 
 
-== Membership expiration warnings ==
+Membership expiration warnings
+------------------------------
 
-When we get close to the expiration date of a given membership, an expiration
-warning is sent to the member, so that he can contact the team's
-administrators (or renew it himself when he has necessary rights) in case
-he wants to retain that membership. This is done by the
-flag-expired-memberships cronscript, which uses
+When we get close to the expiration date of a given membership, an
+expiration warning is sent to the member, so that he can contact the
+team's administrators (or renew it himself when he has necessary rights)
+in case he wants to retain that membership. This is done by the flag-
+expired-memberships cronscript, which uses
 ITeamMembership.sendExpirationWarningEmail to do its job.
 
     >>> import pytz
     >>> from datetime import datetime, timedelta
     >>> utc_now = datetime.now(pytz.timezone('UTC'))
-    >>> kamion_on_ubuntu_team = membershipset.getByPersonAndTeam(
-    ...     kamion, ubuntu_team)
-    >>> kamion_on_ubuntu_team.setExpirationDate(
-    ...     utc_now + timedelta(days=9), mark)
-    >>> flush_database_updates()
-
-Kamion is an admin of the Ubuntu team, but team admins can't change the
-expiration date of their own memberships, so he still has to contact one of
-the other team admins.
-
-    >>> kamion_on_ubuntu_team.status.name
-    'ADMIN'
-    >>> kamion_on_ubuntu_team.sendExpirationWarningEmail()
-    >>> transaction.commit()
-    >>> print_distinct_emails()
-    From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
-    To: colin.watson@xxxxxxxxxxxxxxx
-    Subject: Your membership in ubuntu-team is about to expire
-    <BLANKLINE>
-    On ..., 9 days from now, your membership
-    in the Ubuntu Team (ubuntu-team) Launchpad team
-    is due to expire.
-    <http://launchpad.dev/~ubuntu-team>
-    <BLANKLINE>
-    To prevent this membership from expiring, you should get in touch
-    with one of the team's administrators:
-    Alexander Limi (limi) <http://launchpad.dev/~limi>
-    Foo Bar (name16) <http://launchpad.dev/~name16>
-    Jeff Waugh (jdub) <http://launchpad.dev/~jdub>
-    Mark Shuttleworth (mark) <http://launchpad.dev/~mark>
-    <BLANKLINE>
-    If your membership does expire, we'll send you one more message to let
-    you know it's happened.
-    <BLANKLINE>
-    Thanks for using Launchpad!
-    <BLANKLINE>
-    ----------------------------------------
 
 In the case of the beta-testers team, the email is sent only to the
 team's owner, which doesn't have the necessary rights to renew the
-membership of his team, so he's instructed to contact one of the
-ubuntu-team's admins.
+membership of his team, so he's instructed to contact one of the ubuntu-
+team's admins.
 
     >>> beta_testers = personset.getByName('launchpad-beta-testers')
     >>> beta_testers_on_ubuntu_team = membershipset.getByPersonAndTeam(
@@ -526,11 +507,15 @@
     <BLANKLINE>
     ----------------------------------------
 
-If the team's renewal policy is ONDEMAND, though, the member is invited to
-renew his own membership.
+If the team's renewal policy is ONDEMAND, though, the member is invited
+to renew his own membership.
 
     >>> ubuntu_team.renewal_policy = TeamMembershipRenewalPolicy.ONDEMAND
     >>> ubuntu_team.defaultrenewalperiod = 365
+    >>> kamion_on_ubuntu_team = membershipset.getByPersonAndTeam(
+    ...     kamion, ubuntu_team)
+    >>> kamion_on_ubuntu_team.setExpirationDate(
+    ...     utc_now + timedelta(days=9), mark)
     >>> flush_database_updates()
     >>> kamion_on_ubuntu_team.sendExpirationWarningEmail()
     >>> transaction.commit()
@@ -577,14 +562,15 @@
     <BLANKLINE>
     ----------------------------------------
 
-If the team's renewal policy is NONE but the member has the necessary rights
-to change the expiration date of his own membership (i.e. by being the team's
-owner), the notification he gets will contain a link to his memberhip page,
-where he can extend it.
+If the team's renewal policy is NONE but the member has the necessary
+rights to change the expiration date of his own membership (i.e. by
+being the team's owner), the notification he gets will contain a link to
+his memberhip page, where he can extend it.
 
     >>> landscape.renewal_policy = TeamMembershipRenewalPolicy.NONE
     >>> landscape.teamowner.preferredemail.email
     u'test@xxxxxxxxxxxxx'
+
     >>> sampleperson_on_landscape = membershipset.getByPersonAndTeam(
     ...     sampleperson, landscape)
     >>> sampleperson_on_landscape.setExpirationDate(
@@ -613,15 +599,16 @@
     ----------------------------------------
 
 
-== Membership expiration notification ==
+Membership expiration notification
+----------------------------------
 
-For teams with a renewal policy other than AUTOMATIC, if a membership is not
-renewed before its expiration date it'll be flagged as expired and a
-notification is sent to the team admins and to the member whose membership
-expired. If the renewal policy is AUTOMATIC, though, the memberships that
-should expire will retain their status and have their dateexpires update. A
-notification is also sent to the member and to team admins when a membership
-is automatically renewed.
+For teams with a renewal policy other than AUTOMATIC, if a membership is
+not renewed before its expiration date it'll be flagged as expired and a
+notification is sent to the team admins and to the member whose
+membership expired. If the renewal policy is AUTOMATIC, though, the
+memberships that should expire will retain their status and have their
+dateexpires update. A notification is also sent to the member and to
+team admins when a membership is automatically renewed.
 
     >>> from zope.security.proxy import removeSecurityProxy
     >>> utc_now = datetime.now(pytz.timezone('UTC'))
@@ -633,6 +620,7 @@
 
     # Need to cheat here and set the expiry date manually because the expiry
     # date given to setExpirationDate() must be in the future.
+
     >>> removeSecurityProxy(mark_on_admins).dateexpires = utc_now
 
     >>> ubuntu_team = personset.getByName('ubuntu-team')
@@ -701,12 +689,13 @@
     ----------------------------------------
 
 
-== Memberships renewed by the members themselves ==
+Memberships renewed by the members themselves
+---------------------------------------------
 
-Another possible renewal policy for teams is ONDEMAND, which means that team
-members are invited to renew their membership once it gets close to their
-expiration date. When a member renew his own membership, a notification is
-sent to all team admins.
+Another possible renewal policy for teams is ONDEMAND, which means that
+team members are invited to renew their membership once it gets close to
+their expiration date. When a member renew his own membership, a
+notification is sent to all team admins.
 
     >>> karl = personset.getByName('karl')
     >>> mirror_admins = personset.getByName('ubuntu-mirror-admins')
@@ -715,6 +704,7 @@
     >>> tomorrow = datetime.now(pytz.timezone('UTC')) + timedelta(days=1)
     >>> print karl_on_mirroradmins.status.title
     Approved
+
     >>> print karl_on_mirroradmins.dateexpires
     None
 
@@ -745,10 +735,12 @@
     The Launchpad team
     ----------------------------------------
 
-== Some special cases ==
-
-When creating a new team, the owner has his membership's status changed from
-approved to admin, but he won't get a notification of that.
+
+Some special cases
+------------------
+
+When creating a new team, the owner has his membership's status changed
+from approved to admin, but he won't get a notification of that.
 
     >>> team = personset.newTeam(mark, 'testteam', 'Test')
     >>> run_mail_jobs()
@@ -758,19 +750,22 @@
     # Other tests expect an empty stub.test_emails, but if this one above
     # fails, I don't want a non-empty stub.test_emails to cause the tests
     # below to fail too.
+
     >>> stub.test_emails = []
 
-If cprov is made an administrator of ubuntu_team, he'll only get one email
-notification.
+If cprov is made an administrator of ubuntu_team, he'll only get one
+email notification.
 
     >>> cprov = personset.getByName('cprov')
-    >>> cprov_membership = membershipset.getByPersonAndTeam(cprov, ubuntu_team)
+    >>> cprov_membership = membershipset.getByPersonAndTeam(
+    ...     cprov, ubuntu_team)
     >>> login('mark@xxxxxxxxxxx')
     >>> setStatus(
     ...     cprov_membership, TeamMembershipStatus.ADMIN, reviewer=mark)
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     6
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: colin.watson@xxxxxxxxxxxxxxx, foo.bar@xxxxxxxxxxxxx,
@@ -791,9 +786,9 @@
     <http://launchpad.dev/~ubuntu-team>
     ----------------------------------------
 
-If a team admin changes his own membership, the notification sent will clearly
-say that the change was performed by the user himself, and it will only be
-sent to the team administrators.
+If a team admin changes his own membership, the notification sent will
+clearly say that the change was performed by the user himself, and it
+will only be sent to the team administrators.
 
     >>> jdub = getUtility(IPersonSet).getByName('jdub')
     >>> jdub_membership = membershipset.getByPersonAndTeam(jdub, ubuntu_team)
@@ -802,6 +797,7 @@
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     5
+
     >>> print_distinct_emails()
     From: Ubuntu Team <noreply@xxxxxxxxxxxxx>
     To: celso.providelo@xxxxxxxxxxxxx, colin.watson@xxxxxxxxxxxxxxx,
@@ -814,11 +810,11 @@
     <http://launchpad.dev/~ubuntu-team>
     ----------------------------------------
 
-Deactivating the membership of a team also generates notifications for the
-team which had the membership deactivated and to the administrators of the
-hosting team. Note that the notification sent to the team whose membership
-was deactivated will not talk about "your membership" as it wouldn't make
-sense to the members of the team reading it.
+Deactivating the membership of a team also generates notifications for
+the team which had the membership deactivated and to the administrators
+of the hosting team. Note that the notification sent to the team whose
+membership was deactivated will not talk about "your membership" as it
+wouldn't make sense to the members of the team reading it.
 
     >>> mirror_admins_membership = membershipset.getByPersonAndTeam(
     ...     mirror_admins, ubuntu_team)
@@ -841,8 +837,8 @@
     <http://launchpad.dev/~ubuntu-team>
     ----------------------------------------
 
-Deactivating memberships can also be done silently (no email notifications
-sent) by Launchpad Administrators.
+Deactivating memberships can also be done silently (no email
+notifications sent) by Launchpad Administrators.
 
     >>> dumper = getUtility(IPersonSet).getByName('dumper')
     >>> hwdb_admins = personset.getByName('hwdb-team')
@@ -850,17 +846,19 @@
     ...     hwdb_admins)
     >>> print dumper_hwdb_membership.status.title
     Approved
+
     >>> login_person(admin_person)
     >>> setStatus(dumper_hwdb_membership, TeamMembershipStatus.DEACTIVATED,
     ...     reviewer=admin_person, silent=True)
     >>> run_mail_jobs()
     >>> len(stub.test_emails)
     0
+
     >>> print dumper_hwdb_membership.status.title
     Deactivated
 
-People who are not Launchpad Administrators, may not change other's membership
-statues silently.
+People who are not Launchpad Administrators, may not change other's
+membership statues silently.
 
     >>> kamion = getUtility(IPersonSet).getByName('kamion')
     >>> stevea = getUtility(IPersonSet).getByName('stevea')
@@ -872,20 +870,24 @@
     ...     stevea, ubuntu_team)
     >>> print kamion_ubuntu_team_membership.status.title
     Administrator
+
     >>> print stevea_ubuntu_team_membership.status.title
     Approved
+
     >>> setStatus(stevea_ubuntu_team_membership,
     ...     TeamMembershipStatus.DEACTIVATED, reviewer=kamion, silent=True)
     Traceback (most recent call last):
     UserCannotChangeMembershipSilently: ...
+
     >>> print stevea_ubuntu_team_membership.status.title
     Approved
 
+
 Joining a team with a mailing list
 ----------------------------------
 
-When a user joins a team with a mailing list, the new member's notification
-email contain subscription information.
+When a user joins a team with a mailing list, the new member's
+notification email contain subscription information.
 
     >>> owner = factory.makePerson(name='team-owner')
     >>> login_person(owner)
@@ -938,3 +940,4 @@
     You received this email because team-two is the new member.
     ----------------------------------------
 
+

=== modified file 'lib/lp/registry/doc/teammembership.txt'
--- lib/lp/registry/doc/teammembership.txt	2011-06-28 15:04:29 +0000
+++ lib/lp/registry/doc/teammembership.txt	2011-12-06 06:50:30 +0000
@@ -539,7 +539,8 @@
     AssertionError: ...
     >>> foobar_on_buildd.setExpirationDate(tomorrow, foobar)
 
-Team owners can also renew any memberships of the team they own.
+Team owners and admins can also renew any memberships of the team they
+own or administer.
 
     >>> landscape = getUtility(IPersonSet).getByName(
     ...     'landscape-developers')
@@ -553,27 +554,16 @@
     True
     >>> sampleperson_on_landscape.setExpirationDate(tomorrow, sampleperson)
 
-In the case of a mere team admin, though, he can only change the expiry
-date of other's memberships, not his own.
-
     >>> cprov_on_buildd = membershipset.getByPersonAndTeam(
     ...     cprov, buildd_admins)
     >>> buildd_admins.teamowner.name
     u'name16'
     >>> print cprov_on_buildd.status.title
     Administrator
-
     >>> foobar_on_buildd.canChangeExpirationDate(cprov)
     True
     >>> foobar_on_buildd.setExpirationDate(tomorrow, cprov)
 
-    >>> cprov_on_buildd.canChangeExpirationDate(cprov)
-    False
-    >>> cprov_on_buildd.setExpirationDate(tomorrow, cprov)
-    Traceback (most recent call last):
-    ...
-    AssertionError: ...
-
 
 Flagging expired memberships
 ----------------------------

=== modified file 'lib/lp/registry/model/teammembership.py'
--- lib/lp/registry/model/teammembership.py	2011-09-01 06:18:57 +0000
+++ lib/lp/registry/model/teammembership.py	2011-12-06 06:50:30 +0000
@@ -57,6 +57,7 @@
 from lp.registry.interfaces.persontransferjob import (
     IMembershipNotificationJobSource,
     )
+from lp.registry.interfaces.role import IPersonRoles
 from lp.registry.interfaces.teammembership import (
     ACTIVE_STATES,
     CyclicalTeamMembershipError,
@@ -199,17 +200,9 @@
 
     def canChangeExpirationDate(self, person):
         """See `ITeamMembership`."""
-        person_is_admin = self.team in person.getAdministratedTeams()
-        if (person.inTeam(self.team.teamowner) or
-                person.inTeam(getUtility(ILaunchpadCelebrities).admin)):
-            # The team owner and Launchpad admins can change the expiration
-            # date of anybody's membership.
-            return True
-        elif person_is_admin and person != self.person:
-            # A team admin can only change other member's expiration date.
-            return True
-        else:
-            return False
+        person_is_team_admin = self.team in person.getAdministratedTeams()
+        person_is_lp_admin = IPersonRoles(person).in_admin
+        return person_is_team_admin or person_is_lp_admin
 
     def setExpirationDate(self, date, user):
         """See `ITeamMembership`."""
@@ -273,12 +266,9 @@
                     % (admin.unique_displayname, canonical_url(admin)))
             else:
                 for admin in admins:
-                    # Do not tell the member to contact himself when he can't
-                    # extend his membership.
-                    if admin != member:
-                        admins_names.append(
-                            "%s <%s>" % (admin.unique_displayname,
-                                         canonical_url(admin)))
+                    admins_names.append(
+                        "%s <%s>" % (admin.unique_displayname,
+                                        canonical_url(admin)))
 
                 how_to_renew = (
                     "To prevent this membership from expiring, you should "

=== modified file 'lib/lp/registry/templates/teammembership-index.pt'
--- lib/lp/registry/templates/teammembership-index.pt	2010-10-10 21:54:16 +0000
+++ lib/lp/registry/templates/teammembership-index.pt	2011-12-06 06:50:30 +0000
@@ -109,18 +109,12 @@
           <tr>
             <th>Administrator:</th>
             <td>
-              <tal:can-change condition="view/allowChangeAdmin">
-                <input tal:attributes="checked view/adminIsSelected"
-                       type="radio" value="yes" name="admin" id="admin"/>
-                <label for="admin">Yes</label>
-                <input tal:attributes="checked view/adminIsNotSelected"
-                       type="radio" value="no" name="admin" id="notadmin"/>
-                <label for="notadmin">No</label>
-              </tal:can-change>
-              <tal:cant-change condition="not: view/allowChangeAdmin">
-                <span tal:condition="view/isAdmin">Yes</span>
-                <span tal:condition="not: view/isAdmin">No</span>
-              </tal:cant-change>
+              <input tal:attributes="checked view/adminIsSelected"
+                      type="radio" value="yes" name="admin" id="admin"/>
+              <label for="admin">Yes</label>
+              <input tal:attributes="checked view/adminIsNotSelected"
+                      type="radio" value="no" name="admin" id="notadmin"/>
+              <label for="notadmin">No</label>
             </td>
           </tr>