~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-06-25 08:55:37 UTC
  • mfrom: (13287.1.8 bug-800652)
  • Revision ID: launchpad@pqm.canonical.com-20110625085537-moikyoo2pe98zs7r
[r=jcsackett, julian-edwards][bug=800634,
        800652] Enable and display overrides on sync package uploads.

Show diffs side-by-side

added added

removed removed

Lines of Context:
9
9
__all__ = [
10
10
    'BeginTeamClaimView',
11
11
    'BugSubscriberPackageBugsSearchListingView',
12
 
    'CommonMenuLinks',
13
12
    'EmailToPersonView',
14
13
    'PeopleSearchView',
15
14
    'PersonAccountAdministerView',
43
42
    'PersonRdfView',
44
43
    'PersonRelatedBugTaskSearchListingView',
45
44
    'PersonRelatedSoftwareView',
46
 
    'PersonRenameFormMixin',
47
45
    'PersonReportedBugTaskSearchListingView',
48
46
    'PersonSearchQuestionsView',
49
47
    'PersonSetActionNavigationMenu',
58
56
    'PersonSubscriptionsView',
59
57
    'PersonView',
60
58
    'PersonVouchersView',
61
 
    'PPANavigationMenuMixIn',
62
59
    'RedirectToEditLanguagesView',
63
60
    'RestrictedMembershipsPersonView',
64
61
    'SearchAnsweredQuestionsView',
67
64
    'SearchCreatedQuestionsView',
68
65
    'SearchNeedAttentionQuestionsView',
69
66
    'SearchSubscribedQuestionsView',
 
67
    'TeamAddMyTeamsView',
 
68
    'TeamBreadcrumb',
 
69
    'TeamEditMenu',
 
70
    'TeamIndexMenu',
 
71
    'TeamJoinView',
 
72
    'TeamLeaveView',
 
73
    'TeamMembershipView',
 
74
    'TeamMugshotView',
 
75
    'TeamNavigation',
 
76
    'TeamOverviewMenu',
 
77
    'TeamOverviewNavigationMenu',
 
78
    'TeamReassignmentView',
70
79
    'archive_to_person',
71
80
    ]
72
81
 
73
82
 
74
83
import cgi
75
84
import copy
76
 
from datetime import datetime
 
85
from datetime import (
 
86
    datetime,
 
87
    timedelta,
 
88
    )
77
89
import itertools
78
90
from itertools import chain
79
91
from operator import (
87
99
from lazr.delegates import delegates
88
100
from lazr.restful.interface import copy_field
89
101
from lazr.restful.interfaces import IWebServiceClientRequest
90
 
from lazr.restful.utils import smartquote
91
102
from lazr.uri import URI
92
103
import pytz
93
104
from storm.expr import Join
112
123
from zope.interface.exceptions import Invalid
113
124
from zope.interface.interface import invariant
114
125
from zope.publisher.interfaces import NotFound
 
126
from zope.publisher.interfaces.browser import IBrowserPublisher
115
127
from zope.schema import (
116
128
    Bool,
117
129
    Choice,
 
130
    List,
118
131
    Text,
119
132
    TextLine,
120
133
    )
126
139
from zope.security.interfaces import Unauthorized
127
140
from zope.security.proxy import removeSecurityProxy
128
141
 
129
 
from lp import _
 
142
from canonical.config import config
 
143
from canonical.database.sqlbase import flush_database_updates
 
144
from canonical.launchpad import (
 
145
    _,
 
146
    helpers,
 
147
    )
 
148
from canonical.launchpad.browser.feeds import FeedsMixin
 
149
from canonical.launchpad.interfaces.account import (
 
150
    AccountStatus,
 
151
    IAccount,
 
152
    )
 
153
from canonical.launchpad.interfaces.authtoken import LoginTokenType
 
154
from canonical.launchpad.interfaces.emailaddress import (
 
155
    EmailAddressStatus,
 
156
    IEmailAddress,
 
157
    IEmailAddressSet,
 
158
    )
 
159
from canonical.launchpad.interfaces.gpghandler import (
 
160
    GPGKeyNotFoundError,
 
161
    IGPGHandler,
 
162
    )
 
163
from canonical.launchpad.interfaces.launchpad import (
 
164
    INotificationRecipientSet,
 
165
    UnknownRecipientError,
 
166
    )
 
167
from canonical.launchpad.interfaces.logintoken import ILoginTokenSet
 
168
from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
 
169
from canonical.launchpad.webapp import (
 
170
    ApplicationMenu,
 
171
    canonical_url,
 
172
    ContextMenu,
 
173
    enabled_with_permission,
 
174
    Link,
 
175
    Navigation,
 
176
    NavigationMenu,
 
177
    StandardLaunchpadFacets,
 
178
    stepthrough,
 
179
    stepto,
 
180
    structured,
 
181
    )
 
182
from canonical.launchpad.webapp.authorization import check_permission
 
183
from canonical.launchpad.webapp.batching import (
 
184
    ActiveBatchNavigator,
 
185
    BatchNavigator,
 
186
    InactiveBatchNavigator,
 
187
    )
 
188
from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 
189
from canonical.launchpad.webapp.interfaces import (
 
190
    ILaunchBag,
 
191
    IOpenLaunchBag,
 
192
    )
 
193
from canonical.launchpad.webapp.login import logoutPerson
 
194
from canonical.launchpad.webapp.menu import get_current_view
 
195
from canonical.launchpad.webapp.publisher import LaunchpadView
 
196
from canonical.lazr.utils import smartquote
130
197
from lp.answers.browser.questiontarget import SearchQuestionsView
131
198
from lp.answers.enums import QuestionParticipation
132
199
from lp.answers.interfaces.questioncollection import IQuestionSet
150
217
from lp.app.validators.email import valid_email
151
218
from lp.app.widgets.image import ImageChangeWidget
152
219
from lp.app.widgets.itemswidgets import (
 
220
    LabeledMultiCheckBoxWidget,
153
221
    LaunchpadDropdownWidget,
154
222
    LaunchpadRadioWidget,
155
223
    LaunchpadRadioWidgetWithDescription,
172
240
from lp.code.interfaces.branchnamespace import IBranchNamespaceSet
173
241
from lp.registry.browser import BaseRdfView
174
242
from lp.registry.browser.branding import BrandingChangeView
 
243
from lp.registry.browser.mailinglists import enabled_with_active_mailing_list
175
244
from lp.registry.browser.menu import (
176
245
    IRegistryCollectionNavigationMenu,
177
246
    RegistryCollectionActionMenuBase,
178
247
    TopLevelMenuMixin,
179
248
    )
180
 
from lp.registry.browser.teamjoin import TeamJoinMixin
 
249
from lp.registry.browser.objectreassignment import ObjectReassignmentView
 
250
from lp.registry.browser.team import TeamEditView
181
251
from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
182
252
from lp.registry.interfaces.gpg import IGPGKeySet
183
253
from lp.registry.interfaces.irc import IIrcIDSet
196
266
    IPerson,
197
267
    IPersonClaim,
198
268
    IPersonSet,
 
269
    ITeam,
 
270
    ITeamReassignment,
199
271
    PersonVisibility,
 
272
    TeamMembershipRenewalPolicy,
 
273
    TeamSubscriptionPolicy,
200
274
    )
201
275
from lp.registry.interfaces.personproduct import IPersonProductFactory
202
276
from lp.registry.interfaces.pillar import IPillarNameSet
203
 
from lp.registry.interfaces.poll import IPollSubset
 
277
from lp.registry.interfaces.poll import (
 
278
    IPollSet,
 
279
    IPollSubset,
 
280
    )
204
281
from lp.registry.interfaces.product import IProduct
205
282
from lp.registry.interfaces.ssh import (
206
283
    ISSHKeySet,
209
286
    SSHKeyType,
210
287
    )
211
288
from lp.registry.interfaces.teammembership import (
 
289
    CyclicalTeamMembershipError,
 
290
    DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT,
 
291
    ITeamMembership,
212
292
    ITeamMembershipSet,
213
293
    TeamMembershipStatus,
214
294
    )
218
298
    Milestone,
219
299
    milestone_sort_key,
220
300
    )
221
 
from lp.services.config import config
222
 
from lp.services.database.sqlbase import flush_database_updates
223
 
from lp.services.feeds.browser import FeedsMixin
224
301
from lp.services.fields import LocationField
225
302
from lp.services.geoip.interfaces import IRequestPreferredLanguages
226
 
from lp.services.gpg.interfaces import (
227
 
    GPGKeyNotFoundError,
228
 
    IGPGHandler,
229
 
    )
230
 
from lp.services.helpers import shortlist
231
 
from lp.services.identity.interfaces.account import (
232
 
    AccountStatus,
233
 
    IAccount,
234
 
    )
235
 
from lp.services.identity.interfaces.emailaddress import (
236
 
    EmailAddressStatus,
237
 
    IEmailAddress,
238
 
    IEmailAddressSet,
239
 
    )
240
 
from lp.services.mail.interfaces import (
241
 
    INotificationRecipientSet,
242
 
    UnknownRecipientError,
243
 
    )
244
303
from lp.services.messages.interfaces.message import (
245
304
    IDirectEmailAuthorization,
246
305
    QuotaReachedError,
247
306
    )
248
 
from lp.services.oauth.interfaces import IOAuthConsumerSet
249
307
from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
250
308
from lp.services.openid.browser.openiddiscovery import (
251
309
    XRDSContentNegotiationMixin,
252
310
    )
253
311
from lp.services.openid.interfaces.openid import IOpenIDPersistentIdentity
 
312
from lp.services.openid.interfaces.openidrpsummary import IOpenIDRPSummarySet
254
313
from lp.services.propertycache import (
255
314
    cachedproperty,
256
315
    get_property_cache,
259
318
    ISalesforceVoucherProxy,
260
319
    SalesforceVoucherProxyException,
261
320
    )
262
 
from lp.services.verification.interfaces.authtoken import LoginTokenType
263
 
from lp.services.verification.interfaces.logintoken import ILoginTokenSet
264
 
from lp.services.webapp import (
265
 
    ApplicationMenu,
266
 
    canonical_url,
267
 
    ContextMenu,
268
 
    enabled_with_permission,
269
 
    Link,
270
 
    Navigation,
271
 
    NavigationMenu,
272
 
    StandardLaunchpadFacets,
273
 
    stepthrough,
274
 
    stepto,
275
 
    structured,
276
 
    )
277
 
from lp.services.webapp.authorization import check_permission
278
 
from lp.services.webapp.batching import BatchNavigator
279
 
from lp.services.webapp.interfaces import (
280
 
    ILaunchBag,
281
 
    IOpenLaunchBag,
282
 
    )
283
 
from lp.services.webapp.login import logoutPerson
284
 
from lp.services.webapp.menu import get_current_view
285
 
from lp.services.webapp.publisher import LaunchpadView
286
321
from lp.services.worlddata.interfaces.country import ICountry
287
322
from lp.services.worlddata.interfaces.language import ILanguageSet
288
323
from lp.soyuz.browser.archivesubscription import (
289
324
    traverse_archive_subscription_for_subscriber,
290
325
    )
 
326
from lp.soyuz.enums import ArchiveStatus
 
327
from lp.soyuz.interfaces.archive import IArchiveSet
291
328
from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
292
329
from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuildSet
293
 
from lp.soyuz.interfaces.publishing import ISourcePackagePublishingHistory
294
330
from lp.soyuz.interfaces.sourcepackagerelease import ISourcePackageRelease
295
331
 
296
332
 
421
457
        # Return the found membership regardless of its status as we know
422
458
        # TeamMembershipSelfRenewalView will tell users why the memembership
423
459
        # can't be renewed when necessary.
424
 
        # Circular imports
425
 
        from lp.registry.browser.team import TeamMembershipSelfRenewalView
426
460
        membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
427
461
            self.context, getUtility(IPersonSet).getByName(name))
428
462
        if membership is None:
517
551
        return self.context.getMergeQueue(name)
518
552
 
519
553
 
 
554
class TeamNavigation(PersonNavigation):
 
555
 
 
556
    usedfor = ITeam
 
557
 
 
558
    @stepthrough('+poll')
 
559
    def traverse_poll(self, name):
 
560
        return getUtility(IPollSet).getByTeamAndName(self.context, name)
 
561
 
 
562
    @stepthrough('+invitation')
 
563
    def traverse_invitation(self, name):
 
564
        # Return the found membership regardless of its status as we know
 
565
        # TeamInvitationView can handle memberships in statuses other than
 
566
        # INVITED.
 
567
        membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
 
568
            self.context, getUtility(IPersonSet).getByName(name))
 
569
        if membership is None:
 
570
            return None
 
571
        return TeamInvitationView(membership, self.request)
 
572
 
 
573
    @stepthrough('+member')
 
574
    def traverse_member(self, name):
 
575
        person = getUtility(IPersonSet).getByName(name)
 
576
        if person is None:
 
577
            return None
 
578
        return getUtility(ITeamMembershipSet).getByPersonAndTeam(
 
579
            person, self.context)
 
580
 
 
581
 
 
582
class TeamBreadcrumb(Breadcrumb):
 
583
    """Builds a breadcrumb for an `ITeam`."""
 
584
 
 
585
    @property
 
586
    def text(self):
 
587
        return smartquote('"%s" team') % self.context.displayname
 
588
 
 
589
 
 
590
class TeamMembershipSelfRenewalView(LaunchpadFormView):
 
591
 
 
592
    implements(IBrowserPublisher)
 
593
 
 
594
    # This is needed for our breadcrumbs, as there's no <browser:page>
 
595
    # declaration for this view.
 
596
    __name__ = '+self-renewal'
 
597
    schema = ITeamMembership
 
598
    field_names = []
 
599
    template = ViewPageTemplateFile(
 
600
        '../templates/teammembership-self-renewal.pt')
 
601
 
 
602
    @property
 
603
    def label(self):
 
604
        return "Renew membership of %s in %s" % (
 
605
            self.context.person.displayname, self.context.team.displayname)
 
606
 
 
607
    page_title = label
 
608
 
 
609
    def __init__(self, context, request):
 
610
        # Only the member himself or admins of the member (in case it's a
 
611
        # team) can see the page in which they renew memberships that are
 
612
        # about to expire.
 
613
        if not check_permission('launchpad.Edit', context.person):
 
614
            raise Unauthorized(
 
615
                "You may not renew the membership for %s." %
 
616
                context.person.displayname)
 
617
        LaunchpadFormView.__init__(self, context, request)
 
618
 
 
619
    def browserDefault(self, request):
 
620
        return self, ()
 
621
 
 
622
    @property
 
623
    def reason_for_denied_renewal(self):
 
624
        """Return text describing why the membership can't be renewed."""
 
625
        context = self.context
 
626
        ondemand = TeamMembershipRenewalPolicy.ONDEMAND
 
627
        admin = TeamMembershipStatus.ADMIN
 
628
        approved = TeamMembershipStatus.APPROVED
 
629
        date_limit = datetime.now(pytz.UTC) - timedelta(
 
630
            days=DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT)
 
631
        if context.status not in (admin, approved):
 
632
            text = "it is not active."
 
633
        elif context.team.renewal_policy != ondemand:
 
634
            text = ('<a href="%s">%s</a> is not a team that allows its '
 
635
                    'members to renew their own memberships.'
 
636
                    % (canonical_url(context.team),
 
637
                       context.team.unique_displayname))
 
638
        elif context.dateexpires is None or context.dateexpires > date_limit:
 
639
            if context.person.isTeam():
 
640
                link_text = "Somebody else has already renewed it."
 
641
            else:
 
642
                link_text = (
 
643
                    "You or one of the team administrators has already "
 
644
                    "renewed it.")
 
645
            text = ('it is not set to expire in %d days or less. '
 
646
                    '<a href="%s/+members">%s</a>'
 
647
                    % (DAYS_BEFORE_EXPIRATION_WARNING_IS_SENT,
 
648
                       canonical_url(context.team), link_text))
 
649
        else:
 
650
            raise AssertionError('This membership can be renewed!')
 
651
        return text
 
652
 
 
653
    @property
 
654
    def time_before_expiration(self):
 
655
        return self.context.dateexpires - datetime.now(pytz.timezone('UTC'))
 
656
 
 
657
    @property
 
658
    def next_url(self):
 
659
        return canonical_url(self.context.person)
 
660
 
 
661
    cancel_url = next_url
 
662
 
 
663
    @action(_("Renew"), name="renew")
 
664
    def renew_action(self, action, data):
 
665
        member = self.context.person
 
666
        # This if-statement prevents an exception if the user
 
667
        # double clicks on the submit button.
 
668
        if self.context.canBeRenewedByMember():
 
669
            member.renewTeamMembership(self.context.team)
 
670
        self.request.response.addInfoNotification(
 
671
            _("Membership renewed until ${date}.", mapping=dict(
 
672
                    date=self.context.dateexpires.strftime('%Y-%m-%d'))))
 
673
 
 
674
 
 
675
class ITeamMembershipInvitationAcknowledgementForm(Interface):
 
676
    """Schema for the form in which team admins acknowledge invitations.
 
677
 
 
678
    We could use ITeamMembership for that, but the acknowledger_comment is
 
679
    marked readonly there and that means LaunchpadFormView won't include the
 
680
    value of that in the data given to our action handler.
 
681
    """
 
682
 
 
683
    acknowledger_comment = Text(
 
684
        title=_("Comment"), required=False, readonly=False)
 
685
 
 
686
 
 
687
class TeamInvitationView(LaunchpadFormView):
 
688
    """Where team admins can accept/decline membership invitations."""
 
689
 
 
690
    implements(IBrowserPublisher)
 
691
 
 
692
    # This is needed for our breadcrumbs, as there's no <browser:page>
 
693
    # declaration for this view.
 
694
    __name__ = '+invitation'
 
695
    schema = ITeamMembershipInvitationAcknowledgementForm
 
696
    field_names = ['acknowledger_comment']
 
697
    custom_widget('acknowledger_comment', TextAreaWidget, height=5, width=60)
 
698
    template = ViewPageTemplateFile(
 
699
        '../templates/teammembership-invitation.pt')
 
700
 
 
701
    def __init__(self, context, request):
 
702
        # Only admins of the invited team can see the page in which they
 
703
        # approve/decline invitations.
 
704
        if not check_permission('launchpad.Edit', context.person):
 
705
            raise Unauthorized(
 
706
                "Only team administrators can approve/decline invitations "
 
707
                "sent to this team.")
 
708
        LaunchpadFormView.__init__(self, context, request)
 
709
 
 
710
    @property
 
711
    def label(self):
 
712
        """See `LaunchpadFormView`."""
 
713
        return "Make %s a member of %s" % (
 
714
            self.context.person.displayname, self.context.team.displayname)
 
715
 
 
716
    @property
 
717
    def page_title(self):
 
718
        return smartquote(
 
719
            '"%s" team invitation') % self.context.team.displayname
 
720
 
 
721
    def browserDefault(self, request):
 
722
        return self, ()
 
723
 
 
724
    @property
 
725
    def next_url(self):
 
726
        return canonical_url(self.context.person)
 
727
 
 
728
    @action(_("Accept"), name="accept")
 
729
    def accept_action(self, action, data):
 
730
        if self.context.status != TeamMembershipStatus.INVITED:
 
731
            self.request.response.addInfoNotification(
 
732
                _("This invitation has already been processed."))
 
733
            return
 
734
        member = self.context.person
 
735
        try:
 
736
            member.acceptInvitationToBeMemberOf(
 
737
                self.context.team, data['acknowledger_comment'])
 
738
        except CyclicalTeamMembershipError:
 
739
            self.request.response.addInfoNotification(
 
740
                _("This team may not be added to ${that_team} because it is "
 
741
                  "a member of ${this_team}.",
 
742
                  mapping=dict(
 
743
                      that_team=self.context.team.displayname,
 
744
                      this_team=member.displayname)))
 
745
        else:
 
746
            self.request.response.addInfoNotification(
 
747
                _("This team is now a member of ${team}.", mapping=dict(
 
748
                    team=self.context.team.displayname)))
 
749
 
 
750
    @action(_("Decline"), name="decline")
 
751
    def decline_action(self, action, data):
 
752
        if self.context.status != TeamMembershipStatus.INVITED:
 
753
            self.request.response.addInfoNotification(
 
754
                _("This invitation has already been processed."))
 
755
            return
 
756
        member = self.context.person
 
757
        member.declineInvitationToBeMemberOf(
 
758
            self.context.team, data['acknowledger_comment'])
 
759
        self.request.response.addInfoNotification(
 
760
            _("Declined the invitation to join ${team}", mapping=dict(
 
761
                  team=self.context.team.displayname)))
 
762
 
 
763
    @action(_("Cancel"), name="cancel")
 
764
    def cancel_action(self, action, data):
 
765
        # Simply redirect back.
 
766
        pass
 
767
 
 
768
 
520
769
class PersonSetNavigation(Navigation):
521
770
 
522
771
    usedfor = IPersonSet
612
861
 
613
862
    usedfor = IPerson
614
863
    facet = 'bugs'
615
 
    links = ['affectingbugs', 'assignedbugs', 'commentedbugs', 'reportedbugs',
 
864
    links = ['assignedbugs', 'commentedbugs', 'reportedbugs',
616
865
             'subscribedbugs', 'relatedbugs', 'softwarebugs']
617
866
 
618
867
    def relatedbugs(self):
619
 
        text = 'All related bugs'
620
 
        summary = ('All bug reports which %s reported, is assigned to, '
 
868
        text = 'List all related bugs'
 
869
        summary = ('Lists all bug reports which %s reported, is assigned to, '
621
870
                   'or is subscribed to.' % self.context.displayname)
622
871
        return Link('', text, site='bugs', summary=summary)
623
872
 
624
873
    def assignedbugs(self):
625
 
        text = 'Assigned bugs'
626
 
        summary = 'Bugs assigned to %s.' % self.context.displayname
 
874
        text = 'List assigned bugs'
 
875
        summary = 'Lists bugs assigned to %s.' % self.context.displayname
627
876
        return Link('+assignedbugs', text, site='bugs', summary=summary)
628
877
 
629
878
    def softwarebugs(self):
630
 
        text = 'Subscribed packages'
 
879
        text = 'List subscribed packages'
631
880
        summary = (
632
 
            'A summary report for packages where %s is a subscriber.'
 
881
            'A summary report for packages where %s is a bug supervisor.'
633
882
            % self.context.displayname)
634
883
        return Link('+packagebugs', text, site='bugs', summary=summary)
635
884
 
636
885
    def reportedbugs(self):
637
 
        text = 'Reported bugs'
638
 
        summary = 'Bugs reported by %s.' % self.context.displayname
 
886
        text = 'List reported bugs'
 
887
        summary = 'Lists bugs reported by %s.' % self.context.displayname
639
888
        return Link('+reportedbugs', text, site='bugs', summary=summary)
640
889
 
641
890
    def subscribedbugs(self):
642
 
        text = 'Subscribed bugs'
643
 
        summary = ('Bug reports %s is subscribed to.'
 
891
        text = 'List subscribed bugs'
 
892
        summary = ('Lists bug reports %s is subscribed to.'
644
893
                   % self.context.displayname)
645
894
        return Link('+subscribedbugs', text, site='bugs', summary=summary)
646
895
 
647
896
    def commentedbugs(self):
648
 
        text = 'Commented bugs'
649
 
        summary = ('Bug reports on which %s has commented.'
 
897
        text = 'List commented bugs'
 
898
        summary = ('Lists bug reports on which %s has commented.'
650
899
                   % self.context.displayname)
651
900
        return Link('+commentedbugs', text, site='bugs', summary=summary)
652
901
 
653
 
    def affectingbugs(self):
654
 
        text = 'Affecting bugs'
655
 
        summary = ('Bugs affecting %s.' % self.context.displayname)
656
 
        return Link('+affectingbugs', text, site='bugs', summary=summary)
657
 
 
658
902
 
659
903
class PersonSpecsMenu(NavigationMenu):
660
904
 
747
991
        enabled = bool(self.person.getLatestUploadedPPAPackages())
748
992
        return Link(target, text, enabled=enabled, icon='info')
749
993
 
750
 
    def synchronised(self):
751
 
        target = '+synchronised-packages'
752
 
        text = 'Synchronised packages'
753
 
        enabled = bool(
754
 
            self.person.getLatestSynchronisedPublishings())
755
 
        return Link(target, text, enabled=enabled, icon='info')
756
 
 
757
994
    def projects(self):
758
995
        target = '+related-projects'
759
996
        text = 'Related projects'
822
1059
        'projects',
823
1060
        'activate_ppa',
824
1061
        'maintained',
825
 
        'synchronised',
826
1062
        'view_ppa_subscriptions',
827
1063
        'ppa',
828
1064
        'oauth_tokens',
958
1194
    usedfor = IPersonRelatedSoftwareMenu
959
1195
    facet = 'overview'
960
1196
    links = ('related_software_summary', 'maintained', 'uploaded', 'ppa',
961
 
             'synchronised', 'projects')
 
1197
             'projects')
962
1198
 
963
1199
    @property
964
1200
    def person(self):
996
1232
        return Link(target, text)
997
1233
 
998
1234
 
 
1235
class TeamMenuMixin(PPANavigationMenuMixIn, CommonMenuLinks):
 
1236
    """Base class of team menus.
 
1237
 
 
1238
    You will need to override the team attribute if your menu subclass
 
1239
    has the view as its context object.
 
1240
    """
 
1241
 
 
1242
    def profile(self):
 
1243
        target = ''
 
1244
        text = 'Overview'
 
1245
        return Link(target, text)
 
1246
 
 
1247
    @enabled_with_permission('launchpad.Edit')
 
1248
    def edit(self):
 
1249
        target = '+edit'
 
1250
        text = 'Change details'
 
1251
        return Link(target, text, icon='edit')
 
1252
 
 
1253
    @enabled_with_permission('launchpad.Edit')
 
1254
    def branding(self):
 
1255
        target = '+branding'
 
1256
        text = 'Change branding'
 
1257
        return Link(target, text, icon='edit')
 
1258
 
 
1259
    @enabled_with_permission('launchpad.Owner')
 
1260
    def reassign(self):
 
1261
        target = '+reassign'
 
1262
        text = 'Change owner'
 
1263
        summary = 'Change the owner of the team'
 
1264
        return Link(target, text, summary, icon='edit')
 
1265
 
 
1266
    @enabled_with_permission('launchpad.Moderate')
 
1267
    def delete(self):
 
1268
        target = '+delete'
 
1269
        text = 'Delete'
 
1270
        summary = 'Delete this team'
 
1271
        return Link(target, text, summary, icon='trash-icon')
 
1272
 
 
1273
    @enabled_with_permission('launchpad.View')
 
1274
    def members(self):
 
1275
        target = '+members'
 
1276
        text = 'Show all members'
 
1277
        return Link(target, text, icon='team')
 
1278
 
 
1279
    @enabled_with_permission('launchpad.Edit')
 
1280
    def received_invitations(self):
 
1281
        target = '+invitations'
 
1282
        text = 'Show received invitations'
 
1283
        return Link(target, text, icon='info')
 
1284
 
 
1285
    @enabled_with_permission('launchpad.Edit')
 
1286
    def add_member(self):
 
1287
        target = '+addmember'
 
1288
        text = 'Add member'
 
1289
        return Link(target, text, icon='add')
 
1290
 
 
1291
    @enabled_with_permission('launchpad.Edit')
 
1292
    def proposed_members(self):
 
1293
        target = '+editproposedmembers'
 
1294
        text = 'Approve or decline members'
 
1295
        return Link(target, text, icon='add')
 
1296
 
 
1297
    def map(self):
 
1298
        target = '+map'
 
1299
        text = 'View map and time zones'
 
1300
        return Link(target, text, icon='meeting')
 
1301
 
 
1302
    def add_my_teams(self):
 
1303
        target = '+add-my-teams'
 
1304
        text = 'Add one of my teams'
 
1305
        enabled = True
 
1306
        restricted = TeamSubscriptionPolicy.RESTRICTED
 
1307
        if self.person.subscriptionpolicy == restricted:
 
1308
            # This is a restricted team; users can't join.
 
1309
            enabled = False
 
1310
        return Link(target, text, icon='add', enabled=enabled)
 
1311
 
 
1312
    def memberships(self):
 
1313
        target = '+participation'
 
1314
        text = 'Show team participation'
 
1315
        return Link(target, text, icon='info')
 
1316
 
 
1317
    @enabled_with_permission('launchpad.View')
 
1318
    def mugshots(self):
 
1319
        target = '+mugshots'
 
1320
        text = 'Show member photos'
 
1321
        return Link(target, text, icon='team')
 
1322
 
 
1323
    def polls(self):
 
1324
        target = '+polls'
 
1325
        text = 'Show polls'
 
1326
        return Link(target, text, icon='info')
 
1327
 
 
1328
    @enabled_with_permission('launchpad.Edit')
 
1329
    def add_poll(self):
 
1330
        target = '+newpoll'
 
1331
        text = 'Create a poll'
 
1332
        return Link(target, text, icon='add')
 
1333
 
 
1334
    @enabled_with_permission('launchpad.Edit')
 
1335
    def editemail(self):
 
1336
        target = '+contactaddress'
 
1337
        text = 'Set contact address'
 
1338
        summary = (
 
1339
            'The address Launchpad uses to contact %s' %
 
1340
            self.person.displayname)
 
1341
        return Link(target, text, summary, icon='edit')
 
1342
 
 
1343
    @enabled_with_permission('launchpad.Moderate')
 
1344
    def configure_mailing_list(self):
 
1345
        target = '+mailinglist'
 
1346
        mailing_list = self.person.mailing_list
 
1347
        if mailing_list is not None:
 
1348
            text = 'Configure mailing list'
 
1349
            icon = 'edit'
 
1350
        else:
 
1351
            text = 'Create a mailing list'
 
1352
            icon = 'add'
 
1353
        summary = (
 
1354
            'The mailing list associated with %s' % self.context.displayname)
 
1355
        return Link(target, text, summary, icon=icon)
 
1356
 
 
1357
    @enabled_with_active_mailing_list
 
1358
    @enabled_with_permission('launchpad.Edit')
 
1359
    def moderate_mailing_list(self):
 
1360
        target = '+mailinglist-moderate'
 
1361
        text = 'Moderate mailing list'
 
1362
        summary = (
 
1363
            'The mailing list associated with %s' % self.context.displayname)
 
1364
        return Link(target, text, summary, icon='edit')
 
1365
 
 
1366
    @enabled_with_permission('launchpad.Edit')
 
1367
    def editlanguages(self):
 
1368
        target = '+editlanguages'
 
1369
        text = 'Set preferred languages'
 
1370
        return Link(target, text, icon='edit')
 
1371
 
 
1372
    def leave(self):
 
1373
        enabled = True
 
1374
        if not userIsActiveTeamMember(self.person):
 
1375
            enabled = False
 
1376
        if self.person.teamowner == self.user:
 
1377
            # The owner cannot leave his team.
 
1378
            enabled = False
 
1379
        target = '+leave'
 
1380
        text = 'Leave the Team'
 
1381
        icon = 'remove'
 
1382
        return Link(target, text, icon=icon, enabled=enabled)
 
1383
 
 
1384
    def join(self):
 
1385
        enabled = True
 
1386
        person = self.person
 
1387
        if userIsActiveTeamMember(person):
 
1388
            enabled = False
 
1389
        elif (self.person.subscriptionpolicy ==
 
1390
              TeamSubscriptionPolicy.RESTRICTED):
 
1391
            # This is a restricted team; users can't join.
 
1392
            enabled = False
 
1393
        target = '+join'
 
1394
        text = 'Join the team'
 
1395
        icon = 'add'
 
1396
        return Link(target, text, icon=icon, enabled=enabled)
 
1397
 
 
1398
 
 
1399
class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
 
1400
 
 
1401
    usedfor = ITeam
 
1402
    facet = 'overview'
 
1403
    links = [
 
1404
        'edit',
 
1405
        'branding',
 
1406
        'common_edithomepage',
 
1407
        'members',
 
1408
        'mugshots',
 
1409
        'add_member',
 
1410
        'proposed_members',
 
1411
        'memberships',
 
1412
        'received_invitations',
 
1413
        'editemail',
 
1414
        'configure_mailing_list',
 
1415
        'moderate_mailing_list',
 
1416
        'editlanguages',
 
1417
        'map',
 
1418
        'polls',
 
1419
        'add_poll',
 
1420
        'join',
 
1421
        'leave',
 
1422
        'add_my_teams',
 
1423
        'reassign',
 
1424
        'projects',
 
1425
        'activate_ppa',
 
1426
        'maintained',
 
1427
        'ppa',
 
1428
        'related_software_summary',
 
1429
        'view_recipes',
 
1430
        'subscriptions',
 
1431
        'structural_subscriptions',
 
1432
        ]
 
1433
 
 
1434
 
 
1435
class TeamOverviewNavigationMenu(NavigationMenu, TeamMenuMixin):
 
1436
    """A top-level menu for navigation within a Team."""
 
1437
 
 
1438
    usedfor = ITeam
 
1439
    facet = 'overview'
 
1440
    links = ['profile', 'polls', 'members', 'ppas']
 
1441
 
 
1442
 
 
1443
class TeamMembershipView(LaunchpadView):
 
1444
    """The view behind ITeam/+members."""
 
1445
 
 
1446
    @cachedproperty
 
1447
    def label(self):
 
1448
        return smartquote('Members of "%s"' % self.context.displayname)
 
1449
 
 
1450
    @cachedproperty
 
1451
    def active_memberships(self):
 
1452
        """Current members of the team."""
 
1453
        return ActiveBatchNavigator(
 
1454
            self.context.member_memberships, self.request)
 
1455
 
 
1456
    @cachedproperty
 
1457
    def inactive_memberships(self):
 
1458
        """Former members of the team."""
 
1459
        return InactiveBatchNavigator(
 
1460
            self.context.getInactiveMemberships(), self.request)
 
1461
 
 
1462
    @cachedproperty
 
1463
    def invited_memberships(self):
 
1464
        """Other teams invited to become members of this team."""
 
1465
        return list(self.context.getInvitedMemberships())
 
1466
 
 
1467
    @cachedproperty
 
1468
    def proposed_memberships(self):
 
1469
        """Users who have requested to join this team."""
 
1470
        return list(self.context.getProposedMemberships())
 
1471
 
 
1472
    @property
 
1473
    def have_pending_members(self):
 
1474
        return self.proposed_memberships or self.invited_memberships
 
1475
 
 
1476
 
999
1477
class PersonSetActionNavigationMenu(RegistryCollectionActionMenuBase):
1000
1478
    """Action menu for `PeopleSearchView`."""
1001
1479
    usedfor = IPersonSet
1285
1763
        # Only the IPerson can be traversed to, so it provides the IAccount.
1286
1764
        # It also means that permissions are checked on IAccount, not IPerson.
1287
1765
        self.person = self.context
1288
 
        from lp.services.database.lpstorm import IMasterObject
 
1766
        from canonical.launchpad.interfaces.lpstorm import IMasterObject
1289
1767
        self.context = IMasterObject(self.context.account)
1290
1768
        # Set fields to be displayed.
1291
1769
        self.field_names = ['status', 'status_comment']
1350
1828
        self.updateContextFromData(data)
1351
1829
 
1352
1830
 
 
1831
def userIsActiveTeamMember(team):
 
1832
    """Return True if the user is an active member of this team."""
 
1833
    user = getUtility(ILaunchBag).user
 
1834
    if user is None:
 
1835
        return False
 
1836
    if not check_permission('launchpad.View', team):
 
1837
        return False
 
1838
    return user in team.activemembers
 
1839
 
 
1840
 
1353
1841
class PersonSpecWorkloadView(LaunchpadView):
1354
1842
    """View to render the specification workload for a person or team.
1355
1843
 
1367
1855
        This batch does not test for whether the person has specifications or
1368
1856
        not.
1369
1857
        """
 
1858
        assert self.context.isTeam, (
 
1859
            "PersonSpecWorkloadView.members can only be called on a team.")
1370
1860
        members = self.context.allmembers
1371
1861
        batch_nav = BatchNavigator(members, self.request, size=20)
1372
1862
        return batch_nav
1379
1869
    in a single table.
1380
1870
    """
1381
1871
 
1382
 
    page_title = 'Blueprint workload'
1383
 
 
1384
1872
    class PersonSpec:
1385
1873
        """One record from the workload list."""
1386
1874
 
1413
1901
        return self.context.specifications(filter=filter)
1414
1902
 
1415
1903
 
1416
 
def get_package_search_url(distributionsourcepackage, person_url,
1417
 
                           advanced=False, extra_params=None):
1418
 
    """Construct a default search URL for a distributionsourcepackage.
1419
 
 
1420
 
    Optional filter parameters can be specified as a dict with the
1421
 
    extra_params argument.
1422
 
    """
1423
 
    params = {
1424
 
        "field.distribution": distributionsourcepackage.distribution.name,
1425
 
        "field.sourcepackagename": distributionsourcepackage.name,
1426
 
        "search": "Search"}
1427
 
    if advanced:
1428
 
        params['advanced'] = '1'
1429
 
 
1430
 
    if extra_params is not None:
1431
 
        # We must UTF-8 encode searchtext to play nicely with
1432
 
        # urllib.urlencode, because it may contain non-ASCII characters.
1433
 
        if 'field.searchtext' in extra_params:
1434
 
            extra_params["field.searchtext"] = (
1435
 
                extra_params["field.searchtext"].encode("utf8"))
1436
 
 
1437
 
        params.update(extra_params)
1438
 
 
1439
 
    query_string = urllib.urlencode(sorted(params.items()), doseq=True)
1440
 
 
1441
 
    return person_url + '/+packagebugs-search?%s' % query_string
1442
 
 
1443
 
 
1444
 
class BugSubscriberPackageBugsOverView(LaunchpadView):
1445
 
 
 
1904
class BugSubscriberPackageBugsSearchListingView(BugTaskSearchListingView):
 
1905
    """Bugs reported on packages for a bug subscriber."""
 
1906
 
 
1907
    columns_to_show = ["id", "summary", "importance", "status"]
1446
1908
    page_title = 'Package bugs'
1447
1909
 
 
1910
    @property
 
1911
    def current_package(self):
 
1912
        """Get the package whose bugs are currently being searched."""
 
1913
        if not (
 
1914
            self.widgets['distribution'].hasValidInput() and
 
1915
            self.widgets['distribution'].getInputValue()):
 
1916
            raise UnexpectedFormData("A distribution is required")
 
1917
        if not (
 
1918
            self.widgets['sourcepackagename'].hasValidInput() and
 
1919
            self.widgets['sourcepackagename'].getInputValue()):
 
1920
            raise UnexpectedFormData("A sourcepackagename is required")
 
1921
 
 
1922
        distribution = self.widgets['distribution'].getInputValue()
 
1923
        return distribution.getSourcePackage(
 
1924
            self.widgets['sourcepackagename'].getInputValue())
 
1925
 
 
1926
    def search(self, searchtext=None):
 
1927
        distrosourcepackage = self.current_package
 
1928
        return BugTaskSearchListingView.search(
 
1929
            self, searchtext=searchtext, context=distrosourcepackage)
 
1930
 
 
1931
    def getMilestoneWidgetValues(self):
 
1932
        """See `BugTaskSearchListingView`.
 
1933
 
 
1934
        We return only the active milestones on the current distribution
 
1935
        since any others are irrelevant.
 
1936
        """
 
1937
        current_distro = self.current_package.distribution
 
1938
        vocabulary_registry = getVocabularyRegistry()
 
1939
        vocabulary = vocabulary_registry.get(current_distro, 'Milestone')
 
1940
 
 
1941
        return helpers.shortlist([
 
1942
            dict(title=milestone.title, value=milestone.token, checked=False)
 
1943
            for milestone in vocabulary],
 
1944
            longest_expected=10)
 
1945
 
1448
1946
    @cachedproperty
1449
1947
    def total_bug_counts(self):
1450
1948
        """Return the totals of each type of package bug count as a dict."""
1468
1966
        L = []
1469
1967
        package_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
1470
1968
            self.user, self.context.getBugSubscriberPackages())
1471
 
        person_url = canonical_url(self.context)
1472
1969
        for package_counts in package_counts:
1473
1970
            package = package_counts['package']
1474
1971
            L.append({
1475
1972
                'package_name': package.displayname,
1476
1973
                'package_search_url':
1477
 
                    get_package_search_url(package, person_url),
 
1974
                    self.getBugSubscriberPackageSearchURL(package),
1478
1975
                'open_bugs_count': package_counts['open'],
1479
 
                'open_bugs_url': self.getOpenBugsURL(package, person_url),
 
1976
                'open_bugs_url': self.getOpenBugsURL(package),
1480
1977
                'critical_bugs_count': package_counts['open_critical'],
1481
 
                'critical_bugs_url': self.getCriticalBugsURL(
1482
 
                    package, person_url),
 
1978
                'critical_bugs_url': self.getCriticalBugsURL(package),
1483
1979
                'high_bugs_count': package_counts['open_high'],
1484
 
                'high_bugs_url': self.getHighBugsURL(package, person_url),
 
1980
                'high_bugs_url': self.getHighBugsURL(package),
1485
1981
                'unassigned_bugs_count': package_counts['open_unassigned'],
1486
 
                'unassigned_bugs_url': self.getUnassignedBugsURL(
1487
 
                    package, person_url),
 
1982
                'unassigned_bugs_url': self.getUnassignedBugsURL(package),
1488
1983
                'inprogress_bugs_count': package_counts['open_inprogress'],
1489
 
                'inprogress_bugs_url': self.getInProgressBugsURL(
1490
 
                    package, person_url),
 
1984
                'inprogress_bugs_url': self.getInProgressBugsURL(package),
1491
1985
            })
1492
1986
 
1493
1987
        return sorted(L, key=itemgetter('package_name'))
1494
1988
 
1495
 
    def getOpenBugsURL(self, distributionsourcepackage, person_url):
 
1989
    def getOtherBugSubscriberPackageLinks(self):
 
1990
        """Return a list of the other packages for a bug subscriber.
 
1991
 
 
1992
        This excludes the current package.
 
1993
        """
 
1994
        current_package = self.current_package
 
1995
 
 
1996
        other_packages = [
 
1997
            package for package in self.context.getBugSubscriberPackages()
 
1998
            if package != current_package]
 
1999
 
 
2000
        package_links = []
 
2001
        for other_package in other_packages:
 
2002
            package_links.append({
 
2003
                'title': other_package.displayname,
 
2004
                'url': self.getBugSubscriberPackageSearchURL(other_package)})
 
2005
 
 
2006
        return package_links
 
2007
 
 
2008
    @cachedproperty
 
2009
    def person_url(self):
 
2010
        return canonical_url(self.context)
 
2011
 
 
2012
    def getBugSubscriberPackageSearchURL(self, distributionsourcepackage=None,
 
2013
                                      advanced=False, extra_params=None):
 
2014
        """Construct a default search URL for a distributionsourcepackage.
 
2015
 
 
2016
        Optional filter parameters can be specified as a dict with the
 
2017
        extra_params argument.
 
2018
        """
 
2019
        if distributionsourcepackage is None:
 
2020
            distributionsourcepackage = self.current_package
 
2021
 
 
2022
        params = {
 
2023
            "field.distribution": distributionsourcepackage.distribution.name,
 
2024
            "field.sourcepackagename": distributionsourcepackage.name,
 
2025
            "search": "Search"}
 
2026
 
 
2027
        if extra_params is not None:
 
2028
            # We must UTF-8 encode searchtext to play nicely with
 
2029
            # urllib.urlencode, because it may contain non-ASCII characters.
 
2030
            if 'field.searchtext' in extra_params:
 
2031
                extra_params["field.searchtext"] = (
 
2032
                    extra_params["field.searchtext"].encode("utf8"))
 
2033
 
 
2034
            params.update(extra_params)
 
2035
 
 
2036
        query_string = urllib.urlencode(sorted(params.items()), doseq=True)
 
2037
 
 
2038
        if advanced:
 
2039
            return (self.person_url + '/+packagebugs-search?advanced=1&%s'
 
2040
                    % query_string)
 
2041
        else:
 
2042
            return self.person_url + '/+packagebugs-search?%s' % query_string
 
2043
 
 
2044
    def getBugSubscriberPackageAdvancedSearchURL(self,
 
2045
                                              distributionsourcepackage=None):
 
2046
        """Build the advanced search URL for a distributionsourcepackage."""
 
2047
        return self.getBugSubscriberPackageSearchURL(advanced=True)
 
2048
 
 
2049
    def getOpenBugsURL(self, distributionsourcepackage):
1496
2050
        """Return the URL for open bugs on distributionsourcepackage."""
1497
2051
        status_params = {'field.status': []}
1498
2052
 
1499
2053
        for status in UNRESOLVED_BUGTASK_STATUSES:
1500
2054
            status_params['field.status'].append(status.title)
1501
2055
 
1502
 
        return get_package_search_url(
 
2056
        return self.getBugSubscriberPackageSearchURL(
1503
2057
            distributionsourcepackage=distributionsourcepackage,
1504
 
            person_url=person_url,
1505
2058
            extra_params=status_params)
1506
2059
 
1507
 
    def getCriticalBugsURL(self, distributionsourcepackage, person_url):
 
2060
    def getCriticalBugsURL(self, distributionsourcepackage):
1508
2061
        """Return the URL for critical bugs on distributionsourcepackage."""
1509
2062
        critical_bugs_params = {
1510
2063
            'field.status': [], 'field.importance': "Critical"}
1512
2065
        for status in UNRESOLVED_BUGTASK_STATUSES:
1513
2066
            critical_bugs_params["field.status"].append(status.title)
1514
2067
 
1515
 
        return get_package_search_url(
 
2068
        return self.getBugSubscriberPackageSearchURL(
1516
2069
            distributionsourcepackage=distributionsourcepackage,
1517
 
            person_url=person_url,
1518
2070
            extra_params=critical_bugs_params)
1519
2071
 
1520
 
    def getHighBugsURL(self, distributionsourcepackage, person_url):
 
2072
    def getHighBugsURL(self, distributionsourcepackage):
1521
2073
        """Return URL for high bugs on distributionsourcepackage."""
1522
2074
        high_bugs_params = {
1523
2075
            'field.status': [], 'field.importance': "High"}
1525
2077
        for status in UNRESOLVED_BUGTASK_STATUSES:
1526
2078
            high_bugs_params["field.status"].append(status.title)
1527
2079
 
1528
 
        return get_package_search_url(
 
2080
        return self.getBugSubscriberPackageSearchURL(
1529
2081
            distributionsourcepackage=distributionsourcepackage,
1530
 
            person_url=person_url,
1531
2082
            extra_params=high_bugs_params)
1532
2083
 
1533
 
    def getUnassignedBugsURL(self, distributionsourcepackage, person_url):
 
2084
    def getUnassignedBugsURL(self, distributionsourcepackage):
1534
2085
        """Return the URL for unassigned bugs on distributionsourcepackage."""
1535
2086
        unassigned_bugs_params = {
1536
2087
            "field.status": [], "field.unassigned": "on"}
1538
2089
        for status in UNRESOLVED_BUGTASK_STATUSES:
1539
2090
            unassigned_bugs_params["field.status"].append(status.title)
1540
2091
 
1541
 
        return get_package_search_url(
 
2092
        return self.getBugSubscriberPackageSearchURL(
1542
2093
            distributionsourcepackage=distributionsourcepackage,
1543
 
            person_url=person_url,
1544
2094
            extra_params=unassigned_bugs_params)
1545
2095
 
1546
 
    def getInProgressBugsURL(self, distributionsourcepackage, person_url):
 
2096
    def getInProgressBugsURL(self, distributionsourcepackage):
1547
2097
        """Return the URL for unassigned bugs on distributionsourcepackage."""
1548
2098
        inprogress_bugs_params = {"field.status": "In Progress"}
1549
2099
 
1550
 
        return get_package_search_url(
 
2100
        return self.getBugSubscriberPackageSearchURL(
1551
2101
            distributionsourcepackage=distributionsourcepackage,
1552
 
            person_url=person_url,
1553
2102
            extra_params=inprogress_bugs_params)
1554
2103
 
1555
 
 
1556
 
class BugSubscriberPackageBugsSearchListingView(BugTaskSearchListingView):
1557
 
    """Bugs reported on packages for a bug subscriber."""
1558
 
 
1559
 
    columns_to_show = ["id", "summary", "importance", "status"]
1560
 
    page_title = 'Package bugs'
1561
 
 
1562
 
    @property
1563
 
    def current_package(self):
1564
 
        """Get the package whose bugs are currently being searched."""
1565
 
        if not (
1566
 
            self.widgets['distribution'].hasValidInput() and
1567
 
            self.widgets['distribution'].getInputValue()):
1568
 
            raise UnexpectedFormData("A distribution is required")
1569
 
        if not (
1570
 
            self.widgets['sourcepackagename'].hasValidInput() and
1571
 
            self.widgets['sourcepackagename'].getInputValue()):
1572
 
            raise UnexpectedFormData("A sourcepackagename is required")
1573
 
 
1574
 
        distribution = self.widgets['distribution'].getInputValue()
1575
 
        return distribution.getSourcePackage(
1576
 
            self.widgets['sourcepackagename'].getInputValue())
1577
 
 
1578
 
    def search(self, searchtext=None):
1579
 
        distrosourcepackage = self.current_package
1580
 
        return BugTaskSearchListingView.search(
1581
 
            self, searchtext=searchtext, context=distrosourcepackage)
1582
 
 
1583
 
    def getMilestoneWidgetValues(self):
1584
 
        """See `BugTaskSearchListingView`.
1585
 
 
1586
 
        We return only the active milestones on the current distribution
1587
 
        since any others are irrelevant.
1588
 
        """
1589
 
        current_distro = self.current_package.distribution
1590
 
        vocabulary_registry = getVocabularyRegistry()
1591
 
        vocabulary = vocabulary_registry.get(current_distro, 'Milestone')
1592
 
 
1593
 
        return shortlist([
1594
 
            dict(title=milestone.title, value=milestone.token, checked=False)
1595
 
            for milestone in vocabulary],
1596
 
            longest_expected=10)
1597
 
 
1598
 
    @cachedproperty
1599
 
    def person_url(self):
1600
 
        return canonical_url(self.context)
1601
 
 
1602
 
    def getBugSubscriberPackageSearchURL(self, distributionsourcepackage=None,
1603
 
                                         advanced=False, extra_params=None):
1604
 
        """Construct a default search URL for a distributionsourcepackage.
1605
 
 
1606
 
        Optional filter parameters can be specified as a dict with the
1607
 
        extra_params argument.
1608
 
        """
1609
 
        if distributionsourcepackage is None:
1610
 
            distributionsourcepackage = self.current_package
1611
 
        return get_package_search_url(
1612
 
            distributionsourcepackage, self.person_url, advanced,
1613
 
            extra_params)
1614
 
 
1615
 
    def getBugSubscriberPackageAdvancedSearchURL(self,
1616
 
                                              distributionsourcepackage=None):
1617
 
        """Build the advanced search URL for a distributionsourcepackage."""
1618
 
        return self.getBugSubscriberPackageSearchURL(advanced=True)
1619
 
 
1620
2104
    def shouldShowSearchWidgets(self):
1621
2105
        # XXX: Guilherme Salgado 2005-11-05:
1622
2106
        # It's not possible to search amongst the bugs on maintained
1628
2112
        return "Search bugs in %s" % self.current_package.displayname
1629
2113
 
1630
2114
    def getSimpleSearchURL(self):
1631
 
        return get_package_search_url(self.current_package, self.person_url)
 
2115
        return self.getBugSubscriberPackageSearchURL()
1632
2116
 
1633
2117
    @property
1634
2118
    def label(self):
1635
2119
        return self.getSearchPageHeading()
1636
2120
 
1637
 
    @property
1638
 
    def context_description(self):
1639
 
        """See `BugTaskSearchListingView`."""
1640
 
        return ("in %s related to %s" %
1641
 
                (self.current_package.displayname, self.context.displayname))
1642
 
 
1643
2121
 
1644
2122
class RelevantMilestonesMixin:
1645
2123
    """Mixin to narrow the milestone list to only relevant milestones."""
1700
2178
            assignee_params, subscriber_params, owner_params,
1701
2179
            commenter_params, prejoins=prejoins)
1702
2180
 
1703
 
    @property
1704
 
    def context_description(self):
1705
 
        """See `BugTaskSearchListingView`."""
1706
 
        return "related to %s" % self.context.displayname
1707
 
 
1708
2181
    def getSearchPageHeading(self):
1709
 
        return "Bugs %s" % self.context_description
 
2182
        return "Bugs related to %s" % self.context.displayname
1710
2183
 
1711
2184
    def getAdvancedSearchButtonLabel(self):
1712
 
        return "Search bugs %s" % self.context_description
 
2185
        return "Search bugs related to %s" % self.context.displayname
1713
2186
 
1714
2187
    def getSimpleSearchURL(self):
1715
2188
        return canonical_url(self.context, view_name="+bugs")
1719
2192
        return self.getSearchPageHeading()
1720
2193
 
1721
2194
 
1722
 
class PersonAffectingBugTaskSearchListingView(
1723
 
    RelevantMilestonesMixin, BugTaskSearchListingView):
1724
 
    """All bugs affecting someone."""
1725
 
 
1726
 
    columns_to_show = ["id", "summary", "bugtargetdisplayname",
1727
 
                       "importance", "status"]
1728
 
    view_name = '+affectingbugs'
1729
 
    page_title = 'Bugs affecting'   # The context is added externally.
1730
 
 
1731
 
    def searchUnbatched(self, searchtext=None, context=None,
1732
 
                        extra_params=None, prejoins=[]):
1733
 
        """Return the open bugs assigned to a person."""
1734
 
        if context is None:
1735
 
            context = self.context
1736
 
 
1737
 
        if extra_params is None:
1738
 
            extra_params = dict()
1739
 
        else:
1740
 
            extra_params = dict(extra_params)
1741
 
        extra_params['affected_user'] = context
1742
 
 
1743
 
        sup = super(PersonAffectingBugTaskSearchListingView, self)
1744
 
        return sup.searchUnbatched(
1745
 
            searchtext, context, extra_params, prejoins)
1746
 
 
1747
 
    def shouldShowAssigneeWidget(self):
1748
 
        """Should the assignee widget be shown on the advanced search page?"""
1749
 
        return False
1750
 
 
1751
 
    def shouldShowTeamPortlet(self):
1752
 
        """Should the team assigned bugs portlet be shown?"""
1753
 
        return True
1754
 
 
1755
 
    def shouldShowTagsCombinatorWidget(self):
1756
 
        """Should the tags combinator widget show on the search page?"""
1757
 
        return False
1758
 
 
1759
 
    @property
1760
 
    def context_description(self):
1761
 
        """See `BugTaskSearchListingView`."""
1762
 
        return "affecting %s" % self.context.displayname
1763
 
 
1764
 
    def getSearchPageHeading(self):
1765
 
        """The header for the search page."""
1766
 
        return "Bugs %s" % self.context_description
1767
 
 
1768
 
    def getAdvancedSearchButtonLabel(self):
1769
 
        """The Search button for the advanced search page."""
1770
 
        return "Search bugs %s" % self.context_description
1771
 
 
1772
 
    def getSimpleSearchURL(self):
1773
 
        """Return a URL that can be used as an href to the simple search."""
1774
 
        return canonical_url(self.context, view_name=self.view_name)
1775
 
 
1776
 
    @property
1777
 
    def label(self):
1778
 
        return self.getSearchPageHeading()
1779
 
 
1780
 
 
1781
2195
class PersonAssignedBugTaskSearchListingView(RelevantMilestonesMixin,
1782
2196
                                             BugTaskSearchListingView):
1783
2197
    """All bugs assigned to someone."""
1785
2199
    columns_to_show = ["id", "summary", "bugtargetdisplayname",
1786
2200
                       "importance", "status"]
1787
2201
    page_title = 'Assigned bugs'
1788
 
    view_name = '+assignedbugs'
1789
2202
 
1790
2203
    def searchUnbatched(self, searchtext=None, context=None,
1791
2204
                        extra_params=None, prejoins=[]):
1807
2220
        """Should the assignee widget be shown on the advanced search page?"""
1808
2221
        return False
1809
2222
 
1810
 
    def shouldShowTeamPortlet(self):
 
2223
    def shouldShowAssignedToTeamPortlet(self):
1811
2224
        """Should the team assigned bugs portlet be shown?"""
1812
2225
        return True
1813
2226
 
1815
2228
        """Should the tags combinator widget show on the search page?"""
1816
2229
        return False
1817
2230
 
1818
 
    @property
1819
 
    def context_description(self):
1820
 
        """See `BugTaskSearchListingView`."""
1821
 
        return "assigned to %s" % self.context.displayname
1822
 
 
1823
2231
    def getSearchPageHeading(self):
1824
2232
        """The header for the search page."""
1825
 
        return "Bugs %s" % self.context_description
 
2233
        return "Bugs assigned to %s" % self.context.displayname
1826
2234
 
1827
2235
    def getAdvancedSearchButtonLabel(self):
1828
2236
        """The Search button for the advanced search page."""
1829
 
        return "Search bugs %s" % self.context_description
 
2237
        return "Search bugs assigned to %s" % self.context.displayname
1830
2238
 
1831
2239
    def getSimpleSearchURL(self):
1832
2240
        """Return a URL that can be used as an href to the simple search."""
1861
2269
        return sup.searchUnbatched(
1862
2270
            searchtext, context, extra_params, prejoins)
1863
2271
 
1864
 
    @property
1865
 
    def context_description(self):
1866
 
        """See `BugTaskSearchListingView`."""
1867
 
        return "commented on by %s" % self.context.displayname
1868
 
 
1869
2272
    def getSearchPageHeading(self):
1870
2273
        """The header for the search page."""
1871
 
        return "Bugs %s" % self.context_description
 
2274
        return "Bugs commented on by %s" % self.context.displayname
1872
2275
 
1873
2276
    def getAdvancedSearchButtonLabel(self):
1874
2277
        """The Search button for the advanced search page."""
1875
 
        return "Search bugs %s" % self.context_description
 
2278
        return "Search bugs commented on by %s" % self.context.displayname
1876
2279
 
1877
2280
    def getSimpleSearchURL(self):
1878
2281
        """Return a URL that can be used as an href to the simple search."""
1910
2313
        return sup.searchUnbatched(
1911
2314
            searchtext, context, extra_params, prejoins)
1912
2315
 
1913
 
    @property
1914
 
    def context_description(self):
1915
 
        """See `BugTaskSearchListingView`."""
1916
 
        return "reported by %s" % self.context.displayname
1917
 
 
1918
2316
    def getSearchPageHeading(self):
1919
2317
        """The header for the search page."""
1920
 
        return "Bugs %s" % self.context_description
 
2318
        return "Bugs reported by %s" % self.context.displayname
1921
2319
 
1922
2320
    def getAdvancedSearchButtonLabel(self):
1923
2321
        """The Search button for the advanced search page."""
1924
 
        return "Search bugs %s" % self.context_description
 
2322
        return "Search bugs reported by %s" % self.context.displayname
1925
2323
 
1926
2324
    def getSimpleSearchURL(self):
1927
2325
        """Return a URL that can be used as an href to the simple search."""
1947
2345
    columns_to_show = ["id", "summary", "bugtargetdisplayname",
1948
2346
                       "importance", "status"]
1949
2347
    page_title = 'Subscribed bugs'
1950
 
    view_name = '+subscribedbugs'
1951
2348
 
1952
2349
    def searchUnbatched(self, searchtext=None, context=None,
1953
2350
                        extra_params=None, prejoins=[]):
1965
2362
        return sup.searchUnbatched(
1966
2363
            searchtext, context, extra_params, prejoins)
1967
2364
 
1968
 
    def shouldShowTeamPortlet(self):
1969
 
        """Should the team subscribed bugs portlet be shown?"""
1970
 
        return True
1971
 
 
1972
 
    @property
1973
 
    def context_description(self):
1974
 
        """See `BugTaskSearchListingView`."""
1975
 
        return "%s is subscribed to" % self.context.displayname
1976
 
 
1977
2365
    def getSearchPageHeading(self):
1978
2366
        """The header for the search page."""
1979
 
        return "Bugs %s" % self.context_description
 
2367
        return "Bugs %s is subscribed to" % self.context.displayname
1980
2368
 
1981
2369
    def getAdvancedSearchButtonLabel(self):
1982
2370
        """The Search button for the advanced search page."""
2305
2693
        return self.context.latestKarma().count() > 0
2306
2694
 
2307
2695
 
2308
 
class PersonView(LaunchpadView, FeedsMixin):
 
2696
class TeamJoinMixin:
 
2697
    """Mixin class for views related to joining teams."""
 
2698
 
 
2699
    @property
 
2700
    def user_can_subscribe_to_list(self):
 
2701
        """Can the prospective member subscribe to this team's mailing list?
 
2702
 
 
2703
        A user can subscribe to the list if the team has an active
 
2704
        mailing list, and if they do not already have a subscription.
 
2705
        """
 
2706
        if self.team_has_mailing_list:
 
2707
            # If we are already subscribed, then we can not subscribe again.
 
2708
            return not self.user_is_subscribed_to_list
 
2709
        else:
 
2710
            return False
 
2711
 
 
2712
    @property
 
2713
    def user_is_subscribed_to_list(self):
 
2714
        """Is the user subscribed to the team's mailing list?
 
2715
 
 
2716
        Subscriptions hang around even if the list is deactivated, etc.
 
2717
 
 
2718
        It is an error to ask if the user is subscribed to a mailing list
 
2719
        that doesn't exist.
 
2720
        """
 
2721
        if self.user is None:
 
2722
            return False
 
2723
 
 
2724
        mailing_list = self.context.mailing_list
 
2725
        assert mailing_list is not None, "This team has no mailing list."
 
2726
        has_subscription = bool(mailing_list.getSubscription(self.user))
 
2727
        return has_subscription
 
2728
 
 
2729
    @property
 
2730
    def team_has_mailing_list(self):
 
2731
        """Is the team mailing list available for subscription?"""
 
2732
        mailing_list = self.context.mailing_list
 
2733
        return mailing_list is not None and mailing_list.is_usable
 
2734
 
 
2735
    @property
 
2736
    def user_is_active_member(self):
 
2737
        """Return True if the user is an active member of this team."""
 
2738
        return userIsActiveTeamMember(self.context)
 
2739
 
 
2740
    @property
 
2741
    def user_is_proposed_member(self):
 
2742
        """Return True if the user is a proposed member of this team."""
 
2743
        if self.user is None:
 
2744
            return False
 
2745
        return self.user in self.context.proposedmembers
 
2746
 
 
2747
    @property
 
2748
    def user_can_request_to_leave(self):
 
2749
        """Return true if the user can request to leave this team.
 
2750
 
 
2751
        A given user can leave a team only if he's an active member.
 
2752
        """
 
2753
        return self.user_is_active_member
 
2754
 
 
2755
 
 
2756
class PersonView(LaunchpadView, FeedsMixin, TeamJoinMixin):
2309
2757
    """A View class used in almost all Person's pages."""
2310
2758
 
2311
2759
    @property
2381
2829
        if content is None:
2382
2830
            return None
2383
2831
        elif self.is_probationary_or_invalid_user:
2384
 
            # XXX: Is this really useful?  They can post links in many other
2385
 
            # places. -- mbp 2011-11-20.
2386
2832
            return cgi.escape(content)
2387
2833
        else:
2388
2834
            formatter = FormattersAPI
2389
 
            return formatter(content).markdown()
 
2835
            return formatter(content).text_to_html()
2390
2836
 
2391
2837
    @cachedproperty
2392
2838
    def recently_approved_members(self):
2447
2893
 
2448
2894
    @cachedproperty
2449
2895
    def openpolls(self):
2450
 
        assert self.context.is_team
 
2896
        assert self.context.isTeam()
2451
2897
        return IPollSubset(self.context).getOpenPolls()
2452
2898
 
2453
2899
    @cachedproperty
2454
2900
    def closedpolls(self):
2455
 
        assert self.context.is_team
 
2901
        assert self.context.isTeam()
2456
2902
        return IPollSubset(self.context).getClosedPolls()
2457
2903
 
2458
2904
    @cachedproperty
2459
2905
    def notyetopenedpolls(self):
2460
 
        assert self.context.is_team
 
2906
        assert self.context.isTeam()
2461
2907
        return IPollSubset(self.context).getNotYetOpenedPolls()
2462
2908
 
2463
2909
    @cachedproperty
2610
3056
 
2611
3057
    @property
2612
3058
    def should_show_polls_portlet(self):
2613
 
        # Circular imports.
2614
 
        from lp.registry.browser.team import TeamOverviewMenu
2615
3059
        menu = TeamOverviewMenu(self.context)
2616
3060
        return (
2617
3061
            self.has_current_polls or self.closedpolls
2620
3064
    @property
2621
3065
    def has_current_polls(self):
2622
3066
        """Return True if this team has any non-closed polls."""
2623
 
        assert self.context.is_team
 
3067
        assert self.context.isTeam()
2624
3068
        return bool(self.openpolls) or bool(self.notyetopenedpolls)
2625
3069
 
2626
3070
    def userIsOwner(self):
2671
3115
            EmailAddressVisibleState.PUBLIC, EmailAddressVisibleState.ALLOWED)
2672
3116
        if self.email_address_visibility.state in visible_states:
2673
3117
            emails = [self.context.preferredemail.email]
2674
 
            if not self.context.is_team:
 
3118
            if not self.context.isTeam():
2675
3119
                emails.extend(sorted(
2676
3120
                    email.email for email in self.context.validatedemails))
2677
3121
            return emails
2758
3202
 
2759
3203
        return False
2760
3204
 
2761
 
    @property
2762
 
    def time_zone_offset(self):
2763
 
        """Return a string with offset from UTC"""
2764
 
        return datetime.now(
2765
 
            pytz.timezone(self.context.time_zone)).strftime("%z")
2766
 
 
2767
3205
 
2768
3206
class PersonParticipationView(LaunchpadView):
2769
3207
    """View for the ~person/+participation page."""
2923
3361
        return self.state is EmailAddressVisibleState.ALLOWED
2924
3362
 
2925
3363
 
2926
 
class PersonIndexView(XRDSContentNegotiationMixin, PersonView,
2927
 
                      TeamJoinMixin):
 
3364
class PersonIndexView(XRDSContentNegotiationMixin, PersonView):
2928
3365
    """View class for person +index and +xrds pages."""
2929
3366
 
2930
3367
    xrds_template = ViewPageTemplateFile(
2952
3389
            return "%s does not use Launchpad" % context.displayname
2953
3390
 
2954
3391
    @cachedproperty
2955
 
    def page_description(self):
2956
 
        context = self.context
2957
 
        if context.is_valid_person_or_team:
2958
 
            return (
2959
 
                self.context.homepage_content
2960
 
                or self.context.teamdescription)
2961
 
        else:
2962
 
            return None
2963
 
 
2964
 
    @cachedproperty
2965
3392
    def enable_xrds_discovery(self):
2966
3393
        """Only enable discovery if person is OpenID enabled."""
2967
3394
        return self.is_delegated_identity
3042
3469
            return self.has_visible_location
3043
3470
 
3044
3471
 
 
3472
class TeamIndexView(PersonIndexView):
 
3473
    """The view class for the +index page.
 
3474
 
 
3475
    This class is needed, so an action menu that only applies to
 
3476
    teams can be displayed without showing up on the person index page.
 
3477
    """
 
3478
 
 
3479
    @property
 
3480
    def can_show_subteam_portlet(self):
 
3481
        """Only show the subteam portlet if there is info to display.
 
3482
 
 
3483
        Either the team is a member of another team, or there are
 
3484
        invitations to join a team, and the owner needs to see the
 
3485
        link so that the invitation can be accepted.
 
3486
        """
 
3487
        try:
 
3488
            return (self.context.super_teams.count() > 0
 
3489
                    or (self.context.open_membership_invitations
 
3490
                        and check_permission('launchpad.Edit', self.context)))
 
3491
        except AttributeError, e:
 
3492
            raise AssertionError(e)
 
3493
 
 
3494
    @property
 
3495
    def visibility_info(self):
 
3496
        if self.context.visibility == PersonVisibility.PRIVATE:
 
3497
            return 'Private team'
 
3498
        else:
 
3499
            return 'Public team'
 
3500
 
 
3501
    @property
 
3502
    def visibility_portlet_class(self):
 
3503
        """The portlet class for team visibility."""
 
3504
        if self.context.visibility == PersonVisibility.PUBLIC:
 
3505
            return 'portlet'
 
3506
        return 'portlet private'
 
3507
 
 
3508
    @property
 
3509
    def add_member_step_title(self):
 
3510
        """A string for setup_add_member_handler with escaped quotes."""
 
3511
        vocabulary_registry = getVocabularyRegistry()
 
3512
        vocabulary = vocabulary_registry.get(self.context, 'ValidTeamMember')
 
3513
        return vocabulary.step_title.replace("'", "\\'").replace('"', '\\"')
 
3514
 
 
3515
 
3045
3516
class PersonCodeOfConductEditView(LaunchpadView):
3046
3517
    """View for the ~person/+codesofconduct pages."""
3047
3518
 
3478
3949
    page_title = label
3479
3950
 
3480
3951
 
3481
 
class PersonRenameFormMixin(LaunchpadEditFormView):
3482
 
 
3483
 
    def setUpWidgets(self):
3484
 
        """See `LaunchpadViewForm`.
3485
 
 
3486
 
        Renames are prohibited if a person/team has an active PPA or an
3487
 
        active mailing list.
3488
 
        """
3489
 
        reason = self.context.checkRename()
3490
 
        if reason:
3491
 
            # This makes the field's widget display (i.e. read) only.
3492
 
            self.form_fields['name'].for_display = True
3493
 
        super(PersonRenameFormMixin, self).setUpWidgets()
3494
 
        if reason:
3495
 
            self.widgets['name'].hint = reason
3496
 
 
3497
 
 
3498
 
class PersonEditView(PersonRenameFormMixin, BasePersonEditView):
 
3952
class PersonEditView(BasePersonEditView):
3499
3953
    """The Person 'Edit' page."""
3500
3954
 
3501
3955
    field_names = ['displayname', 'name', 'mugshot', 'homepage_content',
3512
3966
    # account with full knowledge of the consequences.
3513
3967
    i_know_this_is_an_openid_security_issue_input = None
3514
3968
 
 
3969
    def setUpWidgets(self):
 
3970
        """See `LaunchpadViewForm`.
 
3971
 
 
3972
        When a user has a PPA renames are prohibited.
 
3973
        """
 
3974
        has_ppa_with_published_packages = (
 
3975
            getUtility(IArchiveSet).getPPAOwnedByPerson(
 
3976
                self.context, has_packages=True,
 
3977
                statuses=[ArchiveStatus.ACTIVE,
 
3978
                          ArchiveStatus.DELETING]) is not None)
 
3979
        if has_ppa_with_published_packages:
 
3980
            # This makes the field's widget display (i.e. read) only.
 
3981
            self.form_fields['name'].for_display = True
 
3982
        super(PersonEditView, self).setUpWidgets()
 
3983
        if has_ppa_with_published_packages:
 
3984
            self.widgets['name'].hint = _(
 
3985
                'This user has an active PPA with packages published and '
 
3986
                'may not be renamed.')
 
3987
 
3515
3988
    def validate(self, data):
3516
3989
        """If the name changed, warn the user about the implications."""
3517
3990
        new_name = data.get('name')
3518
3991
        bypass_check = self.request.form_ng.getOne(
3519
3992
            'i_know_this_is_an_openid_security_issue', 0)
3520
 
        if (new_name and new_name != self.context.name and not bypass_check):
 
3993
        if (new_name and new_name != self.context.name and
 
3994
            len(self.unknown_trust_roots_user_logged_in) > 0
 
3995
            and not bypass_check):
3521
3996
            # Warn the user that they might shoot themselves in the foot.
3522
3997
            self.setFieldError('name', structured(dedent('''
3523
3998
            <div class="inline-warning">
3530
4005
                    >https://help.launchpad.net/OpenID#rename-account</a>
3531
4006
                  for more information.
3532
4007
              </p>
 
4008
              <p> You may have used your identifier on the following
 
4009
                  sites:<br> %s.
 
4010
              </p>
3533
4011
              <p>If you click 'Save' again, we will rename your account
3534
4012
                 anyway.
3535
4013
              </p>
3536
 
            </div>'''),))
 
4014
            </div>'''),
 
4015
             ", ".join(self.unknown_trust_roots_user_logged_in)))
3537
4016
            self.i_know_this_is_an_openid_security_issue_input = dedent("""\
3538
4017
                <input type="hidden"
3539
4018
                       id="i_know_this_is_an_openid_security_issue"
3540
4019
                       name="i_know_this_is_an_openid_security_issue"
3541
4020
                       value="1">""")
3542
4021
 
 
4022
    @cachedproperty
 
4023
    def unknown_trust_roots_user_logged_in(self):
 
4024
        """The unknown trust roots the user has logged in using OpenID.
 
4025
 
 
4026
        We assume that they logged in using their delegated profile OpenID,
 
4027
        since that's the one we advertise.
 
4028
        """
 
4029
        identifier = IOpenIDPersistentIdentity(self.context)
 
4030
        unknown_trust_root_login_records = list(
 
4031
            getUtility(IOpenIDRPSummarySet).getByIdentifier(
 
4032
                identifier.openid_identity_url, True))
 
4033
        return sorted([
 
4034
            record.trust_root
 
4035
            for record in unknown_trust_root_login_records])
 
4036
 
3543
4037
    @action(_("Save Changes"), name="save")
3544
4038
    def action_save(self, action, data):
3545
4039
        # XXX: BradCrittenden 2010-09-10 bug=634878: Find a cleaner solution
3562
4056
    schema = IPerson
3563
4057
 
3564
4058
 
 
4059
class TeamJoinForm(Interface):
 
4060
    """Schema for team join."""
 
4061
    mailinglist_subscribe = Bool(
 
4062
        title=_("Subscribe me to this team's mailing list"),
 
4063
        required=True, default=True)
 
4064
 
 
4065
 
 
4066
class TeamJoinView(LaunchpadFormView, TeamJoinMixin):
 
4067
    """A view class for joining a team."""
 
4068
    schema = TeamJoinForm
 
4069
 
 
4070
    @property
 
4071
    def label(self):
 
4072
        return 'Join ' + cgi.escape(self.context.displayname)
 
4073
 
 
4074
    page_title = label
 
4075
 
 
4076
    def setUpWidgets(self):
 
4077
        super(TeamJoinView, self).setUpWidgets()
 
4078
        if 'mailinglist_subscribe' in self.field_names:
 
4079
            widget = self.widgets['mailinglist_subscribe']
 
4080
            widget.setRenderedValue(self.user_wants_list_subscriptions)
 
4081
 
 
4082
    @property
 
4083
    def field_names(self):
 
4084
        """See `LaunchpadFormView`.
 
4085
 
 
4086
        If the user can subscribe to the mailing list then include the
 
4087
        mailinglist subscription checkbox otherwise remove it.
 
4088
        """
 
4089
        if self.user_can_subscribe_to_list:
 
4090
            return ['mailinglist_subscribe']
 
4091
        else:
 
4092
            return []
 
4093
 
 
4094
    @property
 
4095
    def join_allowed(self):
 
4096
        """Is the logged in user allowed to join this team?
 
4097
 
 
4098
        The answer is yes if this team's subscription policy is not RESTRICTED
 
4099
        and this team's visibility is either None or PUBLIC.
 
4100
        """
 
4101
        # Joining a moderated team will put you on the proposed_members
 
4102
        # list. If it is a private team, you are not allowed to view the
 
4103
        # proposed_members attribute until you are an active member;
 
4104
        # therefore, it would look like the join button is broken. Either
 
4105
        # private teams should always have a restricted subscription policy,
 
4106
        # or we need a more complicated permission model.
 
4107
        if not (self.context.visibility is None
 
4108
                or self.context.visibility == PersonVisibility.PUBLIC):
 
4109
            return False
 
4110
 
 
4111
        restricted = TeamSubscriptionPolicy.RESTRICTED
 
4112
        return self.context.subscriptionpolicy != restricted
 
4113
 
 
4114
    @property
 
4115
    def user_can_request_to_join(self):
 
4116
        """Can the logged in user request to join this team?
 
4117
 
 
4118
        The user can request if he's allowed to join this team and if he's
 
4119
        not yet an active member of this team.
 
4120
        """
 
4121
        if not self.join_allowed:
 
4122
            return False
 
4123
        return not (self.user_is_active_member or
 
4124
                    self.user_is_proposed_member)
 
4125
 
 
4126
    @property
 
4127
    def user_wants_list_subscriptions(self):
 
4128
        """Is the user interested in subscribing to mailing lists?"""
 
4129
        return (self.user.mailing_list_auto_subscribe_policy !=
 
4130
                MailingListAutoSubscribePolicy.NEVER)
 
4131
 
 
4132
    @property
 
4133
    def team_is_moderated(self):
 
4134
        """Is this team a moderated team?
 
4135
 
 
4136
        Return True if the team's subscription policy is MODERATED.
 
4137
        """
 
4138
        policy = self.context.subscriptionpolicy
 
4139
        return policy == TeamSubscriptionPolicy.MODERATED
 
4140
 
 
4141
    @property
 
4142
    def next_url(self):
 
4143
        return canonical_url(self.context)
 
4144
 
 
4145
    @property
 
4146
    def cancel_url(self):
 
4147
        return canonical_url(self.context)
 
4148
 
 
4149
    @action(_("Join"), name="join")
 
4150
    def action_save(self, action, data):
 
4151
        response = self.request.response
 
4152
 
 
4153
        if self.user_can_request_to_join:
 
4154
            # Shut off mailing list auto-subscription - we want direct
 
4155
            # control over it.
 
4156
            self.user.join(self.context, may_subscribe_to_list=False)
 
4157
 
 
4158
            if self.team_is_moderated:
 
4159
                response.addInfoNotification(
 
4160
                    _('Your request to join ${team} is awaiting '
 
4161
                      'approval.',
 
4162
                      mapping={'team': self.context.displayname}))
 
4163
            else:
 
4164
                response.addInfoNotification(
 
4165
                    _('You have successfully joined ${team}.',
 
4166
                      mapping={'team': self.context.displayname}))
 
4167
            if data.get('mailinglist_subscribe', False):
 
4168
                self._subscribeToList(response)
 
4169
 
 
4170
        else:
 
4171
            response.addErrorNotification(
 
4172
                _('You cannot join ${team}.',
 
4173
                  mapping={'team': self.context.displayname}))
 
4174
 
 
4175
    def _subscribeToList(self, response):
 
4176
        """Subscribe the user to the team's mailing list."""
 
4177
 
 
4178
        if self.user_can_subscribe_to_list:
 
4179
            # 'user_can_subscribe_to_list' should have dealt with
 
4180
            # all of the error cases.
 
4181
            self.context.mailing_list.subscribe(self.user)
 
4182
 
 
4183
            if self.team_is_moderated:
 
4184
                response.addInfoNotification(
 
4185
                    _('Your mailing list subscription is '
 
4186
                      'awaiting approval.'))
 
4187
            else:
 
4188
                response.addInfoNotification(
 
4189
                    structured(
 
4190
                        _("You have been subscribed to this "
 
4191
                          "team&#x2019;s mailing list.")))
 
4192
        else:
 
4193
            # A catch-all case, perhaps from stale or mangled
 
4194
            # form data.
 
4195
            response.addErrorNotification(
 
4196
                _('Mailing list subscription failed.'))
 
4197
 
 
4198
 
 
4199
class TeamAddMyTeamsView(LaunchpadFormView):
 
4200
    """Propose/add to this team any team that you're an administrator of."""
 
4201
 
 
4202
    page_title = 'Propose/add one of your teams to another one'
 
4203
    custom_widget('teams', LabeledMultiCheckBoxWidget)
 
4204
 
 
4205
    def initialize(self):
 
4206
        context = self.context
 
4207
        if context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
 
4208
            self.label = 'Propose these teams as members'
 
4209
        else:
 
4210
            self.label = 'Add these teams to %s' % context.displayname
 
4211
        self.next_url = canonical_url(context)
 
4212
        super(TeamAddMyTeamsView, self).initialize()
 
4213
 
 
4214
    def setUpFields(self):
 
4215
        terms = []
 
4216
        for team in self.candidate_teams:
 
4217
            text = structured(
 
4218
                '<a href="%s">%s</a>', canonical_url(team), team.displayname)
 
4219
            terms.append(SimpleTerm(team, team.name, text))
 
4220
        self.form_fields = FormFields(
 
4221
            List(__name__='teams',
 
4222
                 title=_(''),
 
4223
                 value_type=Choice(vocabulary=SimpleVocabulary(terms)),
 
4224
                 required=False),
 
4225
            render_context=self.render_context)
 
4226
 
 
4227
    def setUpWidgets(self, context=None):
 
4228
        super(TeamAddMyTeamsView, self).setUpWidgets(context)
 
4229
        self.widgets['teams'].display_label = False
 
4230
 
 
4231
    @cachedproperty
 
4232
    def candidate_teams(self):
 
4233
        """Return the set of teams that can be added/proposed for the context.
 
4234
 
 
4235
        We return only teams that the user can administer, that aren't already
 
4236
        a member in the context or that the context isn't a member of. (Of
 
4237
        course, the context is also omitted.)
 
4238
        """
 
4239
        candidates = []
 
4240
        for team in self.user.getAdministratedTeams():
 
4241
            if team == self.context:
 
4242
                continue
 
4243
            elif team.visibility != PersonVisibility.PUBLIC:
 
4244
                continue
 
4245
            elif team in self.context.activemembers:
 
4246
                # The team is already a member of the context object.
 
4247
                continue
 
4248
            elif self.context.hasParticipationEntryFor(team):
 
4249
                # The context object is a member/submember of the team.
 
4250
                continue
 
4251
            candidates.append(team)
 
4252
        return candidates
 
4253
 
 
4254
    @property
 
4255
    def cancel_url(self):
 
4256
        """The return URL."""
 
4257
        return canonical_url(self.context)
 
4258
 
 
4259
    def validate(self, data):
 
4260
        if len(data.get('teams', [])) == 0:
 
4261
            self.setFieldError('teams',
 
4262
                               'Please select the team(s) you want to be '
 
4263
                               'member(s) of this team.')
 
4264
 
 
4265
    def hasCandidates(self, action):
 
4266
        """Return whether the user has teams to propose."""
 
4267
        return len(self.candidate_teams) > 0
 
4268
 
 
4269
    @action(_("Continue"), name="continue", condition=hasCandidates)
 
4270
    def continue_action(self, action, data):
 
4271
        """Make the selected teams join this team."""
 
4272
        context = self.context
 
4273
        is_admin = check_permission('launchpad.Admin', context)
 
4274
        membership_set = getUtility(ITeamMembershipSet)
 
4275
        proposed_team_names = []
 
4276
        added_team_names = []
 
4277
        accepted_invite_team_names = []
 
4278
        membership_set = getUtility(ITeamMembershipSet)
 
4279
        for team in data['teams']:
 
4280
            membership = membership_set.getByPersonAndTeam(team, context)
 
4281
            if (membership is not None
 
4282
                and membership.status == TeamMembershipStatus.INVITED):
 
4283
                team.acceptInvitationToBeMemberOf(
 
4284
                    context,
 
4285
                    'Accepted an already pending invitation while trying to '
 
4286
                    'propose the team for membership.')
 
4287
                accepted_invite_team_names.append(team.displayname)
 
4288
            elif is_admin:
 
4289
                context.addMember(team, reviewer=self.user)
 
4290
                added_team_names.append(team.displayname)
 
4291
            else:
 
4292
                team.join(context, requester=self.user)
 
4293
                membership = membership_set.getByPersonAndTeam(team, context)
 
4294
                if membership.status == TeamMembershipStatus.PROPOSED:
 
4295
                    proposed_team_names.append(team.displayname)
 
4296
                elif membership.status == TeamMembershipStatus.APPROVED:
 
4297
                    added_team_names.append(team.displayname)
 
4298
                else:
 
4299
                    raise AssertionError(
 
4300
                        'Unexpected membership status (%s) for %s.'
 
4301
                        % (membership.status.name, team.name))
 
4302
        full_message = ''
 
4303
        for team_names, message in (
 
4304
            (proposed_team_names, 'proposed to this team.'),
 
4305
            (added_team_names, 'added to this team.'),
 
4306
            (accepted_invite_team_names,
 
4307
             'added to this team because of an existing invite.'),
 
4308
            ):
 
4309
            if len(team_names) == 0:
 
4310
                continue
 
4311
            elif len(team_names) == 1:
 
4312
                verb = 'has been'
 
4313
                team_string = team_names[0]
 
4314
            elif len(team_names) > 1:
 
4315
                verb = 'have been'
 
4316
                team_string = (
 
4317
                    ', '.join(team_names[:-1]) + ' and ' + team_names[-1])
 
4318
            full_message += '%s %s %s' % (team_string, verb, message)
 
4319
        self.request.response.addInfoNotification(full_message)
 
4320
 
 
4321
 
 
4322
class TeamLeaveView(LaunchpadFormView, TeamJoinMixin):
 
4323
    schema = Interface
 
4324
 
 
4325
    @property
 
4326
    def label(self):
 
4327
        return 'Leave ' + cgi.escape(self.context.displayname)
 
4328
 
 
4329
    page_title = label
 
4330
 
 
4331
    @property
 
4332
    def cancel_url(self):
 
4333
        return canonical_url(self.context)
 
4334
 
 
4335
    next_url = cancel_url
 
4336
 
 
4337
    @action(_("Leave"), name="leave")
 
4338
    def action_save(self, action, data):
 
4339
        if self.user_can_request_to_leave:
 
4340
            self.user.leave(self.context)
 
4341
 
 
4342
 
3565
4343
class PersonEditEmailsView(LaunchpadFormView):
3566
4344
    """A view for editing a person's email settings.
3567
4345
 
3933
4711
 
3934
4712
        # XXX j.c.sackett 2010-09-15 bug=628247, 576757 There is a validation
3935
4713
        # system set up for this that is almost identical in
3936
 
        # lp.app.validators.validation, called
 
4714
        # canonical.launchpad.interfaces.validation, called
3937
4715
        # _check_email_available or similar. It would be really nice if we
3938
4716
        # could merge that code somehow with this.
3939
4717
        email = getUtility(IEmailAddressSet).getByEmail(newemail)
4066
4844
        self.next_url = self.action_url
4067
4845
 
4068
4846
 
 
4847
class TeamMugshotView(LaunchpadView):
 
4848
    """A view for the team mugshot (team photo) page"""
 
4849
 
 
4850
    label = "Member photos"
 
4851
    batch_size = config.launchpad.mugshot_batch_size
 
4852
 
 
4853
    def initialize(self):
 
4854
        """Cache images to avoid dying from a million cuts."""
 
4855
        getUtility(IPersonSet).cacheBrandingForPeople(
 
4856
            self.members.currentBatch())
 
4857
 
 
4858
    @cachedproperty
 
4859
    def members(self):
 
4860
        """Get a batch of all members in the team."""
 
4861
        batch_nav = BatchNavigator(
 
4862
            self.context.allmembers, self.request, size=self.batch_size)
 
4863
        return batch_nav
 
4864
 
 
4865
 
 
4866
class TeamReassignmentView(ObjectReassignmentView):
 
4867
 
 
4868
    ownerOrMaintainerAttr = 'teamowner'
 
4869
    schema = ITeamReassignment
 
4870
 
 
4871
    def __init__(self, context, request):
 
4872
        super(TeamReassignmentView, self).__init__(context, request)
 
4873
        self.callback = self._addOwnerAsMember
 
4874
 
 
4875
    def validateOwner(self, new_owner):
 
4876
        """Display error if the owner is not valid.
 
4877
 
 
4878
        Called by ObjectReassignmentView.validate().
 
4879
        """
 
4880
        if self.context.inTeam(new_owner):
 
4881
            path = self.context.findPathToTeam(new_owner)
 
4882
            if len(path) == 1:
 
4883
                relationship = 'a direct member'
 
4884
                path_string = ''
 
4885
            else:
 
4886
                relationship = 'an indirect member'
 
4887
                full_path = [self.context] + path
 
4888
                path_string = '(%s)' % '&rArr;'.join(
 
4889
                    team.displayname for team in full_path)
 
4890
            error = structured(
 
4891
                'Circular team memberships are not allowed. '
 
4892
                '%(new)s cannot be the new team owner, since %(context)s '
 
4893
                'is %(relationship)s of %(new)s. '
 
4894
                '<span style="white-space: nowrap">%(path)s</span>'
 
4895
                % dict(new=new_owner.displayname,
 
4896
                        context=self.context.displayname,
 
4897
                        relationship=relationship,
 
4898
                        path=path_string))
 
4899
            self.setFieldError(self.ownerOrMaintainerName, error)
 
4900
 
 
4901
    @property
 
4902
    def contextName(self):
 
4903
        return self.context.displayname
 
4904
 
 
4905
    def _addOwnerAsMember(self, team, oldOwner, newOwner):
 
4906
        """Add the new and the old owners as administrators of the team.
 
4907
 
 
4908
        When a user creates a new team, he is added as an administrator of
 
4909
        that team. To be consistent with this, we must make the new owner an
 
4910
        administrator of the team. This rule is ignored only if the new owner
 
4911
        is an inactive member of the team, as that means he's not interested
 
4912
        in being a member. The same applies to the old owner.
 
4913
        """
 
4914
        # Both new and old owners won't be added as administrators of the team
 
4915
        # only if they're inactive members. If they're either active or
 
4916
        # proposed members they'll be made administrators of the team.
 
4917
        if newOwner not in team.inactivemembers:
 
4918
            team.addMember(
 
4919
                newOwner, reviewer=oldOwner,
 
4920
                status=TeamMembershipStatus.ADMIN, force_team_add=True)
 
4921
        if oldOwner not in team.inactivemembers:
 
4922
            team.addMember(
 
4923
                oldOwner, reviewer=oldOwner,
 
4924
                status=TeamMembershipStatus.ADMIN, force_team_add=True)
 
4925
 
 
4926
 
4069
4927
class PersonLatestQuestionsView(LaunchpadFormView):
4070
4928
    """View used by the porlet displaying the latest questions made by
4071
4929
    a person.
4237
5095
        return 'Projects for which %s is an answer contact' % (
4238
5096
            self.context.displayname)
4239
5097
 
4240
 
    page_title = label
4241
 
 
4242
5098
    @cachedproperty
4243
5099
    def direct_question_targets(self):
4244
5100
        """List of targets that the IPerson is a direct answer contact.
4310
5166
        return Link('+subscribedquestions', text, summary, icon='question')
4311
5167
 
4312
5168
 
4313
 
class BaseWithStats:
4314
 
    """An ISourcePackageRelease or a ISourcePackagePublishingHistory,
4315
 
    with extra stats added.
4316
 
 
4317
 
    """
4318
 
 
 
5169
class SourcePackageReleaseWithStats:
 
5170
    """An ISourcePackageRelease, with extra stats added."""
 
5171
 
 
5172
    implements(ISourcePackageRelease)
 
5173
    delegates(ISourcePackageRelease)
4319
5174
    failed_builds = None
4320
5175
    needs_building = None
4321
5176
 
4322
 
    def __init__(self, object, open_bugs, open_questions,
 
5177
    def __init__(self, sourcepackage_release, open_bugs, open_questions,
4323
5178
                 failed_builds, needs_building):
4324
 
        self.context = object
 
5179
        self.context = sourcepackage_release
4325
5180
        self.open_bugs = open_bugs
4326
5181
        self.open_questions = open_questions
4327
5182
        self.failed_builds = failed_builds
4328
5183
        self.needs_building = needs_building
4329
5184
 
4330
5185
 
4331
 
class SourcePackageReleaseWithStats(BaseWithStats):
4332
 
    """An ISourcePackageRelease, with extra stats added."""
4333
 
 
4334
 
    implements(ISourcePackageRelease)
4335
 
    delegates(ISourcePackageRelease)
4336
 
 
4337
 
 
4338
 
class SourcePackagePublishingHistoryWithStats(BaseWithStats):
4339
 
    """An ISourcePackagePublishingHistory, with extra stats added."""
4340
 
 
4341
 
    implements(ISourcePackagePublishingHistory)
4342
 
    delegates(ISourcePackagePublishingHistory)
4343
 
 
4344
 
 
4345
5186
class PersonRelatedSoftwareView(LaunchpadView):
4346
5187
    """View for +related-software."""
4347
5188
    implements(IPersonRelatedSoftwareMenu)
4467
5308
        header_message = self._tableHeaderMessage(packages.count())
4468
5309
        return results, header_message
4469
5310
 
4470
 
    def _getDecoratedPublishingsSummary(self, publishings):
4471
 
        """Helper returning decorated publishings for the summary page.
4472
 
 
4473
 
        :param publishings: A SelectResults that contains the query
4474
 
        :return: A tuple of (publishings, header_message).
4475
 
 
4476
 
        The publishings returned are limited to self.max_results_to_display
4477
 
        and decorated with the stats required in the page template.
4478
 
        The header_message is the text to be displayed at the top of the
4479
 
        results table in the template.
4480
 
        """
4481
 
        # This code causes two SQL queries to be generated.
4482
 
        results = self._addStatsToPublishings(
4483
 
            publishings[:self.max_results_to_display])
4484
 
        header_message = self._tableHeaderMessage(publishings.count())
4485
 
        return results, header_message
4486
 
 
4487
5311
    @property
4488
5312
    def latest_uploaded_ppa_packages_with_stats(self):
4489
5313
        """Return the sourcepackagereleases uploaded to PPAs by this person.
4515
5339
        self.uploaded_packages_header_message = header_message
4516
5340
        return results
4517
5341
 
4518
 
    @property
4519
 
    def latest_synchronised_publishings_with_stats(self):
4520
 
        """Return the latest synchronised publishings, including stats.
4521
 
 
4522
 
        """
4523
 
        publishings = self.context.getLatestSynchronisedPublishings()
4524
 
        results, header_message = self._getDecoratedPublishingsSummary(
4525
 
            publishings)
4526
 
        self.synchronised_packages_header_message = header_message
4527
 
        return results
4528
 
 
4529
5342
    def _calculateBuildStats(self, package_releases):
4530
5343
        """Calculate failed builds and needs_build state.
4531
5344
 
4587
5400
                needs_build_by_package[package])
4588
5401
            for package in package_releases]
4589
5402
 
4590
 
    def _addStatsToPublishings(self, publishings):
4591
 
        """Add stats to the given publishings, and return them."""
4592
 
        filtered_spphs = [
4593
 
            spph for spph in publishings if
4594
 
            check_permission('launchpad.View', spph)]
4595
 
        distro_packages = [
4596
 
            spph.meta_sourcepackage.distribution_sourcepackage
4597
 
            for spph in filtered_spphs]
4598
 
        package_bug_counts = getUtility(IBugTaskSet).getBugCountsForPackages(
4599
 
            self.user, distro_packages)
4600
 
        open_bugs = {}
4601
 
        for bug_count in package_bug_counts:
4602
 
            distro_package = bug_count['package']
4603
 
            open_bugs[distro_package] = bug_count['open']
4604
 
 
4605
 
        question_set = getUtility(IQuestionSet)
4606
 
        package_question_counts = question_set.getOpenQuestionCountByPackages(
4607
 
            distro_packages)
4608
 
 
4609
 
        builds_by_package, needs_build_by_package = self._calculateBuildStats(
4610
 
            [spph.sourcepackagerelease for spph in filtered_spphs])
4611
 
 
4612
 
        return [
4613
 
            SourcePackagePublishingHistoryWithStats(
4614
 
                spph,
4615
 
                open_bugs[spph.meta_sourcepackage.distribution_sourcepackage],
4616
 
                package_question_counts[
4617
 
                    spph.meta_sourcepackage.distribution_sourcepackage],
4618
 
                builds_by_package[spph.sourcepackagerelease],
4619
 
                needs_build_by_package[spph.sourcepackagerelease])
4620
 
            for spph in filtered_spphs]
4621
 
 
4622
5403
    def setUpBatch(self, packages):
4623
5404
        """Set up the batch navigation for the page being viewed.
4624
5405
 
4679
5460
        return "PPA packages"
4680
5461
 
4681
5462
 
4682
 
class PersonSynchronisedPackagesView(PersonRelatedSoftwareView):
4683
 
    """View for +synchronised-packages."""
4684
 
    _max_results_key = 'default_batch_size'
4685
 
 
4686
 
    def initialize(self):
4687
 
        """Set up the batch navigation."""
4688
 
        publishings = self.context.getLatestSynchronisedPublishings()
4689
 
        self.setUpBatch(publishings)
4690
 
 
4691
 
    def setUpBatch(self, publishings):
4692
 
        """Set up the batch navigation for the page being viewed.
4693
 
 
4694
 
        This method creates the BatchNavigator and converts its
4695
 
        results batch into a list of decorated sourcepackagepublishinghistory.
4696
 
        """
4697
 
        self.batchnav = BatchNavigator(publishings, self.request)
4698
 
        publishings_batch = list(self.batchnav.currentBatch())
4699
 
        self.batch = self._addStatsToPublishings(publishings_batch)
4700
 
 
4701
 
    @property
4702
 
    def page_title(self):
4703
 
        return "Synchronised packages"
4704
 
 
4705
 
 
4706
5463
class PersonRelatedProjectsView(PersonRelatedSoftwareView):
4707
5464
    """View for +related-projects."""
4708
5465
    _max_results_key = 'default_batch_size'
5173
5930
        return throttle_date + interval
5174
5931
 
5175
5932
    @property
5176
 
    def page_title(self):
 
5933
    def specific_contact_title_text(self):
5177
5934
        """Return the appropriate pagetitle."""
5178
5935
        if self.context.is_team:
5179
5936
            if self.user.inTeam(self.context):
5197
5954
    links = ('edit', 'administer', 'administer_account', 'branding')
5198
5955
 
5199
5956
 
 
5957
class ITeamIndexMenu(Interface):
 
5958
    """A marker interface for the +index navigation menu."""
 
5959
 
 
5960
 
 
5961
class ITeamEditMenu(Interface):
 
5962
    """A marker interface for the edit navigation menu."""
 
5963
 
 
5964
 
 
5965
class TeamNavigationMenuBase(NavigationMenu, TeamMenuMixin):
 
5966
 
 
5967
    @property
 
5968
    def person(self):
 
5969
        """Override CommonMenuLinks since the view is the context."""
 
5970
        return self.context.context
 
5971
 
 
5972
 
 
5973
class TeamIndexMenu(TeamNavigationMenuBase):
 
5974
    """A menu for different aspects of editing a team."""
 
5975
 
 
5976
    usedfor = ITeamIndexMenu
 
5977
    facet = 'overview'
 
5978
    title = 'Change team'
 
5979
    links = ('edit', 'delete', 'join', 'add_my_teams', 'leave')
 
5980
 
 
5981
 
 
5982
class TeamEditMenu(TeamNavigationMenuBase):
 
5983
    """A menu for different aspects of editing a team."""
 
5984
 
 
5985
    usedfor = ITeamEditMenu
 
5986
    facet = 'overview'
 
5987
    title = 'Change team'
 
5988
    links = ('branding', 'common_edithomepage', 'editlanguages', 'reassign',
 
5989
             'editemail')
 
5990
 
 
5991
 
 
5992
classImplements(TeamIndexView, ITeamIndexMenu)
 
5993
classImplements(TeamEditView, ITeamEditMenu)
5200
5994
classImplements(PersonIndexView, IPersonIndexMenu)
5201
5995
 
5202
5996