Merge lp:~sinzui/launchpad/distro-driver into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~sinzui/launchpad/distro-driver
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~sinzui/launchpad/distro-driver
Reviewer Review Type Date Requested Status
Deryck Hodge (community) Approve
Review via email: mp+9909@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (5.5 KiB)

This is my branch to enable distro drivers to manage their series.
It introduces two marker interfaces IBaseDistribution and
IDerivativeDistribution to allow us to set separate permission on instances
of Distribution.

    lp:~sinzui/launchpad/distro-driver
    Diff size: 708
    Launchpad bug: https://bugs.launchpad.net/bugs/distro-driver
    Test command: ./bin/test -vv \
        -t "registry.*("milestone-views|distroseries-views|xx-distroseries-edit)' \
        -t "registry.*doc/(distroseries|productseries)"
    Pre-implementation: flacoste
    Target release: 2.2.8

= Enable distro drivers to manage their series =

Distro drivers for needs the same privileges as project drivers to do
their job, but they cannot because some aspects of creating a distro
series require lots of back end preparation. This problem only affects
Ubuntu because it uses Soyuz and Translations, so the limitation
should only affect Ubuntu.

Assuming that Ubuntu is an exception then, distro drivers need the power
to create a series and doing so make them the series release manager. The
release manager can create and modify milestones.

== Rules ==

Updated the permission rules of distroseries and milestones.
    * A distro.driver may create a series and he is automatically
      made the series.driver (AKA release manager)
      * Requires a new security permission
    * The series.driver may create milestones and modify milestones
      * requires an update to the existing permission

The Ubuntu exception is difficult in the case of adding a series because
security.py permissions are based on object type, not instances. and the
permission exception is only in regards to creating a series. The solution
suggest by Francis is to add marker interfaces based on the instance during
the SQLObject __init() phase. This is the same solution used by Archive.

ADDENDUM

When a product driver creates a series, he should be the series driver too.
A derivative distribution series must allow the driver to set the status.

== QA ==

    * Ask the owner of Baltix to create the series he needs.
    * Ask the owner of Baltix to update his two current milestones by
      assigning them to one of his series
    * Ask the owner of Baltix to create a milestone.

== Lint ==

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/canonical/launchpad/security.py
  lib/lp/registry/configure.zcml
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/distroseries.py
  lib/lp/registry/browser/product.py
  lib/lp/registry/browser/tests/distroseries-views.txt
  lib/lp/registry/browser/tests/milestone-views.txt
  lib/lp/registry/doc/distroseries.txt
  lib/lp/registry/doc/productseries.txt
  lib/lp/registry/interfaces/distribution.py
  lib/lp/registry/model/distribution.py
  lib/lp/registry/model/product.py

== Test ==

    * lib/lp/registry/browser/tests/distroseries-views.txt
      * Added tests to verify that IDerivativeDistribution drivers can
        create series and edit the series that they are drivers of.
        IBaseDistribution drivers cannot.
    * lib/lp/registry/brow...

Read more...

Revision history for this message
Deryck Hodge (deryck) wrote :

Hi, Curtis.

As I mentioned on IRC, because of https://dev.launchpad.net/LaunchpadSecurityPolicy
and the creation of DriveDistributionByDriversOrOwnersOrAdmins,
I did wonder if that should be moved to an app-specific security.py file.
As you noted, "Not until there a real plan to do this. This is foundations task."
I only mention it here for the sake of completeness.

Otherwise, this looks very good.

Cheers,
deryck

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/security.py'
2--- lib/canonical/launchpad/security.py 2009-08-05 14:52:50 +0000
3+++ lib/canonical/launchpad/security.py 2009-08-10 00:54:34 +0000
4@@ -765,6 +765,25 @@
5 user.inTeam(admins))
6
7
8+class DriveDistributionByDriversOrOwnersOrAdmins(AuthorizationBase):
9+ """Distribution drivers, owners, and admins may plan releases.
10+
11+ Drivers of `IDerivativeDistribution`s can create series. Owners and
12+ admins can create series for all `IDistribution`s.
13+ """
14+ permission = 'launchpad.Driver'
15+ usedfor = IDistribution
16+
17+ def checkAuthenticated(self, user):
18+ if user.inTeam(self.obj.driver) and not self.obj.full_functionality:
19+ # Drivers of derivative distributions can create a series that
20+ # they will be the release manager for.
21+ return True
22+ admins = getUtility(ILaunchpadCelebrities).admin
23+ return (user.inTeam(self.obj.owner) or
24+ user.inTeam(admins))
25+
26+
27 class EditDistributionSourcePackageByDistroOwnersOrAdmins(AuthorizationBase):
28 """The owner of a distribution should be able to edit its source
29 package information"""
30@@ -803,6 +822,11 @@
31 usedfor = IDistroSeries
32
33 def checkAuthenticated(self, user):
34+ if (user.inTeam(self.obj.driver)
35+ and not self.obj.distribution.full_functionality):
36+ # The series driver (release manager) may edit a series if the
37+ # distribution is an `IDerivativeDistribution`
38+ return True
39 admins = getUtility(ILaunchpadCelebrities).admin
40 return (user.inTeam(self.obj.owner) or
41 user.inTeam(self.obj.distribution.owner) or
42
43=== modified file 'lib/lp/registry/browser/configure.zcml'
44--- lib/lp/registry/browser/configure.zcml 2009-08-06 18:17:49 +0000
45+++ lib/lp/registry/browser/configure.zcml 2009-08-09 18:01:59 +0000
46@@ -1948,6 +1948,16 @@
47 permission="launchpad.Admin"
48 template="../templates/distroseries-add.pt">
49 </browser:page>
50+
51+ <browser:page
52+ name="+addseries"
53+ for="lp.registry.interfaces.distribution.IDerivativeDistribution"
54+ class="lp.registry.browser.distroseries.DistroSeriesAddView"
55+ facet="overview"
56+ permission="launchpad.Driver"
57+ template="../templates/distroseries-add.pt">
58+ </browser:page>
59+
60 <browser:page
61 for="lp.registry.interfaces.distribution.IDistribution"
62 permission="launchpad.Edit"
63
64=== modified file 'lib/lp/registry/browser/distroseries.py'
65--- lib/lp/registry/browser/distroseries.py 2009-07-17 18:46:25 +0000
66+++ lib/lp/registry/browser/distroseries.py 2009-08-10 05:33:31 +0000
67@@ -267,6 +267,47 @@
68 return self.context.searchPackages(self.text)
69
70
71+class DistroSeriesStatusMixin:
72+ """A mixin that provides status field support."""
73+
74+ def createStatusField(self):
75+ """Create the 'status' field.
76+
77+ Create the status vocabulary according the current distroseries
78+ status:
79+ * stable -> CURRENT, SUPPORTED, OBSOLETE
80+ * unstable -> EXPERIMENTAL, DEVELOPMENT, FROZEN, FUTURE, CURRENT
81+ """
82+ stable_status = (
83+ DistroSeriesStatus.CURRENT,
84+ DistroSeriesStatus.SUPPORTED,
85+ DistroSeriesStatus.OBSOLETE,
86+ )
87+
88+ if self.context.status not in stable_status:
89+ terms = [status for status in DistroSeriesStatus.items
90+ if status not in stable_status]
91+ terms.append(DistroSeriesStatus.CURRENT)
92+ else:
93+ terms = stable_status
94+
95+ status_vocabulary = SimpleVocabulary(
96+ [SimpleTerm(item, item.name, item.title) for item in terms])
97+
98+ return form.Fields(
99+ Choice(__name__='status',
100+ title=_('Status'),
101+ vocabulary=status_vocabulary,
102+ description=_("Select the distroseries status."),
103+ required=True))
104+
105+ def updateDateReleased(self, status):
106+ """Update the datereleased field if the status is set to CURRENT."""
107+ if (self.context.datereleased is None and
108+ status == DistroSeriesStatus.CURRENT):
109+ self.context.datereleased = UTC_NOW
110+
111+
112 class DistroSeriesView(BuildRecordsView, QueueItemsView):
113
114 def initialize(self):
115@@ -308,33 +349,44 @@
116 return True
117
118
119-class DistroSeriesEditView(LaunchpadEditFormView):
120+class DistroSeriesEditView(LaunchpadEditFormView, DistroSeriesStatusMixin):
121 """View class that lets you edit a DistroSeries object.
122
123 It redirects to the main distroseries page after a successful edit.
124 """
125 schema = IDistroSeries
126 field_names = ['displayname', 'title', 'summary', 'description']
127-
128- def initialize(self):
129- """See `LaunchpadEditFormView`.
130-
131- Additionally set the 'label' attribute which will be used in the
132- template.
133+ custom_widget('status', LaunchpadDropdownWidget)
134+
135+ @property
136+ def label(self):
137+ """See `LaunchpadFormView`."""
138+ return 'Change %s details' % self.context.title
139+
140+ def setUpFields(self):
141+ """See `LaunchpadFormView`.
142+
143+ In addition to setting schema fields, also initialize the
144+ 'status' field. See `createStatusField` method.
145 """
146- LaunchpadEditFormView.initialize(self)
147- self.label = 'Change %s details' % self.context.title
148+ LaunchpadEditFormView.setUpFields(self)
149+ if not self.context.distribution.full_functionality:
150+ # This is an IDerivativeDistribution which may set its status.
151+ self.form_fields = (
152+ self.form_fields + self.createStatusField())
153
154 @action("Change")
155 def change_action(self, action, data):
156 """Update the context and redirects to its overviw page."""
157+ if not self.context.distribution.full_functionality:
158+ self.updateDateReleased(data.get('status'))
159 self.updateContextFromData(data)
160 self.request.response.addInfoNotification(
161 'Your changes have been applied.')
162 self.next_url = canonical_url(self.context)
163
164
165-class DistroSeriesAdminView(LaunchpadEditFormView):
166+class DistroSeriesAdminView(LaunchpadEditFormView, DistroSeriesStatusMixin):
167 """View class for administering a DistroSeries object.
168
169 It redirects to the main distroseries page after a successful edit.
170@@ -343,14 +395,10 @@
171 field_names = ['name', 'version', 'changeslist']
172 custom_widget('status', LaunchpadDropdownWidget)
173
174- def initialize(self):
175- """See `LaunchpadEditFormView`.
176-
177- Additionally set the 'label' attribute which will be used in the
178- template.
179- """
180- LaunchpadEditFormView.initialize(self)
181- self.label = 'Administer %s' % self.context.title
182+ @property
183+ def label(self):
184+ """See `LaunchpadFormView`."""
185+ return 'Administer %s' % self.context.title
186
187 def setUpFields(self):
188 """Override `LaunchpadFormView`.
189@@ -362,37 +410,6 @@
190 self.form_fields = (
191 self.form_fields + self.createStatusField())
192
193- def createStatusField(self):
194- """Create the 'status' field.
195-
196- Create the status vocabulary according the current distroseries
197- status:
198- * stable -> CURRENT, SUPPORTED, OBSOLETE
199- * unstable -> EXPERIMENTAL, DEVELOPMENT, FROZEN, FUTURE, CURRENT
200- """
201- stable_status = (
202- DistroSeriesStatus.CURRENT,
203- DistroSeriesStatus.SUPPORTED,
204- DistroSeriesStatus.OBSOLETE,
205- )
206-
207- if self.context.status not in stable_status:
208- terms = [status for status in DistroSeriesStatus.items
209- if status not in stable_status]
210- terms.append(DistroSeriesStatus.CURRENT)
211- else:
212- terms = stable_status
213-
214- status_vocabulary = SimpleVocabulary(
215- [SimpleTerm(item, item.name, item.title) for item in terms])
216-
217- return form.Fields(
218- Choice(__name__='status',
219- title=_('Status'),
220- vocabulary=status_vocabulary,
221- description=_("Select the distroseries status."),
222- required=True))
223-
224 @action("Change")
225 def change_action(self, action, data):
226 """Update the context and redirects to its overviw page.
227@@ -400,11 +417,7 @@
228 Also, set 'datereleased' when a unstable distroseries is made
229 CURRENT.
230 """
231- status = data.get('status')
232- if (self.context.datereleased is None and
233- status == DistroSeriesStatus.CURRENT):
234- self.context.datereleased = UTC_NOW
235-
236+ self.updateDateReleased(data.get('status'))
237 self.updateContextFromData(data)
238
239 self.request.response.addInfoNotification(
240
241=== modified file 'lib/lp/registry/browser/tests/distroseries-views.txt'
242--- lib/lp/registry/browser/tests/distroseries-views.txt 2009-08-01 15:40:52 +0000
243+++ lib/lp/registry/browser/tests/distroseries-views.txt 2009-08-10 05:33:31 +0000
244@@ -1,4 +1,5 @@
245-= DistroSeries view classes =
246+DistroSeries view classes
247+=========================
248
249 >>> from zope.component import getMultiAdapter
250 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
251@@ -137,11 +138,44 @@
252 True
253
254
255+Editing distroseries
256+--------------------
257+
258+The distroseries edit view allows the editor to change series. The form
259+uses the displayname, title, and description fields.
260+
261+ >>> view = create_initialized_view(hoary, '+edit')
262+ >>> print view.label
263+ Change The Hoary Hedgehog Release details
264+
265+ >>> [field.__name__ for field in view.form_fields]
266+ ['displayname', 'title', 'summary', 'description']
267+
268+Series that belong to derivative distributions also contain the status field.
269+
270+ >>> youbuntu = factory.makeDistribution(name='youbuntu')
271+ >>> yo_series = factory.makeDistroRelease(name='melon')
272+ >>> youbuntu.full_functionality
273+ False
274+
275+ >>> yo_driver = factory.makePerson(name='yo-driver')
276+ >>> youbuntu.driver = yo_driver
277+ >>> login_person(yo_driver)
278+ >>> view = create_initialized_view(yo_series, '+edit')
279+ >>> print view.label
280+ Change ... details
281+
282+ >>> [field.__name__ for field in view.form_fields]
283+ ['displayname', 'title', 'summary', 'description', 'status']
284+
285+
286 Creating distroseries
287 ---------------------
288
289 A distroseries is created using the distroseries view.
290
291+ >>> login('foo.bar@canonical.com')
292+
293 >>> view = create_view(ubuntu, '+addseries')
294 >>> print view.page_title
295 Register a series in Ubuntu
296@@ -179,11 +213,94 @@
297 # Save this series to test name and version constraints.
298 >>> transaction.commit()
299
300+
301+Drivers can create distroseries
302+-------------------------------
303+
304+Users who are appointed as drivers of a distribution can create a series.
305+Most distributions are an `IDerivativeDistribution`.
306+
307+ >>> from canonical.launchpad.webapp.authorization import check_permission
308+
309+ >>> login_person(yo_driver)
310+ >>> view = create_view(youbuntu, name='+addseries')
311+ >>> check_permission('launchpad.Driver', view)
312+ True
313+
314+ >>> yo_form = dict(form)
315+ >>> yo_form['field.name'] = 'island'
316+ >>> yo_form['field.displayname'] = 'Island'
317+ >>> yo_form['field.title'] = 'YouBuntu Island'
318+ >>> view = create_initialized_view(
319+ ... youbuntu, name='+addseries', form=yo_form, principal=yo_driver)
320+ >>> view.errors
321+ []
322+
323+ >>> yo_series = youbuntu.getSeries('island')
324+ >>> print yo_series.displayname
325+ Island
326+
327+Ubuntu is an exception. It's drivers cannot create series because Ubuntu
328+is an `IBaseDistribution`.
329+
330+ >>> ubuntu.full_functionality
331+ True
332+
333+ >>> login_person(ubuntu.owner.teamowner)
334+ >>> ubuntu.driver = yo_driver
335+ >>> login_person(yo_driver)
336+ >>> view = create_view(youbuntu, name='+addseries')
337+ >>> check_permission('launchpad.Edit', view)
338+ False
339+
340+
341+Editing Distroseries
342+--------------------
343+
344+The series driver (release manager) can edit a series if the series belongs
345+to a `IDerivativeDistribution`.
346+
347+ >>> print yo_series.driver.name
348+ yo-driver
349+
350+ >>> login_person(yo_driver)
351+ >>> view = create_view(yo_series, name='+edit')
352+ >>> check_permission('launchpad.Edit', view)
353+ True
354+
355+ >>> yo_form = dict(form)
356+ >>> del yo_form['field.actions.create']
357+ >>> yo_form['field.displayname'] = 'Mountain'
358+ >>> yo_form['field.title'] = 'YouBuntu Mountain'
359+ >>> yo_form['field.summary'] = 'Mountain summary'
360+ >>> yo_form['field.description'] = 'Mountain description'
361+ >>> yo_form['field.actions.change'] = 'Change'
362+ >>> view = create_initialized_view(
363+ ... yo_series, name='+edit', form=yo_form, principal=yo_driver)
364+ >>> view.errors
365+ []
366+
367+ >>> print yo_series.displayname
368+ Mountain
369+
370+Drivers of an `IBaseDistribution` such as Ubuntu cannot edit a series.
371+
372+ >>> login_person(ubuntu.owner.teamowner)
373+ >>> hoary.driver = yo_driver
374+ >>> login_person(yo_driver)
375+
376+ >>> view = create_view(hoary, name='+edit')
377+ >>> check_permission('launchpad.Edit', view)
378+ False
379+
380+
381 Distroseries name
382 -----------------
383
384 The distroseries name is unique.
385
386+ >>> login('foo.bar@canonical.com')
387+
388 >>> form['field.name'] = 'sane'
389 >>> form['field.version'] = '2009.07'
390 >>> view = create_initialized_view(ubuntu, '+addseries', form=form)
391
392=== modified file 'lib/lp/registry/browser/tests/milestone-views.txt'
393--- lib/lp/registry/browser/tests/milestone-views.txt 2009-06-13 15:23:18 +0000
394+++ lib/lp/registry/browser/tests/milestone-views.txt 2009-08-10 02:29:03 +0000
395@@ -361,11 +361,58 @@
396 1.1 None None
397
398
399-== Deleting milestones ==
400+Distroseries driver and milestones
401+----------------------------------
402+
403+The driver of a series that belongs to an `IDerivativeDistribution` is a
404+release manager and can create milestones.
405+
406+ >>> distroseries = factory.makeDistroRelease(name='pumpkin')
407+ >>> driver = factory.makePerson(name='a-driver')
408+ >>> distroseries.driver = driver
409+ >>> login_person(driver)
410+
411+ >>> form = {
412+ ... 'field.name': 'pie',
413+ ... 'field.actions.register': 'Register Milestone',
414+ ... }
415+ >>> view = create_initialized_view(
416+ ... distroseries, '+addmilestone', form=form)
417+ >>> milestone = distroseries.milestones[0]
418+ >>> print milestone.name
419+ pie
420+
421+The driver has access to the milestone.
422+
423+ >>> view = create_initialized_view(milestone, '+edit')
424+ >>> check_permission('launchpad.Edit', view)
425+ True
426+
427+The driver of an `IBaseDistribution` such as Ubuntu cannot create a milestone.
428+
429+ >>> login_person(ubuntu_distro.owner.teamowner)
430+ >>> hoary_series.driver = driver
431+ >>> login_person(driver)
432+
433+ >>> view = create_initialized_view(hoary_series, '+addmilestone')
434+ >>> check_permission('launchpad.Edit', view)
435+ False
436+
437+Nor can the driver edit it.
438+
439+ >>> milestone = factory.makeMilestone(distribution=ubuntu_distro)
440+ >>> view = create_initialized_view(milestone, '+edit')
441+ >>> check_permission('launchpad.Edit', view)
442+ False
443+
444+
445+Deleting milestones
446+-------------------
447
448 The DeleteMilestoneView allows users to edit permissions to delete Milestones.
449 The view is restricted to owners of the project and drivers of the series.
450
451+ >>> login_person(owner)
452 >>> milestone = firefox_1_0.newMilestone('1.0.10')
453 >>> print milestone.name
454 1.0.10
455
456=== modified file 'lib/lp/registry/configure.zcml'
457--- lib/lp/registry/configure.zcml 2009-08-05 01:53:16 +0000
458+++ lib/lp/registry/configure.zcml 2009-08-09 18:01:59 +0000
459@@ -1360,6 +1360,9 @@
460 permission="launchpad.Edit"
461 interface="lp.registry.interfaces.distribution.IDistributionEditRestricted"/>
462 <require
463+ permission="launchpad.Driver"
464+ interface="lp.registry.interfaces.distribution.IDistributionDriverRestricted"/>
465+ <require
466 permission="launchpad.Edit"
467 set_attributes="displayname title summary description translation_focus translationgroup translationpermission driver members owner security_contact mirror_admin homepage_content icon logo mugshot enable_bug_expiration bug_reporting_guidelines official_blueprints official_malone official_rosetta official_answers official_bug_tags"/>
468 <require
469
470=== modified file 'lib/lp/registry/doc/distroseries.txt'
471--- lib/lp/registry/doc/distroseries.txt 2009-07-24 12:55:03 +0000
472+++ lib/lp/registry/doc/distroseries.txt 2009-08-10 03:39:09 +0000
473@@ -1,4 +1,4 @@
474-= Distro Releases =
475+= DistroSeries =
476
477 From the DerivationOverview spec
478 <https://launchpad.canonical.com/DerivationOverview>:
479@@ -762,7 +762,75 @@
480 (u'mozilla-firefox-data', u'0.9')
481
482
483-= Specification Listings =
484+Creating DistroSeries
485+---------------------
486+
487+Users with launchpad.Driver permission may create DistroSeries. In the
488+case of an `IDerivativeDistribution` A user who is a driver can create
489+the series and he is automatically assigned to the series' driver role
490+so that he can edit it.
491+
492+ >>> youbuntu = factory.makeDistribution(name='youbuntu')
493+ >>> yo_driver = factory.makePerson(name='yo-driver')
494+ >>> youbuntu.driver = yo_driver
495+ >>> login_person(yo_driver)
496+ >>> youbuntu.full_functionality
497+ False
498+
499+ >>> yo_series = youbuntu.newSeries(
500+ ... name='island', displayname='Island', title='YouBuntu Island',
501+ ... summary='summary', description='description', version='09.07',
502+ ... parent_series=warty, owner=yo_driver)
503+ >>> print yo_series.name
504+ island
505+ >>> print yo_series.driver.name
506+ yo-driver
507+
508+Owners of derivative distributions, and admins can create series too, but
509+they are not automatically set as the series driver because they always
510+have permission to edit the series.
511+
512+ >>> login_person(youbuntu.owner)
513+ >>> yo_series = youbuntu.newSeries(
514+ ... name='forest', displayname='Forest', title='YouBuntu Forest',
515+ ... summary='summary', description='description', version='09.07',
516+ ... parent_series=warty, owner=youbuntu.owner)
517+ >>> print yo_series.name
518+ forest
519+ >>> print yo_series.driver
520+ None
521+
522+Ubuntu is an `IBaseDistribution` which requires special preparation for
523+Soyuz and Translations before a series can be created. Ubuntu driver can
524+not create series.
525+
526+ >>> login_person(ubuntu.owner.activemembers[0])
527+ >>> ubuntu.driver = yo_driver
528+ >>> login_person(yo_driver)
529+ >>> ubuntu.newSeries(
530+ ... name='finch', displayname='Finch', title='Ubuntu Finch',
531+ ... summary='summary', description='description', version='9.06',
532+ ... parent_series=warty, owner=ubuntu.driver)
533+ Traceback (most recent call last):
534+ ...
535+ Unauthorized: ...
536+
537+Owners and admins of base distributions are the only users who can create a
538+series.
539+
540+ >>> login_person(ubuntu.owner.activemembers[0])
541+ >>> u_series = ubuntu.newSeries(
542+ ... name='finch', displayname='Finch', title='Ubuntu Finch',
543+ ... summary='summary', description='description', version='9.06',
544+ ... parent_series=warty, owner=ubuntu.owner)
545+ >>> print u_series.name
546+ finch
547+ >>> print u_series.driver
548+ None
549+
550+
551+Specification Listings
552+----------------------
553
554 We should be able to get lists of specifications in different states
555 related to a distroseries.
556
557=== modified file 'lib/lp/registry/doc/productseries.txt'
558--- lib/lp/registry/doc/productseries.txt 2009-07-23 13:44:13 +0000
559+++ lib/lp/registry/doc/productseries.txt 2009-08-10 03:39:09 +0000
560@@ -75,9 +75,14 @@
561 >>> login_person(firefox.owner)
562 >>> emacs_series = firefox.newSeries(firefox.owner , 'emacs', summary)
563
564+When a driver creates a series, he is also the driver of the new series
565+to make him the release manager.
566+
567 >>> firefox.driver = series_driver
568 >>> login_person(series_driver)
569 >>> emacs2 = firefox.newSeries(series_driver , 'emacs2', summary)
570+ >>> print emacs2.driver.name
571+ driver
572
573 A newly created series is assumed to be in the development state.
574
575
576=== modified file 'lib/lp/registry/interfaces/distribution.py'
577--- lib/lp/registry/interfaces/distribution.py 2009-08-03 13:00:37 +0000
578+++ lib/lp/registry/interfaces/distribution.py 2009-08-09 18:01:59 +0000
579@@ -8,7 +8,10 @@
580 __metaclass__ = type
581
582 __all__ = [
583+ 'IBaseDistribution',
584+ 'IDerivativeDistribution',
585 'IDistribution',
586+ 'IDistributionDriverRestricted',
587 'IDistributionEditRestricted',
588 'IDistributionMirrorMenuMarker',
589 'IDistributionPublic',
590@@ -70,6 +73,10 @@
591 class IDistributionEditRestricted(IOfficialBugTagTargetRestricted):
592 """IDistribution properties requiring launchpad.Edit permission."""
593
594+
595+class IDistributionDriverRestricted(Interface):
596+ """IDistribution properties requiring launchpad.Driver permission."""
597+
598 def newSeries(name, displayname, title, summary, description,
599 version, parent_series, owner):
600 """Creates a new distroseries."""
601@@ -533,6 +540,14 @@
602 IDistribution._v_attrs['official_bug_tags'] = writable_obt_field
603
604
605+class IBaseDistribution(IDistribution):
606+ """A Distribution that is the base for other Distributions."""
607+
608+
609+class IDerivativeDistribution(IDistribution):
610+ """A Distribution that derives from another Distribution."""
611+
612+
613 class IDistributionSet(Interface):
614 """Interface for DistrosSet"""
615 export_as_webservice_collection(IDistribution)
616
617=== modified file 'lib/lp/registry/model/distribution.py'
618--- lib/lp/registry/model/distribution.py 2009-08-03 13:00:37 +0000
619+++ lib/lp/registry/model/distribution.py 2009-08-09 21:14:27 +0000
620@@ -7,7 +7,7 @@
621 __metaclass__ = type
622 __all__ = ['Distribution', 'DistributionSet']
623
624-from zope.interface import implements
625+from zope.interface import alsoProvides, implements
626 from zope.component import getUtility
627
628 from sqlobject import (
629@@ -82,7 +82,8 @@
630 from lp.soyuz.interfaces.build import IBuildSet
631 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
632 from lp.registry.interfaces.distribution import (
633- IDistribution, IDistributionSet)
634+ IBaseDistribution, IDerivativeDistribution, IDistribution,
635+ IDistributionSet)
636 from lp.registry.interfaces.distributionmirror import (
637 IDistributionMirror, MirrorContent, MirrorStatus)
638 from lp.registry.interfaces.distroseries import (
639@@ -183,6 +184,18 @@
640 default=False)
641 active = True # Required by IPillar interface.
642
643+
644+ def _init(self, *args, **kw):
645+ """Initialize an `IBaseDistribution` or `IDerivativeDistribution`."""
646+ SQLBase._init(self, *args, **kw)
647+ # Add a marker interface to set permissions for this kind
648+ # of distribution.
649+ if self == getUtility(ILaunchpadCelebrities).ubuntu:
650+ alsoProvides(self, IBaseDistribution)
651+ else:
652+ alsoProvides(self, IDerivativeDistribution)
653+
654+
655 @property
656 def uploaders(self):
657 """See `IDistribution`."""
658@@ -291,7 +304,7 @@
659 @property
660 def full_functionality(self):
661 """See `IDistribution`."""
662- if self == getUtility(ILaunchpadCelebrities).ubuntu:
663+ if IBaseDistribution.providedBy(self):
664 return True
665 return False
666
667@@ -1488,7 +1501,7 @@
668 def newSeries(self, name, displayname, title, summary,
669 description, version, parent_series, owner):
670 """See `IDistribution`."""
671- return DistroSeries(
672+ series = DistroSeries(
673 distribution=self,
674 name=name,
675 displayname=displayname,
676@@ -1499,6 +1512,10 @@
677 status=DistroSeriesStatus.EXPERIMENTAL,
678 parent_series=parent_series,
679 owner=owner)
680+ if owner.inTeam(self.driver) and not owner.inTeam(self.owner):
681+ # This driver is a release manager.
682+ series.driver = owner
683+ return series
684
685 @property
686 def has_published_binaries(self):
687
688=== modified file 'lib/lp/registry/model/product.py'
689--- lib/lp/registry/model/product.py 2009-07-19 04:41:14 +0000
690+++ lib/lp/registry/model/product.py 2009-08-10 04:11:02 +0000
691@@ -897,8 +897,14 @@
692 # XXX: jamesh 2008-04-11
693 # Set the ID of the new ProductSeries to avoid flush order
694 # loops in ProductSet.createProduct()
695- return ProductSeries(productID=self.id, owner=owner, name=name,
696+ series = ProductSeries(productID=self.id, owner=owner, name=name,
697 summary=summary, branch=branch)
698+ if owner.inTeam(self.driver) and not owner.inTeam(self.owner):
699+ # The user is a product driver, and should be the driver of this
700+ # series to make him the release manager.
701+ series.driver = owner
702+ return series
703+
704
705 def getRelease(self, version):
706 """See `IProduct`."""
707
708=== renamed file 'lib/lp/soyuz/stories/soyuz/xx-distroseries-edit.txt' => 'lib/lp/registry/stories/distroseries/xx-distroseries-edit.txt'