Merge lp:~leonardr/launchpad/multiversion-integration into lp:launchpad/db-devel

Proposed by Leonard Richardson
Status: Merged
Approved by: Eleanor Berger
Approved revision: not available
Merged at revision: not available
Proposed branch: lp:~leonardr/launchpad/multiversion-integration
Merge into: lp:launchpad/db-devel
Diff against target: 1348 lines (+301/-233) (has conflicts)
32 files modified
lib/canonical/launchpad/configure.zcml (+2/-0)
lib/canonical/launchpad/interfaces/launchpad.py (+2/-1)
lib/canonical/launchpad/javascript/bugs/bugtask-index.js (+1/-1)
lib/canonical/launchpad/javascript/client/client.js (+3/-3)
lib/canonical/launchpad/javascript/lp/comment.js (+1/-1)
lib/canonical/launchpad/pagetests/webservice/xx-service.txt (+43/-14)
lib/canonical/launchpad/rest/configuration.py (+4/-5)
lib/canonical/launchpad/testing/pages.py (+2/-0)
lib/canonical/launchpad/webapp/servers.py (+9/-3)
lib/canonical/launchpad/webapp/tests/__init__.py (+0/-27)
lib/canonical/launchpad/webapp/tests/test_dbpolicy.py (+3/-3)
lib/canonical/launchpad/webapp/tests/test_servers.py (+41/-25)
lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js (+15/-15)
lib/canonical/launchpad/zcml/webservice.zcml (+6/-1)
lib/canonical/lazr/doc/folder.txt (+13/-13)
lib/canonical/lazr/doc/menus.txt (+1/-1)
lib/canonical/widgets/lazrjs.py (+9/-4)
lib/lp/bugs/adapters/bug.py (+5/-2)
lib/lp/code/browser/configure.zcml (+6/-0)
lib/lp/code/browser/diff.py (+12/-4)
lib/lp/code/browser/tests/test_tales.py (+12/-1)
lib/lp/code/model/branch.py (+1/-1)
lib/lp/code/model/tests/test_branch.py (+1/-1)
lib/lp/code/stories/branches/xx-bug-branch-links.txt (+31/-5)
lib/lp/code/templates/branch-delete.pt (+1/-1)
lib/lp/registry/doc/sourcepackage.txt (+38/-61)
lib/lp/registry/model/sourcepackage.py (+9/-28)
lib/lp/testing/factory.py (+9/-0)
lib/lp/testopenid/browser/server.py (+3/-6)
lib/lp/testopenid/interfaces/server.py (+12/-0)
lib/lp/testopenid/testing/helpers.py (+2/-2)
versions.cfg (+4/-4)
Text conflict in lib/lp/testing/factory.py
To merge this branch: bzr merge lp:~leonardr/launchpad/multiversion-integration
Reviewer Review Type Date Requested Status
Eleanor Berger (community) code Approve
Review via email: mp+19346@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch depends on updated releases of httplib2, lazr.restful, lazr.restfulclient, and launchpadlib. The new launchpadlib release has not been finalized yet, but you can find the code here: https://code.edge.launchpad.net/~leonardr/launchpadlib/multiversion/+merge/19343

This branch integrates the multi-versioning lazr.restful code into Launchpad.

0. I change IWebServiceApplication so it subclasses IServiceRootResource; lazr.restful now requires the root resource to be registered as a utility, and the IWSA is already a utility.

1. I run grok on the canonical.launchpad.rest directory so that certain code will run. This is the code that looks in IWebServiceConfiguration.active_versions and generates a marker interface for each version.

2. I create a "1.0" web service between "beta" and "devel". To avoid massive changes to tests due to changed URLs, I change the LaunchpadWebServiceCaller so that it still makes requests to the 'beta' web service.

3. I changed Launchpad's test request object to take a version name as an argument, and stamp the request object with that version name and with the corresponding marker interface. lazr.restful takes care of this for normal HTTP requests, but test requests don't go through the normal traversal code.

4. I changed bugcomment_to_entry from a simple IEntry adapter to a multi-adapter that adapts an object and a version marker interface. (All IEntry lookups are now multi-adapter lookups.)

5. I added a test that proves Launchpad responds to all the versions. Since there are as yet no differences between the versions, that's all it tests.

Revision history for this message
Eleanor Berger (intellectronica) :
review: Approve (code)
Revision history for this message
Leonard Richardson (leonardr) wrote :

I'm afraid I need a follow-up review. I had some Javascript test failures because lazr.restful was generating URLs to the most recent version of the web service ("/api/devel/bugs/15"). Launchpad's client.js would look at these URLs, notice that there was no "/api/beta" at the beginning, and slap "/api/beta" on the beginning, creating the invalid URL "/api/beta/api/devel/bugs/15".

After consultation with flacoste, gary, and mars, we decided to make Launchpad start using the bleeding edge version of the web serice. flacoste says our test coverage is good enough that when we make a backwards-incompatible change to 'devel', some Windmill test will start failing and we'll know to change our usage. So this update to the branch changes all /api/beta stuff to use /api/devel.

The two advantages of using /api/devel in the future are that 1) we won't have to have "porting sessions" where we're about to deprecate version N so we have to port all of Launchpad to version N+1. It'll happen over time as we make the backwards-incompatible changes. 2) we won't have to keep changing the version name, because 'devel' will always refer to the bleeding edge.

I'll also be changing versions.txt to use lazr.restful 0.9.20, but I haven't actually created that version yet--the branch is still in review.

Revision history for this message
Leonard Richardson (leonardr) wrote :

Unfortunately I'm experiencing still more test failures. The work continues!

Revision history for this message
Leonard Richardson (leonardr) wrote :

OK, ready for another review. The changes:

1. The folder tests were creating a FakeRequest that wasn't associated with any particular version. This was croaking lazr.restful, which always needs to know the version of an incoming request.

2. The Javascript widgets were poking into the lazr.restful internals and looking at the annotations. The 'mutated_by' annotation was changed in the multiversion code to a dict of 'mutator_annotations'.

3. Changed Launchpad to use the new 0.9.21 release of lazr.restful, which fixes many other test failures.

Revision history for this message
Leonard Richardson (leonardr) wrote :

4. The new version of lazr.restful changed the implementation of get_current_browser_request. If there is no current request, it now returns None instead of crashing. However, there was a test in menus.txt that depended on the crash. Now the same code crashes, but at a later point (when it tries to access an attribute of the request that's actually None). Rather than change get_current_browser_request to crash, I changed the test to detect the later crash.

Revision history for this message
Leonard Richardson (leonardr) wrote :

http://pastebin.ubuntu.com/383025/

Hopefully this will be the last one. The other times I had to go back and do a new release of lazr.restful, and the changes in that new release broke some additional tests in Launchpad. This time I have only made changes to Launchpad, so this shouldn't happen again. (Though I should be making changes to lazr.restful eventually, it doesn't have to be now.)

The major failure here was in servers.py, the unit test of the web service traversal code. This was a subclass of DummyConfigurationTestCase, a class which sets up a small IWebServiceConfiguration object so that the lazr.restful traversal code won't choke.

In the new version of lazr.restful, a small IWebServiceConfiguration object is not good enough. You need an environment that's much more similar to a real web service.

Fortunately, I already ran into this problem with lazr.restful's unit tests, and created a test class that sets up a fake web service, WebServiceTestCase. In this branch, I replace DummyConfigurationTestCase with lazr.restful's WebServiceTestCase, and add a little more stuff (like a top-level collection) to the TestWebServiceRequestTraversal test class, so that traversal will work.

Since WebServiceTestCase has everything found in DummyConfigurationTest, I changed the only other test case that uses DummyConfigurationTest, and removed the DummyConfigurationTest class from Launchpad altogether.

However, WebServiceTestCase (along with IGenericCollection and everything else imported from lazr.restful.tests.test_webservice) is not packaged for import by other code. It was written for use with the unit tests in test_webservice.py, and now that it's being used more generally it should be moved to lazr.restful.testing.webservice.

I plan to move that code to lazr.restful.testing.webservice, but I don't want to do that as part of this branch. I'm working on another lazr.restful release that will immediately have to be integrated into Launchpad, and I can move the code in that release.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/configure.zcml'
2--- lib/canonical/launchpad/configure.zcml 2010-02-18 17:00:54 +0000
3+++ lib/canonical/launchpad/configure.zcml 2010-02-25 00:40:51 +0000
4@@ -8,6 +8,8 @@
5 xmlns:i18n="http://namespaces.zope.org/i18n"
6 i18n_domain="launchpad">
7
8+ <include package="grokcore.component" file="meta.zcml" />
9+
10 <include package="canonical.launchpad.webapp" />
11 <include package="canonical.launchpad.vocabularies" />
12 <include file="links.zcml" />
13
14=== modified file 'lib/canonical/launchpad/interfaces/launchpad.py'
15--- lib/canonical/launchpad/interfaces/launchpad.py 2010-01-20 19:33:29 +0000
16+++ lib/canonical/launchpad/interfaces/launchpad.py 2010-02-25 00:40:50 +0000
17@@ -14,6 +14,7 @@
18 from zope.schema import Bool, Choice, Int, TextLine
19 from persistent import IPersistent
20
21+from lazr.restful.interfaces import IServiceRootResource
22 from canonical.launchpad import _
23 from canonical.launchpad.fields import PublicPersonChoice
24 from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
25@@ -402,7 +403,7 @@
26 """Removes annotation at the given namespace."""
27
28
29-class IWebServiceApplication(ILaunchpadApplication):
30+class IWebServiceApplication(ILaunchpadApplication, IServiceRootResource):
31 """Launchpad web service application root."""
32
33
34
35=== modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js'
36--- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2010-01-29 16:00:43 +0000
37+++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2010-02-25 00:40:50 +0000
38@@ -1416,7 +1416,7 @@
39 // an API object. Once such a solution is available we should
40 // fix this.
41 milestone_content.one('.value').setAttribute(
42- 'href', new_value.replace('/api/beta', ''));
43+ 'href', new_value.replace('/api/devel', ''));
44 }
45 // Set the inline form control's value, so that submitting
46 // it won't override the value we just set.
47
48=== modified file 'lib/canonical/launchpad/javascript/client/client.js'
49--- lib/canonical/launchpad/javascript/client/client.js 2009-12-07 12:26:37 +0000
50+++ lib/canonical/launchpad/javascript/client/client.js 2010-02-25 00:40:50 +0000
51@@ -101,15 +101,15 @@
52 var host_start = uri.indexOf('//');
53 if (host_start != -1) {
54 var host_end = uri.indexOf('/', host_start+2);
55- // eg. "http://www.example.com/api/beta/foo";
56+ // eg. "http://www.example.com/api/devel/foo";
57 // Don't try to insert the service base into what was an
58 // absolute URL. So "http://www.example.com/foo" becomes "/foo"
59 return uri.substring(host_end, uri.length);
60 }
61
62- var base = "/api/beta";
63+ var base = "/api/devel";
64 if (uri.indexOf(base.substring(1, base.length)) === 0) {
65- // eg. "api/beta/foo"
66+ // eg. "api/devel/foo"
67 return '/' + uri;
68 }
69 if (uri.indexOf(base) !== 0) {
70
71=== modified file 'lib/canonical/launchpad/javascript/lp/comment.js'
72--- lib/canonical/launchpad/javascript/lp/comment.js 2010-01-15 02:35:41 +0000
73+++ lib/canonical/launchpad/javascript/lp/comment.js 2010-02-25 00:40:51 +0000
74@@ -317,7 +317,7 @@
75 var reply_link = LP.client.normalize_uri(e.target.get('href'));
76 var root_url = reply_link.substr(0,
77 reply_link.length - '+reply'.length);
78- var object_url = '/api/beta' + root_url;
79+ var object_url = '/api/devel' + root_url;
80 this.activateProgressUI('Loading...');
81 window.scrollTo(0, Y.one('#add-comment').getY());
82 this.lp_client.get(object_url, {
83
84=== modified file 'lib/canonical/launchpad/pagetests/webservice/xx-service.txt'
85--- lib/canonical/launchpad/pagetests/webservice/xx-service.txt 2009-12-16 16:30:00 +0000
86+++ lib/canonical/launchpad/pagetests/webservice/xx-service.txt 2010-02-25 00:40:51 +0000
87@@ -1,10 +1,37 @@
88-= Introduction =
89+************
90+Introduction
91+************
92
93 Some standard behavior is defined by the web service itself, not by
94 the individual resources.
95
96-
97-== No beta team redirection ==
98+Multiple versions
99+=================
100+
101+The Launchpad web service defines three versions: 'beta', '1.0', and
102+'devel'.
103+
104+ >>> def me_link_for_version(version):
105+ ... response = webservice.get("/", api_version=version)
106+ ... print response.jsonBody()['me_link']
107+
108+ >>> me_link_for_version('beta')
109+ http://api.launchpad.dev/beta/people/+me
110+
111+ >>> me_link_for_version('1.0')
112+ http://api.launchpad.dev/1.0/people/+me
113+
114+ >>> me_link_for_version('devel')
115+ http://api.launchpad.dev/devel/people/+me
116+
117+No other versions are available.
118+
119+ >>> print webservice.get("/", api_version="nosuchversion")
120+ HTTP/1.1 404 Not Found
121+ ...
122+
123+No beta team redirection
124+========================
125
126 Members of the beta team aren't redirected to the beta site on API call.
127
128@@ -46,17 +73,18 @@
129
130 >>> ignored = config.pop('beta_data')
131
132-
133-== Resources not exposed on the web service ==
134-
135-Soyuz build set (exposed on the web at builders) are not available on the
136+Resources not exposed on the web service
137+========================================
138+
139+Soyuz build set (exposed on the web at /builders) are not available on the
140 web service:
141
142 >>> print webservice.get("/builders")
143 HTTP/1.1 404 Not Found
144 ...
145
146-== Anonymous requests ==
147+Anonymous requests
148+==================
149
150 A properly signed web service request whose OAuth token key is empty
151 is treated as an anonymous request.
152@@ -128,7 +156,8 @@
153 (<Person at...>, 'displayname', 'launchpad.Edit')
154 ...
155
156-== API Requests to other hosts ==
157+API Requests to other hosts
158+===========================
159
160 JavaScript working with the API must deal with the browser's Same Origin
161 Policy - requests may only be made to the host that the page was loaded
162@@ -144,15 +173,15 @@
163 rather than the api host. The canonical_url() function also returns
164 links to the current host.
165
166-The ServiceRoot for http://bugs.launchpad.dev/api/beta/ is the same as a
167+The ServiceRoot for http://bugs.launchpad.dev/api/devel/ is the same as a
168 request to http://api.launchpad.net/beta/, but with the links pointing
169 to a different host.
170
171 >>> webservice.domain = 'bugs.launchpad.dev'
172 >>> root = webservice.get(
173- ... 'http://bugs.launchpad.dev/api/beta/').jsonBody()
174+ ... 'http://bugs.launchpad.dev/api/devel/').jsonBody()
175 >>> print root['people_collection_link']
176- http://bugs.launchpad.dev/api/beta/people
177+ http://bugs.launchpad.dev/api/devel/people
178
179 Requests on these hosts also honor the standard Launchpad authorization
180 scheme (and don't require OAuth).
181@@ -163,11 +192,11 @@
182 ... domain='bugs.launchpad.dev')
183 >>> sample_auth = 'Basic %s' % 'test@canonical.com:test'.encode('base64')
184 >>> print noauth_webservice.get(
185- ... 'http://bugs.launchpad.dev/api/beta/people/+me',
186+ ... 'http://bugs.launchpad.dev/api/devel/people/+me',
187 ... headers={'Authorization': sample_auth})
188 HTTP/1.1 303 See Other
189 ...
190- Location: http://bugs.launchpad.dev/api/beta/~name12...
191+ Location: http://bugs.launchpad.dev/api/devel/~name12...
192 ...
193
194 But the regular authentication still doesn't work on the normal API
195
196=== modified file 'lib/canonical/launchpad/rest/configuration.py'
197--- lib/canonical/launchpad/rest/configuration.py 2009-10-16 01:54:41 +0000
198+++ lib/canonical/launchpad/rest/configuration.py 2010-02-25 00:40:50 +0000
199@@ -9,10 +9,10 @@
200 ]
201
202 from zope.component import getUtility
203-from zope.interface import implements
204+
205+from lazr.restful.simple import BaseWebServiceConfiguration
206
207 from canonical.config import config
208-from lazr.restful.interfaces import IWebServiceConfiguration
209 from canonical.launchpad.webapp.interfaces import ILaunchBag
210 from canonical.launchpad.webapp.servers import (
211 WebServiceClientRequest, WebServicePublication)
212@@ -20,11 +20,10 @@
213 from canonical.launchpad import versioninfo
214
215
216-class LaunchpadWebServiceConfiguration:
217- implements(IWebServiceConfiguration)
218+class LaunchpadWebServiceConfiguration(BaseWebServiceConfiguration):
219
220 path_override = "api"
221- service_version_uri_prefix = "beta"
222+ active_versions = ["beta", "1.0", "devel"]
223 view_permission = "launchpad.View"
224 set_hop_by_hop_headers = True
225
226
227=== modified file 'lib/canonical/launchpad/testing/pages.py'
228--- lib/canonical/launchpad/testing/pages.py 2010-02-16 21:21:14 +0000
229+++ lib/canonical/launchpad/testing/pages.py 2010-02-25 00:40:50 +0000
230@@ -136,6 +136,8 @@
231 self.handle_errors = handle_errors
232 WebServiceCaller.__init__(self, handle_errors, domain, protocol)
233
234+ default_api_version = "beta"
235+
236 def addHeadersTo(self, full_url, full_headers):
237 if (self.consumer is not None and self.access_token is not None):
238 request = OAuthRequest.from_consumer_and_token(
239
240=== modified file 'lib/canonical/launchpad/webapp/servers.py'
241--- lib/canonical/launchpad/webapp/servers.py 2010-02-12 19:26:23 +0000
242+++ lib/canonical/launchpad/webapp/servers.py 2010-02-25 00:40:51 +0000
243@@ -26,7 +26,7 @@
244 from zope.app.server import wsgi
245 from zope.app.wsgi import WSGIPublisherApplication
246 from zope.component import getUtility
247-from zope.interface import implements
248+from zope.interface import alsoProvides, implements
249 from zope.publisher.browser import (
250 BrowserRequest, BrowserResponse, TestRequest)
251 from zope.publisher.interfaces import NotFound
252@@ -41,7 +41,8 @@
253 from canonical.config import config
254
255 from canonical.lazr.interfaces.feed import IFeed
256-from lazr.restful.interfaces import IWebServiceConfiguration
257+from lazr.restful.interfaces import (
258+ IWebServiceConfiguration, IWebServiceVersion)
259 from lazr.restful.publisher import (
260 WebServicePublicationMixin, WebServiceRequestTraversal)
261
262@@ -1277,7 +1278,7 @@
263 """
264 implements(canonical.launchpad.layers.WebServiceLayer)
265
266- def __init__(self, body_instream=None, environ=None, **kw):
267+ def __init__(self, body_instream=None, environ=None, version=None, **kw):
268 test_environ = {
269 'SERVER_URL': 'http://api.launchpad.dev',
270 'HTTP_HOST': 'api.launchpad.dev',
271@@ -1286,6 +1287,11 @@
272 test_environ.update(environ)
273 super(WebServiceTestRequest, self).__init__(
274 body_instream=body_instream, environ=test_environ, **kw)
275+ if version is None:
276+ version = getUtility(IWebServiceConfiguration).active_versions[-1]
277+ self.version = version
278+ version_marker = getUtility(IWebServiceVersion, name=version)
279+ alsoProvides(self, version_marker)
280
281
282 # ---- xmlrpc
283
284=== modified file 'lib/canonical/launchpad/webapp/tests/__init__.py'
285--- lib/canonical/launchpad/webapp/tests/__init__.py 2010-01-08 03:15:15 +0000
286+++ lib/canonical/launchpad/webapp/tests/__init__.py 2010-02-25 00:40:51 +0000
287@@ -2,30 +2,3 @@
288 # GNU Affero General Public License version 3 (see the file LICENSE).
289
290 __metaclass__ = type
291-
292-from lazr.restful.interfaces import IWebServiceConfiguration
293-from zope.component import getGlobalSiteManager, provideUtility
294-from zope.interface import implements
295-
296-from lp.testing import TestCase
297-
298-
299-class DummyWebServiceConfiguration:
300- """A totally vanilla web service configuration."""
301- implements(IWebServiceConfiguration)
302- path_override = "api"
303- service_version_uri_prefix = "beta"
304-
305-
306-class DummyConfigurationTestCase(TestCase):
307- """A test case that installs a DummyWebServiceConfiguration."""
308-
309- def setUp(self):
310- super(DummyConfigurationTestCase, self).setUp()
311- self.config = DummyWebServiceConfiguration()
312- provideUtility(self.config, IWebServiceConfiguration)
313-
314- def tearDown(self):
315- getGlobalSiteManager().unregisterUtility(
316- self.config, IWebServiceConfiguration)
317- super(DummyConfigurationTestCase, self).tearDown()
318
319=== modified file 'lib/canonical/launchpad/webapp/tests/test_dbpolicy.py'
320--- lib/canonical/launchpad/webapp/tests/test_dbpolicy.py 2010-02-17 15:12:21 +0000
321+++ lib/canonical/launchpad/webapp/tests/test_dbpolicy.py 2010-02-25 00:40:51 +0000
322@@ -172,7 +172,7 @@
323 and will meltdown when the API becomes popular.
324 """
325 api_prefix = getUtility(
326- IWebServiceConfiguration).service_version_uri_prefix
327+ IWebServiceConfiguration).active_versions[0]
328 server_url = 'http://api.launchpad.dev/%s' % api_prefix
329 request = LaunchpadTestRequest(SERVER_URL=server_url)
330 setFirstLayer(request, WebServiceLayer)
331@@ -185,7 +185,7 @@
332 can be outsourced to a slave database when possible.
333 """
334 api_prefix = getUtility(
335- IWebServiceConfiguration).service_version_uri_prefix
336+ IWebServiceConfiguration).active_versions[0]
337 server_url = 'http://api.launchpad.dev/%s' % api_prefix
338 request = LaunchpadTestRequest(SERVER_URL=server_url)
339 newInteraction(request)
340@@ -211,7 +211,7 @@
341 touch_read_only_file()
342 try:
343 api_prefix = getUtility(
344- IWebServiceConfiguration).service_version_uri_prefix
345+ IWebServiceConfiguration).active_versions[0]
346 server_url = 'http://api.launchpad.dev/%s' % api_prefix
347 request = LaunchpadTestRequest(SERVER_URL=server_url)
348 setFirstLayer(request, WebServiceLayer)
349
350=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
351--- lib/canonical/launchpad/webapp/tests/test_servers.py 2010-01-08 03:15:15 +0000
352+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2010-02-25 00:40:51 +0000
353@@ -6,10 +6,17 @@
354 import StringIO
355 import unittest
356
357+from zope.component import getGlobalSiteManager, getUtility
358 from zope.publisher.base import DefaultPublication
359 from zope.testing.doctest import DocTestSuite, NORMALIZE_WHITESPACE, ELLIPSIS
360 from zope.interface import implements, Interface
361
362+from lazr.restful.interfaces import (
363+ IServiceRootResource, IWebServiceConfiguration)
364+from lazr.restful.simple import RootResource
365+from lazr.restful.tests.test_webservice import (
366+ IGenericCollection, IGenericEntry, WebServiceTestCase)
367+
368 from lp.testing import TestCase
369
370 from canonical.launchpad.webapp.servers import (
371@@ -18,7 +25,6 @@
372 TranslationsBrowserRequest, VHostWebServiceRequestPublicationFactory,
373 VirtualHostRequestPublicationFactory, WebServiceRequestPublicationFactory,
374 WebServiceClientRequest, WebServicePublication, WebServiceTestRequest)
375-from canonical.launchpad.webapp.tests import DummyConfigurationTestCase
376
377
378 class SetInWSGIEnvironmentTestCase(TestCase):
379@@ -90,7 +96,7 @@
380 "factory should not have set HTTPS env")
381
382
383-class TestVhostWebserviceFactory(DummyConfigurationTestCase):
384+class TestVhostWebserviceFactory(WebServiceTestCase):
385
386 def setUp(self):
387 super(TestVhostWebserviceFactory, self).setUp()
388@@ -108,7 +114,7 @@
389 @property
390 def working_api_path(self):
391 """A path to the webservice API that should work every time."""
392- return '/' + self.config.path_override
393+ return '/' + getUtility(IWebServiceConfiguration).path_override
394
395 @property
396 def failing_api_path(self):
397@@ -119,7 +125,8 @@
398 """The factory should produce WebService request and publication
399 objects for requests to the /api root URL.
400 """
401- env = self.wsgi_env('/' + self.config.path_override)
402+ env = self.wsgi_env(
403+ '/' + getUtility(IWebServiceConfiguration).path_override)
404
405 # Necessary preamble and sanity check. We need to call
406 # the factory's canHandle() method with an appropriate
407@@ -206,7 +213,8 @@
408 # This is a sanity check, so I can write '/api/foo' instead
409 # of PATH_OVERRIDE + '/foo' in my tests. The former's
410 # intention is clearer.
411- self.assertEqual(self.config.path_override, 'api',
412+ self.assertEqual(
413+ getUtility(IWebServiceConfiguration).path_override, 'api',
414 "Sanity check: The web service path override should be 'api'.")
415
416 self.assert_(
417@@ -241,7 +249,24 @@
418 "/api.")
419
420
421-class TestWebServiceRequestTraversal(DummyConfigurationTestCase):
422+class TestWebServiceRequestTraversal(WebServiceTestCase):
423+
424+ testmodule_objects = [IGenericEntry, IGenericCollection]
425+
426+ def setUp(self):
427+ super(TestWebServiceRequestTraversal, self).setUp()
428+ # For this test we need to make the URL "/foo" resolve to a
429+ # resource. To this end, we'll define a top-level collection
430+ # named 'foo'.
431+ class GenericCollection:
432+ implements(IGenericCollection)
433+ pass
434+
435+ class MyRootResource(RootResource):
436+ def _build_top_level_objects(self):
437+ return ({'foo' : (IGenericEntry, GenericCollection())}, {})
438+ getGlobalSiteManager().registerUtility(
439+ MyRootResource(), IServiceRootResource)
440
441 def test_traversal_of_api_path_urls(self):
442 """Requests that have /api at the root of their path should trim
443@@ -249,35 +274,26 @@
444 """
445 # First, we need to forge a request to the API.
446 data = ''
447- api_url = ('/' + self.config.path_override +
448- '/' + 'beta' + '/' + 'foo')
449+ config = getUtility(IWebServiceConfiguration)
450+ api_url = ('/' + config.path_override +
451+ '/' + '1.0' + '/' + 'foo')
452 env = {'PATH_INFO': api_url}
453- request = WebServiceClientRequest(data, env)
454-
455- # And we need a mock publication object to use during traversal.
456- class WebServicePublicationStub(DefaultPublication):
457- def getResource(self, request, obj):
458- pass
459-
460- request.setPublication(WebServicePublicationStub(None))
461-
462- # And we need a traversible object that knows about the 'foo' name.
463- root = {'foo': object()}
464+ request = config.createRequest(data, env)
465
466 stack = request.getTraversalStack()
467- self.assert_(self.config.path_override in stack,
468+ self.assert_(config.path_override in stack,
469 "Sanity check: the API path should show up in the request's "
470 "traversal stack: %r" % stack)
471
472- request.traverse(root)
473+ request.traverse(None)
474
475 stack = request.getTraversalStack()
476- self.failIf(self.config.path_override in stack,
477+ self.failIf(config.path_override in stack,
478 "Web service paths should be dropped from the webservice "
479 "request traversal stack: %r" % stack)
480
481
482-class TestWebServiceRequest(TestCase):
483+class TestWebServiceRequest(WebServiceTestCase):
484
485 def test_application_url(self):
486 """Requests to the /api path should return the original request's
487@@ -286,14 +302,14 @@
488 # Simulate a request to bugs.launchpad.net/api
489 server_url = 'http://bugs.launchpad.dev'
490 env = {
491- 'PATH_INFO': '/api/beta',
492+ 'PATH_INFO': '/api/devel',
493 'SERVER_URL': server_url,
494 'HTTP_HOST': 'bugs.launchpad.dev',
495 }
496
497 # WebServiceTestRequest will suffice, as it too should conform to
498 # the Same Origin web browser policy.
499- request = WebServiceTestRequest(environ=env)
500+ request = WebServiceTestRequest(environ=env, version="1.0")
501 self.assertEqual(request.getApplicationURL(), server_url)
502
503 def test_response_should_vary_based_on_content_type(self):
504
505=== modified file 'lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js'
506--- lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js 2009-09-17 16:27:19 +0000
507+++ lib/canonical/launchpad/windmill/jstests/launchpad_ajax.js 2010-02-25 00:40:50 +0000
508@@ -35,19 +35,19 @@
509 jum.assertNotUndefined(LP.client.cache.context);
510 var context = LP.client.cache.context;
511 jum.assertNotEquals(context.self_link.indexOf(
512- "/api/beta/firefox/+bug/1"), -1);
513+ "/api/devel/firefox/+bug/1"), -1);
514 jum.assertNotEquals(context.resource_type_link.indexOf(
515- "/api/beta/#bug_task"), -1);
516+ "/api/devel/#bug_task"), -1);
517 jum.assertNotEquals(context.owner_link.indexOf(
518- "/api/beta/~name12"), -1);
519+ "/api/devel/~name12"), -1);
520 jum.assertNotEquals(context.related_tasks_collection_link.indexOf(
521- "/api/beta/firefox/+bug/1/related_tasks"), -1);
522+ "/api/devel/firefox/+bug/1/related_tasks"), -1);
523
524 // Specific views may add additional objects to the object cache
525 // or links to the link cache.
526 var bug = LP.client.cache.bug;
527 jum.assertNotUndefined(LP.client.cache.bug);
528- jum.assertNotEquals(bug.self_link.indexOf("/api/beta/bugs/1"), -1);
529+ jum.assertNotEquals(bug.self_link.indexOf("/api/devel/bugs/1"), -1);
530 }
531 ]);
532
533@@ -71,13 +71,13 @@
534
535 var test_normalize_uri = function() {
536 var normalize = LP.client.normalize_uri;
537- jum.assertEquals(normalize("http://www.example.com/api/beta/foo"),
538- "/api/beta/foo");
539+ jum.assertEquals(normalize("http://www.example.com/api/devel/foo"),
540+ "/api/devel/foo");
541 jum.assertEquals(normalize("http://www.example.com/foo/bar"), "/foo/bar");
542- jum.assertEquals(normalize("/foo/bar"), "/api/beta/foo/bar");
543- jum.assertEquals(normalize("/api/beta/foo/bar"), "/api/beta/foo/bar");
544- jum.assertEquals(normalize("foo/bar"), "/api/beta/foo/bar");
545- jum.assertEquals(normalize("api/beta/foo/bar"), "/api/beta/foo/bar");
546+ jum.assertEquals(normalize("/foo/bar"), "/api/devel/foo/bar");
547+ jum.assertEquals(normalize("/api/devel/foo/bar"), "/api/devel/foo/bar");
548+ jum.assertEquals(normalize("foo/bar"), "/api/devel/foo/bar");
549+ jum.assertEquals(normalize("api/devel/foo/bar"), "/api/devel/foo/bar");
550 };
551
552 var test_append_qs = function() {
553@@ -87,12 +87,12 @@
554 };
555
556 var test_field_uri = function() {
557- jum.assertEquals(LP.client.get_field_uri("http://www.example.com/api/beta/foo", "field"),
558- "/api/beta/foo/field");
559+ jum.assertEquals(LP.client.get_field_uri("http://www.example.com/api/devel/foo", "field"),
560+ "/api/devel/foo/field");
561 jum.assertEquals(LP.client.get_field_uri("/no/slash", "field"),
562- "/api/beta/no/slash/field");
563+ "/api/devel/no/slash/field");
564 jum.assertEquals(LP.client.get_field_uri("/has/slash/", "field"),
565- "/api/beta/has/slash/field");
566+ "/api/devel/has/slash/field");
567 };
568
569 // Test that retrieving a non-existent resource uses the failure handler.
570
571=== modified file 'lib/canonical/launchpad/zcml/webservice.zcml'
572--- lib/canonical/launchpad/zcml/webservice.zcml 2009-07-13 18:15:02 +0000
573+++ lib/canonical/launchpad/zcml/webservice.zcml 2010-02-25 00:40:50 +0000
574@@ -5,6 +5,7 @@
575 <configure
576 xmlns="http://namespaces.zope.org/zope"
577 xmlns:browser="http://namespaces.zope.org/browser"
578+ xmlns:grok="http://namespaces.zope.org/grok"
579 xmlns:webservice="http://namespaces.canonical.com/webservice"
580 xmlns:i18n="http://namespaces.zope.org/i18n"
581 i18n_domain="launchpad">
582@@ -46,11 +47,15 @@
583 />
584
585 <adapter
586- for="canonical.launchpad.interfaces.IBugComment"
587+ for="canonical.launchpad.interfaces.IBugComment
588+ lazr.restful.interfaces.IWebServiceClientRequest"
589 provides="lazr.restful.interfaces.IEntry"
590 factory="canonical.launchpad.rest.bugcomment_to_entry"
591 />
592
593+ <grok:grok package="lazr.restful.directives" />
594+ <grok:grok package="canonical.launchpad.rest" />
595+
596 <webservice:register module="canonical.launchpad.interfaces" />
597
598 <adapter
599
600=== modified file 'lib/canonical/lazr/doc/folder.txt'
601--- lib/canonical/lazr/doc/folder.txt 2009-04-17 10:32:16 +0000
602+++ lib/canonical/lazr/doc/folder.txt 2010-02-25 00:40:51 +0000
603@@ -32,7 +32,7 @@
604 >>> from zope.publisher.interfaces.browser import IBrowserPublisher
605 >>> from lazr.restful.testing.webservice import FakeRequest
606
607- >>> view = MyFolder(object(), FakeRequest())
608+ >>> view = MyFolder(object(), FakeRequest(version="devel"))
609 >>> verifyObject(IBrowserPublisher, view)
610 True
611
612@@ -54,7 +54,7 @@
613 It accepts traversing to the file through an arbitrary revision
614 identifier.
615
616- >>> view = MyFolder(object(), FakeRequest())
617+ >>> view = MyFolder(object(), FakeRequest(version="devel"))
618 >>> view = view.publishTraverse(view.request, 'rev6510')
619 >>> view = view.publishTraverse(view.request, 'image1.gif')
620 >>> print view()
621@@ -62,7 +62,7 @@
622
623 Requesting a directory raises a NotFound.
624
625- >>> view = MyFolder(object(), FakeRequest())
626+ >>> view = MyFolder(object(), FakeRequest(version="devel"))
627 >>> view = view.publishTraverse(view.request, 'a_dir')
628 >>> view()
629 Traceback (most recent call last):
630@@ -72,7 +72,7 @@
631 By default, subdirectories are not exported. (See below on how to enable
632 this)
633
634- >>> view = MyFolder(object(), FakeRequest())
635+ >>> view = MyFolder(object(), FakeRequest(version="devel"))
636 >>> view = view.publishTraverse(view.request, 'a_dir')
637 >>> view = view.publishTraverse(view.request, 'other.txt')
638 >>> view()
639@@ -82,7 +82,7 @@
640
641 Not requesting any file, also raises NotFound.
642
643- >>> view = MyFolder(object(), FakeRequest())
644+ >>> view = MyFolder(object(), FakeRequest(version="devel"))
645 >>> view()
646 Traceback (most recent call last):
647 ...
648@@ -90,7 +90,7 @@
649
650 As requesting a non-existent file.
651
652- >>> view = MyFolder(object(), FakeRequest())
653+ >>> view = MyFolder(object(), FakeRequest(version="devel"))
654 >>> view = view.publishTraverse(view.request, 'image2')
655 >>> view()
656 Traceback (most recent call last):
657@@ -111,7 +111,7 @@
658 >>> class MyImageFolder(ExportedImageFolder):
659 ... folder = resource_dir
660
661- >>> view = MyImageFolder(object(), FakeRequest())
662+ >>> view = MyImageFolder(object(), FakeRequest(version="devel"))
663 >>> view.image_extensions
664 ('.png', '.gif')
665
666@@ -128,12 +128,12 @@
667 >>> file(os.path.join(resource_dir, 'image3.gif'), 'w').write(
668 ... 'Image with extension')
669
670- >>> view = MyImageFolder(object(), FakeRequest())
671+ >>> view = MyImageFolder(object(), FakeRequest(version="devel"))
672 >>> view = view.publishTraverse(view.request, 'image3')
673 >>> print view()
674 Image without extension
675
676- >>> view = MyImageFolder(object(), FakeRequest())
677+ >>> view = MyImageFolder(object(), FakeRequest(version="devel"))
678 >>> view = view.publishTraverse(view.request, 'image3.gif')
679 >>> print view()
680 Image with extension
681@@ -159,7 +159,7 @@
682
683 Traversing to a file in a subdirectory will now work.
684
685- >>> view = MyTree(object(), FakeRequest())
686+ >>> view = MyTree(object(), FakeRequest(version="devel"))
687 >>> view = view.publishTraverse(view.request, 'public')
688 >>> view = view.publishTraverse(view.request, 'subdir1')
689 >>> view = view.publishTraverse(view.request, 'test1.txt')
690@@ -168,7 +168,7 @@
691
692 But traversing to the subdirectory itself will raise a NotFound.
693
694- >>> view = MyTree(object(), FakeRequest())
695+ >>> view = MyTree(object(), FakeRequest(version="devel"))
696 >>> view = view.publishTraverse(view.request, 'public')
697 >>> print view()
698 Traceback (most recent call last):
699@@ -177,7 +177,7 @@
700
701 Trying to request a non-existent file, will also raise a NotFound.
702
703- >>> view = MyTree(object(), FakeRequest())
704+ >>> view = MyTree(object(), FakeRequest(version="devel"))
705 >>> view = view.publishTraverse(view.request, 'public')
706 >>> view = view.publishTraverse(view.request, 'nosuchfile.txt')
707 >>> view()
708@@ -188,7 +188,7 @@
709 Traversing beyond an existing file to a non-existant file raises a
710 NotFound.
711
712- >>> view = MyTree(object(), FakeRequest())
713+ >>> view = MyTree(object(), FakeRequest(version="devel"))
714 >>> view = view.publishTraverse(view.request, 'public')
715 >>> view = view.publishTraverse(view.request, 'subdir1')
716 >>> view = view.publishTraverse(view.request, 'test1.txt')
717
718=== modified file 'lib/canonical/lazr/doc/menus.txt'
719--- lib/canonical/lazr/doc/menus.txt 2009-10-21 17:41:20 +0000
720+++ lib/canonical/lazr/doc/menus.txt 2010-02-25 00:40:50 +0000
721@@ -547,7 +547,7 @@
722 >>> summarise_links(RecipeMenu(recipe))
723 Traceback (most recent call last):
724 ...
725- AttributeError: 'NoneType' object has no attribute 'participations'
726+ AttributeError: 'NoneType' object has no attribute 'getURL'
727
728
729 == Registering menus as adapters for content objects and views ==
730
731=== modified file 'lib/canonical/widgets/lazrjs.py'
732--- lib/canonical/widgets/lazrjs.py 2009-11-30 02:09:48 +0000
733+++ lib/canonical/widgets/lazrjs.py 2010-02-25 00:40:50 +0000
734@@ -329,11 +329,16 @@
735 # The user may not have write access on the attribute itself, but
736 # the REST API may have a mutator method configured, such as
737 # transitionToAssignee.
738- exported_tag = self.interface_attribute.getTaggedValue(
739+ #
740+ # We look at the top of the annotation stack, since Ajax
741+ # requests always go to the most recent version of the web
742+ # service.
743+ exported_tag_stack = self.interface_attribute.getTaggedValue(
744 'lazr.restful.exported')
745- mutator = exported_tag.get('mutated_by')
746- if mutator is not None:
747- return canAccess(self.context, mutator.__name__)
748+ mutator_info = exported_tag_stack.get('mutator_annotations')
749+ if mutator_info is not None:
750+ mutator_method, mutator_extra = mutator_info
751+ return canAccess(self.context, mutator_method.__name__)
752 else:
753 return False
754
755
756=== modified file 'lib/lp/bugs/adapters/bug.py'
757--- lib/lp/bugs/adapters/bug.py 2009-06-25 00:40:31 +0000
758+++ lib/lp/bugs/adapters/bug.py 2010-02-25 00:40:51 +0000
759@@ -8,12 +8,15 @@
760 'bugcomment_to_entry',
761 ]
762
763+from zope.component import getMultiAdapter
764 from lazr.restful.interfaces import IEntry
765
766-def bugcomment_to_entry(comment):
767+
768+def bugcomment_to_entry(comment, version):
769 """Will adapt to the bugcomment to the real IMessage.
770
771 This is needed because navigation to comments doesn't return
772 real IMessage instances but IBugComment.
773 """
774- return IEntry(comment.bugtask.bug.messages[comment.index])
775+ return getMultiAdapter(
776+ (comment.bugtask.bug.messages[comment.index], version), IEntry)
777
778=== modified file 'lib/lp/code/browser/configure.zcml'
779--- lib/lp/code/browser/configure.zcml 2010-02-23 21:48:53 +0000
780+++ lib/lp/code/browser/configure.zcml 2010-02-25 00:40:50 +0000
781@@ -1068,5 +1068,11 @@
782 factory="lp.code.browser.diff.PreviewDiffFormatterAPI"
783 name="fmt"
784 />
785+ <adapter
786+ for="lp.code.interfaces.diff.IDiff"
787+ provides="zope.traversing.interfaces.IPathAdapter"
788+ factory="lp.code.browser.diff.DiffFormatterAPI"
789+ name="fmt"
790+ />
791
792 </configure>
793
794=== modified file 'lib/lp/code/browser/diff.py'
795--- lib/lp/code/browser/diff.py 2010-02-19 16:31:54 +0000
796+++ lib/lp/code/browser/diff.py 2010-02-25 00:40:50 +0000
797@@ -23,8 +23,10 @@
798 usedfor = IPreviewDiff
799
800
801-class PreviewDiffFormatterAPI(ObjectFormatterAPI):
802- """Formatter for preview diffs."""
803+class DiffFormatterAPI(ObjectFormatterAPI):
804+
805+ def _get_url(self, librarian_alias):
806+ return librarian_alias.getURL()
807
808 def url(self, view_name=None, rootsite=None):
809 """Use the url of the librarian file containing the diff.
810@@ -32,8 +34,7 @@
811 librarian_alias = self._context.diff_text
812 if librarian_alias is None:
813 return None
814- else:
815- return canonical_url(self._context) + '/+files/preview.diff'
816+ return self._get_url(librarian_alias)
817
818 def link(self, view_name):
819 """The link to the diff should show the line count.
820@@ -85,3 +86,10 @@
821 '<a href="%(url)s" class="diff-link">'
822 '%(line_count)s%(count_text)s%(file_text)s%(conflict_text)s'
823 '</a>' % args)
824+
825+
826+class PreviewDiffFormatterAPI(DiffFormatterAPI):
827+ """Formatter for preview diffs."""
828+
829+ def _get_url(self, library_):
830+ return canonical_url(self._context) + '/+files/preview.diff'
831
832=== modified file 'lib/lp/code/browser/tests/test_tales.py'
833--- lib/lp/code/browser/tests/test_tales.py 2010-02-18 17:23:49 +0000
834+++ lib/lp/code/browser/tests/test_tales.py 2010-02-25 00:40:51 +0000
835@@ -67,7 +67,7 @@
836 self.assertEqual(False, preview.stale)
837 self.assertEqual(True, self._createStalePreviewDiff().stale)
838 self.assertEqual(u'conflicts', preview.conflicts)
839- self.assertEqual({'filename': (3,2)}, preview.diffstat)
840+ self.assertEqual({'filename': (3, 2)}, preview.diffstat)
841
842 def test_fmt_no_diff(self):
843 # If there is no diff, there is no link.
844@@ -159,6 +159,17 @@
845 test_tales('preview/fmt:link', preview=preview))
846
847
848+class TestDiffFormatter(TestCaseWithFactory):
849+ """Test the DiffFormatterAPI class."""
850+
851+ layer = LaunchpadFunctionalLayer
852+
853+ def test_url(self):
854+ diff = self.factory.makeDiff()
855+ self.assertEqual(
856+ diff.diff_text.getURL(), test_tales('diff/fmt:url', diff=diff))
857+
858+
859 def test_suite():
860 return unittest.TestLoader().loadTestsFromName(__name__)
861
862
863=== modified file 'lib/lp/code/model/branch.py'
864--- lib/lp/code/model/branch.py 2010-02-23 21:48:53 +0000
865+++ lib/lp/code/model/branch.py 2010-02-25 00:40:51 +0000
866@@ -606,7 +606,7 @@
867
868 for bugbranch in self.bug_branches:
869 deletion_operations.append(
870- DeletionCallable(bugbranch,
871+ DeletionCallable(bugbranch.bug.default_bugtask,
872 _('This bug is linked to this branch.'),
873 bugbranch.destroySelf))
874 for spec_link in self.spec_links:
875
876=== modified file 'lib/lp/code/model/tests/test_branch.py'
877--- lib/lp/code/model/tests/test_branch.py 2010-02-19 02:15:27 +0000
878+++ lib/lp/code/model/tests/test_branch.py 2010-02-25 00:40:51 +0000
879@@ -867,7 +867,7 @@
880 """Deletion requirements for a branch with a bug are right."""
881 bug = self.factory.makeBug()
882 bug.linkBranch(self.branch, self.branch.owner)
883- self.assertEqual({bug.linked_branches[0]:
884+ self.assertEqual({bug.default_bugtask:
885 ('delete', _('This bug is linked to this branch.'))},
886 self.branch.deletionRequirements())
887
888
889=== modified file 'lib/lp/code/stories/branches/xx-bug-branch-links.txt'
890--- lib/lp/code/stories/branches/xx-bug-branch-links.txt 2009-11-17 09:08:17 +0000
891+++ lib/lp/code/stories/branches/xx-bug-branch-links.txt 2010-02-25 00:40:51 +0000
892@@ -1,10 +1,13 @@
893-= Bug branch links =
894+================
895+Bug branch links
896+================
897
898 It is possible to link bugs and branches from both the bug page and the branch
899 page.
900
901
902-== From the branch page ==
903+From the branch page
904+====================
905
906 There is a "Link to a bug report" item in the actions menu that is visible
907 to everybody but which links to a page restricted with the
908@@ -98,7 +101,8 @@
909 No bug branch links
910
911
912-== From the bug page ==
913+From the bug page
914+=================
915
916 The action link on the bug page is "Link a related branch". Again this
917 links to a page restricted with the launchpad.AnyPerson permission.
918@@ -129,7 +133,8 @@
919 Bug #11: Make Jokosher use autoaudiosink (Undecided &ndash; New)
920
921
922-=== Quick branch registration ===
923+Quick branch registration
924+=========================
925
926 You can also register a branch at the same time as registering a bug-branch
927 link. Instead of entering the unique name of a branch, you can enter a URL:
928@@ -182,7 +187,8 @@
929 No bug branch links
930
931
932-== Deleting bug branch links ==
933+Deleting bug branch links
934+=========================
935
936 The edit view for the bug branch also now has a delete button to unlink
937 the bug from the branch.
938@@ -192,3 +198,23 @@
939 >>> browser.getLink(url="+delete").click()
940 >>> printBugBranchLinks(browser)
941 No bug branch links
942+
943+
944+Deleting a branch with linked bugs
945+==================================
946+
947+ >>> login('no-priv@canonical.com')
948+ >>> grub = factory.makeAnyBranch()
949+ >>> new_bug = factory.makeBug()
950+ >>> grub_bug_link = grub.linkBug(new_bug, grub.owner)
951+ >>> grub_url = canonical_url(grub)
952+ >>> logout()
953+
954+ >>> admin_browser.open(grub_url)
955+ >>> admin_browser.getLink('Delete branch').click()
956+
957+ >>> print find_tag_by_id(admin_browser.contents, 'deletion-items')
958+ <ul ...
959+ <a href="http://bugs.launchpad.dev/product-name.../+bug/..."
960+ class="sprite bug-undecided">Bug #...: generic-string...</a>...
961+
962
963=== modified file 'lib/lp/code/templates/branch-delete.pt'
964--- lib/lp/code/templates/branch-delete.pt 2009-08-12 00:47:32 +0000
965+++ lib/lp/code/templates/branch-delete.pt 2010-02-25 00:40:51 +0000
966@@ -24,7 +24,7 @@
967
968 <tal:deletelist condition="view/branch_deletion_actions/delete">
969 The following items must be <em>deleted</em>:
970- <ul>
971+ <ul id="deletion-items">
972 <tal:actions repeat="row view/branch_deletion_actions/delete">
973 <li>
974 <img src="/@@/no" title="Insufficient privileges"
975
976=== modified file 'lib/lp/registry/doc/sourcepackage.txt'
977--- lib/lp/registry/doc/sourcepackage.txt 2010-02-15 12:59:55 +0000
978+++ lib/lp/registry/doc/sourcepackage.txt 2010-02-25 00:40:51 +0000
979@@ -256,87 +256,64 @@
980
981 >>> from lp.registry.model.sourcepackage import SourcePackage
982 >>> sp = SourcePackage(sourcepackagename=firefox, distroseries=hoary)
983- >>> sp.productseries.name
984- u'1.0'
985+ >>> print sp.productseries.name
986+ 1.0
987
988-Now we make sure there is no Packaging data for a52dec in hoary.
989+A source package's product series is None when it does not have a
990+Packaging entry. Historical packaging does not affect the state of the
991+productseries attribute.
992
993 >>> from lp.registry.model.packaging import PackagingUtil
994- >>> a52decsp = SourcePackage(sourcepackagename=a52dec,
995- ... distroseries=hoary)
996- >>> a52decsp.productseries.name
997- u'trunk'
998-
999- >>> PackagingUtil().packagingEntryExists(
1000- ... productseries=a52decsp.productseries,
1001- ... sourcepackagename=a52dec,
1002- ... distroseries=hoary)
1003- False
1004-
1005-So far so good.
1006-
1007-Now verify we still get a product for that source package, thanks to the
1008-fact that we have Warty data for it
1009-
1010- >>> PackagingUtil().packagingEntryExists(
1011- ... productseries=a52decsp.productseries,
1012- ... sourcepackagename=a52dec,
1013- ... distroseries=warty)
1014- True
1015-
1016- >>> a52decsp.productseries.product.name
1017- u'a52dec'
1018-
1019-Similarly, we should be able to get the packaging information from a
1020-parent distroseries, on the basis that a derivative is highly unlikely
1021-to change the packaging drastically without changing the name of the
1022-package.
1023-
1024-First, show there is no packging data for a52dec in g2k5:
1025-
1026- >>> PackagingUtil().packagingEntryExists(
1027- ... productseries=a52decsp.productseries,
1028- ... sourcepackagename=a52dec,
1029- ... distroseries=g2k5)
1030- False
1031-
1032-Now verify we still get a product for that source package
1033-
1034- >>> sp = SourcePackage(sourcepackagename=a52dec, distroseries=g2k5)
1035- >>> sp.productseries.product.name
1036- u'a52dec'
1037-
1038-And if we want to link that productseries to a source package in that
1039-distroseries
1040+ >>> hoary_a52dec = SourcePackage(sourcepackagename=a52dec,
1041+ ... distroseries=hoary)
1042+ >>> PackagingUtil().packagingEntryExists(
1043+ ... productseries=hoary_a52dec.productseries,
1044+ ... sourcepackagename=a52dec,
1045+ ... distroseries=hoary)
1046+ False
1047+
1048+ >>> print hoary_a52dec.productseries
1049+ None
1050+
1051+Once a Packaging entry is created to link a distro series source package name
1052+to a product series, the source package does have a product series.
1053
1054 >>> from lp.registry.interfaces.packaging import PackagingType
1055- >>> from lp.registry.model.person import Person
1056- >>> foobar = Person.byName('name16')
1057+
1058+ >>> user = factory.makePerson()
1059+ >>> a52dec_series = factory.makeProductSeries(name='ratty')
1060 >>> PackagingUtil().createPackaging(
1061- ... productseries=sp.productseries,
1062+ ... productseries=a52dec_series,
1063 ... sourcepackagename=a52dec,
1064- ... distroseries=g2k5,
1065+ ... distroseries=hoary,
1066 ... packaging=PackagingType.PRIME,
1067- ... owner=foobar)
1068+ ... owner=user)
1069
1070 >>> PackagingUtil().packagingEntryExists(
1071- ... productseries=sp.productseries,
1072+ ... productseries=a52dec_series,
1073 ... sourcepackagename=a52dec,
1074- ... distroseries=g2k5)
1075+ ... distroseries=hoary)
1076 True
1077
1078-Packaging entries can be deleted using PackagingUtil.deletePackaging.
1079+ >>> print hoary_a52dec.productseries.name
1080+ ratty
1081+
1082+Packaging entries can be deleted using PackagingUtil.deletePackaging. That
1083+also removes the source package product series.
1084
1085 >>> PackagingUtil().deletePackaging(
1086- ... productseries=sp.productseries,
1087+ ... productseries=a52dec_series,
1088 ... sourcepackagename=a52dec,
1089- ... distroseries=g2k5)
1090+ ... distroseries=hoary)
1091 >>> PackagingUtil().packagingEntryExists(
1092- ... productseries=sp.productseries,
1093+ ... productseries=a52dec_series,
1094 ... sourcepackagename=a52dec,
1095- ... distroseries=g2k5)
1096+ ... distroseries=hoary)
1097 False
1098
1099+ >>> print hoary_a52dec.productseries
1100+ None
1101+
1102 Linkified changelogs are available through SourcePackageReleaseView: XXX
1103 julian 2007-09-17 This is duplicating the page test. Instead it should
1104 be more like the bug number linkification just below.
1105
1106=== modified file 'lib/lp/registry/model/sourcepackage.py'
1107--- lib/lp/registry/model/sourcepackage.py 2009-11-21 08:56:44 +0000
1108+++ lib/lp/registry/model/sourcepackage.py 2010-02-25 00:40:51 +0000
1109@@ -12,7 +12,6 @@
1110 ]
1111
1112 from operator import attrgetter
1113-from warnings import warn
1114 from sqlobject.sqlbuilder import SQLConstant
1115 from zope.interface import classProvides, implements
1116 from zope.component import getUtility
1117@@ -337,8 +336,7 @@
1118
1119 return IStore(SourcePackageRelease).find(
1120 SourcePackageRelease,
1121- In(SourcePackageRelease.id, subselect)
1122- ).order_by(Desc(
1123+ In(SourcePackageRelease.id, subselect)).order_by(Desc(
1124 SQL("debversion_sort_key(SourcePackageRelease.version)")))
1125
1126 @property
1127@@ -346,19 +344,9 @@
1128 return self.sourcepackagename.name
1129
1130 @property
1131- def product(self):
1132- # we have moved to focusing on productseries as the linker
1133- warn('SourcePackage.product is deprecated, use .productseries',
1134- DeprecationWarning, stacklevel=2)
1135- ps = self.productseries
1136- if ps is not None:
1137- return ps.product
1138- return None
1139-
1140- @property
1141 def productseries(self):
1142 # See if we can find a relevant packaging record
1143- packaging = self.packaging
1144+ packaging = self.direct_packaging
1145 if packaging is None:
1146 return None
1147 return packaging.productseries
1148@@ -366,16 +354,11 @@
1149 @property
1150 def direct_packaging(self):
1151 """See `ISourcePackage`."""
1152- # XXX flacoste 2008-02-28 For some crack reasons, it is possible
1153- # for multiple productseries (of the same product) to state that they
1154- # are packaged in the same source package. This creates all sort of
1155- # weirdness documented in bug #196774. But in order to work around bug
1156- # #181770, use a sort order that will be stable. I guess it makes the
1157- # most sense to return the latest one.
1158- return Packaging.selectFirstBy(
1159+ store = Store.of(self.sourcepackagename)
1160+ return store.find(
1161+ Packaging,
1162 sourcepackagename=self.sourcepackagename,
1163- distroseries=self.distroseries,
1164- orderBy=['packaging', '-datecreated'])
1165+ distroseries=self.distroseries).one()
1166
1167 @property
1168 def packaging(self):
1169@@ -696,18 +679,16 @@
1170 uploads = [
1171 build.package_upload
1172 for build in builds
1173- if build.package_upload
1174- ]
1175+ if build.package_upload]
1176 custom_files = []
1177 for upload in uploads:
1178 custom_files += [
1179 custom for custom in upload.customfiles
1180- if custom.customformat == our_format
1181- ]
1182+ if custom.customformat == our_format]
1183
1184 custom_files.sort(key=attrgetter('id'))
1185 return [custom.libraryfilealias for custom in custom_files]
1186
1187 def linkedBranches(self):
1188 """See `ISourcePackage`."""
1189- return dict((p.name,b) for (p,b) in self.linked_branches)
1190+ return dict((p.name, b) for (p, b) in self.linked_branches)
1191
1192=== modified file 'lib/lp/testing/factory.py'
1193--- lib/lp/testing/factory.py 2010-02-24 13:37:51 +0000
1194+++ lib/lp/testing/factory.py 2010-02-25 00:40:50 +0000
1195@@ -83,6 +83,7 @@
1196 from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
1197 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
1198 from lp.code.interfaces.revision import IRevisionSet
1199+<<<<<<< TREE
1200 from lp.code.interfaces.sourcepackagerecipe import ISourcePackageRecipeSource
1201 from lp.code.interfaces.sourcepackagerecipebuild import (
1202 ISourcePackageRecipeBuildSource,
1203@@ -91,6 +92,9 @@
1204 from lp.codehosting.codeimport.worker import CodeImportSourceDetails
1205
1206 from lp.registry.interfaces.distribution import IDistributionSet
1207+=======
1208+from lp.code.model.diff import Diff, PreviewDiff, StaticDiff
1209+>>>>>>> MERGE-SOURCE
1210 from lp.registry.model.distributionsourcepackage import (
1211 DistributionSourcePackage)
1212 from lp.registry.interfaces.distroseries import IDistroSeries
1213@@ -1007,6 +1011,11 @@
1214 preview_diff.target_revision_id = self.getUniqueUnicode()
1215 return preview_diff
1216
1217+ def makeStaticDiff(self):
1218+ return StaticDiff.acquireFromText(
1219+ self.getUniqueUnicode(), self.getUniqueUnicode(),
1220+ self.getUniqueString())
1221+
1222 def makeRevision(self, author=None, revision_date=None, parent_ids=None,
1223 rev_id=None, log_body=None, date_created=None):
1224 """Create a single `Revision`."""
1225
1226=== modified file 'lib/lp/testopenid/browser/server.py'
1227--- lib/lp/testopenid/browser/server.py 2010-02-01 12:38:31 +0000
1228+++ lib/lp/testopenid/browser/server.py 2010-02-25 00:40:50 +0000
1229@@ -34,19 +34,16 @@
1230 from canonical.launchpad.webapp.login import (
1231 allowUnauthenticatedSession, logInPrincipal, logoutPerson)
1232 from canonical.launchpad.webapp.publisher import Navigation, stepthrough
1233-from canonical.launchpad.webapp.url import urlappend
1234-from canonical.launchpad.webapp.vhosts import allvhosts
1235
1236 from lp.services.openid.browser.openiddiscovery import (
1237 XRDSContentNegotiationMixin)
1238 from lp.testopenid.interfaces.server import (
1239- ITestOpenIDApplication, ITestOpenIDLoginForm,
1240+ get_server_url, ITestOpenIDApplication, ITestOpenIDLoginForm,
1241 ITestOpenIDPersistentIdentity)
1242
1243
1244 OPENID_REQUEST_SESSION_KEY = 'testopenid.request'
1245 SESSION_PKG_KEY = 'TestOpenID'
1246-SERVER_URL = urlappend(allvhosts.configs['testopenid'].rooturl, '+openid')
1247 openid_store = MemoryStore()
1248
1249
1250@@ -85,7 +82,7 @@
1251 @property
1252 def openid_server_url(self):
1253 """The OpenID Server endpoint URL for Launchpad."""
1254- return SERVER_URL
1255+ return get_server_url()
1256
1257
1258 class TestOpenIDIndexView(
1259@@ -101,7 +98,7 @@
1260
1261 def __init__(self, context, request):
1262 super(OpenIDMixin, self).__init__(context, request)
1263- self.server_url = SERVER_URL
1264+ self.server_url = get_server_url()
1265 self.openid_server = Server(openid_store, self.server_url)
1266
1267 @property
1268
1269=== modified file 'lib/lp/testopenid/interfaces/server.py'
1270--- lib/lp/testopenid/interfaces/server.py 2010-01-21 18:06:42 +0000
1271+++ lib/lp/testopenid/interfaces/server.py 2010-02-25 00:40:50 +0000
1272@@ -2,6 +2,7 @@
1273
1274 __metaclass__ = type
1275 __all__ = [
1276+ 'get_server_url',
1277 'ITestOpenIDApplication',
1278 'ITestOpenIDLoginForm',
1279 'ITestOpenIDPersistentIdentity',
1280@@ -12,6 +13,8 @@
1281
1282 from canonical.launchpad.fields import PasswordField
1283 from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
1284+from canonical.launchpad.webapp.url import urlappend
1285+from canonical.launchpad.webapp.vhosts import allvhosts
1286
1287 from lp.services.openid.interfaces.openid import IOpenIDPersistentIdentity
1288
1289@@ -27,3 +30,12 @@
1290
1291 class ITestOpenIDPersistentIdentity(IOpenIDPersistentIdentity):
1292 """Marker interface for IOpenIDPersistentIdentity on testopenid."""
1293+
1294+
1295+def get_server_url():
1296+ """Return the URL for this server's OpenID endpoint.
1297+
1298+ This is wrapped in a function (instead of a constant) to make sure the
1299+ vhost.testopenid section is not required in production configs.
1300+ """
1301+ return urlappend(allvhosts.configs['testopenid'].rooturl, '+openid')
1302
1303=== modified file 'lib/lp/testopenid/testing/helpers.py'
1304--- lib/lp/testopenid/testing/helpers.py 2010-01-21 14:47:51 +0000
1305+++ lib/lp/testopenid/testing/helpers.py 2010-02-25 00:40:50 +0000
1306@@ -21,7 +21,7 @@
1307
1308 from canonical.launchpad.webapp import LaunchpadView
1309
1310-from lp.testopenid.browser.server import SERVER_URL
1311+from lp.testopenid.interfaces.server import get_server_url
1312
1313
1314 class EchoView(LaunchpadView):
1315@@ -69,6 +69,6 @@
1316 def make_identifier_select_endpoint():
1317 """Create an endpoint for use in OpenID identifier select mode."""
1318 endpoint = OpenIDServiceEndpoint()
1319- endpoint.server_url = SERVER_URL
1320+ endpoint.server_url = get_server_url()
1321 endpoint.type_uris = [OPENID_IDP_2_0_TYPE]
1322 return endpoint
1323
1324=== modified file 'versions.cfg'
1325--- versions.cfg 2010-02-19 03:59:33 +0000
1326+++ versions.cfg 2010-02-25 00:40:50 +0000
1327@@ -19,17 +19,17 @@
1328 functest = 0.8.7
1329 funkload = 1.10.0
1330 grokcore.component = 1.6
1331-httplib2 = 0.4.0
1332+httplib2 = 0.6.0
1333 ipython = 0.9.1
1334-launchpadlib = 1.5.4
1335+launchpadlib = 1.5.5
1336 lazr.authentication = 0.1.1
1337 lazr.batchnavigator = 1.1
1338 lazr.config = 1.1.3
1339 lazr.delegates = 1.1.0
1340 lazr.enum = 1.1.2
1341 lazr.lifecycle = 1.1
1342-lazr.restful = 0.9.17
1343-lazr.restfulclient = 0.9.10
1344+lazr.restful = 0.9.21
1345+lazr.restfulclient = 0.9.11
1346 lazr.smtptest = 1.1
1347 lazr.testing = 0.1.1
1348 lazr.uri = 1.0.2

Subscribers

People subscribed via source and target branches

to status/vote changes: