Merge lp:~salgado/launchpad/request-to-base-template-adapter into lp:launchpad/db-devel

Proposed by Guilherme Salgado
Status: Merged
Merged at revision: 9654
Proposed branch: lp:~salgado/launchpad/request-to-base-template-adapter
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~mwhudson/launchpad/vostok-traverse-distro
Diff against target: 2703 lines (+943/-259)
46 files modified
.bzrignore (+1/-1)
lib/canonical/buildd/debian/changelog (+11/-1)
lib/canonical/buildd/debian/compat (+1/-0)
lib/canonical/buildd/debian/control (+4/-4)
lib/canonical/buildd/debian/launchpad-buildd.examples (+1/-0)
lib/canonical/buildd/debian/launchpad-buildd.init (+10/-0)
lib/canonical/buildd/debian/rules (+4/-3)
lib/canonical/buildd/debian/source/format (+1/-0)
lib/canonical/launchpad/browser/multistep.py (+19/-0)
lib/canonical/launchpad/doc/tales-macro.txt (+3/-1)
lib/canonical/launchpad/emailtemplates/product-other-license.txt (+3/-2)
lib/canonical/launchpad/webapp/configure.zcml (+4/-0)
lib/canonical/launchpad/webapp/launchpadform.py (+3/-0)
lib/canonical/launchpad/webapp/tales.py (+22/-3)
lib/canonical/launchpad/webapp/tests/test_base_template.py (+29/-0)
lib/canonical/widgets/product.py (+1/-0)
lib/canonical/widgets/templates/license.pt (+23/-5)
lib/lp/archivepublisher/config.py (+19/-14)
lib/lp/archivepublisher/ftparchive.py (+11/-3)
lib/lp/archivepublisher/tests/test_config.py (+19/-9)
lib/lp/archivepublisher/tests/test_ftparchive.py (+18/-2)
lib/lp/archiveuploader/tests/__init__.py (+9/-3)
lib/lp/archiveuploader/tests/test_buildduploads.py (+2/-3)
lib/lp/archiveuploader/tests/test_ppauploadprocessor.py (+2/-5)
lib/lp/archiveuploader/tests/test_recipeuploads.py (+2/-3)
lib/lp/archiveuploader/tests/test_securityuploads.py (+3/-7)
lib/lp/archiveuploader/tests/test_uploadprocessor.py (+78/-78)
lib/lp/archiveuploader/uploadprocessor.py (+38/-28)
lib/lp/registry/browser/product.py (+111/-12)
lib/lp/registry/browser/sourcepackage.py (+50/-4)
lib/lp/registry/browser/tests/project-add-views.txt (+23/-23)
lib/lp/registry/browser/tests/sourcepackage-views.txt (+14/-3)
lib/lp/registry/browser/tests/test_sourcepackage_views.py (+146/-0)
lib/lp/registry/model/sourcepackage.py (+0/-2)
lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt (+103/-1)
lib/lp/registry/templates/product-new.pt (+4/-4)
lib/lp/registry/templates/sourcepackage-edit-packaging.pt (+20/-0)
lib/lp/soyuz/scripts/soyuz_process_upload.py (+8/-2)
lib/lp/soyuz/tests/test_publishing.py (+65/-0)
lib/lp/vostok/browser/configure.zcml (+2/-8)
lib/lp/vostok/browser/root.py (+19/-1)
lib/lp/vostok/browser/tests/test_base_template.py (+16/-18)
lib/lp/vostok/browser/tests/test_root.py (+12/-1)
lib/lp/vostok/templates/main-template.pt (+6/-3)
lib/lp/vostok/templates/root.pt (+2/-2)
utilities/sourcedeps.conf (+1/-0)
To merge this branch: bzr merge lp:~salgado/launchpad/request-to-base-template-adapter
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) Approve
Michael Hudson-Doyle Approve
Review via email: mp+31982@code.launchpad.net

Description of the change

This branch makes it possible to use vostok's base template with any of Launchpad's templates.
It also makes all existing pages accessed using the vostok.dev vhost use vostok's base template.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

This all looks good to me. I suppose that you might want to ask Gary or Curtis what they think. It's not like you can land this for another few days anyway :-)

review: Approve
Revision history for this message
Guilherme Salgado (salgado) wrote :

One thing I realized now is that we no longer need the main_template
view registered, so I got rid of that and changed the test to test the
right thing: http://paste.ubuntu.com/476128/

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

That looks fine too :-)

Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Salgado,

This branch looks good, but I don't see a test like TestVostokLayerToMainTemplateAdapter that tests IMainTemplate adapting a layer besides Vostok. That would be good to have.

-Edwin

review: Approve
Revision history for this message
Guilherme Salgado (salgado) wrote :

Fair enough; I've added one. Thanks for the review

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2010-07-15 15:57:40 +0000
3+++ .bzrignore 2010-08-12 22:16:35 +0000
4@@ -56,7 +56,6 @@
5 .bazaar
6 .cache
7 .subversion
8-lib/canonical/buildd/launchpad-files
9 .testrepository
10 .memcache.pid
11 ./pipes
12@@ -77,3 +76,4 @@
13 lib/canonical/launchpad-buildd_*_source.build
14 lib/canonical/launchpad-buildd_*_source.changes
15 lib/canonical/buildd/debian/*
16+lib/canonical/buildd/launchpad-files/*
17
18=== renamed file 'daemons/buildd-slave-example.conf' => 'lib/canonical/buildd/buildd-slave-example.conf'
19=== modified file 'lib/canonical/buildd/debian/changelog'
20--- lib/canonical/buildd/debian/changelog 2010-08-05 21:12:36 +0000
21+++ lib/canonical/buildd/debian/changelog 2010-08-12 22:16:35 +0000
22@@ -1,9 +1,19 @@
23 launchpad-buildd (68) UNRELEASED; urgency=low
24
25+ [ William Grant ]
26 * Take an 'arch_tag' argument, so the master can override the slave
27 architecture.
28
29- -- William Grant <wgrant@ubuntu.com> Sun, 01 Aug 2010 22:00:32 +1000
30+ [ Jelmer Vernooij ]
31+
32+ * Explicitly use source format 1.0.
33+ * Add LSB information to init script.
34+ * Use debhelper >= 5 (available in dapper, not yet deprecated in
35+ maverick).
36+ * Fix spelling in description.
37+ * Install example buildd configuration.
38+
39+ -- Jelmer Vernooij <jelmer@canonical.com> Thu, 12 Aug 2010 17:04:14 +0200
40
41 launchpad-buildd (67) hardy-cat; urgency=low
42
43
44=== added file 'lib/canonical/buildd/debian/compat'
45--- lib/canonical/buildd/debian/compat 1970-01-01 00:00:00 +0000
46+++ lib/canonical/buildd/debian/compat 2010-08-12 22:16:35 +0000
47@@ -0,0 +1,1 @@
48+5
49
50=== modified file 'lib/canonical/buildd/debian/control'
51--- lib/canonical/buildd/debian/control 2010-05-19 15:50:27 +0000
52+++ lib/canonical/buildd/debian/control 2010-08-12 22:16:35 +0000
53@@ -3,15 +3,15 @@
54 Priority: extra
55 Maintainer: Adam Conrad <adconrad@ubuntu.com>
56 Standards-Version: 3.5.9
57-Build-Depends-Indep: debhelper (>= 4)
58+Build-Depends-Indep: debhelper (>= 5)
59
60 Package: launchpad-buildd
61 Section: misc
62 Architecture: all
63-Depends: python-twisted-core, python-twisted-web, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https, lsb-release, apache2
64+Depends: python-twisted-core, python-twisted-web, debootstrap, dpkg-dev, linux32, file, bzip2, sudo, ntpdate, adduser, apt-transport-https, lsb-release, apache2, ${misc:Depends}
65 Description: Launchpad buildd slave
66 This is the launchpad buildd slave package. It contains everything needed to
67 get a launchpad buildd going apart from the database manipulation required to
68 tell launchpad about the slave instance. If you are creating more than one
69- slave instance on the same computer, be sure to give them independant configs
70- and independant filecaches etc.
71+ slave instance on the same computer, be sure to give them independent configs
72+ and independent filecaches etc.
73
74=== added file 'lib/canonical/buildd/debian/launchpad-buildd.examples'
75--- lib/canonical/buildd/debian/launchpad-buildd.examples 1970-01-01 00:00:00 +0000
76+++ lib/canonical/buildd/debian/launchpad-buildd.examples 2010-08-12 22:16:35 +0000
77@@ -0,0 +1,1 @@
78+buildd-slave-example.conf
79
80=== modified file 'lib/canonical/buildd/debian/launchpad-buildd.init'
81--- lib/canonical/buildd/debian/launchpad-buildd.init 2010-03-31 17:10:21 +0000
82+++ lib/canonical/buildd/debian/launchpad-buildd.init 2010-08-12 22:16:35 +0000
83@@ -8,6 +8,16 @@
84 #
85 # Author: Daniel Silverstone <daniel.silverstone@canonical.com>
86
87+### BEGIN INIT INFO
88+# Provides: launchpad_buildd
89+# Required-Start: $local_fs $network $syslog $time
90+# Required-Stop: $local_fs $network $syslog $time
91+# Default-Start: 2 3 4 5
92+# Default-Stop: 0 1 6
93+# X-Interactive: false
94+# Short-Description: Start/stop launchpad buildds
95+### END INIT INFO
96+
97 set -e
98
99 PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
100
101=== modified file 'lib/canonical/buildd/debian/rules'
102--- lib/canonical/buildd/debian/rules 2010-07-18 08:49:02 +0000
103+++ lib/canonical/buildd/debian/rules 2010-08-12 22:16:35 +0000
104@@ -3,7 +3,6 @@
105 # Copyright 2009, 2010 Canonical Ltd. This software is licensed under the
106 # GNU Affero General Public License version 3 (see the file LICENSE).
107
108-export DH_COMPAT=4
109 export DH_OPTIONS
110
111 # This is an incomplete debian rules file for making the launchpad-buildd deb
112@@ -41,6 +40,7 @@
113 etc/launchpad-buildd \
114 usr/share/launchpad-buildd/canonical/launchpad/daemons \
115 usr/share/doc/launchpad-buildd
116+ dh_installexamples
117
118 # Do installs here
119 touch $(pytarget)/../launchpad/__init__.py
120@@ -89,10 +89,11 @@
121 clean:
122 dh_clean
123
124-package:
125- mkdir -p launchpad-files
126+prepare:
127 install -m644 $(daemons)/buildd-slave.tac launchpad-files/buildd-slave.tac
128 cp ../launchpad/daemons/tachandler.py launchpad-files/tachandler.py
129+
130+package: prepare
131 debuild -uc -us -S
132
133 build:
134
135=== added directory 'lib/canonical/buildd/debian/source'
136=== added file 'lib/canonical/buildd/debian/source/format'
137--- lib/canonical/buildd/debian/source/format 1970-01-01 00:00:00 +0000
138+++ lib/canonical/buildd/debian/source/format 2010-08-12 22:16:35 +0000
139@@ -0,0 +1,1 @@
140+1.0
141
142=== added directory 'lib/canonical/buildd/launchpad-files'
143=== modified file 'lib/canonical/launchpad/browser/multistep.py'
144--- lib/canonical/launchpad/browser/multistep.py 2010-05-12 19:06:17 +0000
145+++ lib/canonical/launchpad/browser/multistep.py 2010-08-12 22:16:35 +0000
146@@ -93,6 +93,14 @@
147 view.total_steps = self.total_steps
148 view.is_step = self.getIsStepDict()
149 self.step_number += 1
150+
151+ action_required = None
152+ for name in self.request.form.keys():
153+ if name.startswith('field.actions.'):
154+ action_required = (name, self.request.form[name])
155+ break
156+
157+ action_taken = view.action_taken
158 while view.next_step is not None:
159 view = view.next_step(self.context, self.request)
160 assert isinstance(view, StepView), 'Not a StepView: %s' % view
161@@ -102,8 +110,19 @@
162 view.is_step = self.getIsStepDict()
163 self.step_number += 1
164 view.injectStepNameInRequest()
165+ if view.action_taken is not None:
166+ action_taken = view.action_taken
167+
168 self.view = view
169
170+ if action_required is not None and action_taken is None:
171+ # This is mostly useful for catching tests that pass
172+ # in invalid form data via a dictionary instead of
173+ # using a test browser.
174+ raise AssertionError(
175+ 'MultiStepView did not find action for %s=%r'
176+ % action_required)
177+
178 def render(self):
179 return self.view.render()
180
181
182=== modified file 'lib/canonical/launchpad/doc/tales-macro.txt'
183--- lib/canonical/launchpad/doc/tales-macro.txt 2009-11-23 03:10:04 +0000
184+++ lib/canonical/launchpad/doc/tales-macro.txt 2010-08-12 22:16:35 +0000
185@@ -3,8 +3,9 @@
186 Launchpad has a 'macro:' TALES namespace that offers controls over the
187 layout of the page.
188
189+ >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
190 >>> class FakeView(object):
191- ... pass
192+ ... request = LaunchpadTestRequest()
193
194 Templates should start by specifying the kind of pagetype they use.
195 That's done by using the 'macro:page' traversal. That expression returns
196@@ -50,6 +51,7 @@
197 returns False for older views that do not.
198
199 >>> class LPView(object):
200+ ... request = LaunchpadTestRequest()
201 ... def isBetaUser(self):
202 ... return True
203
204
205=== modified file 'lib/canonical/launchpad/emailtemplates/product-other-license.txt'
206--- lib/canonical/launchpad/emailtemplates/product-other-license.txt 2010-05-12 19:06:17 +0000
207+++ lib/canonical/launchpad/emailtemplates/product-other-license.txt 2010-08-12 22:16:35 +0000
208@@ -28,8 +28,9 @@
209 questions.
210
211 Sometimes new projects are licensed as 'Other/Open Source' because the
212-licensing decisions have not yet been made. If that is your situation we urge
213-you to update the licensing in Launchpad as soon as you make that choice.
214+licensing decisions have not yet been made. If that is your situation
215+we urge you to update the licensing in Launchpad as soon as you make
216+that choice.
217
218 If the license for your project needs to be corrected you can do so by
219 following the 'Change Details' link on your project's overview page.
220
221=== modified file 'lib/canonical/launchpad/webapp/configure.zcml'
222--- lib/canonical/launchpad/webapp/configure.zcml 2010-07-23 13:24:58 +0000
223+++ lib/canonical/launchpad/webapp/configure.zcml 2010-08-12 22:16:35 +0000
224@@ -711,6 +711,10 @@
225 />
226
227 <adapter
228+ factory="canonical.launchpad.webapp.tales.LaunchpadLayerToMainTemplateAdapter"
229+ />
230+
231+ <adapter
232 factory="canonical.launchpad.webapp.snapshot.snapshot_sql_result" />
233 <!-- It also works for the legacy SQLObject interface. -->
234 <adapter
235
236=== modified file 'lib/canonical/launchpad/webapp/launchpadform.py'
237--- lib/canonical/launchpad/webapp/launchpadform.py 2010-06-23 23:07:10 +0000
238+++ lib/canonical/launchpad/webapp/launchpadform.py 2010-08-12 22:16:35 +0000
239@@ -74,6 +74,8 @@
240
241 actions = ()
242
243+ action_taken = None
244+
245 render_context = False
246
247 form_result = None
248@@ -112,6 +114,7 @@
249 self.form_result = action.success(data)
250 if self.next_url:
251 self.request.response.redirect(self.next_url)
252+ self.action_taken = action
253
254 def render(self):
255 """Return the body of the response.
256
257=== modified file 'lib/canonical/launchpad/webapp/tales.py'
258--- lib/canonical/launchpad/webapp/tales.py 2010-07-30 06:08:54 +0000
259+++ lib/canonical/launchpad/webapp/tales.py 2010-08-12 22:16:35 +0000
260@@ -22,7 +22,7 @@
261 from lazr.uri import URI
262
263 from zope.interface import Interface, Attribute, implements
264-from zope.component import getUtility, queryAdapter, getMultiAdapter
265+from zope.component import adapts, getUtility, queryAdapter, getMultiAdapter
266 from zope.app import zapi
267 from zope.publisher.browser import BrowserView
268 from zope.publisher.interfaces import IApplicationRequest
269@@ -31,6 +31,7 @@
270 ITraversable, IPathAdapter, TraversalError)
271 from zope.security.interfaces import Unauthorized
272 from zope.security.proxy import isinstance as zope_isinstance
273+from zope.schema import TextLine
274
275 import pytz
276 from z3c.ptcompat import ViewPageTemplateFile
277@@ -41,6 +42,7 @@
278 ISprint, LicenseStatus)
279 from canonical.launchpad.interfaces.launchpad import (
280 IHasIcon, IHasLogo, IHasMugshot, IPrivacy)
281+from canonical.launchpad.layers import LaunchpadLayer
282 import canonical.launchpad.pagetitles
283 from canonical.launchpad.webapp import canonical_url, urlappend
284 from canonical.launchpad.webapp.authorization import check_permission
285@@ -2297,6 +2299,20 @@
286 return check_permission(name, self.context)
287
288
289+class IMainTemplateFile(Interface):
290+ path = TextLine(title=u'The absolute path to this main template.')
291+
292+
293+class LaunchpadLayerToMainTemplateAdapter:
294+ adapts(LaunchpadLayer)
295+ implements(IMainTemplateFile)
296+
297+ def __init__(self, context):
298+ here = os.path.dirname(os.path.realpath(__file__))
299+ self.path = os.path.join(
300+ here, '../../../lp/app/templates/base-layout.pt')
301+
302+
303 class PageMacroDispatcher:
304 """Selects a macro, while storing information about page layout.
305
306@@ -2316,12 +2332,15 @@
307
308 implements(ITraversable)
309
310- base = ViewPageTemplateFile('../../../lp/app/templates/base-layout.pt')
311-
312 def __init__(self, context):
313 # The context of this object is a view object.
314 self.context = context
315
316+ @property
317+ def base(self):
318+ return ViewPageTemplateFile(
319+ IMainTemplateFile(self.context.request).path)
320+
321 def traverse(self, name, furtherPath):
322 if name == 'page':
323 if len(furtherPath) == 1:
324
325=== added file 'lib/canonical/launchpad/webapp/tests/test_base_template.py'
326--- lib/canonical/launchpad/webapp/tests/test_base_template.py 1970-01-01 00:00:00 +0000
327+++ lib/canonical/launchpad/webapp/tests/test_base_template.py 2010-08-12 22:16:35 +0000
328@@ -0,0 +1,29 @@
329+# Copyright 2010 Canonical Ltd. This software is licensed under the
330+# GNU Affero General Public License version 3 (see the file LICENSE).
331+
332+"""Tests for Launchpad's 'view/macro:page' TALES adapter."""
333+
334+__metaclass__ = type
335+
336+from zope.component import getMultiAdapter
337+from zope.traversing.interfaces import IPathAdapter
338+
339+from canonical.launchpad.webapp.publisher import rootObject
340+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
341+from canonical.testing.layers import FunctionalLayer
342+
343+from lp.testing import TestCase
344+
345+
346+class TestPageMacroDispatcher(TestCase):
347+
348+ layer = FunctionalLayer
349+
350+ def test_base_template(self):
351+ # Requests on the launchpad.dev vhost use the Launchpad base template.
352+ root_view = getMultiAdapter(
353+ (rootObject, LaunchpadTestRequest()), name='index.html')
354+ adapter = getMultiAdapter([root_view], IPathAdapter, name='macro')
355+ self.assertIn('lp/app/templates', adapter.base.filename)
356+ # The base template defines a 'master' macro as the adapter expects.
357+ self.assertIn('master', adapter.base.macros.keys())
358
359=== modified file 'lib/canonical/widgets/product.py'
360--- lib/canonical/widgets/product.py 2010-06-21 04:08:54 +0000
361+++ lib/canonical/widgets/product.py 2010-08-12 22:16:35 +0000
362@@ -316,6 +316,7 @@
363 self, 'license_info', self.license_info, IInputWidget,
364 prefix='field', value=initial_value,
365 context=field.context)
366+ self.source_package_release = None
367 # These will get filled in by _categorize(). They are the number of
368 # selected licenses in the category. The actual count doesn't matter,
369 # since if it's greater than 0 it will start opened. NOte that we
370
371=== modified file 'lib/canonical/widgets/templates/license.pt'
372--- lib/canonical/widgets/templates/license.pt 2009-07-17 17:59:07 +0000
373+++ lib/canonical/widgets/templates/license.pt 2010-08-12 22:16:35 +0000
374@@ -42,7 +42,7 @@
375 // the slider depends on whether there are any checked
376 // licenses in that category. A '0' means 'no'.
377 var arrow = Y.get(arrow_name);
378- if (arrow.getAttribute('count') == '0') {
379+ if (arrow.getAttribute('start_expanded') == '0') {
380 target.slide = Y.lazr.effects.slide_in(table_name);
381 }
382 else {
383@@ -76,6 +76,7 @@
384 target.slide.run();
385 }, target_name);
386 }
387+ make_slider({which: 'copyright'});
388 make_slider({which: 'recommended'});
389 make_slider({which: 'more'});
390 make_slider({which: 'deprecated'});
391@@ -136,6 +137,23 @@
392 //]]>
393 </script>
394 <div style="color: black">
395+ <tal:copyright condition="view/source_package_release">
396+ <a href="" id="copyright-expand" class="js-action">
397+ <img id="copyright-expand-arrow"
398+ src="/@@/treeCollapsed"
399+ title="Copyright info from source package"
400+ alt="Copyright info from source package"
401+ start_expanded="0"/>
402+ Copyright info from source package
403+ </a>
404+ <div id="copyright">
405+ <div
406+ tal:content="structure view/source_package_release/@@+copyright"
407+ style="overflow-x: hidden; overflow-y: auto;
408+ max-width: 60em; max-height: 32em; background: #f7f7f7"
409+ />
410+ </div>
411+ </tal:copyright>
412
413 Select the license(s) under which you release your project.
414 <div tal:condition="view/allow_pending_license"
415@@ -159,7 +177,7 @@
416 src="/@@/treeExpanded"
417 title="Recommended open source licenses"
418 alt="Recommended open source licenses"
419- tal:attributes="count view/recommended_count"/>
420+ tal:attributes="start_expanded view/recommended_count"/>
421 Recommended open source licenses
422 </a>
423 <input tal:replace="structure view/recommended" />
424@@ -168,7 +186,7 @@
425 src="/@@/treeCollapsed"
426 title="More open source licenses"
427 alt="More open source licenses"
428- tal:attributes="count view/more_count"/>
429+ tal:attributes="start_expanded view/more_count"/>
430 More open source licenses
431 </a>
432 <input tal:replace="structure view/more" />
433@@ -178,7 +196,7 @@
434 src="/@@/treeCollapsed"
435 title="Deprecated licenses"
436 alt="Deprecated licenses"
437- tal:attributes="count view/deprecated_count"/>
438+ tal:attributes="start_expanded view/deprecated_count"/>
439 Deprecated licenses
440 </a>
441 <input tal:replace="structure view/deprecated" />
442@@ -188,7 +206,7 @@
443 src="/@@/treeCollapsed"
444 title="Other choices"
445 alt="Other choices"
446- tal:attributes="count view/special_count"/>
447+ tal:attributes="start_expanded view/special_count"/>
448 Other choices
449 </a>
450 <input tal:replace="structure view/special" />
451
452=== added symlink 'lib/deb822.py'
453=== target is u'../sourcecode/python-debian/lib/deb822.py'
454=== added symlink 'lib/debian'
455=== target is u'../sourcecode/python-debian/lib/debian'
456=== modified file 'lib/lp/archivepublisher/config.py'
457--- lib/lp/archivepublisher/config.py 2010-07-07 06:28:03 +0000
458+++ lib/lp/archivepublisher/config.py 2010-08-12 22:16:35 +0000
459@@ -120,18 +120,15 @@
460 config_segment["archtags"].append(
461 dar.architecturetag.encode('utf-8'))
462
463- if not dr.lucilleconfig:
464- raise LucilleConfigError(
465- 'No Lucille configuration section for %s' % dr.name)
466-
467- strio = StringIO(dr.lucilleconfig.encode('utf-8'))
468- config_segment["config"] = ConfigParser()
469- config_segment["config"].readfp(strio)
470- strio.close()
471- config_segment["components"] = config_segment["config"].get(
472- "publishing", "components").split(" ")
473-
474- self._distroseries[distroseries_name] = config_segment
475+ if dr.lucilleconfig:
476+ strio = StringIO(dr.lucilleconfig.encode('utf-8'))
477+ config_segment["config"] = ConfigParser()
478+ config_segment["config"].readfp(strio)
479+ strio.close()
480+ config_segment["components"] = config_segment["config"].get(
481+ "publishing", "components").split(" ")
482+
483+ self._distroseries[distroseries_name] = config_segment
484
485 strio = StringIO(distribution.lucilleconfig.encode('utf-8'))
486 self._distroconfig = ConfigParser()
487@@ -144,11 +141,19 @@
488 # Because dicts iterate for keys only; this works to get dr names
489 return self._distroseries.keys()
490
491+ def series(self, dr):
492+ try:
493+ return self._distroseries[dr]
494+ except KeyError:
495+ raise LucilleConfigError(
496+ 'No Lucille config section for %s in %s' %
497+ (dr, self.distroName))
498+
499 def archTagsForSeries(self, dr):
500- return self._distroseries[dr]["archtags"]
501+ return self.series(dr)["archtags"]
502
503 def componentsForSeries(self, dr):
504- return self._distroseries[dr]["components"]
505+ return self.series(dr)["components"]
506
507 def _extractConfigInfo(self):
508 """Extract configuration information into the attributes we use"""
509
510=== modified file 'lib/lp/archivepublisher/ftparchive.py'
511--- lib/lp/archivepublisher/ftparchive.py 2009-10-26 18:40:04 +0000
512+++ lib/lp/archivepublisher/ftparchive.py 2010-08-12 22:16:35 +0000
513@@ -138,6 +138,14 @@
514 self._config = config
515 self._diskpool = diskpool
516 self.distro = distro
517+ self.distroseries = []
518+ for distroseries in self.distro.series:
519+ if not distroseries.name in self._config.distroSeriesNames():
520+ self.log.warning("Distroseries %s in %s doesn't have "
521+ "a lucille configuration.", distroseries.name,
522+ self.distro.name)
523+ else:
524+ self.distroseries.append(distroseries)
525 self.publisher = publisher
526 self.release_files_needed = {}
527
528@@ -185,7 +193,7 @@
529 # iterate over the pockets, and do the suffix check inside
530 # createEmptyPocketRequest; that would also allow us to replace
531 # the == "" check we do there by a RELEASE match
532- for distroseries in self.distro:
533+ for distroseries in self.distroseries:
534 components = self._config.componentsForSeries(distroseries.name)
535 for pocket, suffix in pocketsuffix.items():
536 if not fullpublish:
537@@ -366,7 +374,7 @@
538
539 def generateOverrides(self, fullpublish=False):
540 """Collect packages that need overrides, and generate them."""
541- for distroseries in self.distro.series:
542+ for distroseries in self.distroseries:
543 for pocket in PackagePublishingPocket.items:
544 if not fullpublish:
545 if not self.publisher.isDirty(distroseries, pocket):
546@@ -629,7 +637,7 @@
547
548 def generateFileLists(self, fullpublish=False):
549 """Collect currently published FilePublishings and write filelists."""
550- for distroseries in self.distro.series:
551+ for distroseries in self.distroseries:
552 for pocket in pocketsuffix:
553 if not fullpublish:
554 if not self.publisher.isDirty(distroseries, pocket):
555
556=== modified file 'lib/lp/archivepublisher/tests/test_config.py'
557--- lib/lp/archivepublisher/tests/test_config.py 2010-07-18 00:24:06 +0000
558+++ lib/lp/archivepublisher/tests/test_config.py 2010-08-12 22:16:35 +0000
559@@ -5,36 +5,48 @@
560
561 __metaclass__ = type
562
563-import unittest
564-
565 from zope.component import getUtility
566
567 from canonical.config import config
568 from canonical.launchpad.interfaces import IDistributionSet
569 from canonical.testing import LaunchpadZopelessLayer
570
571-
572-class TestConfig(unittest.TestCase):
573+from lp.archivepublisher.config import Config, LucilleConfigError
574+from lp.testing import TestCaseWithFactory
575+
576+
577+class TestConfig(TestCaseWithFactory):
578 layer = LaunchpadZopelessLayer
579
580 def setUp(self):
581+ super(TestConfig, self).setUp()
582 self.layer.switchDbUser(config.archivepublisher.dbuser)
583 self.ubuntutest = getUtility(IDistributionSet)['ubuntutest']
584
585+ def testMissingDistroSeries(self):
586+ distroseries = self.factory.makeDistroSeries(
587+ distribution=self.ubuntutest, name="somename")
588+ d = Config(self.ubuntutest)
589+ dsns = d.distroSeriesNames()
590+ self.assertEquals(len(dsns), 2)
591+ self.assertEquals(dsns[0], "breezy-autotest")
592+ self.assertEquals(dsns[1], "hoary-test")
593+ self.assertRaises(LucilleConfigError,
594+ d.archTagsForSeries, "somename")
595+ self.assertRaises(LucilleConfigError,
596+ d.archTagsForSeries, "unknown")
597+
598 def testInstantiate(self):
599 """Config should instantiate"""
600- from lp.archivepublisher.config import Config
601 d = Config(self.ubuntutest)
602
603 def testDistroName(self):
604 """Config should be able to return the distroName"""
605- from lp.archivepublisher.config import Config
606 d = Config(self.ubuntutest)
607 self.assertEqual(d.distroName, "ubuntutest")
608
609 def testDistroSeriesNames(self):
610 """Config should return two distroseries names"""
611- from lp.archivepublisher.config import Config
612 d = Config(self.ubuntutest)
613 dsns = d.distroSeriesNames()
614 self.assertEquals(len(dsns), 2)
615@@ -43,14 +55,12 @@
616
617 def testArchTagsForSeries(self):
618 """Config should have the arch tags for the drs"""
619- from lp.archivepublisher.config import Config
620 d = Config(self.ubuntutest)
621 archs = d.archTagsForSeries("hoary-test")
622 self.assertEquals(len(archs), 2)
623
624 def testDistroConfig(self):
625 """Config should have parsed a distro config"""
626- from lp.archivepublisher.config import Config
627 d = Config(self.ubuntutest)
628 # NOTE: Add checks here when you add stuff in util.py
629 self.assertEquals(d.stayofexecution, 5)
630
631=== modified file 'lib/lp/archivepublisher/tests/test_ftparchive.py'
632--- lib/lp/archivepublisher/tests/test_ftparchive.py 2010-07-18 00:24:06 +0000
633+++ lib/lp/archivepublisher/tests/test_ftparchive.py 2010-08-12 22:16:35 +0000
634@@ -15,7 +15,7 @@
635 from zope.component import getUtility
636
637 from canonical.config import config
638-from canonical.launchpad.scripts.logger import QuietFakeLogger
639+from canonical.launchpad.scripts.logger import BufferLogger, QuietFakeLogger
640 from canonical.testing import LaunchpadZopelessLayer
641 from lp.archivepublisher.config import Config
642 from lp.archivepublisher.diskpool import DiskPool
643@@ -23,6 +23,7 @@
644 from lp.archivepublisher.publishing import Publisher
645 from lp.registry.interfaces.distribution import IDistributionSet
646 from lp.registry.interfaces.pocket import PackagePublishingPocket
647+from lp.testing import TestCaseWithFactory
648
649
650 def sanitize_apt_ftparchive_Sources_output(text):
651@@ -55,10 +56,11 @@
652 return self._result[i:j]
653
654
655-class TestFTPArchive(unittest.TestCase):
656+class TestFTPArchive(TestCaseWithFactory):
657 layer = LaunchpadZopelessLayer
658
659 def setUp(self):
660+ super(TestFTPArchive, self).setUp()
661 self.layer.switchDbUser(config.archivepublisher.dbuser)
662
663 self._distribution = getUtility(IDistributionSet)['ubuntutest']
664@@ -79,6 +81,7 @@
665 self._publisher = SamplePublisher(self._archive)
666
667 def tearDown(self):
668+ super(TestFTPArchive, self).tearDown()
669 shutil.rmtree(self._config.distroroot)
670
671 def _verifyFile(self, filename, directory, output_filter=None):
672@@ -116,6 +119,19 @@
673 self._publisher)
674 return fa
675
676+ def test_NoLucilleConfig(self):
677+ # Distroseries without a lucille configuration get ignored
678+ # and trigger a warning, they don't break the publisher
679+ logger = BufferLogger()
680+ publisher = Publisher(
681+ logger, self._config, self._dp, self._archive)
682+ self.factory.makeDistroSeries(self._distribution, name="somename")
683+ fa = FTPArchiveHandler(logger, self._config, self._dp,
684+ self._distribution, publisher)
685+ fa.createEmptyPocketRequests(fullpublish=True)
686+ self.assertEquals("WARNING: Distroseries somename in ubuntutest doesn't "
687+ "have a lucille configuration.\n", logger.buffer.getvalue())
688+
689 def test_getSourcesForOverrides(self):
690 # getSourcesForOverrides returns a list of tuples containing:
691 # (sourcename, suite, component, section)
692
693=== modified file 'lib/lp/archiveuploader/tests/__init__.py'
694--- lib/lp/archiveuploader/tests/__init__.py 2010-05-04 15:38:08 +0000
695+++ lib/lp/archiveuploader/tests/__init__.py 2010-08-12 22:16:35 +0000
696@@ -1,6 +1,10 @@
697 # Copyright 2009 Canonical Ltd. This software is licensed under the
698 # GNU Affero General Public License version 3 (see the file LICENSE).
699
700+"""Tests for the archive uploader."""
701+
702+from __future__ import with_statement
703+
704 __metaclass__ = type
705
706 __all__ = ['datadir', 'getPolicy', 'insertFakeChangesFile',
707@@ -25,6 +29,7 @@
708 raise ValueError("Path is not relative: %s" % path)
709 return os.path.join(here, 'data', path)
710
711+
712 def insertFakeChangesFile(fileID, path=None):
713 """Insert a fake changes file into the librarian.
714
715@@ -34,11 +39,11 @@
716 """
717 if path is None:
718 path = datadir("ed-0.2-21/ed_0.2-21_source.changes")
719- changes_file_obj = open(path, 'r')
720- test_changes_file = changes_file_obj.read()
721- changes_file_obj.close()
722+ with open(path, 'r') as changes_file_obj:
723+ test_changes_file = changes_file_obj.read()
724 fillLibrarianFile(fileID, content=test_changes_file)
725
726+
727 def insertFakeChangesFileForAllPackageUploads():
728 """Ensure all the PackageUpload records point to a valid changes file."""
729 for id in set(pu.changesfile.id for pu in PackageUploadSet()):
730@@ -53,6 +58,7 @@
731 self.distroseries = distroseries
732 self.buildid = buildid
733
734+
735 def getPolicy(name='anything', distro='ubuntu', distroseries=None,
736 buildid=None):
737 """Build and return an Upload Policy for the given context."""
738
739=== modified file 'lib/lp/archiveuploader/tests/test_buildduploads.py'
740--- lib/lp/archiveuploader/tests/test_buildduploads.py 2010-07-18 00:26:33 +0000
741+++ lib/lp/archiveuploader/tests/test_buildduploads.py 2010-08-12 22:16:35 +0000
742@@ -7,7 +7,6 @@
743
744 from lp.archiveuploader.tests.test_securityuploads import (
745 TestStagedBinaryUploadBase)
746-from lp.archiveuploader.uploadprocessor import UploadProcessor
747 from lp.registry.interfaces.pocket import PackagePublishingPocket
748 from canonical.database.constants import UTC_NOW
749 from canonical.launchpad.interfaces import PackagePublishingStatus
750@@ -84,8 +83,8 @@
751 """Setup an UploadProcessor instance for a given buildd context."""
752 self.options.context = self.policy
753 self.options.buildid = str(build_candidate.id)
754- self.uploadprocessor = UploadProcessor(
755- self.options, self.layer.txn, self.log)
756+ self.uploadprocessor = self.getUploadProcessor(
757+ self.layer.txn)
758
759 def testDelayedBinaryUpload(self):
760 """Check if Soyuz copes with delayed binary uploads.
761
762=== modified file 'lib/lp/archiveuploader/tests/test_ppauploadprocessor.py'
763--- lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2010-08-02 02:13:52 +0000
764+++ lib/lp/archiveuploader/tests/test_ppauploadprocessor.py 2010-08-12 22:16:35 +0000
765@@ -18,7 +18,6 @@
766 from zope.security.proxy import removeSecurityProxy
767
768 from lp.app.errors import NotFoundError
769-from lp.archiveuploader.uploadprocessor import UploadProcessor
770 from lp.archiveuploader.tests.test_uploadprocessor import (
771 TestUploadProcessorBase)
772 from canonical.config import config
773@@ -74,8 +73,7 @@
774
775 # Set up the uploadprocessor with appropriate options and logger
776 self.options.context = 'insecure'
777- self.uploadprocessor = UploadProcessor(
778- self.options, self.layer.txn, self.log)
779+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
780
781 def assertEmail(self, contents=None, recipients=None,
782 ppa_header='name16'):
783@@ -1224,8 +1222,7 @@
784
785 # Re-initialize uploadprocessor since it depends on the new
786 # transaction reset by switchDbUser.
787- self.uploadprocessor = UploadProcessor(
788- self.options, self.layer.txn, self.log)
789+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
790
791 def testPPASizeQuotaSourceRejection(self):
792 """Verify the size quota check for PPA uploads.
793
794=== modified file 'lib/lp/archiveuploader/tests/test_recipeuploads.py'
795--- lib/lp/archiveuploader/tests/test_recipeuploads.py 2010-07-22 15:24:02 +0000
796+++ lib/lp/archiveuploader/tests/test_recipeuploads.py 2010-08-12 22:16:35 +0000
797@@ -12,7 +12,6 @@
798
799 from lp.archiveuploader.tests.test_uploadprocessor import (
800 TestUploadProcessorBase)
801-from lp.archiveuploader.uploadprocessor import UploadProcessor
802 from lp.buildmaster.interfaces.buildbase import BuildStatus
803 from lp.code.interfaces.sourcepackagerecipebuild import (
804 ISourcePackageRecipeBuildSource)
805@@ -42,8 +41,8 @@
806 self.options.context = 'recipe'
807 self.options.buildid = self.build.id
808
809- self.uploadprocessor = UploadProcessor(
810- self.options, self.layer.txn, self.log)
811+ self.uploadprocessor = self.getUploadProcessor(
812+ self.layer.txn)
813
814 def testSetsBuildAndState(self):
815 # Ensure that the upload processor correctly links the SPR to
816
817=== modified file 'lib/lp/archiveuploader/tests/test_securityuploads.py'
818--- lib/lp/archiveuploader/tests/test_securityuploads.py 2010-07-18 00:26:33 +0000
819+++ lib/lp/archiveuploader/tests/test_securityuploads.py 2010-08-12 22:16:35 +0000
820@@ -11,7 +11,6 @@
821
822 from lp.archiveuploader.tests.test_uploadprocessor import (
823 TestUploadProcessorBase)
824-from lp.archiveuploader.uploadprocessor import UploadProcessor
825 from lp.registry.interfaces.pocket import PackagePublishingPocket
826 from lp.soyuz.model.binarypackagebuild import BinaryPackageBuild
827 from lp.soyuz.model.processor import ProcessorFamily
828@@ -70,8 +69,7 @@
829 self.options.context = self.policy
830 self.options.nomails = self.no_mails
831 # Set up the uploadprocessor with appropriate options and logger
832- self.uploadprocessor = UploadProcessor(
833- self.options, self.layer.txn, self.log)
834+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
835 self.builds_before_upload = BinaryPackageBuild.select().count()
836 self.source_queue = None
837 self._uploadSource()
838@@ -232,8 +230,7 @@
839 """
840 build_candidate = self._createBuild('i386')
841 self.options.buildid = str(build_candidate.id)
842- self.uploadprocessor = UploadProcessor(
843- self.options, self.layer.txn, self.log)
844+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
845
846 build_used = self._uploadBinary('i386')
847
848@@ -254,8 +251,7 @@
849 """
850 build_candidate = self._createBuild('hppa')
851 self.options.buildid = str(build_candidate.id)
852- self.uploadprocessor = UploadProcessor(
853- self.options, self.layer.txn, self.log)
854+ self.uploadprocessor = self.getUploadProcessor(self.layer.txn)
855
856 self.assertRaises(AssertionError, self._uploadBinary, 'i386')
857
858
859=== modified file 'lib/lp/archiveuploader/tests/test_uploadprocessor.py'
860--- lib/lp/archiveuploader/tests/test_uploadprocessor.py 2010-08-02 02:13:52 +0000
861+++ lib/lp/archiveuploader/tests/test_uploadprocessor.py 2010-08-12 22:16:35 +0000
862@@ -23,8 +23,10 @@
863 from zope.security.proxy import removeSecurityProxy
864
865 from lp.app.errors import NotFoundError
866-from lp.archiveuploader.uploadpolicy import AbstractUploadPolicy
867+from lp.archiveuploader.uploadpolicy import (AbstractUploadPolicy,
868+ findPolicyByOptions)
869 from lp.archiveuploader.uploadprocessor import UploadProcessor
870+from lp.buildmaster.interfaces.buildbase import BuildStatus
871 from canonical.config import config
872 from canonical.database.constants import UTC_NOW
873 from lp.soyuz.model.archivepermission import ArchivePermission
874@@ -59,7 +61,7 @@
875 ISourcePackageNameSet)
876 from lp.services.mail import stub
877 from canonical.launchpad.testing.fakepackager import FakePackager
878-from lp.testing import TestCaseWithFactory
879+from lp.testing import TestCase, TestCaseWithFactory
880 from lp.testing.mail_helpers import pop_notifications
881 from canonical.launchpad.webapp.errorlog import ErrorReportingUtility
882 from canonical.testing import LaunchpadZopelessLayer
883@@ -113,7 +115,8 @@
884 super(TestUploadProcessorBase, self).setUp()
885
886 self.queue_folder = tempfile.mkdtemp()
887- os.makedirs(os.path.join(self.queue_folder, "incoming"))
888+ self.incoming_folder = os.path.join(self.queue_folder, "incoming")
889+ os.makedirs(self.incoming_folder)
890
891 self.test_files_dir = os.path.join(config.root,
892 "lib/lp/archiveuploader/tests/data/suite")
893@@ -139,6 +142,30 @@
894 shutil.rmtree(self.queue_folder)
895 super(TestUploadProcessorBase, self).tearDown()
896
897+ def getUploadProcessor(self, txn):
898+ def getPolicy(distro):
899+ self.options.distro = distro.name
900+ return findPolicyByOptions(self.options)
901+ return UploadProcessor(
902+ self.options.base_fsroot, self.options.dryrun,
903+ self.options.nomails,
904+ self.options.keep, getPolicy, txn, self.log)
905+
906+ def publishPackage(self, packagename, version, source=True,
907+ archive=None):
908+ """Publish a single package that is currently NEW in the queue."""
909+ queue_items = self.breezy.getQueueItems(
910+ status=PackageUploadStatus.NEW, name=packagename,
911+ version=version, exact_match=True, archive=archive)
912+ self.assertEqual(queue_items.count(), 1)
913+ queue_item = queue_items[0]
914+ queue_item.setAccepted()
915+ if source:
916+ pubrec = queue_item.sources[0].publish(self.log)
917+ else:
918+ pubrec = queue_item.builds[0].publish(self.log)
919+ return pubrec
920+
921 def assertLogContains(self, line):
922 """Assert if a given line is present in the log messages."""
923 self.assertTrue(line in self.log.lines,
924@@ -208,25 +235,29 @@
925 filename, len(content), StringIO(content),
926 'application/x-gtar')
927
928- def queueUpload(self, upload_name, relative_path="", test_files_dir=None):
929+ def queueUpload(self, upload_name, relative_path="", test_files_dir=None,
930+ queue_entry=None):
931 """Queue one of our test uploads.
932
933- upload_name is the name of the test upload directory. It is also
934+ upload_name is the name of the test upload directory. If there
935+ is no explicit queue entry name specified, it is also
936 the name of the queue entry directory we create.
937 relative_path is the path to create inside the upload, eg
938 ubuntu/~malcc/default. If not specified, defaults to "".
939
940 Return the path to the upload queue entry directory created.
941 """
942+ if queue_entry is None:
943+ queue_entry = upload_name
944 target_path = os.path.join(
945- self.queue_folder, "incoming", upload_name, relative_path)
946+ self.incoming_folder, queue_entry, relative_path)
947 if test_files_dir is None:
948 test_files_dir = self.test_files_dir
949 upload_dir = os.path.join(test_files_dir, upload_name)
950 if relative_path:
951 os.makedirs(os.path.dirname(target_path))
952 shutil.copytree(upload_dir, target_path)
953- return os.path.join(self.queue_folder, "incoming", upload_name)
954+ return os.path.join(self.incoming_folder, queue_entry)
955
956 def processUpload(self, processor, upload_dir):
957 """Process an upload queue entry directory.
958@@ -248,8 +279,7 @@
959 self.layer.txn.commit()
960 if policy is not None:
961 self.options.context = policy
962- return UploadProcessor(
963- self.options, self.layer.txn, self.log)
964+ return self.getUploadProcessor(self.layer.txn)
965
966 def assertEmail(self, contents=None, recipients=None):
967 """Check last email content and recipients.
968@@ -341,24 +371,9 @@
969 "Expected acceptance email not rejection. Actually Got:\n%s"
970 % raw_msg)
971
972- def _publishPackage(self, packagename, version, source=True,
973- archive=None):
974- """Publish a single package that is currently NEW in the queue."""
975- queue_items = self.breezy.getQueueItems(
976- status=PackageUploadStatus.NEW, name=packagename,
977- version=version, exact_match=True, archive=archive)
978- self.assertEqual(queue_items.count(), 1)
979- queue_item = queue_items[0]
980- queue_item.setAccepted()
981- if source:
982- pubrec = queue_item.sources[0].publish(self.log)
983- else:
984- pubrec = queue_item.builds[0].publish(self.log)
985- return pubrec
986-
987 def testInstantiate(self):
988 """UploadProcessor should instantiate"""
989- up = UploadProcessor(self.options, None, self.log)
990+ up = self.getUploadProcessor(None)
991
992 def testLocateDirectories(self):
993 """Return a sorted list of subdirs in a directory.
994@@ -372,7 +387,7 @@
995 os.mkdir("%s/dir1" % testdir)
996 os.mkdir("%s/dir2" % testdir)
997
998- up = UploadProcessor(self.options, None, self.log)
999+ up = self.getUploadProcessor(None)
1000 located_dirs = up.locateDirectories(testdir)
1001 self.assertEqual(located_dirs, ['dir1', 'dir2', 'dir3'])
1002 finally:
1003@@ -390,7 +405,7 @@
1004 open("%s/2_source.changes" % testdir, "w").close()
1005 open("%s/3.not_changes" % testdir, "w").close()
1006
1007- up = UploadProcessor(self.options, None, self.log)
1008+ up = self.getUploadProcessor(None)
1009 located_files = up.locateChangesFiles(testdir)
1010 self.assertEqual(
1011 located_files, ["2_source.changes", "1.changes"])
1012@@ -418,7 +433,7 @@
1013
1014 # Move it
1015 self.options.base_fsroot = testdir
1016- up = UploadProcessor(self.options, None, self.log)
1017+ up = self.getUploadProcessor(None)
1018 up.moveUpload(upload, target_name)
1019
1020 # Check it moved
1021@@ -439,7 +454,7 @@
1022
1023 # Remove it
1024 self.options.base_fsroot = testdir
1025- up = UploadProcessor(self.options, None, self.log)
1026+ up = self.getUploadProcessor(None)
1027 up.moveProcessedUpload(upload, "accepted")
1028
1029 # Check it was removed, not moved
1030@@ -462,7 +477,7 @@
1031
1032 # Move it
1033 self.options.base_fsroot = testdir
1034- up = UploadProcessor(self.options, None, self.log)
1035+ up = self.getUploadProcessor(None)
1036 up.moveProcessedUpload(upload, "rejected")
1037
1038 # Check it moved
1039@@ -485,7 +500,7 @@
1040
1041 # Remove it
1042 self.options.base_fsroot = testdir
1043- up = UploadProcessor(self.options, None, self.log)
1044+ up = self.getUploadProcessor(None)
1045 up.removeUpload(upload)
1046
1047 # Check it was removed, not moved
1048@@ -498,7 +513,7 @@
1049
1050 def testOrderFilenames(self):
1051 """orderFilenames sorts _source.changes ahead of other files."""
1052- up = UploadProcessor(self.options, None, self.log)
1053+ up = self.getUploadProcessor(None)
1054
1055 self.assertEqual(["d_source.changes", "a", "b", "c"],
1056 up.orderFilenames(["b", "a", "d_source.changes", "c"]))
1057@@ -522,8 +537,7 @@
1058 # Register our broken upload policy
1059 AbstractUploadPolicy._registerPolicy(BrokenUploadPolicy)
1060 self.options.context = 'broken'
1061- uploadprocessor = UploadProcessor(
1062- self.options, self.layer.txn, self.log)
1063+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1064
1065 # Upload a package to Breezy.
1066 upload_dir = self.queueUpload("baz_1.0-1")
1067@@ -634,7 +648,7 @@
1068 # Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
1069 upload_dir = self.queueUpload("bar_1.0-1")
1070 self.processUpload(uploadprocessor, upload_dir)
1071- bar_source_pub = self._publishPackage('bar', '1.0-1')
1072+ bar_source_pub = self.publishPackage('bar', '1.0-1')
1073 [bar_original_build] = bar_source_pub.createMissingBuilds()
1074
1075 # Move the source from the accepted queue.
1076@@ -653,7 +667,7 @@
1077 self.processUpload(uploadprocessor, upload_dir)
1078 self.assertEqual(
1079 uploadprocessor.last_processed_upload.is_rejected, False)
1080- bar_bin_pubs = self._publishPackage('bar', '1.0-1', source=False)
1081+ bar_bin_pubs = self.publishPackage('bar', '1.0-1', source=False)
1082 # Mangle its publishing component to "restricted" so we can check
1083 # the copy archive ancestry override later.
1084 restricted = getUtility(IComponentSet)["restricted"]
1085@@ -746,14 +760,14 @@
1086 # Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
1087 upload_dir = self.queueUpload("bar_1.0-1")
1088 self.processUpload(uploadprocessor, upload_dir)
1089- bar_source_pub = self._publishPackage('bar', '1.0-1')
1090+ bar_source_pub = self.publishPackage('bar', '1.0-1')
1091 [bar_original_build] = bar_source_pub.createMissingBuilds()
1092
1093 self.options.context = 'buildd'
1094 self.options.buildid = bar_original_build.id
1095 upload_dir = self.queueUpload("bar_1.0-1_binary")
1096 self.processUpload(uploadprocessor, upload_dir)
1097- [bar_binary_pub] = self._publishPackage("bar", "1.0-1", source=False)
1098+ [bar_binary_pub] = self.publishPackage("bar", "1.0-1", source=False)
1099
1100 # Prepare ubuntu/breezy-autotest to build sources in i386.
1101 breezy_autotest = self.ubuntu['breezy-autotest']
1102@@ -803,7 +817,7 @@
1103 # Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
1104 upload_dir = self.queueUpload("bar_1.0-1")
1105 self.processUpload(uploadprocessor, upload_dir)
1106- bar_source_old = self._publishPackage('bar', '1.0-1')
1107+ bar_source_old = self.publishPackage('bar', '1.0-1')
1108
1109 # Upload 'bar-1.0-1' source and binary to ubuntu/breezy.
1110 upload_dir = self.queueUpload("bar_1.0-2")
1111@@ -816,7 +830,7 @@
1112 self.options.buildid = bar_original_build.id
1113 upload_dir = self.queueUpload("bar_1.0-2_binary")
1114 self.processUpload(uploadprocessor, upload_dir)
1115- [bar_binary_pub] = self._publishPackage("bar", "1.0-2", source=False)
1116+ [bar_binary_pub] = self.publishPackage("bar", "1.0-2", source=False)
1117
1118 # Create a COPY archive for building in non-virtual builds.
1119 uploader = getUtility(IPersonSet).getByName('name16')
1120@@ -971,7 +985,7 @@
1121 partner_archive = getUtility(IArchiveSet).getByDistroPurpose(
1122 self.ubuntu, ArchivePurpose.PARTNER)
1123 self.assertTrue(partner_archive)
1124- self._publishPackage("foocomm", "1.0-1", archive=partner_archive)
1125+ self.publishPackage("foocomm", "1.0-1", archive=partner_archive)
1126
1127 # Check the publishing record's archive and component.
1128 foocomm_spph = SourcePackagePublishingHistory.selectOneBy(
1129@@ -1015,7 +1029,7 @@
1130 self.assertEqual(foocomm_bpr.component.name, 'partner')
1131
1132 # Publish the upload so we can check the publishing record.
1133- self._publishPackage("foocomm", "1.0-1", source=False)
1134+ self.publishPackage("foocomm", "1.0-1", source=False)
1135
1136 # Check the publishing record's archive and component.
1137 foocomm_bpph = BinaryPackagePublishingHistory.selectOneBy(
1138@@ -1054,14 +1068,14 @@
1139 # Accept and publish the upload.
1140 partner_archive = getUtility(IArchiveSet).getByDistroPurpose(
1141 self.ubuntu, ArchivePurpose.PARTNER)
1142- self._publishPackage("foocomm", "1.0-1", archive=partner_archive)
1143+ self.publishPackage("foocomm", "1.0-1", archive=partner_archive)
1144
1145 # Now do the same thing with a binary package.
1146 upload_dir = self.queueUpload("foocomm_1.0-1_binary")
1147 self.processUpload(uploadprocessor, upload_dir)
1148
1149 # Accept and publish the upload.
1150- self._publishPackage("foocomm", "1.0-1", source=False,
1151+ self.publishPackage("foocomm", "1.0-1", source=False,
1152 archive=partner_archive)
1153
1154 # Upload the next source version of the package.
1155@@ -1105,8 +1119,7 @@
1156 self.breezy.status = SeriesStatus.CURRENT
1157 self.layer.txn.commit()
1158 self.options.context = 'insecure'
1159- uploadprocessor = UploadProcessor(
1160- self.options, self.layer.txn, self.log)
1161+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1162
1163 # Upload a package for Breezy.
1164 upload_dir = self.queueUpload("foocomm_1.0-1_proposed")
1165@@ -1124,8 +1137,7 @@
1166 self.breezy.status = SeriesStatus.CURRENT
1167 self.layer.txn.commit()
1168 self.options.context = 'insecure'
1169- uploadprocessor = UploadProcessor(
1170- self.options, self.layer.txn, self.log)
1171+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1172
1173 # Upload a package for Breezy.
1174 upload_dir = self.queueUpload("foocomm_1.0-1")
1175@@ -1140,8 +1152,7 @@
1176 pocket and ensure it fails."""
1177 # Set up the uploadprocessor with appropriate options and logger.
1178 self.options.context = 'insecure'
1179- uploadprocessor = UploadProcessor(
1180- self.options, self.layer.txn, self.log)
1181+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1182
1183 # Upload a package for Breezy.
1184 upload_dir = self.queueUpload("foocomm_1.0-1_updates")
1185@@ -1302,8 +1313,7 @@
1186 used.
1187 That exception will then initiate the creation of an OOPS report.
1188 """
1189- processor = UploadProcessor(
1190- self.options, self.layer.txn, self.log)
1191+ processor = self.getUploadProcessor(self.layer.txn)
1192
1193 upload_dir = self.queueUpload("foocomm_1.0-1_proposed")
1194 bogus_changesfile_data = '''
1195@@ -1346,8 +1356,7 @@
1196 self.setupBreezy()
1197 self.layer.txn.commit()
1198 self.options.context = 'absolutely-anything'
1199- uploadprocessor = UploadProcessor(
1200- self.options, self.layer.txn, self.log)
1201+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1202
1203 # Upload the source first to enable the binary later:
1204 upload_dir = self.queueUpload("bar_1.0-1_lzma")
1205@@ -1357,7 +1366,7 @@
1206 self.assertTrue(
1207 "rejected" not in raw_msg,
1208 "Failed to upload bar source:\n%s" % raw_msg)
1209- self._publishPackage("bar", "1.0-1")
1210+ self.publishPackage("bar", "1.0-1")
1211 # Clear out emails generated during upload.
1212 ignore = pop_notifications()
1213
1214@@ -1456,15 +1465,14 @@
1215 permission=ArchivePermissionType.UPLOAD, person=uploader,
1216 component=restricted)
1217
1218- uploadprocessor = UploadProcessor(
1219- self.options, self.layer.txn, self.log)
1220+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1221
1222 # Upload the first version and accept it to make it known in
1223 # Ubuntu. The uploader has rights to upload NEW packages to
1224 # components that he does not have direct rights to.
1225 upload_dir = self.queueUpload("bar_1.0-1")
1226 self.processUpload(uploadprocessor, upload_dir)
1227- bar_source_pub = self._publishPackage('bar', '1.0-1')
1228+ bar_source_pub = self.publishPackage('bar', '1.0-1')
1229 # Clear out emails generated during upload.
1230 ignore = pop_notifications()
1231
1232@@ -1509,15 +1517,14 @@
1233 permission=ArchivePermissionType.UPLOAD, person=uploader,
1234 component=restricted)
1235
1236- uploadprocessor = UploadProcessor(
1237- self.options, self.layer.txn, self.log)
1238+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1239
1240 # Upload the first version and accept it to make it known in
1241 # Ubuntu. The uploader has rights to upload NEW packages to
1242 # components that he does not have direct rights to.
1243 upload_dir = self.queueUpload("bar_1.0-1")
1244 self.processUpload(uploadprocessor, upload_dir)
1245- bar_source_pub = self._publishPackage('bar', '1.0-1')
1246+ bar_source_pub = self.publishPackage('bar', '1.0-1')
1247 # Clear out emails generated during upload.
1248 ignore = pop_notifications()
1249
1250@@ -1590,8 +1597,7 @@
1251 # with pointer to the Soyuz questions in Launchpad and the
1252 # reason why the message was sent to the current recipients.
1253 self.setupBreezy()
1254- uploadprocessor = UploadProcessor(
1255- self.options, self.layer.txn, self.log)
1256+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1257
1258 upload_dir = self.queueUpload("bar_1.0-1", "boing")
1259 self.processUpload(uploadprocessor, upload_dir)
1260@@ -1636,8 +1642,7 @@
1261 self.setupBreezy()
1262 self.layer.txn.commit()
1263 self.options.context = 'absolutely-anything'
1264- uploadprocessor = UploadProcessor(
1265- self.options, self.layer.txn, self.log)
1266+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1267
1268 # Upload the source.
1269 upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
1270@@ -1655,8 +1660,7 @@
1271 permitted_formats=[SourcePackageFormat.FORMAT_3_0_QUILT])
1272 self.layer.txn.commit()
1273 self.options.context = 'absolutely-anything'
1274- uploadprocessor = UploadProcessor(
1275- self.options, self.layer.txn, self.log)
1276+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1277
1278 # Upload the source.
1279 upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
1280@@ -1666,7 +1670,7 @@
1281 self.assertTrue(
1282 "rejected" not in raw_msg,
1283 "Failed to upload bar source:\n%s" % raw_msg)
1284- spph = self._publishPackage("bar", "1.0-1")
1285+ spph = self.publishPackage("bar", "1.0-1")
1286
1287 self.assertEquals(
1288 sorted((sprf.libraryfile.filename, sprf.filetype)
1289@@ -1689,8 +1693,7 @@
1290 permitted_formats=[SourcePackageFormat.FORMAT_3_0_QUILT])
1291 self.layer.txn.commit()
1292 self.options.context = 'absolutely-anything'
1293- uploadprocessor = UploadProcessor(
1294- self.options, self.layer.txn, self.log)
1295+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1296
1297 # Upload the first source.
1298 upload_dir = self.queueUpload("bar_1.0-1_3.0-quilt")
1299@@ -1700,7 +1703,7 @@
1300 self.assertTrue(
1301 "rejected" not in raw_msg,
1302 "Failed to upload bar source:\n%s" % raw_msg)
1303- spph = self._publishPackage("bar", "1.0-1")
1304+ spph = self.publishPackage("bar", "1.0-1")
1305
1306 # Upload another source sharing the same (component) orig.
1307 upload_dir = self.queueUpload("bar_1.0-2_3.0-quilt_without_orig")
1308@@ -1728,8 +1731,7 @@
1309 permitted_formats=[SourcePackageFormat.FORMAT_3_0_NATIVE])
1310 self.layer.txn.commit()
1311 self.options.context = 'absolutely-anything'
1312- uploadprocessor = UploadProcessor(
1313- self.options, self.layer.txn, self.log)
1314+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1315
1316 # Upload the source.
1317 upload_dir = self.queueUpload("bar_1.0_3.0-native")
1318@@ -1739,7 +1741,7 @@
1319 self.assertTrue(
1320 "rejected" not in raw_msg,
1321 "Failed to upload bar source:\n%s" % raw_msg)
1322- spph = self._publishPackage("bar", "1.0")
1323+ spph = self.publishPackage("bar", "1.0")
1324
1325 self.assertEquals(
1326 sorted((sprf.libraryfile.filename, sprf.filetype)
1327@@ -1754,8 +1756,7 @@
1328 self.setupBreezy()
1329 self.layer.txn.commit()
1330 self.options.context = 'absolutely-anything'
1331- uploadprocessor = UploadProcessor(
1332- self.options, self.layer.txn, self.log)
1333+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1334
1335 # Upload the source.
1336 upload_dir = self.queueUpload("bar_1.0-1_1.0-bzip2")
1337@@ -1772,8 +1773,7 @@
1338 self.setupBreezy()
1339 breezy = self.ubuntu['breezy']
1340 breezy.status = SeriesStatus.CURRENT
1341- uploadprocessor = UploadProcessor(
1342- self.options, self.layer.txn, self.log)
1343+ uploadprocessor = self.getUploadProcessor(self.layer.txn)
1344
1345 upload_dir = self.queueUpload("bar_1.0-1")
1346 self.processUpload(uploadprocessor, upload_dir)
1347
1348=== modified file 'lib/lp/archiveuploader/uploadprocessor.py'
1349--- lib/lp/archiveuploader/uploadprocessor.py 2010-08-04 00:30:56 +0000
1350+++ lib/lp/archiveuploader/uploadprocessor.py 2010-08-12 22:16:35 +0000
1351@@ -60,7 +60,7 @@
1352 from lp.archiveuploader.nascentupload import (
1353 NascentUpload, FatalUploadError, EarlyReturnUploadError)
1354 from lp.archiveuploader.uploadpolicy import (
1355- findPolicyByOptions, UploadPolicyError)
1356+ UploadPolicyError)
1357 from lp.soyuz.interfaces.archive import IArchiveSet, NoSuchPPA
1358 from lp.registry.interfaces.distribution import IDistributionSet
1359 from lp.registry.interfaces.person import IPersonSet
1360@@ -108,16 +108,33 @@
1361 class UploadProcessor:
1362 """Responsible for processing uploads. See module docstring."""
1363
1364- def __init__(self, options, ztm, log):
1365- self.options = options
1366+ def __init__(self, base_fsroot, dry_run, no_mails, keep, policy_for_distro,
1367+ ztm, log):
1368+ """Create a new upload processor.
1369+
1370+ :param base_fsroot: Root path for queue to use
1371+ :param dry_run: Run but don't commit changes to database
1372+ :param no_mails: Don't send out any emails
1373+ :param builds: Interpret leaf names as build ids
1374+ :param keep: Leave the files in place, don't move them away
1375+ :param policy_for_distro: callback to obtain Policy object for a
1376+ distribution
1377+ :param ztm: Database transaction to use
1378+ :param log: Logger to use for reporting
1379+ """
1380+ self.base_fsroot = base_fsroot
1381+ self.dry_run = dry_run
1382+ self.keep = keep
1383+ self.last_processed_upload = None
1384+ self.log = log
1385+ self.no_mails = no_mails
1386+ self._getPolicyForDistro = policy_for_distro
1387 self.ztm = ztm
1388- self.log = log
1389- self.last_processed_upload = None
1390
1391- def processUploadQueue(self):
1392+ def processUploadQueue(self, leaf_name=None):
1393 """Search for uploads, and process them.
1394
1395- Uploads are searched for in the 'incoming' directory inside the
1396+ Uploads are searched for in the 'incoming' directory inside the
1397 base_fsroot.
1398
1399 This method also creates the 'incoming', 'accepted', 'rejected', and
1400@@ -127,19 +144,22 @@
1401 self.log.debug("Beginning processing")
1402
1403 for subdir in ["incoming", "accepted", "rejected", "failed"]:
1404- full_subdir = os.path.join(self.options.base_fsroot, subdir)
1405+ full_subdir = os.path.join(self.base_fsroot, subdir)
1406 if not os.path.exists(full_subdir):
1407 self.log.debug("Creating directory %s" % full_subdir)
1408 os.mkdir(full_subdir)
1409
1410- fsroot = os.path.join(self.options.base_fsroot, "incoming")
1411+ fsroot = os.path.join(self.base_fsroot, "incoming")
1412 uploads_to_process = self.locateDirectories(fsroot)
1413 self.log.debug("Checked in %s, found %s"
1414 % (fsroot, uploads_to_process))
1415 for upload in uploads_to_process:
1416 self.log.debug("Considering upload %s" % upload)
1417+ if leaf_name is not None and upload != leaf_name:
1418+ self.log.debug("Skipping %s -- does not match %s" % (
1419+ upload, leaf_name))
1420+ continue
1421 self.processUpload(fsroot, upload)
1422-
1423 finally:
1424 self.log.debug("Rolling back any remaining transactions.")
1425 self.ztm.abort()
1426@@ -152,16 +172,7 @@
1427 is 'failed', otherwise it is the worst of the results from the
1428 individual changes files, in order 'failed', 'rejected', 'accepted'.
1429
1430- If the leafname option is set but its value is not the same as the
1431- name of the upload directory, skip it entirely.
1432-
1433 """
1434- if (self.options.leafname is not None and
1435- upload != self.options.leafname):
1436- self.log.debug("Skipping %s -- does not match %s" % (
1437- upload, self.options.leafname))
1438- return
1439-
1440 upload_path = os.path.join(fsroot, upload)
1441 changes_files = self.locateChangesFiles(upload_path)
1442
1443@@ -242,7 +253,7 @@
1444 # Skip lockfile deletion, see similar code in lp.poppy.hooks.
1445 fsroot_lock.release(skip_delete=True)
1446
1447- sorted_dir_names = sorted(
1448+ sorted_dir_names = sorted(
1449 dir_name
1450 for dir_name in dir_names
1451 if os.path.isdir(os.path.join(fsroot, dir_name)))
1452@@ -321,8 +332,7 @@
1453 "https://help.launchpad.net/Packaging/PPA#Uploading "
1454 "and update your configuration.")))
1455 self.log.debug("Finding fresh policy")
1456- self.options.distro = distribution.name
1457- policy = findPolicyByOptions(self.options)
1458+ policy = self._getPolicyForDistro(distribution)
1459 policy.archive = archive
1460
1461 # DistroSeries overriding respect the following precedence:
1462@@ -396,7 +406,7 @@
1463 # when transaction is committed) this will cause any emails sent
1464 # sent by do_reject to be lost.
1465 notify = True
1466- if self.options.dryrun or self.options.nomails:
1467+ if self.dry_run or self.no_mails:
1468 notify = False
1469 if upload.is_rejected:
1470 result = UploadStatusEnum.REJECTED
1471@@ -415,7 +425,7 @@
1472 for msg in upload.rejections:
1473 self.log.warn("\t%s" % msg)
1474
1475- if self.options.dryrun:
1476+ if self.dry_run:
1477 self.log.info("Dry run, aborting transaction.")
1478 self.ztm.abort()
1479 else:
1480@@ -434,7 +444,7 @@
1481 This includes moving the given upload directory and moving the
1482 matching .distro file, if it exists.
1483 """
1484- if self.options.keep or self.options.dryrun:
1485+ if self.keep or self.dry_run:
1486 self.log.debug("Keeping contents untouched")
1487 return
1488
1489@@ -462,21 +472,21 @@
1490 This includes moving the given upload directory and moving the
1491 matching .distro file, if it exists.
1492 """
1493- if self.options.keep or self.options.dryrun:
1494+ if self.keep or self.dry_run:
1495 self.log.debug("Keeping contents untouched")
1496 return
1497
1498 pathname = os.path.basename(upload)
1499
1500 target_path = os.path.join(
1501- self.options.base_fsroot, subdir_name, pathname)
1502+ self.base_fsroot, subdir_name, pathname)
1503 self.log.debug("Moving upload directory %s to %s" %
1504 (upload, target_path))
1505 shutil.move(upload, target_path)
1506
1507 distro_filename = upload + ".distro"
1508 if os.path.isfile(distro_filename):
1509- target_path = os.path.join(self.options.base_fsroot, subdir_name,
1510+ target_path = os.path.join(self.base_fsroot, subdir_name,
1511 os.path.basename(distro_filename))
1512 self.log.debug("Moving distro file %s to %s" % (distro_filename,
1513 target_path))
1514
1515=== modified file 'lib/lp/registry/browser/product.py'
1516--- lib/lp/registry/browser/product.py 2010-08-04 04:07:21 +0000
1517+++ lib/lp/registry/browser/product.py 2010-08-12 22:16:35 +0000
1518@@ -83,6 +83,7 @@
1519 from lp.registry.interfaces.pillar import IPillarNameSet
1520 from lp.registry.interfaces.product import IProductReviewSearch, License
1521 from lp.registry.interfaces.series import SeriesStatus
1522+from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
1523 from lp.registry.interfaces.product import (
1524 IProduct, IProductSet, LicenseStatus)
1525 from lp.registry.interfaces.productrelease import (
1526@@ -120,7 +121,7 @@
1527 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
1528 from canonical.launchpad.webapp.launchpadform import (
1529 action, custom_widget, LaunchpadEditFormView, LaunchpadFormView,
1530- ReturnToReferrerMixin)
1531+ ReturnToReferrerMixin, safe_action)
1532 from canonical.launchpad.webapp.menu import NavigationMenu
1533 from canonical.launchpad.webapp.tales import MenuAPI
1534 from canonical.widgets.popup import PersonPickerWidget
1535@@ -1833,6 +1834,17 @@
1536 return canonical_url(self.product)
1537
1538
1539+def create_source_package_fields():
1540+ return form.Fields(
1541+ Choice(__name__='source_package_name',
1542+ vocabulary='SourcePackageName',
1543+ required=False),
1544+ Choice(__name__='distroseries',
1545+ vocabulary='DistroSeries',
1546+ required=False),
1547+ )
1548+
1549+
1550 class ProjectAddStepOne(StepView):
1551 """product/+new view class for creating a new project."""
1552
1553@@ -1849,6 +1861,29 @@
1554 step_description = 'Project basics'
1555 search_results_count = 0
1556
1557+ def setUpFields(self):
1558+ """See `LaunchpadFormView`."""
1559+ super(ProjectAddStepOne, self).setUpFields()
1560+ self.form_fields = (
1561+ self.form_fields +
1562+ create_source_package_fields())
1563+
1564+ def setUpWidgets(self):
1565+ """See `LaunchpadFormView`."""
1566+ super(ProjectAddStepOne, self).setUpWidgets()
1567+ self.widgets['source_package_name'].visible = False
1568+ self.widgets['distroseries'].visible = False
1569+
1570+ @property
1571+ def _return_url(self):
1572+ """This view is using the hidden _return_url field.
1573+
1574+ It is not using the `ReturnToReferrerMixin`, since none
1575+ of its other code is used, because multistep views can't
1576+ have next_url set until the form submission succeeds.
1577+ """
1578+ return self.request.form.get('_return_url')
1579+
1580 @property
1581 def _next_step(self):
1582 """Define the next step.
1583@@ -1862,9 +1897,10 @@
1584 def main_action(self, data):
1585 """See `MultiStepView`."""
1586 self.next_step = self._next_step
1587- self.request.form['displayname'] = data['displayname']
1588- self.request.form['name'] = data['name'].lower()
1589- self.request.form['summary'] = data['summary']
1590+
1591+ # Make this a safe_action, so that the sourcepackage page can skip
1592+ # the first step with a link (GET request) providing form values.
1593+ continue_action = safe_action(StepView.continue_action)
1594
1595
1596 class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
1597@@ -1873,7 +1909,6 @@
1598 _field_names = ['displayname', 'name', 'title', 'summary',
1599 'description', 'licenses', 'license_info',
1600 ]
1601- main_action_label = u'Complete Registration'
1602 schema = IProduct
1603 step_name = 'projectaddstep2'
1604 template = ViewPageTemplateFile('../templates/product-new.pt')
1605@@ -1887,6 +1922,25 @@
1606 custom_widget('license_info', GhostWidget)
1607
1608 @property
1609+ def main_action_label(self):
1610+ if self.source_package_name is None:
1611+ return u'Complete Registration'
1612+ else:
1613+ return u'Complete registration and link to %s package' % (
1614+ self.source_package_name.name,
1615+ )
1616+
1617+ @property
1618+ def _return_url(self):
1619+ """This view is using the hidden _return_url field.
1620+
1621+ It is not using the `ReturnToReferrerMixin`, since none
1622+ of its other code is used, because multistep views can't
1623+ have next_url set until the form submission succeeds.
1624+ """
1625+ return self.request.form.get('_return_url')
1626+
1627+ @property
1628 def step_description(self):
1629 """See `MultiStepView`."""
1630 if self.search_results_count > 0:
1631@@ -1897,7 +1951,8 @@
1632 """See `LaunchpadFormView`."""
1633 super(ProjectAddStepTwo, self).setUpFields()
1634 self.form_fields = (self.form_fields +
1635- self._createDisclaimMaintainerField())
1636+ self._createDisclaimMaintainerField() +
1637+ create_source_package_fields())
1638
1639 def _createDisclaimMaintainerField(self):
1640 """Return a Bool field for disclaiming maintainer.
1641@@ -1930,12 +1985,39 @@
1642 "this will be the project's URL.")
1643 self.widgets['displayname'].visible = False
1644
1645+ self.widgets['source_package_name'].visible = False
1646+ self.widgets['distroseries'].visible = False
1647+
1648+ # Set the source_package_release attribute on the licenses
1649+ # widget, so that the source package's copyright info can be
1650+ # displayed.
1651+ ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
1652+ if self.source_package_name is not None:
1653+ release_list = ubuntu.getCurrentSourceReleases(
1654+ [self.source_package_name])
1655+ if len(release_list) != 0:
1656+ self.widgets['licenses'].source_package_release = (
1657+ release_list.items()[0][1])
1658+
1659+ @property
1660+ def source_package_name(self):
1661+ # setUpWidgets() doesn't have access to the data dictionary,
1662+ # so the source package name needs to be converted from a string
1663+ # into an object here.
1664+ package_name_string = self.request.form.get(
1665+ 'field.source_package_name')
1666+ if package_name_string is None:
1667+ return None
1668+ else:
1669+ return getUtility(ISourcePackageNameSet).queryByName(
1670+ package_name_string)
1671+
1672 @cachedproperty
1673 def _search_string(self):
1674 """Return the ORed terms to match."""
1675- search_text = SPACE.join((self.request.form['name'],
1676- self.request.form['displayname'],
1677- self.request.form['summary']))
1678+ search_text = SPACE.join((self.request.form['field.name'],
1679+ self.request.form['field.displayname'],
1680+ self.request.form['field.summary']))
1681 # OR all the terms together.
1682 return OR.join(search_text.split())
1683
1684@@ -1972,7 +2054,8 @@
1685 def label(self):
1686 """See `LaunchpadFormView`."""
1687 return 'Register %s (%s) in Launchpad' % (
1688- self.request.form['displayname'], self.request.form['name'])
1689+ self.request.form['field.displayname'],
1690+ self.request.form['field.name'])
1691
1692 def create_product(self, data):
1693 """Create the product from the user data."""
1694@@ -1996,12 +2079,28 @@
1695 license_info=data['license_info'],
1696 project=project)
1697
1698+ def link_source_package(self, product, data):
1699+ if (data.get('distroseries') is not None
1700+ and self.source_package_name is not None):
1701+ source_package = data['distroseries'].getSourcePackage(
1702+ self.source_package_name)
1703+ source_package.setPackaging(
1704+ product.development_focus, self.user)
1705+ self.request.response.addInfoNotification(
1706+ 'Linked %s project to %s source package.' % (
1707+ product.displayname, self.source_package_name.name))
1708+
1709 def main_action(self, data):
1710 """See `MultiStepView`."""
1711 self.product = self.create_product(data)
1712 self.notifyCommercialMailingList()
1713 notify(ObjectCreatedEvent(self.product))
1714- self.next_url = canonical_url(self.product)
1715+ self.link_source_package(self.product, data)
1716+
1717+ if self._return_url is None:
1718+ self.next_url = canonical_url(self.product)
1719+ else:
1720+ self.next_url = self._return_url
1721
1722
1723 class ProductAddView(MultiStepView):
1724@@ -2027,7 +2126,7 @@
1725
1726 driver = copy_field(IProduct['driver'])
1727
1728- transfer_to_registry = Bool(
1729+ transfer_to_registry = Bool(
1730 title=_("I do not want to maintain this project"),
1731 required=False,
1732 description=_(
1733
1734=== modified file 'lib/lp/registry/browser/sourcepackage.py'
1735--- lib/lp/registry/browser/sourcepackage.py 2010-07-02 14:34:58 +0000
1736+++ lib/lp/registry/browser/sourcepackage.py 2010-08-12 22:16:35 +0000
1737@@ -19,6 +19,8 @@
1738
1739 from apt_pkg import ParseSrcDepends
1740 from cgi import escape
1741+import string
1742+import urllib
1743 from z3c.ptcompat import ViewPageTemplateFile
1744 from zope.app.form.browser import DropdownWidget
1745 from zope.app.form.interfaces import IInputWidget
1746@@ -35,12 +37,14 @@
1747
1748 from canonical.launchpad import helpers
1749 from canonical.launchpad.browser.multistep import MultiStepView, StepView
1750+
1751 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
1752 from canonical.launchpad.browser.packagerelationship import (
1753 relationship_builder)
1754 from lp.answers.browser.questiontarget import (
1755 QuestionTargetFacetMixin, QuestionTargetAnswersMenu)
1756 from lp.services.worlddata.interfaces.country import ICountry
1757+from lp.registry.browser.product import ProjectAddStepOne
1758 from lp.registry.interfaces.packaging import IPackaging, IPackagingUtil
1759 from lp.registry.interfaces.pocket import PackagePublishingPocket
1760 from lp.registry.interfaces.product import IProductSet
1761@@ -62,6 +66,35 @@
1762 from canonical.lazr.utils import smartquote
1763
1764
1765+def get_register_upstream_url(source_package):
1766+ displayname = string.capwords(source_package.name.replace('-', ' '))
1767+ distroseries_string = "%s/%s" % (
1768+ source_package.distroseries.distribution.name,
1769+ source_package.distroseries.name)
1770+ params = {
1771+ '_return_url': canonical_url(source_package),
1772+ 'field.source_package_name': source_package.sourcepackagename.name,
1773+ 'field.distroseries': distroseries_string,
1774+ 'field.name': source_package.name,
1775+ 'field.displayname': displayname,
1776+ 'field.title': displayname,
1777+ 'field.__visited_steps__': ProjectAddStepOne.step_name,
1778+ 'field.actions.continue': 'Continue',
1779+ }
1780+ if len(source_package.releases) == 0:
1781+ params['field.summary'] = ''
1782+ else:
1783+ # This is based on the SourcePackageName.summary attribute, but
1784+ # it eliminates the binary.name and duplicate summary lines.
1785+ summary_set = set()
1786+ for binary in source_package.releases[0].sample_binary_packages:
1787+ summary_set.add(binary.summary)
1788+ params['field.summary'] = '\n'.join(sorted(summary_set))
1789+ query_string = urllib.urlencode(
1790+ sorted(params.items()), doseq=True)
1791+ return '/projects/+new?%s' % query_string
1792+
1793+
1794 class SourcePackageNavigation(GetitemNavigation, BugTargetTraversalMixin):
1795
1796 usedfor = ISourcePackage
1797@@ -190,6 +223,10 @@
1798 self.next_step = SourcePackageChangeUpstreamStepTwo
1799 self.request.form['product'] = data['product']
1800
1801+ @property
1802+ def register_upstream_url(self):
1803+ return get_register_upstream_url(self.context)
1804+
1805
1806 class SourcePackageChangeUpstreamStepTwo(ReturnToReferrerMixin, StepView):
1807 """A view to set the `IProductSeries` of a sourcepackage."""
1808@@ -345,7 +382,7 @@
1809 def processForm(self):
1810 # look for an update to any of the things we track
1811 form = self.request.form
1812- if form.has_key('packaging'):
1813+ if 'packaging' in form:
1814 if self.productseries_widget.hasValidInput():
1815 new_ps = self.productseries_widget.getInputValue()
1816 # we need to create or update the packaging
1817@@ -445,6 +482,7 @@
1818 initial_focus_widget = None
1819 max_suggestions = 9
1820 other_upstream = object()
1821+ register_upstream = object()
1822
1823 def setUpFields(self):
1824 """See `LaunchpadFormView`."""
1825@@ -467,9 +505,12 @@
1826 vocab_terms.append(SimpleTerm(product, product.name, description))
1827 # Add an option to represent the user's decision to choose a
1828 # different project. Note that project names cannot be uppercase.
1829- description = 'Choose another upstream project'
1830- vocab_terms.append(
1831- SimpleTerm(self.other_upstream, 'OTHER_UPSTREAM', description))
1832+ vocab_terms.append(
1833+ SimpleTerm(self.other_upstream, 'OTHER_UPSTREAM',
1834+ 'Choose another upstream project'))
1835+ vocab_terms.append(
1836+ SimpleTerm(self.register_upstream, 'REGISTER_UPSTREAM',
1837+ 'Register the upstream project'))
1838 upstream_vocabulary = SimpleVocabulary(vocab_terms)
1839
1840 self.form_fields = Fields(
1841@@ -487,6 +528,11 @@
1842 self.next_url = canonical_url(
1843 self.context, view_name="+edit-packaging")
1844 return
1845+ elif upstream is self.register_upstream:
1846+ # The user wants to create a new project.
1847+ url = get_register_upstream_url(self.context)
1848+ self.request.response.redirect(url)
1849+ return
1850 self.context.setPackaging(upstream.development_focus, self.user)
1851 self.request.response.addInfoNotification(
1852 'The project %s was linked to this source package.' %
1853
1854=== modified file 'lib/lp/registry/browser/tests/project-add-views.txt'
1855--- lib/lp/registry/browser/tests/project-add-views.txt 2010-05-25 04:41:12 +0000
1856+++ lib/lp/registry/browser/tests/project-add-views.txt 2010-08-12 22:16:35 +0000
1857@@ -15,25 +15,24 @@
1858 are forwarded in the form data to the second step. The title is also
1859 forwarded, but is only required by the Zope machinery, not the view.
1860
1861- >>> form = {'field.actions.continue': 'Continue'}
1862+ >>> from lp.registry.browser.product import ProjectAddStepOne
1863+ >>> form = {
1864+ ... 'field.actions.continue': 'Continue',
1865+ ... 'field.__visited_steps__': ProjectAddStepOne.step_name,
1866+ ... 'field.displayname': '',
1867+ ... 'field.name': '',
1868+ ... 'field.summary': '',
1869+ ... }
1870
1871 >>> view = create_initialized_view(product_set, name='+new', form=form)
1872- Traceback (most recent call last):
1873- ...
1874- KeyError: 'displayname'
1875+ >>> for error in view.view.errors:
1876+ ... print error
1877+ ('displayname', 'Name', RequiredMissing())
1878+ ('name', 'URL', RequiredMissing())
1879+ ('summary', u'Summary', RequiredMissing())
1880
1881 >>> form['field.displayname'] = 'Snowdog'
1882- >>> view = create_initialized_view(product_set, name='+new', form=form)
1883- Traceback (most recent call last):
1884- ...
1885- KeyError: 'name'
1886-
1887 >>> form['field.name'] = 'snowdog'
1888- >>> view = create_initialized_view(product_set, name='+new', form=form)
1889- Traceback (most recent call last):
1890- ...
1891- KeyError: 'summary'
1892-
1893 >>> form['field.summary'] = 'By-tor and the Snowdog'
1894 >>> view = create_initialized_view(product_set, name='+new', form=form)
1895
1896@@ -44,7 +43,6 @@
1897 # steps individually.
1898
1899 >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
1900- >>> from lp.registry.browser.product import ProjectAddStepOne
1901
1902 >>> form['field.__visited_steps__'] = ProjectAddStepOne.step_name
1903 >>> request = LaunchpadTestRequest(form=form, method='POST')
1904@@ -63,10 +61,12 @@
1905
1906 >>> from lp.registry.browser.product import ProjectAddStepTwo
1907 >>> form = {
1908- ... 'displayname': 'Snowdog',
1909- ... 'name': 'snowdog',
1910- ... 'title': 'The Snowdog',
1911- ... 'summary': 'By-tor and the Snowdog',
1912+ ... 'field.actions.continue': 'Continue',
1913+ ... 'field.__visited_steps__': ProjectAddStepTwo.step_name,
1914+ ... 'field.displayname': 'Snowdog',
1915+ ... 'field.name': 'snowdog',
1916+ ... 'field.title': 'The Snowdog',
1917+ ... 'field.summary': 'By-tor and the Snowdog',
1918 ... }
1919
1920 >>> request = LaunchpadTestRequest(form=form, method='POST')
1921@@ -90,7 +90,7 @@
1922 existing projects for possible matches. By tweaking the project summary, we
1923 can see that there are search results available.
1924
1925- >>> form['summary'] = 'My Snowdog ate your Firefox'
1926+ >>> form['field.summary'] = 'My Snowdog ate your Firefox'
1927
1928 >>> request = LaunchpadTestRequest(form=form, method='POST')
1929 >>> view = ProjectAddStepTwo(product_set, request)
1930@@ -229,9 +229,9 @@
1931 questions.
1932 <BLANKLINE>
1933 Sometimes new projects are licensed as 'Other/Open Source' because the
1934- licensing decisions have not yet been made. If that is your situation we u=
1935- rge
1936- you to update the licensing in Launchpad as soon as you make that choice.
1937+ licensing decisions have not yet been made. If that is your situation
1938+ we urge you to update the licensing in Launchpad as soon as you make
1939+ that choice.
1940 <BLANKLINE>
1941 If the license for your project needs to be corrected you can do so by
1942 following the 'Change Details' link on your project's overview page.
1943
1944=== modified file 'lib/lp/registry/browser/tests/sourcepackage-views.txt'
1945--- lib/lp/registry/browser/tests/sourcepackage-views.txt 2010-05-13 18:55:10 +0000
1946+++ lib/lp/registry/browser/tests/sourcepackage-views.txt 2010-08-12 22:16:35 +0000
1947@@ -119,6 +119,7 @@
1948 empty.
1949
1950 >>> form = {
1951+ ... 'field.__visited_steps__': 'sourcepackage_change_upstream_step1',
1952 ... 'field.product': '',
1953 ... 'field.actions.continue': 'Continue',
1954 ... }
1955@@ -133,12 +134,18 @@
1956 but there is no notification message that the upstream link was updated.
1957
1958 >>> form = {
1959- ... 'field.productseries': 'bonkers/crazy',
1960- ... 'field.actions.change': 'Change',
1961+ ... 'field.__visited_steps__': 'sourcepackage_change_upstream_step2',
1962+ ... 'field.product': 'bonkers',
1963+ ... 'field.productseries': 'crazy',
1964+ ... 'field.actions.continue': 'Continue',
1965 ... }
1966 >>> view = create_initialized_view(
1967 ... package, name='+edit-packaging', form=form,
1968 ... principal=product.owner)
1969+ >>> print view.view
1970+ <...SourcePackageChangeUpstreamStepTwo object...>
1971+ >>> print view.view.next_url
1972+ http://launchpad.dev/youbuntu/busy/+source/bonkers
1973 >>> view.view.errors
1974 []
1975
1976@@ -199,6 +206,7 @@
1977 Registered upstream project:
1978 Lernid
1979 Choose another upstream project
1980+ Register the upstream project
1981
1982 The form does not steal focus because it is not the primary purpose of the
1983 page.
1984@@ -229,6 +237,7 @@
1985 Lernid...
1986 Lernid Dev...
1987 Choose another upstream project
1988+ Register the upstream project
1989
1990 Choosing the "Choose another upstream project" option redirects the user
1991 to the +edit-packaging page where the user can search for a project.
1992@@ -259,7 +268,8 @@
1993 ... name='stinkyseries', product=product)
1994 >>> distroseries = factory.makeDistroRelease(name='wonky',
1995 ... distribution=distribution)
1996- >>> sourcepackagename = factory.makeSourcePackageName(name='stinkypackage')
1997+ >>> sourcepackagename = factory.makeSourcePackageName(
1998+ ... name='stinkypackage')
1999 >>> package = factory.makeSourcePackage(
2000 ... sourcepackagename=sourcepackagename, distroseries=distroseries)
2001
2002@@ -360,3 +370,4 @@
2003 match for this source package. Can you help us find one?
2004 Registered upstream project:
2005 Choose another upstream project
2006+ Register the upstream project
2007
2008=== added file 'lib/lp/registry/browser/tests/test_sourcepackage_views.py'
2009--- lib/lp/registry/browser/tests/test_sourcepackage_views.py 1970-01-01 00:00:00 +0000
2010+++ lib/lp/registry/browser/tests/test_sourcepackage_views.py 2010-08-12 22:16:35 +0000
2011@@ -0,0 +1,146 @@
2012+# Copyright 2010 Canonical Ltd. This software is licensed under the
2013+# GNU Affero General Public License version 3 (see the file LICENSE).
2014+
2015+"""Tests for SourcePackage view code."""
2016+
2017+__metaclass__ = type
2018+
2019+import cgi
2020+import urllib
2021+
2022+from zope.component import getUtility
2023+from zope.interface import implements
2024+
2025+from canonical.testing import DatabaseFunctionalLayer
2026+
2027+
2028+from lp.registry.browser.sourcepackage import get_register_upstream_url
2029+from lp.registry.interfaces.distribution import IDistribution
2030+from lp.registry.interfaces.distroseries import (
2031+ IDistroSeries, IDistroSeriesSet)
2032+from lp.registry.interfaces.sourcepackage import ISourcePackage
2033+from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
2034+from lp.testing import TestCaseWithFactory
2035+
2036+
2037+class TestSourcePackageViewHelpers(TestCaseWithFactory):
2038+ """Tests for SourcePackage view helper functions."""
2039+
2040+ layer = DatabaseFunctionalLayer
2041+
2042+ def test_get_register_upstream_url_displayname(self):
2043+ distroseries = self.factory.makeDistroRelease(
2044+ distribution=self.factory.makeDistribution(name='zoobuntu'),
2045+ name='walrus')
2046+ source_package = self.factory.makeSourcePackage(
2047+ distroseries=distroseries,
2048+ sourcepackagename='python-super-package')
2049+ url = get_register_upstream_url(source_package)
2050+ expected_base = '/projects/+new'
2051+ expected_params = [
2052+ ('_return_url',
2053+ 'http://launchpad.dev/zoobuntu/walrus/'
2054+ '+source/python-super-package'),
2055+ ('field.__visited_steps__', 'projectaddstep1'),
2056+ ('field.actions.continue', 'Continue'),
2057+ # The sourcepackagename 'python-super-package' is split on
2058+ # the hyphens, and each word is capitalized.
2059+ ('field.displayname', 'Python Super Package'),
2060+ ('field.distroseries', 'zoobuntu/walrus'),
2061+ ('field.name', 'python-super-package'),
2062+ # The summary is missing, since the source package doesn't
2063+ # have a binary package release, and parse_qsl() excludes
2064+ # empty params.
2065+ ('field.source_package_name', 'python-super-package'),
2066+ ('field.title', 'Python Super Package'),
2067+ ]
2068+ base, query = urllib.splitquery(url)
2069+ params = cgi.parse_qsl(query)
2070+ self.assertEqual((expected_base, expected_params),
2071+ (base, params))
2072+
2073+ def test_get_register_upstream_url_summary(self):
2074+ test_publisher = SoyuzTestPublisher()
2075+ test_data = test_publisher.makeSourcePackageWithBinaryPackageRelease()
2076+ source_package_name = (
2077+ test_data['source_package'].sourcepackagename.name)
2078+ distroseries_id = test_data['distroseries'].id
2079+ test_publisher.updateDistroSeriesPackageCache(
2080+ test_data['distroseries'])
2081+
2082+ # updateDistroSeriesPackageCache reconnects the db, so the
2083+ # objects need to be reloaded.
2084+ distroseries = getUtility(IDistroSeriesSet).get(distroseries_id)
2085+ source_package = distroseries.getSourcePackage(source_package_name)
2086+ url = get_register_upstream_url(source_package)
2087+ expected_base = '/projects/+new'
2088+ expected_params = [
2089+ ('_return_url',
2090+ 'http://launchpad.dev/youbuntu/busy/+source/bonkers'),
2091+ ('field.__visited_steps__', 'projectaddstep1'),
2092+ ('field.actions.continue', 'Continue'),
2093+ ('field.displayname', 'Bonkers'),
2094+ ('field.distroseries', 'youbuntu/busy'),
2095+ ('field.name', 'bonkers'),
2096+ ('field.source_package_name', 'bonkers'),
2097+ ('field.summary', 'summary for flubber-bin\n'
2098+ + 'summary for flubber-lib'),
2099+ ('field.title', 'Bonkers'),
2100+ ]
2101+ base, query = urllib.splitquery(url)
2102+ params = cgi.parse_qsl(query)
2103+ self.assertEqual((expected_base, expected_params),
2104+ (base, params))
2105+
2106+ def test_get_register_upstream_url_summary_duplicates(self):
2107+
2108+ class Faker:
2109+ # Fakes attributes easily.
2110+ def __init__(self, **kw):
2111+ self.__dict__.update(kw)
2112+
2113+ class FakeSourcePackage(Faker):
2114+ # Interface necessary for canonical_url() call in
2115+ # get_register_upstream_url().
2116+ implements(ISourcePackage)
2117+
2118+ class FakeDistroSeries(Faker):
2119+ implements(IDistroSeries)
2120+
2121+ class FakeDistribution(Faker):
2122+ implements(IDistribution)
2123+
2124+ releases = Faker(sample_binary_packages=[
2125+ Faker(summary='summary for foo'),
2126+ Faker(summary='summary for bar'),
2127+ Faker(summary='summary for baz'),
2128+ Faker(summary='summary for baz'),
2129+ ])
2130+ source_package = FakeSourcePackage(
2131+ name='foo',
2132+ sourcepackagename=Faker(name='foo'),
2133+ distroseries=FakeDistroSeries(
2134+ name='walrus',
2135+ distribution=FakeDistribution(name='zoobuntu')),
2136+ releases=[releases])
2137+
2138+ url = get_register_upstream_url(source_package)
2139+ expected_base = '/projects/+new'
2140+ expected_params = [
2141+ ('_return_url',
2142+ 'http://launchpad.dev/zoobuntu/walrus/+source/foo'),
2143+ ('field.__visited_steps__', 'projectaddstep1'),
2144+ ('field.actions.continue', 'Continue'),
2145+ ('field.displayname', 'Foo'),
2146+ ('field.distroseries', 'zoobuntu/walrus'),
2147+ ('field.name', 'foo'),
2148+ ('field.source_package_name', 'foo'),
2149+ ('field.summary', 'summary for bar\n'
2150+ + 'summary for baz\n'
2151+ + 'summary for foo'),
2152+ ('field.title', 'Foo'),
2153+ ]
2154+ base, query = urllib.splitquery(url)
2155+ params = cgi.parse_qsl(query)
2156+ self.assertEqual((expected_base, expected_params),
2157+ (base, params))
2158
2159=== modified file 'lib/lp/registry/model/sourcepackage.py'
2160--- lib/lp/registry/model/sourcepackage.py 2010-08-02 21:38:00 +0000
2161+++ lib/lp/registry/model/sourcepackage.py 2010-08-12 22:16:35 +0000
2162@@ -39,7 +39,6 @@
2163 from lp.registry.model.packaging import Packaging
2164 from lp.translations.model.potemplate import (
2165 HasTranslationTemplatesMixin,
2166- POTemplate,
2167 TranslationTemplatesCollection)
2168 from canonical.launchpad.interfaces.lpstorm import IStore
2169 from lp.soyuz.model.publishing import (
2170@@ -52,7 +51,6 @@
2171 SourcePackageRelease)
2172 from lp.translations.model.translationimportqueue import (
2173 HasTranslationImportsMixin)
2174-from canonical.launchpad.helpers import shortlist
2175 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
2176 from lp.registry.interfaces.packaging import PackagingType
2177 from lp.registry.interfaces.distribution import NoPartnerArchive
2178
2179=== modified file 'lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt'
2180--- lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2010-05-18 17:05:29 +0000
2181+++ lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt 2010-08-12 22:16:35 +0000
2182@@ -1,4 +1,15 @@
2183-= Packaging =
2184+Packaging
2185+=========
2186+
2187+Create test data.
2188+
2189+ >>> from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
2190+ >>> test_publisher = SoyuzTestPublisher()
2191+ >>> login('admin@canonical.com')
2192+ >>> test_data = test_publisher.makeSourcePackageWithBinaryPackageRelease()
2193+ >>> test_publisher.updateDistroSeriesPackageCache(
2194+ ... test_data['distroseries'])
2195+ >>> logout()
2196
2197 No Privileges Person visit the distroseries upstream links page for Hoary
2198 and sees that pmount is not linked.
2199@@ -20,6 +31,7 @@
2200 match for this source package. Can you help us find one?
2201 Registered upstream project:
2202 Choose another upstream project
2203+ Register the upstream project
2204
2205 No Privileges Person knows that the pmount package comes from the thunderbird
2206 project. He sets the upstream packaging link and sees that it is set.
2207@@ -58,3 +70,93 @@
2208 ... user_browser.contents, 'packages_list'))
2209 The Hoary Hedgehog Release (active development) ...
2210 0.1-2 release (main) ... weeks ago
2211+
2212+Register a project from a source package
2213+----------------------------------------
2214+
2215+If an upstream project doesn't already exist in Launchpad, it can
2216+be registered with data from the source package prefilling the first
2217+step of the multistep form.
2218+
2219+ >>> user_browser.open(
2220+ ... 'http://launchpad.dev/youbuntu/busy/+source/bonkers')
2221+ >>> user_browser.getControl(
2222+ ... 'Register the upstream project').selected = True
2223+ >>> user_browser.getControl("Link to Upstream Project").click()
2224+ >>> print user_browser.url.replace('&', '\n&')
2225+ http://launchpad.dev/projects/+new?_return_url=http...%2Bsource%2Fbonkers
2226+ &field.__visited_steps__=projectaddstep1
2227+ &field.actions.continue=Continue
2228+ &field.displayname=Bonkers
2229+ &field.distroseries=youbuntu%2Fbusy
2230+ &field.name=bonkers
2231+ &field.source_package_name=bonkers
2232+ &field.summary=summary+for+flubber-bin%0Asummary+for+flubber-lib
2233+ &field.title=Bonkers
2234+ >>> print user_browser.getControl(name='field.name').value
2235+ bonkers
2236+ >>> print user_browser.getControl(name='field.displayname').value
2237+ Bonkers
2238+ >>> print user_browser.getControl(name='field.title').value
2239+ Bonkers
2240+ >>> print user_browser.getControl(name='field.summary').value
2241+ summary for flubber-bin
2242+ summary for flubber-lib
2243+ >>> print extract_text(
2244+ ... find_tag_by_id(user_browser.contents, 'step-title'))
2245+ Step 2 (of 2): Check for duplicate projects
2246+
2247+If the user selects "Choose another upstream project" and then finds out
2248+that the project doesn't exist, there is a also a link on the
2249++edit-packaging page to register the project.
2250+
2251+ >>> user_browser.open(
2252+ ... 'http://launchpad.dev/youbuntu/busy/+source/bonkers/')
2253+ >>> user_browser.getControl(
2254+ ... 'Choose another upstream project').selected = True
2255+ >>> user_browser.getControl("Link to Upstream Project").click()
2256+ >>> print user_browser.url
2257+ http://launchpad.dev/youbuntu/busy/+source/bonkers/+edit-packaging
2258+
2259+ >>> user_browser.getLink("Register the upstream project").click()
2260+ >>> print user_browser.url.replace('&', '\n&')
2261+ http://launchpad.dev/projects/+new?_return_url=http...%2Bsource%2Fbonkers
2262+ &field.__visited_steps__=projectaddstep1
2263+ &field.actions.continue=Continue
2264+ &field.displayname=Bonkers
2265+ &field.distroseries=youbuntu%2Fbusy
2266+ &field.name=bonkers
2267+ &field.source_package_name=bonkers
2268+ &field.summary=summary+for+flubber-bin%0Asummary+for+flubber-lib
2269+ &field.title=Bonkers
2270+ >>> print user_browser.getControl(name='field.name').value
2271+ bonkers
2272+ >>> print user_browser.getControl(name='field.displayname').value
2273+ Bonkers
2274+ >>> print user_browser.getControl(name='field.title').value
2275+ Bonkers
2276+ >>> print user_browser.getControl(name='field.summary').value
2277+ summary for flubber-bin
2278+ summary for flubber-lib
2279+ >>> print extract_text(
2280+ ... find_tag_by_id(user_browser.contents, 'step-title'))
2281+ Step 2 (of 2): Check for duplicate projects
2282+
2283+If there are no problems with the prefilled data, then the license
2284+just needs to be selected. The user will then be redirected back
2285+to the source package page and an informational message will be displayed.
2286+
2287+ >>> user_browser.getControl(name='field.licenses').value = ['BSD']
2288+ >>> user_browser.getControl(
2289+ ... "Complete registration and link to bonkers package").click()
2290+ >>> print user_browser.url
2291+ http://launchpad.dev/youbuntu/busy/+source/bonkers
2292+ >>> for tag in find_tags_by_class(
2293+ ... user_browser.contents, 'informational message'):
2294+ ... print extract_text(tag)
2295+ Linked Bonkers project to bonkers source package.
2296+ >>> print extract_text(
2297+ ... find_tag_by_id(user_browser.contents, 'upstreams'))
2298+ Bonkers &rArr; trunk
2299+ Change upstream link
2300+ Remove upstream link...
2301
2302=== modified file 'lib/lp/registry/templates/product-new.pt'
2303--- lib/lp/registry/templates/product-new.pt 2010-05-12 19:06:17 +0000
2304+++ lib/lp/registry/templates/product-new.pt 2010-08-12 22:16:35 +0000
2305@@ -299,8 +299,8 @@
2306 <img src="/@@/info" />
2307 There are similar projects already registered in Launchpad.
2308 Is project
2309- <strong><tal:displayname tal:replace="view/request/displayname" />
2310- (<tal:name tal:replace="view/request/name" />)</strong>
2311+ <strong><tal:displayname tal:replace="view/request/field.displayname" />
2312+ (<tal:name tal:replace="view/request/field.name" />)</strong>
2313 one of these?
2314 </div>
2315
2316@@ -326,8 +326,8 @@
2317 tal:condition="view/search_results_count"
2318 >Registration details</h3>
2319 Select the licenses for project
2320- <strong><tal:displayname tal:replace="view/request/displayname" />
2321- (<tal:name tal:replace="view/request/name" />)</strong>
2322+ <strong><tal:displayname tal:replace="view/request/field.displayname" />
2323+ (<tal:name tal:replace="view/request/field.name" />)</strong>
2324 and complete the registration. You may also update the project's
2325 title and summary.
2326 </div>
2327
2328=== modified file 'lib/lp/registry/templates/sourcepackage-edit-packaging.pt'
2329--- lib/lp/registry/templates/sourcepackage-edit-packaging.pt 2010-02-16 17:37:36 +0000
2330+++ lib/lp/registry/templates/sourcepackage-edit-packaging.pt 2010-08-12 22:16:35 +0000
2331@@ -27,6 +27,26 @@
2332 If you need a new series created, contact the owner of
2333 <a tal:content="structure view/product/fmt:link"/>.
2334 </div>
2335+
2336+ <div metal:fill-slot="buttons">
2337+ <input tal:repeat="action view/actions"
2338+ tal:replace="structure action/render"
2339+ />
2340+ &nbsp;or&nbsp;
2341+ <tal:comment condition="nothing">
2342+ This template is for a multistep view, and only the first
2343+ step provides the register_upstream_url.
2344+ </tal:comment>
2345+ <a id="register-upstream-link"
2346+ tal:condition="view/register_upstream_url | nothing"
2347+ tal:attributes="href view/register_upstream_url">
2348+ Register the upstream project
2349+ </a>
2350+ <tal:has-cancel-link condition="view/cancel_url">
2351+ &nbsp;or&nbsp;
2352+ <a tal:attributes="href view/cancel_url">Cancel</a>
2353+ </tal:has-cancel-link>
2354+ </div>
2355 </div>
2356
2357 </div>
2358
2359=== modified file 'lib/lp/soyuz/scripts/soyuz_process_upload.py'
2360--- lib/lp/soyuz/scripts/soyuz_process_upload.py 2010-05-04 15:38:08 +0000
2361+++ lib/lp/soyuz/scripts/soyuz_process_upload.py 2010-08-12 22:16:35 +0000
2362@@ -8,6 +8,7 @@
2363
2364 import os
2365
2366+from lp.archiveuploader.uploadpolicy import findPolicyByOptions
2367 from lp.archiveuploader.uploadprocessor import UploadProcessor
2368 from lp.services.scripts.base import (
2369 LaunchpadCronScript, LaunchpadScriptFailure)
2370@@ -74,8 +75,13 @@
2371 "%s is not a directory" % self.options.base_fsroot)
2372
2373 self.logger.debug("Initialising connection.")
2374- UploadProcessor(
2375- self.options, self.txn, self.logger).processUploadQueue()
2376+ def getPolicy(distro):
2377+ self.options.distro = distro.name
2378+ return findPolicyByOptions(self.options)
2379+ processor = UploadProcessor(self.options.base_fsroot,
2380+ self.options.dryrun, self.options.nomails, self.options.keep,
2381+ getPolicy, self.txn, self.logger)
2382+ processor.processUploadQueue(self.options.leafname)
2383
2384 @property
2385 def lockfilename(self):
2386
2387=== modified file 'lib/lp/soyuz/tests/test_publishing.py'
2388--- lib/lp/soyuz/tests/test_publishing.py 2010-08-07 00:36:52 +0000
2389+++ lib/lp/soyuz/tests/test_publishing.py 2010-08-12 22:16:35 +0000
2390@@ -10,6 +10,7 @@
2391 from StringIO import StringIO
2392 import tempfile
2393
2394+import transaction
2395 import pytz
2396 from zope.component import getUtility
2397 from zope.security.proxy import removeSecurityProxy
2398@@ -18,13 +19,16 @@
2399 from canonical.database.constants import UTC_NOW
2400 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
2401 from canonical.launchpad.webapp.errorlog import ErrorReportingUtility
2402+from canonical.testing.layers import reconnect_stores
2403 from canonical.testing import (
2404 DatabaseFunctionalLayer, LaunchpadZopelessLayer)
2405+
2406 from lp.app.errors import NotFoundError
2407 from lp.archivepublisher.config import Config
2408 from lp.archivepublisher.diskpool import DiskPool
2409 from lp.buildmaster.interfaces.buildbase import BuildStatus
2410 from lp.registry.interfaces.distribution import IDistributionSet
2411+from lp.registry.interfaces.distroseries import IDistroSeriesSet
2412 from lp.registry.interfaces.person import IPersonSet
2413 from lp.registry.interfaces.pocket import PackagePublishingPocket
2414 from lp.registry.interfaces.sourcepackage import SourcePackageUrgency
2415@@ -476,6 +480,67 @@
2416
2417 return source
2418
2419+ def makeSourcePackageWithBinaryPackageRelease(self):
2420+ """Make test data for SourcePackage.summary.
2421+
2422+ The distroseries that is returned from this method needs to be
2423+ passed into updateDistroseriesPackageCache() so that
2424+ SourcePackage.summary can be populated.
2425+ """
2426+ distribution = self.factory.makeDistribution(
2427+ name='youbuntu', displayname='Youbuntu')
2428+ distroseries = self.factory.makeDistroRelease(name='busy',
2429+ distribution=distribution)
2430+ source_package_name = self.factory.makeSourcePackageName(
2431+ name='bonkers')
2432+ source_package = self.factory.makeSourcePackage(
2433+ sourcepackagename=source_package_name,
2434+ distroseries=distroseries)
2435+ component = self.factory.makeComponent('multiverse')
2436+ das = self.factory.makeDistroArchSeries(
2437+ distroseries=distroseries)
2438+ spph = self.factory.makeSourcePackagePublishingHistory(
2439+ sourcepackagename=source_package_name,
2440+ distroseries=distroseries,
2441+ component=component)
2442+
2443+ for name in ('flubber-bin', 'flubber-lib'):
2444+ binary_package_name = self.factory.makeBinaryPackageName(name)
2445+ build = self.factory.makeBinaryPackageBuild(
2446+ source_package_release=spph.sourcepackagerelease,
2447+ archive=self.factory.makeArchive(),
2448+ distroarchseries=das)
2449+ bpr = self.factory.makeBinaryPackageRelease(
2450+ binarypackagename=binary_package_name,
2451+ summary='summary for %s' % name,
2452+ build=build, component=component)
2453+ bpph = self.factory.makeBinaryPackagePublishingHistory(
2454+ binarypackagerelease=bpr, distroarchseries=das)
2455+ return dict(
2456+ distroseries=distroseries,
2457+ source_package=source_package)
2458+
2459+ def updateDistroSeriesPackageCache(
2460+ self, distroseries, restore_db_connection='launchpad'):
2461+ # XXX: EdwinGrubbs 2010-08-04 bug=396419. Currently there is no
2462+ # test api call to switchDbUser that works for non-zopeless layers.
2463+ # When bug 396419 is fixed, we can instead use
2464+ # DatabaseLayer.switchDbUser() instead of reconnect_stores()
2465+ transaction.commit()
2466+ reconnect_stores(config.statistician.dbuser)
2467+ distroseries = getUtility(IDistroSeriesSet).get(distroseries.id)
2468+
2469+ class TestLogger:
2470+ # Silent logger.
2471+ def debug(self, msg):
2472+ pass
2473+ distroseries.updateCompletePackageCache(
2474+ archive=distroseries.distribution.main_archive,
2475+ ztm=transaction,
2476+ log=TestLogger())
2477+ transaction.commit()
2478+ reconnect_stores(restore_db_connection)
2479+
2480
2481 class TestNativePublishingBase(TestCaseWithFactory, SoyuzTestPublisher):
2482 layer = LaunchpadZopelessLayer
2483
2484=== modified file 'lib/lp/vostok/browser/configure.zcml'
2485--- lib/lp/vostok/browser/configure.zcml 2010-08-12 22:16:33 +0000
2486+++ lib/lp/vostok/browser/configure.zcml 2010-08-12 22:16:35 +0000
2487@@ -5,14 +5,6 @@
2488 xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
2489 i18n_domain="launchpad">
2490
2491- <browser:page
2492- for="*"
2493- name="main_template"
2494- template="../templates/main-template.pt"
2495- permission="zope.Public"
2496- layer="lp.vostok.publisher.VostokLayer"
2497- />
2498-
2499 <browser:defaultView
2500 for="lp.vostok.publisher.IVostokRoot"
2501 name="+index"
2502@@ -32,4 +24,6 @@
2503 classes="VostokRootNavigation"
2504 />
2505
2506+ <adapter factory="lp.vostok.browser.root.VostokLayerToMainTemplateAdapter" />
2507+
2508 </configure>
2509
2510=== modified file 'lib/lp/vostok/browser/root.py'
2511--- lib/lp/vostok/browser/root.py 2010-07-30 03:50:17 +0000
2512+++ lib/lp/vostok/browser/root.py 2010-08-12 22:16:35 +0000
2513@@ -6,19 +6,37 @@
2514 __metaclass__ = type
2515 __all__ = [
2516 'VostokRootView',
2517+ 'VostokLayerToMainTemplateAdapter',
2518 ]
2519
2520-from zope.component import getUtility
2521+import os
2522+
2523+from zope.component import adapts, getUtility
2524+from zope.interface import implements
2525
2526 from canonical.launchpad.webapp import LaunchpadView
2527+from canonical.launchpad.webapp.tales import IMainTemplateFile
2528
2529 from lp.registry.interfaces.distribution import IDistributionSet
2530
2531+from lp.vostok.publisher import VostokLayer
2532+
2533
2534 class VostokRootView(LaunchpadView):
2535 """The view for the Vostok root object."""
2536
2537+ page_title = 'Vostok'
2538+
2539 @property
2540 def distributions(self):
2541 """An iterable of all registered distributions."""
2542 return getUtility(IDistributionSet)
2543+
2544+
2545+class VostokLayerToMainTemplateAdapter:
2546+ adapts(VostokLayer)
2547+ implements(IMainTemplateFile)
2548+
2549+ def __init__(self, context):
2550+ here = os.path.dirname(os.path.realpath(__file__))
2551+ self.path = os.path.join(here, '../templates/main-template.pt')
2552
2553=== renamed file 'lib/lp/vostok/browser/tests/test_main_template.py' => 'lib/lp/vostok/browser/tests/test_base_template.py'
2554--- lib/lp/vostok/browser/tests/test_main_template.py 2010-07-29 04:38:37 +0000
2555+++ lib/lp/vostok/browser/tests/test_base_template.py 2010-08-12 22:16:35 +0000
2556@@ -1,33 +1,31 @@
2557 # Copyright 2010 Canonical Ltd. This software is licensed under the
2558 # GNU Affero General Public License version 3 (see the file LICENSE).
2559
2560-"""Tests for the vostok 'main_template'."""
2561+"""Tests for the vostok 'view/macro:page' TALES adapter."""
2562
2563 __metaclass__ = type
2564
2565-import unittest
2566-
2567 from zope.component import getMultiAdapter
2568+from zope.traversing.interfaces import IPathAdapter
2569
2570 from canonical.testing.layers import FunctionalLayer
2571
2572 from lp.testing import TestCase
2573 from lp.vostok.browser.tests.request import VostokTestRequest
2574-
2575-
2576-class TestMainTemplate(TestCase):
2577- """Tests for our main template."""
2578+from lp.vostok.publisher import VostokRoot
2579+
2580+
2581+class TestPageMacroDispatcher(TestCase):
2582
2583 layer = FunctionalLayer
2584
2585- def test_main_template_defines_master_macro(self):
2586- # The main template, which is registered as a view for any object at
2587- # all when in the VostokLayer, defines a 'master' macro.
2588- adapter = getMultiAdapter(
2589- (None, VostokTestRequest()), name='main_template')
2590- self.assertEqual(['master'], adapter.index.macros.keys())
2591- self.assertIn('lp/vostok', adapter.index.filename)
2592-
2593-
2594-def test_suite():
2595- return unittest.TestLoader().loadTestsFromName(__name__)
2596+ def test_base_template(self):
2597+ # For requests on the vostok vhost (i.e. IVostokLayer requests), the
2598+ # base template used is the vostok one.
2599+ root_view = getMultiAdapter(
2600+ (VostokRoot(), VostokTestRequest()), name='+index')
2601+ adapter = getMultiAdapter([root_view], IPathAdapter, name='macro')
2602+ self.assertIn('lp/vostok', adapter.base.filename)
2603+ # The vostok base template defines a 'master' macro as the adapter
2604+ # expects.
2605+ self.assertIn('master', adapter.base.macros.keys())
2606
2607=== modified file 'lib/lp/vostok/browser/tests/test_root.py'
2608--- lib/lp/vostok/browser/tests/test_root.py 2010-07-30 03:50:17 +0000
2609+++ lib/lp/vostok/browser/tests/test_root.py 2010-08-12 22:16:35 +0000
2610@@ -5,12 +5,14 @@
2611
2612 __metaclass__ = type
2613
2614+import os
2615 import unittest
2616
2617 from zope.app.publisher.browser import getDefaultViewName
2618
2619 from canonical.testing.layers import DatabaseFunctionalLayer, FunctionalLayer
2620 from canonical.launchpad.testing.pages import extract_text, find_tag_by_id
2621+from canonical.launchpad.webapp.tales import IMainTemplateFile
2622
2623 from lp.testing import TestCase, TestCaseWithFactory
2624 from lp.testing.views import create_initialized_view
2625@@ -19,7 +21,6 @@
2626 from lp.vostok.publisher import VostokLayer, VostokRoot
2627
2628
2629-
2630 class TestRootRegistrations(TestCase):
2631 """Test the registration of views for `VostokRoot`."""
2632
2633@@ -72,5 +73,15 @@
2634 self.assertEqual(distro.displayname, extract_text(link))
2635
2636
2637+class TestVostokLayerToMainTemplateAdapter(TestCase):
2638+
2639+ layer = FunctionalLayer
2640+
2641+ def test_path(self):
2642+ main_template_path = IMainTemplateFile(VostokTestRequest()).path
2643+ self.assertIn('lp/vostok', main_template_path)
2644+ self.assertTrue(os.path.isfile(main_template_path))
2645+
2646+
2647 def test_suite():
2648 return unittest.TestLoader().loadTestsFromName(__name__)
2649
2650=== modified file 'lib/lp/vostok/templates/main-template.pt'
2651--- lib/lp/vostok/templates/main-template.pt 2010-07-30 04:38:05 +0000
2652+++ lib/lp/vostok/templates/main-template.pt 2010-08-12 22:16:35 +0000
2653@@ -3,13 +3,16 @@
2654 xmlns:tal="http://xml.zope.org/namespaces/tal"
2655 define-macro="master"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2656 <html xmlns="http://www.w3.org/1999/xhtml">
2657+
2658 <head>
2659- <!-- Obviously, we'll need to do something better here. -->
2660- <title>Vostok page</title>
2661+
2662+ <title tal:content="view/fmt:pagetitle">Page Title</title>
2663+
2664 </head>
2665+
2666 <body>
2667 <h1 metal:define-slot="heading" />
2668- <div metal:define-slot="content" />
2669+ <div metal:define-slot="main" />
2670 </body>
2671 </html>
2672 </metal:page>
2673
2674=== modified file 'lib/lp/vostok/templates/root.pt'
2675--- lib/lp/vostok/templates/root.pt 2010-07-15 10:11:03 +0000
2676+++ lib/lp/vostok/templates/root.pt 2010-08-12 22:16:35 +0000
2677@@ -3,13 +3,13 @@
2678 xmlns:tal="http://xml.zope.org/namespaces/tal"
2679 xmlns:metal="http://xml.zope.org/namespaces/metal"
2680 xmlns:i18n="http://xml.zope.org/namespaces/i18n"
2681- metal:use-macro="context/@@main_template/master"
2682+ metal:use-macro="view/macro:page/main_only"
2683 i18n:domain="vostok">
2684 <body>
2685 <tal:heading metal:fill-slot="heading">
2686 <h1>Vostok</h1>
2687 </tal:heading>
2688- <tal:content metal:fill-slot="content">
2689+ <tal:content metal:fill-slot="main">
2690 <ul id="distro-list">
2691 <tal:loop tal:repeat="distro view/distributions">
2692 <li tal:content="structure distro/fmt:link" />
2693
2694=== modified file 'utilities/sourcedeps.conf'
2695--- utilities/sourcedeps.conf 2010-08-03 14:59:22 +0000
2696+++ utilities/sourcedeps.conf 2010-08-12 22:16:35 +0000
2697@@ -12,5 +12,6 @@
2698 pygettextpo lp:~launchpad-pqm/pygettextpo/trunk;revno=24
2699 pygpgme lp:~launchpad-pqm/pygpgme/devel;revno=49
2700 subvertpy lp:~launchpad-pqm/subvertpy/trunk;revno=2042
2701+python-debian lp:~launchpad-pqm/python-debian/devel;revno=185
2702 testresources lp:~launchpad-pqm/testresources/dev;revno=16
2703 shipit lp:~launchpad-pqm/shipit/trunk;revno=8909 optional

Subscribers

People subscribed via source and target branches

to status/vote changes: