launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00917
[Merge] lp:~bac/launchpad/bug-237722 into lp:launchpad/devel
Brad Crittenden has proposed merging lp:~bac/launchpad/bug-237722 into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
#237722 Cyclical team membership allowed in +editproposedmembers
https://bugs.launchpad.net/bugs/237722
= Summary =
If a cycle of team membership could be created by accepting proposed
teams then the model will throw a CyclicalTeamMembershipError. This
error needs to be caught in the UI and transformed into a pleasing
message to the user.
== Proposed fix ==
Catch it and create a notification message.
== Pre-implementation notes ==
None.
== Implementation details ==
As above.
== Tests ==
bin/test -vvm lp.registry -t test_team_view
== Demo and Q/A ==
Follow the QA outlined in the test:
1) Create Team A.
2) Create Team B.
3) Go to https://launchpad.dev/~team-a
4) Select 'Add one of my teams' and choose Team B.
5) Go to https://launchpad.dev/~team-b
6) Select 'Add one of my teams' and choose Team A.
7) Go to https://launchpad.dev/~team-a and select "Edit proposed members".
8) Accept Team B
9) Go to https://launchpad.dev/~team-b and select "Edit proposed members".
10) Accept Team A
11) Note message. Verify you stayed on the +editproposedmembers page.
= Launchpad lint =
Checking for conflicts and issues in changed files.
Linting changed files:
lib/lp/registry/browser/tests/test_team_view.py
lib/lp/registry/browser/team.py
lib/lp/registry/browser/person.py
--
https://code.launchpad.net/~bac/launchpad/bug-237722/+merge/34790
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-237722 into lp:launchpad/devel.
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-09-03 03:12:39 +0000
+++ lib/lp/registry/browser/person.py 2010-09-07 19:20:58 +0000
@@ -208,10 +208,7 @@
DateTimeFormatterAPI,
PersonFormatterAPI,
)
-from canonical.lazr.utils import (
- safe_hasattr,
- smartquote,
- )
+from canonical.lazr.utils import smartquote
from canonical.widgets import (
LaunchpadDropdownWidget,
LaunchpadRadioWidget,
@@ -4435,7 +4432,7 @@
verb = 'have been'
team_string= (
', '.join(team_names[:-1]) + ' and ' + team_names[-1])
- full_message += '%s %s %s ' % (team_string, verb, message)
+ full_message += '%s %s %s' % (team_string, verb, message)
self.request.response.addInfoNotification(full_message)
=== modified file 'lib/lp/registry/browser/team.py'
--- lib/lp/registry/browser/team.py 2010-09-03 03:12:39 +0000
+++ lib/lp/registry/browser/team.py 2010-09-07 19:20:58 +0000
@@ -89,7 +89,10 @@
TeamContactMethod,
TeamSubscriptionPolicy,
)
-from lp.registry.interfaces.teammembership import TeamMembershipStatus
+from lp.registry.interfaces.teammembership import (
+ CyclicalTeamMembershipError,
+ TeamMembershipStatus,
+ )
from lp.services.fields import PublicPersonChoice
from lp.services.propertycache import cachedproperty
@@ -934,31 +937,50 @@
@action('Save changes', name='save')
def action_save(self, action, data):
expires = self.context.defaultexpirationdate
- for person in self.context.proposedmembers:
+ statuses = dict(
+ approve=TeamMembershipStatus.APPROVED,
+ decline=TeamMembershipStatus.DECLINED,
+ )
+ target_team = self.context
+ failed_joins = []
+ for person in target_team.proposedmembers:
action = self.request.form.get('action_%d' % person.id)
- if action == "approve":
- status = TeamMembershipStatus.APPROVED
- elif action == "decline":
- status = TeamMembershipStatus.DECLINED
- else:
+ status = statuses.get(action)
+ if status is None:
# The action is "hold" or no action was specified for this
# person, which could happen if the set of proposed members
# changed while the form was being processed.
continue
+ try:
+ target_team.setMembershipData(
+ person, status, reviewer=self.user, expires=expires,
+ comment=self.request.form.get('comment'))
+ except CyclicalTeamMembershipError:
+ failed_joins.append(person)
- self.context.setMembershipData(
- person, status, reviewer=self.user, expires=expires,
- comment=self.request.form.get('comment'))
+ if len(failed_joins) > 0:
+ failed_names = [person.displayname for person in failed_joins]
+ failed_list = ",".join(failed_names)
+ self.request.response.addInfoNotification(
+ _("${this_team} is a member of the following teams, so those "
+ "teams could not be accepted: "
+ "${failed_list}. These teams should be declined.",
+ mapping=dict(
+ this_team=target_team.displayname,
+ failed_list=failed_list)))
+ self.next_url = ''
+ else:
+ self.next_url = self._next_url
@property
def page_title(self):
return 'Proposed members of %s' % self.context.displayname
@property
- def next_url(self):
+ def _next_url(self):
return '%s/+members' % canonical_url(self.context)
- cancel_url = next_url
+ cancel_url = _next_url
class TeamBrandingView(BrandingChangeView):
=== added file 'lib/lp/registry/browser/tests/test_team_view.py'
--- lib/lp/registry/browser/tests/test_team_view.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_team_view.py 2010-09-07 19:20:58 +0000
@@ -0,0 +1,108 @@
+# Copyright 2010 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""
+Test team views.
+"""
+
+__metaclass__ = type
+
+import transaction
+
+from canonical.testing import DatabaseFunctionalLayer
+
+from lp.registry.interfaces.person import TeamSubscriptionPolicy
+
+from lp.testing import (
+ login_person,
+ TestCaseWithFactory,
+ )
+from lp.testing.views import create_initialized_view
+
+
+class TestProposedTeamMembersEditView(TestCaseWithFactory):
+ """Tests for ProposedTeamMembersEditView."""
+
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super(TestProposedTeamMembersEditView, self).setUp()
+ self.owner = self.factory.makePerson(name="team-owner")
+ self.a_team = self.factory.makeTeam(
+ name="team-a",
+ owner=self.owner,
+ displayname="A-Team",
+ subscription_policy=TeamSubscriptionPolicy.MODERATED)
+ self.b_team = self.factory.makeTeam(
+ name="team-b",
+ owner=self.owner,
+ displayname="B-Team",
+ subscription_policy=TeamSubscriptionPolicy.MODERATED)
+ transaction.commit()
+
+ def test_circular_proposal_acceptance(self):
+ """Two teams can invite each other without horrifying results."""
+
+ # Make the criss-cross invitations.
+ # Owner proposes Team B join Team A.
+ login_person(self.owner)
+ form = {
+ 'field.teams': 'team-b',
+ 'field.actions.continue': 'Continue',
+ }
+ view = create_initialized_view(
+ self.a_team, "+add-my-teams", form=form)
+ self.assertEqual([], view.errors)
+ notifications = view.request.response.notifications
+ self.assertEqual(1, len(notifications))
+ self.assertEqual(
+ u'B-Team has been proposed to this team.',
+ notifications[0].message)
+
+
+ # Owner proposes Team A join Team B.
+ login_person(self.owner)
+ form = {
+ 'field.teams': 'team-a',
+ 'field.actions.continue': 'Continue',
+ }
+ view = create_initialized_view(
+ self.b_team, "+add-my-teams", form=form)
+ self.assertEqual([], view.errors)
+ notifications = view.request.response.notifications
+ self.assertEqual(1, len(notifications))
+ self.assertEqual(
+ u'A-Team has been proposed to this team.',
+ notifications[0].message)
+
+ # Accept Team B into Team A.
+ # Construct the team selection field, based on the id of the team.
+ selector = 'action_%d' % self.b_team.id
+ form = {
+ selector: 'approve',
+ 'field.actions.save': 'Save changes',
+ }
+ view = create_initialized_view(
+ self.a_team, "+editproposedmembers", form=form)
+ self.assertEqual([], view.errors)
+ notifications = view.request.response.notifications
+ self.assertEqual(0, len(notifications))
+
+ # Accept Team A into Team B, or at least try.
+ selector = 'action_%d' % self.a_team.id
+ form = {
+ selector: 'approve',
+ 'field.actions.save': 'Save changes',
+ }
+ view = create_initialized_view(
+ self.b_team, "+editproposedmembers", form=form)
+ self.assertEqual([], view.errors)
+ notifications = view.request.response.notifications
+ self.assertEqual(1, len(notifications))
+ expected = (
+ u'B-Team is a member of the following teams, so those teams '
+ 'could not be accepted: A-Team. These teams should be '
+ 'declined.')
+ self.assertEqual(
+ expected,
+ notifications[0].message)