mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-12-02 15:39:57 +00:00
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:
@@ -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 '' }}"
|
pixelfed: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-pixelfed') if 'web-app-pixelfed' in group_names else '' }}"
|
||||||
phone: "+0 000 000 404"
|
phone: "+0 000 000 404"
|
||||||
wordpress: "{{ '@' ~ users.contact.username ~ '@' ~ domains | get_domain('web-app-wordpress') if 'web-app-wordpress' in group_names else '' }}"
|
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:
|
legal:
|
||||||
editorial_responsible: "Johannes Gutenberg"
|
editorial_responsible: "Johannes Gutenberg"
|
||||||
|
|||||||
41
roles/web-app-desktop/filter_plugins/social.py
Normal file
41
roles/web-app-desktop/filter_plugins/social.py
Normal 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,
|
||||||
|
}
|
||||||
@@ -1,70 +1,87 @@
|
|||||||
followus:
|
followus:
|
||||||
name: Follow Us
|
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:
|
icon:
|
||||||
class: fas fa-newspaper
|
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:
|
children:
|
||||||
{% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %}
|
{% if service_provider.contact.mastodon is defined and service_provider.contact.mastodon != "" %}
|
||||||
- name: Mastodon
|
- name: Mastodon
|
||||||
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} updates on Mastodon.
|
description: Follow {{ 'our' if service_provider.type == 'legal' else 'my' }} updates on Mastodon.
|
||||||
icon:
|
icon:
|
||||||
class: fa-brands fa-mastodon
|
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}}"
|
identifier: "{{ service_provider.contact.mastodon }}"
|
||||||
iframe: {{ applications | get_app_conf('web-app-mastodon','features.desktop') }}
|
iframe: {{ applications | get_app_conf('web-app-mastodon','features.desktop') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %}
|
{% if service_provider.contact.bluesky is defined and service_provider.contact.bluesky != "" %}
|
||||||
- name: 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:
|
icon:
|
||||||
class: fa-brands fa-bluesky
|
class: fa-brands fa-bluesky
|
||||||
alternatives:
|
alternatives:
|
||||||
- link: followus.microblogs.mastodon
|
- link: followus.microblogs.mastodon
|
||||||
identifier: "{{service_provider.contact.bluesky}}"
|
identifier: "{{ service_provider.contact.bluesky }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
{% if service_provider.contact.pixelfed is defined and service_provider.contact.pixelfed != "" %}
|
{% if service_provider.contact.pixelfed is defined and service_provider.contact.pixelfed != "" %}
|
||||||
- name: Pixelfed
|
- name: Pixelfed
|
||||||
description: Explore {{ 'our' if service_provider.type == 'legal' else 'my' }} photo gallery on Pixelfed.
|
description: Explore {{ 'our' if service_provider.type == 'legal' else 'my' }} photo gallery on Pixelfed.
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-camera
|
class: fa-solid fa-camera
|
||||||
identifier: "{{service_provider.contact.pixelfed}}"
|
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') }}
|
iframe: {{ applications | get_app_conf('web-app-pixelfed','features.desktop') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %}
|
|
||||||
|
{% if service_provider.contact.peertube is defined and service_provider.contact.peertube != "" %}
|
||||||
- name: Peertube
|
- name: Peertube
|
||||||
description: Discover {{ 'our' if service_provider.type == 'legal' else 'my' }} videos on Peertube.
|
description: Discover {{ 'our' if service_provider.type == 'legal' else 'my' }} videos on Peertube.
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-video
|
class: fa-solid fa-video
|
||||||
identifier: "{{service_provider.contact.peertube}}"
|
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') }}
|
iframe: {{ applications | get_app_conf('web-app-peertube','features.desktop') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
|
{% if service_provider.contact.wordpress is defined and service_provider.contact.wordpress != "" %}
|
||||||
- name: WordPress
|
- name: WordPress
|
||||||
description: Read {{ 'our' if service_provider.type == 'legal' else 'my' }} articles and stories.
|
description: Read {{ 'our' if service_provider.type == 'legal' else 'my' }} articles and stories.
|
||||||
icon:
|
icon:
|
||||||
class: fa-solid fa-blog
|
class: fa-solid fa-blog
|
||||||
identifier: "{{service_provider.contact.wordpress}}"
|
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') }}
|
iframe: {{ applications | get_app_conf('web-app-wordpress','features.desktop') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if service_provider.contact.friendica is defined and service_provider.contact.friendica != "" %}
|
{% if service_provider.contact.friendica is defined and service_provider.contact.friendica != "" %}
|
||||||
- name: 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:
|
icon:
|
||||||
class: fa-solid fa-network-wired
|
class: fa-solid fa-network-wired
|
||||||
identifier: "{{service_provider.contact.friendica}}"
|
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') }}
|
iframe: {{ applications | get_app_conf('web-app-friendica','features.desktop') }}
|
||||||
{% endif %}
|
{% 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
|
- 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:
|
icon:
|
||||||
class: fa-solid fa-envelope
|
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') }}
|
iframe: {{ applications | get_app_conf('web-app-listmonk','features.desktop') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ portfolio_menu_categories:
|
|||||||
- presentation
|
- presentation
|
||||||
- desktop
|
- desktop
|
||||||
- portfolio
|
- portfolio
|
||||||
|
- mig
|
||||||
|
|
||||||
Marketing:
|
Marketing:
|
||||||
description: "Tools for email campaigns, audience engagement, automation, and digital marketing workflows."
|
description: "Tools for email campaigns, audience engagement, automation, and digital marketing workflows."
|
||||||
@@ -154,7 +155,6 @@ portfolio_menu_categories:
|
|||||||
- phpldapadmin
|
- phpldapadmin
|
||||||
- phpmyadmin
|
- phpmyadmin
|
||||||
- postgres
|
- postgres
|
||||||
- mig
|
|
||||||
|
|
||||||
Sales:
|
Sales:
|
||||||
description: "Applications for e-commerce, ticketing, and sales management."
|
description: "Applications for e-commerce, ticketing, and sales management."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
public_api_activated: False # Security hole. Can be used for spaming # Docker Image version
|
public_api_activated: False # Security hole. Can be used for spaming # Docker Image version
|
||||||
features:
|
features:
|
||||||
matomo: true
|
matomo: true
|
||||||
css: false
|
css: true
|
||||||
desktop: true
|
desktop: true
|
||||||
central_database: true
|
central_database: true
|
||||||
oidc: true
|
oidc: true
|
||||||
|
|||||||
0
tests/unit/roles/web-app-desktop/__init__.py
Normal file
0
tests/unit/roles/web-app-desktop/__init__.py
Normal 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()
|
||||||
Reference in New Issue
Block a user