Merge lp:~allenap/launchpad/dsd-batch-position-reset-bug-822781 into lp:launchpad

Proposed by Gavin Panella
Status: Merged
Approved by: Aaron Bentley
Approved revision: no longer in the source branch.
Merged at revision: 13674
Proposed branch: lp:~allenap/launchpad/dsd-batch-position-reset-bug-822781
Merge into: lp:launchpad
Prerequisite: lp:~allenap/launchpad/phantom-overlay-bug-820828
Diff against target: 257 lines (+119/-18)
5 files modified
lib/canonical/launchpad/webapp/interfaces.py (+7/-0)
lib/canonical/launchpad/webapp/servers.py (+12/-2)
lib/canonical/launchpad/webapp/tests/test_servers.py (+73/-2)
lib/lp/registry/browser/distroseries.py (+12/-6)
lib/lp/registry/browser/tests/test_distroseries.py (+15/-8)
To merge this branch: bzr merge lp:~allenap/launchpad/dsd-batch-position-reset-bug-822781
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+71069@code.launchpad.net

Commit message

[r=abentley][bug=822781] After a successful sync request via DistroSeries:+localpackagediffs and its siblings, redirect to the page from where the request was made with the same batch and filtering parameters.

Description of the change

When redirecting the user after a sync on +localpackagediffs (and its
siblings), keep all the same batch and filtering options in the query
string.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

This looks good, but please clean up the comment "...forms should post to themselves, including GET params..."

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/webapp/interfaces.py'
2--- lib/canonical/launchpad/webapp/interfaces.py 2011-08-01 15:40:06 +0000
3+++ lib/canonical/launchpad/webapp/interfaces.py 2011-08-11 20:06:37 +0000
4@@ -347,6 +347,13 @@
5 If no matching object is found, the tuple (None, None) is returned.
6 """
7
8+ def getURL(level=0, path_only=False, include_query=False):
9+ """See `IHTTPApplicationRequest`.
10+
11+ Additionally, if `include_query` is `True`, the query string is
12+ included in the returned URL.
13+ """
14+
15
16 class IBrowserFormNG(Interface):
17 """Interface to manipulate submitted form data."""
18
19=== modified file 'lib/canonical/launchpad/webapp/servers.py'
20--- lib/canonical/launchpad/webapp/servers.py 2011-05-27 17:09:42 +0000
21+++ lib/canonical/launchpad/webapp/servers.py 2011-08-11 20:06:37 +0000
22@@ -542,6 +542,16 @@
23 """See `IBasicLaunchpadRequest`."""
24 return 'XMLHttpRequest' == self.getHeader('HTTP_X_REQUESTED_WITH')
25
26+ def getURL(self, level=0, path_only=False, include_query=False):
27+ """See `IBasicLaunchpadRequest`."""
28+ sup = super(LaunchpadBrowserRequestMixin, self)
29+ url = sup.getURL(level, path_only)
30+ if include_query:
31+ query_string = self.get('QUERY_STRING')
32+ if query_string is not None and len(query_string) > 0:
33+ url = "%s?%s" % (url, query_string)
34+ return url
35+
36
37 class BasicLaunchpadRequest(LaunchpadBrowserRequestMixin):
38 """Mixin request class to provide stepstogo."""
39@@ -798,8 +808,8 @@
40 return request.response
41
42
43-class LaunchpadTestRequest(TestRequest, ErrorReportRequest,
44- LaunchpadBrowserRequestMixin):
45+class LaunchpadTestRequest(LaunchpadBrowserRequestMixin,
46+ TestRequest, ErrorReportRequest):
47 """Mock request for use in unit and functional tests.
48
49 >>> request = LaunchpadTestRequest(SERVER_URL='http://127.0.0.1/foo/bar')
50
51=== modified file 'lib/canonical/launchpad/webapp/tests/test_servers.py'
52--- lib/canonical/launchpad/webapp/tests/test_servers.py 2010-10-25 13:16:10 +0000
53+++ lib/canonical/launchpad/webapp/tests/test_servers.py 2011-08-11 20:06:37 +0000
54@@ -35,6 +35,7 @@
55 from canonical.launchpad.webapp.servers import (
56 ApplicationServerSettingRequestFactory,
57 LaunchpadBrowserRequest,
58+ LaunchpadTestRequest,
59 VHostWebServiceRequestPublicationFactory,
60 VirtualHostRequestPublicationFactory,
61 WebServiceClientRequest,
62@@ -358,20 +359,90 @@
63 retried_request.response.getHeader('Vary'),
64 'Cookie, Authorization')
65
66+
67+class TestLaunchpadBrowserRequestMixin:
68+ """Tests for `LaunchpadBrowserRequestMixin`.
69+
70+ As `LaunchpadBrowserRequestMixin` is a mixin, it needs to be tested when
71+ mixed into another class, hence why this does not inherit from `TestCase`.
72+ """
73+
74+ request_factory = None # Specify in subclasses.
75+
76 def test_is_ajax_false(self):
77 """Normal requests do not define HTTP_X_REQUESTED_WITH."""
78- request = LaunchpadBrowserRequest(StringIO.StringIO(''), {})
79+ request = self.request_factory(StringIO.StringIO(''), {})
80
81 self.assertFalse(request.is_ajax)
82
83 def test_is_ajax_true(self):
84 """Requests with HTTP_X_REQUESTED_WITH set are ajax requests."""
85- request = LaunchpadBrowserRequest(StringIO.StringIO(''), {
86+ request = self.request_factory(StringIO.StringIO(''), {
87 'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest',
88 })
89
90 self.assertTrue(request.is_ajax)
91
92+ def test_getURL(self):
93+ """
94+ getURL() overrides HTTPRequest.getURL(), but behaves identically by
95+ default.
96+ """
97+ environ = {
98+ "SERVER_URL": "http://geturl.example.com",
99+ "SCRIPT_NAME": "/sabbra/cadabra",
100+ "QUERY_STRING": "tuesday=gone",
101+ }
102+ request = self.request_factory(StringIO.StringIO(''), environ)
103+ self.assertEqual(
104+ "http://geturl.example.com/sabbra/cadabra",
105+ request.getURL())
106+ self.assertEqual(
107+ "http://geturl.example.com/sabbra",
108+ request.getURL(level=1))
109+ self.assertEqual(
110+ "/sabbra/cadabra",
111+ request.getURL(path_only=True))
112+
113+ def test_getURL_include_query(self):
114+ """
115+ getURL() overrides HTTPRequest.getURL(), but appends the query string
116+ if include_query=True.
117+ """
118+ environ = {
119+ "SERVER_URL": "http://geturl.example.com",
120+ "SCRIPT_NAME": "/sabbra/cadabra",
121+ "QUERY_STRING": "tuesday=gone",
122+ }
123+ request = self.request_factory(StringIO.StringIO(''), environ)
124+ self.assertEqual(
125+ "http://geturl.example.com/sabbra/cadabra?tuesday=gone",
126+ request.getURL(include_query=True))
127+ self.assertEqual(
128+ "http://geturl.example.com/sabbra?tuesday=gone",
129+ request.getURL(include_query=True, level=1))
130+ self.assertEqual(
131+ "/sabbra/cadabra?tuesday=gone",
132+ request.getURL(include_query=True, path_only=True))
133+
134+
135+class TestLaunchpadBrowserRequestMixinWithLaunchpadBrowserRequest(
136+ TestLaunchpadBrowserRequestMixin, TestCase):
137+ """
138+ Tests for `LaunchpadBrowserRequestMixin` as found in
139+ `LaunchpadBrowserRequest`.
140+ """
141+ request_factory = LaunchpadBrowserRequest
142+
143+
144+class TestLaunchpadBrowserRequestMixinWithLaunchpadTestRequest(
145+ TestLaunchpadBrowserRequestMixin, TestCase):
146+ """
147+ Tests for `LaunchpadBrowserRequestMixin` as found in
148+ `LaunchpadTestRequest`.
149+ """
150+ request_factory = LaunchpadTestRequest
151+
152
153 class IThingSet(Interface):
154 """Marker interface for a set of things."""
155
156=== modified file 'lib/lp/registry/browser/distroseries.py'
157--- lib/lp/registry/browser/distroseries.py 2011-08-09 15:55:15 +0000
158+++ lib/lp/registry/browser/distroseries.py 2011-08-11 20:06:37 +0000
159@@ -901,16 +901,22 @@
160 self.context, destination_pocket, include_binaries=False,
161 dest_url=series_url, dest_display_name=series_title,
162 person=self.user, force_async=True):
163- # The copy worked so we can redirect back to the page to
164- # show the results.
165- self.next_url = self.request.URL
166+ # The copy worked so we redirect back to show the results. Include
167+ # the query string so that the user ends up on the same batch page
168+ # with the same filtering parameters as before.
169+ self.next_url = self.request.getURL(include_query=True)
170
171 @property
172 def action_url(self):
173- """The forms should post to themselves, including GET params to
174- account for batch parameters.
175+ """The request URL including query string.
176+
177+ Forms should post to the view with a query string containing the
178+ active batch and filtering parameters. Actions should then redirect
179+ using that information so that the user is left on the same batch
180+ page, with the same filtering parameters, as the page from which they
181+ submitted the form.
182 """
183- return "%s?%s" % (self.request.getURL(), self.request['QUERY_STRING'])
184+ return self.request.getURL(include_query=True)
185
186 def validate_sync(self, action, data):
187 """Validate selected differences."""
188
189=== modified file 'lib/lp/registry/browser/tests/test_distroseries.py'
190--- lib/lp/registry/browser/tests/test_distroseries.py 2011-08-09 15:55:57 +0000
191+++ lib/lp/registry/browser/tests/test_distroseries.py 2011-08-11 20:06:37 +0000
192@@ -10,6 +10,7 @@
193 import re
194 from textwrap import TextWrapper
195 from urllib import urlencode
196+from urlparse import urlparse
197
198 from BeautifulSoup import BeautifulSoup
199 from lazr.restful.interfaces import IJSONRequestCache
200@@ -42,17 +43,17 @@
201 from canonical.launchpad.webapp.interaction import get_current_principal
202 from canonical.launchpad.webapp.interfaces import BrowserNotificationLevel
203 from canonical.launchpad.webapp.publisher import canonical_url
204+from canonical.launchpad.webapp.url import urlappend
205 from canonical.testing.layers import (
206 DatabaseFunctionalLayer,
207 LaunchpadFunctionalLayer,
208 LaunchpadZopelessLayer,
209 )
210-from canonical.launchpad.webapp.url import urlappend
211 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
212 from lp.archivepublisher.debversion import Version
213 from lp.registry.browser.distroseries import (
214+ ALL,
215 HIGHER_VERSION_THAN_PARENT,
216- ALL,
217 NON_IGNORED,
218 RESOLVED,
219 seriesToVocab,
220@@ -2067,11 +2068,10 @@
221 # 302 is a redirect back to the same page.
222 self.assertEqual(302, view.request.response.getStatus())
223
224- def test_sync_notification_on_success(self):
225- # A user with upload rights on the destination archive can
226- # sync packages. Notifications about the synced packages are
227- # displayed and the packages are copied inside the destination
228- # series.
229+ def test_sync_success(self):
230+ # A user with upload rights on the destination archive can sync
231+ # packages. Notifications about the synced packages are displayed and
232+ # the packages are copied inside the destination series.
233 versions = {
234 'base': '1.0',
235 'derived': '1.0derived1',
236@@ -2094,13 +2094,20 @@
237 # Now, sync the source from the parent using the form.
238 set_derived_series_sync_feature_flag(self)
239 view = self._syncAndGetView(
240- derived_series, person, [diff_id])
241+ derived_series, person, [diff_id], query_string=(
242+ "batch=12&start=24&my-old-man=dustman"))
243
244 # The parent's version should now be in the derived series and
245 # the notifications displayed:
246 self.assertPackageCopied(
247 derived_series, 'my-src-name', versions['parent'], view)
248
249+ # The URL to which the browser is redirected has same batch and
250+ # filtering options as where the sync request was made.
251+ self.assertEqual(
252+ "batch=12&start=24&my-old-man=dustman",
253+ urlparse(view.next_url).query)
254+
255 def test_sync_success_not_yet_in_derived_series(self):
256 # If the package to sync does not exist yet in the derived series,
257 # upload right to any component inside the destination series will be