Add fediverse_url filter, integrate unified followus URL generation, update Listmonk config, adjust menu categories, and include full Python unittests. Details: https://chatgpt.com/share/69298521-dfc0-800f-9177-fefc7d32fec7

This commit is contained in:
2025-11-28 12:19:12 +01:00
parent 4337b63c2f
commit 3912e9b217
8 changed files with 179 additions and 27 deletions

View File

@@ -26,6 +26,7 @@ defaults_service_provider:
pixelfed: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-pixelfed') if 'web-app-pixelfed' in group_names else '' }}"
phone: "+0 000 000 404"
wordpress: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-wordpress') if 'web-app-wordpress' in group_names else '' }}"
newsletter: "{{ [ domains | get_url('web-app-listmonk', WEB_PROTOCOL), '/subscription/form' ] | url_join if 'web-app-listmonk' in group_names else '' }}"
legal:
editorial_responsible: "Johannes Gutenberg"

View File

@@ -0,0 +1,41 @@
# roles/common/filter_plugins/social.py
from ansible.errors import AnsibleFilterError
def fediverse_url(handle, protocol="https", path_prefix="@"):
"""
Convert a Fediverse handle into a full profile URL.
Examples:
'@user@instance.tld' -> 'https://instance.tld/@user'
'user@instance.tld' -> 'https://instance.tld/@user'
"""
if not handle:
return ""
value = str(handle).strip()
# Optional leading '@'
if value.startswith("@"):
value = value[1:]
parts = value.split("@")
if len(parts) != 2:
raise AnsibleFilterError(f"Invalid Fediverse handle '{handle}'")
username, host = parts
username = username.strip()
host = host.strip()
if not username or not host:
raise AnsibleFilterError(f"Invalid Fediverse handle '{handle}'")
# Allow configurable path prefix, default "@"
return f"{protocol}://{host}/{path_prefix}{username}"
class FilterModule(object):
def filters(self):
return {
"fediverse_url": fediverse_url,
}

View File

@@ -1,70 +1,87 @@
followus:
name: Follow Us
description: Follow us to stay up to receive the newest {{ SOFTWARE_NAME }} updates
description: Follow us to stay up to date with the latest {{ SOFTWARE_NAME }} updates.
icon:
class: fas fa-newspaper
{% if ["web-app-mastodon", "web-app-bluesky"] | any_in(group_names) %}
{% if [
'web-app-mastodon',
'web-app-bluesky',
'web-app-pixelfed',
'web-app-peertube',
'web-app-wordpress',
'web-app-friendica',
'web-app-listmonk'
] | any_in(group_names) %}
children:
{% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %}
- name: Mastodon
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} updates on Mastodon.
icon:
class: fa-brands fa-mastodon
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.mastodon.split('@')[2] }}/@{{ service_provider.contact.mastodon.split('@')[1] }}"
url: "{{ service_provider.contact.mastodon | fediverse_url(WEB_PROTOCOL) }}"
identifier: "{{ service_provider.contact.mastodon }}"
iframe: {{ applications | get_app_conf('web-app-mastodon','features.desktop') }}
{% endif %}
{% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %}
- name: Bluesky
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} on Bluesky.
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} updates on Bluesky.
icon:
class: fa-brands fa-bluesky
alternatives:
- link: followus.microblogs.mastodon
identifier: "{{ service_provider.contact.bluesky }}"
{% endif %}
{% endif %}
{% if service_provider.contact.pixelfed is defined and service_provider.contact.pixelfed != "" %}
- name: Pixelfed
description: Explore {{ 'our' if service_provider.type == 'legal' else 'my' }} photo gallery on Pixelfed.
icon:
class: fa-solid fa-camera
identifier: "{{ service_provider.contact.pixelfed }}"
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.pixelfed.split('@')[2] }}/@{{ service_provider.contact.pixelfed.split('@')[1] }}"
url: "{{ service_provider.contact.pixelfed | fediverse_url(WEB_PROTOCOL) }}"
iframe: {{ applications | get_app_conf('web-app-pixelfed','features.desktop') }}
{% endif %}
{% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %}
- name: Peertube
description: Discover {{ 'our' if service_provider.type == 'legal' else 'my' }} videos on Peertube.
icon:
class: fa-solid fa-video
identifier: "{{ service_provider.contact.peertube }}"
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.peertube.split('@')[2] }}/@{{ service_provider.contact.peertube.split('@')[1] }}"
url: "{{ service_provider.contact.peertube | fediverse_url(WEB_PROTOCOL) }}"
iframe: {{ applications | get_app_conf('web-app-peertube','features.desktop') }}
{% endif %}
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
- name: WordPress
description: Read {{ 'our' if service_provider.type == 'legal' else 'my' }} articles and stories.
icon:
class: fa-solid fa-blog
identifier: "{{ service_provider.contact.wordpress }}"
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.wordpress.split('@')[2] }}/@{{ service_provider.contact.wordpress.split('@')[1] }}"
url: "{{ service_provider.contact.wordpress | fediverse_url(WEB_PROTOCOL) }}"
iframe: {{ applications | get_app_conf('web-app-wordpress','features.desktop') }}
{% endif %}
{% if service_provider.contact.friendica is defined and service_provider.contact.friendica != "" %}
- name: Friendica
description: Visit {{ 'our' if service_provider.type == 'legal' else 'my' }} friendica profile
description: Visit {{ 'our' if service_provider.type == 'legal' else 'my' }} Friendica profile.
icon:
class: fa-solid fa-network-wired
identifier: "{{ service_provider.contact.friendica }}"
url: "{{ WEB_PROTOCOL }}://{{ service_provider.contact.friendica.split('@')[2] }}/@{{ service_provider.contact.friendica.split('@')[1] }}"
url: "{{ service_provider.contact.friendica | fediverse_url(WEB_PROTOCOL) }}"
iframe: {{ applications | get_app_conf('web-app-friendica','features.desktop') }}
{% endif %}
{% if 'web-app-listmonk' in group_names %}
{% if service_provider.contact.newsletter is defined
and service_provider.contact.newsletter != ""
and 'web-app-listmonk' in group_names %}
- name: Newsletter
description: Subscribe {{ 'our' if service_provider.type == 'legal' else 'my' }} our newsletter
description: Subscribe to {{ 'our' if service_provider.type == 'legal' else 'my' }} newsletter.
icon:
class: fa-solid fa-envelope
url: "{{ domains | get_url("web-app-listmonk", WEB_PROTOCOL) }}"
url: "{{ service_provider.contact.newsletter }}"
iframe: {{ applications | get_app_conf('web-app-listmonk','features.desktop') }}
{% endif %}
{% endif %}

View File

@@ -36,6 +36,7 @@ portfolio_menu_categories:
- presentation
- desktop
- portfolio
- mig
Marketing:
description: "Tools for email campaigns, audience engagement, automation, and digital marketing workflows."
@@ -154,7 +155,6 @@ portfolio_menu_categories:
- phpldapadmin
- phpmyadmin
- postgres
- mig
Sales:
description: "Applications for e-commerce, ticketing, and sales management."

View File

@@ -1,7 +1,7 @@
public_api_activated: False # Security hole. Can be used for spaming # Docker Image version
features:
matomo: true
css: false
css: true
desktop: true
central_database: true
oidc: true

View File

@@ -0,0 +1,93 @@
import unittest
import pathlib
import importlib.util
from ansible.errors import AnsibleFilterError
def _load_social_module():
"""
Load the social.py filter plugin module from the roles/web-app-desktop path.
This helper allows the test to be executed from the repository root
without requiring the roles directory to be a Python package.
"""
# Resolve repository root based on this test file location:
# tests/unit/roles/web-app-desktop/filter_plugins/test_social.py
test_file = pathlib.Path(__file__).resolve()
repo_root = test_file.parents[5]
social_path = repo_root / "roles" / "web-app-desktop" / "filter_plugins" / "social.py"
if not social_path.is_file():
raise RuntimeError(f"Could not find social.py at expected path: {social_path}")
spec = importlib.util.spec_from_file_location("social", social_path)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
return module
_social = _load_social_module()
fediverse_url = _social.fediverse_url
class TestFediverseUrlFilter(unittest.TestCase):
"""Unit tests for the fediverse_url filter function."""
def test_valid_handle_with_leading_at_default_protocol(self):
"""Handles '@user@instance.tld' correctly using default https protocol."""
handle = "@alice@example.social"
result = fediverse_url(handle)
self.assertEqual(result, "https://example.social/@alice")
def test_valid_handle_without_leading_at_custom_protocol(self):
"""Handles 'user@instance.tld' correctly when a custom protocol is provided."""
handle = "bob@example.com"
result = fediverse_url(handle, protocol="http")
self.assertEqual(result, "http://example.com/@bob")
def test_handles_whitespace_and_trims_input(self):
"""Strips surrounding whitespace from the handle before processing."""
handle = " @charlie@example.net "
result = fediverse_url(handle)
self.assertEqual(result, "https://example.net/@charlie")
def test_empty_string_returns_empty_string(self):
"""Returns an empty string if the handle is an empty string."""
self.assertEqual(fediverse_url(""), "")
def test_none_returns_empty_string(self):
"""Returns an empty string if the handle is None."""
self.assertEqual(fediverse_url(None), "")
def test_invalid_handle_without_at_raises_error(self):
"""Raises AnsibleFilterError when there is no separator '@'."""
with self.assertRaises(AnsibleFilterError):
fediverse_url("not-a-valid-handle")
def test_invalid_handle_with_three_parts_raises_error(self):
"""Raises AnsibleFilterError when the handle contains more than one '@' separator."""
with self.assertRaises(AnsibleFilterError):
fediverse_url("too@many@parts.example")
def test_invalid_handle_with_empty_username_raises_error(self):
"""Raises AnsibleFilterError when the username part is missing."""
with self.assertRaises(AnsibleFilterError):
fediverse_url("@@example.org")
def test_invalid_handle_with_empty_host_raises_error(self):
"""Raises AnsibleFilterError when the host part is missing."""
with self.assertRaises(AnsibleFilterError):
fediverse_url("@user@")
def test_custom_path_prefix_is_respected(self):
"""Respects a custom path prefix instead of the default '@'."""
handle = "@dana@example.host"
result = fediverse_url(handle, protocol="https", path_prefix="u/")
self.assertEqual(result, "https://example.host/u/dana")
if __name__ == "__main__":
unittest.main()