Repository: zulip/zulip

Issue:
Add system for redirecting a renamed /help or /api article 
We from time to time rename articles in the /help or /api world.  This breaks incoming links, including from other branches of zulip/zulip that haven't been rebases, which is annoying, but more importantly from previous versions of Zulip that might be out there.

When we rename things in ReadTheDocs, we configure a redirect from the old URL to the new one.  We should figure out a convenient way to do this with our markdown system (easy with `urls.py` entries, but would be nice to just have a table somewhere in `zerver/views/documentation.py` that's more convenient) and document it in our guides for documenting them (https://zulip.readthedocs.io/en/latest/documentation/index.html).


Discussion:
Hello @zulip/server-api, @zulip/server-user-docs members, this issue was labeled with the "area: documentation (api and integrations)", "area: documentation (user)" labels, so you may want to check it out!

<!-- areaLabelAddition -->
@zulipbot claim
Welcome to Zulip, @KakoozaJerry! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:
* Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
* [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!
https://github.com/zulip/zulip/pull/17906 has an example of doing such a redirect manually.
Hello @KakoozaJerry, you claimed this issue to work on it, but this issue and any referenced pull requests haven't been updated for 10 days. Are you still working on this issue?

If so, please update this issue by leaving a comment on this issue to let me know that you're still working on it. Otherwise, I'll automatically remove you from this issue in 4 days.

If you've decided to work on something else, simply comment `@zulipbot abandon` so that someone else can claim it and continue from where you left off.

Thank you for your valuable contributions to Zulip!

<!-- inactiveWarning -->
@zulipbot abandon

Resolving patch:
diff --git a/corporate/urls.py b/corporate/urls.py
--- a/corporate/urls.py
+++ b/corporate/urls.py
@@ -22,6 +22,7 @@
 from corporate.views.upgrade import initial_upgrade, sponsorship, upgrade
 from corporate.views.webhook import stripe_webhook
 from zerver.lib.rest import rest_path
+from zerver.lib.url_redirects import LANDING_PAGE_REDIRECTS
 
 i18n_urlpatterns: Any = [
     # Zephyr/MIT
@@ -51,15 +52,11 @@
 landing_page_urls = [
     # Landing page, features pages, signup form, etc.
     path("hello/", hello_view),
-    path("new-user/", RedirectView.as_view(url="/hello", permanent=True)),
     path("features/", landing_view, {"template_name": "corporate/features.html"}),
     path("plans/", plans_view, name="plans"),
     path("apps/", apps_view),
     path("apps/download/<platform>", app_download_link_redirect),
     path("apps/<platform>", apps_view),
-    path(
-        "developer-community/", RedirectView.as_view(url="/development-community/", permanent=True)
-    ),
     path(
         "development-community/",
         landing_view,
@@ -78,17 +75,11 @@
         landing_view,
         {"template_name": "corporate/for/communities.html"},
     ),
-    # We merged this into /for/communities.
-    path(
-        "for/working-groups-and-communities/",
-        RedirectView.as_view(url="/for/communities/", permanent=True),
-    ),
     path("for/education/", landing_view, {"template_name": "corporate/for/education.html"}),
     path("for/events/", landing_view, {"template_name": "corporate/for/events.html"}),
     path("for/open-source/", landing_view, {"template_name": "corporate/for/open-source.html"}),
     path("for/research/", landing_view, {"template_name": "corporate/for/research.html"}),
     path("for/business/", landing_view, {"template_name": "corporate/for/business.html"}),
-    path("for/companies/", RedirectView.as_view(url="/for/business/", permanent=True)),
     # case-studies
     path(
         "case-studies/idrift/",
@@ -126,6 +117,12 @@
         {"template_name": "corporate/case-studies/recurse-center-case-study.html"},
     ),
 ]
+
+# Redirects due to us having moved or combined landing pages:
+for redirect in LANDING_PAGE_REDIRECTS:
+    old_url = redirect.old_url.lstrip("/")
+    landing_page_urls += [path(old_url, RedirectView.as_view(url=redirect.new_url, permanent=True))]
+
 i18n_urlpatterns += landing_page_urls
 
 # Make a copy of i18n_urlpatterns so that they appear without prefix for English
diff --git a/zerver/lib/url_redirects.py b/zerver/lib/url_redirects.py
new file mode 100644
--- /dev/null
+++ b/zerver/lib/url_redirects.py
@@ -0,0 +1,64 @@
+from dataclasses import dataclass
+from typing import List
+
+
+@dataclass
+class URLRedirect:
+    old_url: str
+    new_url: str
+
+
+API_DOCUMENTATION_REDIRECTS: List[URLRedirect] = [
+    # Add URL redirects for REST API documentation here:
+    URLRedirect("/api/delete-stream", "/api/archive-stream"),
+]
+
+POLICY_DOCUMENTATION_REDIRECTS: List[URLRedirect] = [
+    # Add URL redirects for policy documentation here:
+    URLRedirect("/privacy/", "/policies/privacy"),
+    URLRedirect("/terms/", "/policies/terms"),
+]
+
+HELP_DOCUMENTATION_REDIRECTS: List[URLRedirect] = [
+    # Add URL redirects for help center documentation here:
+    URLRedirect("/help/delete-a-stream", "/help/archive-a-stream"),
+    URLRedirect("/help/change-the-topic-of-a-message", "/help/rename-a-topic"),
+    URLRedirect("/help/configure-missed-message-emails", "/help/email-notifications"),
+    URLRedirect("/help/add-an-alert-word", "/help/pm-mention-alert-notifications#alert-words"),
+    URLRedirect("/help/test-mobile-notifications", "/help/mobile-notifications"),
+    URLRedirect(
+        "/help/troubleshooting-desktop-notifications",
+        "/help/desktop-notifications#troubleshooting-desktop-notifications",
+    ),
+    URLRedirect(
+        "/help/change-notification-sound", "/help/desktop-notifications#change-notification-sound"
+    ),
+    URLRedirect("/help/configure-message-notification-emails", "/help/email-notifications"),
+    URLRedirect("/help/disable-new-login-emails", "/help/email-notifications#new-login-emails"),
+    # The `help/about-streams-and-topics` redirect is particularly important,
+    # because the old URL appears in links from Welcome Bot messages.
+    URLRedirect("/help/about-streams-and-topics", "/help/streams-and-topics"),
+    URLRedirect("/help/community-topic-edits", "/help/configure-who-can-edit-topics"),
+    URLRedirect(
+        "/help/only-allow-admins-to-add-emoji", "/help/custom-emoji#change-who-can-add-custom-emoji"
+    ),
+    URLRedirect(
+        "/help/configure-who-can-add-custom-emoji",
+        "/help/custom-emoji#change-who-can-add-custom-emoji",
+    ),
+    URLRedirect("/help/add-custom-emoji", "/help/custom-emoji"),
+    URLRedirect("/help/night-mode", "/help/dark-theme"),
+    URLRedirect("/help/web-public-streams", "/help/public-access-option"),
+]
+
+LANDING_PAGE_REDIRECTS = [
+    # Add URL redirects for corporate landing pages here.
+    URLRedirect("/new-user/", "/hello"),
+    URLRedirect("/developer-community/", "/development-community"),
+    URLRedirect("/for/companies/", "/for/business"),
+    URLRedirect("/for/working-groups-and-communities/", "/for/communities"),
+]
+
+DOCUMENTATION_REDIRECTS = (
+    API_DOCUMENTATION_REDIRECTS + POLICY_DOCUMENTATION_REDIRECTS + HELP_DOCUMENTATION_REDIRECTS
+)
diff --git a/zproject/urls.py b/zproject/urls.py
--- a/zproject/urls.py
+++ b/zproject/urls.py
@@ -18,6 +18,7 @@
 from zerver.forms import LoggingSetPasswordForm
 from zerver.lib.integrations import WEBHOOK_INTEGRATIONS
 from zerver.lib.rest import rest_path
+from zerver.lib.url_redirects import DOCUMENTATION_REDIRECTS
 from zerver.tornado.views import cleanup_event_queue, get_events, get_events_internal, notify
 from zerver.views.alert_words import add_alert_words, list_alert_words, remove_alert_words
 from zerver.views.attachments import list_by_user, remove
@@ -780,98 +781,19 @@
     template_name="zerver/documentation_main.html",
     policies_view=True,
 )
+
+# Redirects due to us having moved help center, API or policy documentation pages:
+for redirect in DOCUMENTATION_REDIRECTS:
+    old_url = redirect.old_url.lstrip("/")
+    urls += [path(old_url, RedirectView.as_view(url=redirect.new_url, permanent=True))]
+
 urls += [
-    # Redirects due to us having moved the docs:
-    path(
-        "help/delete-a-stream", RedirectView.as_view(url="/help/archive-a-stream", permanent=True)
-    ),
-    path("api/delete-stream", RedirectView.as_view(url="/api/archive-stream", permanent=True)),
-    path(
-        "help/change-the-topic-of-a-message",
-        RedirectView.as_view(url="/help/rename-a-topic", permanent=True),
-    ),
-    path(
-        "help/configure-missed-message-emails",
-        RedirectView.as_view(url="/help/email-notifications", permanent=True),
-    ),
-    path(
-        "help/add-an-alert-word",
-        RedirectView.as_view(
-            url="/help/pm-mention-alert-notifications#alert-words", permanent=True
-        ),
-    ),
-    path(
-        "help/test-mobile-notifications",
-        RedirectView.as_view(url="/help/mobile-notifications", permanent=True),
-    ),
-    path(
-        "help/troubleshooting-desktop-notifications",
-        RedirectView.as_view(
-            url="/help/desktop-notifications#troubleshooting-desktop-notifications", permanent=True
-        ),
-    ),
-    path(
-        "help/change-notification-sound",
-        RedirectView.as_view(
-            url="/help/desktop-notifications#change-notification-sound", permanent=True
-        ),
-    ),
-    path(
-        "help/configure-message-notification-emails",
-        RedirectView.as_view(url="/help/email-notifications", permanent=True),
-    ),
-    path(
-        "help/disable-new-login-emails",
-        RedirectView.as_view(url="/help/email-notifications#new-login-emails", permanent=True),
-    ),
-    # This redirect is particularly important, because the old URL
-    # appears in links from Welcome Bot messages.
-    path(
-        "help/about-streams-and-topics",
-        RedirectView.as_view(url="/help/streams-and-topics", permanent=True),
-    ),
-    path(
-        "help/community-topic-edits",
-        RedirectView.as_view(url="/help/configure-who-can-edit-topics", permanent=True),
-    ),
-    path(
-        "help/only-allow-admins-to-add-emoji",
-        RedirectView.as_view(
-            url="/help/custom-emoji#change-who-can-add-custom-emoji", permanent=True
-        ),
-    ),
-    path(
-        "help/configure-who-can-add-custom-emoji",
-        RedirectView.as_view(
-            url="/help/custom-emoji#change-who-can-add-custom-emoji", permanent=True
-        ),
-    ),
-    path(
-        "help/add-custom-emoji",
-        RedirectView.as_view(url="/help/custom-emoji", permanent=True),
-    ),
-    path(
-        "help/night-mode",
-        RedirectView.as_view(url="/help/dark-theme", permanent=True),
-    ),
-    path(
-        "help/web-public-streams",
-        RedirectView.as_view(url="/help/public-access-option", permanent=True),
-    ),
     path("help/", help_documentation_view),
     path("help/<path:article>", help_documentation_view),
     path("api/", api_documentation_view),
     path("api/<slug:article>", api_documentation_view),
     path("policies/", policy_documentation_view),
     path("policies/<slug:article>", policy_documentation_view),
-    path(
-        "privacy/",
-        RedirectView.as_view(url="/policies/privacy"),
-    ),
-    path(
-        "terms/",
-        RedirectView.as_view(url="/policies/terms"),
-    ),
 ]
 
 # Two-factor URLs


Test patch:
diff --git a/zerver/lib/test_helpers.py b/zerver/lib/test_helpers.py
--- a/zerver/lib/test_helpers.py
+++ b/zerver/lib/test_helpers.py
@@ -481,24 +481,6 @@ def find_pattern(pattern: Any, prefixes: List[str]) -> None:
             "confirmation_key/",
             "node-coverage/(?P<path>.+)",
             "docs/(?P<path>.+)",
-            "help/add-custom-emoji",
-            "help/configure-who-can-add-custom-emoji",
-            "help/change-the-topic-of-a-message",
-            "help/configure-missed-message-emails",
-            "help/community-topic-edits",
-            "help/about-streams-and-topics",
-            "help/delete-a-stream",
-            "help/add-an-alert-word",
-            "help/change-notification-sound",
-            "help/configure-message-notification-emails",
-            "help/disable-new-login-emails",
-            "help/test-mobile-notifications",
-            "help/troubleshooting-desktop-notifications",
-            "help/web-public-streams",
-            "for/working-groups-and-communities/",
-            "help/only-allow-admins-to-add-emoji",
-            "help/night-mode",
-            "api/delete-stream",
             "casper/(?P<path>.+)",
             "static/(?P<path>.+)",
             "flush_caches",
diff --git a/zerver/tests/test_urls.py b/zerver/tests/test_urls.py
--- a/zerver/tests/test_urls.py
+++ b/zerver/tests/test_urls.py
@@ -6,6 +6,12 @@
 from django.test import Client
 
 from zerver.lib.test_classes import ZulipTestCase
+from zerver.lib.url_redirects import (
+    API_DOCUMENTATION_REDIRECTS,
+    HELP_DOCUMENTATION_REDIRECTS,
+    LANDING_PAGE_REDIRECTS,
+    POLICY_DOCUMENTATION_REDIRECTS,
+)
 from zerver.models import Realm, Stream
 from zproject import urls
 
@@ -154,3 +160,25 @@ def test_bogus_http_host(self) -> None:
             "/json/users", secure=True, HTTP_REFERER="https://somewhere", HTTP_HOST="$nonsense"
         )
         self.assertEqual(result.status_code, 400)
+
+
+class RedirectURLTest(ZulipTestCase):
+    def test_api_redirects(self) -> None:
+        for redirect in API_DOCUMENTATION_REDIRECTS:
+            result = self.client_get(redirect.old_url, follow=True)
+            self.assert_in_success_response(["Zulip homepage", "API documentation home"], result)
+
+    def test_help_redirects(self) -> None:
+        for redirect in HELP_DOCUMENTATION_REDIRECTS:
+            result = self.client_get(redirect.old_url, follow=True)
+            self.assert_in_success_response(["Zulip homepage", "Help center home"], result)
+
+    def test_policy_redirects(self) -> None:
+        for redirect in POLICY_DOCUMENTATION_REDIRECTS:
+            result = self.client_get(redirect.old_url, follow=True)
+            self.assert_in_success_response(["Policies", "Archive"], result)
+
+    def test_landing_page_redirects(self) -> None:
+        for redirect in LANDING_PAGE_REDIRECTS:
+            result = self.client_get(redirect.old_url, follow=True)
+            self.assert_in_success_response(["Download"], result)