Merge lp:~abentley/launchpad/select-owner into lp:launchpad

Proposed by Aaron Bentley on 2010-11-10
Status: Merged
Approved by: Gavin Panella on 2010-11-11
Approved revision: no longer in the source branch.
Merged at revision: 11948
Proposed branch: lp:~abentley/launchpad/select-owner
Merge into: lp:launchpad
Diff against target: 477 lines (+205/-108)
4 files modified
lib/canonical/widgets/suggestion.py (+169/-103)
lib/lp/code/browser/branch.py (+1/-1)
lib/lp/code/browser/sourcepackagerecipe.py (+2/-1)
lib/lp/code/browser/tests/test_sourcepackagerecipe.py (+33/-3)
To merge this branch: bzr merge lp:~abentley/launchpad/select-owner
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve on 2010-11-11
Curtis Hovey (community) ui 2010-11-10 Approve on 2010-11-11
Guilherme Salgado (community) ui* 2010-11-10 Approve on 2010-11-10
Review via email: mp+40556@code.launchpad.net

Commit Message

Suggest branch owner as recipe owner

Description of the Change

= Summary =
Fix bug #670431: Make it easy to select branch owner as recipe owner when
creating recipe

== Proposed fix ==
Provide a list of suggested owners that includes the logged-in user and, if the
logged-in-user is a member of the branch owner, the branch owner. The normal
list of potential owners is provided as a secondary choice.

See: https://code.launchpad.net/~abentley/launchpad/select-owner

== Pre-implementation notes ==
None

== Implementation details ==
Extracted a SuggestionWidget from TargetBranchWidget, and implemented
RecipeOwnerWidget in terms of SuggestionWidget.

== Tests ==
bin/test -t test_create_new_recipe

== Demo and Q/A ==
Create a branch owned by a team you are a member of. Choose "Create a
packaging recipe". Both you and the owning team should be suggested as
owners.

Leave the team. Now only you should be suggested as an owner.

Create a recipe using a suggested owner.
Create a recipe using "Other".

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/browser/branch.py
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py
  lib/canonical/widgets/suggestion.py

To post a comment you must log in.
Guilherme Salgado (salgado) wrote :

Looks good to me. The only improvement I can think of would be to place the dropdown beside the 'Other' label rather than below it. It may not be worth the effort if it's too difficult to do so, though.

review: Approve (ui*)
Aaron Bentley (abentley) wrote :
Curtis Hovey (sinzui) wrote :

We discussed making the suggestion menu be subordinate to the (*) other radio item, but our attempt broke other things. I think this is a bigger form CSS issue that needs a separate fix. So this UI is good to land.

review: Approve (ui)
Aaron Bentley (abentley) wrote :

Actually, I was able to make the change such that only RecipeOwnerWidget was affected.

Gavin Panella (allenap) wrote :

Looks good! Some quite trivial comments.

[1]

+ @classmethod
+ def _generateSuggestionVocab(cls, context, full_vocabulary):

Any reason this is a class method? Not complaining, just wondering.

[2]

+ terms = [term for term in full_vocabulary
+ if term.value in suggestions]

Indentation is a bit off.

[3]

+ def _renderSuggestions(self):

This seems like an imperative name, "go and render the
suggestions". Perhaps _shouldRenderSuggestions()? Feel free to ignore
me too :)

[4]

In http://people.canonical.com/~abentley/suggest-owner.png the
drop-down box has "Foo Bar", but that's already a radio item. I think
it's confusing, or perhaps ugly, to repeat choices. That's more of a
UI thing though, and this is meant to be a code review :)

[5]

+ text = '%s (<a href="%s">branch details</a>)' % (
+ branch.displayname, canonical_url(branch))

Even if branch.displayname cannot be invalid, please cgi.escape() it,
same for the branch URL. If the implementation of displayname ever
changes it will continue to be safe.

[6]

+ return u'<label for="%s" style="font-weight: normal">%s</label>' % (
+ option_id, text)

Same as [6] for option_id.

[7]

+ def _autoselect_other(self):
...
+ def _get_suggestions(branch):

Other methods are camel-case, so these probably should be too.

review: Approve
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11/11/2010 12:32 PM, Gavin Panella wrote:
> Review: Approve
> Looks good! Some quite trivial comments.
>
>
> [1]
>
> + @classmethod
> + def _generateSuggestionVocab(cls, context, full_vocabulary):
>
> Any reason this is a class method? Not complaining, just wondering.

It needs to be a method to support polymorphism. I think it doesn't
make sense for something to be an instance method when it doesn't use
the instance. It does use the class, so it can't be a staticmethod.

> [2]
>
> + terms = [term for term in full_vocabulary
> + if term.value in suggestions]
>
> Indentation is a bit off.

Fixed.

> [3]
>
> + def _renderSuggestions(self):
>
> This seems like an imperative name, "go and render the
> suggestions". Perhaps _shouldRenderSuggestions()? Feel free to ignore
> me too :)

Done.

> [4]
>
> In http://people.canonical.com/~abentley/suggest-owner.png the
> drop-down box has "Foo Bar", but that's already a radio item. I think
> it's confusing, or perhaps ugly, to repeat choices. That's more of a
> UI thing though, and this is meant to be a code review :)

OTOH, if you were looking for one of your teams in the drop-down, I
think it would be confusing if it wasn't there because it was a radio item.

> [5]
>
> + text = '%s (<a href="%s">branch details</a>)' % (
> + branch.displayname, canonical_url(branch))
>
> Even if branch.displayname cannot be invalid, please cgi.escape() it,
> same for the branch URL. If the implementation of displayname ever
> changes it will continue to be safe.

Done.

> [6]
>
> + return u'<label for="%s" style="font-weight: normal">%s</label>' % (
> + option_id, text)
>
> Same as [6] for option_id.

Done, by creating a new _optionID method for the whole thing.

> [7]
>
> + def _autoselect_other(self):
> ...
> + def _get_suggestions(branch):
>
> Other methods are camel-case, so these probably should be too.

Done. Also, _other_id => _otherId.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.10 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkzcMkgACgkQ0F+nu1YWqI34kgCgiA+4dDTTGg3Cl92uYx4C32gH
aeEAoIHKcHt4gbNMM2W+OGUuH/b2/OXN
=2JzN
-----END PGP SIGNATURE-----

1=== modified file 'lib/canonical/widgets/suggestion.py'
2--- lib/canonical/widgets/suggestion.py 2010-11-11 17:19:11 +0000
3+++ lib/canonical/widgets/suggestion.py 2010-11-11 18:11:46 +0000
4@@ -10,6 +10,8 @@
5 ]
6
7
8+import cgi
9+
10 from zope.app.form.browser.widget import renderElement
11 from zope.app.form.interfaces import IInputWidget, InputErrors
12 from zope.app.form.utility import setUpWidget
13@@ -46,8 +48,8 @@
14 # If there are suggestions to show explicitly, then we want to select
15 # the 'Other' selection item when the user chooses a non-suggested
16 # value.
17- if self._renderSuggestions():
18- self._autoselect_other()
19+ if self._shouldRenderSuggestions():
20+ self._autoselectOther()
21
22 @classmethod
23 def _generateSuggestionVocab(cls, context, full_vocabulary):
24@@ -57,18 +59,23 @@
25 :param full_vocabulary: The vocabulary suggestions may be drawn from.
26 suggestions not present in this vocabulary are ignored.
27 """
28- suggestions = cls._get_suggestions(context)
29- terms = [term for term in full_vocabulary
30- if term.value in suggestions]
31+ suggestions = cls._getSuggestions(context)
32+ terms = [
33+ term for term in full_vocabulary
34+ if term.value in suggestions
35+ ]
36 return SimpleVocabulary(terms)
37
38- def _renderSuggestions(self):
39+ def _shouldRenderSuggestions(self):
40 """Return True if suggestions should be rendered."""
41 return len(self.suggestion_vocab) > 0
42
43- def _other_id(self):
44+ def _optionId(self, index):
45+ return '%s.%d' % (cgi.escape(self.name), index)
46+
47+ def _otherId(self):
48 """Return the id of the "Other" option."""
49- return '%s.%d' % (self.name, len(self.suggestion_vocab))
50+ return self._optionId(len(self.suggestion_vocab))
51
52 def _toFieldValue(self, form_value):
53 """Convert the form value into the target value.
54@@ -77,7 +84,7 @@
55 get the value from the other_selection widget, otherwise get the
56 object reference from the built up vocabulary.
57 """
58- if not self._renderSuggestions() or form_value == "other":
59+ if not self._shouldRenderSuggestions() or form_value == "other":
60 # Get the value from the other selector widget.
61 try:
62 return self.other_selection_widget.getInputValue()
63@@ -94,7 +101,7 @@
64 We need to defer the call to the other widget when either there are no
65 terms in the vocabulary or the other radio button was selected.
66 """
67- if not self._renderSuggestions():
68+ if not self._shouldRenderSuggestions():
69 return self.other_selection_widget.hasInput()
70 else:
71 has_input = LaunchpadRadioWidget.hasInput(self)
72@@ -113,9 +120,8 @@
73
74 def _renderLabel(self, text, index):
75 """Render a label for the option with the specified index."""
76- option_id = '%s.%s' % (self.name, index)
77 return u'<label for="%s" style="font-weight: normal">%s</label>' % (
78- option_id, text)
79+ self._optionId(index), text)
80
81 def _renderSuggestionLabel(self, value, index):
82 """Render a label for the option based on a branch."""
83@@ -171,7 +177,7 @@
84
85 def __call__(self):
86 """Don't render the radio buttons if only one choice."""
87- if not self._renderSuggestions():
88+ if not self._shouldRenderSuggestions():
89 return self.other_selection_widget()
90 else:
91 return LaunchpadRadioWidget.__call__(self)
92@@ -227,23 +233,21 @@
93
94 def _renderSuggestionLabel(self, branch, index):
95 """Render a label for the option based on a branch."""
96- option_id = '%s.%s' % (self.name, index)
97-
98 # To aid usability there needs to be some text connected with the
99 # radio buttons that is not a hyperlink in order to select the radio
100 # button. It was decided not to have the entire text as a link, but
101 # instead to have a separate link to the branch details.
102 text = '%s (<a href="%s">branch details</a>)' % (
103- branch.displayname, canonical_url(branch))
104+ cgi.escape(branch.displayname), cgi.escape(canonical_url(branch)))
105 # If the branch is the development focus, say so.
106 if branch == self.context.context.target.default_merge_target:
107 text = text + "&ndash; <em>development focus</em>"
108 return u'<label for="%s" style="font-weight: normal">%s</label>' % (
109- option_id, text)
110+ self._optionId(index), text)
111
112- def _autoselect_other(self):
113+ def _autoselectOther(self):
114 """Select "other" on keypress."""
115- on_key_press = "selectWidget('%s', event);" % self._other_id()
116+ on_key_press = "selectWidget('%s', event);" % self._otherId()
117 self.other_selection_widget.onKeyPress = on_key_press
118
119
120@@ -259,7 +263,7 @@
121 self.other_selection_widget.cssClass = 'subordinate'
122
123 @staticmethod
124- def _get_suggestions(branch):
125+ def _getSuggestions(branch):
126 """Suggest the branch owner and current user."""
127 logged_in_user = getUtility(ILaunchBag).user
128 return set([branch.owner, logged_in_user])
129@@ -269,7 +273,7 @@
130 """Provide a specialized displayname for Persons"""
131 return value.unique_displayname
132
133- def _autoselect_other(self):
134+ def _autoselectOther(self):
135 """Select "other" on click."""
136- on_click = "onClick=\"selectWidget('%s', event);\"" % self._other_id()
137+ on_click = "onClick=\"selectWidget('%s', event);\"" % self._otherId()
138 self.other_selection_widget.extra = on_click

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== renamed file 'lib/canonical/widgets/branch.py' => 'lib/canonical/widgets/suggestion.py'
2--- lib/canonical/widgets/branch.py 2010-11-08 12:52:43 +0000
3+++ lib/canonical/widgets/suggestion.py 2010-11-19 14:13:57 +0000
4@@ -5,10 +5,13 @@
5
6 __metaclass__ = type
7 __all__ = [
8+ 'RecipeOwnerWidget',
9 'TargetBranchWidget',
10 ]
11
12
13+import cgi
14+
15 from zope.app.form.browser.widget import renderElement
16 from zope.app.form.interfaces import IInputWidget, InputErrors
17 from zope.app.form.utility import setUpWidget
18@@ -20,109 +23,91 @@
19 from canonical.widgets.itemswidgets import LaunchpadRadioWidget
20
21
22-class TargetBranchWidget(LaunchpadRadioWidget):
23- """Widget for selecting a target branch.
24-
25- The default branch for a new branch merge proposal should be
26- the branch associated with the development focus series if there
27- is one (that isn't an import branch).
28-
29- Also in the initial radio button selector are other branches for
30- the product that the branch owner has specified as target branches
31- for other merge proposals.
32-
33- Finally there is an "other" button that gets the user to use the
34- normal branch selector.
35+class SuggestionWidget(LaunchpadRadioWidget):
36+ """Base class for widgets that suggest values.
37+
38+ Users can pick a suggested value from a radio button list, or use the
39+ normal widget to pick any value they could normally pick.
40 """
41
42 def __init__(self, field, vocabulary, request):
43 # Create the vocabulary and pass that to the radio button
44 # constructor.
45- branch = field.context
46- self.branch_selector_vocab = self._generateTargetVocab(branch)
47+ self.suggestion_vocab = self._generateSuggestionVocab(
48+ field.context, vocabulary)
49
50 LaunchpadRadioWidget.__init__(
51- self, field, self.branch_selector_vocab, request)
52+ self, field, self.suggestion_vocab, request)
53
54- self.other_branch_widget = getMultiAdapter(
55+ self.other_selection_widget = getMultiAdapter(
56 (field, request), IInputWidget)
57 setUpWidget(
58- self, 'other_branch', field, IInputWidget,
59- prefix=self.name, context=branch)
60-
61- # If there are branches to show explicitly, then we want to be
62- # able to select the 'Other' selection item when someone types
63- # in any values.
64- branch_count = len(self.branch_selector_vocab)
65- if branch_count > 0:
66- on_key_press = "selectWidget('%s.%d', event);" % (
67- self.name, branch_count)
68- self.other_branch_widget.onKeyPress = on_key_press
69-
70- def _generateTargetVocab(self, branch):
71- """Generate the vocabulary for the radio buttons.
72-
73- The generated vocabulary contains the branch associated with the
74- development series of the product if there is one, and also any other
75- branches that the user has specified before as a target for a proposed
76- merge.
77+ self, 'other_selection', field, IInputWidget,
78+ prefix=self.name, context=field.context)
79+
80+ # If there are suggestions to show explicitly, then we want to select
81+ # the 'Other' selection item when the user chooses a non-suggested
82+ # value.
83+ if self._shouldRenderSuggestions():
84+ self._autoselectOther()
85+
86+ @classmethod
87+ def _generateSuggestionVocab(cls, context, full_vocabulary):
88+ """Generate a vocabulary for the suggestions.
89+
90+ :param context: The context object to generate suggestions for.
91+ :param full_vocabulary: The vocabulary suggestions may be drawn from.
92+ suggestions not present in this vocabulary are ignored.
93 """
94- self.default_target = branch.target.default_merge_target
95- logged_in_user = getUtility(ILaunchBag).user
96- collection = branch.target.collection.targetedBy(logged_in_user)
97- collection = collection.visibleByUser(logged_in_user)
98- branches = collection.getBranches().config(distinct=True)
99- target_branches = list(branches.config(limit=5))
100- # If there is a development focus branch, make sure it is always
101- # shown, and as the first item.
102- if self.default_target is not None:
103- if self.default_target in target_branches:
104- target_branches.remove(self.default_target)
105- target_branches.insert(0, self.default_target)
106-
107- # Make sure the source branch isn't in the target_branches.
108- if branch in target_branches:
109- target_branches.remove(branch)
110-
111- terms = []
112- for branch in target_branches:
113- terms.append(SimpleTerm(
114- branch, branch.unique_name))
115-
116+ suggestions = cls._getSuggestions(context)
117+ terms = [
118+ term for term in full_vocabulary
119+ if term.value in suggestions
120+ ]
121 return SimpleVocabulary(terms)
122
123+ def _shouldRenderSuggestions(self):
124+ """Return True if suggestions should be rendered."""
125+ return len(self.suggestion_vocab) > 0
126+
127+ def _optionId(self, index):
128+ return '%s.%d' % (cgi.escape(self.name), index)
129+
130+ def _otherId(self):
131+ """Return the id of the "Other" option."""
132+ return self._optionId(len(self.suggestion_vocab))
133+
134 def _toFieldValue(self, form_value):
135- """Convert the form value into a branch.
136+ """Convert the form value into the target value.
137
138 If there were no radio button options, or 'other' was selected, then
139- get the value from the branch widget, otherwise get the branch
140- reference from the built up vocabulary.
141+ get the value from the other_selection widget, otherwise get the
142+ object reference from the built up vocabulary.
143 """
144- if (len(self.branch_selector_vocab) == 0 or
145- form_value == "other"):
146- # Get the value from the branch selector widget.
147+ if not self._shouldRenderSuggestions() or form_value == "other":
148+ # Get the value from the other selector widget.
149 try:
150- return self.other_branch_widget.getInputValue()
151+ return self.other_selection_widget.getInputValue()
152 except InputErrors:
153- self._error = self.other_branch_widget._error
154+ self._error = self.other_selection_widget._error
155 raise
156 else:
157- term = self.branch_selector_vocab.getTermByToken(form_value)
158+ term = self.suggestion_vocab.getTermByToken(form_value)
159 return term.value
160
161 def hasInput(self):
162 """Is there any input for the widget.
163
164- We need to defer the call to the other branch widget when either there
165- are no terms in the vocabulary or the other radio button was selected.
166+ We need to defer the call to the other widget when either there are no
167+ terms in the vocabulary or the other radio button was selected.
168 """
169- if len(self.branch_selector_vocab) == 0:
170- return self.other_branch_widget.hasInput()
171+ if not self._shouldRenderSuggestions():
172+ return self.other_selection_widget.hasInput()
173 else:
174 has_input = LaunchpadRadioWidget.hasInput(self)
175 if has_input:
176 if self._getFormInput() == "other":
177- return self.other_branch_widget.hasInput()
178+ return self.other_selection_widget.hasInput()
179 return has_input
180
181 def getInputValue(self):
182@@ -135,56 +120,46 @@
183
184 def _renderLabel(self, text, index):
185 """Render a label for the option with the specified index."""
186- option_id = '%s.%s' % (self.name, index)
187 return u'<label for="%s" style="font-weight: normal">%s</label>' % (
188- option_id, text)
189+ self._optionId(index), text)
190
191- def _renderBranchLabel(self, branch, index):
192+ def _renderSuggestionLabel(self, value, index):
193 """Render a label for the option based on a branch."""
194- option_id = '%s.%s' % (self.name, index)
195+ return self._renderLabel(self._valueDisplayname(value), index)
196
197- # To aid usability there needs to be some text connected with the
198- # radio buttons that is not a hyperlink in order to select the radio
199- # button. It was decided not to have the entire text as a link, but
200- # instead to have a separate link to the branch details.
201- text = '%s (<a href="%s">branch details</a>)' % (
202- branch.displayname, canonical_url(branch))
203- # If the branch is the development focus, say so.
204- if branch == self.default_target:
205- text = text + "&ndash; <em>development focus</em>"
206- return u'<label for="%s" style="font-weight: normal">%s</label>' % (
207- option_id, text)
208+ @staticmethod
209+ def _valueDisplayname(value):
210+ """Return the displayname for a value."""
211+ return value.displayname
212
213 def renderItems(self, value):
214 """Render the items for the selector."""
215 field = self.context
216- product = field.context
217 if value == self._missing:
218 value = field.missing_value
219
220 items = []
221 index = 0
222- # Render each of the branches with the first selected.
223- for index, term in enumerate(self.branch_selector_vocab):
224- branch = term.value
225+ # Render each of the suggestions with the first selected.
226+ for index, term in enumerate(self.suggestion_vocab):
227+ suggestion = term.value
228 if index == 0:
229 renderfunc = self.renderSelectedItem
230 else:
231 renderfunc = self.renderItem
232-
233+ text = self._renderSuggestionLabel(suggestion, index)
234 render_args = dict(
235- index=index, text=self._renderBranchLabel(branch, index),
236- value=branch.unique_name, name=self.name,
237- cssClass=self.cssClass)
238+ index=index, text=text, value=term.token,
239+ name=self.name, cssClass=self.cssClass)
240 items.append(renderfunc(**render_args))
241
242 # Lastly render the other option.
243 index = len(items)
244- other_branch_text = "%s %s" % (
245+ other_selection_text = "%s %s" % (
246 self._renderLabel("Other:", index),
247- self.other_branch_widget())
248- other_branch_onclick = (
249- "this.form['%s.target_branch'].focus()" % self.name)
250+ self.other_selection_widget())
251+ other_selection_onclick = (
252+ "this.form['%s'].focus()" % self.other_selection_widget.name)
253
254 elem = renderElement(u'input',
255 value="other",
256@@ -192,9 +167,9 @@
257 id='%s.%s' % (self.name, index),
258 cssClass=self.cssClass,
259 type='radio',
260- onClick=other_branch_onclick)
261+ onClick=other_selection_onclick)
262
263- other_radio_button = '%s&nbsp;%s' % (elem, other_branch_text)
264+ other_radio_button = '%s&nbsp;%s' % (elem, other_selection_text)
265
266 items.append(other_radio_button)
267
268@@ -202,7 +177,98 @@
269
270 def __call__(self):
271 """Don't render the radio buttons if only one choice."""
272- if len(self.branch_selector_vocab) == 0:
273- return self.other_branch_widget()
274+ if not self._shouldRenderSuggestions():
275+ return self.other_selection_widget()
276 else:
277 return LaunchpadRadioWidget.__call__(self)
278+
279+
280+class TargetBranchWidget(SuggestionWidget):
281+ """Widget for selecting a target branch.
282+
283+ The default branch for a new branch merge proposal should be
284+ the branch associated with the development focus series if there
285+ is one (that isn't an import branch).
286+
287+ Also in the initial radio button selector are other branches for
288+ the product that the branch owner has specified as target branches
289+ for other merge proposals.
290+
291+ Finally there is an "other" button that gets the user to use the
292+ normal branch selector.
293+ """
294+
295+ @staticmethod
296+ def _generateSuggestionVocab(branch, full_vocabulary):
297+ """Generate the vocabulary for the radio buttons.
298+
299+ The generated vocabulary contains the branch associated with the
300+ development series of the product if there is one, and also any other
301+ branches that the user has specified before as a target for a proposed
302+ merge.
303+ """
304+ default_target = branch.target.default_merge_target
305+ logged_in_user = getUtility(ILaunchBag).user
306+ collection = branch.target.collection.targetedBy(logged_in_user)
307+ collection = collection.visibleByUser(logged_in_user)
308+ branches = collection.getBranches().config(distinct=True)
309+ target_branches = list(branches.config(limit=5))
310+ # If there is a development focus branch, make sure it is always
311+ # shown, and as the first item.
312+ if default_target is not None:
313+ if default_target in target_branches:
314+ target_branches.remove(default_target)
315+ target_branches.insert(0, default_target)
316+
317+ # Make sure the source branch isn't in the target_branches.
318+ if branch in target_branches:
319+ target_branches.remove(branch)
320+
321+ terms = []
322+ for branch in target_branches:
323+ terms.append(SimpleTerm(
324+ branch, branch.unique_name))
325+
326+ return SimpleVocabulary(terms)
327+
328+ def _renderSuggestionLabel(self, branch, index):
329+ """Render a label for the option based on a branch."""
330+ # To aid usability there needs to be some text connected with the
331+ # radio buttons that is not a hyperlink in order to select the radio
332+ # button. It was decided not to have the entire text as a link, but
333+ # instead to have a separate link to the branch details.
334+ text = '%s (<a href="%s">branch details</a>)' % (
335+ cgi.escape(branch.displayname), cgi.escape(canonical_url(branch)))
336+ # If the branch is the development focus, say so.
337+ if branch == self.context.context.target.default_merge_target:
338+ text = text + "&ndash; <em>development focus</em>"
339+ return u'<label for="%s" style="font-weight: normal">%s</label>' % (
340+ self._optionId(index), text)
341+
342+ def _autoselectOther(self):
343+ """Select "other" on keypress."""
344+ on_key_press = "selectWidget('%s', event);" % self._otherId()
345+ self.other_selection_widget.onKeyPress = on_key_press
346+
347+
348+class RecipeOwnerWidget(SuggestionWidget):
349+ """Widget for selecting a recipe owner.
350+
351+ The current user and the base branch owner are suggested.
352+ """
353+
354+ @staticmethod
355+ def _getSuggestions(branch):
356+ """Suggest the branch owner and current user."""
357+ logged_in_user = getUtility(ILaunchBag).user
358+ return set([branch.owner, logged_in_user])
359+
360+ @staticmethod
361+ def _valueDisplayname(value):
362+ """Provide a specialized displayname for Persons"""
363+ return value.unique_displayname
364+
365+ def _autoselectOther(self):
366+ """Select "other" on click."""
367+ on_click = "onClick=\"selectWidget('%s', event);\"" % self._otherId()
368+ self.other_selection_widget.extra = on_click
369
370=== modified file 'lib/lp/code/browser/branch.py'
371--- lib/lp/code/browser/branch.py 2010-11-11 11:55:53 +0000
372+++ lib/lp/code/browser/branch.py 2010-11-19 14:13:57 +0000
373@@ -102,7 +102,7 @@
374 from canonical.launchpad.webapp.interfaces import ICanonicalUrlData
375 from canonical.launchpad.webapp.menu import structured
376 from canonical.lazr.utils import smartquote
377-from canonical.widgets.branch import TargetBranchWidget
378+from canonical.widgets.suggestion import TargetBranchWidget
379 from canonical.widgets.itemswidgets import LaunchpadRadioWidgetWithDescription
380 from canonical.widgets.lazrjs import vocabulary_to_choice_edit_items
381 from lp.app.errors import NotFoundError
382
383=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
384--- lib/lp/code/browser/sourcepackagerecipe.py 2010-11-04 19:52:14 +0000
385+++ lib/lp/code/browser/sourcepackagerecipe.py 2010-11-19 14:13:57 +0000
386@@ -57,6 +57,7 @@
387 )
388 from canonical.launchpad.webapp.authorization import check_permission
389 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
390+from canonical.widgets.suggestion import RecipeOwnerWidget
391 from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
392 from lp.code.errors import (
393 BuildAlreadyPending,
394@@ -315,6 +316,7 @@
395
396 schema = ISourcePackageAddEditSchema
397 custom_widget('distros', LabeledMultiCheckBoxWidget)
398+ custom_widget('owner', RecipeOwnerWidget)
399
400 def initialize(self):
401 # XXX: rockstar: This should be removed when source package recipes
402@@ -407,7 +409,6 @@
403 self.form_fields = self.form_fields.omit('owner')
404 self.form_fields = any_owner_field + self.form_fields
405
406-
407 @property
408 def initial_values(self):
409 return {
410
411=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
412--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-11-10 20:53:17 +0000
413+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py 2010-11-19 14:13:57 +0000
414@@ -168,7 +168,7 @@
415 browser.getLink('Create packaging recipe').click()
416
417 # The options for the owner include the Good Chefs team.
418- options = browser.getControl('Owner').displayOptions
419+ options = browser.getControl(name='field.owner.owner').displayOptions
420 self.assertEquals(
421 ['Good Chefs (good-chefs)', 'Master Chef (chef)'],
422 sorted([str(option) for option in options]))
423@@ -185,7 +185,9 @@
424 browser.getControl('Description').value = 'Make some food!'
425 browser.getControl('Secret Squirrel').click()
426 browser.getControl('Automatically build each day').click()
427- browser.getControl('Owner').displayValue = ['Good Chefs']
428+ browser.getControl('Other').click()
429+ browser.getControl(name='field.owner.owner').displayValue = [
430+ 'Good Chefs']
431 browser.getControl('Create Recipe').click()
432
433 login(ANONYMOUS)
434@@ -193,6 +195,33 @@
435 self.assertEqual(team, recipe.owner)
436 self.assertEqual('daily', recipe.name)
437
438+ def test_create_new_recipe_suggests_user(self):
439+ """The current user is suggested as a recipe owner, once."""
440+ branch = self.factory.makeBranch(owner=self.chef)
441+ text = self.getMainText(branch, '+new-recipe')
442+ self.assertTextMatchesExpressionIgnoreWhitespace(
443+ r'Owner: Master Chef \(chef\) Other:', text)
444+
445+ def test_create_new_recipe_suggests_user_team(self):
446+ """If current user is a member of branch owner, it is suggested."""
447+ team = self.factory.makeTeam(
448+ name='branch-team', displayname='Branch Team',
449+ members=[self.chef])
450+ branch = self.factory.makeBranch(owner=team)
451+ text = self.getMainText(branch, '+new-recipe')
452+ self.assertTextMatchesExpressionIgnoreWhitespace(
453+ r'Owner: Master Chef \(chef\)'
454+ r' Branch Team \(branch-team\) Other:', text)
455+
456+ def test_create_new_recipe_ignores_non_user_team(self):
457+ """If current user isn't a member of branch owner, it is ignored."""
458+ team = self.factory.makeTeam(
459+ name='branch-team', displayname='Branch Team')
460+ branch = self.factory.makeBranch(owner=team)
461+ text = self.getMainText(branch, '+new-recipe')
462+ self.assertTextMatchesExpressionIgnoreWhitespace(
463+ r'Owner: Master Chef \(chef\) Other:', text)
464+
465 def test_create_recipe_forbidden_instruction(self):
466 # We don't allow the "run" instruction in our recipes. Make sure this
467 # is communicated to the user properly.
468@@ -267,7 +296,8 @@
469 merge %(package_branch)s
470 ''' % {
471 'branch': branch.bzr_identity,
472- 'package_branch': package_branch.bzr_identity,}),
473+ 'package_branch': package_branch.bzr_identity,
474+ }),
475 branch=branch)
476 self.assertEqual(
477 get_message_text(browser, 2),