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.
This commit is contained in:
Tim Abbott 2021-11-03 13:36:54 -07:00 committed by Tim Abbott
parent 95854d9d94
commit ee77c6365a
27 changed files with 204 additions and 189 deletions

View File

@ -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`, - This release contains a migration, `0009_confirmation_expiry_date_backfill`,
that can take several minutes to run on a server with millions of that can take several minutes to run on a server with millions of
messages of history. 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 #### Full feature changelog

View File

@ -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 user the opportunity to edit it before submitting. When `True`, Zulip
assumes the name is correct, and new users will not be presented with 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 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 ## Adding more authentication backends

View File

@ -86,12 +86,25 @@ and configure this service.
### Terms of Service and Privacy policy ### Terms of Service and Privacy policy
Zulip allows you to configure your server's Terms of Service and Zulip allows you to configure your server's Terms of Service and
Privacy Policy pages (`/terms` and `/privacy`, respectively). You can Privacy Policy pages (`/terms` and `/privacy`, respectively).
use the `TERMS_OF_SERVICE` and `PRIVACY_POLICY` settings to configure
the path to your server's policies. The syntax is Markdown (with You can configure this using the `POLICIES_DIRECTORY` setting. We
support for included HTML). A good approach is to use paths like recommend using `/etc/zulip/policies`, so that your policies are
`/etc/zulip/terms.md`, so that it's easy to back up your policy naturally backed up with the server's other configuration. Just place
configuration along with your other Zulip server configuration. 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 ### Miscellaneous server settings

View File

@ -0,0 +1,5 @@
# Terms and policies
* [Terms of Service](/policies/terms)
* [Privacy Policy](/policies/privacy)
* [Rules of Use](/policies/rules)

View File

@ -0,0 +1 @@
No such page.

View File

@ -0,0 +1,2 @@
## [Terms of Service](/policies/terms)
## [Privacy Policy](/policies/privacy)

View File

@ -10,10 +10,20 @@
<div class="app help terms-page inline-block{% if page_is_help_center %} help-center{% endif %}{% if page_is_api_center %} api-center{% endif %}"> <div class="app help terms-page inline-block{% if page_is_help_center %} help-center{% endif %}{% if page_is_api_center %} api-center{% endif %}">
<div class="sidebar"> <div class="sidebar">
<div class="content"> <div class="content">
{% if not page_is_policy_center %}
<h1><a href="https://zulip.com" class="no-underline">Zulip homepage</a></h1> <h1><a href="https://zulip.com" class="no-underline">Zulip homepage</a></h1>
<h1><a href="{{ doc_root }}" class="no-underline">{{ doc_root_title }} home</a></h1> <h1><a href="{{ doc_root }}" class="no-underline">{{ doc_root_title }} home</a></h1>
{% endif %}
{% if page_is_policy_center %}
{{ render_markdown_path(sidebar_index, pure_markdown=True) }}
{% else %}
{{ render_markdown_path(sidebar_index, api_uri_context) }} {{ render_markdown_path(sidebar_index, api_uri_context) }}
{% endif %}
{% if not page_is_policy_center %}
<h1 class="home-link"><a href="/" class="no-underline">Back to Zulip</a></h1> <h1 class="home-link"><a href="/" class="no-underline">Back to Zulip</a></h1>
{% endif %}
</div> </div>
</div> </div>
@ -23,7 +33,11 @@
<div class="markdown"> <div class="markdown">
<div class="content"> <div class="content">
{% if page_is_policy_center %}
{{ render_markdown_path(article, pure_markdown=True) }}
{% else %}
{{ render_markdown_path(article, api_uri_context) }} {{ render_markdown_path(article, api_uri_context) }}
{% endif %}
<div id="footer" class="documentation-footer"> <div id="footer" class="documentation-footer">
<hr /> <hr />

View File

@ -0,0 +1,6 @@
This server is an installation of [Zulip](https://zulip.com), open
source software for team collaboration.
This installation of Zulip has not been configured to display its
policies. You can contact its administrators using the email address
displayed below.

View File

@ -0,0 +1 @@
## No policies configured

View File

@ -17,6 +17,9 @@
{% if page_is_api_center %} {% if page_is_api_center %}
<span class="light"> | <a href="{{ root_domain_uri }}/api/">{{ doc_root_title }}</a></span> <span class="light"> | <a href="{{ root_domain_uri }}/api/">{{ doc_root_title }}</a></span>
{% endif %} {% endif %}
{% if page_is_policy_center %}
<span class="light"> | <a href="{{ root_domain_uri }}/policies/">{{ doc_root_title }}</a></span>
{% endif %}
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,38 +0,0 @@
{% extends "zerver/portico.html" %}
{% set entrypoint = "landing-page" %}
{% block title %}
<title>Zulip: the best group chat for open source projects</title>
{% endblock %}
{% block customhead %}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{% endblock %}
{% block portico_content %}
{% include 'zerver/landing_nav.html' %}
<div class="portico-landing why-page">
<div class="hero small-hero">
<h1 class="center">{% trans %}Privacy policy{% endtrans %}</h1>
</div>
<div class="main">
<div class="padded-content">
<div class="inner-content markdown">
{% 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 <a href="mailto:{{ support_email }}">server's administrator</a>
if you have any questions.
{% endtrans %}
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,35 +0,0 @@
{% extends "zerver/portico.html" %}
{% set entrypoint = "landing-page" %}
{# Terms of Service. #}
{% block customhead %}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{% endblock %}
{% block portico_content %}
{% include 'zerver/landing_nav.html' %}
<div class="portico-landing why-page">
<div class="hero small-hero">
<h1 class="center">{% trans %}Terms of Service{% endtrans %}</h1>
</div>
<div class="main">
<div class="padded-content">
<div class="inner-content markdown">
{% 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 <a href="mailto:{{ support_email }}">server's administrator</a>
if you have any questions.
{% endtrans %}
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -81,6 +81,11 @@ def get_apps_page_url() -> str:
return "https://zulip.com/apps/" 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]: def zulip_default_context(request: HttpRequest) -> Dict[str, Any]:
"""Context available to all Zulip Jinja2 templates that have a request """Context available to all Zulip Jinja2 templates that have a request
passed in. Designed to provide the long list of variables at the 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, "custom_logo_url": settings.CUSTOM_LOGO_URL,
"register_link_disabled": register_link_disabled, "register_link_disabled": register_link_disabled,
"login_link_disabled": login_link_disabled, "login_link_disabled": login_link_disabled,
"terms_of_service": settings.TERMS_OF_SERVICE, "terms_of_service": settings.TERMS_OF_SERVICE_VERSION is not None,
"privacy_policy": settings.PRIVACY_POLICY,
"login_url": settings.HOME_NOT_LOGGED_IN, "login_url": settings.HOME_NOT_LOGGED_IN,
"only_sso": settings.ONLY_SSO, "only_sso": settings.ONLY_SSO,
"external_host": settings.EXTERNAL_HOST, "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, "platform": RequestNotes.get_notes(request).client_name,
"allow_search_engine_indexing": allow_search_engine_indexing, "allow_search_engine_indexing": allow_search_engine_indexing,
"landing_page_navbar_message": settings.LANDING_PAGE_NAVBAR_MESSAGE, "landing_page_navbar_message": settings.LANDING_PAGE_NAVBAR_MESSAGE,
"is_isolated_page": is_isolated_page(request),
"default_page_params": default_page_params, "default_page_params": default_page_params,
} }

View File

@ -126,7 +126,7 @@ class RegistrationForm(forms.Form):
del kwargs["realm_creation"] del kwargs["realm_creation"]
super().__init__(*args, **kwargs) 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["terms"] = forms.BooleanField(required=True)
self.fields["realm_name"] = forms.CharField( self.fields["realm_name"] = forms.CharField(
max_length=Realm.MAX_REALM_NAME_LENGTH, required=self.realm_creation max_length=Realm.MAX_REALM_NAME_LENGTH, required=self.realm_creation

View File

@ -1416,7 +1416,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
self.assertFalse(user_profile.has_usable_password()) 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: def test_social_auth_registration(self) -> None:
"""If the user doesn't exist yet, social auth can be used to register an account""" """If the user doesn't exist yet, social auth can be used to register an account"""
email = "newuser@zulip.com" email = "newuser@zulip.com"
@ -1431,7 +1431,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
result, realm, subdomain, email, name, name, self.BACKEND_CLASS.full_name_validated 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: def test_social_auth_mobile_registration(self) -> None:
email = "newuser@zulip.com" email = "newuser@zulip.com"
name = "Full Name" name = "Full Name"
@ -1458,7 +1458,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
mobile_flow_otp=mobile_flow_otp, 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: def test_social_auth_desktop_registration(self) -> None:
email = "newuser@zulip.com" email = "newuser@zulip.com"
name = "Full Name" name = "Full Name"
@ -1485,7 +1485,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
desktop_flow_otp=desktop_flow_otp, 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: def test_social_auth_registration_invitation_exists(self) -> None:
""" """
This tests the registration flow in the case where an invitation for the user 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 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: def test_social_auth_with_invalid_multiuse_invite(self) -> None:
email = "newuser@zulip.com" email = "newuser@zulip.com"
name = "Full Name" name = "Full Name"
@ -1528,7 +1528,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
self.assertEqual(result.status_code, 404) self.assertEqual(result.status_code, 404)
self.assert_in_response("Whoops. The confirmation link is malformed.", result) 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: 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""" """If the user doesn't exist yet, social auth can be used to register an account"""
email = "newuser@zulip.com" email = "newuser@zulip.com"
@ -1628,7 +1628,7 @@ class SocialAuthBase(DesktopFlowTestingLib, ZulipTestCase):
result, 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: def test_social_auth_with_ldap_populate_registration_from_confirmation(self) -> None:
self.init_default_ldap_database() self.init_default_ldap_database()
email = "newuser@zulip.com" 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"] 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: def test_social_auth_with_ldap_auth_registration_from_confirmation(self) -> None:
""" """
This test checks that in configurations that use the LDAP authentication backend 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.assertEqual(result.status_code, 302)
self.assertIn("login", result.url) 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: def test_social_auth_invited_as_admin_but_expired(self) -> None:
iago = self.example_user("iago") iago = self.example_user("iago")
email = self.nonreg_email("alice") email = self.nonreg_email("alice")
@ -2167,7 +2167,7 @@ class SAMLAuthBackendTest(SocialAuthBase):
result, result,
) )
@override_settings(TERMS_OF_SERVICE=None) @override_settings(TERMS_OF_SERVICE_VERSION=None)
def test_social_auth_registration_auto_signup(self) -> None: def test_social_auth_registration_auto_signup(self) -> None:
""" """
Verify that with SAML auto signup enabled, a user coming from the /login page 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], 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: def test_social_auth_registration_auto_signup(self) -> None:
""" """
The analogue of the auto_signup test for SAML. The analogue of the auto_signup test for SAML.

View File

@ -561,49 +561,54 @@ class AppsPageTest(ZulipTestCase):
class PrivacyTermsTest(ZulipTestCase): class PrivacyTermsTest(ZulipTestCase):
def test_custom_tos_template(self) -> None: def test_terms_and_policies_index(self) -> None:
response = self.client_get("/terms/") with self.settings(POLICIES_DIRECTORY="corporate/policies"):
response = self.client_get("/policies/")
self.assert_in_success_response( self.assert_in_success_response(["Terms and policies"], response)
[
'Thanks for using our products and services ("Services"). ',
"By using our Services, you are agreeing to these terms",
],
response,
)
def test_custom_terms_of_service_template(self) -> None: def test_custom_terms_of_service_template(self) -> None:
not_configured_message = ( not_configured_message = "This server is an installation"
"This installation of Zulip does not have a configured terms of service" with self.settings(POLICIES_DIRECTORY="zerver/policies_absent"):
) response = self.client_get("/policies/terms")
with self.settings(TERMS_OF_SERVICE=None): self.assert_in_response(not_configured_message, response)
response = self.client_get("/terms/")
self.assert_in_success_response([not_configured_message], response) with self.settings(POLICIES_DIRECTORY="corporate/policies"):
with self.settings(TERMS_OF_SERVICE="zerver/tests/markdown/test_markdown.md"): response = self.client_get("/policies/terms")
response = self.client_get("/terms/") self.assert_in_success_response(["Kandra Labs"], response)
self.assert_in_success_response(["This is some <em>bold text</em>."], response)
self.assert_not_in_success_response([not_configured_message], response)
def test_custom_privacy_policy_template(self) -> None: def test_custom_privacy_policy_template(self) -> None:
not_configured_message = ( not_configured_message = "This server is an installation"
"This installation of Zulip does not have a configured privacy policy" with self.settings(POLICIES_DIRECTORY="zerver/policies_absent"):
) response = self.client_get("/policies/privacy")
with self.settings(PRIVACY_POLICY=None): self.assert_in_response(not_configured_message, response)
response = self.client_get("/privacy/")
self.assert_in_success_response([not_configured_message], response) with self.settings(POLICIES_DIRECTORY="corporate/policies"):
with self.settings(PRIVACY_POLICY="zerver/tests/markdown/test_markdown.md"): response = self.client_get("/policies/privacy")
response = self.client_get("/privacy/") self.assert_in_success_response(["Kandra Labs"], response)
self.assert_in_success_response(["This is some <em>bold text</em>."], response)
self.assert_not_in_success_response([not_configured_message], response)
def test_custom_privacy_policy_template_with_absolute_url(self) -> None: 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__)) current_dir = os.path.dirname(os.path.abspath(__file__))
abs_path = os.path.join( abs_path = os.path.abspath(
current_dir, "..", "..", "templates/zerver/tests/markdown/test_markdown.md" os.path.join(current_dir, "..", "..", "templates/corporate/policies")
) )
with self.settings(PRIVACY_POLICY=abs_path): with self.settings(POLICIES_DIRECTORY=abs_path):
response = self.client_get("/privacy/") response = self.client_get("/policies/privacy")
self.assert_in_success_response(["This is some <em>bold text</em>."], response) 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: def test_no_nav(self) -> None:
# Test that our ?nav=0 feature of /privacy and /terms, # 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 # policies that ToS/Privacy pages linked from an iOS app have
# no links to the rest of the site if there's pricing # no links to the rest of the site if there's pricing
# information for anything elsewhere on the site. # 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"}) # We don't have this link at all on these pages; this first
self.assert_not_in_success_response(["Plans"], response) # 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"}) response = self.client_get("/policies/terms", {"nav": "no"})
self.assert_not_in_success_response(["Plans"], response) 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)

View File

@ -387,6 +387,7 @@ class HomeTest(ZulipTestCase):
# Should be successful after calling 2fa login function. # Should be successful after calling 2fa login function.
self.check_rendered_logged_in_app(result) self.check_rendered_logged_in_app(result)
@override_settings(TERMS_OF_SERVICE_VERSION=None)
def test_num_queries_for_realm_admin(self) -> 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. # Verify number of queries for Realm admin isn't much higher than for normal users.
self.login("iago") self.login("iago")
@ -457,10 +458,7 @@ class HomeTest(ZulipTestCase):
user.tos_version = user_tos_version user.tos_version = user_tos_version
user.save() user.save()
with self.settings(TERMS_OF_SERVICE="whatever"), self.settings( with self.settings(TERMS_OF_SERVICE_VERSION="99.99"):
TERMS_OF_SERVICE_VERSION="99.99"
):
result = self.client_get("/", dict(stream="Denmark")) result = self.client_get("/", dict(stream="Denmark"))
html = result.content.decode() html = result.content.decode()

View File

@ -5127,7 +5127,7 @@ class UserSignUpTest(InviteUserBase):
LDAP_APPEND_DOMAIN="zulip.com", LDAP_APPEND_DOMAIN="zulip.com",
AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map, AUTH_LDAP_USER_ATTR_MAP=ldap_user_attr_map,
AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",), AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",),
TERMS_OF_SERVICE=False, TERMS_OF_SERVICE_VERSION=1.0,
): ):
result = self.client_get(confirmation_url) result = self.client_get(confirmation_url)
self.assertEqual(result.status_code, 200) 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] "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: def test_dev_user_registration(self) -> None:
"""Verify that /devtools/register_user creates a new user, logs them """Verify that /devtools/register_user creates a new user, logs them
in, and redirects to the logged-in app.""" in, and redirects to the logged-in app."""
@ -5275,7 +5275,7 @@ class UserSignUpTest(InviteUserBase):
self.assertEqual(result["Location"], "http://zulip.testserver/") self.assertEqual(result["Location"], "http://zulip.testserver/")
self.assert_logged_in_user_id(user_profile.id) 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: def test_dev_user_registration_create_realm(self) -> None:
count = UserProfile.objects.count() count = UserProfile.objects.count()
string_id = f"realm-{count}" string_id = f"realm-{count}"
@ -5293,7 +5293,7 @@ class UserSignUpTest(InviteUserBase):
assert user_profile is not None assert user_profile is not None
self.assert_logged_in_user_id(user_profile.id) 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: def test_dev_user_registration_create_demo_realm(self) -> None:
result = self.client_post("/devtools/register_demo_realm/") result = self.client_post("/devtools/register_demo_realm/")
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)

View File

@ -69,6 +69,7 @@ class ApiURLView(TemplateView):
class MarkdownDirectoryView(ApiURLView): class MarkdownDirectoryView(ApiURLView):
path_template = "" path_template = ""
policies_view = False
def get_path(self, article: str) -> DocumentationArticle: def get_path(self, article: str) -> DocumentationArticle:
http_status = 200 http_status = 200
@ -87,10 +88,25 @@ class MarkdownDirectoryView(ApiURLView):
endpoint_name = None endpoint_name = None
endpoint_method = 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. # The following is a somewhat hacky approach to extract titles from articles.
# Hack: `context["article"] has a leading `/`, so we use + to add directories. # Hack: `context["article"] has a leading `/`, so we use + to add directories.
article_path = os.path.join(settings.DEPLOY_ROOT, "templates") + path 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": if (not os.path.exists(article_path)) and self.path_template == "/zerver/api/%s.md":
try: try:
endpoint_name, endpoint_method = get_endpoint_from_operationid(article) endpoint_name, endpoint_method = get_endpoint_from_operationid(article)
@ -102,6 +118,7 @@ class MarkdownDirectoryView(ApiURLView):
endpoint_path=None, endpoint_path=None,
endpoint_method=None, endpoint_method=None,
) )
try: try:
loader.get_template(path) loader.get_template(path)
return DocumentationArticle( return DocumentationArticle(
@ -124,6 +141,20 @@ class MarkdownDirectoryView(ApiURLView):
documentation_article = self.get_path(article) documentation_article = self.get_path(article)
context["article"] = documentation_article.article_path 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 # For disabling the "Back to home" on the homepage
context["not_index_page"] = not context["article"].endswith("/index.md") 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_article = self.get_path("include/sidebar_index")
sidebar_index = sidebar_article.article_path sidebar_index = sidebar_article.article_path
title_base = "Zulip Help Center" 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: else:
context["page_is_api_center"] = True context["page_is_api_center"] = True
context["doc_root"] = "/api/" context["doc_root"] = "/api/"
@ -143,8 +181,6 @@ class MarkdownDirectoryView(ApiURLView):
title_base = "Zulip API documentation" title_base = "Zulip API documentation"
# The following is a somewhat hacky approach to extract titles from articles. # 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_name = None
endpoint_method = None endpoint_method = None
if os.path.exists(article_path): if os.path.exists(article_path):
@ -188,6 +224,12 @@ class MarkdownDirectoryView(ApiURLView):
return context return context
def get(self, request: HttpRequest, article: str = "") -> HttpResponse: 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) documentation_article = self.get_path(article)
http_status = documentation_article.article_http_status http_status = documentation_article.article_http_status
result = super().get(self, article=article) result = super().get(self, article=article)

View File

@ -26,9 +26,6 @@ def need_accept_tos(user_profile: Optional[UserProfile]) -> bool:
if user_profile is None: if user_profile is None:
return False return False
if settings.TERMS_OF_SERVICE is None: # nocoverage
return False
if settings.TERMS_OF_SERVICE_VERSION is None: if settings.TERMS_OF_SERVICE_VERSION is None:
return False return False

View File

@ -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 @add_google_analytics
def landing_view(request: HttpRequest, template_name: str) -> HttpResponse: def landing_view(request: HttpRequest, template_name: str) -> HttpResponse:
return TemplateResponse(request, template_name) return TemplateResponse(request, template_name)
@ -108,21 +103,3 @@ def landing_view(request: HttpRequest, template_name: str) -> HttpResponse:
@add_google_analytics @add_google_analytics
def hello_view(request: HttpRequest) -> HttpResponse: def hello_view(request: HttpRequest) -> HttpResponse:
return TemplateResponse(request, "zerver/hello.html", latest_info_context()) 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)},
)

View File

@ -177,8 +177,7 @@ TORNADO_PORTS: List[int] = []
USING_TORNADO = True USING_TORNADO = True
# ToS/Privacy templates # ToS/Privacy templates
PRIVACY_POLICY: Optional[str] = None POLICIES_DIRECTORY: str = "zerver/policies_absent"
TERMS_OF_SERVICE: Optional[str] = None
# Security # Security
ENABLE_FILE_LINKS = False ENABLE_FILE_LINKS = False

View File

@ -77,8 +77,9 @@ OPEN_REALM_CREATION = True
WEB_PUBLIC_STREAMS_ENABLED = True WEB_PUBLIC_STREAMS_ENABLED = True
INVITES_MIN_USER_AGE_DAYS = 0 INVITES_MIN_USER_AGE_DAYS = 0
TERMS_OF_SERVICE = "corporate/terms.md" # For development convenience, configure the ToS/Privacy Policies
PRIVACY_POLICY = "corporate/privacy.md" POLICIES_DIRECTORY = "corporate/policies"
TERMS_OF_SERVICE_VERSION = "1.0"
EMBEDDED_BOTS_ENABLED = True EMBEDDED_BOTS_ENABLED = True
@ -166,9 +167,6 @@ SEARCH_PILLS_ENABLED = bool(os.getenv("SEARCH_PILLS_ENABLED", False))
BILLING_ENABLED = True BILLING_ENABLED = True
LANDING_PAGE_NAVBAR_MESSAGE: Optional[str] = None 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 # 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 # 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 # server's own port); this setting tells Django to read that HTTP

View File

@ -760,9 +760,11 @@ CAMO_URI = "/external_content/"
## together into one bucket when applying rate-limiting. ## together into one bucket when applying rate-limiting.
# RATE_LIMIT_TOR_TOGETHER = False # RATE_LIMIT_TOR_TOGETHER = False
## If you want to set a Terms of Service for your server, set the path ## Configuration for Terms of Service and Privacy Policy for the
## to your Markdown file, and uncomment the following line. ## server. If unset, Zulip will never prompt users to accept Terms of
# TERMS_OF_SERVICE = '/etc/zulip/terms.md' ## 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. ## Directory containing Markdown files for the server's policies.
# PRIVACY_POLICY = '/etc/zulip/privacy.md' # POLICIES_DIRECTORY = "/etc/zulip/policies/"

View File

@ -83,9 +83,7 @@ from zerver.views.portico import (
hello_view, hello_view,
landing_view, landing_view,
plans_view, plans_view,
privacy_view,
team_view, team_view,
terms_view,
) )
from zerver.views.presence import ( from zerver.views.presence import (
get_presence_backend, get_presence_backend,
@ -607,6 +605,9 @@ i18n_urls = [
path("apps/", apps_view), path("apps/", apps_view),
path("apps/download/<platform>", app_download_link_redirect), path("apps/download/<platform>", app_download_link_redirect),
path("apps/<platform>", apps_view), path("apps/<platform>", apps_view),
path(
"developer-community/", RedirectView.as_view(url="/development-community/", permanent=True)
),
path( path(
"development-community/", "development-community/",
landing_view, landing_view,
@ -641,9 +642,6 @@ i18n_urls = [
RedirectView.as_view(url="/for/communities/", permanent=True), RedirectView.as_view(url="/for/communities/", permanent=True),
), ),
path("security/", landing_view, {"template_name": "zerver/security.html"}), 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 # 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( api_documentation_view = MarkdownDirectoryView.as_view(
template_name="zerver/documentation_main.html", path_template="/zerver/api/%s.md" 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 += [ urls += [
# Redirects due to us having moved the docs: # Redirects due to us having moved the docs:
path( path(
@ -883,6 +885,16 @@ urls += [
path("help/<path:article>", help_documentation_view), path("help/<path:article>", help_documentation_view),
path("api/", api_documentation_view), path("api/", api_documentation_view),
path("api/<slug:article>", 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 # Two-factor URLs