Merge lp:~edwin-grubbs/launchpad/bug-568390-cyclic-membership-error into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Paul Hummer
Approved revision: no longer in the source branch.
Merged at revision: 11110
Proposed branch: lp:~edwin-grubbs/launchpad/bug-568390-cyclic-membership-error
Merge into: lp:launchpad
Diff against target: 157 lines (+115/-7)
3 files modified
lib/lp/registry/browser/objectreassignment.py (+5/-6)
lib/lp/registry/browser/person.py (+27/-1)
lib/lp/registry/browser/tests/team-reassignment-view.txt (+83/-0)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-568390-cyclic-membership-error
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+29407@code.launchpad.net

Description of the change

Summary
-------

The TeamReassignmentView changes the owner of a team and then adds the
new owner as a member of that team. The view now disipays an error if
this creates a cyclical team membership, so that the model won't raise
an exception.

Implementation details
----------------------

The ObjectReassignmentView.isValidOwner() was not very flexible, since
it shouldn't be used to set a custom error message, so I changed it to
validateOwner(). This gets called at the end of
ObjectReassignmentView.validate(), after a new team has been created, if
the user is not using an existing team. isValidOwner() does not appear
to be used in any of the other subclasses of ObjectReassignmentView.

    lib/lp/registry/browser/objectreassignment.py
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/team-reassignment-view.txt

Tests
-----

./bin/test -vv -t team-reassignment-view.txt

Demo and Q/A
------------

* Open https://launchpad.dev/people/+newteam
  * Create three new teams.
  * Add teams as members, so A is a member of B is a member of C.
  * Open https://launchpad.dev/~A/+reassign
    * Try to make C the owner of A.
    * It should display an error that this will cause a cyclical team
      membership.

To post a comment you must log in.
Revision history for this message
Paul Hummer (rockstar) wrote :

<rockstar> EdwinGrubbs, maybe validateOwner's abstract method should return False or raise NotImplementedError.
<EdwinGrubbs> rockstar: it's not required that validateOwner() be overridden. The vocabulary normally does a good enough job. This is the only subclass that actually does override it.
<EdwinGrubbs> so raising NotImplementedError isn't necessary, and returning false would be meaningless.
<rockstar> EdwinGrubbs, pass seems like it's very unopinionated
<EdwinGrubbs> rockstar: it's no different than LaunchpadFormView.validate().
<rockstar> EdwinGrubbs, okay.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/objectreassignment.py'
--- lib/lp/registry/browser/objectreassignment.py 2010-02-16 20:36:48 +0000
+++ lib/lp/registry/browser/objectreassignment.py 2010-07-09 01:44:44 +0000
@@ -111,13 +111,13 @@
111 if callable(self.callback):111 if callable(self.callback):
112 self.callback(self.context, oldOwner, newOwner)112 self.callback(self.context, oldOwner, newOwner)
113113
114 def isValidOwner(self, newOwner):114 def validateOwner(self, new_owner):
115 """Check whether the new owner is acceptable for the context object.115 """Check whether the new owner is acceptable for the context object.
116116
117 If it's not acceptable, return False and assign an error message to117 If it's not acceptable, display an error by calling:
118 self.errormessage to inform the user.118 self.setFieldError(self.ownerOrMaintainerName, 'some error info')
119 """119 """
120 return True120 pass
121121
122 def _validate(self, action, data):122 def _validate(self, action, data):
123 """Override _validate() method."""123 """Override _validate() method."""
@@ -179,5 +179,4 @@
179 owner = personset.newTeam(179 owner = personset.newTeam(
180 self.user, owner_name, owner_name.capitalize())180 self.user, owner_name, owner_name.capitalize())
181181
182 if not self.isValidOwner(owner):182 self.validateOwner(owner)
183 self.setFieldError(self.ownerOrMaintainerName, 'Invalid owner.')
184183
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-07-08 19:29:05 +0000
+++ lib/lp/registry/browser/person.py 2010-07-09 01:44:44 +0000
@@ -4780,9 +4780,35 @@
4780 schema = ITeamReassignment4780 schema = ITeamReassignment
47814781
4782 def __init__(self, context, request):4782 def __init__(self, context, request):
4783 ObjectReassignmentView.__init__(self, context, request)4783 super(TeamReassignmentView, self).__init__(context, request)
4784 self.callback = self._addOwnerAsMember4784 self.callback = self._addOwnerAsMember
47854785
4786 def validateOwner(self, new_owner):
4787 """Display error if the owner is not valid.
4788
4789 Called by ObjectReassignmentView.validate().
4790 """
4791 if self.context.inTeam(new_owner):
4792 path = self.context.findPathToTeam(new_owner)
4793 if len(path) == 1:
4794 relationship = 'a direct member'
4795 path_string = ''
4796 else:
4797 relationship = 'an indirect member'
4798 full_path = [self.context] + path
4799 path_string = '(%s)' % '&rArr;'.join(
4800 team.displayname for team in full_path)
4801 error = structured(
4802 'Circular team memberships are not allowed. '
4803 '%(new)s cannot be the new team owner, since %(context)s '
4804 'is %(relationship)s of %(new)s. '
4805 '<span style="white-space: nowrap">%(path)s</span>'
4806 % dict(new=new_owner.displayname,
4807 context=self.context.displayname,
4808 relationship=relationship,
4809 path=path_string))
4810 self.setFieldError(self.ownerOrMaintainerName, error)
4811
4786 @property4812 @property
4787 def contextName(self):4813 def contextName(self):
4788 return self.context.displayname4814 return self.context.displayname
47894815
=== added file 'lib/lp/registry/browser/tests/team-reassignment-view.txt'
--- lib/lp/registry/browser/tests/team-reassignment-view.txt 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/team-reassignment-view.txt 2010-07-09 01:44:44 +0000
@@ -0,0 +1,83 @@
1Team Reassignment
2=================
3
4There are many restrictions for setting the owner of a team.
5
6
7Existing Team Error
8-------------------
9
10The ObjectReassignmentView displays radio buttons that give you the
11option to create a team as opposed to using an existing team. If the
12user tries to create a new team with the same name as an existing team,
13an error is displayed.
14
15 >>> from lp.registry.interfaces.person import IPersonSet
16 >>> person_set = getUtility(IPersonSet)
17 >>> operator = person_set.getByName('name12')
18 >>> login_person(operator)
19 >>> a_team = factory.makeTeam(
20 ... owner=operator, name='a-team', displayname='A-Team')
21 >>> b_team = factory.makeTeam(
22 ... owner=operator, name='b-team', displayname='B-Team')
23 >>> c_team = factory.makeTeam(
24 ... owner=operator, name='c-team', displayname='C-Team')
25 >>> a_team.addMember(b_team, operator)
26 (True...
27 >>> b_team.addMember(c_team, operator)
28 (True...
29 >>> transaction.commit()
30
31 >>> view = create_initialized_view(
32 ... c_team, '+reassign', principal=operator)
33 >>> print list(w.name for w in view.widgets)
34 ['field.owner', 'field.existing']
35
36 >>> form = {
37 ... 'field.owner': 'a-team',
38 ... 'field.existing': 'new',
39 ... 'field.actions.change': 'Change',
40 ... }
41 >>> view = create_initialized_view(
42 ... a_team, '+reassign', form=form, principal=operator)
43 >>> print view.errors
44 [u"There's already a person/team with the name 'a-team' in Launchpad.
45 Please choose a different name or select the option to make that
46 person/team the new owner, if that's what you want."]
47
48
49Cyclical Team Membership Error
50------------------------------
51
52When a person or team becomes the owner of another team, they are also
53added as a member. Team memberships cannot be cyclical; therefore, the
54team can't have its owner be a team of which it is a direct or indirect
55member.
56
57 >>> form = {
58 ... 'field.owner': 'b-team',
59 ... 'field.existing': 'existing',
60 ... 'field.actions.change': 'Change',
61 ... }
62 >>> view = create_initialized_view(
63 ... c_team, '+reassign', form=form, principal=operator)
64 >>> print view.widget_errors
65 {'owner': u'Circular team memberships are not allowed.
66 B-Team cannot be the new team owner, since C-Team is a direct member
67 of B-Team. <...><...>'}
68
69If there is an indirect membership between the teams, the path between
70the teams is displayed so that the user has a better idea how to resolve
71the issue.
72
73 >>> form = {
74 ... 'field.owner': 'a-team',
75 ... 'field.existing': 'existing',
76 ... 'field.actions.change': 'Change',
77 ... }
78 >>> view = create_initialized_view(
79 ... c_team, '+reassign', form=form, principal=operator)
80 >>> print view.widget_errors
81 {'owner': u'Circular team memberships are not allowed.
82 A-Team cannot be the new team owner, since C-Team is an
83 indirect member of A-Team. <...>(C-Team&rArr;B-Team&rArr;A-Team)<...>'}