From ee77c6365a7f45c02f649d9d9b6bde67e45d46c9 Mon Sep 17 00:00:00 2001 From: Tim Abbott Date: Wed, 3 Nov 2021 13:36:54 -0700 Subject: [PATCH] portico: Use /help/ style pages for displaying policies. This replaces the TERMS_OF_SERVICE and PRIVACY_POLICY settings with just a POLICIES_DIRECTORY setting, in order to support settings (like Zulip Cloud) where there's more policies than just those two. With minor changes by Eeshan Garg. --- docs/overview/changelog.md | 4 + docs/production/authentication-methods.md | 2 +- docs/production/settings.md | 25 +++-- templates/corporate/policies/index.md | 5 + templates/corporate/policies/missing.md | 1 + templates/corporate/{ => policies}/privacy.md | 0 templates/corporate/policies/sidebar_index.md | 2 + templates/corporate/{ => policies}/terms.md | 0 templates/zerver/documentation_main.html | 14 +++ templates/zerver/policies_absent/missing.md | 6 ++ .../zerver/policies_absent/sidebar_index.md | 1 + templates/zerver/portico-header.html | 3 + templates/zerver/privacy.html | 38 -------- templates/zerver/terms.html | 35 ------- zerver/context_processors.py | 9 +- zerver/forms.py | 2 +- zerver/tests/test_auth_backends.py | 22 ++--- zerver/tests/test_docs.py | 91 ++++++++++--------- zerver/tests/test_home.py | 6 +- zerver/tests/test_signup.py | 8 +- zerver/views/documentation.py | 48 +++++++++- zerver/views/home.py | 3 - zerver/views/portico.py | 23 ----- zproject/default_settings.py | 3 +- zproject/dev_settings.py | 8 +- zproject/prod_settings_template.py | 12 ++- zproject/urls.py | 22 ++++- 27 files changed, 204 insertions(+), 189 deletions(-) create mode 100644 templates/corporate/policies/index.md create mode 100644 templates/corporate/policies/missing.md rename templates/corporate/{ => policies}/privacy.md (100%) create mode 100644 templates/corporate/policies/sidebar_index.md rename templates/corporate/{ => policies}/terms.md (100%) create mode 100644 templates/zerver/policies_absent/missing.md create mode 100644 templates/zerver/policies_absent/sidebar_index.md delete mode 100644 templates/zerver/privacy.html delete mode 100644 templates/zerver/terms.html diff --git a/docs/overview/changelog.md b/docs/overview/changelog.md index 37ab270d95..605f3db7d4 100644 --- a/docs/overview/changelog.md +++ b/docs/overview/changelog.md @@ -34,6 +34,10 @@ log][commit-log] for an up-to-date list of raw changes. - This release contains a migration, `0009_confirmation_expiry_date_backfill`, that can take several minutes to run on a server with millions of messages of history. +- The `TERMS_OF_SERVICE` and `PRIVACY_POLICY` settings have been + removed in favor of a system that supports additional policy + documents, such as a code of conduct. See the [updated + documentation](../production/settings.md) for the new system. #### Full feature changelog diff --git a/docs/production/authentication-methods.md b/docs/production/authentication-methods.md index 463e78ad9a..99dd9c0266 100644 --- a/docs/production/authentication-methods.md +++ b/docs/production/authentication-methods.md @@ -850,7 +850,7 @@ prefills that value in the new account creation form, but gives the user the opportunity to edit it before submitting. When `True`, Zulip assumes the name is correct, and new users will not be presented with a registration form unless they need to accept Terms of Service for -the server (i.e. `TERMS_OF_SERVICE=True`). +the server (i.e. `TERMS_OF_SERVICE_VERSION` is set). ## Adding more authentication backends diff --git a/docs/production/settings.md b/docs/production/settings.md index 6b73f7fd17..ab67426762 100644 --- a/docs/production/settings.md +++ b/docs/production/settings.md @@ -86,12 +86,25 @@ and configure this service. ### Terms of Service and Privacy policy Zulip allows you to configure your server's Terms of Service and -Privacy Policy pages (`/terms` and `/privacy`, respectively). You can -use the `TERMS_OF_SERVICE` and `PRIVACY_POLICY` settings to configure -the path to your server's policies. The syntax is Markdown (with -support for included HTML). A good approach is to use paths like -`/etc/zulip/terms.md`, so that it's easy to back up your policy -configuration along with your other Zulip server configuration. +Privacy Policy pages (`/terms` and `/privacy`, respectively). + +You can configure this using the `POLICIES_DIRECTORY` setting. We +recommend using `/etc/zulip/policies`, so that your policies are +naturally backed up with the server's other configuration. Just place +Markdown files named `terms.md` and `privacy.md` in that directory, +and set `TERMS_OF_SERVICE_VERSION` to `1.0` to enable this feature. + +You can place additional files in this directory to document +additional policies; if you do so, you may want to: + +- Create a Markdown file `sidebar_index.md` listing the pages in your + policies site; this generates the policies site navigation. +- Create a Markdown file `missing.md` with custom content for 404s in + this directory. + +Please make clear in these pages what organization is hosting your +Zulip server, so that nobody could be confused that your policies are +the policies for Zulip Cloud. ### Miscellaneous server settings diff --git a/templates/corporate/policies/index.md b/templates/corporate/policies/index.md new file mode 100644 index 0000000000..d7f889656c --- /dev/null +++ b/templates/corporate/policies/index.md @@ -0,0 +1,5 @@ +# Terms and policies + +* [Terms of Service](/policies/terms) +* [Privacy Policy](/policies/privacy) +* [Rules of Use](/policies/rules) diff --git a/templates/corporate/policies/missing.md b/templates/corporate/policies/missing.md new file mode 100644 index 0000000000..b48cabaf27 --- /dev/null +++ b/templates/corporate/policies/missing.md @@ -0,0 +1 @@ +No such page. diff --git a/templates/corporate/privacy.md b/templates/corporate/policies/privacy.md similarity index 100% rename from templates/corporate/privacy.md rename to templates/corporate/policies/privacy.md diff --git a/templates/corporate/policies/sidebar_index.md b/templates/corporate/policies/sidebar_index.md new file mode 100644 index 0000000000..0fbc683943 --- /dev/null +++ b/templates/corporate/policies/sidebar_index.md @@ -0,0 +1,2 @@ +## [Terms of Service](/policies/terms) +## [Privacy Policy](/policies/privacy) diff --git a/templates/corporate/terms.md b/templates/corporate/policies/terms.md similarity index 100% rename from templates/corporate/terms.md rename to templates/corporate/policies/terms.md diff --git a/templates/zerver/documentation_main.html b/templates/zerver/documentation_main.html index a3ca21ac25..f3f3b6f9d1 100644 --- a/templates/zerver/documentation_main.html +++ b/templates/zerver/documentation_main.html @@ -10,10 +10,20 @@
@@ -23,7 +33,11 @@
+ {% if page_is_policy_center %} + {{ render_markdown_path(article, pure_markdown=True) }} + {% else %} {{ render_markdown_path(article, api_uri_context) }} + {% endif %} {% endif %}
diff --git a/templates/zerver/privacy.html b/templates/zerver/privacy.html deleted file mode 100644 index c7508f8387..0000000000 --- a/templates/zerver/privacy.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "zerver/portico.html" %} -{% set entrypoint = "landing-page" %} - -{% block title %} -Zulip: the best group chat for open source projects -{% endblock %} - -{% block customhead %} - -{% endblock %} - -{% block portico_content %} - -{% include 'zerver/landing_nav.html' %} - -
-
-

{% trans %}Privacy policy{% endtrans %}

-
-
-
-
- {% if privacy_policy %} - {{ render_markdown_path(privacy_policy, pure_markdown=True) }} - {% else %} - {% trans %} - This installation of Zulip does not have a configured privacy policy. - Contact this server's administrator - if you have any questions. - {% endtrans %} - {% endif %} -
-
-
-
- - -{% endblock %} diff --git a/templates/zerver/terms.html b/templates/zerver/terms.html deleted file mode 100644 index 7b5ff8e013..0000000000 --- a/templates/zerver/terms.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "zerver/portico.html" %} -{% set entrypoint = "landing-page" %} - -{# Terms of Service. #} - -{% block customhead %} - -{% endblock %} - -{% block portico_content %} - -{% include 'zerver/landing_nav.html' %} - -
-
-

{% trans %}Terms of Service{% endtrans %}

-
-
-
-
- {% if terms_of_service %} - {{ render_markdown_path(terms_of_service, pure_markdown=True) }} - {% else %} - {% trans %} - This installation of Zulip does not have a configured terms of service. - Contact this server's administrator - if you have any questions. - {% endtrans %} - {% endif %} -
-
-
-
- -{% endblock %} diff --git a/zerver/context_processors.py b/zerver/context_processors.py index fffcda8532..50bc6198cc 100644 --- a/zerver/context_processors.py +++ b/zerver/context_processors.py @@ -81,6 +81,11 @@ def get_apps_page_url() -> str: return "https://zulip.com/apps/" +def is_isolated_page(request: HttpRequest) -> bool: + """Accept a GET param `?nav=no` to render an isolated, navless page.""" + return request.GET.get("nav") == "no" + + def zulip_default_context(request: HttpRequest) -> Dict[str, Any]: """Context available to all Zulip Jinja2 templates that have a request passed in. Designed to provide the long list of variables at the @@ -145,8 +150,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]: "custom_logo_url": settings.CUSTOM_LOGO_URL, "register_link_disabled": register_link_disabled, "login_link_disabled": login_link_disabled, - "terms_of_service": settings.TERMS_OF_SERVICE, - "privacy_policy": settings.PRIVACY_POLICY, + "terms_of_service": settings.TERMS_OF_SERVICE_VERSION is not None, "login_url": settings.HOME_NOT_LOGGED_IN, "only_sso": settings.ONLY_SSO, "external_host": settings.EXTERNAL_HOST, @@ -172,6 +176,7 @@ def zulip_default_context(request: HttpRequest) -> Dict[str, Any]: "platform": RequestNotes.get_notes(request).client_name, "allow_search_engine_indexing": allow_search_engine_indexing, "landing_page_navbar_message": settings.LANDING_PAGE_NAVBAR_MESSAGE, + "is_isolated_page": is_isolated_page(request), "default_page_params": default_page_params, } diff --git a/zerver/forms.py b/zerver/forms.py index 07c275e953..35f1055b64 100644 --- a/zerver/forms.py +++ b/zerver/forms.py @@ -126,7 +126,7 @@ class RegistrationForm(forms.Form): del kwargs["realm_creation"] super().__init__(*args, **kwargs) - if settings.TERMS_OF_SERVICE: + if settings.TERMS_OF_SERVICE_VERSION is not None: self.fields["terms"] = forms.BooleanField(required=True) self.fields["realm_name"] = forms.CharField( max_length=Realm.MAX_REALM_NAME_LENGTH, required=self.realm_creation diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index 9adc7fc979..e82573d551 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -1416,7 +1416,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): self.assertFalse(user_profile.has_usable_password()) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_registration(self) -> None: """If the user doesn't exist yet, social auth can be used to register an account""" email = "newuser@zulip.com" @@ -1431,7 +1431,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): result, realm, subdomain, email, name, name, self.BACKEND_CLASS.full_name_validated ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_mobile_registration(self) -> None: email = "newuser@zulip.com" name = "Full Name" @@ -1458,7 +1458,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): mobile_flow_otp=mobile_flow_otp, ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_desktop_registration(self) -> None: email = "newuser@zulip.com" name = "Full Name" @@ -1485,7 +1485,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): desktop_flow_otp=desktop_flow_otp, ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_registration_invitation_exists(self) -> None: """ This tests the registration flow in the case where an invitation for the user @@ -1507,7 +1507,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): result, realm, subdomain, email, name, name, self.BACKEND_CLASS.full_name_validated ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_with_invalid_multiuse_invite(self) -> None: email = "newuser@zulip.com" name = "Full Name" @@ -1528,7 +1528,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): self.assertEqual(result.status_code, 404) self.assert_in_response("Whoops. The confirmation link is malformed.", result) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_registration_using_multiuse_invite(self) -> None: """If the user doesn't exist yet, social auth can be used to register an account""" email = "newuser@zulip.com" @@ -1628,7 +1628,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): result, ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_with_ldap_populate_registration_from_confirmation(self) -> None: self.init_default_ldap_database() email = "newuser@zulip.com" @@ -1691,7 +1691,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): log_warn.output, [f"WARNING:root:New account email {email} could not be found in LDAP"] ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_with_ldap_auth_registration_from_confirmation(self) -> None: """ This test checks that in configurations that use the LDAP authentication backend @@ -1784,7 +1784,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase): self.assertEqual(result.status_code, 302) self.assertIn("login", result.url) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_invited_as_admin_but_expired(self) -> None: iago = self.example_user("iago") email = self.nonreg_email("alice") @@ -2167,7 +2167,7 @@ class SAMLAuthBackendTest(SocialAuthBase): result, ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_registration_auto_signup(self) -> None: """ Verify that with SAML auto signup enabled, a user coming from the /login page @@ -3335,7 +3335,7 @@ class GenericOpenIdConnectTest(SocialAuthBase): family_name=name.split(" ")[1], ) - @override_settings(TERMS_OF_SERVICE=None) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_social_auth_registration_auto_signup(self) -> None: """ The analogue of the auto_signup test for SAML. diff --git a/zerver/tests/test_docs.py b/zerver/tests/test_docs.py index 59a5975d36..0ce4e4514f 100644 --- a/zerver/tests/test_docs.py +++ b/zerver/tests/test_docs.py @@ -561,49 +561,54 @@ class AppsPageTest(ZulipTestCase): class PrivacyTermsTest(ZulipTestCase): - def test_custom_tos_template(self) -> None: - response = self.client_get("/terms/") - - self.assert_in_success_response( - [ - 'Thanks for using our products and services ("Services"). ', - "By using our Services, you are agreeing to these terms", - ], - response, - ) + def test_terms_and_policies_index(self) -> None: + with self.settings(POLICIES_DIRECTORY="corporate/policies"): + response = self.client_get("/policies/") + self.assert_in_success_response(["Terms and policies"], response) def test_custom_terms_of_service_template(self) -> None: - not_configured_message = ( - "This installation of Zulip does not have a configured terms of service" - ) - with self.settings(TERMS_OF_SERVICE=None): - response = self.client_get("/terms/") - self.assert_in_success_response([not_configured_message], response) - with self.settings(TERMS_OF_SERVICE="zerver/tests/markdown/test_markdown.md"): - response = self.client_get("/terms/") - self.assert_in_success_response(["This is some bold text."], response) - self.assert_not_in_success_response([not_configured_message], response) + not_configured_message = "This server is an installation" + with self.settings(POLICIES_DIRECTORY="zerver/policies_absent"): + response = self.client_get("/policies/terms") + self.assert_in_response(not_configured_message, response) + + with self.settings(POLICIES_DIRECTORY="corporate/policies"): + response = self.client_get("/policies/terms") + self.assert_in_success_response(["Kandra Labs"], response) def test_custom_privacy_policy_template(self) -> None: - not_configured_message = ( - "This installation of Zulip does not have a configured privacy policy" - ) - with self.settings(PRIVACY_POLICY=None): - response = self.client_get("/privacy/") - self.assert_in_success_response([not_configured_message], response) - with self.settings(PRIVACY_POLICY="zerver/tests/markdown/test_markdown.md"): - response = self.client_get("/privacy/") - self.assert_in_success_response(["This is some bold text."], response) - self.assert_not_in_success_response([not_configured_message], response) + not_configured_message = "This server is an installation" + with self.settings(POLICIES_DIRECTORY="zerver/policies_absent"): + response = self.client_get("/policies/privacy") + self.assert_in_response(not_configured_message, response) + + with self.settings(POLICIES_DIRECTORY="corporate/policies"): + response = self.client_get("/policies/privacy") + self.assert_in_success_response(["Kandra Labs"], response) def test_custom_privacy_policy_template_with_absolute_url(self) -> None: + """Verify that using our recommended production default of an absolute path + like /etc/zulip/policies/ works.""" current_dir = os.path.dirname(os.path.abspath(__file__)) - abs_path = os.path.join( - current_dir, "..", "..", "templates/zerver/tests/markdown/test_markdown.md" + abs_path = os.path.abspath( + os.path.join(current_dir, "..", "..", "templates/corporate/policies") ) - with self.settings(PRIVACY_POLICY=abs_path): - response = self.client_get("/privacy/") - self.assert_in_success_response(["This is some bold text."], response) + with self.settings(POLICIES_DIRECTORY=abs_path): + response = self.client_get("/policies/privacy") + self.assert_in_success_response(["Kandra Labs"], response) + + with self.settings(POLICIES_DIRECTORY=abs_path): + response = self.client_get("/policies/nonexistent") + self.assert_in_response("No such page", response) + + def test_redirects_from_older_urls(self) -> None: + with self.settings(POLICIES_DIRECTORY="corporate/policies"): + result = self.client_get("/privacy/", follow=True) + self.assert_in_success_response(["Kandra Labs"], result) + + with self.settings(POLICIES_DIRECTORY="corporate/policies"): + result = self.client_get("/terms/", follow=True) + self.assert_in_success_response(["Kandra Labs"], result) def test_no_nav(self) -> None: # Test that our ?nav=0 feature of /privacy and /terms, @@ -611,11 +616,15 @@ class PrivacyTermsTest(ZulipTestCase): # policies that ToS/Privacy pages linked from an iOS app have # no links to the rest of the site if there's pricing # information for anything elsewhere on the site. - response = self.client_get("/terms/") - self.assert_in_success_response(["Plans"], response) - response = self.client_get("/terms/", {"nav": "no"}) - self.assert_not_in_success_response(["Plans"], response) + # We don't have this link at all on these pages; this first + # line of the test would change if we were to adjust the + # design. + response = self.client_get("/policies/terms") + self.assert_not_in_success_response(["Back to Zulip"], response) - response = self.client_get("/privacy/", {"nav": "no"}) - self.assert_not_in_success_response(["Plans"], response) + response = self.client_get("/policies/terms", {"nav": "no"}) + self.assert_not_in_success_response(["Back to Zulip"], response) + + response = self.client_get("/policies/privacy", {"nav": "no"}) + self.assert_not_in_success_response(["Back to Zulip"], response) diff --git a/zerver/tests/test_home.py b/zerver/tests/test_home.py index b5d17f5d81..329cf3648f 100644 --- a/zerver/tests/test_home.py +++ b/zerver/tests/test_home.py @@ -387,6 +387,7 @@ class HomeTest(ZulipTestCase): # Should be successful after calling 2fa login function. self.check_rendered_logged_in_app(result) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_num_queries_for_realm_admin(self) -> None: # Verify number of queries for Realm admin isn't much higher than for normal users. self.login("iago") @@ -457,10 +458,7 @@ class HomeTest(ZulipTestCase): user.tos_version = user_tos_version user.save() - with self.settings(TERMS_OF_SERVICE="whatever"), self.settings( - TERMS_OF_SERVICE_VERSION="99.99" - ): - + with self.settings(TERMS_OF_SERVICE_VERSION="99.99"): result = self.client_get("/", dict(stream="Denmark")) html = result.content.decode() diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index b0757a7632..1b794fafd3 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -5127,7 +5127,7 @@ class UserSignUpTest(InviteUserBase): LDAP_APPEND_DOMAIN="zulip.com", AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map, AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",), - TERMS_OF_SERVICE=False, + TERMS_OF_SERVICE_VERSION=1.0, ): result = self.client_get(confirmation_url) self.assertEqual(result.status_code, 200) @@ -5259,7 +5259,7 @@ class UserSignUpTest(InviteUserBase): "AssertionError: Mirror dummy user is already active!" in error_log.output[0] ) - @override_settings(TERMS_OF_SERVICE=False) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_dev_user_registration(self) -> None: """Verify that /devtools/register_user creates a new user, logs them in, and redirects to the logged-in app.""" @@ -5275,7 +5275,7 @@ class UserSignUpTest(InviteUserBase): self.assertEqual(result["Location"], "http://zulip.testserver/") self.assert_logged_in_user_id(user_profile.id) - @override_settings(TERMS_OF_SERVICE=False) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_dev_user_registration_create_realm(self) -> None: count = UserProfile.objects.count() string_id = f"realm-{count}" @@ -5293,7 +5293,7 @@ class UserSignUpTest(InviteUserBase): assert user_profile is not None self.assert_logged_in_user_id(user_profile.id) - @override_settings(TERMS_OF_SERVICE=False) + @override_settings(TERMS_OF_SERVICE_VERSION=None) def test_dev_user_registration_create_demo_realm(self) -> None: result = self.client_post("/devtools/register_demo_realm/") self.assertEqual(result.status_code, 302) diff --git a/zerver/views/documentation.py b/zerver/views/documentation.py index 8bc95b9488..4c7babf557 100644 --- a/zerver/views/documentation.py +++ b/zerver/views/documentation.py @@ -69,6 +69,7 @@ class ApiURLView(TemplateView): class MarkdownDirectoryView(ApiURLView): path_template = "" + policies_view = False def get_path(self, article: str) -> DocumentationArticle: http_status = 200 @@ -87,10 +88,25 @@ class MarkdownDirectoryView(ApiURLView): endpoint_name = None endpoint_method = None + if self.policies_view and self.path_template.startswith("/"): + # This block is required because neither the Django + # template loader nor the article_path logic below support + # settings.POLICIES_DIRECTORY being an absolute path. + if not os.path.exists(path): + article = "missing" + http_status = 404 + path = self.path_template % (article,) + + return DocumentationArticle( + article_path=path, + article_http_status=http_status, + endpoint_path=None, + endpoint_method=None, + ) + # The following is a somewhat hacky approach to extract titles from articles. # Hack: `context["article"] has a leading `/`, so we use + to add directories. article_path = os.path.join(settings.DEPLOY_ROOT, "templates") + path - if (not os.path.exists(article_path)) and self.path_template == "/zerver/api/%s.md": try: endpoint_name, endpoint_method = get_endpoint_from_operationid(article) @@ -102,6 +118,7 @@ class MarkdownDirectoryView(ApiURLView): endpoint_path=None, endpoint_method=None, ) + try: loader.get_template(path) return DocumentationArticle( @@ -124,6 +141,20 @@ class MarkdownDirectoryView(ApiURLView): documentation_article = self.get_path(article) context["article"] = documentation_article.article_path + if documentation_article.article_path.startswith("/") and os.path.exists( + documentation_article.article_path + ): + # Absolute path case + article_path = documentation_article.article_path + elif documentation_article.article_path.startswith("/"): + # Hack: `context["article"] has a leading `/`, so we use + to add directories. + article_path = ( + os.path.join(settings.DEPLOY_ROOT, "templates") + documentation_article.article_path + ) + else: + article_path = os.path.join( + settings.DEPLOY_ROOT, "templates", documentation_article.article_path + ) # For disabling the "Back to home" on the homepage context["not_index_page"] = not context["article"].endswith("/index.md") @@ -134,6 +165,13 @@ class MarkdownDirectoryView(ApiURLView): sidebar_article = self.get_path("include/sidebar_index") sidebar_index = sidebar_article.article_path title_base = "Zulip Help Center" + elif self.path_template == f"{settings.POLICIES_DIRECTORY}/%s.md": + context["page_is_policy_center"] = True + context["doc_root"] = "/policies/" + context["doc_root_title"] = "Terms and policies" + sidebar_article = self.get_path("sidebar_index") + sidebar_index = sidebar_article.article_path + title_base = "Zulip terms and policies" else: context["page_is_api_center"] = True context["doc_root"] = "/api/" @@ -143,8 +181,6 @@ class MarkdownDirectoryView(ApiURLView): title_base = "Zulip API documentation" # The following is a somewhat hacky approach to extract titles from articles. - # Hack: `context["article"] has a leading `/`, so we use + to add directories. - article_path = os.path.join(settings.DEPLOY_ROOT, "templates") + context["article"] endpoint_name = None endpoint_method = None if os.path.exists(article_path): @@ -188,6 +224,12 @@ class MarkdownDirectoryView(ApiURLView): return context def get(self, request: HttpRequest, article: str = "") -> HttpResponse: + # Hack: It's hard to reinitialize urls.py from tests, and so + # we want to defer the use of settings.POLICIES_DIRECTORY to + # runtime. + if self.policies_view: + self.path_template = f"{settings.POLICIES_DIRECTORY}/%s.md" + documentation_article = self.get_path(article) http_status = documentation_article.article_http_status result = super().get(self, article=article) diff --git a/zerver/views/home.py b/zerver/views/home.py index 6438e58448..9b9d7217fb 100644 --- a/zerver/views/home.py +++ b/zerver/views/home.py @@ -26,9 +26,6 @@ def need_accept_tos(user_profile: Optional[UserProfile]) -> bool: if user_profile is None: return False - if settings.TERMS_OF_SERVICE is None: # nocoverage - return False - if settings.TERMS_OF_SERVICE_VERSION is None: return False diff --git a/zerver/views/portico.py b/zerver/views/portico.py index 43e78ece27..ed9bfa0d4c 100644 --- a/zerver/views/portico.py +++ b/zerver/views/portico.py @@ -95,11 +95,6 @@ def team_view(request: HttpRequest) -> HttpResponse: ) -def get_isolated_page(request: HttpRequest) -> bool: - """Accept a GET param `?nav=no` to render an isolated, navless page.""" - return request.GET.get("nav") == "no" - - @add_google_analytics def landing_view(request: HttpRequest, template_name: str) -> HttpResponse: return TemplateResponse(request, template_name) @@ -108,21 +103,3 @@ def landing_view(request: HttpRequest, template_name: str) -> HttpResponse: @add_google_analytics def hello_view(request: HttpRequest) -> HttpResponse: return TemplateResponse(request, "zerver/hello.html", latest_info_context()) - - -@add_google_analytics -def terms_view(request: HttpRequest) -> HttpResponse: - return TemplateResponse( - request, - "zerver/terms.html", - context={"isolated_page": get_isolated_page(request)}, - ) - - -@add_google_analytics -def privacy_view(request: HttpRequest) -> HttpResponse: - return TemplateResponse( - request, - "zerver/privacy.html", - context={"isolated_page": get_isolated_page(request)}, - ) diff --git a/zproject/default_settings.py b/zproject/default_settings.py index 43d2cb0023..3e511e420f 100644 --- a/zproject/default_settings.py +++ b/zproject/default_settings.py @@ -177,8 +177,7 @@ TORNADO_PORTS: List[int] = [] USING_TORNADO = True # ToS/Privacy templates -PRIVACY_POLICY: Optional[str] = None -TERMS_OF_SERVICE: Optional[str] = None +POLICIES_DIRECTORY: str = "zerver/policies_absent" # Security ENABLE_FILE_LINKS = False diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py index 57262dcc38..cb30e382ee 100644 --- a/zproject/dev_settings.py +++ b/zproject/dev_settings.py @@ -77,8 +77,9 @@ OPEN_REALM_CREATION = True WEB_PUBLIC_STREAMS_ENABLED = True INVITES_MIN_USER_AGE_DAYS = 0 -TERMS_OF_SERVICE = "corporate/terms.md" -PRIVACY_POLICY = "corporate/privacy.md" +# For development convenience, configure the ToS/Privacy Policies +POLICIES_DIRECTORY = "corporate/policies" +TERMS_OF_SERVICE_VERSION = "1.0" EMBEDDED_BOTS_ENABLED = True @@ -166,9 +167,6 @@ SEARCH_PILLS_ENABLED = bool(os.getenv("SEARCH_PILLS_ENABLED", False)) BILLING_ENABLED = True LANDING_PAGE_NAVBAR_MESSAGE: Optional[str] = None -# Test custom TOS template rendering -TERMS_OF_SERVICE = "corporate/terms.md" - # Our run-dev.py proxy uses X-Forwarded-Port to communicate to Django # that the request is actually on port 9991, not port 9992 (the Django # server's own port); this setting tells Django to read that HTTP diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index 0e422f10e2..de296af78c 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -760,9 +760,11 @@ CAMO_URI = "/external_content/" ## together into one bucket when applying rate-limiting. # RATE_LIMIT_TOR_TOGETHER = False -## If you want to set a Terms of Service for your server, set the path -## to your Markdown file, and uncomment the following line. -# TERMS_OF_SERVICE = '/etc/zulip/terms.md' +## Configuration for Terms of Service and Privacy Policy for the +## server. If unset, Zulip will never prompt users to accept Terms of +## Service. Users will be prompted to accept the terms during account +## registration, and during login if this value has changed. +# TERMS_OF_SERVICE_VERSION = "1.0" -## Similarly if you want to set a Privacy Policy. -# PRIVACY_POLICY = '/etc/zulip/privacy.md' +## Directory containing Markdown files for the server's policies. +# POLICIES_DIRECTORY = "/etc/zulip/policies/" diff --git a/zproject/urls.py b/zproject/urls.py index 374d467c10..040e738bc6 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -83,9 +83,7 @@ from zerver.views.portico import ( hello_view, landing_view, plans_view, - privacy_view, team_view, - terms_view, ) from zerver.views.presence import ( get_presence_backend, @@ -607,6 +605,9 @@ i18n_urls = [ path("apps/", apps_view), path("apps/download/", app_download_link_redirect), path("apps/", apps_view), + path( + "developer-community/", RedirectView.as_view(url="/development-community/", permanent=True) + ), path( "development-community/", landing_view, @@ -641,9 +642,6 @@ i18n_urls = [ RedirectView.as_view(url="/for/communities/", permanent=True), ), path("security/", landing_view, {"template_name": "zerver/security.html"}), - # Terms of Service and privacy pages. - path("terms/", terms_view), - path("privacy/", privacy_view), ] # Make a copy of i18n_urls so that they appear without prefix for english @@ -805,6 +803,10 @@ help_documentation_view = MarkdownDirectoryView.as_view( api_documentation_view = MarkdownDirectoryView.as_view( template_name="zerver/documentation_main.html", path_template="/zerver/api/%s.md" ) +policy_documentation_view = MarkdownDirectoryView.as_view( + template_name="zerver/documentation_main.html", + policies_view=True, +) urls += [ # Redirects due to us having moved the docs: path( @@ -883,6 +885,16 @@ urls += [ path("help/", help_documentation_view), path("api/", api_documentation_view), path("api/", api_documentation_view), + path("policies/", policy_documentation_view), + path("policies/", policy_documentation_view), + path( + "privacy/", + RedirectView.as_view(url="/policies/privacy"), + ), + path( + "terms/", + RedirectView.as_view(url="/policies/terms"), + ), ] # Two-factor URLs