Merge lp:~sinzui/launchpad/closed-teams-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 12018
Proposed branch: lp:~sinzui/launchpad/closed-teams-0
Merge into: lp:launchpad
Diff against target: 762 lines (+264/-144)
15 files modified
lib/canonical/launchpad/vocabularies/dbobjects.py (+2/-0)
lib/canonical/launchpad/webapp/vocabulary.py (+5/-1)
lib/canonical/widgets/popup.py (+21/-13)
lib/lp/answers/vocabulary.py (+1/-2)
lib/lp/app/widgets/tests/test_popup.py (+43/-0)
lib/lp/blueprints/vocabularies/specificationdependency.py (+1/-0)
lib/lp/code/vocabularies/branch.py (+1/-0)
lib/lp/registry/browser/configure.zcml (+1/-1)
lib/lp/registry/browser/person.py (+7/-0)
lib/lp/registry/browser/tests/test_team.py (+14/-0)
lib/lp/registry/doc/vocabularies.txt (+1/-93)
lib/lp/registry/javascript/team.js (+2/-2)
lib/lp/registry/templates/team-portlet-membership.pt (+4/-2)
lib/lp/registry/tests/test_person_vocabularies.py (+99/-18)
lib/lp/registry/vocabularies.py (+62/-12)
To merge this branch: bzr merge lp:~sinzui/launchpad/closed-teams-0
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+42500@code.launchpad.net

Description of the change

Need a closed team and user vocabulary and picker

    Launchpad bug: https://bugs.launchpad.net/bugs/681035
    Pre-implementation: jcsackett, flacoste, gary_poster
    Test command: ./bin/test -vv \
        -t test_popup -t test_team -t test_person_vocabularies

There are several cases where Launchpad must prevent users from selecting a
team with an open subscription policy because it compromises security or
discloses information. The person picker and forms need a vocabulary that is
composed on closed teams (restricted and moderated), and users. The
picker/vocabulary is needed is needed in:

1. closed team owner role
2. closed team +addmember
3. moderated team +proposemember

------------------------------------------------------------------------------

RULES

    * Update the ValidTeamMemberVocabulary and ValidTeamOwnerVocabulary
      to exclude open teams when the context team is closed.
    * Update IHugeVocabulary, <intermediate classes>, VocabularyPickerWidget
      to use the vocabs step_title so that users understand what can and
      cannot be searched for. This issue was discovered in user testing.
    * Update all IHugeVocabularies to have a step_title
    * Update the add member ajax feature to also use the vocabulary's
      step_title

QA

    Using a moderated or restricted team to test.
    * Verify that open teams to not appear in the person picker to add
      a new member from the team's front page.
    * Verify that open teams to not appear in the person picker to add
      a new member from the teams +addmember page.
    * Verify that open teams to not appear in the person picker to change
      the team owner

LINT

    lib/canonical/launchpad/vocabularies/dbobjects.py
    lib/canonical/launchpad/webapp/vocabulary.py
    lib/canonical/widgets/popup.py
    lib/lp/answers/vocabulary.py
    lib/lp/app/widgets/
    lib/lp/app/widgets/__init__.py
    lib/lp/app/widgets/tests/
    lib/lp/app/widgets/tests/__init__.py
    lib/lp/app/widgets/tests/test_popup.py
    lib/lp/blueprints/vocabularies/specificationdependency.py
    lib/lp/code/vocabularies/branch.py
    lib/lp/registry/vocabularies.py
    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_team.py
    lib/lp/registry/javascript/team.js
    lib/lp/registry/templates/team-portlet-membership.pt
    lib/lp/registry/tests/test_person_vocabularies.py

IMPLEMENTATION

Updated ValidTeamMemberVocabulary and ValidTeamOwnerVocabulary to exclude
open teams when the context team is closed.
    lib/lp/registry/vocabularies.py
    lib/lp/registry/tests/test_person_vocabularies.py

Added step_title to the vocabulary. The value is the same that appears in
the UI now.
    lib/canonical/launchpad/webapp/vocabulary.py
    lib/canonical/launchpad/vocabularies/dbobjects.py
    lib/lp/answers/vocabulary.py
    lib/lp/blueprints/vocabularies/specificationdependency.py
    lib/lp/code/vocabularies/branch.py
    lib/lp/registry/vocabularies.py

Updated VocabularyPickerWidget to use the vocab's step_title. There are
no direct tests of this widget. Moving the whole module would make the diff
hard to read so I create a test where the module should be moved.
    lib/canonical/widgets/popup.py
    lib/lp/app/widgets/
    lib/lp/app/widgets/__init__.py
    lib/lp/app/widgets/tests/
    lib/lp/app/widgets/tests/__init__.py
    lib/lp/app/widgets/tests/test_popup.py

Updated the TeamIndexView and team membership portlet to use the step_title
from the vocabulary.
    lib/lp/registry/browser/configure.zcml
    lib/lp/registry/browser/person.py
    lib/lp/registry/browser/tests/test_team.py
    lib/lp/registry/javascript/team.js
    lib/lp/registry/templates/team-portlet-membership.pt

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Hi Curtis. Some incredibly minor comments, +1.

[1]

         js = js_template % simplejson.dumps(args)
         # If the YUI widget or javascript is not supported in the browser,
         # it will degrade to being this "Find..." link instead of the
@@ -154,8 +163,7 @@
             css = ''
         return ('<span class="%s">(<a id="%s" href="/people/">'
                 'Find&hellip;</a>)</span>'
- '\n<script>\n%s\n</script>'
- ) % (css, self.show_widget_id, js)
+ '\n<script>\n%s\n</script>') % (css, self.show_widget_id, js)

It's not part of your change, but using simplejson.dumps() can be
hazardous for embedding javascript into <script> tags. See:

  http://code.google.com/p/simplejson/issues/detail?id=66

Instead the following should be used:

  simplejson.dumps(args, cls=simplejson.encoder.JSONEncoderForHTML)

Or:

  simplejson.encoder.JSONEncoderForHTML().encode(obj)

[2]

+ Y.lp.registry.team.setup_add_member_handler(
+ '${step_title}');

The lack of escaping and all that jazz makes me a little uneasy, but
it's not user-supplied so I'm not too worried.

[3]

+ def test_open_team_cannot_be_a_member_or_a_closed_team(self):

s/or/of/

[4]

+ def test_open_team_can_be_a_member_or_an_open_team(self):

s/or/of/

[5]

+ return 'Search for a restricted or moderated team or person'

This could be parsed as:

  Search for a (restricted or moderated) (team or person)

Perhaps the following would be less ambiguous:

  Search for a restricted team, a moderated team, or a person

?

[6]

+ Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)

Maybe it's worth taking a defensive approach here (to someone adding
another TeamSubscriptionPolicy member) instead:

  In(Person.subscriptionpolicy, (
      TeamSubscriptionPolicy.RESTRICTED,
      TeamSubscriptionPolicy.MODERATED))

The same clause appears twice.

review: Approve
Revision history for this message
Curtis Hovey (sinzui) wrote :

On Thu, 2010-12-02 at 17:12 +0000, Gavin Panella wrote:
> Review: Approve
> Hi Curtis. Some incredibly minor comments, +1.
>
>
> [1]
>
> js = js_template % simplejson.dumps(args)
> # If the YUI widget or javascript is not supported in the browser,
> # it will degrade to being this "Find..." link instead of the
> @@ -154,8 +163,7 @@
> css = ''
> return ('<span class="%s">(<a id="%s" href="/people/">'
> 'Find&hellip;</a>)</span>'
> - '\n<script>\n%s\n</script>'
> - ) % (css, self.show_widget_id, js)
> + '\n<script>\n%s\n</script>') % (css, self.show_widget_id, js)
>
> It's not part of your change, but using simplejson.dumps() can be
> hazardous for embedding javascript into <script> tags. See:
>
> http://code.google.com/p/simplejson/issues/detail?id=66
>
> Instead the following should be used:
>
> simplejson.dumps(args, cls=simplejson.encoder.JSONEncoderForHTML)
>
> Or:
>
> simplejson.encoder.JSONEncoderForHTML().encode(obj)

Interesting. We cannot do this right now because Lp is using 2.0.9. I
can see it my version of 2.1.1

> [2]
>
> + Y.lp.registry.team.setup_add_member_handler(
> + '${step_title}');
>
> The lack of escaping and all that jazz makes me a little uneasy, but
> it's not user-supplied so I'm not too worried.

This content is in a tal:content rule and is escaped. A single-quote
will break the script. I updated the method to escape quotes and added a
test.

> [3]
>
> + def test_open_team_cannot_be_a_member_or_a_closed_team(self):
>
> s/or/of/

fixed

> [4]
>
> + def test_open_team_can_be_a_member_or_an_open_team(self):
>
> s/or/of/

fixed

> [5]
>
> + return 'Search for a restricted or moderated team or person'
>
> This could be parsed as:
>
> Search for a (restricted or moderated) (team or person)
>
> Perhaps the following would be less ambiguous:
>
> Search for a restricted team, a moderated team, or a person

I accept your proposal.

> [6]
>
> + Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)
>
> Maybe it's worth taking a defensive approach here (to someone adding
> another TeamSubscriptionPolicy member) instead:
>
> In(Person.subscriptionpolicy, (
> TeamSubscriptionPolicy.RESTRICTED,
> TeamSubscriptionPolicy.MODERATED))

We have not changed the policies in 5 years. I do not think we will be
adding other policies. If I am wrong, we may need to update the proposed
revision. Since this issue is about OPEN, I think the clause should
remain as is.

--
__Curtis C. Hovey_________
http://launchpad.net/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/canonical/launchpad/vocabularies/dbobjects.py'
--- lib/canonical/launchpad/vocabularies/dbobjects.py 2010-11-09 08:38:23 +0000
+++ lib/canonical/launchpad/vocabularies/dbobjects.py 2010-12-03 00:04:45 +0000
@@ -141,6 +141,7 @@
141class BugTrackerVocabulary(SQLObjectVocabularyBase):141class BugTrackerVocabulary(SQLObjectVocabularyBase):
142 """All web and email based external bug trackers."""142 """All web and email based external bug trackers."""
143 displayname = 'Select a bug tracker'143 displayname = 'Select a bug tracker'
144 step_title = 'Search'
144 implements(IHugeVocabulary)145 implements(IHugeVocabulary)
145 _table = BugTracker146 _table = BugTracker
146 _filter = True147 _filter = True
@@ -401,6 +402,7 @@
401 Person.q.id == Archive.q.ownerID,402 Person.q.id == Archive.q.ownerID,
402 Archive.q.purpose == ArchivePurpose.PPA)403 Archive.q.purpose == ArchivePurpose.PPA)
403 displayname = 'Select a PPA'404 displayname = 'Select a PPA'
405 step_title = 'Search'
404406
405 def toTerm(self, archive):407 def toTerm(self, archive):
406 """See `IVocabulary`."""408 """See `IVocabulary`."""
407409
=== modified file 'lib/canonical/launchpad/webapp/vocabulary.py'
--- lib/canonical/launchpad/webapp/vocabulary.py 2010-11-09 08:38:23 +0000
+++ lib/canonical/launchpad/webapp/vocabulary.py 2010-12-03 00:04:45 +0000
@@ -70,7 +70,10 @@
70 """70 """
7171
72 displayname = Attribute(72 displayname = Attribute(
73 'A name for this vocabulary, to be displayed in the popup window.')73 'A name for this vocabulary, to be displayed in the picker window.')
74
75 step_title = Attribute(
76 'The search step title in the picker window.')
7477
75 def searchForTerms(query=None):78 def searchForTerms(query=None):
76 """Return a `CountableIterator` of `SimpleTerm`s that match the query.79 """Return a `CountableIterator` of `SimpleTerm`s that match the query.
@@ -365,6 +368,7 @@
365 implements(IHugeVocabulary)368 implements(IHugeVocabulary)
366 _orderBy = 'name'369 _orderBy = 'name'
367 displayname = None370 displayname = None
371 step_title = 'Search'
368 # The iterator class will be used to wrap the results; its iteration372 # The iterator class will be used to wrap the results; its iteration
369 # methods should return SimpleTerms, as the reference implementation373 # methods should return SimpleTerms, as the reference implementation
370 # CountableIterator does.374 # CountableIterator does.
371375
=== modified file 'lib/canonical/widgets/popup.py'
--- lib/canonical/widgets/popup.py 2010-08-24 10:45:57 +0000
+++ lib/canonical/widgets/popup.py 2010-12-03 00:04:45 +0000
@@ -37,7 +37,7 @@
37 style = ''37 style = ''
38 cssClass = ''38 cssClass = ''
3939
40 step_title = 'Search'40 step_title = None
41 # Defaults to self.vocabulary.displayname.41 # Defaults to self.vocabulary.displayname.
42 header = None42 header = None
4343
@@ -79,19 +79,20 @@
7979
80 def inputField(self):80 def inputField(self):
81 d = {81 d = {
82 'formToken' : cgi.escape(self.formToken, quote=True),82 'formToken': cgi.escape(self.formToken, quote=True),
83 'name': self.name,83 'name': self.name,
84 'displayWidth': self.displayWidth,84 'displayWidth': self.displayWidth,
85 'displayMaxWidth': self.displayMaxWidth,85 'displayMaxWidth': self.displayMaxWidth,
86 'onKeyPress': self.onKeyPress,86 'onKeyPress': self.onKeyPress,
87 'style': self.style,87 'style': self.style,
88 'cssClass': self.cssClass88 'cssClass': self.cssClass,
89 }89 }
90 return """<input type="text" value="%(formToken)s" id="%(name)s"90 return """<input type="text" value="%(formToken)s" id="%(name)s"
91 name="%(name)s" size="%(displayWidth)s"91 name="%(name)s" size="%(displayWidth)s"
92 maxlength="%(displayMaxWidth)s"92 maxlength="%(displayMaxWidth)s"
93 onKeyPress="%(onKeyPress)s" style="%(style)s"93 onKeyPress="%(onKeyPress)s" style="%(style)s"
94 class="%(cssClass)s" />""" % d94 class="%(cssClass)s" />""" % d
95
95 @property96 @property
96 def suffix(self):97 def suffix(self):
97 return self.name.replace('.', '-')98 return self.name.replace('.', '-')
@@ -126,23 +127,31 @@
126 % (choice.context, choice.__name__))127 % (choice.context, choice.__name__))
127 return choice.vocabularyName128 return choice.vocabularyName
128129
129 def chooseLink(self):130 def js_template_args(self):
130 js_file = os.path.join(os.path.dirname(__file__),131 """return a dict of args to configure the picker javascript."""
131 'templates/vocabulary-picker.js.template')
132 js_template = open(js_file).read()
133
134 if self.header is None:132 if self.header is None:
135 header = self.vocabulary.displayname133 header = self.vocabulary.displayname
136 else:134 else:
137 header = self.header135 header = self.header
138136
139 args = dict(137 if self.step_title is None:
138 step_title = self.vocabulary.step_title
139 else:
140 step_title = self.step_title
141
142 return dict(
140 vocabulary=self.vocabulary_name,143 vocabulary=self.vocabulary_name,
141 header=header,144 header=header,
142 step_title=self.step_title,145 step_title=step_title,
143 show_widget_id=self.show_widget_id,146 show_widget_id=self.show_widget_id,
144 input_id=self.name,147 input_id=self.name,
145 extra_no_results_message=self.extra_no_results_message)148 extra_no_results_message=self.extra_no_results_message)
149
150 def chooseLink(self):
151 js_file = os.path.join(os.path.dirname(__file__),
152 'templates/vocabulary-picker.js.template')
153 js_template = open(js_file).read()
154 args = self.js_template_args()
146 js = js_template % simplejson.dumps(args)155 js = js_template % simplejson.dumps(args)
147 # If the YUI widget or javascript is not supported in the browser,156 # If the YUI widget or javascript is not supported in the browser,
148 # it will degrade to being this "Find..." link instead of the157 # it will degrade to being this "Find..." link instead of the
@@ -154,8 +163,7 @@
154 css = ''163 css = ''
155 return ('<span class="%s">(<a id="%s" href="/people/">'164 return ('<span class="%s">(<a id="%s" href="/people/">'
156 'Find&hellip;</a>)</span>'165 'Find&hellip;</a>)</span>'
157 '\n<script>\n%s\n</script>'166 '\n<script>\n%s\n</script>') % (css, self.show_widget_id, js)
158 ) % (css, self.show_widget_id, js)
159167
160 @property168 @property
161 def nonajax_uri(self):169 def nonajax_uri(self):
162170
=== modified file 'lib/lp/answers/vocabulary.py'
--- lib/lp/answers/vocabulary.py 2010-08-20 20:31:18 +0000
+++ lib/lp/answers/vocabulary.py 2010-12-03 00:04:45 +0000
@@ -24,6 +24,7 @@
24 implements(IHugeVocabulary)24 implements(IHugeVocabulary)
2525
26 displayname = 'Select a FAQ'26 displayname = 'Select a FAQ'
27 step_title = 'Search'
2728
28 def __init__(self, context):29 def __init__(self, context):
29 """Create a new vocabulary for the context.30 """Create a new vocabulary for the context.
@@ -72,5 +73,3 @@
72 """See `IHugeVocabulary`."""73 """See `IHugeVocabulary`."""
73 results = self.context.findSimilarFAQs(query)74 results = self.context.findSimilarFAQs(query)
74 return CountableIterator(results.count(), results, self.toTerm)75 return CountableIterator(results.count(), results, self.toTerm)
75
76
7776
=== added directory 'lib/lp/app/widgets'
=== added file 'lib/lp/app/widgets/__init__.py'
=== added directory 'lib/lp/app/widgets/tests'
=== added file 'lib/lp/app/widgets/tests/__init__.py'
=== added file 'lib/lp/app/widgets/tests/test_popup.py'
--- lib/lp/app/widgets/tests/test_popup.py 1970-01-01 00:00:00 +0000
+++ lib/lp/app/widgets/tests/test_popup.py 2010-12-03 00:04:45 +0000
@@ -0,0 +1,43 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4__metaclass__ = type
5
6from zope.schema.vocabulary import getVocabularyRegistry
7
8from canonical.launchpad.webapp.servers import LaunchpadTestRequest
9from canonical.testing.layers import DatabaseFunctionalLayer
10from canonical.widgets.popup import VocabularyPickerWidget
11from lp.registry.interfaces.person import ITeam
12from lp.testing import TestCaseWithFactory
13
14
15class TestVocabularyPickerWidget(TestCaseWithFactory):
16
17 layer = DatabaseFunctionalLayer
18
19 def setUp(self):
20 super(TestVocabularyPickerWidget, self).setUp()
21 context = self.factory.makeTeam()
22 field = ITeam['teamowner']
23 self.bound_field = field.bind(context)
24 vocabulary_registry = getVocabularyRegistry()
25 self.vocabulary = vocabulary_registry.get(context, 'ValidTeamOwner')
26 self.request = LaunchpadTestRequest()
27
28 def test_js_template_args(self):
29 picker_widget = VocabularyPickerWidget(
30 self.bound_field, self.vocabulary, self.request)
31 js_template_args = picker_widget.js_template_args()
32 self.assertEqual(
33 'ValidTeamOwner', js_template_args['vocabulary'])
34 self.assertEqual(
35 self.vocabulary.displayname, js_template_args['header'])
36 self.assertEqual(
37 self.vocabulary.step_title, js_template_args['step_title'])
38 self.assertEqual(
39 'show-widget-field-teamowner', js_template_args['show_widget_id'])
40 self.assertEqual(
41 'field.teamowner', js_template_args['input_id'])
42 self.assertEqual(
43 None, js_template_args['extra_no_results_message'])
044
=== modified file 'lib/lp/blueprints/vocabularies/specificationdependency.py'
--- lib/lp/blueprints/vocabularies/specificationdependency.py 2010-11-01 03:36:04 +0000
+++ lib/lp/blueprints/vocabularies/specificationdependency.py 2010-12-03 00:04:45 +0000
@@ -58,6 +58,7 @@
58 _table = Specification58 _table = Specification
59 _orderBy = 'name'59 _orderBy = 'name'
60 displayname = 'Select a blueprint'60 displayname = 'Select a blueprint'
61 step_title = 'Search'
6162
62 def _is_valid_candidate(self, spec, check_target=False):63 def _is_valid_candidate(self, spec, check_target=False):
63 """Is `spec` a valid candidate spec for self.context?64 """Is `spec` a valid candidate spec for self.context?
6465
=== modified file 'lib/lp/code/vocabularies/branch.py'
--- lib/lp/code/vocabularies/branch.py 2010-09-20 01:06:36 +0000
+++ lib/lp/code/vocabularies/branch.py 2010-12-03 00:04:45 +0000
@@ -44,6 +44,7 @@
44 _table = Branch44 _table = Branch
45 _orderBy = ['name', 'id']45 _orderBy = ['name', 'id']
46 displayname = 'Select a branch'46 displayname = 'Select a branch'
47 step_title = 'Search'
4748
48 def toTerm(self, branch):49 def toTerm(self, branch):
49 """The display should include the URL if there is one."""50 """The display should include the URL if there is one."""
5051
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-11-30 20:34:31 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-12-03 00:04:45 +0000
@@ -1127,7 +1127,7 @@
1127 template="../templates/team-portlet-map.pt"/>1127 template="../templates/team-portlet-map.pt"/>
1128 <browser:page1128 <browser:page
1129 for="lp.registry.interfaces.person.ITeam"1129 for="lp.registry.interfaces.person.ITeam"
1130 class="lp.registry.browser.person.PersonIndexView"1130 class="lp.registry.browser.person.TeamIndexView"
1131 permission="zope.Public"1131 permission="zope.Public"
1132 name="+portlet-membership"1132 name="+portlet-membership"
1133 template="../templates/team-portlet-membership.pt"/>1133 template="../templates/team-portlet-membership.pt"/>
11341134
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py 2010-11-30 12:53:20 +0000
+++ lib/lp/registry/browser/person.py 2010-12-03 00:04:45 +0000
@@ -3567,6 +3567,13 @@
3567 return 'portlet'3567 return 'portlet'
3568 return 'portlet private'3568 return 'portlet private'
35693569
3570 @property
3571 def add_member_step_title(self):
3572 """A string for setup_add_member_handler with escaped quotes."""
3573 vocabulary_registry = getVocabularyRegistry()
3574 vocabulary = vocabulary_registry.get(self.context, 'ValidTeamMember')
3575 return vocabulary.step_title.replace("'", "\\'").replace('"', '\\"')
3576
35703577
3571class PersonCodeOfConductEditView(LaunchpadView):3578class PersonCodeOfConductEditView(LaunchpadView):
3572 """View for the ~person/+codesofconduct pages."""3579 """View for the ~person/+codesofconduct pages."""
35733580
=== modified file 'lib/lp/registry/browser/tests/test_team.py'
--- lib/lp/registry/browser/tests/test_team.py 2010-11-23 04:42:24 +0000
+++ lib/lp/registry/browser/tests/test_team.py 2010-12-03 00:04:45 +0000
@@ -135,3 +135,17 @@
135 self.assertEqual(135 self.assertEqual(
136 "You can't add a team that doesn't have any active members.",136 "You can't add a team that doesn't have any active members.",
137 view.errors[0])137 view.errors[0])
138
139
140class TestTeamIndexView(TestCaseWithFactory):
141
142 layer = DatabaseFunctionalLayer
143
144 def setUp(self):
145 super(TestTeamIndexView, self).setUp()
146 self.team = self.factory.makeTeam(name='test-team')
147 login_person(self.team.teamowner)
148
149 def test_add_member_step_title(self):
150 view = create_initialized_view(self.team, '+index')
151 self.assertEqual('Search', view.add_member_step_title)
138152
=== modified file 'lib/lp/registry/doc/vocabularies.txt'
--- lib/lp/registry/doc/vocabularies.txt 2010-11-05 14:35:36 +0000
+++ lib/lp/registry/doc/vocabularies.txt 2010-12-03 00:04:45 +0000
@@ -1052,103 +1052,11 @@
1052 [(u'Ubuntu Team', u'Mark Shuttleworth')]1052 [(u'Ubuntu Team', u'Mark Shuttleworth')]
10531053
10541054
1055=== ValidTeamMember ===
1056
1057With the exception of all teams that have this team as a member and the
1058team itself, all valid persons and teams are valid members.
1059
1060 >>> login('foo.bar@canonical.com')
1061 >>> team = person_set.getByName('ubuntu-team')
1062 >>> team2 = person_set.getByName('guadamen')
1063 >>> person = person_set.getByName('name16')
1064
1065ValidTeamMember needs a context:
1066
1067 >>> vocab = get_naked_vocab(None, "ValidTeamMember")
1068 Traceback (most recent call last):
1069 ...
1070 AssertionError: ...
1071
1072ValidTeamMember's context must implement ITeam:
1073
1074 >>> vocab = get_naked_vocab(person, "ValidTeamMember")
1075 Traceback (most recent call last):
1076 ...
1077 AssertionError: ...
1078
1079'name16' is a valid member for 'ubuntu-team':
1080
1081 >>> vocab = get_naked_vocab(team, "ValidTeamMember")
1082 >>> vocab.displayname
1083 'Select a Person or Team'
1084
1085 >>> person in vocab
1086 True
1087 >>> [person.name for person in vocab.search('foo.bar')]
1088 [u'name16']
1089
1090'ubuntu-team' is not a valid member for itself:
1091
1092 >>> team in vocab
1093 False
1094 >>> [person.name for person in vocab.search('ubuntu-team')]
1095 [u'name18', u'ubuntu-security']
1096
1097'ubuntu-team' is a member of 'guadamen', so 'guadamen' can't be a member
1098of 'ubuntu-team'.
1099
1100 >>> team2 in vocab
1101 False
1102 >>> [person.name for person in vocab.search('guadamen')]
1103 []
1104
1105
1106=== ValidTeamOwner ===
1107
1108With the exception of the team itself and all teams owned by that team,
1109all valid persons and teams are valid owners for the team in context.
1110
1111ValidTeamOwner needs a context.
1112
1113 >>> vocab = get_naked_vocab(None, "ValidTeamOwner")
1114 Traceback (most recent call last):
1115 ...
1116 AssertionError: ...
1117
1118ValidTeamOwner's context must provide IPerson or IPersonSet.
1119
1120 >>> get_naked_vocab(team, "ValidTeamOwner")
1121 <...ValidTeamOwnerVocabulary...
1122 >>> get_naked_vocab(person_set, "ValidTeamOwner")
1123 <...ValidTeamOwnerVocabulary...
1124 >>> get_naked_vocab(firefox, "ValidTeamOwner")
1125 Traceback (most recent call last):
1126 ...
1127 AssertionError: ...
1128
1129'ubuntu-team' is not a valid owner for itself.
1130
1131 >>> vocab = get_naked_vocab(team, "ValidTeamOwner")
1132 >>> vocab.displayname
1133 'Select a Person or Team'
1134
1135 >>> team in vocab
1136 False
1137 >>> [person.name for person in vocab.search('ubuntu-team')]
1138 [u'name18', u'ubuntu-security']
1139
1140'name16' is a valid owner for 'ubuntu-team'.
1141
1142 >>> person in vocab
1143 True
1144 >>> [person.name for person in vocab.search('foo.bar')]
1145 [u'name16']
1146
1147
1148=== ValidPerson ===1055=== ValidPerson ===
11491056
1150All 'valid' persons who are not a team.1057All 'valid' persons who are not a team.
11511058
1059 >>> login('foo.bar@canonical.com')
1152 >>> vocab = get_naked_vocab(None, "ValidPerson")1060 >>> vocab = get_naked_vocab(None, "ValidPerson")
1153 >>> vocab.displayname1061 >>> vocab.displayname
1154 'Select a Person'1062 'Select a Person'
11551063
=== modified file 'lib/lp/registry/javascript/team.js'
--- lib/lp/registry/javascript/team.js 2010-10-21 22:06:57 +0000
+++ lib/lp/registry/javascript/team.js 2010-12-03 00:04:45 +0000
@@ -14,14 +14,14 @@
14 *14 *
15 * @method setup_add_member_handler15 * @method setup_add_member_handler
16 */16 */
17module.setup_add_member_handler = function() {17module.setup_add_member_handler = function(step_title) {
18 if (Y.UA.ie) {18 if (Y.UA.ie) {
19 return;19 return;
20 }20 }
2121
22 var config = {22 var config = {
23 header: 'Add a member',23 header: 'Add a member',
24 step_title: 'Search',24 step_title: step_title,
25 picker_activator: '.menu-link-add_member'25 picker_activator: '.menu-link-add_member'
26 };26 };
2727
2828
=== modified file 'lib/lp/registry/templates/team-portlet-membership.pt'
--- lib/lp/registry/templates/team-portlet-membership.pt 2010-11-10 15:33:47 +0000
+++ lib/lp/registry/templates/team-portlet-membership.pt 2010-12-03 00:04:45 +0000
@@ -101,14 +101,16 @@
101 <table style="margin: 0px 0px .5em 0px;">101 <table style="margin: 0px 0px .5em 0px;">
102 <tr>102 <tr>
103 <td style="padding: 0px 1em 1em 0px;"103 <td style="padding: 0px 1em 1em 0px;"
104 tal:define="link context/menu:overview/add_member"104 tal:define="link context/menu:overview/add_member;
105 step_title view/add_member_step_title;"
105 tal:condition="link/enabled">106 tal:condition="link/enabled">
106 <script type="text/javascript"107 <script type="text/javascript"
107 tal:content="string:108 tal:content="string:
108 LPS.use('lp.registry.team', function(Y) {109 LPS.use('lp.registry.team', function(Y) {
109 Y.on('load',110 Y.on('load',
110 function(e) {111 function(e) {
111 Y.lp.registry.team.setup_add_member_handler();112 Y.lp.registry.team.setup_add_member_handler(
113 '${step_title}');
112 },114 },
113 window);115 window);
114 });116 });
115117
=== modified file 'lib/lp/registry/tests/test_person_vocabularies.py'
--- lib/lp/registry/tests/test_person_vocabularies.py 2010-10-20 15:45:40 +0000
+++ lib/lp/registry/tests/test_person_vocabularies.py 2010-12-03 00:04:45 +0000
@@ -11,36 +11,117 @@
1111
12from canonical.launchpad.ftests import login_person12from canonical.launchpad.ftests import login_person
13from canonical.testing.layers import DatabaseFunctionalLayer13from canonical.testing.layers import DatabaseFunctionalLayer
14from lp.registry.interfaces.person import PersonVisibility14from lp.registry.interfaces.person import (
15 PersonVisibility,
16 TeamSubscriptionPolicy,
17 )
15from lp.testing import TestCaseWithFactory18from lp.testing import TestCaseWithFactory
1619
1720
18class TestValidTeamMemberVocabulary(TestCaseWithFactory):21class VocabularyTestBase:
22
23 vocabulary_name = None
24
25 def setUp(self):
26 super(VocabularyTestBase, self).setUp()
27 self.vocabulary_registry = getVocabularyRegistry()
28
29 def getVocabulary(self, context):
30 return self.vocabulary_registry.get(context, self.vocabulary_name)
31
32 def searchVocabulary(self, context, text):
33 Store.of(context).flush()
34 vocabulary = self.getVocabulary(context)
35 return removeSecurityProxy(vocabulary)._doSearch(text)
36
37 def test_open_team_cannot_be_a_member_of_a_closed_team(self):
38 context_team = self.factory.makeTeam(
39 subscription_policy=TeamSubscriptionPolicy.MODERATED)
40 open_team = self.factory.makeTeam(
41 subscription_policy=TeamSubscriptionPolicy.OPEN)
42 moderated_team = self.factory.makeTeam(
43 subscription_policy=TeamSubscriptionPolicy.MODERATED)
44 restricted_team = self.factory.makeTeam(
45 subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
46 user = self.factory.makePerson()
47 all_possible_members = self.searchVocabulary(context_team, '')
48 self.assertNotIn(open_team, all_possible_members)
49 self.assertIn(moderated_team, all_possible_members)
50 self.assertIn(restricted_team, all_possible_members)
51 self.assertIn(user, all_possible_members)
52
53 def test_open_team_can_be_a_member_of_an_open_team(self):
54 context_team = self.factory.makeTeam(
55 subscription_policy=TeamSubscriptionPolicy.OPEN)
56 open_team = self.factory.makeTeam(
57 subscription_policy=TeamSubscriptionPolicy.OPEN)
58 moderated_team = self.factory.makeTeam(
59 subscription_policy=TeamSubscriptionPolicy.MODERATED)
60 restricted_team = self.factory.makeTeam(
61 subscription_policy=TeamSubscriptionPolicy.RESTRICTED)
62 user = self.factory.makePerson()
63 all_possible_members = self.searchVocabulary(context_team, '')
64 self.assertIn(open_team, all_possible_members)
65 self.assertIn(moderated_team, all_possible_members)
66 self.assertIn(restricted_team, all_possible_members)
67 self.assertIn(user, all_possible_members)
68
69 def test_vocabulary_displayname(self):
70 context_team = self.factory.makeTeam(
71 subscription_policy=TeamSubscriptionPolicy.OPEN)
72 vocabulary = self.getVocabulary(context_team)
73 self.assertEqual(
74 'Select a Team or Person', vocabulary.displayname)
75
76 def test_open_team_vocabulary_step_title(self):
77 context_team = self.factory.makeTeam(
78 subscription_policy=TeamSubscriptionPolicy.OPEN)
79 vocabulary = self.getVocabulary(context_team)
80 self.assertEqual('Search', vocabulary.step_title)
81
82 def test_closed_team_vocabulary_step_title(self):
83 context_team = self.factory.makeTeam(
84 subscription_policy=TeamSubscriptionPolicy.MODERATED)
85 vocabulary = self.getVocabulary(context_team)
86 self.assertEqual(
87 'Search for a restricted team, a moderated team, or a person',
88 vocabulary.step_title)
89
90
91class TestValidTeamMemberVocabulary(VocabularyTestBase, TestCaseWithFactory):
19 """Test that the ValidTeamMemberVocabulary behaves as expected."""92 """Test that the ValidTeamMemberVocabulary behaves as expected."""
2093
21 layer = DatabaseFunctionalLayer94 layer = DatabaseFunctionalLayer
2295 vocabulary_name = 'ValidTeamMember'
23 def searchVocabulary(self, team, text):
24 vocabulary_registry = getVocabularyRegistry()
25 naked_vocabulary = removeSecurityProxy(
26 vocabulary_registry.get(team, 'ValidTeamMember'))
27 return naked_vocabulary._doSearch(text)
2896
29 def test_public_team_cannot_be_a_member_of_itself(self):97 def test_public_team_cannot_be_a_member_of_itself(self):
30 # A public team should be filtered by the vocab.extra_clause98 # A public team should be filtered by the vocab.extra_clause
31 # when provided a search term.99 # when provided a search term.
32 team_owner = self.factory.makePerson()100 team = self.factory.makeTeam()
33 login_person(team_owner)101 self.assertNotIn(team, self.searchVocabulary(team, team.name))
34 team = self.factory.makeTeam(owner=team_owner)
35 Store.of(team).flush()
36 self.assertFalse(team in self.searchVocabulary(team, team.name))
37102
38 def test_private_team_cannot_be_a_member_of_itself(self):103 def test_private_team_cannot_be_a_member_of_itself(self):
39 # A private team should be filtered by the vocab.extra_clause104 # A private team should be filtered by the vocab.extra_clause
40 # when provided a search term.105 # when provided a search term.
41 team_owner = self.factory.makePerson()
42 login_person(team_owner)
43 team = self.factory.makeTeam(106 team = self.factory.makeTeam(
44 owner=team_owner, visibility=PersonVisibility.PRIVATE)107 visibility=PersonVisibility.PRIVATE)
45 Store.of(team).flush()108 login_person(team.teamowner)
46 self.assertFalse(team in self.searchVocabulary(team, team.name))109 self.assertNotIn(team, self.searchVocabulary(team, team.name))
110
111
112class TestValidTeamOwnerVocabulary(VocabularyTestBase, TestCaseWithFactory):
113 """Test that the ValidTeamOwnerVocabulary behaves as expected."""
114
115 layer = DatabaseFunctionalLayer
116 vocabulary_name = 'ValidTeamOwner'
117
118 def test_team_cannot_own_itself(self):
119 context_team = self.factory.makeTeam()
120 results = self.searchVocabulary(context_team, context_team.name)
121 self.assertNotIn(context_team, results)
122
123 def test_team_cannot_own_its_owner(self):
124 context_team = self.factory.makeTeam()
125 owned_team = self.factory.makeTeam(owner=context_team)
126 results = self.searchVocabulary(context_team, owned_team.name)
127 self.assertNotIn(owned_team, results)
47128
=== modified file 'lib/lp/registry/vocabularies.py'
--- lib/lp/registry/vocabularies.py 2010-11-09 08:38:23 +0000
+++ lib/lp/registry/vocabularies.py 2010-12-03 00:04:45 +0000
@@ -47,6 +47,7 @@
47 'SourcePackageNameVocabulary',47 'SourcePackageNameVocabulary',
48 'UserTeamsParticipationVocabulary',48 'UserTeamsParticipationVocabulary',
49 'UserTeamsParticipationPlusSelfVocabulary',49 'UserTeamsParticipationPlusSelfVocabulary',
50 'ValidPersonOrClosedTeamVocabulary',
50 'ValidPersonOrTeamVocabulary',51 'ValidPersonOrTeamVocabulary',
51 'ValidPersonVocabulary',52 'ValidPersonVocabulary',
52 'ValidTeamMemberVocabulary',53 'ValidTeamMemberVocabulary',
@@ -143,6 +144,7 @@
143 IPersonSet,144 IPersonSet,
144 ITeam,145 ITeam,
145 PersonVisibility,146 PersonVisibility,
147 TeamSubscriptionPolicy,
146 )148 )
147from lp.registry.interfaces.pillar import IPillarName149from lp.registry.interfaces.pillar import IPillarName
148from lp.registry.interfaces.product import (150from lp.registry.interfaces.product import (
@@ -219,6 +221,7 @@
219class ProductVocabulary(SQLObjectVocabularyBase):221class ProductVocabulary(SQLObjectVocabularyBase):
220 """All `IProduct` objects vocabulary."""222 """All `IProduct` objects vocabulary."""
221 implements(IHugeVocabulary)223 implements(IHugeVocabulary)
224 step_title = 'Search'
222225
223 _table = Product226 _table = Product
224 _orderBy = 'displayname'227 _orderBy = 'displayname'
@@ -271,6 +274,7 @@
271 _table = ProjectGroup274 _table = ProjectGroup
272 _orderBy = 'displayname'275 _orderBy = 'displayname'
273 displayname = 'Select a project group'276 displayname = 'Select a project group'
277 step_title = 'Search'
274278
275 def __contains__(self, obj):279 def __contains__(self, obj):
276 where = "active='t' and id=%d"280 where = "active='t' and id=%d"
@@ -361,6 +365,7 @@
361365
362 _orderBy = ['displayname']366 _orderBy = ['displayname']
363 displayname = 'Select a Person or Team'367 displayname = 'Select a Person or Team'
368 step_title = 'Search'
364369
365 def __contains__(self, obj):370 def __contains__(self, obj):
366 return obj in self._select()371 return obj in self._select()
@@ -391,6 +396,7 @@
391396
392 _orderBy = ['displayname']397 _orderBy = ['displayname']
393 displayname = 'Select a Person to Merge'398 displayname = 'Select a Person to Merge'
399 step_title = 'Search'
394 must_have_email = True400 must_have_email = True
395401
396 def __contains__(self, obj):402 def __contains__(self, obj):
@@ -439,7 +445,7 @@
439 implements(IHugeVocabulary)445 implements(IHugeVocabulary)
440446
441 displayname = 'Select a Person or Team'447 displayname = 'Select a Person or Team'
442448 step_title = 'Search'
443 # This is what subclasses must change if they want any extra filtering of449 # This is what subclasses must change if they want any extra filtering of
444 # results.450 # results.
445 extra_clause = True451 extra_clause = True
@@ -722,11 +728,32 @@
722 cache_table_name = 'ValidPersonCache'728 cache_table_name = 'ValidPersonCache'
723729
724730
725class ValidTeamMemberVocabulary(ValidPersonOrTeamVocabulary):731class TeamVocabularyMixin:
732 """Common methods for team vocabularies."""
733
734 displayname = 'Select a Team or Person'
735
736 @property
737 def is_closed_team(self):
738 return self.team.subscriptionpolicy != TeamSubscriptionPolicy.OPEN
739
740 @property
741 def step_title(self):
742 """See `IHugeVocabulary`."""
743 if self.is_closed_team:
744 return (
745 'Search for a restricted team, a moderated team, or a person')
746 else:
747 return 'Search'
748
749
750class ValidTeamMemberVocabulary(TeamVocabularyMixin,
751 ValidPersonOrTeamVocabulary):
726 """The set of valid members of a given team.752 """The set of valid members of a given team.
727753
728 With the exception of all teams that have this team as a member and the754 With the exception of all teams that have this team as a member and the
729 team itself, all valid persons and teams are valid members.755 team itself, all valid persons and teams are valid members. Restricted
756 and moderated teams cannot have open teams as members.
730 """757 """
731758
732 def __init__(self, context):759 def __init__(self, context):
@@ -740,19 +767,29 @@
740 "Got %s" % str(context))767 "Got %s" % str(context))
741768
742 ValidPersonOrTeamVocabulary.__init__(self, context)769 ValidPersonOrTeamVocabulary.__init__(self, context)
743 self.extra_clause = """770
771 @property
772 def extra_clause(self):
773 clause = SQL("""
744 Person.id NOT IN (774 Person.id NOT IN (
745 SELECT team FROM TeamParticipation775 SELECT team FROM TeamParticipation
746 WHERE person = %d776 WHERE person = %d
747 )777 )
748 """ % self.team.id778 """ % self.team.id)
749779 if self.is_closed_team:
750780 clause = And(
751class ValidTeamOwnerVocabulary(ValidPersonOrTeamVocabulary):781 clause,
782 Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)
783 return clause
784
785
786class ValidTeamOwnerVocabulary(TeamVocabularyMixin,
787 ValidPersonOrTeamVocabulary):
752 """The set of Persons/Teams that can be owner of a team.788 """The set of Persons/Teams that can be owner of a team.
753789
754 With the exception of the team itself and all teams owned by that team,790 With the exception of the team itself and all teams owned by that team,
755 all valid persons and teams are valid owners for the team.791 all valid persons and teams are valid owners for the team. Restricted
792 and moderated teams cannot have open teams as members.
756 """793 """
757794
758 def __init__(self, context):795 def __init__(self, context):
@@ -760,9 +797,7 @@
760 raise AssertionError('ValidTeamOwnerVocabulary needs a context.')797 raise AssertionError('ValidTeamOwnerVocabulary needs a context.')
761798
762 if IPerson.providedBy(context):799 if IPerson.providedBy(context):
763 self.extra_clause = """800 self.team = context
764 (person.teamowner != %d OR person.teamowner IS NULL) AND
765 person.id != %d""" % (context.id, context.id)
766 elif IPersonSet.providedBy(context):801 elif IPersonSet.providedBy(context):
767 # The context is an IPersonSet, which means we're creating a new802 # The context is an IPersonSet, which means we're creating a new
768 # team and thus we don't need any extra_clause --any valid person803 # team and thus we don't need any extra_clause --any valid person
@@ -774,6 +809,17 @@
774 "or IPersonSet.")809 "or IPersonSet.")
775 ValidPersonOrTeamVocabulary.__init__(self, context)810 ValidPersonOrTeamVocabulary.__init__(self, context)
776811
812 @property
813 def extra_clause(self):
814 clause = SQL("""
815 (person.teamowner != %d OR person.teamowner IS NULL) AND
816 person.id != %d""" % (self.team.id, self.team.id))
817 if self.is_closed_team:
818 clause = And(
819 clause,
820 Person.subscriptionpolicy != TeamSubscriptionPolicy.OPEN)
821 return clause
822
777823
778class AllUserTeamsParticipationVocabulary(ValidTeamVocabulary):824class AllUserTeamsParticipationVocabulary(ValidTeamVocabulary):
779 """The set of teams where the current user is a member.825 """The set of teams where the current user is a member.
@@ -844,6 +890,7 @@
844 implements(IHugeVocabulary)890 implements(IHugeVocabulary)
845891
846 displayname = 'Select an active mailing list.'892 displayname = 'Select an active mailing list.'
893 step_title = 'Search'
847894
848 def __init__(self, context):895 def __init__(self, context):
849 assert context is None, (896 assert context is None, (
@@ -965,6 +1012,7 @@
965 implements(IHugeVocabulary)1012 implements(IHugeVocabulary)
9661013
967 displayname = 'Select a Product Release'1014 displayname = 'Select a Product Release'
1015 step_title = 'Search'
968 _table = ProductRelease1016 _table = ProductRelease
969 # XXX carlos Perello Marin 2005-05-16 bugs=687:1017 # XXX carlos Perello Marin 2005-05-16 bugs=687:
970 # Sorting by version won't give the expected results, because it's just a1018 # Sorting by version won't give the expected results, because it's just a
@@ -1032,6 +1080,7 @@
1032 implements(IHugeVocabulary)1080 implements(IHugeVocabulary)
10331081
1034 displayname = 'Select a Release Series'1082 displayname = 'Select a Release Series'
1083 step_title = 'Search'
1035 _table = ProductSeries1084 _table = ProductSeries
1036 _order_by = [Product.name, ProductSeries.name]1085 _order_by = [Product.name, ProductSeries.name]
1037 _clauseTables = ['Product']1086 _clauseTables = ['Product']
@@ -1265,6 +1314,7 @@
12651314
1266 _table = Product1315 _table = Product
1267 _orderBy = 'displayname'1316 _orderBy = 'displayname'
1317 step_title = 'Search'
12681318
1269 @property1319 @property
1270 def displayname(self):1320 def displayname(self):