mirror of https://github.com/zulip/zulip.git
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:
parent
95854d9d94
commit
ee77c6365a
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Terms and policies
|
||||||
|
|
||||||
|
* [Terms of Service](/policies/terms)
|
||||||
|
* [Privacy Policy](/policies/privacy)
|
||||||
|
* [Rules of Use](/policies/rules)
|
|
@ -0,0 +1 @@
|
||||||
|
No such page.
|
|
@ -0,0 +1,2 @@
|
||||||
|
## [Terms of Service](/policies/terms)
|
||||||
|
## [Privacy Policy](/policies/privacy)
|
|
@ -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 />
|
||||||
|
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
## No policies configured
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
|
|
@ -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 %}
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)},
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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/"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue