Merge lp:~edwin-grubbs/launchpad/bug-553384-deactivated-project-oops into lp:launchpad
- bug-553384-deactivated-project-oops
- Merge into devel
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 |
Related bugs: |
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/
Description of the change
Summary
-------
Fixed bug 553384 and bug 140526.
The $sourcepackage/
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/
lib/
lib/
lib/
Removed workaround for bug 140526:
lib/
lib/
Fixed tests:
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
lib/
Tests
-----
./bin/test -vv -t 'test_product|
Demo and Q/A
------------
* Open http://
* 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://
* 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."
Gavin Panella (allenap) wrote : | # |
Brad Crittenden (bac) wrote : | # |
Hi Edwin,
Thanks for making this fix...it sure had lots of tentacles.
> === modified file 'lib/lp/
> --- lib/lp/
+++ lib/lp/
> @@ -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_
> + >>> unlink_
> >>> firefox.active = False
> >>> sorted(target.name
> ... for target in no_priv.
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -103,26 +103,6 @@
> bugtask = self.context
> upstream = bugtask.
> 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.
> - 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.
> - bugtask.
> - self.request.
> - structured(
> - _("""
> - This package is linked to an inactive upstream. You
> - can <a href="%
> - to avoid this step in the future."""),
> - package_
> - return
> -
I'm really glad you found and removed these.
> try:
> valid_upstreamt
> except WidgetsError:
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -1444,6 +1444,15 @@
> """See `LaunchpadFormV
> self.validate_
>
> + if data['active'] == False and self.context.active == True:
> + if len(self.
> + self.setFieldEr
> + structured(
> + 'This project cannot be dea...
Edwin Grubbs (edwin-grubbs) wrote : | # |
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -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_
>>> unlink_
>>> firefox.active = False
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -208,7 +208,7 @@
>>> from canonical.
>>> 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_
>>> unlink_
>>> upstream_
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -156,7 +156,7 @@
>>> firefox = getUtility(
>>> 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_
>>> unlink_
>>> firefox.active = False
=== modified file 'lib/lp/
--- lib/lp/
+++ lib/lp/
@@ -1376,7 +1376,7 @@
return self.next_url
-class EditPrivateBugs
+class ProductValidati
def validate_
"""Perform validation for the private bugs setting."""
@@ -1387,8 +1387,20 @@
-
-class ProductAdminVie
+ def validate_
+ """Verify whether a product can be safely deactivated."""
+ if data['active'] == False and self.context.active == True:
+ if len(self.
+ self.setFieldEr
+ structured(
+ 'This project cannot be deactivated since it is '
+ 'linked to one or more '
+ '<a href="%s">source packages</a>.',
+ canonical_
+
+
+class ProductAdminVie
+ """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...
Edwin Grubbs (edwin-grubbs) wrote : | # |
> 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/
> > --- lib/lp/
> > +++ lib/lp/
> > @@ -1444,6 +1444,15 @@
> > """See `LaunchpadFormV
> > self.validate_
> >
> > + if data['active'] == False and self.context.active == True:
> > + if len(self.
> > + self.setFieldEr
> > + structured(
> > + 'This project cannot be deactivated since it is '
> > + 'linked to '
> > + '<a href="%
> > + canonical_
>
> You could change this to instead pass view_name=
> 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 `LaunchpadFormV
> > @@ -1491,6 +1500,15 @@
> > # supervisor.
> > self.validate_
> >
> > + if data['active'] == False and self.context.active == True:
> > + if len(self.
> > + self.setFieldEr
> > + structured(
> > + 'This project cannot be deactivated since it is '
> > + 'linked to '
> > + '<a href="%
> > + canonical_
>
> Same here, re. view_name.
>
> ProductReviewLi
> in ProductAdminVie
> that both ProductAdminView and ProductReviewLi
Done.
> > +
> >
> > class ProductAddSerie
> > """A form to add new product series"""
> >
> > === modified file 'lib/lp/
> > --- lib/lp/
Gavin Panella (allenap) wrote : | # |
Thanks Edwin, looks good :)
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
1 | === modified file 'lib/lp/answers/doc/person.txt' |
2 | --- lib/lp/answers/doc/person.txt 2010-02-05 21:25:23 +0000 |
3 | +++ lib/lp/answers/doc/person.txt 2010-04-22 14:31:33 +0000 |
4 | @@ -351,6 +351,10 @@ |
5 | supported projects. |
6 | |
7 | >>> login('foo.bar@canonical.com') |
8 | + |
9 | + # Unlink the source packages so the project can be deactivated. |
10 | + >>> from lp.testing import unlink_source_packages |
11 | + >>> unlink_source_packages(firefox) |
12 | >>> firefox.active = False |
13 | >>> sorted(target.name |
14 | ... for target in no_priv.getDirectAnswerQuestionTargets()) |
15 | |
16 | === modified file 'lib/lp/blueprints/doc/specification.txt' |
17 | --- lib/lp/blueprints/doc/specification.txt 2009-08-13 19:03:36 +0000 |
18 | +++ lib/lp/blueprints/doc/specification.txt 2010-04-22 14:31:33 +0000 |
19 | @@ -205,14 +205,18 @@ |
20 | |
21 | Specs from inactive products are filtered out. |
22 | |
23 | - >>> from canonical.database.sqlbase import flush_database_updates |
24 | - >>> login('mark@example.com') |
25 | - >>> upstream_firefox.active = False |
26 | - >>> flush_database_updates() |
27 | - >>> for spec in specset.specifications(filter=['install']): |
28 | - ... print spec.name, spec.target.name |
29 | - cluster-installation kubuntu |
30 | - media-integrity-check ubuntu |
31 | + >>> from canonical.database.sqlbase import flush_database_updates |
32 | + >>> login('mark@example.com') |
33 | + |
34 | + # Unlink the source packages so the project can be deactivated. |
35 | + >>> from lp.testing import unlink_source_packages |
36 | + >>> unlink_source_packages(upstream_firefox) |
37 | + >>> upstream_firefox.active = False |
38 | + >>> flush_database_updates() |
39 | + >>> for spec in specset.specifications(filter=['install']): |
40 | + ... print spec.name, spec.target.name |
41 | + cluster-installation kubuntu |
42 | + media-integrity-check ubuntu |
43 | |
44 | |
45 | Reset firefox so we don't mess up later tests. |
46 | |
47 | === modified file 'lib/lp/blueprints/doc/sprint.txt' |
48 | --- lib/lp/blueprints/doc/sprint.txt 2010-02-17 11:13:06 +0000 |
49 | +++ lib/lp/blueprints/doc/sprint.txt 2010-04-22 14:31:33 +0000 |
50 | @@ -151,14 +151,18 @@ |
51 | |
52 | Inactive products are excluded from the listings. |
53 | |
54 | - >>> from canonical.launchpad.interfaces import IProductSet |
55 | - >>> from canonical.launchpad.ftests import login |
56 | - >>> firefox = getUtility(IProductSet).getByName('firefox') |
57 | - >>> login("foo.bar@canonical.com") |
58 | - >>> firefox.active = False |
59 | - >>> flush_database_updates() |
60 | - >>> ubz.specifications().count() |
61 | - 0 |
62 | + >>> from canonical.launchpad.interfaces import IProductSet |
63 | + >>> from canonical.launchpad.ftests import login |
64 | + >>> firefox = getUtility(IProductSet).getByName('firefox') |
65 | + >>> login("foo.bar@canonical.com") |
66 | + |
67 | + # Unlink the source packages so the project can be deactivated. |
68 | + >>> from lp.testing import unlink_source_packages |
69 | + >>> unlink_source_packages(firefox) |
70 | + >>> firefox.active = False |
71 | + >>> flush_database_updates() |
72 | + >>> ubz.specifications().count() |
73 | + 0 |
74 | |
75 | Reset firefox so we don't mess up later tests. |
76 | |
77 | |
78 | === modified file 'lib/lp/bugs/browser/bugalsoaffects.py' |
79 | --- lib/lp/bugs/browser/bugalsoaffects.py 2010-01-15 03:32:46 +0000 |
80 | +++ lib/lp/bugs/browser/bugalsoaffects.py 2010-04-22 14:31:33 +0000 |
81 | @@ -103,26 +103,6 @@ |
82 | bugtask = self.context |
83 | upstream = bugtask.target.upstream_product |
84 | if upstream is not None: |
85 | - if not upstream.active: |
86 | - # XXX: Guilherme Salgado 2007-09-18 bug=140526: This is only |
87 | - # possible because of bug 140526, which allows packages to |
88 | - # be linked to inactive products. |
89 | - series = bugtask.distribution.currentseries |
90 | - assert series is not None, ( |
91 | - "This package is linked to a product series so this " |
92 | - "package's distribution must have at least one distro " |
93 | - "series.") |
94 | - sourcepackage = series.getSourcePackage( |
95 | - bugtask.sourcepackagename) |
96 | - self.request.response.addWarningNotification( |
97 | - structured( |
98 | - _(""" |
99 | - This package is linked to an inactive upstream. You |
100 | - can <a href="%(package_url)s/+edit-packaging">fix it</a> |
101 | - to avoid this step in the future."""), |
102 | - package_url=canonical_url(sourcepackage))) |
103 | - return |
104 | - |
105 | try: |
106 | valid_upstreamtask(bugtask.bug, upstream) |
107 | except WidgetsError: |
108 | |
109 | === modified file 'lib/lp/bugs/browser/tests/bugtask-adding-views.txt' |
110 | --- lib/lp/bugs/browser/tests/bugtask-adding-views.txt 2009-07-06 16:23:49 +0000 |
111 | +++ lib/lp/bugs/browser/tests/bugtask-adding-views.txt 2010-04-22 14:31:33 +0000 |
112 | @@ -162,42 +162,6 @@ |
113 | >>> len(add_task_view.request.response.notifications) |
114 | 0 |
115 | |
116 | -XXX: Because of bug 140526, it's possible to have a package linked to a series |
117 | -of an inactive upstream. In a case like that we must ask the user to specify |
118 | -the upstream, but we also display a warning telling the user there's a |
119 | -package linked to an inactive upstream. -- Guilherme Salgado, 2007-09-18 |
120 | - |
121 | - # Create the view manually just so we can get the correct upstream |
122 | - # easily. |
123 | - >>> from lp.bugs.browser.bugalsoaffects import ( |
124 | - ... ChooseProductStep) |
125 | - >>> view = ChooseProductStep( |
126 | - ... ubuntu_firefox_task, LaunchpadTestRequest(method='GET', form={})) |
127 | - >>> upstream = view.context.target.upstream_product |
128 | - >>> upstream.active |
129 | - True |
130 | - |
131 | - # Mark the upstream as inactive. |
132 | - >>> from zope.security.proxy import removeSecurityProxy |
133 | - >>> removeSecurityProxy(upstream).active = False |
134 | - >>> removeSecurityProxy(upstream).syncUpdate() |
135 | - |
136 | - >>> add_task_view = get_and_setup_view( |
137 | - ... ubuntu_firefox_task, '+choose-affected-product', form={}, |
138 | - ... method='GET') |
139 | - |
140 | - >>> add_task_view.step_name |
141 | - 'choose_product' |
142 | - >>> print add_task_view.widgets['product']._getFormInput() |
143 | - None |
144 | - >>> for notification in add_task_view.request.response.notifications: |
145 | - ... print notification.message.strip() |
146 | - This package is linked to an inactive upstream... |
147 | - |
148 | - # Mark the upstream back as active. |
149 | - >>> removeSecurityProxy(upstream).active = True |
150 | - >>> removeSecurityProxy(upstream).syncUpdate() |
151 | - |
152 | Let's take a look at the second step now, where we may enter the URL of |
153 | the remote bug and confirm the bugtask creation. |
154 | In order to show that all the events get fired off, let's create an |
155 | |
156 | === modified file 'lib/lp/registry/browser/product.py' |
157 | --- lib/lp/registry/browser/product.py 2010-04-14 17:16:56 +0000 |
158 | +++ lib/lp/registry/browser/product.py 2010-04-22 14:31:33 +0000 |
159 | @@ -1376,7 +1376,7 @@ |
160 | return self.next_url |
161 | |
162 | |
163 | -class EditPrivateBugsMixin: |
164 | +class ProductValidationMixin: |
165 | |
166 | def validate_private_bugs(self, data): |
167 | """Perform validation for the private bugs setting.""" |
168 | @@ -1387,8 +1387,20 @@ |
169 | 'for this project first.', |
170 | canonical_url(self.context, rootsite="bugs"))) |
171 | |
172 | - |
173 | -class ProductAdminView(ProductEditView, EditPrivateBugsMixin): |
174 | + def validate_deactivation(self, data): |
175 | + """Verify whether a product can be safely deactivated.""" |
176 | + if data['active'] == False and self.context.active == True: |
177 | + if len(self.context.sourcepackages) > 0: |
178 | + self.setFieldError('active', |
179 | + structured( |
180 | + 'This project cannot be deactivated since it is ' |
181 | + 'linked to one or more ' |
182 | + '<a href="%s">source packages</a>.', |
183 | + canonical_url(self.context, view_name='+packages'))) |
184 | + |
185 | + |
186 | +class ProductAdminView(ProductEditView, ProductValidationMixin): |
187 | + """View for $project/+admin""" |
188 | label = "Administer project details" |
189 | field_names = ["name", "owner", "active", "autoupdate", "private_bugs"] |
190 | |
191 | @@ -1443,6 +1455,7 @@ |
192 | def validate(self, data): |
193 | """See `LaunchpadFormView`.""" |
194 | self.validate_private_bugs(data) |
195 | + self.validate_deactivation(data) |
196 | |
197 | @property |
198 | def cancel_url(self): |
199 | @@ -1451,7 +1464,7 @@ |
200 | |
201 | |
202 | class ProductReviewLicenseView(ReturnToReferrerMixin, |
203 | - ProductEditView, EditPrivateBugsMixin): |
204 | + ProductEditView, ProductValidationMixin): |
205 | """A view to review a project and change project privileges.""" |
206 | label = "Review project" |
207 | field_names = [ |
208 | @@ -1490,6 +1503,7 @@ |
209 | # Private bugs can only be enabled if the product has a bug |
210 | # supervisor. |
211 | self.validate_private_bugs(data) |
212 | + self.validate_deactivation(data) |
213 | |
214 | |
215 | class ProductAddSeriesView(LaunchpadFormView): |
216 | |
217 | === modified file 'lib/lp/registry/browser/tests/product-views.txt' |
218 | --- lib/lp/registry/browser/tests/product-views.txt 2010-04-14 20:54:40 +0000 |
219 | +++ lib/lp/registry/browser/tests/product-views.txt 2010-04-22 14:31:33 +0000 |
220 | @@ -155,9 +155,27 @@ |
221 | ['license_reviewed', 'license_approved', 'active', 'private_bugs', |
222 | 'reviewer_whiteboard'] |
223 | |
224 | +The reviewer cannot deactivate a project if it is linked |
225 | +to a source package. |
226 | + |
227 | + >>> firefox.active |
228 | + True |
229 | + |
230 | + >>> form = { |
231 | + ... 'field.active.used': '', # unchecked |
232 | + ... 'field.reviewer_whiteboard': 'Looks bogus', |
233 | + ... 'field.actions.change': 'Change', |
234 | + ... } |
235 | + >>> view = create_initialized_view( |
236 | + ... firefox, name='+review-license', form=form) |
237 | + >>> view.errors |
238 | + [...This project cannot be deactivated since it is linked to |
239 | + ...source packages</a>.'] |
240 | + |
241 | The reviewer can deactivate a project if he concludes it is bogus. |
242 | |
243 | - >>> firefox.active |
244 | + >>> product = factory.makeProduct(name='tomato', title='Tomato') |
245 | + >>> product.active |
246 | True |
247 | |
248 | >>> form = { |
249 | @@ -166,12 +184,12 @@ |
250 | ... 'field.actions.change': 'Change', |
251 | ... } |
252 | >>> view = create_initialized_view( |
253 | - ... firefox, name='+review-license', form=form) |
254 | + ... product, name='+review-license', form=form) |
255 | >>> view.errors |
256 | [] |
257 | - >>> firefox.active |
258 | + >>> product.active |
259 | False |
260 | - >>> print firefox.reviewer_whiteboard |
261 | + >>> print product.reviewer_whiteboard |
262 | Looks bogus |
263 | |
264 | The reviewer can enable privileged features like private bugs. He can |
265 | @@ -439,8 +457,8 @@ |
266 | officially supports. |
267 | |
268 | >>> from canonical.launchpad.testing.pages import find_tag_by_id |
269 | + >>> product = factory.makeProduct(name='tomato', title='Tomato') |
270 | |
271 | - >>> product = factory.makeProduct(name='tomato', title='Tomato') |
272 | >>> owner = product.owner |
273 | >>> login_person(owner) |
274 | >>> question = factory.makeQuestion(target=product) |
275 | |
276 | === modified file 'lib/lp/registry/doc/milestone.txt' |
277 | --- lib/lp/registry/doc/milestone.txt 2010-04-16 15:06:55 +0000 |
278 | +++ lib/lp/registry/doc/milestone.txt 2010-04-22 14:31:33 +0000 |
279 | @@ -221,6 +221,10 @@ |
280 | |
281 | >>> print [milestone.name for milestone in gnome.milestones] |
282 | [u'1.1.'] |
283 | + |
284 | + # Unlink the source packages so the project can be deactivated. |
285 | + >>> from lp.testing import unlink_source_packages |
286 | + >>> unlink_source_packages(netapplet) |
287 | >>> netapplet.active = False |
288 | >>> syncUpdate(netapplet) |
289 | >>> print [milestone.name for milestone in gnome.milestones] |
290 | |
291 | === modified file 'lib/lp/registry/doc/person.txt' |
292 | --- lib/lp/registry/doc/person.txt 2010-04-18 17:20:47 +0000 |
293 | +++ lib/lp/registry/doc/person.txt 2010-04-22 14:31:33 +0000 |
294 | @@ -1344,6 +1344,10 @@ |
295 | >>> from canonical.launchpad.ftests import login |
296 | >>> firefox = getUtility(IProductSet).getByName('firefox') |
297 | >>> login("mark@example.com") |
298 | + |
299 | + # Unlink the source packages so the project can be deactivated. |
300 | + >>> from lp.testing import unlink_source_packages |
301 | + >>> unlink_source_packages(firefox) |
302 | >>> firefox.active = False |
303 | >>> flush_database_updates() |
304 | >>> cprov.specifications(filter=['svg']).count() |
305 | |
306 | === modified file 'lib/lp/registry/doc/pillar.txt' |
307 | --- lib/lp/registry/doc/pillar.txt 2010-04-19 15:13:39 +0000 |
308 | +++ lib/lp/registry/doc/pillar.txt 2010-04-22 14:31:33 +0000 |
309 | @@ -113,6 +113,9 @@ |
310 | |
311 | But only if the pillar which they point to is active. |
312 | |
313 | + # Unlink the source packages so the project can be deactivated. |
314 | + >>> from lp.testing import unlink_source_packages |
315 | + >>> unlink_source_packages(firefox) |
316 | >>> firefox.active = False |
317 | >>> 'iceweasel' in pillar_set |
318 | False |
319 | |
320 | === modified file 'lib/lp/registry/doc/product.txt' |
321 | --- lib/lp/registry/doc/product.txt 2010-04-16 15:06:55 +0000 |
322 | +++ lib/lp/registry/doc/product.txt 2010-04-22 14:31:33 +0000 |
323 | @@ -54,7 +54,7 @@ |
324 | >>> evo = p |
325 | |
326 | To fetch a product we use IProductSet.getByName() or IProductSet.__getitem__. |
327 | -The former will, by default, reeturn active and inactive products, while the |
328 | +The former will, by default, return active and inactive products, while the |
329 | later returns only active ones. Both can be used to look up products by their |
330 | aliases, though. |
331 | |
332 | @@ -91,6 +91,9 @@ |
333 | |
334 | Now, to test the active flag. If we disabled a product: |
335 | |
336 | + # Unlink the source packages so the project can be deactivated. |
337 | + >>> from lp.testing import unlink_source_packages |
338 | + >>> unlink_source_packages(a52dec) |
339 | >>> a52dec.active = False |
340 | |
341 | It should no longer be retrievable via ProductSet's __getitem__: |
342 | @@ -193,6 +196,9 @@ |
343 | |
344 | Only active products are listed as translatables. |
345 | |
346 | + # Unlink the source packages so the project can be deactivated. |
347 | + >>> from lp.testing import unlink_source_packages |
348 | + >>> unlink_source_packages(evo) |
349 | >>> evo.active = False |
350 | >>> for product in productset.getTranslatables(): |
351 | ... print product.name |
352 | @@ -517,12 +523,12 @@ |
353 | |
354 | Only active products are returned. |
355 | |
356 | - >>> firefox.active = False |
357 | + >>> evo.active = False |
358 | >>> from canonical.launchpad.ftests import syncUpdate |
359 | >>> syncUpdate(firefox) |
360 | >>> for product in productset.getProductsWithBranches(): |
361 | ... print product.name |
362 | - evolution |
363 | + firefox |
364 | gnome-terminal |
365 | iso-codes |
366 | landscape |
367 | |
368 | === modified file 'lib/lp/registry/doc/project.txt' |
369 | --- lib/lp/registry/doc/project.txt 2010-04-19 09:39:29 +0000 |
370 | +++ lib/lp/registry/doc/project.txt 2010-04-22 14:31:33 +0000 |
371 | @@ -31,7 +31,7 @@ |
372 | == Looking up existing projects == |
373 | |
374 | To fetch a project we use IProjectGroupSet.getByName() or |
375 | -IProjectGroupSet.__getitem__. The former will, by default, reeturn active and |
376 | +IProjectGroupSet.__getitem__. The former will, by default, return active and |
377 | inactive projects, while the latter returns only active ones. Both can be |
378 | used to look up projects by their aliases, though. |
379 | |
380 | @@ -108,6 +108,10 @@ |
381 | [u'applets', u'evolution', u'gnome-terminal', u'gnomebaker', u'netapplet'] |
382 | |
383 | >>> netapplet = gnome.getProduct('netapplet') |
384 | + |
385 | + # Unlink the source packages so the project can be deactivated. |
386 | + >>> from lp.testing import unlink_source_packages |
387 | + >>> unlink_source_packages(netapplet) |
388 | >>> netapplet.active = False |
389 | >>> flush_database_updates() |
390 | >>> [product.name for product in gnome.products] |
391 | @@ -161,6 +165,10 @@ |
392 | |
393 | >>> from canonical.launchpad.interfaces import IProductSet |
394 | >>> firefox = getUtility(IProductSet).getByName('firefox') |
395 | + |
396 | + # Unlink the source packages so the project can be deactivated. |
397 | + >>> from lp.testing import unlink_source_packages |
398 | + >>> unlink_source_packages(firefox) |
399 | >>> firefox.active = False |
400 | >>> flush_database_updates() |
401 | >>> filter = [SpecificationFilter.INCOMPLETE] |
402 | |
403 | === modified file 'lib/lp/registry/model/product.py' |
404 | --- lib/lp/registry/model/product.py 2010-04-19 09:39:29 +0000 |
405 | +++ lib/lp/registry/model/product.py 2010-04-22 14:31:33 +0000 |
406 | @@ -273,7 +273,6 @@ |
407 | |
408 | enable_bug_expiration = BoolCol(dbName='enable_bug_expiration', |
409 | notNull=True, default=False) |
410 | - active = BoolCol(dbName='active', notNull=True, default=True) |
411 | license_reviewed = BoolCol(dbName='reviewed', notNull=True, default=False) |
412 | reviewer_whiteboard = StringCol(notNull=False, default=None) |
413 | private_bugs = BoolCol( |
414 | @@ -290,6 +289,18 @@ |
415 | bug_reporting_guidelines = StringCol(default=None) |
416 | _cached_licenses = None |
417 | |
418 | + def _validate_active(self, attr, value): |
419 | + # Validate deactivation. |
420 | + if self.active == True and value == False: |
421 | + if len(self.sourcepackages) > 0: |
422 | + raise AssertionError( |
423 | + 'This project cannot be deactivated since it is ' |
424 | + 'linked to source packages.') |
425 | + return value |
426 | + |
427 | + active = BoolCol(dbName='active', notNull=True, default=True, |
428 | + storm_validator=_validate_active) |
429 | + |
430 | def _validate_license_info(self, attr, value): |
431 | if not self._SO_creating and value != self.license_info: |
432 | # Clear the license_reviewed and license_approved flags |
433 | |
434 | === modified file 'lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt' |
435 | --- lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt 2008-01-16 19:27:48 +0000 |
436 | +++ lib/lp/registry/stories/pillar/xx-pillar-deactivation.txt 2010-04-22 14:31:33 +0000 |
437 | @@ -7,12 +7,20 @@ |
438 | cover that part of the change. See bug #156263 for more details. |
439 | -- kiko, 2007-10-23 |
440 | |
441 | + >>> from lp.testing import unlink_source_packages |
442 | + >>> from lp.registry.interfaces.pillar import IPillarNameSet |
443 | + >>> from lp.registry.interfaces.product import IProduct |
444 | + >>> from zope.component import getUtility |
445 | + >>> pillar_set = getUtility(IPillarNameSet) |
446 | >>> def toggleProject(name): |
447 | - ... admin_browser.open('http://launchpad.dev/%s' % name) |
448 | - ... admin_browser.getLink(url='+review').click() |
449 | - ... state = admin_browser.getControl('Active').selected |
450 | - ... admin_browser.getControl('Active').selected = (not state) |
451 | - ... admin_browser.getControl('Change').click() |
452 | + ... login('admin@canonical.com') |
453 | + ... pillar = pillar_set.getByName(name) |
454 | + ... if IProduct.providedBy(pillar) and pillar.active: |
455 | + ... # A product cannot be deactivated if |
456 | + ... # it is linked to source packages. |
457 | + ... unlink_source_packages(pillar) |
458 | + ... pillar.active = not pillar.active |
459 | + ... logout() |
460 | |
461 | We start off with active and visible projects: |
462 | |
463 | @@ -72,13 +80,12 @@ |
464 | >>> anon_browser.getLink(url='/firefox').click() |
465 | >>> anon_browser.title |
466 | 'Mozilla Firefox in Launchpad' |
467 | - >>> print find_tag_by_id(admin_browser.contents, 'project-inactive') |
468 | + >>> print find_tag_by_id(anon_browser.contents, 'project-inactive') |
469 | None |
470 | |
471 | >>> anon_browser.open('http://launchpad.dev/projects/+index?text=mozilla') |
472 | >>> anon_browser.getLink(url='/mozilla').click() |
473 | >>> anon_browser.title |
474 | 'The Mozilla Project in Launchpad' |
475 | - >>> print find_tag_by_id(admin_browser.contents, 'project-inactive') |
476 | + >>> print find_tag_by_id(anon_browser.contents, 'project-inactive') |
477 | None |
478 | - |
479 | |
480 | === modified file 'lib/lp/registry/stories/product/xx-product-edit.txt' |
481 | --- lib/lp/registry/stories/product/xx-product-edit.txt 2010-03-16 22:07:45 +0000 |
482 | +++ lib/lp/registry/stories/product/xx-product-edit.txt 2010-04-22 14:31:33 +0000 |
483 | @@ -332,27 +332,27 @@ |
484 | |
485 | The Admins and Registry Experts can deactivate a project. |
486 | |
487 | - >>> registry_browser.open('http://launchpad.dev/firefox/+review-license') |
488 | + >>> registry_browser.open('http://launchpad.dev/bzr/+review-license') |
489 | >>> registry_browser.getControl(name='field.active').value = False |
490 | >>> registry_browser.getControl(name='field.actions.change').click() |
491 | >>> print registry_browser.url |
492 | - http://launchpad.dev/firefox |
493 | + http://launchpad.dev/bzr |
494 | |
495 | The product overview page should show a notice that a product is |
496 | inactive with a link to a form to re-activate it. Admins and Commercial |
497 | Admins can still see the product, but regular users can't. |
498 | |
499 | - >>> registry_browser.open('http://launchpad.dev/firefox') |
500 | + >>> registry_browser.open('http://launchpad.dev/bzr') |
501 | >>> contents = find_main_content(registry_browser.contents) |
502 | >>> print extract_text(contents.find(id='project-inactive')) |
503 | This project is currently inactive ... |
504 | |
505 | - >>> admin_browser.open('http://launchpad.dev/firefox') |
506 | + >>> admin_browser.open('http://launchpad.dev/bzr') |
507 | >>> contents = find_main_content(admin_browser.contents) |
508 | >>> print extract_text(contents.find(id='project-inactive')) |
509 | This project is currently inactive ... |
510 | |
511 | - >>> user_browser.open('http://launchpad.dev/firefox') |
512 | + >>> user_browser.open('http://launchpad.dev/bzr') |
513 | Traceback (most recent call last): |
514 | ... |
515 | NotFound... |
516 | @@ -361,12 +361,12 @@ |
517 | |
518 | >>> registry_browser.getLink('Review project').click() |
519 | >>> print registry_browser.url |
520 | - http://launchpad.dev/firefox/+review-license |
521 | + http://launchpad.dev/bzr/+review-license |
522 | |
523 | >>> registry_browser.getControl(name='field.active').value = True |
524 | >>> registry_browser.getControl(name='field.actions.change').click() |
525 | >>> print registry_browser.url |
526 | - http://launchpad.dev/firefox |
527 | + http://launchpad.dev/bzr |
528 | |
529 | >>> contents = find_main_content(registry_browser.contents) |
530 | >>> print contents.find(id='project-inactive') |
531 | |
532 | === modified file 'lib/lp/registry/stories/project/xx-project-index.txt' |
533 | --- lib/lp/registry/stories/project/xx-project-index.txt 2010-04-19 08:11:52 +0000 |
534 | +++ lib/lp/registry/stories/project/xx-project-index.txt 2010-04-22 14:31:33 +0000 |
535 | @@ -150,6 +150,12 @@ |
536 | # (i.e. login()) and bypass the security proxy. |
537 | >>> from lp.registry.model.product import Product |
538 | >>> firefox = Product.byName('firefox') |
539 | + |
540 | + # Unlink the source packages so the project can be deactivated. |
541 | + >>> from lp.testing import unlink_source_packages |
542 | + >>> login('admin@canonical.com') |
543 | + >>> unlink_source_packages(firefox) |
544 | + >>> logout() |
545 | >>> firefox.active = False |
546 | >>> firefox.syncUpdate() |
547 | |
548 | |
549 | === modified file 'lib/lp/registry/tests/test_product.py' |
550 | --- lib/lp/registry/tests/test_product.py 2009-10-23 13:48:28 +0000 |
551 | +++ lib/lp/registry/tests/test_product.py 2010-04-22 14:31:33 +0000 |
552 | @@ -26,6 +26,34 @@ |
553 | CommercialSubscription) |
554 | from lp.testing import TestCaseWithFactory |
555 | |
556 | +class TestProduct(TestCaseWithFactory): |
557 | + """Tests product object.""" |
558 | + |
559 | + layer = LaunchpadFunctionalLayer |
560 | + |
561 | + def test_deactivation_failure(self): |
562 | + # Ensure that a product cannot be deactivated if |
563 | + # it is linked to source packages. |
564 | + login('admin@canonical.com') |
565 | + product = self.factory.makeProduct() |
566 | + source_package = self.factory.makeSourcePackage() |
567 | + self.assertEqual(True, product.active) |
568 | + source_package.setPackaging( |
569 | + product.development_focus, self.factory.makePerson()) |
570 | + self.assertRaises( |
571 | + AssertionError, |
572 | + setattr, product, 'active', False) |
573 | + |
574 | + def test_deactivation_success(self): |
575 | + # Ensure that a product can be deactivated if |
576 | + # it is not linked to source packages. |
577 | + login('admin@canonical.com') |
578 | + product = self.factory.makeProduct() |
579 | + self.assertEqual(True, product.active) |
580 | + product.active = False |
581 | + self.assertEqual(False, product.active) |
582 | + |
583 | + |
584 | class TestProductFiles(unittest.TestCase): |
585 | """Tests for downloadable product files.""" |
586 | |
587 | |
588 | === modified file 'lib/lp/testing/__init__.py' |
589 | --- lib/lp/testing/__init__.py 2010-04-18 22:31:40 +0000 |
590 | +++ lib/lp/testing/__init__.py 2010-04-22 14:31:33 +0000 |
591 | @@ -31,6 +31,7 @@ |
592 | 'TestCaseWithFactory', |
593 | 'test_tales', |
594 | 'time_counter', |
595 | + 'unlink_source_packages', |
596 | # XXX: This really shouldn't be exported from here. People should import |
597 | # it from Zope. |
598 | 'verifyObject', |
599 | @@ -83,6 +84,7 @@ |
600 | from canonical.launchpad.webapp.interfaces import ILaunchBag |
601 | from canonical.launchpad.windmill.testing import constants |
602 | from lp.codehosting.vfs import branch_id_to_path, get_multi_server |
603 | +from lp.registry.interfaces.packaging import IPackagingUtil |
604 | # Import the login and logout functions here as it is a much better |
605 | # place to import them from in tests. |
606 | from lp.testing._login import ( |
607 | @@ -930,3 +932,15 @@ |
608 | name, mock_args, real_args)) |
609 | else: |
610 | break |
611 | + |
612 | +def unlink_source_packages(product): |
613 | + """Remove all links between the product and source packages. |
614 | + |
615 | + A product cannot be deactivated if it is linked to source packages. |
616 | + """ |
617 | + packaging_util = getUtility(IPackagingUtil) |
618 | + for source_package in product.sourcepackages: |
619 | + packaging_util.deletePackaging( |
620 | + source_package.productseries, |
621 | + source_package.sourcepackagename, |
622 | + source_package.distroseries) |
623 | |
624 | === modified file 'lib/lp/translations/doc/translationimportqueue.txt' |
625 | --- lib/lp/translations/doc/translationimportqueue.txt 2010-02-05 15:28:42 +0000 |
626 | +++ lib/lp/translations/doc/translationimportqueue.txt 2010-04-22 14:31:33 +0000 |
627 | @@ -1043,6 +1043,9 @@ |
628 | An administrator deactivates Firefox, thinking that the project is being |
629 | run in violation of Launchpad policies. |
630 | |
631 | + # Unlink the source packages so the project can be deactivated. |
632 | + >>> from lp.testing import unlink_source_packages |
633 | + >>> unlink_source_packages(firefox) |
634 | >>> firefox.active = False |
635 | >>> syncUpdate(firefox) |
636 | |
637 | |
638 | === modified file 'lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt' |
639 | --- lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt 2009-08-25 16:42:56 +0000 |
640 | +++ lib/lp/translations/stories/translationgroups/30-show-group-translation-targets.txt 2010-04-22 14:31:33 +0000 |
641 | @@ -26,6 +26,13 @@ |
642 | >>> admin_browser.url |
643 | 'http://launchpad.dev/projectgroups' |
644 | |
645 | + # Unlink the source packages so the project can be deactivated. |
646 | + >>> from lp.testing import unlink_source_packages |
647 | + >>> from lp.registry.interfaces.product import IProductSet |
648 | + >>> from zope.component import getUtility |
649 | + >>> login('admin@canonical.com') |
650 | + >>> unlink_source_packages(getUtility(IProductSet).getByName('netapplet')) |
651 | + >>> logout() |
652 | >>> admin_browser.open("http://launchpad.dev/netapplet/+admin") |
653 | >>> admin_browser.getControl("Active").click() |
654 | >>> admin_browser.getControl("Change").click() |
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' answers/ doc/person. txt 2010-02-05 21:25:23 +0000 answers/ doc/person. txt 2010-04-21 11:03:32 +0000 source_ packages source_ packages( firefox) getDirectAnswer QuestionTargets ()) blueprints/ doc/specificati on.txt' blueprints/ doc/specificati on.txt 2009-08-13 19:03:36 +0000 blueprints/ doc/specificati on.txt 2010-04-21 11:03:32 +0000 database. sqlbase import flush_database_ updates firefox. active = False updates( ) specifications( filter= ['install' ]): installation kubuntu -check ubuntu database. sqlbase import flush_database_ updates source_ packages source_ packages( upstream_ firefox) firefox. active = False updates( ) specifications( filter= ['install' ]): installation kubuntu -check ubuntu blueprints/ doc/sprint. txt' blueprints/ doc/sprint. txt 2010-02-17 11:13:06 +0000 blueprints/ doc/sprint. txt 2010-04-21 11:03:32 +0000 launchpad. interfaces import IProductSet launchpad. ftests import login IProductSet) .getByName( 'firefox' ) updates( ) ons().count( ) launchpad. interfaces import IProductSet launchpad. ftests import login IProductSet) .getByName( 'firefox' ) source_ packages source_ packages( firefox) updates( ) ons().count( )
> --- lib/lp/
> +++ lib/lp/
> @@ -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_
> + >>> unlink_
> >>> firefox.active = False
> >>> sorted(target.name
> ... for target in no_priv.
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -205,14 +205,18 @@
>
> Specs from inactive products are filtered out.
>
> - >>> from canonical.
> - >>> login('<email address hidden>')
> - >>> upstream_
> - >>> flush_database_
> - >>> for spec in specset.
> - ... print spec.name, spec.target.name
> - cluster-
> - media-integrity
> + >>> from canonical.
> + >>> login('<email address hidden>')
> +
> + # A product cannot be deactivated if it is linked to source packages.
> + >>> from lp.testing import unlink_
> + >>> unlink_
> + >>> upstream_
> + >>> flush_database_
> + >>> for spec in specset.
> + ... print spec.name, spec.target.name
> + cluster-
> + media-integrity
>
>
> Reset firefox so we don't mess up later tests.
>
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -151,14 +151,18 @@
>
> Inactive products are excluded from the listings.
>
> - >>> from canonical.
> - >>> from canonical.
> - >>> firefox = getUtility(
> - >>> login("<email address hidden>")
> - >>> firefox.active = False
> - >>> flush_database_
> - >>> ubz.specificati
> - 0
> + >>> from canonical.
> + >>> from canonical.
> + >>> firefox = getUtility(
> + >>> login("<email address hidden>")
> +
> + # A product cannot be deactivated if it is linked to source packages.
> + >>> from lp.testing import unlink_
> + >>> unlink_
> + >>> firefox.active = False
> + >>> flush_database_
> + >>> ubz.specificati
> + 0
>
> ...