Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ db.sqlite3
build
dist
.vscode
.idea
.idea/dataSources.xml
.idea/misc.xml
.coverage
Expand Down
8 changes: 8 additions & 0 deletions example-configs/lunes-cms.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ ALLOWED_HOSTS =
lunes.tuerantuer.org
vokabeltrainer.tuerantuer.org

[legal]
# URL of the privacy policy, linked in the admin and on the public upload page
# [optional, defaults to an empty string which hides the link]
PRIVACY_POLICY_URL = https://lunes.app/datenschutz
# URL of the imprint, linked in the same places as the privacy policy
# [optional, defaults to an empty string which hides the link]
IMPRINT_URL = https://lunes.app/impressum/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODOs:

  • Privacy policy for CMS
  • Configure on test and production environment


[static-files]
# The directory for static files [required]
STATIC_ROOT = /var/www/lunes-cms/static
Expand Down
1 change: 1 addition & 0 deletions lunes_cms/cms/templates/admin/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<a href="https://www.tuerantuer.org/digitalfabrik" target="_blank">
<strong>Tür an Tür Digitalfabrik gGmbH</strong>
</a>
{% include "admin/includes/legal_links.html" %}
<span class="float-right">v{{ jazzmin_settings.site_version }}</span>
</footer>
{% endif %}
Expand Down
14 changes: 14 additions & 0 deletions lunes_cms/cms/templates/admin/includes/legal_links.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% load i18n %}
{% if privacy_policy_url or imprint_url %}
<span class="legal-links">
{% if privacy_policy_url %}
<a href="{{ privacy_policy_url }}" target="_blank" rel="noopener noreferrer">{% translate "Privacy policy" %}</a>
{% endif %}
{% if privacy_policy_url and imprint_url %}
<span aria-hidden="true">&middot;</span>
{% endif %}
{% if imprint_url %}
<a href="{{ imprint_url }}" target="_blank" rel="noopener noreferrer">{% translate "Imprint" %}</a>
{% endif %}
</span>
{% endif %}
10 changes: 10 additions & 0 deletions lunes_cms/cms/templates/admin/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% extends "admin/login.html" %}

{# Appends the legal links below the login form. Jazzmin's login template only #}
{# defines the "content" block, so the form itself is reused via block.super. #}
{% block content %}
{{ block.super }}
<div class="text-center mt-3">
{% include "admin/includes/legal_links.html" %}
</div>
{% endblock %}
21 changes: 21 additions & 0 deletions lunes_cms/core/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from typing import Any

from django.conf import settings
from django.http import HttpRequest

from ..cms.feedback_filter import filter_feedback_by_creator
Expand All @@ -16,6 +17,26 @@
from ..cmsv2.models import Feedback as FeedbackV2


def legal_links_processor( # pylint: disable=unused-argument
request: HttpRequest,
) -> dict[str, Any]:
"""
This context processor injects the URLs of the privacy policy and the imprint into the
template context. They are read per request instead of being passed as static template
context, so that the login page (which is rendered for anonymous users) gets them, too.

:param request: The current http request
:type request: ~django.http.HttpRequest

:return: The template context containing the legal link URLs
:rtype: dict
"""
return {
"privacy_policy_url": settings.PRIVACY_POLICY_URL,
"imprint_url": settings.IMPRINT_URL,
}


def feedback_processor(request: HttpRequest) -> dict[str, Any]:
"""
This context processor injects the number of unread feedback entries into the template context.
Expand Down
13 changes: 12 additions & 1 deletion lunes_cms/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from django.utils.translation import gettext_lazy as _

from .logging_formatter import ColorFormatter
from .utils import strtobool
from .utils import legal_menu_links, strtobool

###################
# CUSTOM SETTINGS #
Expand All @@ -39,6 +39,14 @@
#: How many documents a training sets needs at least to get released
TRAININGSET_MIN_DOCS = int(os.environ.get("LUNES_CMS_TRAININGSET_MIN_DOCS", 4))

#: URL of the privacy policy which is linked in the admin (user menu, footer and
#: login page) and on the public upload page. If empty, no link is shown.
PRIVACY_POLICY_URL = os.environ.get("LUNES_CMS_PRIVACY_POLICY_URL", "")

#: URL of the imprint which is linked in the same places as the privacy policy
#: (see :attr:`~lunes_cms.core.settings.PRIVACY_POLICY_URL`). If empty, no link is shown.
IMPRINT_URL = os.environ.get("LUNES_CMS_IMPRINT_URL", "")

#: API Key for OpenAI
OPENAI_API_KEY = os.environ.get("LUNES_CMS_OPENAI_API_KEY")

Expand Down Expand Up @@ -195,6 +203,7 @@
"django.contrib.messages.context_processors.messages",
"lunes_cms.core.context_processors.feedback_processor",
"lunes_cms.core.context_processors.feedbackv2_processor",
"lunes_cms.core.context_processors.legal_links_processor",
],
},
},
Expand Down Expand Up @@ -614,6 +623,8 @@
"cmsv2.Feedback": "fas fa-comment",
},
"site_version": _cms_version,
# Legal links in the user menu. Only rendered if the respective URL is configured.
"usermenu_links": legal_menu_links(PRIVACY_POLICY_URL, IMPRINT_URL),
# Render the Analytics app section directly below Dashboard. Jazzmin's
# sidebar lists apps in this order; anything not mentioned trails after.
"order_with_respect_to": ["cmsv2", "analytics", "auth", "cms"],
Expand Down
26 changes: 26 additions & 0 deletions lunes_cms/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from django.contrib.staticfiles.storage import ManifestStaticFilesStorage
from django.core.files.base import File
from django.utils.translation import gettext_lazy as _


class LaxManifestStaticFilesStorage(ManifestStaticFilesStorage):
Expand All @@ -27,6 +28,31 @@ def hashed_name(
return name


def legal_menu_links(privacy_policy_url: str, imprint_url: str) -> list[dict[str, Any]]:
"""Build the Jazzmin user menu entries for the legal links.

Entries without a configured URL are left out, so an installation which
only configures one of the two links does not end up with a dead menu item.
Jazzmin passes absolute URLs through unchanged, only URL names without a
slash are reversed.
"""
links: list[dict[str, Any]] = [
{
"name": _("Privacy policy"),
"url": privacy_policy_url,
"new_window": True,
"icon": "fas fa-user-shield",
},
{
"name": _("Imprint"),
"url": imprint_url,
"new_window": True,
"icon": "fas fa-building",
},
]
return [link for link in links if link["url"]]


def strtobool(string: str) -> int:
"""Convert a string representation of truth to true (1) or false (0).

Expand Down
2 changes: 1 addition & 1 deletion lunes_cms/help/templates/public_upload.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ <h3 class="h5 mb-4 mt-4 font-weight-normal">
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Upload</button>
<p class="mt-5 mb-3 text-muted"></p>
<p class="mt-5 mb-3 text-muted">{% include "admin/includes/legal_links.html" %}</p>
</form>
</body>
</html>
8 changes: 8 additions & 0 deletions lunes_cms/locale/de/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,14 @@ msgid_plural "and %(remaining)s more objects will be deleted"
msgstr[0] "und %(remaining)s weiteres Objekt wird gelöscht"
msgstr[1] "und %(remaining)s weitere Objekte werden gelöscht"

#: cms/templates/admin/includes/legal_links.html core/utils.py
msgid "Privacy policy"
msgstr "Datenschutzerklärung"

#: cms/templates/admin/includes/legal_links.html core/utils.py
msgid "Imprint"
msgstr "Impressum"

#: cms/templates/admin/object_delete_summary.html
msgid "Summary"
msgstr "Zusammenfassung"
Expand Down
60 changes: 60 additions & 0 deletions tests/core/test_legal_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Tests for the privacy policy and imprint links.
"""

from __future__ import annotations

import pytest
from django.contrib.auth.models import User
from django.test import Client
from django.test.utils import override_settings
from django.urls import reverse

from lunes_cms.core.utils import legal_menu_links

PRIVACY_POLICY_URL = "https://lunes.app/datenschutz"
IMPRINT_URL = "https://www.tuerantuer.org/impressum"


@pytest.mark.django_db
@override_settings(PRIVACY_POLICY_URL=PRIVACY_POLICY_URL, IMPRINT_URL=IMPRINT_URL)
def test_login_page_shows_legal_links(client: Client) -> None:
"""Both links have to be reachable without being logged in."""
content = client.get(reverse("admin:login")).content.decode()
assert PRIVACY_POLICY_URL in content
assert IMPRINT_URL in content


@pytest.mark.django_db
@override_settings(PRIVACY_POLICY_URL="", IMPRINT_URL="")
def test_login_page_without_configured_urls_shows_no_legal_links(
client: Client,
) -> None:
"""Without configured URLs, no empty links may be rendered."""
content = client.get(reverse("admin:login")).content.decode()
assert "legal-links" not in content


@pytest.mark.django_db
@override_settings(PRIVACY_POLICY_URL=PRIVACY_POLICY_URL, IMPRINT_URL=IMPRINT_URL)
def test_admin_footer_shows_legal_links(client: Client) -> None:
"""The admin footer carries the links on every page of the admin."""
user = User.objects.create_superuser("legal-test", "legal@example.com", "password")
client.force_login(user)
content = client.get(reverse("admin:index")).content.decode()
assert PRIVACY_POLICY_URL in content
assert IMPRINT_URL in content


def test_legal_menu_links_skips_unconfigured_urls() -> None:
"""A link without a URL must not end up in the user menu."""
assert legal_menu_links("", "") == []
links = legal_menu_links(PRIVACY_POLICY_URL, "")
assert [link["url"] for link in links] == [PRIVACY_POLICY_URL]


def test_legal_menu_links_open_in_new_window() -> None:
"""Both links leave the admin, so they must not replace the current tab."""
links = legal_menu_links(PRIVACY_POLICY_URL, IMPRINT_URL)
assert [link["url"] for link in links] == [PRIVACY_POLICY_URL, IMPRINT_URL]
assert all(link["new_window"] for link in links)