517
551
return self.context.getMergeQueue(name)
554
class TeamNavigation(PersonNavigation):
558
@stepthrough('+poll')
559
def traverse_poll(self, name):
560
return getUtility(IPollSet).getByTeamAndName(self.context, name)
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
567
membership = getUtility(ITeamMembershipSet).getByPersonAndTeam(
568
self.context, getUtility(IPersonSet).getByName(name))
569
if membership is None:
571
return TeamInvitationView(membership, self.request)
573
@stepthrough('+member')
574
def traverse_member(self, name):
575
person = getUtility(IPersonSet).getByName(name)
578
return getUtility(ITeamMembershipSet).getByPersonAndTeam(
579
person, self.context)
582
class TeamBreadcrumb(Breadcrumb):
583
"""Builds a breadcrumb for an `ITeam`."""
587
return smartquote('"%s" team') % self.context.displayname
590
class TeamMembershipSelfRenewalView(LaunchpadFormView):
592
implements(IBrowserPublisher)
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
599
template = ViewPageTemplateFile(
600
'../templates/teammembership-self-renewal.pt')
604
return "Renew membership of %s in %s" % (
605
self.context.person.displayname, self.context.team.displayname)
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
613
if not check_permission('launchpad.Edit', context.person):
615
"You may not renew the membership for %s." %
616
context.person.displayname)
617
LaunchpadFormView.__init__(self, context, request)
619
def browserDefault(self, request):
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."
643
"You or one of the team administrators has already "
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))
650
raise AssertionError('This membership can be renewed!')
654
def time_before_expiration(self):
655
return self.context.dateexpires - datetime.now(pytz.timezone('UTC'))
659
return canonical_url(self.context.person)
661
cancel_url = next_url
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'))))
675
class ITeamMembershipInvitationAcknowledgementForm(Interface):
676
"""Schema for the form in which team admins acknowledge invitations.
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.
683
acknowledger_comment = Text(
684
title=_("Comment"), required=False, readonly=False)
687
class TeamInvitationView(LaunchpadFormView):
688
"""Where team admins can accept/decline membership invitations."""
690
implements(IBrowserPublisher)
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')
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):
706
"Only team administrators can approve/decline invitations "
707
"sent to this team.")
708
LaunchpadFormView.__init__(self, context, request)
712
"""See `LaunchpadFormView`."""
713
return "Make %s a member of %s" % (
714
self.context.person.displayname, self.context.team.displayname)
717
def page_title(self):
719
'"%s" team invitation') % self.context.team.displayname
721
def browserDefault(self, request):
726
return canonical_url(self.context.person)
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."))
734
member = self.context.person
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}.",
743
that_team=self.context.team.displayname,
744
this_team=member.displayname)))
746
self.request.response.addInfoNotification(
747
_("This team is now a member of ${team}.", mapping=dict(
748
team=self.context.team.displayname)))
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."))
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)))
763
@action(_("Cancel"), name="cancel")
764
def cancel_action(self, action, data):
765
# Simply redirect back.
520
769
class PersonSetNavigation(Navigation):
522
771
usedfor = IPersonSet
613
862
usedfor = IPerson
615
links = ['affectingbugs', 'assignedbugs', 'commentedbugs', 'reportedbugs',
864
links = ['assignedbugs', 'commentedbugs', 'reportedbugs',
616
865
'subscribedbugs', 'relatedbugs', 'softwarebugs']
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)
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)
629
878
def softwarebugs(self):
630
text = 'Subscribed packages'
879
text = 'List subscribed packages'
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)
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)
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)
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)
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)
659
903
class PersonSpecsMenu(NavigationMenu):
996
1232
return Link(target, text)
1235
class TeamMenuMixin(PPANavigationMenuMixIn, CommonMenuLinks):
1236
"""Base class of team menus.
1238
You will need to override the team attribute if your menu subclass
1239
has the view as its context object.
1245
return Link(target, text)
1247
@enabled_with_permission('launchpad.Edit')
1250
text = 'Change details'
1251
return Link(target, text, icon='edit')
1253
@enabled_with_permission('launchpad.Edit')
1255
target = '+branding'
1256
text = 'Change branding'
1257
return Link(target, text, icon='edit')
1259
@enabled_with_permission('launchpad.Owner')
1261
target = '+reassign'
1262
text = 'Change owner'
1263
summary = 'Change the owner of the team'
1264
return Link(target, text, summary, icon='edit')
1266
@enabled_with_permission('launchpad.Moderate')
1270
summary = 'Delete this team'
1271
return Link(target, text, summary, icon='trash-icon')
1273
@enabled_with_permission('launchpad.View')
1276
text = 'Show all members'
1277
return Link(target, text, icon='team')
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')
1285
@enabled_with_permission('launchpad.Edit')
1286
def add_member(self):
1287
target = '+addmember'
1289
return Link(target, text, icon='add')
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')
1299
text = 'View map and time zones'
1300
return Link(target, text, icon='meeting')
1302
def add_my_teams(self):
1303
target = '+add-my-teams'
1304
text = 'Add one of my teams'
1306
restricted = TeamSubscriptionPolicy.RESTRICTED
1307
if self.person.subscriptionpolicy == restricted:
1308
# This is a restricted team; users can't join.
1310
return Link(target, text, icon='add', enabled=enabled)
1312
def memberships(self):
1313
target = '+participation'
1314
text = 'Show team participation'
1315
return Link(target, text, icon='info')
1317
@enabled_with_permission('launchpad.View')
1319
target = '+mugshots'
1320
text = 'Show member photos'
1321
return Link(target, text, icon='team')
1326
return Link(target, text, icon='info')
1328
@enabled_with_permission('launchpad.Edit')
1331
text = 'Create a poll'
1332
return Link(target, text, icon='add')
1334
@enabled_with_permission('launchpad.Edit')
1335
def editemail(self):
1336
target = '+contactaddress'
1337
text = 'Set contact address'
1339
'The address Launchpad uses to contact %s' %
1340
self.person.displayname)
1341
return Link(target, text, summary, icon='edit')
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'
1351
text = 'Create a mailing list'
1354
'The mailing list associated with %s' % self.context.displayname)
1355
return Link(target, text, summary, icon=icon)
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'
1363
'The mailing list associated with %s' % self.context.displayname)
1364
return Link(target, text, summary, icon='edit')
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')
1374
if not userIsActiveTeamMember(self.person):
1376
if self.person.teamowner == self.user:
1377
# The owner cannot leave his team.
1380
text = 'Leave the Team'
1382
return Link(target, text, icon=icon, enabled=enabled)
1386
person = self.person
1387
if userIsActiveTeamMember(person):
1389
elif (self.person.subscriptionpolicy ==
1390
TeamSubscriptionPolicy.RESTRICTED):
1391
# This is a restricted team; users can't join.
1394
text = 'Join the team'
1396
return Link(target, text, icon=icon, enabled=enabled)
1399
class TeamOverviewMenu(ApplicationMenu, TeamMenuMixin, HasRecipesMenuMixin):
1406
'common_edithomepage',
1412
'received_invitations',
1414
'configure_mailing_list',
1415
'moderate_mailing_list',
1428
'related_software_summary',
1431
'structural_subscriptions',
1435
class TeamOverviewNavigationMenu(NavigationMenu, TeamMenuMixin):
1436
"""A top-level menu for navigation within a Team."""
1440
links = ['profile', 'polls', 'members', 'ppas']
1443
class TeamMembershipView(LaunchpadView):
1444
"""The view behind ITeam/+members."""
1448
return smartquote('Members of "%s"' % self.context.displayname)
1451
def active_memberships(self):
1452
"""Current members of the team."""
1453
return ActiveBatchNavigator(
1454
self.context.member_memberships, self.request)
1457
def inactive_memberships(self):
1458
"""Former members of the team."""
1459
return InactiveBatchNavigator(
1460
self.context.getInactiveMemberships(), self.request)
1463
def invited_memberships(self):
1464
"""Other teams invited to become members of this team."""
1465
return list(self.context.getInvitedMemberships())
1468
def proposed_memberships(self):
1469
"""Users who have requested to join this team."""
1470
return list(self.context.getProposedMemberships())
1473
def have_pending_members(self):
1474
return self.proposed_memberships or self.invited_memberships
999
1477
class PersonSetActionNavigationMenu(RegistryCollectionActionMenuBase):
1000
1478
"""Action menu for `PeopleSearchView`."""
1001
1479
usedfor = IPersonSet
1413
1901
return self.context.specifications(filter=filter)
1416
def get_package_search_url(distributionsourcepackage, person_url,
1417
advanced=False, extra_params=None):
1418
"""Construct a default search URL for a distributionsourcepackage.
1420
Optional filter parameters can be specified as a dict with the
1421
extra_params argument.
1424
"field.distribution": distributionsourcepackage.distribution.name,
1425
"field.sourcepackagename": distributionsourcepackage.name,
1428
params['advanced'] = '1'
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"))
1437
params.update(extra_params)
1439
query_string = urllib.urlencode(sorted(params.items()), doseq=True)
1441
return person_url + '/+packagebugs-search?%s' % query_string
1444
class BugSubscriberPackageBugsOverView(LaunchpadView):
1904
class BugSubscriberPackageBugsSearchListingView(BugTaskSearchListingView):
1905
"""Bugs reported on packages for a bug subscriber."""
1907
columns_to_show = ["id", "summary", "importance", "status"]
1446
1908
page_title = 'Package bugs'
1911
def current_package(self):
1912
"""Get the package whose bugs are currently being searched."""
1914
self.widgets['distribution'].hasValidInput() and
1915
self.widgets['distribution'].getInputValue()):
1916
raise UnexpectedFormData("A distribution is required")
1918
self.widgets['sourcepackagename'].hasValidInput() and
1919
self.widgets['sourcepackagename'].getInputValue()):
1920
raise UnexpectedFormData("A sourcepackagename is required")
1922
distribution = self.widgets['distribution'].getInputValue()
1923
return distribution.getSourcePackage(
1924
self.widgets['sourcepackagename'].getInputValue())
1926
def search(self, searchtext=None):
1927
distrosourcepackage = self.current_package
1928
return BugTaskSearchListingView.search(
1929
self, searchtext=searchtext, context=distrosourcepackage)
1931
def getMilestoneWidgetValues(self):
1932
"""See `BugTaskSearchListingView`.
1934
We return only the active milestones on the current distribution
1935
since any others are irrelevant.
1937
current_distro = self.current_package.distribution
1938
vocabulary_registry = getVocabularyRegistry()
1939
vocabulary = vocabulary_registry.get(current_distro, 'Milestone')
1941
return helpers.shortlist([
1942
dict(title=milestone.title, value=milestone.token, checked=False)
1943
for milestone in vocabulary],
1944
longest_expected=10)
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."""
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']
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),
1493
1987
return sorted(L, key=itemgetter('package_name'))
1495
def getOpenBugsURL(self, distributionsourcepackage, person_url):
1989
def getOtherBugSubscriberPackageLinks(self):
1990
"""Return a list of the other packages for a bug subscriber.
1992
This excludes the current package.
1994
current_package = self.current_package
1997
package for package in self.context.getBugSubscriberPackages()
1998
if package != current_package]
2001
for other_package in other_packages:
2002
package_links.append({
2003
'title': other_package.displayname,
2004
'url': self.getBugSubscriberPackageSearchURL(other_package)})
2006
return package_links
2009
def person_url(self):
2010
return canonical_url(self.context)
2012
def getBugSubscriberPackageSearchURL(self, distributionsourcepackage=None,
2013
advanced=False, extra_params=None):
2014
"""Construct a default search URL for a distributionsourcepackage.
2016
Optional filter parameters can be specified as a dict with the
2017
extra_params argument.
2019
if distributionsourcepackage is None:
2020
distributionsourcepackage = self.current_package
2023
"field.distribution": distributionsourcepackage.distribution.name,
2024
"field.sourcepackagename": distributionsourcepackage.name,
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"))
2034
params.update(extra_params)
2036
query_string = urllib.urlencode(sorted(params.items()), doseq=True)
2039
return (self.person_url + '/+packagebugs-search?advanced=1&%s'
2042
return self.person_url + '/+packagebugs-search?%s' % query_string
2044
def getBugSubscriberPackageAdvancedSearchURL(self,
2045
distributionsourcepackage=None):
2046
"""Build the advanced search URL for a distributionsourcepackage."""
2047
return self.getBugSubscriberPackageSearchURL(advanced=True)
2049
def getOpenBugsURL(self, distributionsourcepackage):
1496
2050
"""Return the URL for open bugs on distributionsourcepackage."""
1497
2051
status_params = {'field.status': []}
1499
2053
for status in UNRESOLVED_BUGTASK_STATUSES:
1500
2054
status_params['field.status'].append(status.title)
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)
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"}
1538
2089
for status in UNRESOLVED_BUGTASK_STATUSES:
1539
2090
unassigned_bugs_params["field.status"].append(status.title)
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)
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"}
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)
1556
class BugSubscriberPackageBugsSearchListingView(BugTaskSearchListingView):
1557
"""Bugs reported on packages for a bug subscriber."""
1559
columns_to_show = ["id", "summary", "importance", "status"]
1560
page_title = 'Package bugs'
1563
def current_package(self):
1564
"""Get the package whose bugs are currently being searched."""
1566
self.widgets['distribution'].hasValidInput() and
1567
self.widgets['distribution'].getInputValue()):
1568
raise UnexpectedFormData("A distribution is required")
1570
self.widgets['sourcepackagename'].hasValidInput() and
1571
self.widgets['sourcepackagename'].getInputValue()):
1572
raise UnexpectedFormData("A sourcepackagename is required")
1574
distribution = self.widgets['distribution'].getInputValue()
1575
return distribution.getSourcePackage(
1576
self.widgets['sourcepackagename'].getInputValue())
1578
def search(self, searchtext=None):
1579
distrosourcepackage = self.current_package
1580
return BugTaskSearchListingView.search(
1581
self, searchtext=searchtext, context=distrosourcepackage)
1583
def getMilestoneWidgetValues(self):
1584
"""See `BugTaskSearchListingView`.
1586
We return only the active milestones on the current distribution
1587
since any others are irrelevant.
1589
current_distro = self.current_package.distribution
1590
vocabulary_registry = getVocabularyRegistry()
1591
vocabulary = vocabulary_registry.get(current_distro, 'Milestone')
1594
dict(title=milestone.title, value=milestone.token, checked=False)
1595
for milestone in vocabulary],
1596
longest_expected=10)
1599
def person_url(self):
1600
return canonical_url(self.context)
1602
def getBugSubscriberPackageSearchURL(self, distributionsourcepackage=None,
1603
advanced=False, extra_params=None):
1604
"""Construct a default search URL for a distributionsourcepackage.
1606
Optional filter parameters can be specified as a dict with the
1607
extra_params argument.
1609
if distributionsourcepackage is None:
1610
distributionsourcepackage = self.current_package
1611
return get_package_search_url(
1612
distributionsourcepackage, self.person_url, advanced,
1615
def getBugSubscriberPackageAdvancedSearchURL(self,
1616
distributionsourcepackage=None):
1617
"""Build the advanced search URL for a distributionsourcepackage."""
1618
return self.getBugSubscriberPackageSearchURL(advanced=True)
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
1719
2192
return self.getSearchPageHeading()
1722
class PersonAffectingBugTaskSearchListingView(
1723
RelevantMilestonesMixin, BugTaskSearchListingView):
1724
"""All bugs affecting someone."""
1726
columns_to_show = ["id", "summary", "bugtargetdisplayname",
1727
"importance", "status"]
1728
view_name = '+affectingbugs'
1729
page_title = 'Bugs affecting' # The context is added externally.
1731
def searchUnbatched(self, searchtext=None, context=None,
1732
extra_params=None, prejoins=[]):
1733
"""Return the open bugs assigned to a person."""
1735
context = self.context
1737
if extra_params is None:
1738
extra_params = dict()
1740
extra_params = dict(extra_params)
1741
extra_params['affected_user'] = context
1743
sup = super(PersonAffectingBugTaskSearchListingView, self)
1744
return sup.searchUnbatched(
1745
searchtext, context, extra_params, prejoins)
1747
def shouldShowAssigneeWidget(self):
1748
"""Should the assignee widget be shown on the advanced search page?"""
1751
def shouldShowTeamPortlet(self):
1752
"""Should the team assigned bugs portlet be shown?"""
1755
def shouldShowTagsCombinatorWidget(self):
1756
"""Should the tags combinator widget show on the search page?"""
1760
def context_description(self):
1761
"""See `BugTaskSearchListingView`."""
1762
return "affecting %s" % self.context.displayname
1764
def getSearchPageHeading(self):
1765
"""The header for the search page."""
1766
return "Bugs %s" % self.context_description
1768
def getAdvancedSearchButtonLabel(self):
1769
"""The Search button for the advanced search page."""
1770
return "Search bugs %s" % self.context_description
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)
1778
return self.getSearchPageHeading()
1781
2195
class PersonAssignedBugTaskSearchListingView(RelevantMilestonesMixin,
1782
2196
BugTaskSearchListingView):
1783
2197
"""All bugs assigned to someone."""
2305
2693
return self.context.latestKarma().count() > 0
2308
class PersonView(LaunchpadView, FeedsMixin):
2696
class TeamJoinMixin:
2697
"""Mixin class for views related to joining teams."""
2700
def user_can_subscribe_to_list(self):
2701
"""Can the prospective member subscribe to this team's mailing list?
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.
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
2713
def user_is_subscribed_to_list(self):
2714
"""Is the user subscribed to the team's mailing list?
2716
Subscriptions hang around even if the list is deactivated, etc.
2718
It is an error to ask if the user is subscribed to a mailing list
2721
if self.user is None:
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
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
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)
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:
2745
return self.user in self.context.proposedmembers
2748
def user_can_request_to_leave(self):
2749
"""Return true if the user can request to leave this team.
2751
A given user can leave a team only if he's an active member.
2753
return self.user_is_active_member
2756
class PersonView(LaunchpadView, FeedsMixin, TeamJoinMixin):
2309
2757
"""A View class used in almost all Person's pages."""
3562
4056
schema = IPerson
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)
4066
class TeamJoinView(LaunchpadFormView, TeamJoinMixin):
4067
"""A view class for joining a team."""
4068
schema = TeamJoinForm
4072
return 'Join ' + cgi.escape(self.context.displayname)
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)
4083
def field_names(self):
4084
"""See `LaunchpadFormView`.
4086
If the user can subscribe to the mailing list then include the
4087
mailinglist subscription checkbox otherwise remove it.
4089
if self.user_can_subscribe_to_list:
4090
return ['mailinglist_subscribe']
4095
def join_allowed(self):
4096
"""Is the logged in user allowed to join this team?
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.
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):
4111
restricted = TeamSubscriptionPolicy.RESTRICTED
4112
return self.context.subscriptionpolicy != restricted
4115
def user_can_request_to_join(self):
4116
"""Can the logged in user request to join this team?
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.
4121
if not self.join_allowed:
4123
return not (self.user_is_active_member or
4124
self.user_is_proposed_member)
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)
4133
def team_is_moderated(self):
4134
"""Is this team a moderated team?
4136
Return True if the team's subscription policy is MODERATED.
4138
policy = self.context.subscriptionpolicy
4139
return policy == TeamSubscriptionPolicy.MODERATED
4143
return canonical_url(self.context)
4146
def cancel_url(self):
4147
return canonical_url(self.context)
4149
@action(_("Join"), name="join")
4150
def action_save(self, action, data):
4151
response = self.request.response
4153
if self.user_can_request_to_join:
4154
# Shut off mailing list auto-subscription - we want direct
4156
self.user.join(self.context, may_subscribe_to_list=False)
4158
if self.team_is_moderated:
4159
response.addInfoNotification(
4160
_('Your request to join ${team} is awaiting '
4162
mapping={'team': self.context.displayname}))
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)
4171
response.addErrorNotification(
4172
_('You cannot join ${team}.',
4173
mapping={'team': self.context.displayname}))
4175
def _subscribeToList(self, response):
4176
"""Subscribe the user to the team's mailing list."""
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)
4183
if self.team_is_moderated:
4184
response.addInfoNotification(
4185
_('Your mailing list subscription is '
4186
'awaiting approval.'))
4188
response.addInfoNotification(
4190
_("You have been subscribed to this "
4191
"team’s mailing list.")))
4193
# A catch-all case, perhaps from stale or mangled
4195
response.addErrorNotification(
4196
_('Mailing list subscription failed.'))
4199
class TeamAddMyTeamsView(LaunchpadFormView):
4200
"""Propose/add to this team any team that you're an administrator of."""
4202
page_title = 'Propose/add one of your teams to another one'
4203
custom_widget('teams', LabeledMultiCheckBoxWidget)
4205
def initialize(self):
4206
context = self.context
4207
if context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED:
4208
self.label = 'Propose these teams as members'
4210
self.label = 'Add these teams to %s' % context.displayname
4211
self.next_url = canonical_url(context)
4212
super(TeamAddMyTeamsView, self).initialize()
4214
def setUpFields(self):
4216
for team in self.candidate_teams:
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',
4223
value_type=Choice(vocabulary=SimpleVocabulary(terms)),
4225
render_context=self.render_context)
4227
def setUpWidgets(self, context=None):
4228
super(TeamAddMyTeamsView, self).setUpWidgets(context)
4229
self.widgets['teams'].display_label = False
4232
def candidate_teams(self):
4233
"""Return the set of teams that can be added/proposed for the context.
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.)
4240
for team in self.user.getAdministratedTeams():
4241
if team == self.context:
4243
elif team.visibility != PersonVisibility.PUBLIC:
4245
elif team in self.context.activemembers:
4246
# The team is already a member of the context object.
4248
elif self.context.hasParticipationEntryFor(team):
4249
# The context object is a member/submember of the team.
4251
candidates.append(team)
4255
def cancel_url(self):
4256
"""The return URL."""
4257
return canonical_url(self.context)
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.')
4265
def hasCandidates(self, action):
4266
"""Return whether the user has teams to propose."""
4267
return len(self.candidate_teams) > 0
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(
4285
'Accepted an already pending invitation while trying to '
4286
'propose the team for membership.')
4287
accepted_invite_team_names.append(team.displayname)
4289
context.addMember(team, reviewer=self.user)
4290
added_team_names.append(team.displayname)
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)
4299
raise AssertionError(
4300
'Unexpected membership status (%s) for %s.'
4301
% (membership.status.name, team.name))
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.'),
4309
if len(team_names) == 0:
4311
elif len(team_names) == 1:
4313
team_string = team_names[0]
4314
elif len(team_names) > 1:
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)
4322
class TeamLeaveView(LaunchpadFormView, TeamJoinMixin):
4327
return 'Leave ' + cgi.escape(self.context.displayname)
4332
def cancel_url(self):
4333
return canonical_url(self.context)
4335
next_url = cancel_url
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)
3565
4343
class PersonEditEmailsView(LaunchpadFormView):
3566
4344
"""A view for editing a person's email settings.
4066
4844
self.next_url = self.action_url
4847
class TeamMugshotView(LaunchpadView):
4848
"""A view for the team mugshot (team photo) page"""
4850
label = "Member photos"
4851
batch_size = config.launchpad.mugshot_batch_size
4853
def initialize(self):
4854
"""Cache images to avoid dying from a million cuts."""
4855
getUtility(IPersonSet).cacheBrandingForPeople(
4856
self.members.currentBatch())
4860
"""Get a batch of all members in the team."""
4861
batch_nav = BatchNavigator(
4862
self.context.allmembers, self.request, size=self.batch_size)
4866
class TeamReassignmentView(ObjectReassignmentView):
4868
ownerOrMaintainerAttr = 'teamowner'
4869
schema = ITeamReassignment
4871
def __init__(self, context, request):
4872
super(TeamReassignmentView, self).__init__(context, request)
4873
self.callback = self._addOwnerAsMember
4875
def validateOwner(self, new_owner):
4876
"""Display error if the owner is not valid.
4878
Called by ObjectReassignmentView.validate().
4880
if self.context.inTeam(new_owner):
4881
path = self.context.findPathToTeam(new_owner)
4883
relationship = 'a direct member'
4886
relationship = 'an indirect member'
4887
full_path = [self.context] + path
4888
path_string = '(%s)' % '⇒'.join(
4889
team.displayname for team in full_path)
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,
4899
self.setFieldError(self.ownerOrMaintainerName, error)
4902
def contextName(self):
4903
return self.context.displayname
4905
def _addOwnerAsMember(self, team, oldOwner, newOwner):
4906
"""Add the new and the old owners as administrators of the team.
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.
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:
4919
newOwner, reviewer=oldOwner,
4920
status=TeamMembershipStatus.ADMIN, force_team_add=True)
4921
if oldOwner not in team.inactivemembers:
4923
oldOwner, reviewer=oldOwner,
4924
status=TeamMembershipStatus.ADMIN, force_team_add=True)
4069
4927
class PersonLatestQuestionsView(LaunchpadFormView):
4070
4928
"""View used by the porlet displaying the latest questions made by