~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/registry/browser/team.py

  • Committer: Curtis Hovey
  • Date: 2011-08-21 14:21:06 UTC
  • mto: This revision was merged to the branch mainline in revision 13745.
  • Revision ID: curtis.hovey@canonical.com-20110821142106-x93hajd6iguma8gx
Update test that was enforcing bad grammar.

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
__all__ = [
6
6
    'HasRenewalPolicyMixin',
7
7
    'ProposedTeamMembersEditView',
8
 
    'TeamAddMyTeamsView',
9
8
    'TeamAddView',
10
9
    'TeamBadges',
11
10
    'TeamBrandingView',
12
 
    'TeamBreadcrumb',
13
11
    'TeamContactAddressView',
14
 
    'TeamEditMenu',
15
12
    'TeamEditView',
16
 
    'TeamIndexMenu',
17
 
    'TeamJoinView',
18
 
    'TeamLeaveView',
 
13
    'TeamHierarchyView',
19
14
    'TeamMailingListConfigurationView',
20
15
    'TeamMailingListModerationView',
21
16
    'TeamMailingListSubscribersView',
24
19
    'TeamMapView',
25
20
    'TeamMapLtdView',
26
21
    'TeamMemberAddView',
27
 
    'TeamMembershipView',
28
 
    'TeamMugshotView',
29
 
    'TeamNavigation',
30
 
    'TeamOverviewMenu',
31
 
    'TeamOverviewNavigationMenu',
32
22
    'TeamPrivacyAdapter',
33
 
    'TeamReassignmentView',
34
23
    ]
35
24
 
36
25
 
37
 
import cgi
38
 
from datetime import (
39
 
    datetime,
40
 
    timedelta,
41
 
    )
 
26
from datetime import datetime
42
27
import math
43
28
from urllib import unquote
44
29
 
45
 
from lazr.restful.utils import smartquote
46
30
import pytz
47
 
from z3c.ptcompat import ViewPageTemplateFile
48
31
from zope.app.form.browser import TextAreaWidget
49
32
from zope.component import getUtility
50
33
from zope.formlib import form
51
 
from zope.formlib.form import FormFields
52
34
from zope.interface import (
53
 
    classImplements,
54
35
    implements,
55
36
    Interface,
56
37
    )
57
 
from zope.publisher.interfaces.browser import IBrowserPublisher
58
 
from zope.schema import (
59
 
    Bool,
60
 
    Choice,
61
 
    List,
62
 
    Text,
63
 
    )
 
38
from zope.schema import Choice
64
39
from zope.schema.vocabulary import (
65
 
    getVocabularyRegistry,
66
40
    SimpleTerm,
67
41
    SimpleVocabulary,
68
42
    )
69
 
from zope.security.interfaces import Unauthorized
70
43
 
71
 
from canonical.config import config
72
44
from canonical.launchpad import _
73
 
from lp.services.verification.interfaces.authtoken import LoginTokenType
74
 
from lp.services.verification.interfaces.logintoken import ILoginTokenSet
 
45
from canonical.launchpad.interfaces.authtoken import LoginTokenType
 
46
from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
 
47
from canonical.launchpad.interfaces.logintoken import ILoginTokenSet
 
48
from canonical.launchpad.interfaces.validation import validate_new_team_email
75
49
from canonical.launchpad.webapp import (
76
 
    ApplicationMenu,
77
50
    canonical_url,
78
 
    enabled_with_permission,
79
51
    LaunchpadView,
80
 
    Link,
81
 
    NavigationMenu,
82
 
    stepthrough,
83
 
    )
84
 
from canonical.launchpad.webapp.authorization import (
85
 
    check_permission,
86
 
    clear_cache,
87
 
    )
88
 
from canonical.launchpad.webapp.batching import (
89
 
    ActiveBatchNavigator,
90
 
    BatchNavigator,
91
 
    InactiveBatchNavigator,
92
 
    )
93
 
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 
52
    )
 
53
from canonical.launchpad.webapp.authorization import check_permission
 
54
from canonical.launchpad.webapp.badge import HasBadgeBase
 
55
from canonical.launchpad.webapp.batching import BatchNavigator
94
56
from canonical.launchpad.webapp.interfaces import ILaunchBag
95
57
from canonical.launchpad.webapp.menu import structured
96
58
from canonical.lazr.interfaces import IObjectPrivacy
97
 
from lp.app.browser.badge import HasBadgeBase
98
59
from lp.app.browser.launchpadform import (
99
60
    action,
100
61
    custom_widget,
 
62
    LaunchpadEditFormView,
101
63
    LaunchpadFormView,
102
64
    )
103
65
from lp.app.browser.tales import PersonFormatterAPI
104
66
from lp.app.errors import UnexpectedFormData
105
67
from lp.app.validators import LaunchpadValidationError
106
 
from lp.app.validators.validation import validate_new_team_email
107
68
from lp.app.widgets.itemswidgets import (
108
 
    LabeledMultiCheckBoxWidget,
109
69
    LaunchpadRadioWidget,
110
70
    LaunchpadRadioWidgetWithDescription,
111
71
    )
112
72
from lp.app.widgets.owner import HiddenUserWidget
113
73
from lp.app.widgets.popup import PersonPickerWidget
114
 
from lp.code.browser.sourcepackagerecipelisting import HasRecipesMenuMixin
115
74
from lp.registry.browser.branding import BrandingChangeView
116
 
from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
117
 
from lp.registry.browser.objectreassignment import ObjectReassignmentView
118
 
from lp.registry.browser.person import (
119
 
    CommonMenuLinks,
120
 
    PersonIndexView,
121
 
    PersonNavigation,
122
 
    PersonRenameFormMixin,
123
 
    PPANavigationMenuMixIn,
124
 
    )
125
 
from lp.registry.browser.teamjoin import (
126
 
    TeamJoinMixin,
127
 
    userIsActiveTeamMember,
128
 
    )
129
 
from lp.registry.errors import TeamSubscriptionPolicyError
130
75
from lp.registry.interfaces.mailinglist import (
131
76
    IMailingList,
132
77
    IMailingListSet,
134
79
    PostedMessageStatus,
135
80
    PURGE_STATES,
136
81
    )
137
 
from lp.registry.interfaces.mailinglistsubscription import (
138
 
    MailingListAutoSubscribePolicy,
139
 
    )
140
82
from lp.registry.interfaces.person import (
141
 
    CLOSED_TEAM_POLICY,
142
83
    ImmutableVisibilityError,
143
84
    IPersonSet,
144
85
    ITeam,
145
86
    ITeamContactAddressForm,
146
87
    ITeamCreation,
147
 
    ITeamReassignment,
148
 
    OPEN_TEAM_POLICY,
149
88
    PersonVisibility,
150
89
    PRIVATE_TEAM_PREFIX,
151
90
    TeamContactMethod,
152
 
    TeamMembershipRenewalPolicy,
153
91
    TeamSubscriptionPolicy,
154
92
    )
155
 
from lp.registry.interfaces.poll import IPollSet
156
93
from lp.registry.interfaces.teammembership import (
157
94
    CyclicalTeamMembershipError,
158
 
    DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT,
159
 
    ITeamMembership,
160
 
    ITeamMembershipSet,
161
95
    TeamMembershipStatus,
162
96
    )
163
97
from lp.services.fields import PublicPersonChoice
164
 
from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
165
98
from lp.services.propertycache import cachedproperty
166
99
 
167
100
 
265
198
            self.form_fields = self.form_fields.omit('visibility')
266
199
 
267
200
 
268
 
class TeamEditView(TeamFormMixin, PersonRenameFormMixin,
269
 
                   HasRenewalPolicyMixin):
 
201
class TeamEditView(TeamFormMixin, HasRenewalPolicyMixin,
 
202
                   LaunchpadEditFormView):
270
203
    """View for editing team details."""
271
204
    schema = ITeam
272
205
 
297
230
        super(TeamEditView, self).setUpFields()
298
231
        self.conditionallyOmitVisibility()
299
232
 
300
 
    def setUpWidgets(self):
301
 
        super(TeamEditView, self).setUpWidgets()
302
 
        team = self.context
303
 
        # Do we need to only show open subscription policy choices?
304
 
        try:
305
 
            team.checkClosedSubscriptionPolicyAllowed()
306
 
        except TeamSubscriptionPolicyError:
307
 
            # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but
308
 
            # it doesn't so we need to be a bit more verbose.
309
 
            self.widgets['subscriptionpolicy'].vocabulary = (
310
 
                SimpleVocabulary([SimpleVocabulary.createTerm(
311
 
                    policy, policy.name, policy.title)
312
 
                    for policy in OPEN_TEAM_POLICY])
313
 
                )
314
 
        # Do we need to only show closed subscription policy choices?
315
 
        try:
316
 
            team.checkOpenSubscriptionPolicyAllowed()
317
 
        except TeamSubscriptionPolicyError:
318
 
            # Ideally SimpleVocabulary.fromItems() would accept 3-tuples but
319
 
            # it doesn't so we need to be a bit more verbose.
320
 
            self.widgets['subscriptionpolicy'].vocabulary = (
321
 
                SimpleVocabulary([SimpleVocabulary.createTerm(
322
 
                    policy, policy.name, policy.title)
323
 
                    for policy in CLOSED_TEAM_POLICY])
324
 
                )
325
 
 
326
233
    @action('Save', name='save')
327
234
    def action_save(self, action, data):
328
235
        try:
342
249
 
343
250
    cancel_url = next_url
344
251
 
 
252
    def setUpWidgets(self):
 
253
        """See `LaunchpadViewForm`.
 
254
 
 
255
        When a team has a mailing list, a PPA or is private, renames
 
256
        are prohibited.
 
257
        """
 
258
        mailing_list = getUtility(IMailingListSet).get(self.context.name)
 
259
        has_mailing_list = (
 
260
            mailing_list is not None and
 
261
            mailing_list.status != MailingListStatus.PURGED)
 
262
        is_private = self.context.visibility == PersonVisibility.PRIVATE
 
263
        has_ppa = self.context.archive is not None
 
264
 
 
265
        block_renaming = (has_mailing_list or is_private or has_ppa)
 
266
        if block_renaming:
 
267
            # This makes the field's widget display (i.e. read) only.
 
268
            self.form_fields['name'].for_display = True
 
269
 
 
270
        super(TeamEditView, self).setUpWidgets()
 
271
 
 
272
        # Tweak the field form-help including an explanation for the
 
273
        # read-only mode if necessary.
 
274
        if block_renaming:
 
275
            # Group the read-only mode reasons in textual form.
 
276
            # Private teams can't be associated with mailing lists
 
277
            # or PPAs yet, so it's a dominant condition.
 
278
            if is_private:
 
279
                reason = 'is private'
 
280
            else:
 
281
                if not has_mailing_list:
 
282
                    reason = 'has a PPA'
 
283
                elif not has_ppa:
 
284
                    reason = 'has a mailing list'
 
285
                else:
 
286
                    reason = 'has a mailing list and a PPA'
 
287
            self.widgets['name'].hint = _(
 
288
                'This team cannot be renamed because it %s.' % reason)
 
289
 
345
290
 
346
291
def generateTokenAndValidationEmail(email, team):
347
292
    """Send a validation message to the given email."""
604
549
        already been approved or declined. This can only happen
605
550
        through bypassing the UI.
606
551
        """
607
 
        getUtility(IMailingListSet).get(self.context.name)
 
552
        mailing_list = getUtility(IMailingListSet).get(self.context.name)
608
553
        if self.getListInState(MailingListStatus.REGISTERED) is None:
609
554
            self.addError("This application can't be cancelled.")
610
555
 
1040
985
            failed_names = [person.displayname for person in failed_joins]
1041
986
            failed_list = ", ".join(failed_names)
1042
987
 
1043
 
            mapping = dict(this_team=target_team.displayname,
 
988
            mapping = dict( this_team=target_team.displayname,
1044
989
                failed_list=failed_list)
1045
990
 
1046
991
            if len(failed_joins) == 1:
1115
1060
        newmember = data.get('newmember')
1116
1061
        error = None
1117
1062
        if newmember is not None:
1118
 
            if newmember.is_team and not newmember.activemembers:
 
1063
            if newmember.isTeam() and not newmember.activemembers:
1119
1064
                error = _("You can't add a team that doesn't have any active"
1120
1065
                          " members.")
1121
1066
            elif newmember in self.context.activemembers:
1256
1201
    """An XML dump of the locations of limited number of team members."""
1257
1202
 
1258
1203
 
1259
 
class TeamNavigation(PersonNavigation):
1260
 
 
1261
 
    usedfor = ITeam
1262
 
 
1263
 
    @stepthrough('+poll')
1264
 
    def traverse_poll(self, name):
1265
 
        return getUtility(IPollSet).getByTeamAndName(self.context, name)
1266
 
 
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
1271
 
        # INVITED.
1272
 
        membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
1273
 
            self.context, getUtility(IPersonSet).getByName(name))
1274
 
        if membership is None:
1275
 
            return None
1276
 
        return TeamInvitationView(membership, self.request)
1277
 
 
1278
 
    @stepthrough('+member')
1279
 
    def traverse_member(self, name):
1280
 
        person = getUtility(IPersonSet).getByName(name)
1281
 
        if person is None:
1282
 
            return None
1283
 
        return getUtility(ITeamMembershipSet).getByPersonAndTeam(
1284
 
            person, self.context)
1285
 
 
1286
 
 
1287
 
class TeamBreadcrumb(Breadcrumb):
1288
 
    """Builds a breadcrumb for an `ITeam`."""
1289
 
 
1290
 
    @property
1291
 
    def text(self):
1292
 
        return smartquote('"%s" team') % self.context.displayname
1293
 
 
1294
 
 
1295
 
class TeamMembershipSelfRenewalView(LaunchpadFormView):
1296
 
 
1297
 
    implements(IBrowserPublisher)
1298
 
 
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
1303
 
    field_names = []
1304
 
    template = ViewPageTemplateFile(
1305
 
        '../templates/teammembership-self-renewal.pt')
1306
 
 
1307
 
    @property
1308
 
    def label(self):
1309
 
        return "Renew membership of %s in %s" % (
1310
 
            self.context.person.displayname, self.context.team.displayname)
1311
 
 
1312
 
    page_title = label
1313
 
 
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
1317
 
        # about to expire.
1318
 
        if not check_permission('launchpad.Edit', context.person):
1319
 
            raise Unauthorized(
1320
 
                "You may not renew the membership for %s." %
1321
 
                context.person.displayname)
1322
 
        LaunchpadFormView.__init__(self, context, request)
1323
 
 
1324
 
    def browserDefault(self, request):
1325
 
        return self, ()
1326
 
 
1327
 
    @property
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."
1346
 
            else:
1347
 
                link_text = (
1348
 
                    "You or one of the team administrators has already "
1349
 
                    "renewed it.")
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))
1354
 
        else:
1355
 
            raise AssertionError('This membership can be renewed!')
1356
 
        return text
1357
 
 
1358
 
    @property
1359
 
    def time_before_expiration(self):
1360
 
        return self.context.dateexpires - datetime.now(pytz.timezone('UTC'))
1361
 
 
1362
 
    @property
1363
 
    def next_url(self):
1364
 
        return canonical_url(self.context.person)
1365
 
 
1366
 
    cancel_url = next_url
1367
 
 
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'))))
1378
 
 
1379
 
 
1380
 
class ITeamMembershipInvitationAcknowledgementForm(Interface):
1381
 
    """Schema for the form in which team admins acknowledge invitations.
1382
 
 
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.
1386
 
    """
1387
 
 
1388
 
    acknowledger_comment = Text(
1389
 
        title=_("Comment"), required=False, readonly=False)
1390
 
 
1391
 
 
1392
 
class TeamInvitationView(LaunchpadFormView):
1393
 
    """Where team admins can accept/decline membership invitations."""
1394
 
 
1395
 
    implements(IBrowserPublisher)
1396
 
 
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')
1405
 
 
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):
1410
 
            raise Unauthorized(
1411
 
                "Only team administrators can approve/decline invitations "
1412
 
                "sent to this team.")
1413
 
        LaunchpadFormView.__init__(self, context, request)
1414
 
 
1415
 
    @property
1416
 
    def label(self):
1417
 
        """See `LaunchpadFormView`."""
1418
 
        return "Make %s a member of %s" % (
1419
 
            self.context.person.displayname, self.context.team.displayname)
1420
 
 
1421
 
    @property
1422
 
    def page_title(self):
1423
 
        return smartquote(
1424
 
            '"%s" team invitation') % self.context.team.displayname
1425
 
 
1426
 
    def browserDefault(self, request):
1427
 
        return self, ()
1428
 
 
1429
 
    @property
1430
 
    def next_url(self):
1431
 
        return canonical_url(self.context.person)
1432
 
 
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."))
1438
 
            return
1439
 
        member = self.context.person
1440
 
        try:
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}.",
1447
 
                  mapping=dict(
1448
 
                      that_team=self.context.team.displayname,
1449
 
                      this_team=member.displayname)))
1450
 
        else:
1451
 
            self.request.response.addInfoNotification(
1452
 
                _("This team is now a member of ${team}.", mapping=dict(
1453
 
                    team=self.context.team.displayname)))
1454
 
 
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."))
1460
 
            return
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)))
1467
 
 
1468
 
    @action(_("Cancel"), name="cancel")
1469
 
    def cancel_action(self, action, data):
1470
 
        # Simply redirect back.
1471
 
        pass
1472
 
 
1473
 
 
1474
 
class TeamMenuMixin(PPANavigationMenuMixIn, CommonMenuLinks):
1475
 
    """Base class of team menus.
1476
 
 
1477
 
    You will need to override the team attribute if your menu subclass
1478
 
    has the view as its context object.
1479
 
    """
1480
 
 
1481
 
    def profile(self):
1482
 
        target = ''
1483
 
        text = 'Overview'
1484
 
        return Link(target, text)
1485
 
 
1486
 
    @enabled_with_permission('launchpad.Edit')
1487
 
    def edit(self):
1488
 
        target = '+edit'
1489
 
        text = 'Change details'
1490
 
        return Link(target, text, icon='edit')
1491
 
 
1492
 
    @enabled_with_permission('launchpad.Edit')
1493
 
    def branding(self):
1494
 
        target = '+branding'
1495
 
        text = 'Change branding'
1496
 
        return Link(target, text, icon='edit')
1497
 
 
1498
 
    @enabled_with_permission('launchpad.Owner')
1499
 
    def reassign(self):
1500
 
        target = '+reassign'
1501
 
        text = 'Change owner'
1502
 
        summary = 'Change the owner of the team'
1503
 
        return Link(target, text, summary, icon='edit')
1504
 
 
1505
 
    @enabled_with_permission('launchpad.Moderate')
1506
 
    def delete(self):
1507
 
        target = '+delete'
1508
 
        text = 'Delete'
1509
 
        summary = 'Delete this team'
1510
 
        return Link(target, text, summary, icon='trash-icon')
1511
 
 
1512
 
    @enabled_with_permission('launchpad.View')
1513
 
    def members(self):
1514
 
        target = '+members'
1515
 
        text = 'Show all members'
1516
 
        return Link(target, text, icon='team')
1517
 
 
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')
1523
 
 
1524
 
    @enabled_with_permission('launchpad.Edit')
1525
 
    def add_member(self):
1526
 
        target = '+addmember'
1527
 
        text = 'Add member'
1528
 
        return Link(target, text, icon='add')
1529
 
 
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')
1535
 
 
1536
 
    def map(self):
1537
 
        target = '+map'
1538
 
        text = 'View map and time zones'
1539
 
        return Link(target, text, icon='meeting')
1540
 
 
1541
 
    def add_my_teams(self):
1542
 
        target = '+add-my-teams'
1543
 
        text = 'Add one of my teams'
1544
 
        enabled = True
1545
 
        restricted = TeamSubscriptionPolicy.RESTRICTED
1546
 
        if self.person.subscriptionpolicy == restricted:
1547
 
            # This is a restricted team; users can't join.
1548
 
            enabled = False
1549
 
        return Link(target, text, icon='add', enabled=enabled)
1550
 
 
1551
 
    def memberships(self):
1552
 
        target = '+participation'
1553
 
        text = 'Show team participation'
1554
 
        return Link(target, text, icon='info')
1555
 
 
1556
 
    @enabled_with_permission('launchpad.View')
1557
 
    def mugshots(self):
1558
 
        target = '+mugshots'
1559
 
        text = 'Show member photos'
1560
 
        return Link(target, text, icon='team')
1561
 
 
1562
 
    def polls(self):
1563
 
        target = '+polls'
1564
 
        text = 'Show polls'
1565
 
        return Link(target, text, icon='info')
1566
 
 
1567
 
    @enabled_with_permission('launchpad.Edit')
1568
 
    def add_poll(self):
1569
 
        target = '+newpoll'
1570
 
        text = 'Create a poll'
1571
 
        return Link(target, text, icon='add')
1572
 
 
1573
 
    @enabled_with_permission('launchpad.Edit')
1574
 
    def editemail(self):
1575
 
        target = '+contactaddress'
1576
 
        text = 'Set contact address'
1577
 
        summary = (
1578
 
            'The address Launchpad uses to contact %s' %
1579
 
            self.person.displayname)
1580
 
        return Link(target, text, summary, icon='edit')
1581
 
 
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'
1588
 
            icon = 'edit'
1589
 
        else:
1590
 
            text = 'Create a mailing list'
1591
 
            icon = 'add'
1592
 
        summary = (
1593
 
            'The mailing list associated with %s' % self.context.displayname)
1594
 
        return Link(target, text, summary, icon=icon)
1595
 
 
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'
1601
 
        summary = (
1602
 
            'The mailing list associated with %s' % self.context.displayname)
1603
 
        return Link(target, text, summary, icon='edit')
1604
 
 
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')
1610
 
 
1611
 
    def leave(self):
1612
 
        enabled = True
1613
 
        if not userIsActiveTeamMember(self.person):
1614
 
            enabled = False
1615
 
        if self.person.teamowner == self.user:
1616
 
            # The owner cannot leave his team.
1617
 
            enabled = False
1618
 
        target = '+leave'
1619
 
        text = 'Leave the Team'
1620
 
        icon = 'remove'
1621
 
        return Link(target, text, icon=icon, enabled=enabled)
1622
 
 
1623
 
    def join(self):
1624
 
        enabled = True
1625
 
        person = self.person
1626
 
        if userIsActiveTeamMember(person):
1627
 
            enabled = False
1628
 
        elif (self.person.subscriptionpolicy ==
1629
 
              TeamSubscriptionPolicy.RESTRICTED):
1630
 
            # This is a restricted team; users can't join.
1631
 
            enabled = False
1632
 
        target = '+join'
1633
 
        text = 'Join the team'
1634
 
        icon = 'add'
1635
 
        return Link(target, text, icon=icon, enabled=enabled)
1636
 
 
1637
 
 
1638
 
class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
1639
 
 
1640
 
    usedfor = ITeam
1641
 
    facet = 'overview'
1642
 
    links = [
1643
 
        'edit',
1644
 
        'branding',
1645
 
        'common_edithomepage',
1646
 
        'members',
1647
 
        'mugshots',
1648
 
        'add_member',
1649
 
        'proposed_members',
1650
 
        'memberships',
1651
 
        'received_invitations',
1652
 
        'editemail',
1653
 
        'configure_mailing_list',
1654
 
        'moderate_mailing_list',
1655
 
        'editlanguages',
1656
 
        'map',
1657
 
        'polls',
1658
 
        'add_poll',
1659
 
        'join',
1660
 
        'leave',
1661
 
        'add_my_teams',
1662
 
        'reassign',
1663
 
        'projects',
1664
 
        'activate_ppa',
1665
 
        'maintained',
1666
 
        'ppa',
1667
 
        'related_software_summary',
1668
 
        'view_recipes',
1669
 
        'subscriptions',
1670
 
        'structural_subscriptions',
1671
 
        ]
1672
 
 
1673
 
 
1674
 
class TeamOverviewNavigationMenu(NavigationMenu, TeamMenuMixin):
1675
 
    """A top-level menu for navigation within a Team."""
1676
 
 
1677
 
    usedfor = ITeam
1678
 
    facet = 'overview'
1679
 
    links = ['profile', 'polls', 'members', 'ppas']
1680
 
 
1681
 
 
1682
 
class TeamMembershipView(LaunchpadView):
1683
 
    """The view behind ITeam/+members."""
1684
 
 
1685
 
    @cachedproperty
1686
 
    def label(self):
1687
 
        return smartquote('Members of "%s"' % self.context.displayname)
1688
 
 
1689
 
    @cachedproperty
1690
 
    def active_memberships(self):
1691
 
        """Current members of the team."""
1692
 
        return ActiveBatchNavigator(
1693
 
            self.context.member_memberships, self.request)
1694
 
 
1695
 
    @cachedproperty
1696
 
    def inactive_memberships(self):
1697
 
        """Former members of the team."""
1698
 
        return InactiveBatchNavigator(
1699
 
            self.context.getInactiveMemberships(), self.request)
1700
 
 
1701
 
    @cachedproperty
1702
 
    def invited_memberships(self):
1703
 
        """Other teams invited to become members of this team."""
1704
 
        return list(self.context.getInvitedMemberships())
1705
 
 
1706
 
    @cachedproperty
1707
 
    def proposed_memberships(self):
1708
 
        """Users who have requested to join this team."""
1709
 
        return list(self.context.getProposedMemberships())
1710
 
 
1711
 
    @property
1712
 
    def have_pending_members(self):
1713
 
        return self.proposed_memberships or self.invited_memberships
1714
 
 
1715
 
 
1716
 
class TeamIndexView(PersonIndexView, TeamJoinMixin):
1717
 
    """The view class for the +index page.
1718
 
 
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.
1721
 
    """
1722
 
 
1723
 
    @property
1724
 
    def super_teams(self):
1725
 
        """Return only the super teams that the viewer is able to see."""
1726
 
        return [
1727
 
            team for team in self.context.super_teams
1728
 
            if check_permission('launchpad.View', team)]
1729
 
 
1730
 
    @property
1731
 
    def can_show_subteam_portlet(self):
1732
 
        """Only show the subteam portlet if there is info to display.
1733
 
 
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.
1737
 
        """
1738
 
        try:
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)
1744
 
 
1745
 
    @property
1746
 
    def visibility_info(self):
1747
 
        if self.context.visibility == PersonVisibility.PRIVATE:
1748
 
            return 'Private team'
1749
 
        else:
1750
 
            return 'Public team'
1751
 
 
1752
 
    @property
1753
 
    def visibility_portlet_class(self):
1754
 
        """The portlet class for team visibility."""
1755
 
        if self.context.visibility == PersonVisibility.PUBLIC:
1756
 
            return 'portlet'
1757
 
        return 'portlet private'
1758
 
 
1759
 
    @property
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('"', '\\"')
1765
 
 
1766
 
 
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)
1772
 
 
1773
 
 
1774
 
class TeamJoinView(LaunchpadFormView, TeamJoinMixin):
1775
 
    """A view class for joining a team."""
1776
 
    schema = TeamJoinForm
1777
 
 
1778
 
    @property
1779
 
    def label(self):
1780
 
        return 'Join ' + cgi.escape(self.context.displayname)
1781
 
 
1782
 
    page_title = label
1783
 
 
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)
1789
 
 
1790
 
    @property
1791
 
    def field_names(self):
1792
 
        """See `LaunchpadFormView`.
1793
 
 
1794
 
        If the user can subscribe to the mailing list then include the
1795
 
        mailinglist subscription checkbox otherwise remove it.
1796
 
        """
1797
 
        if self.user_can_subscribe_to_list:
1798
 
            return ['mailinglist_subscribe']
1799
 
        else:
1800
 
            return []
1801
 
 
1802
 
    @property
1803
 
    def join_allowed(self):
1804
 
        """Is the logged in user allowed to join this team?
1805
 
 
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.
1808
 
        """
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):
1817
 
            return False
1818
 
 
1819
 
        restricted = TeamSubscriptionPolicy.RESTRICTED
1820
 
        return self.context.subscriptionpolicy != restricted
1821
 
 
1822
 
    @property
1823
 
    def user_can_request_to_join(self):
1824
 
        """Can the logged in user request to join this team?
1825
 
 
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.
1828
 
        """
1829
 
        if not self.join_allowed:
1830
 
            return False
1831
 
        return not (self.user_is_active_member or
1832
 
                    self.user_is_proposed_member)
1833
 
 
1834
 
    @property
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)
1839
 
 
1840
 
    @property
1841
 
    def team_is_moderated(self):
1842
 
        """Is this team a moderated team?
1843
 
 
1844
 
        Return True if the team's subscription policy is MODERATED.
1845
 
        """
1846
 
        policy = self.context.subscriptionpolicy
1847
 
        return policy == TeamSubscriptionPolicy.MODERATED
1848
 
 
1849
 
    @property
1850
 
    def next_url(self):
1851
 
        return canonical_url(self.context)
1852
 
 
1853
 
    @property
1854
 
    def cancel_url(self):
1855
 
        return canonical_url(self.context)
1856
 
 
1857
 
    @action(_("Join"), name="join")
1858
 
    def action_save(self, action, data):
1859
 
        response = self.request.response
1860
 
 
1861
 
        if self.user_can_request_to_join:
1862
 
            # Shut off mailing list auto-subscription - we want direct
1863
 
            # control over it.
1864
 
            self.user.join(self.context, may_subscribe_to_list=False)
1865
 
 
1866
 
            if self.team_is_moderated:
1867
 
                response.addInfoNotification(
1868
 
                    _('Your request to join ${team} is awaiting '
1869
 
                      'approval.',
1870
 
                      mapping={'team': self.context.displayname}))
1871
 
            else:
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)
1877
 
 
1878
 
        else:
1879
 
            response.addErrorNotification(
1880
 
                _('You cannot join ${team}.',
1881
 
                  mapping={'team': self.context.displayname}))
1882
 
 
1883
 
    def _subscribeToList(self, response):
1884
 
        """Subscribe the user to the team's mailing list."""
1885
 
 
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)
1890
 
 
1891
 
            if self.team_is_moderated:
1892
 
                response.addInfoNotification(
1893
 
                    _('Your mailing list subscription is '
1894
 
                      'awaiting approval.'))
1895
 
            else:
1896
 
                response.addInfoNotification(
1897
 
                    structured(
1898
 
                        _("You have been subscribed to this "
1899
 
                          "team&#x2019;s mailing list.")))
1900
 
        else:
1901
 
            # A catch-all case, perhaps from stale or mangled
1902
 
            # form data.
1903
 
            response.addErrorNotification(
1904
 
                _('Mailing list subscription failed.'))
1905
 
 
1906
 
 
1907
 
class TeamAddMyTeamsView(LaunchpadFormView):
1908
 
    """Propose/add to this team any team that you're an administrator of."""
1909
 
 
1910
 
    page_title = 'Propose/add one of your teams to another one'
1911
 
    custom_widget('teams', LabeledMultiCheckBoxWidget)
1912
 
 
1913
 
    def initialize(self):
1914
 
        context = self.context
1915
 
        if context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
1916
 
            self.label = 'Propose these teams as members'
1917
 
        else:
1918
 
            self.label = 'Add these teams to %s' % context.displayname
1919
 
        self.next_url = canonical_url(context)
1920
 
        super(TeamAddMyTeamsView, self).initialize()
1921
 
 
1922
 
    def setUpFields(self):
1923
 
        terms = []
1924
 
        for team in self.candidate_teams:
1925
 
            text = structured(
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',
1930
 
                 title=_(''),
1931
 
                 value_type=Choice(vocabulary=SimpleVocabulary(terms)),
1932
 
                 required=False),
1933
 
            render_context=self.render_context)
1934
 
 
1935
 
    def setUpWidgets(self, context=None):
1936
 
        super(TeamAddMyTeamsView, self).setUpWidgets(context)
1937
 
        self.widgets['teams'].display_label = False
1938
 
 
1939
 
    @cachedproperty
1940
 
    def candidate_teams(self):
1941
 
        """Return the set of teams that can be added/proposed for the context.
1942
 
 
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.)
1946
 
        """
1947
 
        candidates = []
1948
 
        for team in self.user.getAdministratedTeams():
1949
 
            if team == self.context:
1950
 
                continue
1951
 
            elif team.visibility != PersonVisibility.PUBLIC:
1952
 
                continue
1953
 
            elif team in self.context.activemembers:
1954
 
                # The team is already a member of the context object.
1955
 
                continue
1956
 
            elif self.context.hasParticipationEntryFor(team):
1957
 
                # The context object is a member/submember of the team.
1958
 
                continue
1959
 
            candidates.append(team)
1960
 
        return candidates
1961
 
 
1962
 
    @property
1963
 
    def cancel_url(self):
1964
 
        """The return URL."""
1965
 
        return canonical_url(self.context)
1966
 
 
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.')
1972
 
 
1973
 
    def hasCandidates(self, action):
1974
 
        """Return whether the user has teams to propose."""
1975
 
        return len(self.candidate_teams) > 0
1976
 
 
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(
1992
 
                    context,
1993
 
                    'Accepted an already pending invitation while trying to '
1994
 
                    'propose the team for membership.')
1995
 
                accepted_invite_team_names.append(team.displayname)
1996
 
            elif is_admin:
1997
 
                context.addMember(team, reviewer=self.user)
1998
 
                added_team_names.append(team.displayname)
1999
 
            else:
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)
2006
 
                else:
2007
 
                    raise AssertionError(
2008
 
                        'Unexpected membership status (%s) for %s.'
2009
 
                        % (membership.status.name, team.name))
2010
 
        full_message = ''
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.'),
2016
 
            ):
2017
 
            if len(team_names) == 0:
2018
 
                continue
2019
 
            elif len(team_names) == 1:
2020
 
                verb = 'has been'
2021
 
                team_string = team_names[0]
2022
 
            elif len(team_names) > 1:
2023
 
                verb = 'have been'
2024
 
                team_string = (
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)
2028
 
 
2029
 
 
2030
 
class TeamLeaveView(LaunchpadFormView, TeamJoinMixin):
2031
 
    schema = Interface
2032
 
 
2033
 
    @property
2034
 
    def label(self):
2035
 
        return 'Leave ' + cgi.escape(self.context.displayname)
2036
 
 
2037
 
    page_title = label
2038
 
 
2039
 
    @property
2040
 
    def cancel_url(self):
2041
 
        return canonical_url(self.context)
2042
 
 
2043
 
    next_url = cancel_url
2044
 
 
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)
2049
 
 
2050
 
 
2051
 
class TeamReassignmentView(ObjectReassignmentView):
2052
 
 
2053
 
    ownerOrMaintainerAttr = 'teamowner'
2054
 
    schema = ITeamReassignment
2055
 
 
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)
2061
 
 
2062
 
    def validateOwner(self, new_owner):
2063
 
        """Display error if the owner is not valid.
2064
 
 
2065
 
        Called by ObjectReassignmentView.validate().
2066
 
        """
2067
 
        if self.context.inTeam(new_owner):
2068
 
            path = self.context.findPathToTeam(new_owner)
2069
 
            if len(path) == 1:
2070
 
                relationship = 'a direct member'
2071
 
                path_string = ''
2072
 
            else:
2073
 
                relationship = 'an indirect member'
2074
 
                full_path = [self.context] + path
2075
 
                path_string = '(%s)' % '&rArr;'.join(
2076
 
                    team.displayname for team in full_path)
2077
 
            error = structured(
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,
2085
 
                        path=path_string))
2086
 
            self.setFieldError(self.ownerOrMaintainerName, error)
2087
 
 
2088
 
    @property
2089
 
    def contextName(self):
2090
 
        return self.context.displayname
2091
 
 
2092
 
    @property
2093
 
    def next_url(self):
2094
 
        return self._next_url
2095
 
 
2096
 
    def _afterOwnerChange(self, team, oldOwner, newOwner):
2097
 
        """Add the new and the old owners as administrators of the team.
2098
 
 
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.
2104
 
        """
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:
2109
 
            team.addMember(
2110
 
                newOwner, reviewer=oldOwner,
2111
 
                status=TeamMembershipStatus.ADMIN, force_team_add=True)
2112
 
        if oldOwner not in team.inactivemembers:
2113
 
            team.addMember(
2114
 
                oldOwner, reviewer=oldOwner,
2115
 
                status=TeamMembershipStatus.ADMIN, force_team_add=True)
2116
 
 
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.
2120
 
        clear_cache()
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)
2127
 
 
2128
 
 
2129
 
class ITeamIndexMenu(Interface):
2130
 
    """A marker interface for the +index navigation menu."""
2131
 
 
2132
 
 
2133
 
class ITeamEditMenu(Interface):
2134
 
    """A marker interface for the edit navigation menu."""
2135
 
 
2136
 
 
2137
 
class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):
2138
 
 
2139
 
    @property
2140
 
    def person(self):
2141
 
        """Override CommonMenuLinks since the view is the context."""
2142
 
        return self.context.context
2143
 
 
2144
 
 
2145
 
class TeamIndexMenu(TeamNavigationMenuBase):
2146
 
    """A menu for different aspects of editing a team."""
2147
 
 
2148
 
    usedfor = ITeamIndexMenu
2149
 
    facet = 'overview'
2150
 
    title = 'Change team'
2151
 
    links = ('edit', 'delete', 'join', 'add_my_teams', 'leave')
2152
 
 
2153
 
 
2154
 
class TeamEditMenu(TeamNavigationMenuBase):
2155
 
    """A menu for different aspects of editing a team."""
2156
 
 
2157
 
    usedfor = ITeamEditMenu
2158
 
    facet = 'overview'
2159
 
    title = 'Change team'
2160
 
    links = ('branding', 'common_edithomepage', 'editlanguages', 'reassign',
2161
 
             'editemail')
2162
 
 
2163
 
 
2164
 
class TeamMugshotView(LaunchpadView):
2165
 
    """A view for the team mugshot (team photo) page"""
2166
 
 
2167
 
    label = "Member photos"
2168
 
    batch_size = config.launchpad.mugshot_batch_size
2169
 
 
2170
 
    def initialize(self):
2171
 
        """Cache images to avoid dying from a million cuts."""
2172
 
        getUtility(IPersonSet).cacheBrandingForPeople(
2173
 
            self.members.currentBatch())
2174
 
 
2175
 
    @cachedproperty
2176
 
    def members(self):
2177
 
        """Get a batch of all members in the team."""
2178
 
        batch_nav = BatchNavigator(
2179
 
            self.context.allmembers, self.request, size=self.batch_size)
2180
 
        return batch_nav
2181
 
 
2182
 
 
2183
 
classImplements(TeamIndexView, ITeamIndexMenu)
2184
 
classImplements(TeamEditView, ITeamEditMenu)
 
1204
class TeamHierarchyView(LaunchpadView):
 
1205
    """View for ~team/+teamhierarchy page."""
 
1206
 
 
1207
    @property
 
1208
    def label(self):
 
1209
        return 'Team relationships for ' + self.context.displayname
 
1210
 
 
1211
    @property
 
1212
    def has_sub_teams(self):
 
1213
        return self.context.sub_teams.count() > 0
 
1214
 
 
1215
    @property
 
1216
    def has_super_teams(self):
 
1217
        return self.context.super_teams.count() > 0
 
1218
 
 
1219
    @property
 
1220
    def has_only_super_teams(self):
 
1221
        return self.has_super_teams and not self.has_sub_teams
 
1222
 
 
1223
    @property
 
1224
    def has_only_sub_teams(self):
 
1225
        return not self.has_super_teams and self.has_sub_teams
 
1226
 
 
1227
    @property
 
1228
    def has_relationships(self):
 
1229
        return self.has_sub_teams or self.has_super_teams