Merge lp:~wallyworld/launchpad/rename-private-team-795771 into lp:launchpad

Proposed by Ian Booth
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 13813
Proposed branch: lp:~wallyworld/launchpad/rename-private-team-795771
Merge into: lp:launchpad
Diff against target: 465 lines (+181/-202)
4 files modified
lib/lp/registry/browser/team.py (+9/-15)
lib/lp/registry/browser/tests/test_team_view.py (+168/-6)
lib/lp/registry/stories/team/xx-team-edit.txt (+0/-180)
lib/lp/testing/factory.py (+4/-1)
To merge this branch: bzr merge lp:~wallyworld/launchpad/rename-private-team-795771
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Steve Kowalik (community) code Needs Fixing
Review via email: mp+72516@code.launchpad.net

Commit message

[r=sinzui][bug=795771] Allow private teams to be renamed

Description of the change

Private teams can be renamed.

== Implementation ==

Remove the private team check in setUpWidgets of TeamEditView

== Tests ==

Write some, there were none.

TestTeamEditView
    test_can_rename_private_team
    test_cannot_rename_team_with_ppa
    test_cannot_rename_team_with_active_mailinglist
    test_can_rename_team_with_purged_mailinglist

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/browser/team.py
  lib/lp/registry/browser/tests/test_team_view.py

To post a comment you must log in.
Revision history for this message
Robert Collins (lifeless) wrote :

+ if not has_mailing_list:
+ reason = 'has a PPA'
+ elif not has_ppa:
+ reason = 'has a mailing list'

This made me blink.

Perhaps:
reasons = []
if has_mailing_list:
    reasons.append('has a mailing list')
if has_ppa:
    reasons.append('has a PPA')
assert reasons, 'rejecting but no reasons found!'
reason = ' and '.join(reasons)

Revision history for this message
Ian Booth (wallyworld) wrote :

What's there is how the code was originally written, just with one check
for is_private removed. It's not possible to reject for a reason other
than a mailing list or ppa so the assert is not strictly required. The
code below will work but does change the text output if both a ppa and
mailing list exist (a bit more verbose).

I'll change it as suggested and modify any tests accordingly.

On 23/08/11 18:04, Robert Collins wrote:
> + if not has_mailing_list:
> + reason = 'has a PPA'
> + elif not has_ppa:
> + reason = 'has a mailing list'
>
> This made me blink.
>
> Perhaps:
> reasons = []
> if has_mailing_list:
> reasons.append('has a mailing list')
> if has_ppa:
> reasons.append('has a PPA')
> assert reasons, 'rejecting but no reasons found!'
> reason = ' and '.join(reasons)
>

Revision history for this message
Steve Kowalik (stevenk) wrote :

At the moment you only check that the widget is editable and then check the error conditions. Please add an end-to-end test that confirms a private team can be renamed.

review: Needs Fixing (code)
Revision history for this message
Ian Booth (wallyworld) wrote :

I removed the entire xx-team-edit doc test (replaced with unit tests).
End-end rename test is added, not just for private teams but also teams
with purged mailing lists.

On 24/08/11 11:11, Steve Kowalik wrote:
> Review: Needs Fixing code
> At the moment you only check that the widget is editable and then check the error conditions. Please add an end-to-end test that confirms a private team can be renamed.

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

I am very wary of converting stories into unittests. Most are predicated on issues
that are relevant. A story only needs to show the happy path that demonstrates
that the user can perform a goal. In this case I believe only one browser test
is needs to show that a user can navigate from the team page to the edit page
and submit a change to be returned to the team page.

Do not use TestBrowser (an integration test) to test what a view
does (unittest). Browser tests do not document form contracts or directly
exercise the lines in the view it claims to be testing.

test_team_name_already_used is not testing the view's error message.
It is testing that base-layout-macros, which I am pretty sure is not telling
us anything more than the 100 other tests like this. I image a test that
realley exercises that the view does soemthing like this:

    def test_team_name_already_used(self):
        # If we try to use a name which is already in use, we'll get an error
        # message explaining it.

        self.factory.makeTeam(name="existing")
        owner = self.factory.makePerson()
        team = self.factory.makeTeam(name="team", owner=owner)

        form = {
            'field.name': 'existing'
            # May need more field to document that actual contract the
            # for requires.
            'field.actions.save': 'Save changes',
            }
        login_person(owner)
        view = create_initialized_view(team, '+edit' form=form)
        self.assertEqual(1, len(view.errors))
        notifications = view.request.response.notifications
        self.assertEqual(1, len(notifications))
        self.assertEqual(
            'existing is already in use by another person or team.',
            notifications[0].message)

The page rendering has nothing to do with permission. This is a zope
secuirty rule on a view. Never test with "mark" you do not know why
permission is given. Test that the team owner has permission and other
users do not. Also this ancient test is unaware that registry-experts
is the celebrity that actually does most of the administration today.

from canonical.launchpad.webapp.authorization import check_permission

    def test_edit_team_view_permission(self):
        # Only an administrator (as well as the team owner) of a team can
        # change the details of that team.
        person = self.factory.makePerson()
        team = self.factory.makeTeam()
        view = create_view(team, '+edit')
        login_person(person)
        self.assertFalse(check_permission('launchpad.Edit', view))
        login.person(team.owner)
        self.assertTrue(check_permission('launchpad.Edit', view))

review: Needs Fixing (test)
Revision history for this message
Ian Booth (wallyworld) wrote :

On 25/08/11 00:53, Curtis Hovey wrote:
> Review: Needs Fixing test
> I am very wary of converting stories into unittests. Most are predicated on issues
> that are relevant. A story only needs to show the happy path that demonstrates
> that the user can perform a goal. In this case I believe only one browser test
> is needs to show that a user can navigate from the team page to the edit page
> and submit a change to be returned to the team page.
>
> Do not use TestBrowser (an integration test) to test what a view
> does (unittest). Browser tests do not document form contracts or directly
> exercise the lines in the view it claims to be testing.
>

I used the browser because I needed the end-end behaviour to test the
saving of a private team as requested in a review comment. The other
uses were from porting the story code and I didn't know how to do it
using the view. Many thanks for providing the examples to show how.

Revision history for this message
Ian Booth (wallyworld) wrote :

> > Do not use TestBrowser (an integration test) to test what a view
> > does (unittest). Browser tests do not document form contracts or directly
> > exercise the lines in the view it claims to be testing.
> >
>
> I used the browser because I needed the end-end behaviour to test the
> saving of a private team as requested in a review comment. The other
> uses were from porting the story code and I didn't know how to do it
> using the view. Many thanks for providing the examples to show how.

Al tests except the end-end test now only use the view, not the browser.

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

Thank you

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/browser/team.py'
2--- lib/lp/registry/browser/team.py 2011-08-07 04:05:52 +0000
3+++ lib/lp/registry/browser/team.py 2011-08-25 01:46:27 +0000
4@@ -259,10 +259,9 @@
5 has_mailing_list = (
6 mailing_list is not None and
7 mailing_list.status != MailingListStatus.PURGED)
8- is_private = self.context.visibility == PersonVisibility.PRIVATE
9 has_ppa = self.context.archive is not None
10
11- block_renaming = (has_mailing_list or is_private or has_ppa)
12+ block_renaming = (has_mailing_list or has_ppa)
13 if block_renaming:
14 # This makes the field's widget display (i.e. read) only.
15 self.form_fields['name'].for_display = True
16@@ -273,17 +272,12 @@
17 # read-only mode if necessary.
18 if block_renaming:
19 # Group the read-only mode reasons in textual form.
20- # Private teams can't be associated with mailing lists
21- # or PPAs yet, so it's a dominant condition.
22- if is_private:
23- reason = 'is private'
24- else:
25- if not has_mailing_list:
26- reason = 'has a PPA'
27- elif not has_ppa:
28- reason = 'has a mailing list'
29- else:
30- reason = 'has a mailing list and a PPA'
31+ reasons = []
32+ if has_mailing_list:
33+ reasons.append('has a mailing list')
34+ if has_ppa:
35+ reasons.append('has a PPA')
36+ reason = ' and '.join(reasons)
37 self.widgets['name'].hint = _(
38 'This team cannot be renamed because it %s.' % reason)
39
40@@ -549,7 +543,7 @@
41 already been approved or declined. This can only happen
42 through bypassing the UI.
43 """
44- mailing_list = getUtility(IMailingListSet).get(self.context.name)
45+ getUtility(IMailingListSet).get(self.context.name)
46 if self.getListInState(MailingListStatus.REGISTERED) is None:
47 self.addError("This application can't be cancelled.")
48
49@@ -985,7 +979,7 @@
50 failed_names = [person.displayname for person in failed_joins]
51 failed_list = ", ".join(failed_names)
52
53- mapping = dict( this_team=target_team.displayname,
54+ mapping = dict(this_team=target_team.displayname,
55 failed_list=failed_list)
56
57 if len(failed_joins) == 1:
58
59=== modified file 'lib/lp/registry/browser/tests/test_team_view.py'
60--- lib/lp/registry/browser/tests/test_team_view.py 2010-10-04 19:50:45 +0000
61+++ lib/lp/registry/browser/tests/test_team_view.py 2011-08-25 01:46:27 +0000
62@@ -8,16 +8,28 @@
63 __metaclass__ = type
64
65 import transaction
66-
67-from canonical.testing.layers import DatabaseFunctionalLayer
68-
69-from lp.registry.interfaces.person import TeamSubscriptionPolicy
70-
71+from zope.security.proxy import removeSecurityProxy
72+
73+from canonical.launchpad.webapp.authorization import check_permission
74+from canonical.launchpad.webapp.publisher import canonical_url
75+from canonical.testing.layers import (
76+ DatabaseFunctionalLayer,
77+ LaunchpadFunctionalLayer,
78+ )
79+from lp.registry.interfaces.mailinglist import MailingListStatus
80+from lp.registry.interfaces.person import (
81+ PersonVisibility,
82+ TeamSubscriptionPolicy,
83+ TeamMembershipRenewalPolicy)
84 from lp.testing import (
85 login_person,
86+ person_logged_in,
87 TestCaseWithFactory,
88 )
89-from lp.testing.views import create_initialized_view
90+from lp.testing.views import (
91+ create_view,
92+ create_initialized_view,
93+ )
94
95
96 class TestProposedTeamMembersEditView(TestCaseWithFactory):
97@@ -141,3 +153,153 @@
98 failed = (self.a_team, self.b_team)
99 successful = (self.c_team, self.d_team)
100 self.acceptTeam(self.super_team, successful, failed)
101+
102+
103+class TestTeamEditView(TestCaseWithFactory):
104+
105+ layer = LaunchpadFunctionalLayer
106+
107+ def test_cannot_rename_team_with_ppa(self):
108+ # A team with a ppa cannot be renamed.
109+ owner = self.factory.makePerson()
110+ team = self.factory.makeTeam(owner=owner)
111+ removeSecurityProxy(team).archive = self.factory.makeArchive()
112+ with person_logged_in(owner):
113+ view = create_initialized_view(team, name="+edit")
114+ self.assertTrue(view.form_fields['name'].for_display)
115+ self.assertEqual(
116+ 'This team cannot be renamed because it has a PPA.',
117+ view.widgets['name'].hint)
118+
119+ def test_cannot_rename_team_with_active_mailinglist(self):
120+ # Because renaming mailing lists is non-trivial in Mailman 2.1,
121+ # renaming teams with mailing lists is prohibited.
122+ owner = self.factory.makePerson()
123+ team = self.factory.makeTeam(owner=owner)
124+ self.factory.makeMailingList(team, owner)
125+ with person_logged_in(owner):
126+ view = create_initialized_view(team, name="+edit")
127+ self.assertTrue(view.form_fields['name'].for_display)
128+ self.assertEqual(
129+ 'This team cannot be renamed because it has a mailing list.',
130+ view.widgets['name'].hint)
131+
132+ def test_can_rename_team_with_purged_mailinglist(self):
133+ # A team with a mailing list which is purged can be renamed.
134+ owner = self.factory.makePerson()
135+ team = self.factory.makeTeam(owner=owner)
136+ team_list = self.factory.makeMailingList(team, owner)
137+ team_list.deactivate()
138+ team_list.transitionToStatus(MailingListStatus.INACTIVE)
139+ team_list.purge()
140+ with person_logged_in(owner):
141+ view = create_initialized_view(team, name="+edit")
142+ self.assertFalse(view.form_fields['name'].for_display)
143+
144+ def test_cannot_rename_team_with_multiple_reasons(self):
145+ # Since public teams can have mailing lists and PPAs simultaneously,
146+ # there will be scenarios where more than one of these conditions are
147+ # actually blocking the team to be renamed.
148+ owner = self.factory.makePerson()
149+ team = self.factory.makeTeam(owner=owner)
150+ self.factory.makeMailingList(team, owner)
151+ removeSecurityProxy(team).archive = self.factory.makeArchive()
152+ with person_logged_in(owner):
153+ view = create_initialized_view(team, name="+edit")
154+ self.assertTrue(view.form_fields['name'].for_display)
155+ self.assertEqual(
156+ ('This team cannot be renamed because it has a mailing list '
157+ 'and has a PPA.'),
158+ view.widgets['name'].hint)
159+
160+ def test_edit_team_view_permission(self):
161+ # Only an administrator or the team owner of a team can
162+ # change the details of that team.
163+ person = self.factory.makePerson()
164+ owner = self.factory.makePerson()
165+ team = self.factory.makeTeam(owner=owner)
166+ view = create_view(team, '+edit')
167+ login_person(person)
168+ self.assertFalse(check_permission('launchpad.Edit', view))
169+ login_person(owner)
170+ self.assertTrue(check_permission('launchpad.Edit', view))
171+
172+ def test_edit_team_view_data(self):
173+ # The edit view renders the team's details correctly.
174+ owner = self.factory.makePerson()
175+ team = self.factory.makeTeam(
176+ name="team", displayname='A Team',
177+ description="A great team", owner=owner,
178+ subscription_policy=TeamSubscriptionPolicy.MODERATED)
179+ with person_logged_in(owner):
180+ view = create_initialized_view(team, name="+edit")
181+ self.assertEqual('team', view.widgets['name']._data)
182+ self.assertEqual(
183+ 'A Team', view.widgets['displayname']._data)
184+ self.assertEqual(
185+ 'A great team', view.widgets['teamdescription']._data)
186+ self.assertEqual(
187+ TeamSubscriptionPolicy.MODERATED,
188+ view.widgets['subscriptionpolicy']._data)
189+ self.assertEqual(
190+ TeamMembershipRenewalPolicy.NONE,
191+ view.widgets['renewal_policy']._data)
192+ self.assertIsNone(view.widgets['defaultrenewalperiod']._data)
193+
194+ def test_edit_team_view_save(self):
195+ # A team can be edited and saved, including a name change, even if it
196+ # is a private team and has a purged mailing list.
197+ owner = self.factory.makePerson()
198+ team = self.factory.makeTeam(
199+ name="team", displayname='A Team',
200+ description="A great team", owner=owner,
201+ visibility=PersonVisibility.PRIVATE,
202+ subscription_policy=TeamSubscriptionPolicy.MODERATED)
203+
204+ with person_logged_in(owner):
205+ team_list = self.factory.makeMailingList(team, owner)
206+ team_list.deactivate()
207+ team_list.transitionToStatus(MailingListStatus.INACTIVE)
208+ team_list.purge()
209+ url = canonical_url(team)
210+ browser = self.getUserBrowser(url, user=owner)
211+ browser.getLink('Change details').click()
212+ browser.getControl('Name', index=0).value = 'ubuntuteam'
213+ browser.getControl('Display Name').value = 'Ubuntu Team'
214+ browser.getControl('Team Description').value = ''
215+ browser.getControl('Restricted Team').selected = True
216+ browser.getControl('Save').click()
217+
218+ # We're now redirected to the team's home page, which is now on a
219+ # different URL since we changed its name.
220+ self.assertEqual('http://launchpad.dev/~ubuntuteam', browser.url)
221+
222+ # Check the values again.
223+ browser.getLink('Change details').click()
224+ self.assertEqual(
225+ 'ubuntuteam', browser.getControl('Name', index=0).value)
226+ self.assertEqual(
227+ 'Ubuntu Team', browser.getControl('Display Name', index=0).value)
228+ self.assertEqual(
229+ '', browser.getControl('Team Description', index=0).value)
230+ self.assertTrue(
231+ browser.getControl('Restricted Team', index=0).selected)
232+
233+ def test_team_name_already_used(self):
234+ # If we try to use a name which is already in use, we'll get an error
235+ # message explaining it.
236+
237+ self.factory.makeTeam(name="existing")
238+ owner = self.factory.makePerson()
239+ team = self.factory.makeTeam(name="team", owner=owner)
240+
241+ form = {
242+ 'field.name': 'existing',
243+ 'field.actions.save': 'Save',
244+ }
245+ login_person(owner)
246+ view = create_initialized_view(team, '+edit', form=form)
247+ self.assertEqual(1, len(view.errors))
248+ self.assertEqual(
249+ 'existing is already in use by another person or team.',
250+ view.errors[0].doc())
251
252=== removed file 'lib/lp/registry/stories/team/xx-team-edit.txt'
253--- lib/lp/registry/stories/team/xx-team-edit.txt 2011-08-07 08:04:38 +0000
254+++ lib/lp/registry/stories/team/xx-team-edit.txt 1970-01-01 00:00:00 +0000
255@@ -1,180 +0,0 @@
256-Editing team details
257-====================
258-
259-Only an administrator (as well as the team owner) of a team can change the
260-details of that team.
261-
262- >>> user_browser.open('http://launchpad.dev/~ubuntu-team/+edit')
263- Traceback (most recent call last):
264- ...
265- Unauthorized:...
266-
267- >>> browser.addHeader('Authorization', 'Basic mark@example.com:test')
268- >>> browser.open('http://launchpad.dev/~ubuntu-team')
269- >>> browser.getLink('Change details').click()
270- >>> browser.url
271- 'http://launchpad.dev/~ubuntu-team/+edit'
272- >>> browser.getControl('Name', index=0).value
273- 'ubuntu-team'
274- >>> browser.getControl('Display Name').value
275- 'Ubuntu Team'
276- >>> browser.getControl('Team Description').value
277- 'This Team is responsible for the Ubuntu Distribution'
278- >>> browser.getControl('Moderated Team').selected
279- True
280- >>> browser.getControl('invite them to apply for renewal').selected
281- True
282- >>> print browser.getControl('Renewal period').value
283-
284-Now we'll change some values.
285-
286- >>> browser.getControl('Name', index=0).value = 'ubuntuteam'
287- >>> browser.getControl('Display Name').value = 'The Ubuntu Team'
288- >>> browser.getControl('Team Description').value = ''
289- >>> browser.getControl('Restricted Team').selected = True
290-
291- >>> browser.getControl('Save').click()
292-
293-We're now redirected to the team's home page, which is now on a different
294-URL since we changed its name.
295-
296- >>> browser.url
297- 'http://launchpad.dev/~ubuntuteam'
298- >>> browser.getLink('Change details').click()
299-
300- >>> browser.url
301- 'http://launchpad.dev/~ubuntuteam/+edit'
302- >>> browser.getControl('Name', index=0).value
303- 'ubuntuteam'
304- >>> browser.getControl('Display Name').value
305- 'The Ubuntu Team'
306- >>> browser.getControl('Team Description').value
307- ''
308- >>> browser.getControl('Restricted Team').selected
309- True
310-
311-If we try to use a name which is already in use, we'll get an error
312-message explaining it.
313-
314- >>> browser.getControl('Name', index=0).value = 'mark'
315- >>> browser.getControl('Save').click()
316-
317- >>> browser.url
318- 'http://launchpad.dev/%7Eubuntuteam/+edit'
319- >>> for tag in find_tags_by_class(browser.contents, 'message'):
320- ... print extract_text(tag)
321- There is 1 error.
322- mark is already in use by another person or team.
323-
324-
325-Teams with mailing lists may not be renamed
326--------------------------------------------
327-
328-Because renaming mailing lists is non-trivial in Mailman 2.1, renaming teams
329-with mailing lists is prohibited. This is done by making the 'name' field of
330-such team pages read-only.
331-
332- >>> browser.open('http://launchpad.dev/~landscape-developers')
333- >>> browser.getLink(url='+mailinglist').click()
334- >>> browser.getControl('Create new Mailing List').click()
335-
336- # Approval of mailing lists is not yet exposed through the web.
337- >>> from lp.registry.model.mailinglist import MailingListSet
338- >>> from canonical.launchpad.ftests import login, logout
339- >>> login('foo.bar@canonical.com')
340- >>> mailing_list = MailingListSet().get('landscape-developers')
341- >>> mailing_list.syncUpdate()
342- >>> logout()
343-
344-Because the 'name' field is read-only, it's not a control that we can get
345-a hold of to change.
346-
347- >>> browser.getLink('Change details').click()
348- >>> browser.getControl(name='field.name').value
349- Traceback (most recent call last):
350- ...
351- LookupError: name 'field.name'
352-
353-The reason why the field is immutable is displayed in the field
354-description.
355-
356- >>> print extract_text(
357- ... first_tag_by_class(browser.contents, 'form'))
358- Name: landscape-developers
359- This team cannot be renamed because it has a mailing list.
360- ...
361-
362-Of course, teams without mailing lists still have an editable name field.
363-
364- >>> print MailingListSet().get('guadamen')
365- None
366- >>> browser.open('http://launchpad.dev/~guadamen')
367- >>> browser.getLink('Change details').click()
368- >>> browser.getControl(name='field.name').value
369- 'guadamen'
370-
371-
372-Private teams may not be renamed
373---------------------------------
374-
375- >>> # Create the necessary teams.
376- >>> login('foo.bar@canonical.com')
377- >>> from lp.registry.interfaces.person import PersonVisibility
378- >>> owner = factory.makePerson(name='team-owner')
379- >>> priv_team = factory.makeTeam(name='private-team',
380- ... owner=owner,
381- ... visibility=PersonVisibility.PRIVATE)
382- >>> logout()
383-
384- >>> browser.open('http://launchpad.dev/~private-team')
385- >>> browser.getLink('Change details').click()
386- >>> browser.getControl(name='field.name').value
387- Traceback (most recent call last):
388- ...
389- LookupError: name 'field.name'
390-
391-The reason why the field is immutable is displayed in the field
392-description.
393-
394- >>> print extract_text(
395- ... first_tag_by_class(browser.contents, 'form'))
396- Name: private-team
397- This team cannot be renamed because it is private.
398- ...
399-
400-
401-Multiple conditions blocking team renaming
402-------------------------------------------
403-
404-Since public teams can have mailing lists and PPAs simultaneously,
405-there will be scenarios where more than one of these conditions are
406-actually blocking the team to be renamed.
407-
408-The Landscape Developers team already has a mailing list, we will
409-also activate its PPA.
410-
411- >>> browser.open('http://launchpad.dev/~landscape-developers')
412- >>> browser.getLink('Create a new PPA').click()
413- >>> browser.getControl(
414- ... name="field.displayname").value = 'Devel PPA'
415- >>> browser.getControl(name="field.accepted").value = True
416- >>> browser.getControl(
417- ... name="field.description").value = 'Hoohay for Team PPA.'
418- >>> browser.getControl("Activate").click()
419-
420-Back in the team edit form, the 'name' field remains read-only and has
421-a complete explanation about why it cannot be modified.
422-
423- >>> browser.getLink('Overview').click()
424- >>> browser.getLink('Change details').click()
425-
426- >>> browser.getControl(name='field.name').value
427- Traceback (most recent call last):
428- ...
429- LookupError: name 'field.name'
430-
431- >>> print extract_text(
432- ... first_tag_by_class(browser.contents, 'form'))
433- Name: landscape-developers
434- This team cannot be renamed because it has a mailing list and a PPA.
435- ...
436
437=== modified file 'lib/lp/testing/factory.py'
438--- lib/lp/testing/factory.py 2011-08-16 09:14:07 +0000
439+++ lib/lp/testing/factory.py 2011-08-25 01:46:27 +0000
440@@ -755,6 +755,7 @@
441 address, person, email_status, account)
442
443 def makeTeam(self, owner=None, displayname=None, email=None, name=None,
444+ description=None,
445 subscription_policy=TeamSubscriptionPolicy.OPEN,
446 visibility=None, members=None):
447 """Create and return a new, arbitrary Team.
448@@ -764,6 +765,7 @@
449 :type owner: `IPerson` or string
450 :param displayname: The team's display name. If not given we'll use
451 the auto-generated name.
452+ :param description: Team team's description.
453 :type string:
454 :param email: The email address to use as the team's contact address.
455 :type email: string
456@@ -789,7 +791,8 @@
457 displayname = SPACE.join(
458 word.capitalize() for word in name.split('-'))
459 team = getUtility(IPersonSet).newTeam(
460- owner, name, displayname, subscriptionpolicy=subscription_policy)
461+ owner, name, displayname, teamdescription=description,
462+ subscriptionpolicy=subscription_policy)
463 if visibility is not None:
464 # Visibility is normally restricted to launchpad.Commercial, so
465 # removing the security proxy as we don't care here.