Repository: wagtail/wagtail

Issue:
Redirects: 'target page route' with non-RoutablePageMixin pages causes server error
### Issue Summary

https://github.com/wagtail/wagtail/pull/7827 introduced the 'target page route' option to the create redirect form. This option is meant to be used in conjunction with 'Redirect to a page', it allows editors to choose a page that inherits from `RoutablePageMixin` and specify a route on the RoutablePage to redirect to.

Now for the issue: it is possible to choose a page that does not inherit from `RoutablePageMixin` and specify a 'target route page'. When the created redirect is visited, a server error like this is raised:

```
AttributeError at /test/
'MyPage' object has no attribute 'resolve_subpage'
```

I foresee this will be very confusing for editors. 'Why is my redirect not working?'.

**Possible solutions**
1. My preferred solution would be to completely hide the 'target route page' field and _only_ show it if a page that is a RoutablePage is chosen. I feel like it is better to hide it as it serves a very specific purpose and might be a bit confusing for editors as to what the field is used for.

2. The easy solution: just ignore 'target route page' if the page is not a RoutablePage, would probably involve checking https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/redirects/models.py#L62  if the page has a `resolve_subpage` attribute before trying to use it...
### Steps to Reproduce

1. Set up the bakerydemo project (https://github.com/wagtail/bakerydemo/)
2. Upgrade Wagtail to 2.16rc1 using `pip install wagtail==2.16rc1` and run migrations (`./manage.py migrate`).
3. Launch the django dev server and navigate to http://127.0.0.1:8000/admin/redirects/
3. Create a new redirect and choose the following options:
- Redirect from: '/test'
- Redirect to a page: 'About' (this is not a routable page!)
- Target page route: '/causes-server-errors' (or anything you'd like really, it doesn't matter)
4. Save redirect and navigate to http://localhost:8000/test/
5. You should now be seeing the following error:
```Internal Server Error: /test/
Traceback (most recent call last):
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/django/utils/deprecation.py", line 119, in __call__
    response = self.process_response(request, response)
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/wagtail/contrib/redirects/middleware.py", line 57, in process_response
    if redirect.link is None:
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/wagtail/contrib/redirects/models.py", line 62, in link
    page.resolve_subpage(self.redirect_page_route_path)
AttributeError: 'StandardPage' object has no attribute 'resolve_subpage'
```


* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes


### Technical details

* Python version: Python 3.9.10
* Django version: 3.2.11
* Wagtail version: 2.16rc1
* Browser version: Chrome 97

Redirects: 'target page route' with non-RoutablePageMixin pages causes server error
### Issue Summary

https://github.com/wagtail/wagtail/pull/7827 introduced the 'target page route' option to the create redirect form. This option is meant to be used in conjunction with 'Redirect to a page', it allows editors to choose a page that inherits from `RoutablePageMixin` and specify a route on the RoutablePage to redirect to.

Now for the issue: it is possible to choose a page that does not inherit from `RoutablePageMixin` and specify a 'target route page'. When the created redirect is visited, a server error like this is raised:

```
AttributeError at /test/
'MyPage' object has no attribute 'resolve_subpage'
```

I foresee this will be very confusing for editors. 'Why is my redirect not working?'.

**Possible solutions**
1. My preferred solution would be to completely hide the 'target route page' field and _only_ show it if a page that is a RoutablePage is chosen. I feel like it is better to hide it as it serves a very specific purpose and might be a bit confusing for editors as to what the field is used for.

2. The easy solution: just ignore 'target route page' if the page is not a RoutablePage, would probably involve checking https://github.com/wagtail/wagtail/blob/main/wagtail/contrib/redirects/models.py#L62  if the page has a `resolve_subpage` attribute before trying to use it...
### Steps to Reproduce

1. Set up the bakerydemo project (https://github.com/wagtail/bakerydemo/)
2. Upgrade Wagtail to 2.16rc1 using `pip install wagtail==2.16rc1` and run migrations (`./manage.py migrate`).
3. Launch the django dev server and navigate to http://127.0.0.1:8000/admin/redirects/
3. Create a new redirect and choose the following options:
- Redirect from: '/test'
- Redirect to a page: 'About' (this is not a routable page!)
- Target page route: '/causes-server-errors' (or anything you'd like really, it doesn't matter)
4. Save redirect and navigate to http://localhost:8000/test/
5. You should now be seeing the following error:
```Internal Server Error: /test/
Traceback (most recent call last):
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/django/utils/deprecation.py", line 119, in __call__
    response = self.process_response(request, response)
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/wagtail/contrib/redirects/middleware.py", line 57, in process_response
    if redirect.link is None:
  File "/Users/storm/projects/wagtail/bakerydemo/venv/lib/python3.9/site-packages/wagtail/contrib/redirects/models.py", line 62, in link
    page.resolve_subpage(self.redirect_page_route_path)
AttributeError: 'StandardPage' object has no attribute 'resolve_subpage'
```


* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: yes


### Technical details

* Python version: Python 3.9.10
* Django version: 3.2.11
* Wagtail version: 2.16rc1
* Browser version: Chrome 97



Discussion:
Discussed in team meeting.
option 2 is the most likely fix. 

There is a question aroudn help text for this option on the form. @ababic give this is for automated redirects, should this show up in the UI at all?
@zerolab Aye, this is mostly for creating more useful redirects automatically. If we can't find a way for it to make sense in the UI, then I'm happy for it to be dropped from the form. I think we need to think about the UI for this page as a whole, so maybe we should postpone quick UI fixes until we have time to look at things more holistically.
@zerolab In fact, something I didn't think of before... If a user views a redirect that has this value set, I think they might be equally confused by NOT seeing it and testing it out. I think the best approach is to just fix the error for now, and leave the field where it is.
Discussed in team meeting.
option 2 is the most likely fix. 

There is a question aroudn help text for this option on the form. @ababic give this is for automated redirects, should this show up in the UI at all?
@zerolab Aye, this is mostly for creating more useful redirects automatically. If we can't find a way for it to make sense in the UI, then I'm happy for it to be dropped from the form. I think we need to think about the UI for this page as a whole, so maybe we should postpone quick UI fixes until we have time to look at things more holistically.
@zerolab In fact, something I didn't think of before... If a user views a redirect that has this value set, I think they might be equally confused by NOT seeing it and testing it out. I think the best approach is to just fix the error for now, and leave the field where it is.

Resolving patch:
diff --git a/wagtail/contrib/redirects/forms.py b/wagtail/contrib/redirects/forms.py
--- a/wagtail/contrib/redirects/forms.py
+++ b/wagtail/contrib/redirects/forms.py
@@ -43,7 +43,7 @@ def clean(self):
 
     class Meta:
         model = Redirect
-        fields = ('old_path', 'site', 'is_permanent', 'redirect_page', 'redirect_page_route_path', 'redirect_link')
+        fields = ('old_path', 'site', 'is_permanent', 'redirect_page', 'redirect_link')
 
 
 class ImportForm(forms.Form):
diff --git a/wagtail/contrib/redirects/models.py b/wagtail/contrib/redirects/models.py
--- a/wagtail/contrib/redirects/models.py
+++ b/wagtail/contrib/redirects/models.py
@@ -60,7 +60,7 @@ def link(self):
                 return base_url
             try:
                 page.resolve_subpage(self.redirect_page_route_path)
-            except Resolver404:
+            except (AttributeError, Resolver404):
                 return base_url
             return base_url.rstrip("/") + self.redirect_page_route_path
         elif self.redirect_link:


Test patch:
diff --git a/wagtail/contrib/redirects/tests/test_redirects.py b/wagtail/contrib/redirects/tests/test_redirects.py
--- a/wagtail/contrib/redirects/tests/test_redirects.py
+++ b/wagtail/contrib/redirects/tests/test_redirects.py
@@ -170,6 +170,7 @@ def test_redirect_to_specific_page_route(self):
             title="Routable Page",
             live=True,
         ))
+        contact_page = Page.objects.get(url_path='/home/contact-us/')
 
         # test redirect with a VALID route path
         models.Redirect.add_redirect(old_path='/old-path-one', redirect_to=routable_page, page_route_path='/render-method-test-custom-template/')
@@ -179,8 +180,15 @@ def test_redirect_to_specific_page_route(self):
         # test redirect with an INVALID route path
         models.Redirect.add_redirect(old_path='/old-path-two', redirect_to=routable_page, page_route_path='/invalid-route/')
         response = self.client.get('/old-path-two/', HTTP_HOST='test.example.com')
+        # we should still make it to the correct page
         self.assertRedirects(response, '/routable-page/', status_code=301, fetch_redirect_response=False)
 
+        # test redirect with route path for a non-routable page
+        models.Redirect.add_redirect(old_path="/old-path-three", redirect_to=contact_page, page_route_path="/route-to-nowhere/")
+        response = self.client.get('/old-path-three/', HTTP_HOST='test.example.com')
+        # we should still make it to the correct page
+        self.assertRedirects(response, "/contact-us/", status_code=301, fetch_redirect_response=False)
+
     def test_redirect_from_any_site(self):
         contact_page = Page.objects.get(url_path='/home/contact-us/')
         Site.objects.create(hostname='other.example.com', port=80, root_page=contact_page)