Merge lp:~leonardr/launchpad/multiversion-integration into lp:launchpad/db-devel
- multiversion-integration
- Merge into db-devel
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eleanor Berger (community) | code | Approve | |
Review via email: mp+19346@code.launchpad.net |
Commit message
Description of the change
Leonard Richardson (leonardr) wrote : | # |
Eleanor Berger (intellectronica) : | # |
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/
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-
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-
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.
Leonard Richardson (leonardr) wrote : | # |
Unfortunately I'm experiencing still more test failures. The work continues!
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_
3. Changed Launchpad to use the new 0.9.21 release of lazr.restful, which fixes many other test failures.
Leonard Richardson (leonardr) wrote : | # |
4. The new version of lazr.restful changed the implementation of get_current_
Leonard Richardson (leonardr) wrote : | # |
http://
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 DummyConfigurat
In the new version of lazr.restful, a small IWebServiceConf
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 DummyConfigurat
Since WebServiceTestCase has everything found in DummyConfigurat
However, WebServiceTestCase (along with IGenericCollection and everything else imported from lazr.restful.
I plan to move that code to lazr.restful.
Preview Diff
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 – 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 |
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 IWebServiceAppl ication so it subclasses IServiceRootRes ource; 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 IWebServiceConf iguration. 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 LaunchpadWebSer viceCaller 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.