Merge lp:~edwin-grubbs/launchpad/bug-553384-deactivated-project-oops into lp:launchpad

Proposed by Edwin Grubbs
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~edwin-grubbs/launchpad/bug-553384-deactivated-project-oops
Merge into: lp:launchpad
Diff against target: 654 lines (+190/-101)
20 files modified
lib/lp/answers/doc/person.txt (+4/-0)
lib/lp/blueprints/doc/specification.txt (+12/-8)
lib/lp/blueprints/doc/sprint.txt (+12/-8)
lib/lp/bugs/browser/bugalsoaffects.py (+0/-20)
lib/lp/bugs/browser/tests/bugtask-adding-views.txt (+0/-36)
lib/lp/registry/browser/product.py (+18/-4)
lib/lp/registry/browser/tests/product-views.txt (+23/-5)
lib/lp/registry/doc/milestone.txt (+4/-0)
lib/lp/registry/doc/person.txt (+4/-0)
lib/lp/registry/doc/pillar.txt (+3/-0)
lib/lp/registry/doc/product.txt (+9/-3)
lib/lp/registry/doc/project.txt (+9/-1)
lib/lp/registry/model/product.py (+12/-1)
lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt (+15/-8)
lib/lp/registry/stories/product/xx-product-edit.txt (+7/-7)
lib/lp/registry/stories/project/xx-project-index.txt (+6/-0)
lib/lp/registry/tests/test_product.py (+28/-0)
lib/lp/testing/__init__.py (+14/-0)
lib/lp/translations/doc/translationimportqueue.txt (+3/-0)
lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt (+7/-0)
To merge this branch: bzr merge lp:~edwin-grubbs/launchpad/bug-553384-deactivated-project-oops
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Brad Crittenden (community) code Approve
Review via email: mp+23794@code.launchpad.net

Commit message

Don't let a project linked to source packages be deactivated, since it will cause an oops on the $sourcepackage/+edit-packaging page.

Description of the change

Summary
-------

Fixed bug 553384 and bug 140526.

The $sourcepackage/+edit-packaging has an oops when it is linked to an
inactive project. To prevent this, it should not be possible to deactivate
a project until all its links to source packages have been removed.

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

Don't allow deactivation of products linked to source packages in the
model or in the views.
    lib/lp/registry/model/product.py
    lib/lp/registry/browser/product.py
    lib/lp/registry/browser/tests/product-views.txt
    lib/lp/registry/tests/test_product.py

Removed workaround for bug 140526:
    lib/lp/bugs/browser/bugalsoaffects.py
    lib/lp/bugs/browser/tests/bugtask-adding-views.txt

Fixed tests:
    lib/lp/testing/__init__.py
    lib/lp/answers/doc/person.txt
    lib/lp/blueprints/doc/specification.txt
    lib/lp/blueprints/doc/sprint.txt
    lib/lp/registry/doc/milestone.txt
    lib/lp/registry/doc/person.txt
    lib/lp/registry/doc/pillar.txt
    lib/lp/registry/doc/product.txt
    lib/lp/registry/doc/project.txt
    lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt
    lib/lp/registry/stories/product/xx-product-edit.txt
    lib/lp/registry/stories/project/xx-project-index.txt
    lib/lp/translations/doc/translationimportqueue.txt
    lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt

Tests
-----

./bin/test -vv -t 'test_product|product-views.txt|xx-product-edit|doc/product.txt'

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

* Open http://launchpad.dev/firefox/+review-license
  * Uncheck the "active" input.
  * Click "Change"
  * The "active" field should have the error message:
    "This project cannot be deactivated since it is still linked to
    source packages."
* Open http://launchpad.dev/firefox/+admin
  * Uncheck the "active" input.
  * Click "Change"
  * The "active" field should have the error message:
    "This project cannot be deactivated since it is still linked to
    source packages."

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

Hi Edwin,

This is nice, and it's good to see some XXXs go in the process. The
view code with the Storm validator as a backstop is elegant.

I've got a few questions and comments, so Needs Information for now.

Cheers, Gavin.

> === modified file 'lib/lp/answers/doc/person.txt'
> --- lib/lp/answers/doc/person.txt 2010-02-05 21:25:23 +0000
> +++ lib/lp/answers/doc/person.txt 2010-04-21 11:03:32 +0000
> @@ -351,6 +351,10 @@
> supported projects.
>
> >>> login('<email address hidden>')
> +
> + # A product cannot be deactivated if it is linked to source packages.
> + >>> from lp.testing import unlink_source_packages
> + >>> unlink_source_packages(firefox)
> >>> firefox.active = False
> >>> sorted(target.name
> ... for target in no_priv.getDirectAnswerQuestionTargets())
>
> === modified file 'lib/lp/blueprints/doc/specification.txt'
> --- lib/lp/blueprints/doc/specification.txt 2009-08-13 19:03:36 +0000
> +++ lib/lp/blueprints/doc/specification.txt 2010-04-21 11:03:32 +0000
> @@ -205,14 +205,18 @@
>
> Specs from inactive products are filtered out.
>
> - >>> from canonical.database.sqlbase import flush_database_updates
> - >>> login('<email address hidden>')
> - >>> upstream_firefox.active = False
> - >>> flush_database_updates()
> - >>> for spec in specset.specifications(filter=['install']):
> - ... print spec.name, spec.target.name
> - cluster-installation kubuntu
> - media-integrity-check ubuntu
> + >>> from canonical.database.sqlbase import flush_database_updates
> + >>> login('<email address hidden>')
> +
> + # A product cannot be deactivated if it is linked to source packages.
> + >>> from lp.testing import unlink_source_packages
> + >>> unlink_source_packages(upstream_firefox)
> + >>> upstream_firefox.active = False
> + >>> flush_database_updates()
> + >>> for spec in specset.specifications(filter=['install']):
> + ... print spec.name, spec.target.name
> + cluster-installation kubuntu
> + media-integrity-check ubuntu
>
>
> Reset firefox so we don't mess up later tests.
>
> === modified file 'lib/lp/blueprints/doc/sprint.txt'
> --- lib/lp/blueprints/doc/sprint.txt 2010-02-17 11:13:06 +0000
> +++ lib/lp/blueprints/doc/sprint.txt 2010-04-21 11:03:32 +0000
> @@ -151,14 +151,18 @@
>
> Inactive products are excluded from the listings.
>
> - >>> from canonical.launchpad.interfaces import IProductSet
> - >>> from canonical.launchpad.ftests import login
> - >>> firefox = getUtility(IProductSet).getByName('firefox')
> - >>> login("<email address hidden>")
> - >>> firefox.active = False
> - >>> flush_database_updates()
> - >>> ubz.specifications().count()
> - 0
> + >>> from canonical.launchpad.interfaces import IProductSet
> + >>> from canonical.launchpad.ftests import login
> + >>> firefox = getUtility(IProductSet).getByName('firefox')
> + >>> login("<email address hidden>")
> +
> + # A product cannot be deactivated if it is linked to source packages.
> + >>> from lp.testing import unlink_source_packages
> + >>> unlink_source_packages(firefox)
> + >>> firefox.active = False
> + >>> flush_database_updates()
> + >>> ubz.specifications().count()
> + 0
>
> ...

review: Needs Information (code)
Revision history for this message
Brad Crittenden (bac) wrote :
Download full text (7.0 KiB)

Hi Edwin,

Thanks for making this fix...it sure had lots of tentacles.

> === modified file 'lib/lp/answers/doc/person.txt'
> --- lib/lp/answers/doc/person.txt 2010-02-05 21:25:23 +0000
+++ lib/lp/answers/doc/person.txt 2010-04-20 20:36:22 +0000
> @@ -351,6 +351,10 @@
> supported projects.
>
> >>> login('<email address hidden>')
> +
> + # A product cannot be deactivated if it is linked to source packages.

I think this comment would be easier to read in context if it were
phrased positively, i.e.

# Unlink the source packages so the firefox project can be deactivated.

I see you've repeated the phrase many, many times in your changes.
I'll leave it up to you to decide whether you want to make the wording
change everywhere as it is only a minor improvement.

> + >>> from lp.testing import unlink_source_packages
> + >>> unlink_source_packages(firefox)
> >>> firefox.active = False
> >>> sorted(target.name
> ... for target in no_priv.getDirectAnswerQuestionTargets())

> === modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
> --- lib/lp/bugs/browser/bugalsoaffects.py 2010-01-15 03:32:46 +0000
> +++ lib/lp/bugs/browser/bugalsoaffects.py 2010-04-20 20:36:22 +0000
> @@ -103,26 +103,6 @@
> bugtask = self.context
> upstream = bugtask.target.upstream_product
> if upstream is not None:
> - if not upstream.active:
> - # XXX: Guilherme Salgado 2007-09-18 bug=140526: This is only
> - # possible because of bug 140526, which allows packages to
> - # be linked to inactive products.
> - series = bugtask.distribution.currentseries
> - assert series is not None, (
> - "This package is linked to a product series so this "
> - "package's distribution must have at least one distro "
> - "series.")
> - sourcepackage = series.getSourcePackage(
> - bugtask.sourcepackagename)
> - self.request.response.addWarningNotification(
> - structured(
> - _("""
> - This package is linked to an inactive upstream. You
> - can <a href="%(package_url)s/+edit-packaging">fix it</a>
> - to avoid this step in the future."""),
> - package_url=canonical_url(sourcepackage)))
> - return
> -

I'm really glad you found and removed these.

> try:
> valid_upstreamtask(bugtask.bug, upstream)
> except WidgetsError:

> === modified file 'lib/lp/registry/browser/product.py'
> --- lib/lp/registry/browser/product.py 2010-04-14 17:16:56 +0000
> +++ lib/lp/registry/browser/product.py 2010-04-20 20:36:22 +0000
> @@ -1444,6 +1444,15 @@
> """See `LaunchpadFormView`."""
> self.validate_private_bugs(data)
>
> + if data['active'] == False and self.context.active == True:
> + if len(self.context.sourcepackages) > 0:
> + self.setFieldError('active',
> + structured(
> + 'This project cannot be dea...

Read more...

review: Approve (code)
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (11.9 KiB)

=== modified file 'lib/lp/answers/doc/person.txt'
--- lib/lp/answers/doc/person.txt 2010-04-20 20:27:09 +0000
+++ lib/lp/answers/doc/person.txt 2010-04-21 16:05:41 +0000
@@ -352,7 +352,7 @@

     >>> login('<email address hidden>')

- # A product cannot be deactivated if it is linked to source packages.
+ # Unlink the source packages so the project can be deactivated.
     >>> from lp.testing import unlink_source_packages
     >>> unlink_source_packages(firefox)
     >>> firefox.active = False

=== modified file 'lib/lp/blueprints/doc/specification.txt'
--- lib/lp/blueprints/doc/specification.txt 2010-04-20 20:27:09 +0000
+++ lib/lp/blueprints/doc/specification.txt 2010-04-21 16:05:41 +0000
@@ -208,7 +208,7 @@
     >>> from canonical.database.sqlbase import flush_database_updates
     >>> login('<email address hidden>')

- # A product cannot be deactivated if it is linked to source packages.
+ # Unlink the source packages so the project can be deactivated.
     >>> from lp.testing import unlink_source_packages
     >>> unlink_source_packages(upstream_firefox)
     >>> upstream_firefox.active = False

=== modified file 'lib/lp/blueprints/doc/sprint.txt'
--- lib/lp/blueprints/doc/sprint.txt 2010-04-20 20:27:09 +0000
+++ lib/lp/blueprints/doc/sprint.txt 2010-04-21 16:05:41 +0000
@@ -156,7 +156,7 @@
     >>> firefox = getUtility(IProductSet).getByName('firefox')
     >>> login("<email address hidden>")

- # A product cannot be deactivated if it is linked to source packages.
+ # Unlink the source packages so the project can be deactivated.
     >>> from lp.testing import unlink_source_packages
     >>> unlink_source_packages(firefox)
     >>> firefox.active = False

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2010-04-19 20:03:36 +0000
+++ lib/lp/registry/browser/product.py 2010-04-21 14:34:21 +0000
@@ -1376,7 +1376,7 @@
         return self.next_url

-class EditPrivateBugsMixin:
+class ProductValidationMixin:

     def validate_private_bugs(self, data):
         """Perform validation for the private bugs setting."""
@@ -1387,8 +1387,20 @@
                     'for this project first.',
                     canonical_url(self.context, rootsite="bugs")))

-
-class ProductAdminView(ProductEditView, EditPrivateBugsMixin):
+ def validate_deactivation(self, data):
+ """Verify whether a product can be safely deactivated."""
+ if data['active'] == False and self.context.active == True:
+ if len(self.context.sourcepackages) > 0:
+ self.setFieldError('active',
+ structured(
+ 'This project cannot be deactivated since it is '
+ 'linked to one or more '
+ '<a href="%s">source packages</a>.',
+ canonical_url(self.context, view_name='+packages')))
+
+
+class ProductAdminView(ProductEditView, ProductValidationMixin):
+ """View for $project/+admin"""
     label = "Administer project details"
     field_names = ["name", "owner", "active", "autoupdate", "private_bugs"]

@@ -1443,15 +1455,7 @@
     def validate(self, data):
         """See `La...

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :
Download full text (11.4 KiB)

> Hi Edwin,
>
> This is nice, and it's good to see some XXXs go in the process. The
> view code with the Storm validator as a backstop is elegant.
>
> I've got a few questions and comments, so Needs Information for now.
>
> Cheers, Gavin.
>

Hi Gavin and Brad,

Thanks for the reviews. The incremental diff is in the previous comment.

> > === modified file 'lib/lp/registry/browser/product.py'
> > --- lib/lp/registry/browser/product.py 2010-04-14 17:16:56 +0000
> > +++ lib/lp/registry/browser/product.py 2010-04-21 11:03:32 +0000
> > @@ -1444,6 +1444,15 @@
> > """See `LaunchpadFormView`."""
> > self.validate_private_bugs(data)
> >
> > + if data['active'] == False and self.context.active == True:
> > + if len(self.context.sourcepackages) > 0:
> > + self.setFieldError('active',
> > + structured(
> > + 'This project cannot be deactivated since it is '
> > + 'linked to '
> > + '<a href="%s/+packages">source packages</a>.',
> > + canonical_url(self.context)))
>
> You could change this to instead pass view_name='+packages' to
> canonical_url().

Fixed.

> It is worth showing this message before the user tries to deactivate
> the project? Perhaps a message next to the control, and the control
> disabled.

That's a nice idea, however, the validation would still be necessary for
race conditions when the form is displayed before the source package is
linked. Since deactivation will be done much more often by reviewers
than regular users, I don't know if it is worth it. I believe Curtis
still reviews most of the projects, so I'll ask him.

> Also, the text "This project cannot be deactivated since it is linked
> to source packages" doesn't sound quite right. How about "... linked
> to one or more source packages"?

Fixed.

> Perhaps it's worth getting a UI review?

Since I'm already talking to Curtis, and he's a UI reviewer, I'll ask him.

> > +
> > @property
> > def cancel_url(self):
> > """See `LaunchpadFormView`."""
> > @@ -1491,6 +1500,15 @@
> > # supervisor.
> > self.validate_private_bugs(data)
> >
> > + if data['active'] == False and self.context.active == True:
> > + if len(self.context.sourcepackages) > 0:
> > + self.setFieldError('active',
> > + structured(
> > + 'This project cannot be deactivated since it is '
> > + 'linked to '
> > + '<a href="%s/+packages">source packages</a>.',
> > + canonical_url(self.context)))
>
> Same here, re. view_name.
>
> ProductReviewLicenseView.validate() uses the same validation code as
> in ProductAdminView.validate(). Consider putting this in a mixin class
> that both ProductAdminView and ProductReviewLicenseView can use.

Done.

> > +
> >
> > class ProductAddSeriesView(LaunchpadFormView):
> > """A form to add new product series"""
> >
> > === modified file 'lib/lp/registry/tests/test_product.py'
> > --- lib/lp/registry/tests/test_product.py 2...

Revision history for this message
Gavin Panella (allenap) wrote :

Thanks Edwin, looks good :)

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

Hello Edwin, et al.

Regarding who and how projects are deactivated...

Users cannot deactivate a project, reviewers and admins can. The rule of not deactivating a linked project seemed obvious 6 months ago and I have always deleted bad packaging links /before/ deactivating a project. It is clear from the oops this was not obvious to everyone so I reported a bug to stop other reviewers from doing the wrong thing.

I do not deactivate projects with legitimate links to Ubuntu. This has frustrated two users who think their desires outweigh the community. We *will not* deactivate any linked project because someone else will just have it reactivated.

This issue relates to series deletion. Users cannot delete a series that is linked to a package. The user can delete the packaing links. In the case of the sugar project, the user recreated links for the correct series.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/answers/doc/person.txt'
--- lib/lp/answers/doc/person.txt 2010-02-05 21:25:23 +0000
+++ lib/lp/answers/doc/person.txt 2010-04-22 14:31:33 +0000
@@ -351,6 +351,10 @@
351supported projects.351supported projects.
352352
353 >>> login('foo.bar@canonical.com')353 >>> login('foo.bar@canonical.com')
354
355 # Unlink the source packages so the project can be deactivated.
356 >>> from lp.testing import unlink_source_packages
357 >>> unlink_source_packages(firefox)
354 >>> firefox.active = False358 >>> firefox.active = False
355 >>> sorted(target.name359 >>> sorted(target.name
356 ... for target in no_priv.getDirectAnswerQuestionTargets())360 ... for target in no_priv.getDirectAnswerQuestionTargets())
357361
=== modified file 'lib/lp/blueprints/doc/specification.txt'
--- lib/lp/blueprints/doc/specification.txt 2009-08-13 19:03:36 +0000
+++ lib/lp/blueprints/doc/specification.txt 2010-04-22 14:31:33 +0000
@@ -205,14 +205,18 @@
205205
206Specs from inactive products are filtered out.206Specs from inactive products are filtered out.
207207
208 >>> from canonical.database.sqlbase import flush_database_updates208 >>> from canonical.database.sqlbase import flush_database_updates
209 >>> login('mark@example.com')209 >>> login('mark@example.com')
210 >>> upstream_firefox.active = False210
211 >>> flush_database_updates()211 # Unlink the source packages so the project can be deactivated.
212 >>> for spec in specset.specifications(filter=['install']):212 >>> from lp.testing import unlink_source_packages
213 ... print spec.name, spec.target.name213 >>> unlink_source_packages(upstream_firefox)
214 cluster-installation kubuntu214 >>> upstream_firefox.active = False
215 media-integrity-check ubuntu215 >>> flush_database_updates()
216 >>> for spec in specset.specifications(filter=['install']):
217 ... print spec.name, spec.target.name
218 cluster-installation kubuntu
219 media-integrity-check ubuntu
216220
217221
218Reset firefox so we don't mess up later tests.222Reset firefox so we don't mess up later tests.
219223
=== modified file 'lib/lp/blueprints/doc/sprint.txt'
--- lib/lp/blueprints/doc/sprint.txt 2010-02-17 11:13:06 +0000
+++ lib/lp/blueprints/doc/sprint.txt 2010-04-22 14:31:33 +0000
@@ -151,14 +151,18 @@
151151
152Inactive products are excluded from the listings.152Inactive products are excluded from the listings.
153153
154 >>> from canonical.launchpad.interfaces import IProductSet154 >>> from canonical.launchpad.interfaces import IProductSet
155 >>> from canonical.launchpad.ftests import login155 >>> from canonical.launchpad.ftests import login
156 >>> firefox = getUtility(IProductSet).getByName('firefox')156 >>> firefox = getUtility(IProductSet).getByName('firefox')
157 >>> login("foo.bar@canonical.com")157 >>> login("foo.bar@canonical.com")
158 >>> firefox.active = False158
159 >>> flush_database_updates()159 # Unlink the source packages so the project can be deactivated.
160 >>> ubz.specifications().count()160 >>> from lp.testing import unlink_source_packages
161 0161 >>> unlink_source_packages(firefox)
162 >>> firefox.active = False
163 >>> flush_database_updates()
164 >>> ubz.specifications().count()
165 0
162166
163Reset firefox so we don't mess up later tests.167Reset firefox so we don't mess up later tests.
164168
165169
=== modified file 'lib/lp/bugs/browser/bugalsoaffects.py'
--- lib/lp/bugs/browser/bugalsoaffects.py 2010-01-15 03:32:46 +0000
+++ lib/lp/bugs/browser/bugalsoaffects.py 2010-04-22 14:31:33 +0000
@@ -103,26 +103,6 @@
103 bugtask = self.context103 bugtask = self.context
104 upstream = bugtask.target.upstream_product104 upstream = bugtask.target.upstream_product
105 if upstream is not None:105 if upstream is not None:
106 if not upstream.active:
107 # XXX: Guilherme Salgado 2007-09-18 bug=140526: This is only
108 # possible because of bug 140526, which allows packages to
109 # be linked to inactive products.
110 series = bugtask.distribution.currentseries
111 assert series is not None, (
112 "This package is linked to a product series so this "
113 "package's distribution must have at least one distro "
114 "series.")
115 sourcepackage = series.getSourcePackage(
116 bugtask.sourcepackagename)
117 self.request.response.addWarningNotification(
118 structured(
119 _("""
120 This package is linked to an inactive upstream. You
121 can <a href="%(package_url)s/+edit-packaging">fix it</a>
122 to avoid this step in the future."""),
123 package_url=canonical_url(sourcepackage)))
124 return
125
126 try:106 try:
127 valid_upstreamtask(bugtask.bug, upstream)107 valid_upstreamtask(bugtask.bug, upstream)
128 except WidgetsError:108 except WidgetsError:
129109
=== modified file 'lib/lp/bugs/browser/tests/bugtask-adding-views.txt'
--- lib/lp/bugs/browser/tests/bugtask-adding-views.txt 2009-07-06 16:23:49 +0000
+++ lib/lp/bugs/browser/tests/bugtask-adding-views.txt 2010-04-22 14:31:33 +0000
@@ -162,42 +162,6 @@
162 >>> len(add_task_view.request.response.notifications)162 >>> len(add_task_view.request.response.notifications)
163 0163 0
164164
165XXX: Because of bug 140526, it's possible to have a package linked to a series
166of an inactive upstream. In a case like that we must ask the user to specify
167the upstream, but we also display a warning telling the user there's a
168package linked to an inactive upstream. -- Guilherme Salgado, 2007-09-18
169
170 # Create the view manually just so we can get the correct upstream
171 # easily.
172 >>> from lp.bugs.browser.bugalsoaffects import (
173 ... ChooseProductStep)
174 >>> view = ChooseProductStep(
175 ... ubuntu_firefox_task, LaunchpadTestRequest(method='GET', form={}))
176 >>> upstream = view.context.target.upstream_product
177 >>> upstream.active
178 True
179
180 # Mark the upstream as inactive.
181 >>> from zope.security.proxy import removeSecurityProxy
182 >>> removeSecurityProxy(upstream).active = False
183 >>> removeSecurityProxy(upstream).syncUpdate()
184
185 >>> add_task_view = get_and_setup_view(
186 ... ubuntu_firefox_task, '+choose-affected-product', form={},
187 ... method='GET')
188
189 >>> add_task_view.step_name
190 'choose_product'
191 >>> print add_task_view.widgets['product']._getFormInput()
192 None
193 >>> for notification in add_task_view.request.response.notifications:
194 ... print notification.message.strip()
195 This package is linked to an inactive upstream...
196
197 # Mark the upstream back as active.
198 >>> removeSecurityProxy(upstream).active = True
199 >>> removeSecurityProxy(upstream).syncUpdate()
200
201Let's take a look at the second step now, where we may enter the URL of165Let's take a look at the second step now, where we may enter the URL of
202the remote bug and confirm the bugtask creation.166the remote bug and confirm the bugtask creation.
203In order to show that all the events get fired off, let's create an167In order to show that all the events get fired off, let's create an
204168
=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py 2010-04-14 17:16:56 +0000
+++ lib/lp/registry/browser/product.py 2010-04-22 14:31:33 +0000
@@ -1376,7 +1376,7 @@
1376 return self.next_url1376 return self.next_url
13771377
13781378
1379class EditPrivateBugsMixin:1379class ProductValidationMixin:
13801380
1381 def validate_private_bugs(self, data):1381 def validate_private_bugs(self, data):
1382 """Perform validation for the private bugs setting."""1382 """Perform validation for the private bugs setting."""
@@ -1387,8 +1387,20 @@
1387 'for this project first.',1387 'for this project first.',
1388 canonical_url(self.context, rootsite="bugs")))1388 canonical_url(self.context, rootsite="bugs")))
13891389
13901390 def validate_deactivation(self, data):
1391class ProductAdminView(ProductEditView, EditPrivateBugsMixin):1391 """Verify whether a product can be safely deactivated."""
1392 if data['active'] == False and self.context.active == True:
1393 if len(self.context.sourcepackages) > 0:
1394 self.setFieldError('active',
1395 structured(
1396 'This project cannot be deactivated since it is '
1397 'linked to one or more '
1398 '<a href="%s">source packages</a>.',
1399 canonical_url(self.context, view_name='+packages')))
1400
1401
1402class ProductAdminView(ProductEditView, ProductValidationMixin):
1403 """View for $project/+admin"""
1392 label = "Administer project details"1404 label = "Administer project details"
1393 field_names = ["name", "owner", "active", "autoupdate", "private_bugs"]1405 field_names = ["name", "owner", "active", "autoupdate", "private_bugs"]
13941406
@@ -1443,6 +1455,7 @@
1443 def validate(self, data):1455 def validate(self, data):
1444 """See `LaunchpadFormView`."""1456 """See `LaunchpadFormView`."""
1445 self.validate_private_bugs(data)1457 self.validate_private_bugs(data)
1458 self.validate_deactivation(data)
14461459
1447 @property1460 @property
1448 def cancel_url(self):1461 def cancel_url(self):
@@ -1451,7 +1464,7 @@
14511464
14521465
1453class ProductReviewLicenseView(ReturnToReferrerMixin,1466class ProductReviewLicenseView(ReturnToReferrerMixin,
1454 ProductEditView, EditPrivateBugsMixin):1467 ProductEditView, ProductValidationMixin):
1455 """A view to review a project and change project privileges."""1468 """A view to review a project and change project privileges."""
1456 label = "Review project"1469 label = "Review project"
1457 field_names = [1470 field_names = [
@@ -1490,6 +1503,7 @@
1490 # Private bugs can only be enabled if the product has a bug1503 # Private bugs can only be enabled if the product has a bug
1491 # supervisor.1504 # supervisor.
1492 self.validate_private_bugs(data)1505 self.validate_private_bugs(data)
1506 self.validate_deactivation(data)
14931507
14941508
1495class ProductAddSeriesView(LaunchpadFormView):1509class ProductAddSeriesView(LaunchpadFormView):
14961510
=== modified file 'lib/lp/registry/browser/tests/product-views.txt'
--- lib/lp/registry/browser/tests/product-views.txt 2010-04-14 20:54:40 +0000
+++ lib/lp/registry/browser/tests/product-views.txt 2010-04-22 14:31:33 +0000
@@ -155,9 +155,27 @@
155 ['license_reviewed', 'license_approved', 'active', 'private_bugs',155 ['license_reviewed', 'license_approved', 'active', 'private_bugs',
156 'reviewer_whiteboard']156 'reviewer_whiteboard']
157157
158The reviewer cannot deactivate a project if it is linked
159to a source package.
160
161 >>> firefox.active
162 True
163
164 >>> form = {
165 ... 'field.active.used': '', # unchecked
166 ... 'field.reviewer_whiteboard': 'Looks bogus',
167 ... 'field.actions.change': 'Change',
168 ... }
169 >>> view = create_initialized_view(
170 ... firefox, name='+review-license', form=form)
171 >>> view.errors
172 [...This project cannot be deactivated since it is linked to
173 ...source packages</a>.']
174
158The reviewer can deactivate a project if he concludes it is bogus.175The reviewer can deactivate a project if he concludes it is bogus.
159176
160 >>> firefox.active177 >>> product = factory.makeProduct(name='tomato', title='Tomato')
178 >>> product.active
161 True179 True
162180
163 >>> form = {181 >>> form = {
@@ -166,12 +184,12 @@
166 ... 'field.actions.change': 'Change',184 ... 'field.actions.change': 'Change',
167 ... }185 ... }
168 >>> view = create_initialized_view(186 >>> view = create_initialized_view(
169 ... firefox, name='+review-license', form=form)187 ... product, name='+review-license', form=form)
170 >>> view.errors188 >>> view.errors
171 []189 []
172 >>> firefox.active190 >>> product.active
173 False191 False
174 >>> print firefox.reviewer_whiteboard192 >>> print product.reviewer_whiteboard
175 Looks bogus193 Looks bogus
176194
177The reviewer can enable privileged features like private bugs. He can195The reviewer can enable privileged features like private bugs. He can
@@ -439,8 +457,8 @@
439officially supports.457officially supports.
440458
441 >>> from canonical.launchpad.testing.pages import find_tag_by_id459 >>> from canonical.launchpad.testing.pages import find_tag_by_id
460 >>> product = factory.makeProduct(name='tomato', title='Tomato')
442461
443 >>> product = factory.makeProduct(name='tomato', title='Tomato')
444 >>> owner = product.owner462 >>> owner = product.owner
445 >>> login_person(owner)463 >>> login_person(owner)
446 >>> question = factory.makeQuestion(target=product)464 >>> question = factory.makeQuestion(target=product)
447465
=== modified file 'lib/lp/registry/doc/milestone.txt'
--- lib/lp/registry/doc/milestone.txt 2010-04-16 15:06:55 +0000
+++ lib/lp/registry/doc/milestone.txt 2010-04-22 14:31:33 +0000
@@ -221,6 +221,10 @@
221221
222 >>> print [milestone.name for milestone in gnome.milestones]222 >>> print [milestone.name for milestone in gnome.milestones]
223 [u'1.1.']223 [u'1.1.']
224
225 # Unlink the source packages so the project can be deactivated.
226 >>> from lp.testing import unlink_source_packages
227 >>> unlink_source_packages(netapplet)
224 >>> netapplet.active = False228 >>> netapplet.active = False
225 >>> syncUpdate(netapplet)229 >>> syncUpdate(netapplet)
226 >>> print [milestone.name for milestone in gnome.milestones]230 >>> print [milestone.name for milestone in gnome.milestones]
227231
=== modified file 'lib/lp/registry/doc/person.txt'
--- lib/lp/registry/doc/person.txt 2010-04-18 17:20:47 +0000
+++ lib/lp/registry/doc/person.txt 2010-04-22 14:31:33 +0000
@@ -1344,6 +1344,10 @@
1344 >>> from canonical.launchpad.ftests import login1344 >>> from canonical.launchpad.ftests import login
1345 >>> firefox = getUtility(IProductSet).getByName('firefox')1345 >>> firefox = getUtility(IProductSet).getByName('firefox')
1346 >>> login("mark@example.com")1346 >>> login("mark@example.com")
1347
1348 # Unlink the source packages so the project can be deactivated.
1349 >>> from lp.testing import unlink_source_packages
1350 >>> unlink_source_packages(firefox)
1347 >>> firefox.active = False1351 >>> firefox.active = False
1348 >>> flush_database_updates()1352 >>> flush_database_updates()
1349 >>> cprov.specifications(filter=['svg']).count()1353 >>> cprov.specifications(filter=['svg']).count()
13501354
=== modified file 'lib/lp/registry/doc/pillar.txt'
--- lib/lp/registry/doc/pillar.txt 2010-04-19 15:13:39 +0000
+++ lib/lp/registry/doc/pillar.txt 2010-04-22 14:31:33 +0000
@@ -113,6 +113,9 @@
113113
114But only if the pillar which they point to is active.114But only if the pillar which they point to is active.
115115
116 # Unlink the source packages so the project can be deactivated.
117 >>> from lp.testing import unlink_source_packages
118 >>> unlink_source_packages(firefox)
116 >>> firefox.active = False119 >>> firefox.active = False
117 >>> 'iceweasel' in pillar_set120 >>> 'iceweasel' in pillar_set
118 False121 False
119122
=== modified file 'lib/lp/registry/doc/product.txt'
--- lib/lp/registry/doc/product.txt 2010-04-16 15:06:55 +0000
+++ lib/lp/registry/doc/product.txt 2010-04-22 14:31:33 +0000
@@ -54,7 +54,7 @@
54 >>> evo = p54 >>> evo = p
5555
56To fetch a product we use IProductSet.getByName() or IProductSet.__getitem__.56To fetch a product we use IProductSet.getByName() or IProductSet.__getitem__.
57The former will, by default, reeturn active and inactive products, while the57The former will, by default, return active and inactive products, while the
58later returns only active ones. Both can be used to look up products by their58later returns only active ones. Both can be used to look up products by their
59aliases, though.59aliases, though.
6060
@@ -91,6 +91,9 @@
9191
92Now, to test the active flag. If we disabled a product:92Now, to test the active flag. If we disabled a product:
9393
94 # Unlink the source packages so the project can be deactivated.
95 >>> from lp.testing import unlink_source_packages
96 >>> unlink_source_packages(a52dec)
94 >>> a52dec.active = False97 >>> a52dec.active = False
9598
96It should no longer be retrievable via ProductSet's __getitem__:99It should no longer be retrievable via ProductSet's __getitem__:
@@ -193,6 +196,9 @@
193196
194Only active products are listed as translatables.197Only active products are listed as translatables.
195198
199 # Unlink the source packages so the project can be deactivated.
200 >>> from lp.testing import unlink_source_packages
201 >>> unlink_source_packages(evo)
196 >>> evo.active = False202 >>> evo.active = False
197 >>> for product in productset.getTranslatables():203 >>> for product in productset.getTranslatables():
198 ... print product.name204 ... print product.name
@@ -517,12 +523,12 @@
517523
518Only active products are returned.524Only active products are returned.
519525
520 >>> firefox.active = False526 >>> evo.active = False
521 >>> from canonical.launchpad.ftests import syncUpdate527 >>> from canonical.launchpad.ftests import syncUpdate
522 >>> syncUpdate(firefox)528 >>> syncUpdate(firefox)
523 >>> for product in productset.getProductsWithBranches():529 >>> for product in productset.getProductsWithBranches():
524 ... print product.name530 ... print product.name
525 evolution531 firefox
526 gnome-terminal532 gnome-terminal
527 iso-codes533 iso-codes
528 landscape534 landscape
529535
=== modified file 'lib/lp/registry/doc/project.txt'
--- lib/lp/registry/doc/project.txt 2010-04-19 09:39:29 +0000
+++ lib/lp/registry/doc/project.txt 2010-04-22 14:31:33 +0000
@@ -31,7 +31,7 @@
31== Looking up existing projects ==31== Looking up existing projects ==
3232
33To fetch a project we use IProjectGroupSet.getByName() or33To fetch a project we use IProjectGroupSet.getByName() or
34IProjectGroupSet.__getitem__. The former will, by default, reeturn active and34IProjectGroupSet.__getitem__. The former will, by default, return active and
35inactive projects, while the latter returns only active ones. Both can be35inactive projects, while the latter returns only active ones. Both can be
36used to look up projects by their aliases, though.36used to look up projects by their aliases, though.
3737
@@ -108,6 +108,10 @@
108 [u'applets', u'evolution', u'gnome-terminal', u'gnomebaker', u'netapplet']108 [u'applets', u'evolution', u'gnome-terminal', u'gnomebaker', u'netapplet']
109109
110 >>> netapplet = gnome.getProduct('netapplet')110 >>> netapplet = gnome.getProduct('netapplet')
111
112 # Unlink the source packages so the project can be deactivated.
113 >>> from lp.testing import unlink_source_packages
114 >>> unlink_source_packages(netapplet)
111 >>> netapplet.active = False115 >>> netapplet.active = False
112 >>> flush_database_updates()116 >>> flush_database_updates()
113 >>> [product.name for product in gnome.products]117 >>> [product.name for product in gnome.products]
@@ -161,6 +165,10 @@
161165
162 >>> from canonical.launchpad.interfaces import IProductSet166 >>> from canonical.launchpad.interfaces import IProductSet
163 >>> firefox = getUtility(IProductSet).getByName('firefox')167 >>> firefox = getUtility(IProductSet).getByName('firefox')
168
169 # Unlink the source packages so the project can be deactivated.
170 >>> from lp.testing import unlink_source_packages
171 >>> unlink_source_packages(firefox)
164 >>> firefox.active = False172 >>> firefox.active = False
165 >>> flush_database_updates()173 >>> flush_database_updates()
166 >>> filter = [SpecificationFilter.INCOMPLETE]174 >>> filter = [SpecificationFilter.INCOMPLETE]
167175
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-04-19 09:39:29 +0000
+++ lib/lp/registry/model/product.py 2010-04-22 14:31:33 +0000
@@ -273,7 +273,6 @@
273273
274 enable_bug_expiration = BoolCol(dbName='enable_bug_expiration',274 enable_bug_expiration = BoolCol(dbName='enable_bug_expiration',
275 notNull=True, default=False)275 notNull=True, default=False)
276 active = BoolCol(dbName='active', notNull=True, default=True)
277 license_reviewed = BoolCol(dbName='reviewed', notNull=True, default=False)276 license_reviewed = BoolCol(dbName='reviewed', notNull=True, default=False)
278 reviewer_whiteboard = StringCol(notNull=False, default=None)277 reviewer_whiteboard = StringCol(notNull=False, default=None)
279 private_bugs = BoolCol(278 private_bugs = BoolCol(
@@ -290,6 +289,18 @@
290 bug_reporting_guidelines = StringCol(default=None)289 bug_reporting_guidelines = StringCol(default=None)
291 _cached_licenses = None290 _cached_licenses = None
292291
292 def _validate_active(self, attr, value):
293 # Validate deactivation.
294 if self.active == True and value == False:
295 if len(self.sourcepackages) > 0:
296 raise AssertionError(
297 'This project cannot be deactivated since it is '
298 'linked to source packages.')
299 return value
300
301 active = BoolCol(dbName='active', notNull=True, default=True,
302 storm_validator=_validate_active)
303
293 def _validate_license_info(self, attr, value):304 def _validate_license_info(self, attr, value):
294 if not self._SO_creating and value != self.license_info:305 if not self._SO_creating and value != self.license_info:
295 # Clear the license_reviewed and license_approved flags306 # Clear the license_reviewed and license_approved flags
296307
=== modified file 'lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt'
--- lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt 2008-01-16 19:27:48 +0000
+++ lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt 2010-04-22 14:31:33 +0000
@@ -7,12 +7,20 @@
7cover that part of the change. See bug #156263 for more details.7cover that part of the change. See bug #156263 for more details.
8 -- kiko, 2007-10-238 -- kiko, 2007-10-23
99
10 >>> from lp.testing import unlink_source_packages
11 >>> from lp.registry.interfaces.pillar import IPillarNameSet
12 >>> from lp.registry.interfaces.product import IProduct
13 >>> from zope.component import getUtility
14 >>> pillar_set = getUtility(IPillarNameSet)
10 >>> def toggleProject(name):15 >>> def toggleProject(name):
11 ... admin_browser.open('http://launchpad.dev/%s' % name)16 ... login('admin@canonical.com')
12 ... admin_browser.getLink(url='+review').click()17 ... pillar = pillar_set.getByName(name)
13 ... state = admin_browser.getControl('Active').selected18 ... if IProduct.providedBy(pillar) and pillar.active:
14 ... admin_browser.getControl('Active').selected = (not state)19 ... # A product cannot be deactivated if
15 ... admin_browser.getControl('Change').click()20 ... # it is linked to source packages.
21 ... unlink_source_packages(pillar)
22 ... pillar.active = not pillar.active
23 ... logout()
1624
17We start off with active and visible projects:25We start off with active and visible projects:
1826
@@ -72,13 +80,12 @@
72 >>> anon_browser.getLink(url='/firefox').click()80 >>> anon_browser.getLink(url='/firefox').click()
73 >>> anon_browser.title81 >>> anon_browser.title
74 'Mozilla Firefox in Launchpad'82 'Mozilla Firefox in Launchpad'
75 >>> print find_tag_by_id(admin_browser.contents, 'project-inactive')83 >>> print find_tag_by_id(anon_browser.contents, 'project-inactive')
76 None84 None
7785
78 >>> anon_browser.open('http://launchpad.dev/projects/+index?text=mozilla')86 >>> anon_browser.open('http://launchpad.dev/projects/+index?text=mozilla')
79 >>> anon_browser.getLink(url='/mozilla').click()87 >>> anon_browser.getLink(url='/mozilla').click()
80 >>> anon_browser.title88 >>> anon_browser.title
81 'The Mozilla Project in Launchpad'89 'The Mozilla Project in Launchpad'
82 >>> print find_tag_by_id(admin_browser.contents, 'project-inactive')90 >>> print find_tag_by_id(anon_browser.contents, 'project-inactive')
83 None91 None
84
8592
=== modified file 'lib/lp/registry/stories/product/xx-product-edit.txt'
--- lib/lp/registry/stories/product/xx-product-edit.txt 2010-03-16 22:07:45 +0000
+++ lib/lp/registry/stories/product/xx-product-edit.txt 2010-04-22 14:31:33 +0000
@@ -332,27 +332,27 @@
332332
333The Admins and Registry Experts can deactivate a project.333The Admins and Registry Experts can deactivate a project.
334334
335 >>> registry_browser.open('http://launchpad.dev/firefox/+review-license')335 >>> registry_browser.open('http://launchpad.dev/bzr/+review-license')
336 >>> registry_browser.getControl(name='field.active').value = False336 >>> registry_browser.getControl(name='field.active').value = False
337 >>> registry_browser.getControl(name='field.actions.change').click()337 >>> registry_browser.getControl(name='field.actions.change').click()
338 >>> print registry_browser.url338 >>> print registry_browser.url
339 http://launchpad.dev/firefox339 http://launchpad.dev/bzr
340340
341The product overview page should show a notice that a product is341The product overview page should show a notice that a product is
342inactive with a link to a form to re-activate it. Admins and Commercial342inactive with a link to a form to re-activate it. Admins and Commercial
343Admins can still see the product, but regular users can't.343Admins can still see the product, but regular users can't.
344344
345 >>> registry_browser.open('http://launchpad.dev/firefox')345 >>> registry_browser.open('http://launchpad.dev/bzr')
346 >>> contents = find_main_content(registry_browser.contents)346 >>> contents = find_main_content(registry_browser.contents)
347 >>> print extract_text(contents.find(id='project-inactive'))347 >>> print extract_text(contents.find(id='project-inactive'))
348 This project is currently inactive ...348 This project is currently inactive ...
349349
350 >>> admin_browser.open('http://launchpad.dev/firefox')350 >>> admin_browser.open('http://launchpad.dev/bzr')
351 >>> contents = find_main_content(admin_browser.contents)351 >>> contents = find_main_content(admin_browser.contents)
352 >>> print extract_text(contents.find(id='project-inactive'))352 >>> print extract_text(contents.find(id='project-inactive'))
353 This project is currently inactive ...353 This project is currently inactive ...
354354
355 >>> user_browser.open('http://launchpad.dev/firefox')355 >>> user_browser.open('http://launchpad.dev/bzr')
356 Traceback (most recent call last):356 Traceback (most recent call last):
357 ...357 ...
358 NotFound...358 NotFound...
@@ -361,12 +361,12 @@
361361
362 >>> registry_browser.getLink('Review project').click()362 >>> registry_browser.getLink('Review project').click()
363 >>> print registry_browser.url363 >>> print registry_browser.url
364 http://launchpad.dev/firefox/+review-license364 http://launchpad.dev/bzr/+review-license
365365
366 >>> registry_browser.getControl(name='field.active').value = True366 >>> registry_browser.getControl(name='field.active').value = True
367 >>> registry_browser.getControl(name='field.actions.change').click()367 >>> registry_browser.getControl(name='field.actions.change').click()
368 >>> print registry_browser.url368 >>> print registry_browser.url
369 http://launchpad.dev/firefox369 http://launchpad.dev/bzr
370370
371 >>> contents = find_main_content(registry_browser.contents)371 >>> contents = find_main_content(registry_browser.contents)
372 >>> print contents.find(id='project-inactive')372 >>> print contents.find(id='project-inactive')
373373
=== modified file 'lib/lp/registry/stories/project/xx-project-index.txt'
--- lib/lp/registry/stories/project/xx-project-index.txt 2010-04-19 08:11:52 +0000
+++ lib/lp/registry/stories/project/xx-project-index.txt 2010-04-22 14:31:33 +0000
@@ -150,6 +150,12 @@
150 # (i.e. login()) and bypass the security proxy.150 # (i.e. login()) and bypass the security proxy.
151 >>> from lp.registry.model.product import Product151 >>> from lp.registry.model.product import Product
152 >>> firefox = Product.byName('firefox')152 >>> firefox = Product.byName('firefox')
153
154 # Unlink the source packages so the project can be deactivated.
155 >>> from lp.testing import unlink_source_packages
156 >>> login('admin@canonical.com')
157 >>> unlink_source_packages(firefox)
158 >>> logout()
153 >>> firefox.active = False159 >>> firefox.active = False
154 >>> firefox.syncUpdate()160 >>> firefox.syncUpdate()
155161
156162
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2009-10-23 13:48:28 +0000
+++ lib/lp/registry/tests/test_product.py 2010-04-22 14:31:33 +0000
@@ -26,6 +26,34 @@
26 CommercialSubscription)26 CommercialSubscription)
27from lp.testing import TestCaseWithFactory27from lp.testing import TestCaseWithFactory
2828
29class TestProduct(TestCaseWithFactory):
30 """Tests product object."""
31
32 layer = LaunchpadFunctionalLayer
33
34 def test_deactivation_failure(self):
35 # Ensure that a product cannot be deactivated if
36 # it is linked to source packages.
37 login('admin@canonical.com')
38 product = self.factory.makeProduct()
39 source_package = self.factory.makeSourcePackage()
40 self.assertEqual(True, product.active)
41 source_package.setPackaging(
42 product.development_focus, self.factory.makePerson())
43 self.assertRaises(
44 AssertionError,
45 setattr, product, 'active', False)
46
47 def test_deactivation_success(self):
48 # Ensure that a product can be deactivated if
49 # it is not linked to source packages.
50 login('admin@canonical.com')
51 product = self.factory.makeProduct()
52 self.assertEqual(True, product.active)
53 product.active = False
54 self.assertEqual(False, product.active)
55
56
29class TestProductFiles(unittest.TestCase):57class TestProductFiles(unittest.TestCase):
30 """Tests for downloadable product files."""58 """Tests for downloadable product files."""
3159
3260
=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py 2010-04-18 22:31:40 +0000
+++ lib/lp/testing/__init__.py 2010-04-22 14:31:33 +0000
@@ -31,6 +31,7 @@
31 'TestCaseWithFactory',31 'TestCaseWithFactory',
32 'test_tales',32 'test_tales',
33 'time_counter',33 'time_counter',
34 'unlink_source_packages',
34 # XXX: This really shouldn't be exported from here. People should import35 # XXX: This really shouldn't be exported from here. People should import
35 # it from Zope.36 # it from Zope.
36 'verifyObject',37 'verifyObject',
@@ -83,6 +84,7 @@
83from canonical.launchpad.webapp.interfaces import ILaunchBag84from canonical.launchpad.webapp.interfaces import ILaunchBag
84from canonical.launchpad.windmill.testing import constants85from canonical.launchpad.windmill.testing import constants
85from lp.codehosting.vfs import branch_id_to_path, get_multi_server86from lp.codehosting.vfs import branch_id_to_path, get_multi_server
87from lp.registry.interfaces.packaging import IPackagingUtil
86# Import the login and logout functions here as it is a much better88# Import the login and logout functions here as it is a much better
87# place to import them from in tests.89# place to import them from in tests.
88from lp.testing._login import (90from lp.testing._login import (
@@ -930,3 +932,15 @@
930 name, mock_args, real_args))932 name, mock_args, real_args))
931 else:933 else:
932 break934 break
935
936def unlink_source_packages(product):
937 """Remove all links between the product and source packages.
938
939 A product cannot be deactivated if it is linked to source packages.
940 """
941 packaging_util = getUtility(IPackagingUtil)
942 for source_package in product.sourcepackages:
943 packaging_util.deletePackaging(
944 source_package.productseries,
945 source_package.sourcepackagename,
946 source_package.distroseries)
933947
=== modified file 'lib/lp/translations/doc/translationimportqueue.txt'
--- lib/lp/translations/doc/translationimportqueue.txt 2010-02-05 15:28:42 +0000
+++ lib/lp/translations/doc/translationimportqueue.txt 2010-04-22 14:31:33 +0000
@@ -1043,6 +1043,9 @@
1043An administrator deactivates Firefox, thinking that the project is being1043An administrator deactivates Firefox, thinking that the project is being
1044run in violation of Launchpad policies.1044run in violation of Launchpad policies.
10451045
1046 # Unlink the source packages so the project can be deactivated.
1047 >>> from lp.testing import unlink_source_packages
1048 >>> unlink_source_packages(firefox)
1046 >>> firefox.active = False1049 >>> firefox.active = False
1047 >>> syncUpdate(firefox)1050 >>> syncUpdate(firefox)
10481051
10491052
=== modified file 'lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt'
--- lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt 2009-08-25 16:42:56 +0000
+++ lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt 2010-04-22 14:31:33 +0000
@@ -26,6 +26,13 @@
26 >>> admin_browser.url26 >>> admin_browser.url
27 'http://launchpad.dev/projectgroups'27 'http://launchpad.dev/projectgroups'
2828
29 # Unlink the source packages so the project can be deactivated.
30 >>> from lp.testing import unlink_source_packages
31 >>> from lp.registry.interfaces.product import IProductSet
32 >>> from zope.component import getUtility
33 >>> login('admin@canonical.com')
34 >>> unlink_source_packages(getUtility(IProductSet).getByName('netapplet'))
35 >>> logout()
29 >>> admin_browser.open("http://launchpad.dev/netapplet/+admin")36 >>> admin_browser.open("http://launchpad.dev/netapplet/+admin")
30 >>> admin_browser.getControl("Active").click()37 >>> admin_browser.getControl("Active").click()
31 >>> admin_browser.getControl("Change").click()38 >>> admin_browser.getControl("Change").click()