Merge ~cjwatson/launchpad:mypy-next-cancel-url into launchpad:master

Proposed by Colin Watson
Status: Rejected
Rejected by: Colin Watson
Proposed branch: ~cjwatson/launchpad:mypy-next-cancel-url
Merge into: launchpad:master
Diff against target: 1668 lines (+204/-168)
54 files modified
lib/lp/answers/browser/faq.py (+1/-1)
lib/lp/answers/browser/faqtarget.py (+1/-1)
lib/lp/answers/browser/question.py (+3/-3)
lib/lp/answers/browser/questiontarget.py (+1/-1)
lib/lp/app/browser/launchpadform.py (+17/-3)
lib/lp/app/doc/launchpadform.rst (+1/-1)
lib/lp/blueprints/browser/specification.py (+7/-7)
lib/lp/blueprints/browser/specificationdependency.py (+1/-1)
lib/lp/blueprints/browser/sprint.py (+1/-1)
lib/lp/bugs/browser/bugalsoaffects.py (+2/-2)
lib/lp/bugs/browser/bugattachment.py (+4/-4)
lib/lp/bugs/browser/buglinktarget.py (+2/-2)
lib/lp/bugs/browser/bugmessage.py (+2/-2)
lib/lp/bugs/browser/bugtarget.py (+2/-2)
lib/lp/bugs/browser/bugtracker.py (+4/-4)
lib/lp/buildmaster/browser/builder.py (+1/-1)
lib/lp/charms/browser/charmrecipe.py (+10/-6)
lib/lp/code/browser/branch.py (+4/-4)
lib/lp/code/browser/branchmergeproposal.py (+9/-9)
lib/lp/code/browser/codeimport.py (+4/-2)
lib/lp/code/browser/gitref.py (+1/-1)
lib/lp/code/browser/gitrepository.py (+7/-5)
lib/lp/code/browser/sourcepackagerecipe.py (+5/-5)
lib/lp/coop/answersbugs/browser.py (+1/-1)
lib/lp/oci/browser/ocirecipe.py (+5/-5)
lib/lp/registry/browser/announcement.py (+16/-6)
lib/lp/registry/browser/codeofconduct.py (+2/-2)
lib/lp/registry/browser/distribution.py (+3/-3)
lib/lp/registry/browser/distributionmirror.py (+6/-6)
lib/lp/registry/browser/distroseries.py (+4/-4)
lib/lp/registry/browser/featuredproject.py (+2/-2)
lib/lp/registry/browser/karma.py (+1/-1)
lib/lp/registry/browser/milestone.py (+6/-4)
lib/lp/registry/browser/ociproject.py (+1/-1)
lib/lp/registry/browser/peoplemerge.py (+5/-3)
lib/lp/registry/browser/person.py (+11/-11)
lib/lp/registry/browser/poll.py (+4/-4)
lib/lp/registry/browser/product.py (+2/-2)
lib/lp/registry/browser/productrelease.py (+4/-4)
lib/lp/registry/browser/productseries.py (+2/-2)
lib/lp/registry/browser/sourcepackage.py (+3/-3)
lib/lp/registry/browser/team.py (+11/-11)
lib/lp/services/oauth/browser/__init__.py (+1/-1)
lib/lp/services/webhooks/browser.py (+2/-2)
lib/lp/snappy/browser/snap.py (+5/-5)
lib/lp/soyuz/browser/archive.py (+6/-6)
lib/lp/soyuz/browser/archivesubscription.py (+1/-1)
lib/lp/soyuz/browser/build.py (+1/-1)
lib/lp/soyuz/browser/distroarchseries.py (+1/-1)
lib/lp/soyuz/browser/livefs.py (+3/-3)
lib/lp/translations/browser/distroseries.py (+2/-2)
lib/lp/translations/browser/hastranslationimports.py (+1/-1)
lib/lp/translations/browser/person.py (+1/-1)
lib/lp/translations/browser/translationgroup.py (+1/-1)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+437108@code.launchpad.net

Commit message

Avoid substitution issue with next_url and cancel_url

Description of the change

Launchpad's form views are allowed to define `next_url` and `cancel_url` to indicate where the form should redirect to after a successful submission or if the user chooses to cancel. Some do this by assigning to those attributes (especially if the URL is computed based on the submitted form), while some do this by defining those attributes as properties (especially if there are multiple form actions that use the same URLs). There's no obvious way to settle on one approach or the other here, but until now it hasn't mattered.

Unfortunately, `mypy` detects a technical difficulty here. If a base class declares something as a plain attribute, then a subclass that redefines that as a property without also providing a setter (which wouldn't really make sense here) violates the Liskov substitution principle, because assigning to that attribute is allowed for instances of the base class but forbidden for instances of the subclass. Conversely, if a base class declares something as a property without a setter, then instances of the subclass may not assign to it; and if a base class declares something as a property with a setter, then subclasses may not redefine it as a property without a setter.

None of the options here are very nice, but it would be useful to get out of this particular jam so that we can extend `mypy` coverage further. The simplest approach I can find is to have `LaunchpadFormView` declare `next_url` and `cancel_url` as properties that return `mutable_next_url` and `mutable_cancel_url` respectively; then subclasses that want to assign to those attributes can assign to the mutable variants, and subclasses that want to define properties can continue doing so as before. The result is a little more verbose, but it works.

(I also considered defining a `Protocol` that would declare types for `next_url` and `cancel_url` without having to actually initialize them in the base class, but I don't see how to concisely ensure that all form views implement that protocol without having to subclass it and thus getting back to the base class problem above.)

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

Actually, I might have found a slightly less unpleasant way to do this. Testing ...

Revision history for this message
Colin Watson (cjwatson) wrote :

I have indeed. Withdrawing this one.

Unmerged commits

8f8c612... by Colin Watson

Avoid substitution issue with next_url and cancel_url

Launchpad's form views are allowed to define `next_url` and `cancel_url`
to indicate where the form should redirect to after a successful
submission or if the user chooses to cancel. Some do this by assigning
to those attributes (especially if the URL is computed based on the
submitted form), while some do this by defining those attributes as
properties (especially if there are multiple form actions that use the
same URLs). There's no obvious way to settle on one approach or the
other here, but until now it hasn't mattered.

Unfortunately, `mypy` detects a technical difficulty here. If a base
class declares something as a plain attribute, then a subclass that
redefines that as a property without also providing a setter (which
wouldn't really make sense here) violates the Liskov substitution
principle, because assigning to that attribute is allowed for instances
of the base class but forbidden for instances of the subclass.
Conversely, if a base class declares something as a property without a
setter, then instances of the subclass may not assign to it; and if a
base class declares something as a property with a setter, then
subclasses may not redefine it as a property without a setter.

None of the options here are very nice, but it would be useful to get
out of this particular jam so that we can extend `mypy` coverage
further. The simplest approach I can find is to have
`LaunchpadFormView` declare `next_url` and `cancel_url` as properties
that return `mutable_next_url` and `mutable_cancel_url` respectively;
then subclasses that want to assign to those attributes can assign to
the mutable variants, and subclasses that want to define properties can
continue doing so as before. The result is a little more verbose, but
it works.

(I also considered defining a `Protocol` that would declare types for
`next_url` and `cancel_url` without having to actually initialize them
in the base class, but I don't see how to concisely ensure that all form
views implement that protocol without having to subclass it and thus
getting back to the base class problem above.)

Succeeded
[SUCCEEDED] docs:0 (build)
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] mypy:0 (build)
13 of 3 results

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/answers/browser/faq.py b/lib/lp/answers/browser/faq.py
2index 732bd2d..724d27d 100644
3--- a/lib/lp/answers/browser/faq.py
4+++ b/lib/lp/answers/browser/faq.py
5@@ -78,4 +78,4 @@ class FAQEditView(LaunchpadEditFormView):
6 def save_action(self, action, data):
7 """Update the FAQ details."""
8 self.updateContextFromData(data)
9- self.next_url = canonical_url(self.context)
10+ self.mutable_next_url = canonical_url(self.context)
11diff --git a/lib/lp/answers/browser/faqtarget.py b/lib/lp/answers/browser/faqtarget.py
12index 7f37282..457d455 100644
13--- a/lib/lp/answers/browser/faqtarget.py
14+++ b/lib/lp/answers/browser/faqtarget.py
15@@ -51,4 +51,4 @@ class FAQCreateView(LaunchpadFormView):
16 data["content"],
17 keywords=data["keywords"],
18 )
19- self.next_url = canonical_url(faq)
20+ self.mutable_next_url = canonical_url(faq)
21diff --git a/lib/lp/answers/browser/question.py b/lib/lp/answers/browser/question.py
22index 65bbd94..3e1bf30 100644
23--- a/lib/lp/answers/browser/question.py
24+++ b/lib/lp/answers/browser/question.py
25@@ -340,7 +340,7 @@ class QuestionSetView(LaunchpadFormView):
26 scope = self.context
27 else:
28 scope = self.widgets["scope"].getInputValue()
29- self.next_url = "%s/+tickets?%s" % (
30+ self.mutable_next_url = "%s/+tickets?%s" % (
31 canonical_url(scope),
32 self.request["QUERY_STRING"],
33 )
34@@ -1199,7 +1199,7 @@ class QuestionWorkflowView(LaunchpadFormView, LinkFAQMixin):
35 _("You have subscribed to this question.")
36 )
37
38- self.next_url = canonical_url(self.context)
39+ self.mutable_next_url = canonical_url(self.context)
40
41 @property
42 def new_question_url(self):
43@@ -1416,7 +1416,7 @@ class QuestionCreateFAQView(LinkFAQMixin, LaunchpadFormView):
44 self.context.linkFAQ(self.user, faq, data["message"])
45
46 # Redirect to the question.
47- self.next_url = canonical_url(self.context)
48+ self.mutable_next_url = canonical_url(self.context)
49
50
51 class SearchableFAQRadioWidget(LaunchpadRadioWidget):
52diff --git a/lib/lp/answers/browser/questiontarget.py b/lib/lp/answers/browser/questiontarget.py
53index 8682cf9..1c33fb3 100644
54--- a/lib/lp/answers/browser/questiontarget.py
55+++ b/lib/lp/answers/browser/questiontarget.py
56@@ -850,7 +850,7 @@ class ManageAnswerContactView(UserSupportLanguagesMixin, LaunchpadFormView):
57 )
58 )
59
60- self.next_url = canonical_url(self.context, rootsite="answers")
61+ self.mutable_next_url = canonical_url(self.context, rootsite="answers")
62
63 @property
64 def administrated_teams(self):
65diff --git a/lib/lp/app/browser/launchpadform.py b/lib/lp/app/browser/launchpadform.py
66index 126eb3f..f6e12ae 100644
67--- a/lib/lp/app/browser/launchpadform.py
68+++ b/lib/lp/app/browser/launchpadform.py
69@@ -66,11 +66,25 @@ class LaunchpadFormView(LaunchpadView):
70 # Subset of fields to use
71 field_names = None # type: Optional[List[str]]
72
73- # The next URL to redirect to on successful form submission
74- next_url = None # type: Optional[str]
75+ # The next URL to redirect to on successful form submission. Set this
76+ # if your view needs to set the next URL in a way that can't easily be
77+ # done by redefining the `next_url` property instead.
78+ mutable_next_url = None # type: Optional[str]
79+ # The URL to use as the Cancel link in a form. Set this if your view
80+ # needs to set the cancel URL in a way that can't easily be done by
81+ # redefining the `cancel_url` property instead.
82+ mutable_cancel_url = None # type: Optional[str]
83+
84+ # The next URL to redirect to on successful form submission.
85+ @property
86+ def next_url(self) -> Optional[str]:
87+ return self.mutable_next_url
88+
89 # The cancel URL is rendered as a Cancel link in the form
90 # macro if set in a derived class.
91- cancel_url = None # type: Optional[str]
92+ @property
93+ def cancel_url(self) -> Optional[str]:
94+ return self.mutable_cancel_url
95
96 # The name of the widget that will receive initial focus in the form.
97 # By default, the first widget will receive focus. Set this to None
98diff --git a/lib/lp/app/doc/launchpadform.rst b/lib/lp/app/doc/launchpadform.rst
99index 996ec23..c237a8d 100644
100--- a/lib/lp/app/doc/launchpadform.rst
101+++ b/lib/lp/app/doc/launchpadform.rst
102@@ -402,7 +402,7 @@ Form Rendering
103 ...
104 ... @action("Redirect", name="redirect")
105 ... def redirect_action(self, action, data):
106- ... self.next_url = "http://launchpad.test/"
107+ ... self.mutable_next_url = "http://launchpad.test/"
108 ...
109 ... def handleUpdateFailure(self, action, data, errors):
110 ... return "Some errors occurred."
111diff --git a/lib/lp/blueprints/browser/specification.py b/lib/lp/blueprints/browser/specification.py
112index 1186390..1c0a987 100644
113--- a/lib/lp/blueprints/browser/specification.py
114+++ b/lib/lp/blueprints/browser/specification.py
115@@ -888,7 +888,7 @@ class SpecificationEditView(LaunchpadEditFormView):
116 self.request.response.addNotification(
117 'Blueprint is now considered "%s".' % new_status.title
118 )
119- self.next_url = canonical_url(self.context)
120+ self.mutable_next_url = canonical_url(self.context)
121
122
123 class SpecificationEditWhiteboardView(SpecificationEditView):
124@@ -908,7 +908,7 @@ class SpecificationEditWorkItemsView(SpecificationEditView):
125 def change_action(self, action, data):
126 with notify_modified(self.context, ["workitems_text"]):
127 self.context.setWorkItems(data["workitems_text"])
128- self.next_url = canonical_url(self.context)
129+ self.mutable_next_url = canonical_url(self.context)
130
131
132 class SpecificationEditPeopleView(SpecificationEditView):
133@@ -1015,7 +1015,7 @@ class SpecificationGoalProposeView(LaunchpadEditFormView):
134 def continue_action(self, action, data):
135 self.context.whiteboard = data["whiteboard"]
136 self.context.proposeGoal(data["distroseries"], self.user)
137- self.next_url = canonical_url(self.context)
138+ self.mutable_next_url = canonical_url(self.context)
139
140 @property
141 def cancel_url(self):
142@@ -1030,7 +1030,7 @@ class SpecificationProductSeriesGoalProposeView(SpecificationGoalProposeView):
143 def continue_action(self, action, data):
144 self.context.whiteboard = data["whiteboard"]
145 self.context.proposeGoal(data["productseries"], self.user)
146- self.next_url = canonical_url(self.context)
147+ self.mutable_next_url = canonical_url(self.context)
148
149 @property
150 def cancel_url(self):
151@@ -1186,7 +1186,7 @@ class SpecificationSupersedingView(LaunchpadFormView):
152 self.request.response.addNotification(
153 'Blueprint is now considered "%s".' % newstate.title
154 )
155- self.next_url = canonical_url(self.context)
156+ self.mutable_next_url = canonical_url(self.context)
157
158 @property
159 def cancel_url(self):
160@@ -1393,7 +1393,7 @@ class SpecificationSprintAddView(LaunchpadFormView):
161 @action(_("Continue"), name="continue")
162 def continue_action(self, action, data):
163 self.context.linkSprint(data["sprint"], self.user)
164- self.next_url = canonical_url(self.context)
165+ self.mutable_next_url = canonical_url(self.context)
166
167 @property
168 def cancel_url(self):
169@@ -1695,7 +1695,7 @@ class SpecificationSetView(AppFrontPageSearchView, HasSpecificationsView):
170 search_text = data["search_text"]
171 if search_text is not None:
172 url += "?searchtext=" + search_text
173- self.next_url = url
174+ self.mutable_next_url = url
175
176
177 @component.adapter(ISpecification, Interface, IWebServiceClientRequest)
178diff --git a/lib/lp/blueprints/browser/specificationdependency.py b/lib/lp/blueprints/browser/specificationdependency.py
179index cdf6ed7..121b6a5 100644
180--- a/lib/lp/blueprints/browser/specificationdependency.py
181+++ b/lib/lp/blueprints/browser/specificationdependency.py
182@@ -80,7 +80,7 @@ class SpecificationDependencyRemoveView(LaunchpadFormView):
183 @action("Continue", name="continue")
184 def continue_action(self, action, data):
185 self.context.removeDependency(data["dependency"])
186- self.next_url = canonical_url(self.context)
187+ self.mutable_next_url = canonical_url(self.context)
188
189 @property
190 def cancel_url(self):
191diff --git a/lib/lp/blueprints/browser/sprint.py b/lib/lp/blueprints/browser/sprint.py
192index 047084a..179aee9 100644
193--- a/lib/lp/blueprints/browser/sprint.py
194+++ b/lib/lp/blueprints/browser/sprint.py
195@@ -414,7 +414,7 @@ class SprintDeleteView(LaunchpadFormView):
196 def delete_action(self, action, data):
197 owner = self.context.owner
198 self.context.destroySelf()
199- self.next_url = canonical_url(owner)
200+ self.mutable_next_url = canonical_url(owner)
201
202
203 class SprintTopicSetView(HasSpecificationsView, LaunchpadView):
204diff --git a/lib/lp/bugs/browser/bugalsoaffects.py b/lib/lp/bugs/browser/bugalsoaffects.py
205index 0ec2af3..f7f66f8 100644
206--- a/lib/lp/bugs/browser/bugalsoaffects.py
207+++ b/lib/lp/bugs/browser/bugalsoaffects.py
208@@ -338,7 +338,7 @@ class BugTaskCreationStep(AlsoAffectsStep):
209 )
210
211 notify(ObjectCreatedEvent(task_added))
212- self.next_url = canonical_url(task_added)
213+ self.mutable_next_url = canonical_url(task_added)
214
215
216 class IAddDistroBugTaskForm(IAddBugTaskForm):
217@@ -982,4 +982,4 @@ class BugAlsoAffectsProductWithProductCreationView(
218
219 if set_bugtracker:
220 data["product"].bugtracker = view.task_added.bugwatch.bugtracker
221- self.next_url = canonical_url(view.task_added)
222+ self.mutable_next_url = canonical_url(view.task_added)
223diff --git a/lib/lp/bugs/browser/bugattachment.py b/lib/lp/bugs/browser/bugattachment.py
224index ce004b8..48534a1 100644
225--- a/lib/lp/bugs/browser/bugattachment.py
226+++ b/lib/lp/bugs/browser/bugattachment.py
227@@ -109,7 +109,7 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
228
229 def __init__(self, context, request):
230 LaunchpadFormView.__init__(self, context, request)
231- self.next_url = self.cancel_url = canonical_url(
232+ self.mutable_next_url = self.mutable_cancel_url = canonical_url(
233 ICanonicalUrlData(context).inside
234 )
235 if not context.libraryfile:
236@@ -158,8 +158,8 @@ class BugAttachmentEditView(LaunchpadFormView, BugAttachmentContentCheck):
237 if new_type_consistent_with_guessed_type:
238 self.context.type = new_type
239 else:
240- self.next_url = self.nextUrlForInconsistentPatchFlags(
241- self.context
242+ self.mutable_next_url = (
243+ self.nextUrlForInconsistentPatchFlags(self.context)
244 )
245 else:
246 self.context.type = new_type
247@@ -211,7 +211,7 @@ class BugAttachmentPatchConfirmationView(LaunchpadFormView):
248
249 def __init__(self, context, request):
250 LaunchpadFormView.__init__(self, context, request)
251- self.next_url = self.cancel_url = canonical_url(
252+ self.mutable_next_url = self.mutable_cancel_url = canonical_url(
253 ICanonicalUrlData(context).inside
254 )
255
256diff --git a/lib/lp/bugs/browser/buglinktarget.py b/lib/lp/bugs/browser/buglinktarget.py
257index 9dda500..512fe12 100644
258--- a/lib/lp/bugs/browser/buglinktarget.py
259+++ b/lib/lp/bugs/browser/buglinktarget.py
260@@ -76,7 +76,7 @@ class BugLinkView(LaunchpadFormView):
261 )
262 )
263 notify(ObjectModifiedEvent(self.context, target_unmodified, ["bugs"]))
264- self.next_url = canonical_url(self.context)
265+ self.mutable_next_url = canonical_url(self.context)
266
267
268 class BugLinksListingView(LaunchpadView):
269@@ -166,7 +166,7 @@ class BugsUnlinkView(LaunchpadFormView):
270 )
271 )
272 notify(ObjectModifiedEvent(self.context, target_unmodified, ["bugs"]))
273- self.next_url = canonical_url(self.context)
274+ self.mutable_next_url = canonical_url(self.context)
275
276 def bugsWithPermission(self):
277 """Return the bugs that the user has permission to remove. This
278diff --git a/lib/lp/bugs/browser/bugmessage.py b/lib/lp/bugs/browser/bugmessage.py
279index 30cff9e..ac7ffbc 100644
280--- a/lib/lp/bugs/browser/bugmessage.py
281+++ b/lib/lp/bugs/browser/bugmessage.py
282@@ -107,7 +107,7 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
283 "Thank you for your comment."
284 )
285
286- self.next_url = canonical_url(self.context)
287+ self.mutable_next_url = canonical_url(self.context)
288
289 attachment_description = data.get("attachment_description")
290
291@@ -150,7 +150,7 @@ class BugMessageAddFormView(LaunchpadFormView, BugAttachmentContentCheck):
292 )
293
294 if not patch_flag_consistent:
295- self.next_url = self.nextUrlForInconsistentPatchFlags(
296+ self.mutable_next_url = self.nextUrlForInconsistentPatchFlags(
297 attachment
298 )
299
300diff --git a/lib/lp/bugs/browser/bugtarget.py b/lib/lp/bugs/browser/bugtarget.py
301index 5ae70f2..89b33ad 100644
302--- a/lib/lp/bugs/browser/bugtarget.py
303+++ b/lib/lp/bugs/browser/bugtarget.py
304@@ -771,7 +771,7 @@ class FileBugViewBase(LaunchpadFormView):
305 "You have subscribed to this bug report."
306 )
307
308- self.next_url = canonical_url(bug.bugtasks[0])
309+ self.mutable_next_url = canonical_url(bug.bugtasks[0])
310
311 def showFileBugForm(self):
312 """Override this method in base classes to show the filebug form."""
313@@ -1357,7 +1357,7 @@ class OfficialBugTagsManageView(LaunchpadEditFormView):
314 def save_action(self, action, data):
315 """Action for saving new official bug tags."""
316 self.context.official_bug_tags = data["official_bug_tags"]
317- self.next_url = canonical_url(self.context)
318+ self.mutable_next_url = canonical_url(self.context)
319
320 @property
321 def tags_js_data(self):
322diff --git a/lib/lp/bugs/browser/bugtracker.py b/lib/lp/bugs/browser/bugtracker.py
323index 7460e9b..a69d798 100644
324--- a/lib/lp/bugs/browser/bugtracker.py
325+++ b/lib/lp/bugs/browser/bugtracker.py
326@@ -152,7 +152,7 @@ class BugTrackerAddView(LaunchpadFormView):
327 contactdetails=data["contactdetails"],
328 owner=getUtility(ILaunchBag).user,
329 )
330- self.next_url = canonical_url(bugtracker)
331+ self.mutable_next_url = canonical_url(bugtracker)
332
333 @property
334 def cancel_url(self):
335@@ -348,7 +348,7 @@ class BugTrackerEditView(LaunchpadEditFormView):
336 data["aliases"].append(current_baseurl)
337
338 self.updateContextFromData(data)
339- self.next_url = canonical_url(self.context)
340+ self.mutable_next_url = canonical_url(self.context)
341
342 @cachedproperty
343 def delete_not_possible_reasons(self):
344@@ -440,7 +440,7 @@ class BugTrackerEditView(LaunchpadEditFormView):
345 )
346
347 # Go back to the bug tracker listing.
348- self.next_url = canonical_url(getUtility(IBugTrackerSet))
349+ self.mutable_next_url = canonical_url(getUtility(IBugTrackerSet))
350
351 @property
352 def cancel_url(self):
353@@ -464,7 +464,7 @@ class BugTrackerEditView(LaunchpadEditFormView):
354 self.request.response.addInfoNotification(
355 "All bug watches on %s have been rescheduled." % self.context.title
356 )
357- self.next_url = canonical_url(self.context)
358+ self.mutable_next_url = canonical_url(self.context)
359
360
361 class BugTrackerNavigation(Navigation):
362diff --git a/lib/lp/buildmaster/browser/builder.py b/lib/lp/buildmaster/browser/builder.py
363index 1c24da5..c9d0e49 100644
364--- a/lib/lp/buildmaster/browser/builder.py
365+++ b/lib/lp/buildmaster/browser/builder.py
366@@ -466,7 +466,7 @@ class BuilderSetAddView(LaunchpadFormView):
367 restricted_resources=data.get("restricted_resources"),
368 )
369 notify(ObjectCreatedEvent(builder))
370- self.next_url = canonical_url(builder)
371+ self.mutable_next_url = canonical_url(builder)
372
373 @property
374 def page_title(self):
375diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py
376index f80aebb..e63611f 100644
377--- a/lib/lp/charms/browser/charmrecipe.py
378+++ b/lib/lp/charms/browser/charmrecipe.py
379@@ -301,8 +301,10 @@ def log_oops(error, request):
380 class CharmRecipeAuthorizeMixin:
381 def requestAuthorization(self, recipe):
382 try:
383- self.next_url = CharmRecipeAuthorizeView.requestAuthorization(
384- recipe, self.request
385+ self.mutable_next_url = (
386+ CharmRecipeAuthorizeView.requestAuthorization(
387+ recipe, self.request
388+ )
389 )
390 except BadRequestPackageUploadResponse as e:
391 self.setFieldError(
392@@ -393,7 +395,7 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
393 if data["store_upload"]:
394 self.requestAuthorization(recipe)
395 else:
396- self.next_url = canonical_url(recipe)
397+ self.mutable_next_url = canonical_url(recipe)
398
399 def validate(self, data):
400 super().validate(data)
401@@ -478,7 +480,7 @@ class BaseCharmRecipeEditView(
402 if need_charmhub_reauth:
403 self.requestAuthorization(self.context)
404 else:
405- self.next_url = canonical_url(self.context)
406+ self.mutable_next_url = canonical_url(self.context)
407
408 @property
409 def adapters(self):
410@@ -634,7 +636,9 @@ class CharmRecipeDeleteView(BaseCharmRecipeEditView):
411 def delete_action(self, action, data):
412 owner = self.context.owner
413 self.context.destroySelf()
414- self.next_url = canonical_url(owner, view_name="+charm-recipes")
415+ self.mutable_next_url = canonical_url(
416+ owner, view_name="+charm-recipes"
417+ )
418
419
420 class CharmRecipeRequestBuildsView(LaunchpadFormView):
421@@ -675,4 +679,4 @@ class CharmRecipeRequestBuildsView(LaunchpadFormView):
422 self.request.response.addNotification(
423 _("Builds will be dispatched soon.")
424 )
425- self.next_url = self.cancel_url
426+ self.mutable_next_url = self.cancel_url
427diff --git a/lib/lp/code/browser/branch.py b/lib/lp/code/browser/branch.py
428index 0b4add6..51c6dd3 100644
429--- a/lib/lp/code/browser/branch.py
430+++ b/lib/lp/code/browser/branch.py
431@@ -652,7 +652,7 @@ class BranchRescanView(LaunchpadEditFormView):
432 def rescan(self, action, data):
433 self.context.unscan(rescan=True)
434 self.request.response.addNotification("Branch scan scheduled")
435- self.next_url = canonical_url(self.context)
436+ self.mutable_next_url = canonical_url(self.context)
437
438
439 class BranchEditFormView(LaunchpadEditFormView):
440@@ -1030,7 +1030,7 @@ class BranchDeletionView(LaunchpadFormView):
441 if self.all_permitted():
442 # Since the user is going to delete the branch, we need to have
443 # somewhere valid to send them next.
444- self.next_url = canonical_url(branch.target)
445+ self.mutable_next_url = canonical_url(branch.target)
446 message = "Branch %s deleted." % branch.unique_name
447 self.context.destroySelf(break_references=True)
448 self.request.response.addNotification(message)
449@@ -1038,7 +1038,7 @@ class BranchDeletionView(LaunchpadFormView):
450 self.request.response.addNotification(
451 "This branch cannot be deleted."
452 )
453- self.next_url = canonical_url(branch)
454+ self.mutable_next_url = canonical_url(branch)
455
456 @property
457 def branch_deletion_actions(self):
458@@ -1393,7 +1393,7 @@ class RegisterBranchMergeProposalView(LaunchpadFormView):
459 )
460 return None
461 else:
462- self.next_url = canonical_url(proposal)
463+ self.mutable_next_url = canonical_url(proposal)
464 except InvalidBranchMergeProposal as error:
465 self.addError(str(error))
466
467diff --git a/lib/lp/code/browser/branchmergeproposal.py b/lib/lp/code/browser/branchmergeproposal.py
468index a789afb..72dcf5d 100644
469--- a/lib/lp/code/browser/branchmergeproposal.py
470+++ b/lib/lp/code/browser/branchmergeproposal.py
471@@ -656,7 +656,7 @@ class BranchMergeProposalView(
472 request.claimReview(self.user)
473 except ClaimReviewFailed as e:
474 self.request.response.addErrorNotification(str(e))
475- self.next_url = canonical_url(self.context)
476+ self.mutable_next_url = canonical_url(self.context)
477
478 @property
479 def comment_location(self):
480@@ -899,7 +899,7 @@ class BranchMergeProposalRescanView(LaunchpadEditFormView):
481 if target_job and target_job.job.status == JobStatus.FAILED:
482 self.context.merge_target.rescan()
483 self.request.response.addNotification("Rescan scheduled")
484- self.next_url = canonical_url(self.context)
485+ self.mutable_next_url = canonical_url(self.context)
486
487
488 @delegate_to(ICodeReviewVoteReference)
489@@ -1046,7 +1046,7 @@ class BranchMergeProposalScheduleUpdateDiffView(LaunchpadEditFormView):
490 def update(self, action, data):
491 self.context.scheduleDiffUpdates()
492 self.request.response.addNotification("Diff update scheduled")
493- self.next_url = canonical_url(self.context)
494+ self.mutable_next_url = canonical_url(self.context)
495
496
497 class IReviewRequest(Interface):
498@@ -1120,8 +1120,8 @@ class MergeProposalEditView(
499
500 def initialize(self):
501 # Record next_url and cancel url now
502- self.next_url = canonical_url(self.context)
503- self.cancel_url = self.next_url
504+ self.mutable_next_url = canonical_url(self.context)
505+ self.mutable_cancel_url = self.next_url
506 super().initialize()
507
508
509@@ -1157,7 +1157,7 @@ class BranchMergeProposalResubmitView(
510 )
511
512 def initialize(self):
513- self.cancel_url = canonical_url(self.context)
514+ self.mutable_cancel_url = canonical_url(self.context)
515 super().initialize()
516
517 @property
518@@ -1222,12 +1222,12 @@ class BranchMergeProposalResubmitView(
519 url=canonical_url(e.existing_proposal),
520 )
521 self.request.response.addErrorNotification(message)
522- self.next_url = canonical_url(self.context)
523+ self.mutable_next_url = canonical_url(self.context)
524 return None
525 except InvalidBranchMergeProposal as e:
526 self.addError(str(e))
527 return None
528- self.next_url = canonical_url(proposal)
529+ self.mutable_next_url = canonical_url(proposal)
530 return proposal
531
532
533@@ -1291,7 +1291,7 @@ class BranchMergeProposalDeleteView(MergeProposalEditView):
534 """Delete the merge proposal and go back to the source branch."""
535 self.context.deleteProposal()
536 # Override the next url to be the source branch.
537- self.next_url = canonical_url(self.merge_source)
538+ self.mutable_next_url = canonical_url(self.merge_source)
539
540
541 class BranchMergeProposalMergedView(LaunchpadEditFormView):
542diff --git a/lib/lp/code/browser/codeimport.py b/lib/lp/code/browser/codeimport.py
543index 7856269..122fcc2 100644
544--- a/lib/lp/code/browser/codeimport.py
545+++ b/lib/lp/code/browser/codeimport.py
546@@ -487,7 +487,7 @@ class CodeImportNewView(CodeImportBaseView, CodeImportNameValidationMixin):
547 self.user,
548 )
549
550- self.next_url = canonical_url(code_import.target)
551+ self.mutable_next_url = canonical_url(code_import.target)
552
553 self.request.response.addNotification(
554 """
555@@ -660,7 +660,9 @@ class CodeImportEditView(CodeImportBaseView):
556 if not self._is_edit_user and not self._is_moderator_user:
557 raise Unauthorized
558 # The next and cancel location is the target details page.
559- self.cancel_url = self.next_url = canonical_url(self.context)
560+ self.mutable_cancel_url = self.mutable_next_url = canonical_url(
561+ self.context
562+ )
563 super().initialize()
564
565 @property
566diff --git a/lib/lp/code/browser/gitref.py b/lib/lp/code/browser/gitref.py
567index df8c704..98930f2 100644
568--- a/lib/lp/code/browser/gitref.py
569+++ b/lib/lp/code/browser/gitref.py
570@@ -456,7 +456,7 @@ class GitRefRegisterMergeProposalView(LaunchpadFormView):
571 )
572 return None
573 else:
574- self.next_url = canonical_url(proposal)
575+ self.mutable_next_url = canonical_url(proposal)
576 except InvalidBranchMergeProposal as error:
577 self.addError(str(error))
578
579diff --git a/lib/lp/code/browser/gitrepository.py b/lib/lp/code/browser/gitrepository.py
580index c3724f0..84f29f2 100644
581--- a/lib/lp/code/browser/gitrepository.py
582+++ b/lib/lp/code/browser/gitrepository.py
583@@ -586,7 +586,7 @@ class GitRepositoryForkView(LaunchpadEditFormView):
584 def fork(self, action, data):
585 forked = self.context.fork(self.user, data.get("owner"))
586 self.request.response.addNotification("Repository forked.")
587- self.next_url = canonical_url(forked)
588+ self.mutable_next_url = canonical_url(forked)
589
590 @property
591 def cancel_url(self):
592@@ -603,7 +603,7 @@ class GitRepositoryRescanView(LaunchpadEditFormView):
593 def rescan(self, action, data):
594 self.context.rescan()
595 self.request.response.addNotification("Repository scan scheduled")
596- self.next_url = canonical_url(self.context)
597+ self.mutable_next_url = canonical_url(self.context)
598
599
600 class GitRepositoryEditFormView(LaunchpadEditFormView):
601@@ -1545,7 +1545,9 @@ class GitRepositoryPermissionsView(LaunchpadFormView):
602 self.request.response.addNotification(
603 "Saved permissions for %s" % self.context.identity
604 )
605- self.next_url = canonical_url(self.context, view_name="+permissions")
606+ self.mutable_next_url = canonical_url(
607+ self.context, view_name="+permissions"
608+ )
609
610
611 class GitRepositoryDeletionView(LaunchpadFormView):
612@@ -1601,7 +1603,7 @@ class GitRepositoryDeletionView(LaunchpadFormView):
613 if self.all_permitted():
614 # Since the user is going to delete the repository, we need to
615 # have somewhere valid to send them next.
616- self.next_url = canonical_url(repository.target)
617+ self.mutable_next_url = canonical_url(repository.target)
618 message = "Repository %s deleted." % repository.unique_name
619 self.context.destroySelf(break_references=True)
620 self.request.response.addNotification(message)
621@@ -1609,7 +1611,7 @@ class GitRepositoryDeletionView(LaunchpadFormView):
622 self.request.response.addNotification(
623 "This repository cannot be deleted."
624 )
625- self.next_url = canonical_url(repository)
626+ self.mutable_next_url = canonical_url(repository)
627
628 @property
629 def repository_deletion_actions(self):
630diff --git a/lib/lp/code/browser/sourcepackagerecipe.py b/lib/lp/code/browser/sourcepackagerecipe.py
631index 002520d..f80dbec 100644
632--- a/lib/lp/code/browser/sourcepackagerecipe.py
633+++ b/lib/lp/code/browser/sourcepackagerecipe.py
634@@ -458,7 +458,7 @@ class SourcePackageRecipeRequestBuildsHtmlView(
635 @action("Request builds", name="request")
636 def request_action(self, action, data):
637 builds, informational = self.requestBuild(data)
638- self.next_url = self.cancel_url
639+ self.mutable_next_url = self.cancel_url
640 already_pending = informational.get("already_pending")
641 notification_text = new_builds_notification_text(
642 builds, already_pending
643@@ -554,7 +554,7 @@ class SourcePackageRecipeRequestDailyBuildView(LaunchpadFormView):
644 builds = recipe.performDailyBuild()
645 except ArchiveDisabled as e:
646 self.request.response.addErrorNotification(str(e))
647- self.next_url = canonical_url(recipe)
648+ self.mutable_next_url = canonical_url(recipe)
649 return
650
651 if self.request.is_ajax:
652@@ -566,7 +566,7 @@ class SourcePackageRecipeRequestDailyBuildView(LaunchpadFormView):
653 contains_unbuildable = recipe.containsUnbuildableSeries(
654 recipe.daily_build_archive
655 )
656- self.next_url = canonical_url(recipe)
657+ self.mutable_next_url = canonical_url(recipe)
658 self.request.response.addNotification(
659 new_builds_notification_text(
660 builds, contains_unbuildable=contains_unbuildable
661@@ -915,7 +915,7 @@ class SourcePackageRecipeAddView(
662 except ErrorHandled:
663 return
664
665- self.next_url = canonical_url(source_package_recipe)
666+ self.mutable_next_url = canonical_url(source_package_recipe)
667
668 def validate(self, data):
669 super().validate(data)
670@@ -1038,7 +1038,7 @@ class SourcePackageRecipeEditView(
671 )
672 )
673
674- self.next_url = canonical_url(self.context)
675+ self.mutable_next_url = canonical_url(self.context)
676
677 @property
678 def adapters(self):
679diff --git a/lib/lp/coop/answersbugs/browser.py b/lib/lp/coop/answersbugs/browser.py
680index 879a801..9aa286c 100644
681--- a/lib/lp/coop/answersbugs/browser.py
682+++ b/lib/lp/coop/answersbugs/browser.py
683@@ -65,4 +65,4 @@ class QuestionMakeBugView(LaunchpadFormView):
684 self.request.response.addNotification(
685 _("Thank you! Bug #$bugid created.", mapping={"bugid": bug.id})
686 )
687- self.next_url = canonical_url(bug)
688+ self.mutable_next_url = canonical_url(bug)
689diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
690index 1e5e06e..877b02b 100644
691--- a/lib/lp/oci/browser/ocirecipe.py
692+++ b/lib/lp/oci/browser/ocirecipe.py
693@@ -788,7 +788,7 @@ class OCIRecipeEditPushRulesView(LaunchpadFormView):
694
695 if not self.errors:
696 self.request.response.addNotification("Saved push rules")
697- self.next_url = canonical_url(self.context)
698+ self.mutable_next_url = canonical_url(self.context)
699
700
701 class OCIRecipeRequestBuildsView(LaunchpadFormView):
702@@ -842,7 +842,7 @@ class OCIRecipeRequestBuildsView(LaunchpadFormView):
703 "Your builds were scheduled and should start soon. "
704 "Refresh this page for details."
705 )
706- self.next_url = self.cancel_url
707+ self.mutable_next_url = self.cancel_url
708
709
710 class IOCIRecipeEditSchema(Interface):
711@@ -1127,7 +1127,7 @@ class OCIRecipeAddView(
712 # image_name is only available if using distribution credentials.
713 image_name=data.get("image_name"),
714 )
715- self.next_url = canonical_url(recipe)
716+ self.mutable_next_url = canonical_url(recipe)
717
718
719 class BaseOCIRecipeEditView(LaunchpadEditFormView):
720@@ -1159,7 +1159,7 @@ class BaseOCIRecipeEditView(LaunchpadEditFormView):
721 )
722
723 self.updateContextFromData(data)
724- self.next_url = canonical_url(self.context)
725+ self.mutable_next_url = canonical_url(self.context)
726
727 @property
728 def adapters(self):
729@@ -1350,4 +1350,4 @@ class OCIRecipeDeleteView(BaseOCIRecipeEditView):
730 def delete_action(self, action, data):
731 oci_project = self.context.oci_project
732 self.context.destroySelf()
733- self.next_url = canonical_url(oci_project)
734+ self.mutable_next_url = canonical_url(oci_project)
735diff --git a/lib/lp/registry/browser/announcement.py b/lib/lp/registry/browser/announcement.py
736index ef7957e..6c91284 100644
737--- a/lib/lp/registry/browser/announcement.py
738+++ b/lib/lp/registry/browser/announcement.py
739@@ -146,7 +146,7 @@ class AnnouncementAddView(LaunchpadFormView):
740 url=data.get("url"),
741 publication_date=data.get("publication_date"),
742 )
743- self.next_url = canonical_url(self.context)
744+ self.mutable_next_url = canonical_url(self.context)
745
746 @property
747 def action_url(self):
748@@ -184,7 +184,9 @@ class AnnouncementEditView(AnnouncementFormMixin, LaunchpadFormView):
749 summary=data.get("summary"),
750 url=data.get("url"),
751 )
752- self.next_url = canonical_url(self.context.target) + "/+announcements"
753+ self.mutable_next_url = (
754+ canonical_url(self.context.target) + "/+announcements"
755+ )
756
757
758 class AnnouncementRetargetForm(Interface):
759@@ -234,7 +236,9 @@ class AnnouncementRetargetView(AnnouncementFormMixin, LaunchpadFormView):
760 def retarget_action(self, action, data):
761 target = data.get("target")
762 self.context.retarget(target)
763- self.next_url = canonical_url(self.context.target) + "/+announcements"
764+ self.mutable_next_url = (
765+ canonical_url(self.context.target) + "/+announcements"
766+ )
767
768
769 class AnnouncementPublishView(AnnouncementFormMixin, LaunchpadFormView):
770@@ -250,7 +254,9 @@ class AnnouncementPublishView(AnnouncementFormMixin, LaunchpadFormView):
771 def publish_action(self, action, data):
772 publication_date = data["publication_date"]
773 self.context.setPublicationDate(publication_date)
774- self.next_url = canonical_url(self.context.target) + "/+announcements"
775+ self.mutable_next_url = (
776+ canonical_url(self.context.target) + "/+announcements"
777+ )
778
779
780 class AnnouncementRetractView(AnnouncementFormMixin, LaunchpadFormView):
781@@ -262,7 +268,9 @@ class AnnouncementRetractView(AnnouncementFormMixin, LaunchpadFormView):
782 @action(_("Retract"), name="retract")
783 def retract_action(self, action, data):
784 self.context.retract()
785- self.next_url = canonical_url(self.context.target) + "/+announcements"
786+ self.mutable_next_url = (
787+ canonical_url(self.context.target) + "/+announcements"
788+ )
789
790
791 class AnnouncementDeleteView(AnnouncementFormMixin, LaunchpadFormView):
792@@ -274,7 +282,9 @@ class AnnouncementDeleteView(AnnouncementFormMixin, LaunchpadFormView):
793 @action(_("Delete"), name="delete", validator="validate_cancel")
794 def action_delete(self, action, data):
795 self.context.destroySelf()
796- self.next_url = canonical_url(self.context.target) + "/+announcements"
797+ self.mutable_next_url = (
798+ canonical_url(self.context.target) + "/+announcements"
799+ )
800
801
802 @implementer(IAnnouncementCreateMenu)
803diff --git a/lib/lp/registry/browser/codeofconduct.py b/lib/lp/registry/browser/codeofconduct.py
804index 3c47070..120891a 100644
805--- a/lib/lp/registry/browser/codeofconduct.py
806+++ b/lib/lp/registry/browser/codeofconduct.py
807@@ -198,7 +198,7 @@ class AffirmCodeOfConductView(LaunchpadFormView):
808 if error_message:
809 self.addError(error_message)
810 return
811- self.next_url = canonical_url(self.user) + "/+codesofconduct"
812+ self.mutable_next_url = canonical_url(self.user) + "/+codesofconduct"
813
814
815 class SignedCodeOfConductAddView(LaunchpadFormView):
816@@ -222,7 +222,7 @@ class SignedCodeOfConductAddView(LaunchpadFormView):
817 if error_message:
818 self.addError(error_message)
819 return
820- self.next_url = canonical_url(self.user) + "/+codesofconduct"
821+ self.mutable_next_url = canonical_url(self.user) + "/+codesofconduct"
822
823 @property
824 def current(self):
825diff --git a/lib/lp/registry/browser/distribution.py b/lib/lp/registry/browser/distribution.py
826index 7180941..d2f3b4b 100644
827--- a/lib/lp/registry/browser/distribution.py
828+++ b/lib/lp/registry/browser/distribution.py
829@@ -1100,7 +1100,7 @@ class DistributionAddView(
830 )
831
832 notify(ObjectCreatedEvent(distribution))
833- self.next_url = canonical_url(distribution)
834+ self.mutable_next_url = canonical_url(distribution)
835
836
837 class DistributionEditView(
838@@ -1256,7 +1256,7 @@ class DistributionAdminView(LaunchpadEditFormView):
839 @action("Change", name="change")
840 def change_action(self, action, data):
841 self.updateContextFromData(data)
842- self.next_url = canonical_url(self.context)
843+ self.mutable_next_url = canonical_url(self.context)
844
845
846 class DistributionSeriesBaseView(LaunchpadView):
847@@ -1650,4 +1650,4 @@ class DistributionPublisherConfigView(LaunchpadFormView):
848 self.request.response.addInfoNotification(
849 "Your changes have been applied."
850 )
851- self.next_url = canonical_url(self.context)
852+ self.mutable_next_url = canonical_url(self.context)
853diff --git a/lib/lp/registry/browser/distributionmirror.py b/lib/lp/registry/browser/distributionmirror.py
854index 262784e..527e493 100644
855--- a/lib/lp/registry/browser/distributionmirror.py
856+++ b/lib/lp/registry/browser/distributionmirror.py
857@@ -198,10 +198,10 @@ class DistributionMirrorDeleteView(LaunchpadFormView):
858 self.request.response.addInfoNotification(
859 "This mirror has been probed and thus can't be deleted."
860 )
861- self.next_url = canonical_url(self.context)
862+ self.mutable_next_url = canonical_url(self.context)
863 return
864
865- self.next_url = canonical_url(
866+ self.mutable_next_url = canonical_url(
867 self.context.distribution, view_name="+pendingreviewmirrors"
868 )
869 self.request.response.addInfoNotification(
870@@ -265,7 +265,7 @@ class DistributionMirrorAddView(LaunchpadFormView):
871 official_candidate=data["official_candidate"],
872 )
873
874- self.next_url = canonical_url(mirror)
875+ self.mutable_next_url = canonical_url(mirror)
876 notify(ObjectCreatedEvent(mirror))
877
878
879@@ -296,7 +296,7 @@ class DistributionMirrorReviewView(LaunchpadEditFormView):
880 context.reviewer = self.user
881 context.date_reviewed = datetime.now(pytz.timezone("UTC"))
882 self.updateContextFromData(data)
883- self.next_url = canonical_url(context)
884+ self.mutable_next_url = canonical_url(context)
885
886
887 class DistributionMirrorEditView(LaunchpadEditFormView):
888@@ -335,7 +335,7 @@ class DistributionMirrorEditView(LaunchpadEditFormView):
889 @action(_("Save"), name="save")
890 def action_save(self, action, data):
891 self.updateContextFromData(data)
892- self.next_url = canonical_url(self.context)
893+ self.mutable_next_url = canonical_url(self.context)
894
895
896 class DistributionMirrorResubmitView(LaunchpadEditFormView):
897@@ -359,7 +359,7 @@ class DistributionMirrorResubmitView(LaunchpadEditFormView):
898 "The mirror is not in the correct state"
899 " (broken) and cannot be resubmitted."
900 )
901- self.next_url = canonical_url(self.context)
902+ self.mutable_next_url = canonical_url(self.context)
903
904
905 class DistributionMirrorReassignmentView(ObjectReassignmentView):
906diff --git a/lib/lp/registry/browser/distroseries.py b/lib/lp/registry/browser/distroseries.py
907index f1a25fe..facb566 100644
908--- a/lib/lp/registry/browser/distroseries.py
909+++ b/lib/lp/registry/browser/distroseries.py
910@@ -655,7 +655,7 @@ class DistroSeriesEditView(LaunchpadEditFormView, SeriesStatusMixin):
911 self.request.response.addInfoNotification(
912 "Your changes have been applied."
913 )
914- self.next_url = canonical_url(self.context)
915+ self.mutable_next_url = canonical_url(self.context)
916
917
918 class DistroSeriesAdminView(LaunchpadEditFormView, SeriesStatusMixin):
919@@ -710,7 +710,7 @@ class DistroSeriesAdminView(LaunchpadEditFormView, SeriesStatusMixin):
920 self.request.response.addInfoNotification(
921 "Your changes have been applied."
922 )
923- self.next_url = canonical_url(self.context)
924+ self.mutable_next_url = canonical_url(self.context)
925
926
927 class IDistroSeriesAddForm(Interface):
928@@ -773,7 +773,7 @@ class DistroSeriesAddView(LaunchpadFormView):
929 registrant=self.user,
930 )
931 notify(ObjectCreatedEvent(distroseries))
932- self.next_url = canonical_url(distroseries)
933+ self.mutable_next_url = canonical_url(distroseries)
934 return distroseries
935
936 @property
937@@ -1050,7 +1050,7 @@ class DistroSeriesDifferenceBaseView(
938 # The copy worked so we redirect back to show the results. Include
939 # the query string so that the user ends up on the same batch page
940 # with the same filtering parameters as before.
941- self.next_url = self.request.getURL(include_query=True)
942+ self.mutable_next_url = self.request.getURL(include_query=True)
943
944 @property
945 def action_url(self):
946diff --git a/lib/lp/registry/browser/featuredproject.py b/lib/lp/registry/browser/featuredproject.py
947index 4152919..968a5d2 100644
948--- a/lib/lp/registry/browser/featuredproject.py
949+++ b/lib/lp/registry/browser/featuredproject.py
950@@ -62,11 +62,11 @@ class FeaturedProjectsView(LaunchpadFormView):
951 for project in remove:
952 getUtility(IPillarNameSet).remove_featured_project(project)
953
954- self.next_url = canonical_url(self.context)
955+ self.mutable_next_url = canonical_url(self.context)
956
957 @action(_("Cancel"), name="cancel", validator="validate_cancel")
958 def action_cancel(self, action, data):
959- self.next_url = canonical_url(self.context)
960+ self.mutable_next_url = canonical_url(self.context)
961
962 @property
963 def action_url(self):
964diff --git a/lib/lp/registry/browser/karma.py b/lib/lp/registry/browser/karma.py
965index 19daccc..5b25f9e 100644
966--- a/lib/lp/registry/browser/karma.py
967+++ b/lib/lp/registry/browser/karma.py
968@@ -61,7 +61,7 @@ class KarmaActionEditView(LaunchpadEditFormView):
969 @action(_("Change"), name="change")
970 def change_action(self, action, data):
971 self.updateContextFromData(data)
972- self.next_url = self.cancel_url
973+ self.mutable_next_url = self.cancel_url
974
975
976 class KarmaContextContributor:
977diff --git a/lib/lp/registry/browser/milestone.py b/lib/lp/registry/browser/milestone.py
978index e6926a3..862563c 100644
979--- a/lib/lp/registry/browser/milestone.py
980+++ b/lib/lp/registry/browser/milestone.py
981@@ -493,7 +493,7 @@ class MilestoneAddView(MilestoneTagBase, LaunchpadFormView):
982 tags = data.get("tags")
983 if tags:
984 milestone.setTags(tags.lower().split(), self.user)
985- self.next_url = canonical_url(self.context)
986+ self.mutable_next_url = canonical_url(self.context)
987
988 @property
989 def action_url(self):
990@@ -574,7 +574,7 @@ class MilestoneEditView(MilestoneTagBase, LaunchpadEditFormView):
991 tags = data.pop("tags") or ""
992 self.updateContextFromData(data)
993 self.context.setTags(tags.lower().split(), self.user)
994- self.next_url = canonical_url(self.context)
995+ self.mutable_next_url = canonical_url(self.context)
996
997
998 class MilestoneDeleteView(LaunchpadFormView, RegistryDeleteViewMixin):
999@@ -622,7 +622,7 @@ class MilestoneDeleteView(LaunchpadFormView, RegistryDeleteViewMixin):
1000 self.request.response.addInfoNotification(
1001 "Milestone %s deleted." % name
1002 )
1003- self.next_url = canonical_url(series)
1004+ self.mutable_next_url = canonical_url(series)
1005
1006
1007 class ISearchMilestoneTagsForm(Interface):
1008@@ -665,7 +665,9 @@ class MilestoneTagView(
1009 def search_by_tags(self, action, data):
1010 tags = data["tags"].split()
1011 milestone_tag = ProjectGroupMilestoneTag(self.context.target, tags)
1012- self.next_url = canonical_url(milestone_tag, request=self.request)
1013+ self.mutable_next_url = canonical_url(
1014+ milestone_tag, request=self.request
1015+ )
1016
1017
1018 class ObjectMilestonesView(LaunchpadView):
1019diff --git a/lib/lp/registry/browser/ociproject.py b/lib/lp/registry/browser/ociproject.py
1020index e5d00c7..16fc785 100644
1021--- a/lib/lp/registry/browser/ociproject.py
1022+++ b/lib/lp/registry/browser/ociproject.py
1023@@ -129,7 +129,7 @@ class OCIProjectAddView(LaunchpadFormView):
1024 oci_project = getUtility(IOCIProjectSet).new(
1025 registrant=self.user, pillar=self.context, name=oci_project_name
1026 )
1027- self.next_url = canonical_url(oci_project)
1028+ self.mutable_next_url = canonical_url(oci_project)
1029
1030 def validate(self, data):
1031 super().validate(data)
1032diff --git a/lib/lp/registry/browser/peoplemerge.py b/lib/lp/registry/browser/peoplemerge.py
1033index 63f7bd9..24ac34b 100644
1034--- a/lib/lp/registry/browser/peoplemerge.py
1035+++ b/lib/lp/registry/browser/peoplemerge.py
1036@@ -175,7 +175,7 @@ class AdminMergeBaseView(ValidatingMergeView):
1037 requester=self.user,
1038 )
1039 self.request.response.addInfoNotification(self.merge_message)
1040- self.next_url = self.success_url
1041+ self.mutable_next_url = self.success_url
1042
1043
1044 class AdminPeopleMergeView(AdminMergeBaseView):
1045@@ -494,7 +494,9 @@ class RequestPeopleMergeView(ValidatingMergeView):
1046 # The dupe account have more than one email address. Must redirect
1047 # the user to another page to ask which of those emails they
1048 # want to claim.
1049- self.next_url = "+requestmerge-multiple?dupe=%d" % dupeaccount.id
1050+ self.mutable_next_url = (
1051+ "+requestmerge-multiple?dupe=%d" % dupeaccount.id
1052+ )
1053 return
1054
1055 assert emails_count == 1
1056@@ -510,4 +512,4 @@ class RequestPeopleMergeView(ValidatingMergeView):
1057 LoginTokenType.ACCOUNTMERGE,
1058 )
1059 token.sendMergeRequestEmail()
1060- self.next_url = "./+mergerequest-sent?dupe=%d" % dupeaccount.id
1061+ self.mutable_next_url = "./+mergerequest-sent?dupe=%d" % dupeaccount.id
1062diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
1063index 8145cc0..0ca2baf 100644
1064--- a/lib/lp/registry/browser/person.py
1065+++ b/lib/lp/registry/browser/person.py
1066@@ -1100,7 +1100,7 @@ class PersonDeactivateAccountView(LaunchpadFormView):
1067 self.request.response.addInfoNotification(
1068 _("Your account has been deactivated.")
1069 )
1070- self.next_url = self.request.getApplicationURL()
1071+ self.mutable_next_url = self.request.getApplicationURL()
1072
1073
1074 class BeginTeamClaimView(LaunchpadFormView):
1075@@ -3095,7 +3095,7 @@ class PersonEditEmailsView(LaunchpadFormView):
1076 self.request.response.addInfoNotification(
1077 "The email address '%s' has been removed." % emailaddress.email
1078 )
1079- self.next_url = self.action_url
1080+ self.mutable_next_url = self.action_url
1081
1082 def validate_action_set_preferred(self, action, data):
1083 """Make sure the user selected an address."""
1084@@ -3126,7 +3126,7 @@ class PersonEditEmailsView(LaunchpadFormView):
1085 "Your contact address has been changed to: %s"
1086 % (emailaddress.email)
1087 )
1088- self.next_url = self.action_url
1089+ self.mutable_next_url = self.action_url
1090
1091 def validate_action_confirm(self, action, data):
1092 """Make sure the user selected an email address to confirm."""
1093@@ -3151,7 +3151,7 @@ class PersonEditEmailsView(LaunchpadFormView):
1094 "instructions on how to confirm that "
1095 "it belongs to you." % email
1096 )
1097- self.next_url = self.action_url
1098+ self.mutable_next_url = self.action_url
1099
1100 def validate_action_remove_unvalidated(self, action, data):
1101 """Make sure the user selected an email address to remove."""
1102@@ -3189,7 +3189,7 @@ class PersonEditEmailsView(LaunchpadFormView):
1103 self.request.response.addInfoNotification(
1104 "The email address '%s' has been removed." % email
1105 )
1106- self.next_url = self.action_url
1107+ self.mutable_next_url = self.action_url
1108
1109 def validate_action_add_email(self, action, data):
1110 """Make sure the user entered a valid email address.
1111@@ -3267,7 +3267,7 @@ class PersonEditEmailsView(LaunchpadFormView):
1112 "provider might use 'greylisting', which could delay the "
1113 "message for up to an hour or two.)" % newemail
1114 )
1115- self.next_url = self.action_url
1116+ self.mutable_next_url = self.action_url
1117
1118
1119 @implementer(IPersonEditMenu)
1120@@ -3465,7 +3465,7 @@ class PersonEditMailingListsView(LaunchpadFormView):
1121 mailing_list.changeAddress(self.context, new_value)
1122 if dirty:
1123 self.request.response.addInfoNotification("Subscriptions updated.")
1124- self.next_url = self.action_url
1125+ self.mutable_next_url = self.action_url
1126
1127 def validate_action_update_autosubscribe_policy(self, action, data):
1128 """Ensure that the requested auto-subscribe setting is valid."""
1129@@ -3489,7 +3489,7 @@ class PersonEditMailingListsView(LaunchpadFormView):
1130 self.request.response.addInfoNotification(
1131 "Your auto-subscribe policy has been updated."
1132 )
1133- self.next_url = self.action_url
1134+ self.mutable_next_url = self.action_url
1135
1136
1137 class BaseWithStats:
1138@@ -4357,7 +4357,7 @@ class PersonEditOCIRegistryCredentialsView(LaunchpadFormView):
1139
1140 if not self.errors:
1141 self.request.response.addNotification("Saved credentials")
1142- self.next_url = canonical_url(self.context)
1143+ self.mutable_next_url = canonical_url(self.context)
1144
1145
1146 class PersonLiveFSView(LaunchpadView):
1147@@ -4733,7 +4733,7 @@ class EmailToPersonView(LaunchpadFormView):
1148 "does not have a preferred email address."
1149 )
1150 )
1151- self.next_url = canonical_url(self.context)
1152+ self.mutable_next_url = canonical_url(self.context)
1153 return
1154 try:
1155 send_direct_contact_email(
1156@@ -4759,7 +4759,7 @@ class EmailToPersonView(LaunchpadFormView):
1157 mapping=dict(name=self.context.displayname),
1158 )
1159 )
1160- self.next_url = canonical_url(self.context)
1161+ self.mutable_next_url = canonical_url(self.context)
1162
1163 @property
1164 def cancel_url(self):
1165diff --git a/lib/lp/registry/browser/poll.py b/lib/lp/registry/browser/poll.py
1166index b182a91..9c3aa96 100644
1167--- a/lib/lp/registry/browser/poll.py
1168+++ b/lib/lp/registry/browser/poll.py
1169@@ -442,7 +442,7 @@ class PollAddView(LaunchpadFormView):
1170 secrecy,
1171 data["allowspoilt"],
1172 )
1173- self.next_url = canonical_url(poll)
1174+ self.mutable_next_url = canonical_url(poll)
1175 notify(ObjectCreatedEvent(poll))
1176
1177
1178@@ -468,7 +468,7 @@ class PollEditView(LaunchpadEditFormView):
1179 @action("Save", name="save")
1180 def save_action(self, action, data):
1181 self.updateContextFromData(data)
1182- self.next_url = canonical_url(self.context)
1183+ self.mutable_next_url = canonical_url(self.context)
1184
1185
1186 class PollOptionEditView(LaunchpadEditFormView):
1187@@ -488,7 +488,7 @@ class PollOptionEditView(LaunchpadEditFormView):
1188 @action("Save", name="save")
1189 def save_action(self, action, data):
1190 self.updateContextFromData(data)
1191- self.next_url = canonical_url(self.context.poll)
1192+ self.mutable_next_url = canonical_url(self.context.poll)
1193
1194
1195 class PollOptionAddView(LaunchpadFormView):
1196@@ -508,7 +508,7 @@ class PollOptionAddView(LaunchpadFormView):
1197 @action("Create", name="create")
1198 def create_action(self, action, data):
1199 polloption = self.context.newOption(data["name"], data["title"])
1200- self.next_url = canonical_url(self.context)
1201+ self.mutable_next_url = canonical_url(self.context)
1202 notify(ObjectCreatedEvent(polloption))
1203
1204
1205diff --git a/lib/lp/registry/browser/product.py b/lib/lp/registry/browser/product.py
1206index f90e27a..448a7ff 100644
1207--- a/lib/lp/registry/browser/product.py
1208+++ b/lib/lp/registry/browser/product.py
1209@@ -2894,9 +2894,9 @@ class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
1210 self.link_source_package(self.product, data)
1211
1212 if self._return_url is None:
1213- self.next_url = canonical_url(self.product)
1214+ self.mutable_next_url = canonical_url(self.product)
1215 else:
1216- self.next_url = self._return_url
1217+ self.mutable_next_url = self._return_url
1218
1219
1220 class ProductAddView(PillarViewMixin, MultiStepView):
1221diff --git a/lib/lp/registry/browser/productrelease.py b/lib/lp/registry/browser/productrelease.py
1222index 3bf5b32..3760fb1 100644
1223--- a/lib/lp/registry/browser/productrelease.py
1224+++ b/lib/lp/registry/browser/productrelease.py
1225@@ -134,7 +134,7 @@ class ProductReleaseAddViewBase(LaunchpadFormView):
1226 # should not be targeted to a milestone in the past.
1227 if data.get("keep_milestone_active") is False:
1228 milestone.active = False
1229- self.next_url = canonical_url(newrelease.milestone)
1230+ self.mutable_next_url = canonical_url(newrelease.milestone)
1231 notify(ObjectCreatedEvent(newrelease))
1232
1233 @property
1234@@ -254,7 +254,7 @@ class ProductReleaseEditView(LaunchpadEditFormView):
1235 @action("Change", name="change")
1236 def change_action(self, action, data):
1237 self.updateContextFromData(data)
1238- self.next_url = canonical_url(self.context)
1239+ self.mutable_next_url = canonical_url(self.context)
1240
1241 @property
1242 def cancel_url(self):
1243@@ -353,7 +353,7 @@ class ProductReleaseAddDownloadFileView(LaunchpadFormView):
1244 % release_file.libraryfile.filename
1245 )
1246
1247- self.next_url = canonical_url(self.context)
1248+ self.mutable_next_url = canonical_url(self.context)
1249
1250 @property
1251 def cancel_url(self):
1252@@ -381,7 +381,7 @@ class ProductReleaseDeleteView(LaunchpadFormView, RegistryDeleteViewMixin):
1253 self.request.response.addInfoNotification(
1254 "Release %s deleted." % version
1255 )
1256- self.next_url = canonical_url(series)
1257+ self.mutable_next_url = canonical_url(series)
1258
1259 @property
1260 def cancel_url(self):
1261diff --git a/lib/lp/registry/browser/productseries.py b/lib/lp/registry/browser/productseries.py
1262index 9b05317..533ced7 100644
1263--- a/lib/lp/registry/browser/productseries.py
1264+++ b/lib/lp/registry/browser/productseries.py
1265@@ -817,7 +817,7 @@ class ProductSeriesDeleteView(RegistryDeleteViewMixin, LaunchpadEditFormView):
1266 name = self.context.name
1267 self._deleteProductSeries(self.context)
1268 self.request.response.addInfoNotification("Series %s deleted." % name)
1269- self.next_url = canonical_url(product)
1270+ self.mutable_next_url = canonical_url(product)
1271
1272
1273 class ProductSeriesSetBranchView(ProductSetBranchView, ProductSeriesView):
1274@@ -869,7 +869,7 @@ class ProductSeriesReviewView(LaunchpadEditFormView):
1275 self.request.response.addInfoNotification(
1276 _("This Series has been changed")
1277 )
1278- self.next_url = canonical_url(self.context)
1279+ self.mutable_next_url = canonical_url(self.context)
1280
1281
1282 class ProductSeriesRdfView(BaseRdfView):
1283diff --git a/lib/lp/registry/browser/sourcepackage.py b/lib/lp/registry/browser/sourcepackage.py
1284index 461a963..2c628d0 100644
1285--- a/lib/lp/registry/browser/sourcepackage.py
1286+++ b/lib/lp/registry/browser/sourcepackage.py
1287@@ -393,7 +393,7 @@ class SourcePackageChangeUpstreamStepTwo(ReturnToReferrerMixin, StepView):
1288 productseries = data["productseries"]
1289 # Because it is part of a multistep view, the next_url can't
1290 # be set until the action is called, or it will skip the step.
1291- self.next_url = self._return_url
1292+ self.mutable_next_url = self._return_url
1293 if self.context.productseries == productseries:
1294 # There is nothing to do.
1295 return
1296@@ -657,7 +657,7 @@ class SourcePackageAssociationPortletView(LaunchpadFormView):
1297 upstream = data.get("upstream")
1298 if upstream is self.other_upstream:
1299 # The user wants to link to an alternate upstream project.
1300- self.next_url = canonical_url(
1301+ self.mutable_next_url = canonical_url(
1302 self.context, view_name="+edit-packaging"
1303 )
1304 return
1305@@ -671,7 +671,7 @@ class SourcePackageAssociationPortletView(LaunchpadFormView):
1306 "The project %s was linked to this source package."
1307 % upstream.displayname
1308 )
1309- self.next_url = self.request.getURL()
1310+ self.mutable_next_url = self.request.getURL()
1311
1312
1313 class PackageUpstreamTracking(EnumeratedType):
1314diff --git a/lib/lp/registry/browser/team.py b/lib/lp/registry/browser/team.py
1315index 8de222c..1ca580f 100644
1316--- a/lib/lp/registry/browser/team.py
1317+++ b/lib/lp/registry/browser/team.py
1318@@ -650,7 +650,7 @@ class TeamMailingListConfigurationView(MailingListTeamBaseView):
1319 ):
1320 self.mailing_list.welcome_message = welcome_message
1321
1322- self.next_url = canonical_url(self.context)
1323+ self.mutable_next_url = canonical_url(self.context)
1324
1325 def cancel_list_creation_validator(self, action, data):
1326 """Validator for the `cancel_list_creation` action.
1327@@ -675,7 +675,7 @@ class TeamMailingListConfigurationView(MailingListTeamBaseView):
1328 self.request.response.addInfoNotification(
1329 "Mailing list application cancelled."
1330 )
1331- self.next_url = canonical_url(self.context)
1332+ self.mutable_next_url = canonical_url(self.context)
1333
1334 def create_list_creation_validator(self, action, data):
1335 """Validator for the `create_list_creation` action.
1336@@ -701,7 +701,7 @@ class TeamMailingListConfigurationView(MailingListTeamBaseView):
1337 "The mailing list is being created and will be available for "
1338 "use in a few minutes."
1339 )
1340- self.next_url = canonical_url(self.context)
1341+ self.mutable_next_url = canonical_url(self.context)
1342
1343 def deactivate_list_validator(self, action, data):
1344 """Adds an error if someone tries to deactivate a non-active list.
1345@@ -722,7 +722,7 @@ class TeamMailingListConfigurationView(MailingListTeamBaseView):
1346 self.request.response.addInfoNotification(
1347 "The mailing list will be deactivated within a few minutes."
1348 )
1349- self.next_url = canonical_url(self.context)
1350+ self.mutable_next_url = canonical_url(self.context)
1351
1352 def reactivate_list_validator(self, action, data):
1353 """Adds an error if a non-deactivated list is reactivated.
1354@@ -742,7 +742,7 @@ class TeamMailingListConfigurationView(MailingListTeamBaseView):
1355 self.request.response.addInfoNotification(
1356 "The mailing list will be reactivated within a few minutes."
1357 )
1358- self.next_url = canonical_url(self.context)
1359+ self.mutable_next_url = canonical_url(self.context)
1360
1361 def purge_list_validator(self, action, data):
1362 """Adds an error if the list is not safe to purge.
1363@@ -762,7 +762,7 @@ class TeamMailingListConfigurationView(MailingListTeamBaseView):
1364 self.request.response.addInfoNotification(
1365 "The mailing list has been purged."
1366 )
1367- self.next_url = canonical_url(self.context)
1368+ self.mutable_next_url = canonical_url(self.context)
1369
1370 @property
1371 def list_is_usable_but_not_contact_method(self):
1372@@ -1043,7 +1043,7 @@ class TeamMailingListModerationView(MailingListTeamBaseView):
1373 "Messages still held for review: %d of %d"
1374 % (still_held, reviewable)
1375 )
1376- self.next_url = canonical_url(self.context)
1377+ self.mutable_next_url = canonical_url(self.context)
1378
1379
1380 class TeamMailingListArchiveView(LaunchpadView):
1381@@ -1118,7 +1118,7 @@ class TeamAddView(TeamFormMixin, HasRenewalPolicyMixin, LaunchpadFormView):
1382
1383 if self.request.is_ajax:
1384 return ""
1385- self.next_url = canonical_url(team)
1386+ self.mutable_next_url = canonical_url(team)
1387
1388 def _validateVisibilityConsistency(self, value):
1389 """See `TeamFormMixin`."""
1390@@ -1221,9 +1221,9 @@ class ProposedTeamMembersEditView(LaunchpadFormView):
1391 mapping=mapping,
1392 )
1393 )
1394- self.next_url = ""
1395+ self.mutable_next_url = ""
1396 else:
1397- self.next_url = self._next_url
1398+ self.mutable_next_url = self._next_url
1399
1400 @property
1401 def page_title(self):
1402@@ -2104,7 +2104,7 @@ class TeamAddMyTeamsView(LaunchpadFormView):
1403 self.label = "Propose these teams as members"
1404 else:
1405 self.label = "Add these teams to %s" % context.displayname
1406- self.next_url = canonical_url(context)
1407+ self.mutable_next_url = canonical_url(context)
1408 super().initialize()
1409
1410 def setUpFields(self):
1411diff --git a/lib/lp/services/oauth/browser/__init__.py b/lib/lp/services/oauth/browser/__init__.py
1412index 2a67f72..9e72a40 100644
1413--- a/lib/lp/services/oauth/browser/__init__.py
1414+++ b/lib/lp/services/oauth/browser/__init__.py
1415@@ -411,7 +411,7 @@ class OAuthAuthorizeTokenView(LaunchpadFormView, JSONTokenMixin):
1416 return
1417 callback = self.request.form.get("oauth_callback")
1418 if callback:
1419- self.next_url = callback
1420+ self.mutable_next_url = callback
1421
1422
1423 def lookup_oauth_context(context):
1424diff --git a/lib/lp/services/webhooks/browser.py b/lib/lp/services/webhooks/browser.py
1425index fefbf51..a72d563 100644
1426--- a/lib/lp/services/webhooks/browser.py
1427+++ b/lib/lp/services/webhooks/browser.py
1428@@ -137,7 +137,7 @@ class WebhookAddView(LaunchpadFormView):
1429 active=data["active"],
1430 secret=data["secret"],
1431 )
1432- self.next_url = canonical_url(webhook)
1433+ self.mutable_next_url = canonical_url(webhook)
1434
1435
1436 class WebhookView(LaunchpadEditFormView):
1437@@ -198,4 +198,4 @@ class WebhookDeleteView(LaunchpadFormView):
1438 self.request.response.addNotification(
1439 "Webhook for %s deleted." % self.context.delivery_url
1440 )
1441- self.next_url = canonical_url(target, view_name="+webhooks")
1442+ self.mutable_next_url = canonical_url(target, view_name="+webhooks")
1443diff --git a/lib/lp/snappy/browser/snap.py b/lib/lp/snappy/browser/snap.py
1444index 80819c0..1641bab 100644
1445--- a/lib/lp/snappy/browser/snap.py
1446+++ b/lib/lp/snappy/browser/snap.py
1447@@ -477,7 +477,7 @@ class SnapRequestBuildsView(LaunchpadFormView):
1448 self.request.response.addNotification(
1449 _("Builds will be dispatched soon.")
1450 )
1451- self.next_url = self.cancel_url
1452+ self.mutable_next_url = self.cancel_url
1453
1454
1455 class ISnapEditSchema(Interface):
1456@@ -529,7 +529,7 @@ def log_oops(error, request):
1457 class SnapAuthorizeMixin:
1458 def requestAuthorization(self, snap):
1459 try:
1460- self.next_url = SnapAuthorizeView.requestAuthorization(
1461+ self.mutable_next_url = SnapAuthorizeView.requestAuthorization(
1462 snap, self.request
1463 )
1464 except BadRequestPackageUploadResponse as e:
1465@@ -746,7 +746,7 @@ class SnapAddView(
1466 if data["store_upload"]:
1467 self.requestAuthorization(snap)
1468 else:
1469- self.next_url = canonical_url(snap)
1470+ self.mutable_next_url = canonical_url(snap)
1471
1472 def validate(self, data):
1473 super().validate(data)
1474@@ -890,7 +890,7 @@ class BaseSnapEditView(
1475 if need_store_reauth:
1476 self.requestAuthorization(self.context)
1477 else:
1478- self.next_url = canonical_url(self.context)
1479+ self.mutable_next_url = canonical_url(self.context)
1480
1481 @property
1482 def adapters(self):
1483@@ -1157,4 +1157,4 @@ class SnapDeleteView(BaseSnapEditView):
1484 def delete_action(self, action, data):
1485 owner = self.context.owner
1486 self.context.destroySelf()
1487- self.next_url = canonical_url(owner, view_name="+snaps")
1488+ self.mutable_next_url = canonical_url(owner, view_name="+snaps")
1489diff --git a/lib/lp/soyuz/browser/archive.py b/lib/lp/soyuz/browser/archive.py
1490index 6bbca05..d11a2a5 100644
1491--- a/lib/lp/soyuz/browser/archive.py
1492+++ b/lib/lp/soyuz/browser/archive.py
1493@@ -1189,9 +1189,9 @@ class ArchiveSourceSelectionFormView(ArchiveSourcePackageListViewBase):
1494 """
1495 query_string = self.request.get("QUERY_STRING", "")
1496 if query_string:
1497- self.next_url = "%s?%s" % (self.request.URL, query_string)
1498+ self.mutable_next_url = "%s?%s" % (self.request.URL, query_string)
1499 else:
1500- self.next_url = self.request.URL
1501+ self.mutable_next_url = self.request.URL
1502
1503 def setUpWidgets(self, context=None):
1504 """Setup our custom widget depending on the filter widget values."""
1505@@ -1761,7 +1761,7 @@ class ArchiveEditDependenciesView(ArchiveViewBase, LaunchpadFormView):
1506 page_title = label
1507
1508 def initialize(self):
1509- self.cancel_url = canonical_url(self.context)
1510+ self.mutable_cancel_url = canonical_url(self.context)
1511 self._messages = []
1512 LaunchpadFormView.initialize(self)
1513
1514@@ -2107,7 +2107,7 @@ class ArchiveEditDependenciesView(ArchiveViewBase, LaunchpadFormView):
1515 if len(self.messages) > 0:
1516 self.request.response.addNotification(structured(self.messages))
1517 # Redirect after POST.
1518- self.next_url = self.request.URL
1519+ self.mutable_next_url = self.request.URL
1520
1521
1522 class ArchiveActivateView(LaunchpadFormView):
1523@@ -2202,7 +2202,7 @@ class ArchiveActivateView(LaunchpadFormView):
1524 description,
1525 private=self.is_private_team,
1526 )
1527- self.next_url = canonical_url(ppa)
1528+ self.mutable_next_url = canonical_url(ppa)
1529
1530 @property
1531 def is_private_team(self):
1532@@ -2254,7 +2254,7 @@ class BaseArchiveEditView(LaunchpadEditFormView, ArchiveViewBase):
1533 )
1534 del data["processors"]
1535 self.updateContextFromData(data)
1536- self.next_url = canonical_url(self.context)
1537+ self.mutable_next_url = canonical_url(self.context)
1538
1539 @property
1540 def cancel_url(self):
1541diff --git a/lib/lp/soyuz/browser/archivesubscription.py b/lib/lp/soyuz/browser/archivesubscription.py
1542index de0bb9a..fa07b03 100644
1543--- a/lib/lp/soyuz/browser/archivesubscription.py
1544+++ b/lib/lp/soyuz/browser/archivesubscription.py
1545@@ -254,7 +254,7 @@ class ArchiveSubscribersView(LaunchpadFormView):
1546 self.request.response.addNotification(notification)
1547
1548 # Just ensure a redirect happens (back to ourselves).
1549- self.next_url = str(self.request.URL)
1550+ self.mutable_next_url = str(self.request.URL)
1551
1552
1553 class ArchiveSubscriptionEditView(LaunchpadEditFormView):
1554diff --git a/lib/lp/soyuz/browser/build.py b/lib/lp/soyuz/browser/build.py
1555index 27d3b5b..af00a85 100644
1556--- a/lib/lp/soyuz/browser/build.py
1557+++ b/lib/lp/soyuz/browser/build.py
1558@@ -358,7 +358,7 @@ class BuildRescoringView(LaunchpadFormView):
1559 any action will send the user back to the context build page.
1560 """
1561 build_url = canonical_url(self.context)
1562- self.next_url = self.cancel_url = build_url
1563+ self.mutable_next_url = self.mutable_cancel_url = build_url
1564
1565 if not self.context.can_be_rescored:
1566 self.request.response.redirect(build_url)
1567diff --git a/lib/lp/soyuz/browser/distroarchseries.py b/lib/lp/soyuz/browser/distroarchseries.py
1568index d3dae16..50c582e 100644
1569--- a/lib/lp/soyuz/browser/distroarchseries.py
1570+++ b/lib/lp/soyuz/browser/distroarchseries.py
1571@@ -125,7 +125,7 @@ class DistroArchSeriesAddView(LaunchpadFormView):
1572 data["official"],
1573 self.user,
1574 )
1575- self.next_url = canonical_url(distroarchseries)
1576+ self.mutable_next_url = canonical_url(distroarchseries)
1577
1578
1579 class DistroArchSeriesAdminView(LaunchpadEditFormView):
1580diff --git a/lib/lp/soyuz/browser/livefs.py b/lib/lp/soyuz/browser/livefs.py
1581index 0fea4d9..210bede 100644
1582--- a/lib/lp/soyuz/browser/livefs.py
1583+++ b/lib/lp/soyuz/browser/livefs.py
1584@@ -261,7 +261,7 @@ class LiveFSAddView(LiveFSMetadataValidatorMixin, LaunchpadFormView):
1585 data["name"],
1586 json.loads(data["metadata"]),
1587 )
1588- self.next_url = canonical_url(livefs)
1589+ self.mutable_next_url = canonical_url(livefs)
1590
1591 def validate(self, data):
1592 super().validate(data)
1593@@ -289,7 +289,7 @@ class BaseLiveFSEditView(LaunchpadEditFormView):
1594 @action("Update live filesystem", name="update")
1595 def request_action(self, action, data):
1596 self.updateContextFromData(data)
1597- self.next_url = canonical_url(self.context)
1598+ self.mutable_next_url = canonical_url(self.context)
1599
1600 @property
1601 def adapters(self):
1602@@ -394,4 +394,4 @@ class LiveFSDeleteView(BaseLiveFSEditView):
1603 self.context.destroySelf()
1604 # XXX cjwatson 2015-05-07 bug=1332479: This should go to
1605 # Person:+livefs once that exists.
1606- self.next_url = canonical_url(owner)
1607+ self.mutable_next_url = canonical_url(owner)
1608diff --git a/lib/lp/translations/browser/distroseries.py b/lib/lp/translations/browser/distroseries.py
1609index 563d9b0..ec3a352 100644
1610--- a/lib/lp/translations/browser/distroseries.py
1611+++ b/lib/lp/translations/browser/distroseries.py
1612@@ -187,7 +187,7 @@ class DistroSeriesLanguagePackView(LaunchpadEditFormView):
1613 self.request.response.addInfoNotification(
1614 "Your changes have been applied."
1615 )
1616- self.next_url = canonical_url(
1617+ self.mutable_next_url = canonical_url(
1618 self.context, rootsite="translations", view_name="+language-packs"
1619 )
1620
1621@@ -195,7 +195,7 @@ class DistroSeriesLanguagePackView(LaunchpadEditFormView):
1622 def request_action(self, action, data):
1623 self.updateContextFromData(data)
1624 self._request_full_export()
1625- self.next_url = canonical_url(
1626+ self.mutable_next_url = canonical_url(
1627 self.context, rootsite="translations", view_name="+language-packs"
1628 )
1629
1630diff --git a/lib/lp/translations/browser/hastranslationimports.py b/lib/lp/translations/browser/hastranslationimports.py
1631index c1f7623..f133d02 100644
1632--- a/lib/lp/translations/browser/hastranslationimports.py
1633+++ b/lib/lp/translations/browser/hastranslationimports.py
1634@@ -205,7 +205,7 @@ class HasTranslationImportsView(LaunchpadFormView):
1635 )
1636
1637 # Redirect to the filtered URL.
1638- self.next_url = (
1639+ self.mutable_next_url = (
1640 "%s?%sfield.filter_status=%s&field.filter_extension=%s"
1641 % (
1642 self.request.URL,
1643diff --git a/lib/lp/translations/browser/person.py b/lib/lp/translations/browser/person.py
1644index 12e0c34..0504f68 100644
1645--- a/lib/lp/translations/browser/person.py
1646+++ b/lib/lp/translations/browser/person.py
1647@@ -524,7 +524,7 @@ class PersonTranslationRelicensingView(LaunchpadFormView):
1648 raise AssertionError(
1649 "Unknown allow_relicensing value: %r" % allow_relicensing
1650 )
1651- self.next_url = self.getSafeRedirectURL(data["back_to"])
1652+ self.mutable_next_url = self.getSafeRedirectURL(data["back_to"])
1653
1654
1655 class TranslationActivityView(LaunchpadView):
1656diff --git a/lib/lp/translations/browser/translationgroup.py b/lib/lp/translations/browser/translationgroup.py
1657index 2aa2d50..9f7361b 100644
1658--- a/lib/lp/translations/browser/translationgroup.py
1659+++ b/lib/lp/translations/browser/translationgroup.py
1660@@ -208,7 +208,7 @@ class TranslationGroupAddView(LaunchpadFormView):
1661 owner=self.user,
1662 )
1663
1664- self.next_url = canonical_url(new_group)
1665+ self.mutable_next_url = canonical_url(new_group)
1666
1667 def validate(self, data):
1668 """Do not allow new groups with duplicated names."""

Subscribers

People subscribed via source and target branches

to status/vote changes: