Skip to content

Commit c0ddcdb

Browse files
committed
feat(legal): easier customization
- customization of the legal menu - clarified when LEGAL_URL / PRIVACY_URL are applied - support for customizing legal CSS - the legal app can now utilize external documents - clarified the customization documentation
1 parent 19f3a59 commit c0ddcdb

20 files changed

Lines changed: 454 additions & 68 deletions

docs/admin/config.rst

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,6 +1324,64 @@ Defaults to -1.
13241324
* :setting:`IP_BEHIND_REVERSE_PROXY`
13251325
* :setting:`IP_PROXY_HEADER`
13261326

1327+
.. setting:: LEGAL_DOCUMENT_CSS_CLASS
1328+
1329+
LEGAL_DOCUMENT_CSS_CLASS
1330+
------------------------
1331+
1332+
.. versionadded:: 2026.7
1333+
1334+
CSS class added to the wrappers around legal document templates.
1335+
1336+
Defaults to ``"tos"``, which enables the built-in legal document numbering and
1337+
spacing rules. Set this to an empty string to render legal documents without
1338+
the built-in numbering.
1339+
1340+
.. code-block:: python
1341+
1342+
LEGAL_DOCUMENT_CSS_CLASS = ""
1343+
1344+
.. seealso::
1345+
1346+
:ref:`legal`
1347+
1348+
.. setting:: LEGAL_HIDDEN_DOCUMENTS
1349+
1350+
LEGAL_HIDDEN_DOCUMENTS
1351+
----------------------
1352+
1353+
.. versionadded:: 2026.7
1354+
1355+
List of legal document page identifiers to hide from the legal module.
1356+
1357+
The ``index`` page is always visible. Supported document identifiers are
1358+
``terms``, ``cookies``, ``privacy``, and ``contracts``.
1359+
1360+
Hidden pages are removed from the legal menu and return a 404 response when
1361+
requested directly. Hiding ``terms`` or ``privacy`` is not recommended when
1362+
terms of service confirmation is enabled.
1363+
1364+
When ``terms`` or ``privacy`` is hidden, links exposed through the
1365+
``terms_url`` and ``privacy_url`` template variables use :setting:`LEGAL_URL`
1366+
and :setting:`PRIVACY_URL` as fallbacks when configured. If no fallback URL is
1367+
configured, the related link is omitted.
1368+
1369+
With terms of service confirmation enabled, hiding ``terms`` and setting
1370+
:setting:`LEGAL_URL` makes the confirmation page link to the external terms
1371+
document instead of embedding :file:`legal/documents/tos.html`.
1372+
1373+
In non-Docker deployments, define :setting:`LEGAL_HIDDEN_DOCUMENTS` and
1374+
:setting:`LEGAL_URL` before ``SPECTACULAR_SETTINGS`` is created so the API
1375+
schema terms link uses the same fallback.
1376+
1377+
.. code-block:: python
1378+
1379+
LEGAL_HIDDEN_DOCUMENTS = ("contracts",)
1380+
1381+
.. seealso::
1382+
1383+
:ref:`legal`
1384+
13271385
.. setting:: LEGAL_TOS_DATE
13281386

13291387
LEGAL_TOS_DATE
@@ -1353,8 +1411,9 @@ URL where your Weblate instance shows its legal documents.
13531411

13541412
.. hint::
13551413

1356-
Useful if you host your legal documents outside Weblate for embedding them inside Weblate.
1357-
Please check :ref:`legal` for details.
1414+
Useful if you host your legal documents outside Weblate instead of using
1415+
the :ref:`legal` module. When the legal module is enabled, Weblate links to
1416+
the internal legal pages by default.
13581417

13591418
Example:
13601419

@@ -1595,8 +1654,9 @@ URL where your Weblate instance shows its privacy policy.
15951654

15961655
.. hint::
15971656

1598-
Useful if you host your legal documents outside Weblate for embedding them inside Weblate,
1599-
please check :ref:`legal` for details.
1657+
Useful if you host your privacy policy outside Weblate instead of using
1658+
the :ref:`legal` module. When the legal module is enabled, Weblate links to
1659+
the internal legal pages by default.
16001660

16011661
Example:
16021662

docs/admin/install/docker.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,10 @@ Generic settings
908908
:file:`/app/data/python/customize/templates/legal/documents`, see
909909
:ref:`docker-static-override`.
910910

911+
Recreate the Docker container after changing this environment variable,
912+
for example using :program:`docker compose up -d`. Restarting an existing
913+
container does not apply changed environment values.
914+
911915
**Example:**
912916

913917
.. code-block:: yaml
@@ -920,6 +924,35 @@ Generic settings
920924
* :ref:`legal`
921925
* :ref:`docker-static-override`
922926

927+
.. envvar:: WEBLATE_LEGAL_DOCUMENT_CSS_CLASS
928+
929+
Configures :setting:`LEGAL_DOCUMENT_CSS_CLASS` in Docker deployments with
930+
:envvar:`WEBLATE_LEGAL_INTEGRATION` enabled.
931+
932+
Set this to an empty string to disable the built-in legal document
933+
numbering.
934+
935+
**Example:**
936+
937+
.. code-block:: yaml
938+
939+
environment:
940+
WEBLATE_LEGAL_DOCUMENT_CSS_CLASS: ""
941+
942+
.. envvar:: WEBLATE_LEGAL_HIDDEN_DOCUMENTS
943+
944+
Configures :setting:`LEGAL_HIDDEN_DOCUMENTS` in Docker deployments with
945+
:envvar:`WEBLATE_LEGAL_INTEGRATION` enabled.
946+
947+
Provide a comma-separated list of legal document page identifiers.
948+
949+
**Example:**
950+
951+
.. code-block:: yaml
952+
953+
environment:
954+
WEBLATE_LEGAL_HIDDEN_DOCUMENTS: contracts
955+
923956
.. envvar:: WEBLATE_PUBLIC_ENGAGE
924957

925958
Enables :setting:`PUBLIC_ENGAGE`.

docs/admin/optionals.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ following templates in the documents:
138138
Privacy policy document
139139
:file:`legal/documents/summary.html`
140140
Short overview of the terms of service and privacy policy
141+
:file:`legal/documents/contracts.html`
142+
Subcontractor information
143+
144+
The legal module embeds these templates inside Weblate and uses
145+
:file:`legal/documents/tos.html` for terms of service confirmation. This is
146+
separate from :setting:`LEGAL_URL` and :setting:`PRIVACY_URL`, which are meant
147+
for linking to externally hosted legal documents from the footer when the
148+
legal module is not enabled. When the legal module is enabled, Weblate links to
149+
the internal legal pages by default.
141150

142151
On changing the terms of service documents, please adjust
143152
:setting:`LEGAL_TOS_DATE` so that users are forced to agree with the updated
@@ -193,11 +202,41 @@ Installation
193202
:file:`/app/data/python/customize/templates/legal/documents`, see
194203
:ref:`docker-static-override`.
195204

205+
Recreate the Docker container after changing environment variables, for
206+
example using :program:`docker compose up -d`. Restarting an existing
207+
container does not apply changed environment values.
208+
196209
Usage
197210
+++++
198211

199212
After installation and editing, the legal documents are shown in the Weblate UI.
200213

214+
The legal document templates are regular Django templates. Text is translated
215+
only when you use Django translation tags such as ``{% translate %}`` or
216+
``{% blocktranslate %}``; plain HTML text is shown as written.
217+
218+
Legal pages and the sign-in and registration overview provide ``terms_url`` and
219+
``privacy_url`` variables for linking to the terms of service and privacy
220+
policy documents.
221+
222+
By default, legal document wrappers use the ``tos`` CSS class. This class
223+
automatically numbers ``h2`` headings, paragraphs with ``item``, ``subitem``,
224+
or ``subsubitem`` classes, and top-level ordered list items. If your legal
225+
text already contains numbering, set :setting:`LEGAL_DOCUMENT_CSS_CLASS` to an
226+
empty string to disable this styling.
227+
228+
Use :setting:`LEGAL_HIDDEN_DOCUMENTS` to hide optional legal pages such as
229+
subcontractors from the legal menu. Hidden pages return a 404 response when
230+
requested directly. If ``terms`` or ``privacy`` is hidden, links using
231+
``terms_url`` or ``privacy_url`` fall back to :setting:`LEGAL_URL` or
232+
:setting:`PRIVACY_URL` when configured, otherwise the link is omitted.
233+
234+
To use externally hosted legal documents with terms confirmation, configure
235+
:setting:`LEGAL_HIDDEN_DOCUMENTS` to hide ``terms`` and ``privacy`` and set
236+
:setting:`LEGAL_URL` and :setting:`PRIVACY_URL`. The confirmation page then
237+
links to those external documents without requiring a
238+
:file:`legal/documents/tos.html` template override.
239+
201240
.. _avatars:
202241

203242
Avatars

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Weblate 2026.7
99

1010
* Management interface access control is now more fine-grained with dedicated site-wide permissions.
1111
* Default commit and merge request message templates now use Conventional Commits, and settings forms can restore installation defaults for individual message templates.
12+
* Documented :ref:`legal` customizations and added options to hide legal pages or disable document numbering.
1213

1314
.. rubric:: Bug fixes
1415

weblate/api/spectacular.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55

66
from __future__ import annotations
77

8-
from typing import Any
8+
from typing import TYPE_CHECKING, Any
99

1010
from django.utils.functional import lazy
1111
from django.utils.translation import gettext_lazy
1212

13+
if TYPE_CHECKING:
14+
from collections.abc import Sequence
15+
1316

1417
def get_doc_url_wrapper(page: str, anchor: str = "") -> str:
1518
"""
@@ -23,8 +26,27 @@ def get_doc_url_wrapper(page: str, anchor: str = "") -> str:
2326
return get_doc_url(page, anchor, doc_version="latest")
2427

2528

29+
def get_legal_terms_url(
30+
legal_hidden_documents: Sequence[str] | str = (), legal_url: str | None = None
31+
) -> str | None:
32+
if isinstance(legal_hidden_documents, str):
33+
hidden_documents = legal_hidden_documents.split(",")
34+
else:
35+
hidden_documents = legal_hidden_documents
36+
37+
for document in hidden_documents:
38+
if document.strip() == "terms":
39+
return legal_url
40+
return "/legal/terms/"
41+
42+
2643
def get_spectacular_settings(
27-
installed_apps: list[str], site_url: str, site_title: str
44+
installed_apps: list[str],
45+
site_url: str,
46+
site_title: str,
47+
*,
48+
legal_hidden_documents: Sequence[str] | str = (),
49+
legal_url: str | None = None,
2850
) -> dict[str, Any]:
2951
settings = {
3052
# Use redoc from sidecar
@@ -202,7 +224,9 @@ def get_spectacular_settings(
202224
"WEBHOOKS": ["weblate.addons.webhooks.change_event_webhook"],
203225
}
204226
if "weblate.legal" in installed_apps:
205-
settings["TOS"] = "/legal/terms/"
227+
terms_url = get_legal_terms_url(legal_hidden_documents, legal_url)
228+
if terms_url:
229+
settings["TOS"] = terms_url
206230

207231
return settings
208232

weblate/legal/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
class WeblateLegalConf(AppConf):
2929
# Current TOS date
3030
LEGAL_TOS_DATE = DEFAULT_TOS_DATE
31+
# CSS class for styling legal document templates
32+
LEGAL_DOCUMENT_CSS_CLASS = "tos"
33+
# Legal documents to hide from the UI
34+
LEGAL_HIDDEN_DOCUMENTS = ()
3135

3236
class Meta:
3337
prefix = ""

weblate/legal/templates/legal/confirm.html

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,24 @@
1616
<h4 class="card-title">{% translate "You have to agree to the General Terms and Conditions" %}</h4>
1717
</div>
1818
<div class="card-body">
19-
<p>{% translate "Please read following General Terms and Conditions document:" %}</p>
20-
<div class="list-group-item pre-scrollable tos">{% include "legal/documents/tos.html" %}</div>
19+
{% if terms_document_hidden and terms_url %}
20+
<p>{% translate "Please read the following legal documents:" %}</p>
21+
<ul>
22+
<li>
23+
<a href="{{ terms_url }}">{% translate "General Terms and Conditions" %}</a>
24+
</li>
25+
{% if privacy_url %}
26+
<li>
27+
<a href="{{ privacy_url }}">{% translate "Privacy Policy" %}</a>
28+
</li>
29+
{% endif %}
30+
</ul>
31+
{% else %}
32+
<p>{% translate "Please read the following General Terms and Conditions document:" %}</p>
33+
<div class="list-group-item pre-scrollable{% if legal_document_css_class %} {{ legal_document_css_class }}{% endif %}">
34+
{% include "legal/documents/tos.html" %}
35+
</div>
36+
{% endif %}
2137
{{ form|crispy }}
2238
{% csrf_token %}
2339
</div>

weblate/legal/templates/legal/contracts.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
<div class="card-header">
2020
<h4 class="card-title">{% translate "Subcontractors" %}</h4>
2121
</div>
22-
<div class="card-body tos">{% include "legal/documents/contracts.html" %}</div>
22+
<div class="card-body{% if legal_document_css_class %} {{ legal_document_css_class }}{% endif %}">
23+
{% include "legal/documents/contracts.html" %}
24+
</div>
2325
</div>
2426

2527
{% endblock content %}

weblate/legal/templates/legal/cookies.html

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,24 @@ <h4 class="card-title">{% translate "Cookies Policy" %}</h4>
2121
</div>
2222
<div class="card-body">
2323

24-
<p>
25-
{% blocktranslate %}This page is based on the General Terms and Conditions and the Privacy Policy, you should still read the original documents to fully understand them:{% endblocktranslate %}
26-
</p>
27-
28-
<ul>
29-
<li>
30-
<a href="{% url 'legal:terms' %}">{% translate "General Terms and Conditions" %}</a>
31-
</li>
32-
<li>
33-
<a href="{% url 'legal:privacy' %}">{% translate "Privacy Policy" %}</a>
34-
</li>
35-
</ul>
24+
{% if terms_url or privacy_url %}
25+
<p>
26+
{% blocktranslate %}This page is based on the General Terms and Conditions and the Privacy Policy, you should still read the original documents to fully understand them:{% endblocktranslate %}
27+
</p>
28+
29+
<ul>
30+
{% if terms_url %}
31+
<li>
32+
<a href="{{ terms_url }}">{% translate "General Terms and Conditions" %}</a>
33+
</li>
34+
{% endif %}
35+
{% if privacy_url %}
36+
<li>
37+
<a href="{{ privacy_url }}">{% translate "Privacy Policy" %}</a>
38+
</li>
39+
{% endif %}
40+
</ul>
41+
{% endif %}
3642

3743
<p>
3844
{% blocktranslate %}Cookies are used on this site for the following:{% endblocktranslate %}

weblate/legal/templates/legal/index.html

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,24 @@ <h4 class="card-title">{% translate "Legal Terms Overview" %}</h4>
2020

2121
{% include 'legal/documents/summary.html' %}
2222

23-
<p>
24-
{% blocktranslate %}This page is based on the General Terms and Conditions and the Privacy Policy, you should still read the original documents to fully understand them:{% endblocktranslate %}
25-
</p>
26-
27-
<ul>
28-
<li>
29-
<a href="{% url 'legal:terms' %}">{% translate "General Terms and Conditions" %}</a>
30-
</li>
31-
<li>
32-
<a href="{% url 'legal:privacy' %}">{% translate "Privacy Policy" %}</a>
33-
</li>
34-
</ul>
23+
{% if terms_url or privacy_url %}
24+
<p>
25+
{% blocktranslate %}This page is based on the General Terms and Conditions and the Privacy Policy, you should still read the original documents to fully understand them:{% endblocktranslate %}
26+
</p>
27+
28+
<ul>
29+
{% if terms_url %}
30+
<li>
31+
<a href="{{ terms_url }}">{% translate "General Terms and Conditions" %}</a>
32+
</li>
33+
{% endif %}
34+
{% if privacy_url %}
35+
<li>
36+
<a href="{{ privacy_url }}">{% translate "Privacy Policy" %}</a>
37+
</li>
38+
{% endif %}
39+
</ul>
40+
{% endif %}
3541

3642
</div>
3743
</div>

0 commit comments

Comments
 (0)