Merge lp:~sinzui/launchpad/package-link-validation-3 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Merged at revision: not available
Proposed branch: lp:~sinzui/launchpad/package-link-validation-3
Merge into: lp:launchpad
Diff against target: 1099 lines
14 files modified
lib/canonical/launchpad/icing/style-3-0.css (+3/-0)
lib/lp/registry/browser/configure.zcml (+0/-3)
lib/lp/registry/browser/distributionsourcepackage.py (+22/-84)
lib/lp/registry/browser/packaging.py (+83/-0)
lib/lp/registry/browser/tests/packaging-views.txt (+237/-0)
lib/lp/registry/browser/tests/productseries-views.txt (+0/-229)
lib/lp/registry/browser/tests/test_packaging.py (+1/-1)
lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt (+4/-3)
lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt (+7/-5)
lib/lp/registry/templates/distributionsourcepackage-index.pt (+6/-5)
lib/lp/registry/templates/productseries-index.pt (+2/-37)
lib/lp/registry/templates/productseries-portlet-packages.pt (+42/-21)
lib/lp/registry/templates/sourcepackage-index.pt (+1/-3)
lib/lp/registry/templates/sourcepackage-portlet-upstream.pt (+0/-17)
To merge this branch: bzr merge lp:~sinzui/launchpad/package-link-validation-3
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+13844@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (5.5 KiB)

This is my fourth branch to ensure valid upstream package links. There
are many oopses relating to the creation and efforts to fix invalid packages.
The root cause is a bad DB constraint and two views that do not do the
required sanity checks: +addpackage and +ubuntupkg. The views are fixed,
but we have not provided users with a way to delete packaging links
to unbuilt packages. User need to do this to fix their mistakes. We want
to do this so that we can delete the duplicate packages.

This branch refactors portlets and forms so that my next branch can add
delete links to the product +packaing report.

    lp:~sinzui/launchpad/package-link-validation-3
    Diff size: 565
    Launchpad bug: https://bugs.launchpad.net/bugs/276409
    Test command: ./bin/test -vv -t 'lp.(reg|soyuz).*(productseries|packaging)'
    Pre-implementation: flacoste, beuno
    Target release: 3.1.10

== Fixing upstream packaging links ==

Bug 276409 ["Delete Link" is too easy on distribution source package page]
    On the page for a distribution source package with upstream links, such as
    <https://launchpad.net/ubuntu/+source/hwdb-client>, the button for
    deleting an upstream link is quite large and prominent, much more so than
    the button for correcting the link.

    A small improvement would be to change the "Delete Link" button to an
    icon-only button, but that would still leave deleting much easier than
    updating.

    There is little value in making the delete action an editing action
    because the user does not have enough information to select another
    series and sourcepackage. The user will almost always delete. Fixing the
    icon is all that is needed.

== Rules ==

Bug 276409 ["Delete Link" is too easy on distribution source package page]
    * Add a link from the productseries packaing portlet to the product
      +packages view.
    * Extract the delete packaging link rules to a separate view so that
      they can be reused
    * Change the Delete link button to a remove icon.

== QA ==

On staging
    * Visit a bzr series
    * Verify the packaging portlet has a link to All packages
    * Visit the bzr DSP
    * Verify eack packaging link has a the remove and edit icons int this
      order: (-) (/)

== Lint ==

Linting changed files:
  lib/canonical/launchpad/icing/style-3-0.css
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/distributionsourcepackage.py
  lib/lp/registry/browser/packaging.py
  lib/lp/registry/browser/tests/test_packaging.py
  lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt
  lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt
  lib/lp/registry/templates/distributionsourcepackage-index.pt
  lib/lp/registry/templates/productseries-index.pt
  lib/lp/registry/templates/productseries-portlet-packages.pt
  lib/lp/registry/templates/sourcepackage-index.pt
  lib/lp/registry/templates/team-portlet-membership.pt

== Test ==

    * lib/lp/registry/browser/tests/test_packaging.py
      * Updated the test to use the submit's name since images do not have
        labels.
    * lib/lp/registry/stories/packaging/xx-distributionsourc...

Read more...

Revision history for this message
Curtis Hovey (sinzui) wrote :
Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/icing/style-3-0.css'
2--- lib/canonical/launchpad/icing/style-3-0.css 2009-10-22 15:22:42 +0000
3+++ lib/canonical/launchpad/icing/style-3-0.css 2009-10-24 20:01:11 +0000
4@@ -383,6 +383,9 @@
5 form table tbody th {
6 font-weight: bold;
7 }
8+input[type='image'] {
9+ vertical-align: middle;
10+ }
11 em {
12 font-style: italic;
13 }
14
15=== modified file 'lib/lp/registry/browser/configure.zcml'
16--- lib/lp/registry/browser/configure.zcml 2009-10-21 01:55:17 +0000
17+++ lib/lp/registry/browser/configure.zcml 2009-10-24 20:01:11 +0000
18@@ -2001,9 +2001,6 @@
19 facet="overview"
20 permission="zope.Public">
21 <browser:page
22- name="+portlet-upstream"
23- template="../templates/sourcepackage-portlet-upstream.pt"/>
24- <browser:page
25 name="+portlet-releases"
26 template="../templates/sourcepackage-portlet-releases.pt"/>
27 </browser:pages>
28
29=== modified file 'lib/lp/registry/browser/distributionsourcepackage.py'
30--- lib/lp/registry/browser/distributionsourcepackage.py 2009-10-23 22:09:49 +0000
31+++ lib/lp/registry/browser/distributionsourcepackage.py 2009-10-24 20:01:11 +0000
32@@ -20,20 +20,16 @@
33 import pytz
34
35 from zope.component import getUtility
36-from zope.formlib import form
37 from zope.interface import implements, Interface
38-from zope.schema import Choice
39-from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
40
41 from canonical.cachedproperty import cachedproperty
42-from canonical.launchpad import _
43 from canonical.launchpad.interfaces import IBugSet
44 from lp.answers.interfaces.questionenums import QuestionStatus
45 from lp.soyuz.interfaces.archive import IArchiveSet
46 from lp.soyuz.interfaces.distributionsourcepackagerelease import (
47 IDistributionSourcePackageRelease)
48 from lp.soyuz.interfaces.packagediff import IPackageDiffSet
49-from lp.registry.interfaces.packaging import IPackagingUtil
50+from lp.registry.browser.packaging import PackagingDeleteView
51 from lp.registry.interfaces.pocket import pocketsuffix
52 from lp.registry.interfaces.product import IDistributionSourcePackage
53 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
54@@ -42,7 +38,7 @@
55 from canonical.launchpad.browser.structuralsubscription import (
56 StructuralSubscriptionTargetTraversalMixin)
57 from canonical.launchpad.webapp import (
58- ApplicationMenu, LaunchpadEditFormView, LaunchpadFormView, LaunchpadView,
59+ ApplicationMenu, LaunchpadEditFormView, LaunchpadView,
60 Link, Navigation, StandardLaunchpadFacets, action, canonical_url,
61 redirection)
62 from canonical.launchpad.webapp.menu import (
63@@ -57,6 +53,7 @@
64
65 class DistributionSourcePackageBreadcrumb(Breadcrumb):
66 """Builds a breadcrumb for an `IDistributionSourcePackage`."""
67+
68 @property
69 def text(self):
70 return smartquote('"%s" package') % (
71@@ -71,6 +68,7 @@
72
73
74 class DistributionSourcePackageLinksMixin:
75+
76 def subscribe(self):
77 return Link('+subscribe', 'Subscribe to bug mail', icon='edit')
78
79@@ -184,6 +182,8 @@
80 """Common features to all `DistributionSourcePackage` views."""
81
82 def releases(self):
83+ """All releases for this `IDistributionSourcePackage`."""
84+
85 def not_empty(text):
86 return (
87 text is not None and isinstance(text, basestring)
88@@ -215,11 +215,10 @@
89 result_set = self.context.getPersonsByEmail(unique_emails)
90 # Ignore the persons who want their email addresses hidden.
91 self._person_data = dict(
92- [(email.email,person) for (email,person) in result_set
93+ [(email.email, person) for (email, person) in result_set
94 if not person.hide_email_addresses])
95 else:
96 self._person_data = None
97-
98 # Collate diffs for relevant SourcePackageReleases
99 pkg_diffs = getUtility(IPackageDiffSet).getDiffsToReleases(sprs)
100 spr_diffs = {}
101@@ -235,7 +234,7 @@
102
103
104 class DistributionSourcePackageView(DistributionSourcePackageBaseView,
105- LaunchpadFormView):
106+ PackagingDeleteView):
107 """View class for DistributionSourcePackage."""
108 implements(IDistributionSourcePackageActionMenu)
109
110@@ -243,17 +242,18 @@
111 def label(self):
112 return self.context.title
113
114- def setUpFields(self):
115+ @property
116+ def next_url(self):
117 """See `LaunchpadFormView`."""
118- # No schema is set in this form, because all fields are created with
119- # custom vocabularies. So we must not call the inherited setUpField
120- # method.
121- self.form_fields = self._createPackagingField()
122+ return canonical_url(self.context)
123
124 @property
125- def can_delete_packaging(self):
126- """Whether the user can delete existing packaging links."""
127- return self.user is not None
128+ def all_packaging(self):
129+ """See `PackagingDeleteView`."""
130+ for sourcepackage in self.context.get_distroseries_packages():
131+ packaging = sourcepackage.direct_packaging
132+ if packaging is not None:
133+ yield packaging
134
135 @property
136 def all_published_in_active_distroseries(self):
137@@ -267,11 +267,11 @@
138 for pub in self.context.current_publishing_records:
139 if pub.distroseries.active:
140 entry = {
141- "suite" : (pub.distroseries.name.capitalize() +
142+ "suite": (pub.distroseries.name.capitalize() +
143 pocketsuffix[pub.pocket]),
144- "description" : "(%s): %s/%s" % (
145+ "description": "(%s): %s/%s" % (
146 pub.sourcepackagerelease.version,
147- pub.component.name, pub.section.name)
148+ pub.component.name, pub.section.name),
149 }
150 results.append(entry)
151 return results
152@@ -331,12 +331,12 @@
153 versions.append(
154 "%s (%s)" % (
155 pub.distroseries.displayname,
156- pub.source_package_version
157+ pub.source_package_version,
158 )
159 )
160 archive_versions.append({
161 'archive': archive,
162- 'versions': ", ".join(versions)
163+ 'versions': ", ".join(versions),
164 })
165
166 return archive_versions
167@@ -350,68 +350,6 @@
168 self.context.name,
169 )
170
171- def _createPackagingField(self):
172- """Create a field to specify a Packaging association.
173-
174- Create a contextual vocabulary that can specify one of the Packaging
175- associated to this DistributionSourcePackage.
176- """
177- terms = []
178- for sourcepackage in self.context.get_distroseries_packages():
179- packaging = sourcepackage.direct_packaging
180- if packaging is None:
181- continue
182- terms.append(SimpleTerm(packaging, packaging.id))
183- return form.Fields(
184- Choice(__name__='packaging', vocabulary=SimpleVocabulary(terms),
185- required=True))
186-
187- def _renderHiddenPackagingField(self, packaging):
188- """Render a hidden input that fills in the packaging field."""
189- if not self.can_delete_packaging:
190- return None
191- vocabulary = self.form_fields['packaging'].field.vocabulary
192- return '<input type="hidden" name="field.packaging" value="%s" />' % (
193- vocabulary.getTerm(packaging).token)
194-
195- def renderDeletePackagingAction(self):
196- """Render a submit input for the delete_packaging_action."""
197- assert self.can_delete_packaging, 'User cannot delete Packaging.'
198- return ('<input type="submit" class="button" value="Delete Link" '
199- 'style="padding: 0pt; font-size: 80%%" '
200- 'name="%s"/>' % (self.delete_packaging_action.__name__,))
201-
202- def handleDeletePackagingError(self, action, data, errors):
203- """Handle errors on package link deletion.
204-
205- If 'packaging' is not set in the form data, we assume that means the
206- provided Packaging id was not found, which should only happen if the
207- same Packaging object was concurrently deleted. In this case, we want
208- to display a more informative error message than the default 'Invalid
209- value'.
210- """
211- if data.get('packaging') is None:
212- self.setFieldError(
213- 'packaging',
214- _("This upstream association was deleted already."))
215-
216- @action(_("Delete Link"), name='delete_packaging',
217- failure=handleDeletePackagingError)
218- def delete_packaging_action(self, action, data):
219- """Delete a Packaging association."""
220- packaging = data['packaging']
221- productseries = packaging.productseries
222- distroseries = packaging.distroseries
223- getUtility(IPackagingUtil).deletePackaging(
224- productseries, packaging.sourcepackagename, distroseries)
225- self.request.response.addNotification(
226- _("Removed upstream association between ${product} "
227- "${productseries} and ${distroseries}.", mapping=dict(
228- product=productseries.product.displayname,
229- productseries=productseries.displayname,
230- distroseries=distroseries.displayname)))
231- self.next_url = canonical_url(self.context)
232-
233 @cachedproperty
234 def active_distroseries_packages(self):
235 """Cached proxy call to context/get_distroseries_packages."""
236
237=== modified file 'lib/lp/registry/browser/packaging.py'
238--- lib/lp/registry/browser/packaging.py 2009-10-21 21:16:09 +0000
239+++ lib/lp/registry/browser/packaging.py 2009-10-24 20:01:11 +0000
240@@ -5,9 +5,13 @@
241
242 __all__ = [
243 'PackagingAddView',
244+ 'PackagingDeleteView',
245 ]
246
247 from zope.component import getUtility
248+from zope.formlib import form
249+from zope.schema import Choice
250+from zope.schema.vocabulary import SimpleTerm, SimpleVocabulary
251
252 from canonical.launchpad import _
253 from lp.registry.interfaces.packaging import (
254@@ -77,3 +81,82 @@
255 getUtility(IPackagingUtil).createPackaging(
256 productseries, data['sourcepackagename'], data['distroseries'],
257 data['packaging'], owner=self.user)
258+
259+
260+class PackagingDeleteView(LaunchpadFormView):
261+ """A base view that provides packaging link deletion."""
262+
263+ @property
264+ def all_packaging(self):
265+ """An iterator of the context's packaging links."""
266+ raise NotImplementedError
267+
268+ def setUpFields(self):
269+ """See `LaunchpadFormView`."""
270+ # No schema is set in this form, because all fields are created with
271+ # custom vocabularies. So we must not call the inherited setUpField
272+ # method.
273+ self.form_fields = self._createPackagingField()
274+
275+ @property
276+ def can_delete_packaging(self):
277+ """Whether the user can delete existing packaging links."""
278+ return self.user is not None
279+
280+ def _createPackagingField(self):
281+ """Create a field to specify a Packaging association.
282+
283+ Create a contextual vocabulary that can specify one of the Packaging
284+ associated to this DistributionSourcePackage.
285+ """
286+ terms = []
287+ for packaging in self.all_packaging:
288+ terms.append(SimpleTerm(packaging, packaging.id))
289+ return form.Fields(
290+ Choice(__name__='packaging', vocabulary=SimpleVocabulary(terms),
291+ required=True))
292+
293+ def _renderHiddenPackagingField(self, packaging):
294+ """Render a hidden input that fills in the packaging field."""
295+ if not self.can_delete_packaging:
296+ return None
297+ vocabulary = self.form_fields['packaging'].field.vocabulary
298+ return '<input type="hidden" name="field.packaging" value="%s" />' % (
299+ vocabulary.getTerm(packaging).token)
300+
301+ def renderDeletePackagingAction(self):
302+ """Render a submit input for the delete_packaging_action."""
303+ assert self.can_delete_packaging, 'User cannot delete Packaging.'
304+ return ('<input type="image" value="Delete Link" '
305+ 'src="/@@/remove" title="Delete upsteam link" '
306+ 'name="%s"/>' % self.delete_packaging_action.__name__)
307+
308+ def handleDeletePackagingError(self, action, data, errors):
309+ """Handle errors on package link deletion.
310+
311+ If 'packaging' is not set in the form data, we assume that means the
312+ provided Packaging id was not found, which should only happen if the
313+ same Packaging object was concurrently deleted. In this case, we want
314+ to display a more informative error message than the default 'Invalid
315+ value'.
316+ """
317+ if data.get('packaging') is None:
318+ self.setFieldError(
319+ 'packaging',
320+ _("This upstream association was deleted already."))
321+
322+ @action(_("Delete Link"), name='delete_packaging',
323+ failure=handleDeletePackagingError)
324+ def delete_packaging_action(self, action, data):
325+ """Delete a Packaging association."""
326+ packaging = data['packaging']
327+ productseries = packaging.productseries
328+ distroseries = packaging.distroseries
329+ getUtility(IPackagingUtil).deletePackaging(
330+ productseries, packaging.sourcepackagename, distroseries)
331+ self.request.response.addNotification(
332+ _("Removed upstream association between ${product} "
333+ "${productseries} and ${distroseries}.", mapping=dict(
334+ product=productseries.product.displayname,
335+ productseries=productseries.displayname,
336+ distroseries=distroseries.displayname)))
337
338=== added file 'lib/lp/registry/browser/tests/packaging-views.txt'
339--- lib/lp/registry/browser/tests/packaging-views.txt 1970-01-01 00:00:00 +0000
340+++ lib/lp/registry/browser/tests/packaging-views.txt 2009-10-24 20:01:11 +0000
341@@ -0,0 +1,237 @@
342+Packaging views
343+===============
344+
345+Packaging links connect a sourcepackage to a distroseries and a productseries.
346+
347+
348+Productseries linking packages
349+------------------------------
350+
351+Distro series sourcepackages can be linked to product series using the
352++addpackage named view.
353+
354+ >>> from canonical.launchpad.interfaces.launchpad import (
355+ ... ILaunchpadCelebrities)
356+
357+ >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
358+ >>> hoary = ubuntu.getSeries('hoary')
359+ >>> sourcepackagename = factory.makeSourcePackageName('hot')
360+ >>> sourcepackage = factory.makeSourcePackage(
361+ ... sourcepackagename=sourcepackagename, distroseries=hoary)
362+ >>> product = factory.makeProduct(name="hot", displayname='Hot')
363+ >>> productseries = factory.makeProductSeries(
364+ ... product=product, name='hotter')
365+ >>> productseries.sourcepackages
366+ []
367+
368+The view has a label and requires a distro series, source package name,
369+and a packaging contents.
370+
371+ >>> view = create_view(productseries, '+addpackage')
372+ >>> print view.label
373+ Packaging of hotter in distributions
374+
375+ >>> print view.page_title
376+ Packaging of hotter in distributions
377+
378+ >>> print view.field_names
379+ ['distroseries', 'sourcepackagename', 'packaging']
380+
381+ >>> print view.cancel_url
382+ http://launchpad.dev/hot/hotter
383+
384+ >>> form = {
385+ ... 'field.distroseries': 'ubuntu/hoary',
386+ ... 'field.sourcepackagename': 'hot',
387+ ... 'field.packaging': 'Primary Product',
388+ ... 'field.actions.continue': 'Continue',
389+ ... }
390+ >>> view = create_initialized_view(
391+ ... productseries, '+addpackage', form=form)
392+ >>> view.errors
393+ []
394+ >>> for package in productseries.sourcepackages:
395+ ... print package.name
396+ hot
397+
398+ >>> transaction.commit()
399+
400+It is an error to link a series to the same package and distro series twice.
401+
402+ >>> form = {
403+ ... 'field.distroseries': 'ubuntu/hoary',
404+ ... 'field.sourcepackagename': 'hot',
405+ ... 'field.packaging': 'Primary Product',
406+ ... 'field.actions.continue': 'Continue',
407+ ... }
408+ >>> view = create_initialized_view(
409+ ... productseries, '+addpackage', form=form)
410+ >>> for error in view.errors:
411+ ... print error
412+ This series is already packaged in Hoary.
413+
414+Once a distro series sourcepackage is linked to a product series, no other
415+product series can link to it.
416+
417+ >>> other_productseries = factory.makeProductSeries(
418+ ... product=product, name='hotest')
419+ >>> form = {
420+ ... 'field.distroseries': 'ubuntu/hoary',
421+ ... 'field.sourcepackagename': 'hot',
422+ ... 'field.packaging': 'Primary Product',
423+ ... 'field.actions.continue': 'Continue',
424+ ... }
425+ >>> view = create_initialized_view(
426+ ... other_productseries, '+addpackage', form=form)
427+ >>> for error in view.errors:
428+ ... print error
429+ The <a href=".../hoary/+source/hot">hot</a> package in Hoary is already
430+ linked to another series.
431+
432+A source package name must be provided.
433+
434+ >>> form = {
435+ ... 'field.distroseries': 'ubuntu/hoary',
436+ ... 'field.sourcepackagename': '',
437+ ... 'field.packaging': 'Primary Product',
438+ ... 'field.actions.continue': 'Continue',
439+ ... }
440+ >>> view = create_initialized_view(
441+ ... productseries, '+addpackage', form=form)
442+ >>> for error in view.errors:
443+ ... print error
444+ ('sourcepackagename', u'Source Package Name', RequiredMissing())
445+ You must choose the source package name.
446+
447+The +addpackage view provides the default_distroseries property. It is None
448+by default, but subclasses may change it.
449+
450+ >>> print view.default_distroseries
451+ None
452+
453+
454+Productseries linking Ubuntu packages
455+-------------------------------------
456+
457+The +ubuntupkg named view is a subclass of the +addpackage named view. It
458+allows the user to update the current linked Ubuntu package.
459+
460+ >>> from lp.registry.browser.packaging import PackagingAddView
461+
462+ >>> view = create_initialized_view(productseries, '+ubuntupkg')
463+ >>> isinstance(view, PackagingAddView)
464+ True
465+
466+ >>> print view.label
467+ Ubuntu source packaging
468+
469+ >>> print view.page_title
470+ Ubuntu source packaging
471+
472+ >>> print view.field_names
473+ ['sourcepackagename']
474+
475+ >>> print view.cancel_url
476+ http://launchpad.dev/hot/hotter
477+
478+The view restricts the packaging to the current Ubuntu series.
479+
480+ >>> print view.default_distroseries.name
481+ hoary
482+
483+The sourcepackagename is None if the package link was never set. The view's
484+packaging history is empty, and the sourcepackagename widget is empty.
485+
486+ >>> new_productseries = factory.makeProductSeries(
487+ ... product=product, name='cold')
488+ >>> view = create_initialized_view(new_productseries, '+ubuntupkg')
489+
490+ >>> print view.default_sourcepackagename
491+ None
492+
493+ >>> print view.widgets.get('sourcepackagename')._getFormValue()
494+ <BLANKLINE>
495+
496+ >>> print view.ubuntu_history
497+ []
498+
499+Series have been packaged in Ubuntu do have the current information and
500+a history.
501+
502+ >>> view = create_initialized_view(productseries, '+ubuntupkg')
503+ >>> print view.default_sourcepackagename.name
504+ hot
505+
506+ >>> print view.widgets.get('sourcepackagename')._getFormValue().name
507+ hot
508+
509+ >>> for packaging in view.ubuntu_history:
510+ ... print packaging.distroseries.name
511+ ... print packaging.sourcepackagename.name
512+ hoary hot
513+
514+The package in the current Ubuntu series can be updated.
515+
516+ >>> form = {
517+ ... 'field.sourcepackagename': 'thunderbird',
518+ ... 'field.actions.continue': 'Update',
519+ ... }
520+ >>> view = create_initialized_view(
521+ ... productseries, '+ubuntupkg', form=form)
522+ >>> view.errors
523+ []
524+
525+ >>> for packaging in view.ubuntu_history:
526+ ... print packaging.distroseries.name
527+ ... print packaging.sourcepackagename.name
528+ hoary thunderbird
529+
530+It is not an error to submit the same sourcepackagename information, the
531+action is ignored because there is no change
532+
533+ >>> form = {
534+ ... 'field.sourcepackagename': 'thunderbird',
535+ ... 'field.actions.continue': 'Update',
536+ ... }
537+ >>> view = create_initialized_view(
538+ ... productseries, '+ubuntupkg', form=form)
539+ >>> view.errors
540+ []
541+
542+ >>> for packaging in view.ubuntu_history:
543+ ... print packaging.distroseries.name
544+ ... print packaging.sourcepackagename.name
545+ hoary thunderbird
546+
547+When the current Ubuntu series changes, the sourcepackagename is not known,
548+and a new entry can be added to the packaging history.
549+
550+ >>> from lp.registry.interfaces.distroseries import DistroSeriesStatus
551+
552+ >>> login('admin@canonical.com')
553+ >>> hoary.status = DistroSeriesStatus.CURRENT
554+ >>> grumpy_series = ubuntu.getSeries('grumpy')
555+ >>> grumpy_series.status = DistroSeriesStatus.FROZEN
556+
557+ >>> a_user = factory.makePerson(name="hedgehog")
558+ >>> login_person(a_user)
559+ >>> form = {
560+ ... 'field.sourcepackagename': 'hot',
561+ ... 'field.actions.continue': 'Update',
562+ ... }
563+ >>> view = create_initialized_view(
564+ ... productseries, '+ubuntupkg', form=form)
565+ >>> view.errors
566+ []
567+
568+ >>> print view.default_distroseries.name
569+ grumpy
570+
571+ >>> print view.default_sourcepackagename
572+ None
573+
574+ >>> for packaging in view.ubuntu_history:
575+ ... print packaging.distroseries.name
576+ ... print packaging.sourcepackagename.name
577+ grumpy hot
578+ hoary thunderbird
579
580=== modified file 'lib/lp/registry/browser/tests/productseries-views.txt'
581--- lib/lp/registry/browser/tests/productseries-views.txt 2009-10-21 17:09:39 +0000
582+++ lib/lp/registry/browser/tests/productseries-views.txt 2009-10-24 20:01:11 +0000
583@@ -323,232 +323,3 @@
584 True
585 >>> print productseries.name
586 field-rabbit-20090501-193424
587-
588-
589-Linking packages
590-----------------
591-
592-Distro series sourcepackages can be linked to product series using the
593-+addpackage named view.
594-
595- >>> ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
596- >>> hoary = ubuntu.getSeries('hoary')
597- >>> sourcepackagename = factory.makeSourcePackageName('hot')
598- >>> sourcepackage = factory.makeSourcePackage(
599- ... sourcepackagename=sourcepackagename, distroseries=hoary)
600- >>> product = factory.makeProduct(name="hot", displayname='Hot')
601- >>> productseries = factory.makeProductSeries(
602- ... product=product, name='hotter')
603- >>> productseries.sourcepackages
604- []
605-
606-The view has a label and requires a distro series, source package name,
607-and a packaging contents.
608-
609- >>> view = create_view(productseries, '+addpackage')
610- >>> print view.label
611- Packaging of hotter in distributions
612-
613- >>> print view.page_title
614- Packaging of hotter in distributions
615-
616- >>> print view.field_names
617- ['distroseries', 'sourcepackagename', 'packaging']
618-
619- >>> print view.cancel_url
620- http://launchpad.dev/hot/hotter
621-
622- >>> form = {
623- ... 'field.distroseries': 'ubuntu/hoary',
624- ... 'field.sourcepackagename': 'hot',
625- ... 'field.packaging': 'Primary Product',
626- ... 'field.actions.continue': 'Continue',
627- ... }
628- >>> view = create_initialized_view(
629- ... productseries, '+addpackage', form=form)
630- >>> view.errors
631- []
632- >>> for package in productseries.sourcepackages:
633- ... print package.name
634- hot
635-
636- >>> transaction.commit()
637-
638-It is an error to link a series to the same package and distro series twice.
639-
640- >>> form = {
641- ... 'field.distroseries': 'ubuntu/hoary',
642- ... 'field.sourcepackagename': 'hot',
643- ... 'field.packaging': 'Primary Product',
644- ... 'field.actions.continue': 'Continue',
645- ... }
646- >>> view = create_initialized_view(
647- ... productseries, '+addpackage', form=form)
648- >>> for error in view.errors:
649- ... print error
650- This series is already packaged in Hoary.
651-
652-Once a distro series sourcepackage is linked to a product series, no other
653-product series can link to it.
654-
655- >>> other_productseries = factory.makeProductSeries(
656- ... product=product, name='hotest')
657- >>> form = {
658- ... 'field.distroseries': 'ubuntu/hoary',
659- ... 'field.sourcepackagename': 'hot',
660- ... 'field.packaging': 'Primary Product',
661- ... 'field.actions.continue': 'Continue',
662- ... }
663- >>> view = create_initialized_view(
664- ... other_productseries, '+addpackage', form=form)
665- >>> for error in view.errors:
666- ... print error
667- The <a href=".../hoary/+source/hot">hot</a> package in Hoary is already
668- linked to another series.
669-
670-A source package name must be provided.
671-
672- >>> form = {
673- ... 'field.distroseries': 'ubuntu/hoary',
674- ... 'field.sourcepackagename': '',
675- ... 'field.packaging': 'Primary Product',
676- ... 'field.actions.continue': 'Continue',
677- ... }
678- >>> view = create_initialized_view(
679- ... productseries, '+addpackage', form=form)
680- >>> for error in view.errors:
681- ... print error
682- ('sourcepackagename', u'Source Package Name', RequiredMissing())
683- You must choose the source package name.
684-
685-The +addpackage view provides the default_distroseries property. It is None
686-by default, but subclasses may change it.
687-
688- >>> print view.default_distroseries
689- None
690-
691-
692-Linking Ubuntu packages
693------------------------
694-
695-The +ubuntupkg named view is a subclass of the +addpackage named view. It
696-allows the user to update the current linked Ubuntu package.
697-
698- >>> from lp.registry.browser.packaging import PackagingAddView
699-
700- >>> view = create_initialized_view(productseries, '+ubuntupkg')
701- >>> isinstance(view, PackagingAddView)
702- True
703-
704- >>> print view.label
705- Ubuntu source packaging
706-
707- >>> print view.page_title
708- Ubuntu source packaging
709-
710- >>> print view.field_names
711- ['sourcepackagename']
712-
713- >>> print view.cancel_url
714- http://launchpad.dev/hot/hotter
715-
716-The view restricts the packaging to the current Ubuntu series.
717-
718- >>> print view.default_distroseries.name
719- hoary
720-
721-The sourcepackagename is None if the package link was never set. The view's
722-packaging history is empty, and the sourcepackagename widget is empty.
723-
724- >>> new_productseries = factory.makeProductSeries(
725- ... product=product, name='cold')
726- >>> view = create_initialized_view(new_productseries, '+ubuntupkg')
727-
728- >>> print view.default_sourcepackagename
729- None
730-
731- >>> print view.widgets.get('sourcepackagename')._getFormValue()
732- <BLANKLINE>
733-
734- >>> print view.ubuntu_history
735- []
736-
737-Series have been packaged in Ubuntu do have the current information and
738-a history.
739-
740- >>> view = create_initialized_view(productseries, '+ubuntupkg')
741- >>> print view.default_sourcepackagename.name
742- hot
743-
744- >>> print view.widgets.get('sourcepackagename')._getFormValue().name
745- hot
746-
747- >>> for packaging in view.ubuntu_history:
748- ... print packaging.distroseries.name
749- ... print packaging.sourcepackagename.name
750- hoary hot
751-
752-The package in the current Ubuntu series can be updated.
753-
754- >>> form = {
755- ... 'field.sourcepackagename': 'thunderbird',
756- ... 'field.actions.continue': 'Update',
757- ... }
758- >>> view = create_initialized_view(
759- ... productseries, '+ubuntupkg', form=form)
760- >>> view.errors
761- []
762-
763- >>> for packaging in view.ubuntu_history:
764- ... print packaging.distroseries.name
765- ... print packaging.sourcepackagename.name
766- hoary thunderbird
767-
768-It is not an error to submit the same sourcepackagename information, the
769-action is ignored because there is no change
770-
771- >>> form = {
772- ... 'field.sourcepackagename': 'thunderbird',
773- ... 'field.actions.continue': 'Update',
774- ... }
775- >>> view = create_initialized_view(
776- ... productseries, '+ubuntupkg', form=form)
777- >>> view.errors
778- []
779-
780- >>> for packaging in view.ubuntu_history:
781- ... print packaging.distroseries.name
782- ... print packaging.sourcepackagename.name
783- hoary thunderbird
784-
785-When the current Ubuntu series changes, the sourcepackagename is not known,
786-and a new entry can be added to the packaging history.
787-
788- >>> from lp.registry.interfaces.distroseries import DistroSeriesStatus
789-
790- >>> login('admin@canonical.com')
791- >>> hoary.status = DistroSeriesStatus.CURRENT
792- >>> grumpy_series = ubuntu.getSeries('grumpy')
793- >>> grumpy_series.status = DistroSeriesStatus.FROZEN
794-
795- >>> login_person(a_user)
796- >>> form = {
797- ... 'field.sourcepackagename': 'hot',
798- ... 'field.actions.continue': 'Update',
799- ... }
800- >>> view = create_initialized_view(
801- ... productseries, '+ubuntupkg', form=form)
802- >>> view.errors
803- []
804-
805- >>> print view.default_distroseries.name
806- grumpy
807-
808- >>> print view.default_sourcepackagename
809- None
810-
811- >>> for packaging in view.ubuntu_history:
812- ... print packaging.distroseries.name
813- ... print packaging.sourcepackagename.name
814- grumpy hot
815- hoary thunderbird
816
817=== modified file 'lib/lp/registry/browser/tests/test_packaging.py'
818--- lib/lp/registry/browser/tests/test_packaging.py 2009-10-16 15:00:55 +0000
819+++ lib/lp/registry/browser/tests/test_packaging.py 2009-10-24 20:01:11 +0000
820@@ -56,7 +56,7 @@
821 user_browser = self.user_browser
822 user_browser.open('http://launchpad.dev/ubuntu/+source/alsa-utils')
823 form = user_browser.getForm("delete_warty_alsa-utils_trunk")
824- form.getControl("Delete Link").click()
825+ form.getControl(name="field.actions.delete_packaging").click()
826 # Check that the change was committed.
827 login('no-priv@canonical.com')
828 self.assertFalse(packaging_util.packagingEntryExists(
829
830=== renamed file 'lib/lp/registry/stories/distribution/xx-distributionsourcepackage-packaging-concurrent-deletion.txt' => 'lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt'
831--- lib/lp/registry/stories/distribution/xx-distributionsourcepackage-packaging-concurrent-deletion.txt 2009-08-27 20:08:53 +0000
832+++ lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging-concurrent-deletion.txt 2009-10-24 20:01:11 +0000
833@@ -1,4 +1,5 @@
834-=== Concurrent Deletion of Packaging ===
835+Concurrent Deletion of Packaging
836+================================
837
838 When two browsers are used to concurrently delete the same packaging
839 association, only one of them can succeed. The other one does not oops
840@@ -16,7 +17,7 @@
841 deletion succeeds and the usual informational message is displayed.
842
843 >>> form = first_browser.getForm("delete_warty_alsa-utils_trunk")
844- >>> form.getControl("Delete Link").click()
845+ >>> form.getControl(name="field.actions.delete_packaging").click()
846 >>> content = first_browser.contents
847 >>> for tag in find_tags_by_class(content, 'error'):
848 ... print extract_text(tag)
849@@ -37,7 +38,7 @@
850 -- David Allouche 2007-12-07
851
852 >>> form = second_browser.getForm("delete_warty_alsa-utils_trunk")
853- >>> form.getControl("Delete Link").click()
854+ >>> form.getControl(name="field.actions.delete_packaging").click()
855 >>> content = second_browser.contents
856 >>> for tag in find_tags_by_class(content, 'informational'):
857 ... print extract_text(tag)
858
859=== renamed file 'lib/lp/registry/stories/distribution/xx-distributionsourcepackage-packaging.txt' => 'lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt'
860--- lib/lp/registry/stories/distribution/xx-distributionsourcepackage-packaging.txt 2009-10-21 01:55:17 +0000
861+++ lib/lp/registry/stories/packaging/xx-distributionsourcepackage-packaging.txt 2009-10-24 20:01:11 +0000
862@@ -1,4 +1,5 @@
863-== Distribution Packaging ==
864+Distribution Packaging
865+======================
866
867 The packaging records for a source package in a given distribution are
868 displayed on the page of the distribution source package.
869@@ -18,15 +19,16 @@
870 1.0.9a-4 release (main) ... weeks ago
871
872
873-=== Delete Link Button ===
874+Delete Link Button
875+------------------
876
877 A button is displayed to authenticated users to delete existing
878 packaging links.
879
880 >>> user_browser.open('http://launchpad.dev/ubuntu/+source/alsa-utils')
881 >>> form = user_browser.getForm("delete_warty_alsa-utils_trunk")
882- >>> print form.getControl("Delete Link")
883- <SubmitControl name='field.actions.delete_packaging' type='submit'>
884+ >>> print form.getControl(name="field.actions.delete_packaging")
885+ <ImageControl name='field.actions.delete_packaging' type='image'>
886
887 This button is not displayed to anonymous users.
888
889@@ -39,7 +41,7 @@
890 Clicking this button deletes the corresponding packaging association.
891
892 >>> form = user_browser.getForm("delete_warty_alsa-utils_trunk")
893- >>> form.getControl("Delete Link").click()
894+ >>> form.getControl(name="field.actions.delete_packaging").click()
895 >>> content = user_browser.contents
896 >>> for tag in find_tags_by_class(content, 'error'):
897 ... print extract_text(tag)
898
899=== modified file 'lib/lp/registry/templates/distributionsourcepackage-index.pt'
900--- lib/lp/registry/templates/distributionsourcepackage-index.pt 2009-09-25 17:00:20 +0000
901+++ lib/lp/registry/templates/distributionsourcepackage-index.pt 2009-10-24 20:01:11 +0000
902@@ -149,10 +149,6 @@
903 <tal:has_packaging condition="row/packaging">
904 <img tal:replace="structure row/packaging/productseries/image:icon"/>
905 <a tal:replace="structure row/packaging/productseries/fmt:link"/>
906- <a tal:attributes="
907- href row/series_package/menu:overview/edit_packaging/url">
908- <img src="/@@/edit"/>
909- </a>
910 <form style="display: inline"
911 method="POST"
912 tal:condition="row/hidden_packaging_field"
913@@ -161,6 +157,11 @@
914 <tal:hidden replace="structure row/hidden_packaging_field" />
915 <tal:action replace="structure view/renderDeletePackagingAction" />
916 </form>
917+ <a tal:attributes="
918+ href row/series_package/menu:overview/edit_packaging/url;
919+ title string:Edit upsteam link">
920+ <img src="/@@/edit"/>
921+ </a>
922 </tal:has_packaging>
923 </div><!--float right-->
924 </td>
925@@ -215,7 +216,7 @@
926 </tal:rows>
927 </table>
928 <script
929- tal:content="string:LP.client.cache['archive_context_url'] = '${archive/fmt:url}';"/>
930+ tal:content="string:LP.client.cache['archive_context_url'] = '${archive/fmt:url}';"></script>
931 <metal:js use-macro="archive/@@+macros/expandable-table-js"/>
932
933 <p style="float:right; padding-top:1em">
934
935=== modified file 'lib/lp/registry/templates/productseries-index.pt'
936--- lib/lp/registry/templates/productseries-index.pt 2009-10-20 16:13:57 +0000
937+++ lib/lp/registry/templates/productseries-index.pt 2009-10-24 20:01:11 +0000
938@@ -24,8 +24,7 @@
939 </tal:heading>
940
941 <div metal:fill-slot="main"
942- tal:define="overview_menu context/menu:overview;
943- sourcepackages context/sourcepackages">
944+ tal:define="overview_menu context/menu:overview">
945
946 <div class="top-portlet">
947 <div id="description"
948@@ -144,41 +143,7 @@
949 tal:content="structure context/@@+portlet-latestbugs"
950 tal:condition="context/@@+get-involved/official_malone" />
951
952- <div class="portlet">
953- <h2>Distribution packaging</h2>
954-
955- <p id="distribution-packaging-explanation">
956- <tal:distro condition="sourcepackages">
957- This series is packaged in the following distribution series:
958- </tal:distro>
959- <tal:no-distro condition="not: sourcepackages">
960- This series is not packaged in any distribution series.
961- </tal:no-distro>
962- </p>
963-
964- <ul id="distribution-packaging"
965- tal:condition="sourcepackages">
966- <li
967- tal:repeat="package sourcepackages">
968- <a class="sprite package-source"
969- tal:attributes="href package/fmt:url"><tal:distro
970- tal:content="package/distroseries/distribution/displayname">
971- Ubuntu</tal:distro>
972- <tal:series tal:replace="package/distroseries/displayname">
973- Warty</tal:series>
974- <tal:package tal:content="package/name">firefox</tal:package></a>
975- </li>
976- </ul>
977-
978- <ul class="horizontal">
979- <li>
980- <a tal:replace="structure overview_menu/ubuntupkg/fmt:icon-link" />
981- </li>
982- <li>
983- <a tal:replace="structure overview_menu/add_package/fmt:icon-link" />
984- </li>
985- </ul>
986- </div>
987+ <div tal:content="structure context/@@+portlet-packages" />
988 </div>
989
990 <div class="yui-u">
991
992=== modified file 'lib/lp/registry/templates/productseries-portlet-packages.pt'
993--- lib/lp/registry/templates/productseries-portlet-packages.pt 2009-07-17 17:59:07 +0000
994+++ lib/lp/registry/templates/productseries-portlet-packages.pt 2009-10-24 20:01:11 +0000
995@@ -1,25 +1,46 @@
996-<tal:root
997+<div class="portlet" id="portlet-packages"
998 xmlns:tal="http://xml.zope.org/namespaces/tal"
999 xmlns:metal="http://xml.zope.org/namespaces/metal"
1000 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1001- omit-tag="">
1002-
1003-<div class="portlet" id="portlet-packages"
1004- tal:condition="context/sourcepackages">
1005- <h2>Distribution packaging</h2>
1006-
1007- <div class="portletBody portletContent ">
1008- This series is packaged in the following places:
1009- <ul>
1010- <li tal:repeat="package context/sourcepackages">
1011- <span tal:replace="package/distroseries/distribution/name"
1012- />
1013- <span tal:replace="package/distroseries/name"
1014- />
1015- <a tal:content="package/name"
1016- tal:attributes="href package/fmt:url">apache</a>
1017- </li>
1018- </ul>
1019- </div>
1020+ tal:define="sourcepackages context/sourcepackages">
1021+ <h2>
1022+ <span class="see-all">
1023+ <a tal:attributes="href context/product/menu:overview/packages/fmt:url">
1024+ All packages
1025+ </a>
1026+ </span>
1027+ Distribution packaging
1028+ </h2>
1029+
1030+ <p id="distribution-packaging-explanation">
1031+ <tal:distro condition="sourcepackages">
1032+ This series is packaged in the following distribution series:
1033+ </tal:distro>
1034+ <tal:no-distro condition="not: sourcepackages">
1035+ This series is not packaged in any distribution series.
1036+ </tal:no-distro>
1037+ </p>
1038+
1039+ <ul id="distribution-packaging"
1040+ tal:condition="sourcepackages">
1041+ <li
1042+ tal:repeat="package sourcepackages">
1043+ <a class="sprite package-source"
1044+ tal:attributes="href package/fmt:url"><tal:distro
1045+ tal:content="package/distroseries/distribution/displayname">
1046+ Ubuntu</tal:distro>
1047+ <tal:series tal:replace="package/distroseries/displayname">
1048+ Warty</tal:series>
1049+ <tal:package tal:content="package/name">firefox</tal:package></a>
1050+ </li>
1051+ </ul>
1052+
1053+ <ul class="horizontal">
1054+ <li>
1055+ <a tal:replace="structure context/menu:overview/ubuntupkg/fmt:icon-link" />
1056+ </li>
1057+ <li>
1058+ <a tal:replace="structure context/menu:overview/add_package/fmt:icon-link" />
1059+ </li>
1060+ </ul>
1061 </div>
1062-</tal:root>
1063
1064=== modified file 'lib/lp/registry/templates/sourcepackage-index.pt'
1065--- lib/lp/registry/templates/sourcepackage-index.pt 2009-09-25 17:00:20 +0000
1066+++ lib/lp/registry/templates/sourcepackage-index.pt 2009-10-24 20:01:11 +0000
1067@@ -114,9 +114,7 @@
1068 </div>
1069
1070 <div class="yui-u">
1071- <div tal:replace="structure context/@@+portlet-upstream" />
1072-
1073- <div tal:condition="current">
1074+ <div class="portlet" tal:condition="current">
1075 <h2>Binary packages</h2>
1076
1077 <div id="binaries" tal:define="binaries view/binaries">
1078
1079=== removed file 'lib/lp/registry/templates/sourcepackage-portlet-upstream.pt'
1080--- lib/lp/registry/templates/sourcepackage-portlet-upstream.pt 2009-07-17 17:59:07 +0000
1081+++ lib/lp/registry/templates/sourcepackage-portlet-upstream.pt 1970-01-01 00:00:00 +0000
1082@@ -1,17 +0,0 @@
1083-<tal:root
1084- xmlns:tal="http://xml.zope.org/namespaces/tal"
1085- xmlns:metal="http://xml.zope.org/namespaces/metal"
1086- xmlns:i18n="http://xml.zope.org/namespaces/i18n"
1087- omit-tag="">
1088-
1089-<div class="portlet" id="portlet-upstream">
1090- <h2>Upstream release series</h2>
1091- <div class="portletBody portletContent">
1092-
1093- <a tal:condition="context/productseries"
1094- tal:content="context/productseries/title"
1095- tal:attributes="href context/productseries/fmt:url">apache head</a>
1096- <i tal:condition="not: context/productseries">None linked.</i>
1097- </div>
1098-</div>
1099-</tal:root>