Repository: wagtail/wagtail

Issue:
Dynamically set action_url for adding a new item in snippet index.html
### Is your proposal related to a problem?
Hey folks!

On a project I needed to pass some GET query parameters from the snippet index view to the snippet create view, based on the current value of the filter, to prefill an initial value on the snippet create form.

Right now, the action_url value is being set [here](https://github.com/wagtail/wagtail/blob/main/wagtail/snippets/templates/wagtailsnippets/snippets/index.html#L17), meaning that it takes a static parameter from the view. 

The only option I see is to fully override the `index.html` template by extending the base one, copying the whole `content` block and setting the desired value.

### Describe the solution you'd like

Ideally, we wouldn't take the static value, but instead use [get_add_url()](https://github.com/wagtail/wagtail/blob/main/wagtail/admin/views/generic/models.py#L378C5-L378C5) here which we can override in a custom `IndexView`. This seems easier than overriding basically the whole template, and is already being done in generic views. ([view](https://github.com/wagtail/wagtail/blob/af1ce3ff88ff8c906470d3a9a6a2e8bf9d68b59b/wagtail/admin/views/generic/models.py#L482), [template](https://github.com/wagtail/wagtail/blob/af1ce3ff88ff8c906470d3a9a6a2e8bf9d68b59b/wagtail/admin/templates/wagtailadmin/generic/index.html#L10))

Is there a reason why the behavior is different in those two cases?

### Describe alternatives you've considered

As I mentioned above, overriding the template seems like the only way to do it.


### Additional context

If we agree that this is something that should be changed in the future, I could potentially take on the work of doing so and creating a PR, if you want me to.



Discussion:
Hi, thank you for the report. This seems like what #11133 aims to fix.

> Is there a reason why the behavior is different in those two cases?

I don't think there's a specific reason. The view was gradually migrated over from a function based view to a class-based view, from using its own template, to extending the generic views' templates. It's likely this just wasn't picked up in the process 🙂 
I agree, it does seem like it's the same thing. My bad, I searched for `get_add_url` in issues, but not in pull requests as well. I think that this can be closed, thanks for the quick reply!

Resolving patch:
diff --git a/wagtail/admin/utils.py b/wagtail/admin/utils.py
--- a/wagtail/admin/utils.py
+++ b/wagtail/admin/utils.py
@@ -68,5 +68,5 @@ def set_query_params(url: str, params: dict):
     querydict = parse_qs(query)
     querydict.update(params)
     querydict = {key: value for key, value in querydict.items() if value is not None}
-    query = urlencode(querydict)
+    query = urlencode(querydict, doseq=True)
     return urlunsplit((scheme, netloc, path, query, fragment))
diff --git a/wagtail/admin/views/generic/models.py b/wagtail/admin/views/generic/models.py
--- a/wagtail/admin/views/generic/models.py
+++ b/wagtail/admin/views/generic/models.py
@@ -377,7 +377,7 @@ def get_delete_url(self, instance):
 
     def get_add_url(self):
         if self.add_url_name:
-            return reverse(self.add_url_name)
+            return self._set_locale_query_param(reverse(self.add_url_name))
 
     def get_page_title(self):
         if not self.page_title and self.model:


Test patch:
diff --git a/wagtail/snippets/tests/test_viewset.py b/wagtail/snippets/tests/test_viewset.py
--- a/wagtail/snippets/tests/test_viewset.py
+++ b/wagtail/snippets/tests/test_viewset.py
@@ -9,7 +9,7 @@
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ImproperlyConfigured
 from django.template.defaultfilters import date
-from django.test import SimpleTestCase, TestCase, TransactionTestCase
+from django.test import SimpleTestCase, TestCase, TransactionTestCase, override_settings
 from django.urls import NoReverseMatch, resolve, reverse
 from django.utils.timezone import now
 from openpyxl import load_workbook
@@ -435,11 +435,7 @@ def test_get_include_filters_form_media(self):
 
     def test_unfiltered_no_results(self):
         response = self.get()
-        add_url = self.get_url("add")
-        self.assertContains(
-            response,
-            f'No full-featured snippets have been created. Why not <a href="{add_url}">add one</a>',
-        )
+        self.assertContains(response, "No full-featured snippets have been created.")
         self.assertContains(
             response,
             '<label for="id_country_code_0"><input type="radio" name="country_code" value="" id="id_country_code_0" checked>All</label>',
@@ -1493,3 +1489,27 @@ def test_inspect_view(self):
             {"url": "", "label": "Inspect"},
         ]
         self.assertBreadcrumbsItemsRendered(items, response.content)
+
+
+class TestCustomMethods(BaseSnippetViewSetTests):
+    model = FullFeaturedSnippet
+
+    def test_index_view_get_add_url_is_respected(self):
+        response = self.client.get(self.get_url("list"))
+        add_url = self.get_url("add") + "?customised=param"
+        soup = self.get_soup(response.content)
+        # Should contain the customised add URL in two places:
+        # The main action button, and the "Why not add one?" suggestion
+        links = soup.find_all("a", attrs={"href": add_url})
+        self.assertEqual(len(links), 2)
+
+    @override_settings(WAGTAIL_I18N_ENABLED=True)
+    def test_index_view_get_add_url_is_respected_with_i18n(self):
+        Locale.objects.create(language_code="fr")
+        response = self.client.get(self.get_url("list") + "?locale=fr")
+        add_url = self.get_url("add") + "?locale=fr&customised=param"
+        soup = self.get_soup(response.content)
+        # Should contain the customised add URL in two places:
+        # The main action button, and the "Why not add one?" suggestion
+        links = soup.find_all("a", attrs={"href": add_url})
+        self.assertEqual(len(links), 2)
diff --git a/wagtail/test/testapp/wagtail_hooks.py b/wagtail/test/testapp/wagtail_hooks.py
--- a/wagtail/test/testapp/wagtail_hooks.py
+++ b/wagtail/test/testapp/wagtail_hooks.py
@@ -18,6 +18,7 @@
 from wagtail.admin.site_summary import SummaryItem
 from wagtail.admin.ui.components import Component
 from wagtail.admin.ui.tables import BooleanColumn, UpdatedAtColumn
+from wagtail.admin.utils import set_query_params
 from wagtail.admin.views.account import BaseSettingsPanel
 from wagtail.admin.widgets import Button
 from wagtail.snippets.bulk_actions.snippet_bulk_action import SnippetBulkAction
@@ -301,6 +302,12 @@ class FullFeaturedSnippetViewSet(SnippetViewSet):
     menu_order = 999999
     inspect_view_enabled = True
 
+    class IndexView(SnippetViewSet.index_view_class):
+        def get_add_url(self):
+            return set_query_params(super().get_add_url(), {"customised": "param"})
+
+    index_view_class = IndexView
+
     # TODO: When specific search fields are supported in SQLite FTS (see #10217),
     # specify search_fields or get_search_fields here