1256
1201
"""An XML dump of the locations of limited number of team members."""
1259
class TeamNavigation(PersonNavigation):
1263
@stepthrough('+poll')
1264
def traverse_poll(self, name):
1265
return getUtility(IPollSet).getByTeamAndName(self.context, name)
1267
@stepthrough('+invitation')
1268
def traverse_invitation(self, name):
1269
# Return the found membership regardless of its status as we know
1270
# TeamInvitationView can handle memberships in statuses other than
1272
membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1273
self.context, getUtility(IPersonSet).getByName(name))
1274
if membership is None:
1276
return TeamInvitationView(membership, self.request)
1278
@stepthrough('+member')
1279
def traverse_member(self, name):
1280
person = getUtility(IPersonSet).getByName(name)
1283
return getUtility(ITeamMembershipSet).getByPersonAndTeam(
1284
person, self.context)
1287
class TeamBreadcrumb(Breadcrumb):
1288
"""Builds a breadcrumb for an `ITeam`."""
1292
return smartquote('"%s" team') % self.context.displayname
1295
class TeamMembershipSelfRenewalView(LaunchpadFormView):
1297
implements(IBrowserPublisher)
1299
# This is needed for our breadcrumbs, as there's no <browser:page>
1300
# declaration for this view.
1301
__name__ = '+self-renewal'
1302
schema = ITeamMembership
1304
template = ViewPageTemplateFile(
1305
'../templates/teammembership-self-renewal.pt')
1309
return "Renew membership of %s in %s" % (
1310
self.context.person.displayname, self.context.team.displayname)
1314
def __init__(self, context, request):
1315
# Only the member himself or admins of the member (in case it's a
1316
# team) can see the page in which they renew memberships that are
1318
if not check_permission('launchpad.Edit', context.person):
1320
"You may not renew the membership for %s." %
1321
context.person.displayname)
1322
LaunchpadFormView.__init__(self, context, request)
1324
def browserDefault(self, request):
1328
def reason_for_denied_renewal(self):
1329
"""Return text describing why the membership can't be renewed."""
1330
context = self.context
1331
ondemand = TeamMembershipRenewalPolicy.ONDEMAND
1332
admin = TeamMembershipStatus.ADMIN
1333
approved = TeamMembershipStatus.APPROVED
1334
date_limit = datetime.now(pytz.UTC) - timedelta(
1335
days=DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT)
1336
if context.status not in (admin, approved):
1337
text = "it is not active."
1338
elif context.team.renewal_policy != ondemand:
1339
text = ('<a href="%s">%s</a> is not a team that allows its '
1340
'members to renew their own memberships.'
1341
% (canonical_url(context.team),
1342
context.team.unique_displayname))
1343
elif context.dateexpires is None or context.dateexpires > date_limit:
1344
if context.person.is_team:
1345
link_text = "Somebody else has already renewed it."
1348
"You or one of the team administrators has already "
1350
text = ('it is not set to expire in %d days or less. '
1351
'<a href="%s/+members">%s</a>'
1352
% (DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT,
1353
canonical_url(context.team), link_text))
1355
raise AssertionError('This membership can be renewed!')
1359
def time_before_expiration(self):
1360
return self.context.dateexpires - datetime.now(pytz.timezone('UTC'))
1364
return canonical_url(self.context.person)
1366
cancel_url = next_url
1368
@action(_("Renew"), name="renew")
1369
def renew_action(self, action, data):
1370
member = self.context.person
1371
# This if-statement prevents an exception if the user
1372
# double clicks on the submit button.
1373
if self.context.canBeRenewedByMember():
1374
member.renewTeamMembership(self.context.team)
1375
self.request.response.addInfoNotification(
1376
_("Membership renewed until ${date}.", mapping=dict(
1377
date=self.context.dateexpires.strftime('%Y-%m-%d'))))
1380
class ITeamMembershipInvitationAcknowledgementForm(Interface):
1381
"""Schema for the form in which team admins acknowledge invitations.
1383
We could use ITeamMembership for that, but the acknowledger_comment is
1384
marked readonly there and that means LaunchpadFormView won't include the
1385
value of that in the data given to our action handler.
1388
acknowledger_comment = Text(
1389
title=_("Comment"), required=False, readonly=False)
1392
class TeamInvitationView(LaunchpadFormView):
1393
"""Where team admins can accept/decline membership invitations."""
1395
implements(IBrowserPublisher)
1397
# This is needed for our breadcrumbs, as there's no <browser:page>
1398
# declaration for this view.
1399
__name__ = '+invitation'
1400
schema = ITeamMembershipInvitationAcknowledgementForm
1401
field_names = ['acknowledger_comment']
1402
custom_widget('acknowledger_comment', TextAreaWidget, height=5, width=60)
1403
template = ViewPageTemplateFile(
1404
'../templates/teammembership-invitation.pt')
1406
def __init__(self, context, request):
1407
# Only admins of the invited team can see the page in which they
1408
# approve/decline invitations.
1409
if not check_permission('launchpad.Edit', context.person):
1411
"Only team administrators can approve/decline invitations "
1412
"sent to this team.")
1413
LaunchpadFormView.__init__(self, context, request)
1417
"""See `LaunchpadFormView`."""
1418
return "Make %s a member of %s" % (
1419
self.context.person.displayname, self.context.team.displayname)
1422
def page_title(self):
1424
'"%s" team invitation') % self.context.team.displayname
1426
def browserDefault(self, request):
1431
return canonical_url(self.context.person)
1433
@action(_("Accept"), name="accept")
1434
def accept_action(self, action, data):
1435
if self.context.status != TeamMembershipStatus.INVITED:
1436
self.request.response.addInfoNotification(
1437
_("This invitation has already been processed."))
1439
member = self.context.person
1441
member.acceptInvitationToBeMemberOf(
1442
self.context.team, data['acknowledger_comment'])
1443
except CyclicalTeamMembershipError:
1444
self.request.response.addInfoNotification(
1445
_("This team may not be added to ${that_team} because it is "
1446
"a member of ${this_team}.",
1448
that_team=self.context.team.displayname,
1449
this_team=member.displayname)))
1451
self.request.response.addInfoNotification(
1452
_("This team is now a member of ${team}.", mapping=dict(
1453
team=self.context.team.displayname)))
1455
@action(_("Decline"), name="decline")
1456
def decline_action(self, action, data):
1457
if self.context.status != TeamMembershipStatus.INVITED:
1458
self.request.response.addInfoNotification(
1459
_("This invitation has already been processed."))
1461
member = self.context.person
1462
member.declineInvitationToBeMemberOf(
1463
self.context.team, data['acknowledger_comment'])
1464
self.request.response.addInfoNotification(
1465
_("Declined the invitation to join ${team}", mapping=dict(
1466
team=self.context.team.displayname)))
1468
@action(_("Cancel"), name="cancel")
1469
def cancel_action(self, action, data):
1470
# Simply redirect back.
1474
class TeamMenuMixin(PPANavigationMenuMixIn, CommonMenuLinks):
1475
"""Base class of team menus.
1477
You will need to override the team attribute if your menu subclass
1478
has the view as its context object.
1484
return Link(target, text)
1486
@enabled_with_permission('launchpad.Edit')
1489
text = 'Change details'
1490
return Link(target, text, icon='edit')
1492
@enabled_with_permission('launchpad.Edit')
1494
target = '+branding'
1495
text = 'Change branding'
1496
return Link(target, text, icon='edit')
1498
@enabled_with_permission('launchpad.Owner')
1500
target = '+reassign'
1501
text = 'Change owner'
1502
summary = 'Change the owner of the team'
1503
return Link(target, text, summary, icon='edit')
1505
@enabled_with_permission('launchpad.Moderate')
1509
summary = 'Delete this team'
1510
return Link(target, text, summary, icon='trash-icon')
1512
@enabled_with_permission('launchpad.View')
1515
text = 'Show all members'
1516
return Link(target, text, icon='team')
1518
@enabled_with_permission('launchpad.Edit')
1519
def received_invitations(self):
1520
target = '+invitations'
1521
text = 'Show received invitations'
1522
return Link(target, text, icon='info')
1524
@enabled_with_permission('launchpad.Edit')
1525
def add_member(self):
1526
target = '+addmember'
1528
return Link(target, text, icon='add')
1530
@enabled_with_permission('launchpad.Edit')
1531
def proposed_members(self):
1532
target = '+editproposedmembers'
1533
text = 'Approve or decline members'
1534
return Link(target, text, icon='add')
1538
text = 'View map and time zones'
1539
return Link(target, text, icon='meeting')
1541
def add_my_teams(self):
1542
target = '+add-my-teams'
1543
text = 'Add one of my teams'
1545
restricted = TeamSubscriptionPolicy.RESTRICTED
1546
if self.person.subscriptionpolicy == restricted:
1547
# This is a restricted team; users can't join.
1549
return Link(target, text, icon='add', enabled=enabled)
1551
def memberships(self):
1552
target = '+participation'
1553
text = 'Show team participation'
1554
return Link(target, text, icon='info')
1556
@enabled_with_permission('launchpad.View')
1558
target = '+mugshots'
1559
text = 'Show member photos'
1560
return Link(target, text, icon='team')
1565
return Link(target, text, icon='info')
1567
@enabled_with_permission('launchpad.Edit')
1570
text = 'Create a poll'
1571
return Link(target, text, icon='add')
1573
@enabled_with_permission('launchpad.Edit')
1574
def editemail(self):
1575
target = '+contactaddress'
1576
text = 'Set contact address'
1578
'The address Launchpad uses to contact %s' %
1579
self.person.displayname)
1580
return Link(target, text, summary, icon='edit')
1582
@enabled_with_permission('launchpad.Moderate')
1583
def configure_mailing_list(self):
1584
target = '+mailinglist'
1585
mailing_list = self.person.mailing_list
1586
if mailing_list is not None:
1587
text = 'Configure mailing list'
1590
text = 'Create a mailing list'
1593
'The mailing list associated with %s' % self.context.displayname)
1594
return Link(target, text, summary, icon=icon)
1596
@enabled_with_active_mailing_list
1597
@enabled_with_permission('launchpad.Edit')
1598
def moderate_mailing_list(self):
1599
target = '+mailinglist-moderate'
1600
text = 'Moderate mailing list'
1602
'The mailing list associated with %s' % self.context.displayname)
1603
return Link(target, text, summary, icon='edit')
1605
@enabled_with_permission('launchpad.Edit')
1606
def editlanguages(self):
1607
target = '+editlanguages'
1608
text = 'Set preferred languages'
1609
return Link(target, text, icon='edit')
1613
if not userIsActiveTeamMember(self.person):
1615
if self.person.teamowner == self.user:
1616
# The owner cannot leave his team.
1619
text = 'Leave the Team'
1621
return Link(target, text, icon=icon, enabled=enabled)
1625
person = self.person
1626
if userIsActiveTeamMember(person):
1628
elif (self.person.subscriptionpolicy ==
1629
TeamSubscriptionPolicy.RESTRICTED):
1630
# This is a restricted team; users can't join.
1633
text = 'Join the team'
1635
return Link(target, text, icon=icon, enabled=enabled)
1638
class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
1645
'common_edithomepage',
1651
'received_invitations',
1653
'configure_mailing_list',
1654
'moderate_mailing_list',
1667
'related_software_summary',
1670
'structural_subscriptions',
1674
class TeamOverviewNavigationMenu(NavigationMenu, TeamMenuMixin):
1675
"""A top-level menu for navigation within a Team."""
1679
links = ['profile', 'polls', 'members', 'ppas']
1682
class TeamMembershipView(LaunchpadView):
1683
"""The view behind ITeam/+members."""
1687
return smartquote('Members of "%s"' % self.context.displayname)
1690
def active_memberships(self):
1691
"""Current members of the team."""
1692
return ActiveBatchNavigator(
1693
self.context.member_memberships, self.request)
1696
def inactive_memberships(self):
1697
"""Former members of the team."""
1698
return InactiveBatchNavigator(
1699
self.context.getInactiveMemberships(), self.request)
1702
def invited_memberships(self):
1703
"""Other teams invited to become members of this team."""
1704
return list(self.context.getInvitedMemberships())
1707
def proposed_memberships(self):
1708
"""Users who have requested to join this team."""
1709
return list(self.context.getProposedMemberships())
1712
def have_pending_members(self):
1713
return self.proposed_memberships or self.invited_memberships
1716
class TeamIndexView(PersonIndexView, TeamJoinMixin):
1717
"""The view class for the +index page.
1719
This class is needed, so an action menu that only applies to
1720
teams can be displayed without showing up on the person index page.
1724
def super_teams(self):
1725
"""Return only the super teams that the viewer is able to see."""
1727
team for team in self.context.super_teams
1728
if check_permission('launchpad.View', team)]
1731
def can_show_subteam_portlet(self):
1732
"""Only show the subteam portlet if there is info to display.
1734
Either the team is a member of another team, or there are
1735
invitations to join a team, and the owner needs to see the
1736
link so that the invitation can be accepted.
1739
return (len(self.super_teams) > 0
1740
or (self.context.open_membership_invitations
1741
and check_permission('launchpad.Edit', self.context)))
1742
except AttributeError, e:
1743
raise AssertionError(e)
1746
def visibility_info(self):
1747
if self.context.visibility == PersonVisibility.PRIVATE:
1748
return 'Private team'
1750
return 'Public team'
1753
def visibility_portlet_class(self):
1754
"""The portlet class for team visibility."""
1755
if self.context.visibility == PersonVisibility.PUBLIC:
1757
return 'portlet private'
1760
def add_member_step_title(self):
1761
"""A string for setup_add_member_handler with escaped quotes."""
1762
vocabulary_registry = getVocabularyRegistry()
1763
vocabulary = vocabulary_registry.get(self.context, 'ValidTeamMember')
1764
return vocabulary.step_title.replace("'", "\\'").replace('"', '\\"')
1767
class TeamJoinForm(Interface):
1768
"""Schema for team join."""
1769
mailinglist_subscribe = Bool(
1770
title=_("Subscribe me to this team's mailing list"),
1771
required=True, default=True)
1774
class TeamJoinView(LaunchpadFormView, TeamJoinMixin):
1775
"""A view class for joining a team."""
1776
schema = TeamJoinForm
1780
return 'Join ' + cgi.escape(self.context.displayname)
1784
def setUpWidgets(self):
1785
super(TeamJoinView, self).setUpWidgets()
1786
if 'mailinglist_subscribe' in self.field_names:
1787
widget = self.widgets['mailinglist_subscribe']
1788
widget.setRenderedValue(self.user_wants_list_subscriptions)
1791
def field_names(self):
1792
"""See `LaunchpadFormView`.
1794
If the user can subscribe to the mailing list then include the
1795
mailinglist subscription checkbox otherwise remove it.
1797
if self.user_can_subscribe_to_list:
1798
return ['mailinglist_subscribe']
1803
def join_allowed(self):
1804
"""Is the logged in user allowed to join this team?
1806
The answer is yes if this team's subscription policy is not RESTRICTED
1807
and this team's visibility is either None or PUBLIC.
1809
# Joining a moderated team will put you on the proposed_members
1810
# list. If it is a private team, you are not allowed to view the
1811
# proposed_members attribute until you are an active member;
1812
# therefore, it would look like the join button is broken. Either
1813
# private teams should always have a restricted subscription policy,
1814
# or we need a more complicated permission model.
1815
if not (self.context.visibility is None
1816
or self.context.visibility == PersonVisibility.PUBLIC):
1819
restricted = TeamSubscriptionPolicy.RESTRICTED
1820
return self.context.subscriptionpolicy != restricted
1823
def user_can_request_to_join(self):
1824
"""Can the logged in user request to join this team?
1826
The user can request if he's allowed to join this team and if he's
1827
not yet an active member of this team.
1829
if not self.join_allowed:
1831
return not (self.user_is_active_member or
1832
self.user_is_proposed_member)
1835
def user_wants_list_subscriptions(self):
1836
"""Is the user interested in subscribing to mailing lists?"""
1837
return (self.user.mailing_list_auto_subscribe_policy !=
1838
MailingListAutoSubscribePolicy.NEVER)
1841
def team_is_moderated(self):
1842
"""Is this team a moderated team?
1844
Return True if the team's subscription policy is MODERATED.
1846
policy = self.context.subscriptionpolicy
1847
return policy == TeamSubscriptionPolicy.MODERATED
1851
return canonical_url(self.context)
1854
def cancel_url(self):
1855
return canonical_url(self.context)
1857
@action(_("Join"), name="join")
1858
def action_save(self, action, data):
1859
response = self.request.response
1861
if self.user_can_request_to_join:
1862
# Shut off mailing list auto-subscription - we want direct
1864
self.user.join(self.context, may_subscribe_to_list=False)
1866
if self.team_is_moderated:
1867
response.addInfoNotification(
1868
_('Your request to join ${team} is awaiting '
1870
mapping={'team': self.context.displayname}))
1872
response.addInfoNotification(
1873
_('You have successfully joined ${team}.',
1874
mapping={'team': self.context.displayname}))
1875
if data.get('mailinglist_subscribe', False):
1876
self._subscribeToList(response)
1879
response.addErrorNotification(
1880
_('You cannot join ${team}.',
1881
mapping={'team': self.context.displayname}))
1883
def _subscribeToList(self, response):
1884
"""Subscribe the user to the team's mailing list."""
1886
if self.user_can_subscribe_to_list:
1887
# 'user_can_subscribe_to_list' should have dealt with
1888
# all of the error cases.
1889
self.context.mailing_list.subscribe(self.user)
1891
if self.team_is_moderated:
1892
response.addInfoNotification(
1893
_('Your mailing list subscription is '
1894
'awaiting approval.'))
1896
response.addInfoNotification(
1898
_("You have been subscribed to this "
1899
"team’s mailing list.")))
1901
# A catch-all case, perhaps from stale or mangled
1903
response.addErrorNotification(
1904
_('Mailing list subscription failed.'))
1907
class TeamAddMyTeamsView(LaunchpadFormView):
1908
"""Propose/add to this team any team that you're an administrator of."""
1910
page_title = 'Propose/add one of your teams to another one'
1911
custom_widget('teams', LabeledMultiCheckBoxWidget)
1913
def initialize(self):
1914
context = self.context
1915
if context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
1916
self.label = 'Propose these teams as members'
1918
self.label = 'Add these teams to %s' % context.displayname
1919
self.next_url = canonical_url(context)
1920
super(TeamAddMyTeamsView, self).initialize()
1922
def setUpFields(self):
1924
for team in self.candidate_teams:
1926
'<a href="%s">%s</a>', canonical_url(team), team.displayname)
1927
terms.append(SimpleTerm(team, team.name, text))
1928
self.form_fields = FormFields(
1929
List(__name__='teams',
1931
value_type=Choice(vocabulary=SimpleVocabulary(terms)),
1933
render_context=self.render_context)
1935
def setUpWidgets(self, context=None):
1936
super(TeamAddMyTeamsView, self).setUpWidgets(context)
1937
self.widgets['teams'].display_label = False
1940
def candidate_teams(self):
1941
"""Return the set of teams that can be added/proposed for the context.
1943
We return only teams that the user can administer, that aren't already
1944
a member in the context or that the context isn't a member of. (Of
1945
course, the context is also omitted.)
1948
for team in self.user.getAdministratedTeams():
1949
if team == self.context:
1951
elif team.visibility != PersonVisibility.PUBLIC:
1953
elif team in self.context.activemembers:
1954
# The team is already a member of the context object.
1956
elif self.context.hasParticipationEntryFor(team):
1957
# The context object is a member/submember of the team.
1959
candidates.append(team)
1963
def cancel_url(self):
1964
"""The return URL."""
1965
return canonical_url(self.context)
1967
def validate(self, data):
1968
if len(data.get('teams', [])) == 0:
1969
self.setFieldError('teams',
1970
'Please select the team(s) you want to be '
1971
'member(s) of this team.')
1973
def hasCandidates(self, action):
1974
"""Return whether the user has teams to propose."""
1975
return len(self.candidate_teams) > 0
1977
@action(_("Continue"), name="continue", condition=hasCandidates)
1978
def continue_action(self, action, data):
1979
"""Make the selected teams join this team."""
1980
context = self.context
1981
is_admin = check_permission('launchpad.Admin', context)
1982
membership_set = getUtility(ITeamMembershipSet)
1983
proposed_team_names = []
1984
added_team_names = []
1985
accepted_invite_team_names = []
1986
membership_set = getUtility(ITeamMembershipSet)
1987
for team in data['teams']:
1988
membership = membership_set.getByPersonAndTeam(team, context)
1989
if (membership is not None
1990
and membership.status == TeamMembershipStatus.INVITED):
1991
team.acceptInvitationToBeMemberOf(
1993
'Accepted an already pending invitation while trying to '
1994
'propose the team for membership.')
1995
accepted_invite_team_names.append(team.displayname)
1997
context.addMember(team, reviewer=self.user)
1998
added_team_names.append(team.displayname)
2000
team.join(context, requester=self.user)
2001
membership = membership_set.getByPersonAndTeam(team, context)
2002
if membership.status == TeamMembershipStatus.PROPOSED:
2003
proposed_team_names.append(team.displayname)
2004
elif membership.status == TeamMembershipStatus.APPROVED:
2005
added_team_names.append(team.displayname)
2007
raise AssertionError(
2008
'Unexpected membership status (%s) for %s.'
2009
% (membership.status.name, team.name))
2011
for team_names, message in (
2012
(proposed_team_names, 'proposed to this team.'),
2013
(added_team_names, 'added to this team.'),
2014
(accepted_invite_team_names,
2015
'added to this team because of an existing invite.'),
2017
if len(team_names) == 0:
2019
elif len(team_names) == 1:
2021
team_string = team_names[0]
2022
elif len(team_names) > 1:
2025
', '.join(team_names[:-1]) + ' and ' + team_names[-1])
2026
full_message += '%s %s %s' % (team_string, verb, message)
2027
self.request.response.addInfoNotification(full_message)
2030
class TeamLeaveView(LaunchpadFormView, TeamJoinMixin):
2035
return 'Leave ' + cgi.escape(self.context.displayname)
2040
def cancel_url(self):
2041
return canonical_url(self.context)
2043
next_url = cancel_url
2045
@action(_("Leave"), name="leave")
2046
def action_save(self, action, data):
2047
if self.user_can_request_to_leave:
2048
self.user.leave(self.context)
2051
class TeamReassignmentView(ObjectReassignmentView):
2053
ownerOrMaintainerAttr = 'teamowner'
2054
schema = ITeamReassignment
2056
def __init__(self, context, request):
2057
super(TeamReassignmentView, self).__init__(context, request)
2058
self.callback = self._afterOwnerChange
2059
self.teamdisplayname = self.contextName
2060
self._next_url = canonical_url(self.context)
2062
def validateOwner(self, new_owner):
2063
"""Display error if the owner is not valid.
2065
Called by ObjectReassignmentView.validate().
2067
if self.context.inTeam(new_owner):
2068
path = self.context.findPathToTeam(new_owner)
2070
relationship = 'a direct member'
2073
relationship = 'an indirect member'
2074
full_path = [self.context] + path
2075
path_string = '(%s)' % '⇒'.join(
2076
team.displayname for team in full_path)
2078
'Circular team memberships are not allowed. '
2079
'%(new)s cannot be the new team owner, since %(context)s '
2080
'is %(relationship)s of %(new)s. '
2081
'<span style="white-space: nowrap">%(path)s</span>'
2082
% dict(new=new_owner.displayname,
2083
context=self.context.displayname,
2084
relationship=relationship,
2086
self.setFieldError(self.ownerOrMaintainerName, error)
2089
def contextName(self):
2090
return self.context.displayname
2094
return self._next_url
2096
def _afterOwnerChange(self, team, oldOwner, newOwner):
2097
"""Add the new and the old owners as administrators of the team.
2099
When a user creates a new team, he is added as an administrator of
2100
that team. To be consistent with this, we must make the new owner an
2101
administrator of the team. This rule is ignored only if the new owner
2102
is an inactive member of the team, as that means he's not interested
2103
in being a member. The same applies to the old owner.
2105
# Both new and old owners won't be added as administrators of the team
2106
# only if they're inactive members. If they're either active or
2107
# proposed members they'll be made administrators of the team.
2108
if newOwner not in team.inactivemembers:
2110
newOwner, reviewer=oldOwner,
2111
status=TeamMembershipStatus.ADMIN, force_team_add=True)
2112
if oldOwner not in team.inactivemembers:
2114
oldOwner, reviewer=oldOwner,
2115
status=TeamMembershipStatus.ADMIN, force_team_add=True)
2117
# If the current logged in user cannot see the team anymore as a
2118
# result of the ownership change, we don't want them to get a nasty
2119
# error page. So we redirect to launchpad.net with a notification.
2121
if not check_permission('launchpad.LimitedView', team):
2122
self.request.response.addNotification(
2123
"The owner of team %s was successfully changed but you are "
2124
"now no longer authorised to view the team."
2125
% self.teamdisplayname)
2126
self._next_url = canonical_url(self.user)
2129
class ITeamIndexMenu(Interface):
2130
"""A marker interface for the +index navigation menu."""
2133
class ITeamEditMenu(Interface):
2134
"""A marker interface for the edit navigation menu."""
2137
class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):
2141
"""Override CommonMenuLinks since the view is the context."""
2142
return self.context.context
2145
class TeamIndexMenu(TeamNavigationMenuBase):
2146
"""A menu for different aspects of editing a team."""
2148
usedfor = ITeamIndexMenu
2150
title = 'Change team'
2151
links = ('edit', 'delete', 'join', 'add_my_teams', 'leave')
2154
class TeamEditMenu(TeamNavigationMenuBase):
2155
"""A menu for different aspects of editing a team."""
2157
usedfor = ITeamEditMenu
2159
title = 'Change team'
2160
links = ('branding', 'common_edithomepage', 'editlanguages', 'reassign',
2164
class TeamMugshotView(LaunchpadView):
2165
"""A view for the team mugshot (team photo) page"""
2167
label = "Member photos"
2168
batch_size = config.launchpad.mugshot_batch_size
2170
def initialize(self):
2171
"""Cache images to avoid dying from a million cuts."""
2172
getUtility(IPersonSet).cacheBrandingForPeople(
2173
self.members.currentBatch())
2177
"""Get a batch of all members in the team."""
2178
batch_nav = BatchNavigator(
2179
self.context.allmembers, self.request, size=self.batch_size)
2183
classImplements(TeamIndexView, ITeamIndexMenu)
2184
classImplements(TeamEditView, ITeamEditMenu)
1204
class TeamHierarchyView(LaunchpadView):
1205
"""View for ~team/+teamhierarchy page."""
1209
return 'Team relationships for ' + self.context.displayname
1212
def has_sub_teams(self):
1213
return self.context.sub_teams.count() > 0
1216
def has_super_teams(self):
1217
return self.context.super_teams.count() > 0
1220
def has_only_super_teams(self):
1221
return self.has_super_teams and not self.has_sub_teams
1224
def has_only_sub_teams(self):
1225
return not self.has_super_teams and self.has_sub_teams
1228
def has_relationships(self):
1229
return self.has_sub_teams or self.has_super_teams