decidim: public-commons process text + isoc avatar hook

This commit is contained in:
Hernâni Marques 2026-08-17 15:55:23 +02:00
parent 03e5c0d3ad
commit 6a99f7d6bb
No known key found for this signature in database
GPG key ID: CB5738652768F7E9
3 changed files with 148 additions and 30 deletions

View file

@ -35,7 +35,9 @@ login-health: probes + containers + linger + these units enabled. Loopback HTTP
## Public example + isoc ## Public example + isoc
- Process **example** (published, not private): `https://decidim.hacktivism.ch/processes/example` - Process **example** (published, not private): `https://decidim.hacktivism.ch/processes/example`
- One published meeting in that process (readable without login). — title **Öffentliche digitale Allmende** (public-by-default reading).
- Published meeting **Offene Runde: Lesen ohne Konto** + one public proposal + background page.
- Homepage block `highlighted_processes` published. - Homepage block `highlighted_processes` published.
- Participant `isoc` on the host (`users.env`); password not in this repo. - Participant `isoc` on the host (`users.env`); password not in this repo.
- Avatar: gold ISOC globe (same crop as Bonfire `@isoc`); host file `~/koopa-decidim/assets/img/isoc-gold.png`, not in this repo.
- Seed: `scripts/decidim/seed-example.sh` / `configs/decidim/seed_example.rb`. - Seed: `scripts/decidim/seed-example.sh` / `configs/decidim/seed_example.rb`.

View file

@ -1,16 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
# Public example process + meeting (readable without an account). # Public process + meeting + page + proposal (readable without an account).
# Optional participant from env: DECIDIM_ISOC_EMAIL / DECIDIM_ISOC_PASSWORD. # Optional participant from env: DECIDIM_ISOC_EMAIL / DECIDIM_ISOC_PASSWORD.
# Optional avatar: DECIDIM_ISOC_AVATAR (PNG path inside the container).
# Usage: bundle exec rails runner /code/lib/koopa_seed_example.rb # Usage: bundle exec rails runner /code/lib/koopa_seed_example.rb
def t(en, de, fr) def t(en, de, fr)
{ "en" => en, "de" => de, "fr" => fr } { "en" => en, "de" => de, "fr" => fr }
end end
def html(en, de, fr)
t("<p>#{en}</p>", "<p>#{de}</p>", "<p>#{fr}</p>")
end
def fail_model!(record) def fail_model!(record)
raise "#{record.class}: #{record.errors.full_messages.join("; ")}" raise "#{record.class}: #{record.errors.full_messages.join("; ")}"
end end
@ -24,21 +21,40 @@ org.update!(force_users_to_authenticate_before_access_organization: false)
slug = "example" slug = "example"
process = Decidim::ParticipatoryProcess.find_or_initialize_by(organization: org, slug: slug) process = Decidim::ParticipatoryProcess.find_or_initialize_by(organization: org, slug: slug)
process.assign_attributes( process.assign_attributes(
title: t("Example activity", "Beispielaktivität", "Activité exemple"), title: t(
"Public digital commons",
"Öffentliche digitale Allmende",
"Communs numériques publics"
),
subtitle: t( subtitle: t(
"Public demo — no account needed to read", "Which self-hosted services stay readable without an account?",
"Öffentliches Beispiel — Lesen ohne Konto", "Welche selbst gehosteten Dienste bleiben ohne Konto lesbar?",
"Démo publique — lecture sans compte" "Quels services auto-hébergés restent lisibles sans compte ?"
), ),
short_description: html( short_description: t(
"A published participatory process so visitors can see Decidim without signing in.", "<p>hacktivism.ch already runs Lemmy, Bonfire, Castopod and Decidim. This process asks what must stay readable without a login — and what may require an account to write.</p>",
"Ein veröffentlichter Beteiligungsprozess, den Besucherinnen ohne Anmeldung lesen können.", "<p>hacktivism.ch betreibt bereits Lemmy, Bonfire, Castopod und Decidim. Dieser Prozess fragt, was ohne Anmeldung lesbar bleiben muss — und wofür ein Konto zum Schreiben reicht.</p>",
"Un processus participatif publié, lisible sans compte." "<p>hacktivism.ch fait déjà tourner Lemmy, Bonfire, Castopod et Decidim. Ce processus demande ce qui doit rester lisible sans compte — et ce qui peut exiger un compte pour écrire.</p>"
), ),
description: html( description: t(
"This is a small public example on hacktivism. Meetings and the process page are visible to anyone. Signing in is only needed to take part.", "<p>Reading civic material should not depend on creating an account. Writing (proposals, comments, votes) can.</p>" \
"Kleines öffentliches Beispiel auf hacktivism. Prozessseite und Treffen sind für alle sichtbar. Anmelden nur zum Mitmachen.", "<p>Concrete questions:</p><ul>" \
"Petit exemple public sur hacktivism. La page du processus et la rencontre sont visibles sans compte. La connexion sert uniquement à participer." "<li>Process pages, meetings and last activities stay public.</li>" \
"<li>No third-party trackers; cookies only what the software needs.</li>" \
"<li>Which other stacks on hacktivism.ch (Lemmy feeds, Castopod episodes, Bonfire public posts) follow the same rule.</li>" \
"</ul><p>Anyone can read this process. Sign in only to take part.</p>",
"<p>Öffentliches Beteiligungsmaterial soll ohne Konto lesbar sein. Schreiben (Vorschläge, Kommentare, Stimmen) kann ein Konto brauchen.</p>" \
"<p>Konkrete Fragen:</p><ul>" \
"<li>Prozessseiten, Treffen und letzte Aktivitäten bleiben öffentlich.</li>" \
"<li>Keine Drittanbieter-Tracker; Cookies nur, was die Software braucht.</li>" \
"<li>Welche anderen Dienste auf hacktivism.ch (Lemmy, Castopod, Bonfire) derselben Regel folgen.</li>" \
"</ul><p>Lesen ohne Anmeldung. Anmelden nur zum Mitmachen.</p>",
"<p>Lire du matériel civique ne doit pas exiger un compte. Écrire (propositions, commentaires, votes) peut le faire.</p>" \
"<p>Questions concrètes :</p><ul>" \
"<li>Pages de processus, rencontres et dernières activités restent publiques.</li>" \
"<li>Pas de traqueurs tiers ; cookies uniquement pour le logiciel.</li>" \
"<li>Quels autres services sur hacktivism.ch (Lemmy, Castopod, Bonfire) suivent la même règle.</li>" \
"</ul><p>Lecture sans compte. Connexion seulement pour participer.</p>"
), ),
start_date: Date.current, start_date: Date.current,
end_date: Date.current + 180, end_date: Date.current + 180,
@ -53,8 +69,12 @@ puts "process id=#{process.id} slug=#{process.slug} published=#{process.publishe
step = process.steps.find_or_initialize_by(position: 1) step = process.steps.find_or_initialize_by(position: 1)
step.assign_attributes( step.assign_attributes(
title: t("Open", "Offen", "Ouvert"), title: t("Open discussion", "Offene Diskussion", "Discussion ouverte"),
description: html("Example step.", "Beispielphase.", "Étape exemple."), description: t(
"<p>Collect positions on public-by-default reading. No decision this step.</p>",
"<p>Positionen zum Lesen ohne Konto sammeln. In dieser Phase keine Entscheidung.</p>",
"<p>Recueillir les positions sur la lecture sans compte. Pas de décision à cette étape.</p>"
),
start_date: Date.current, start_date: Date.current,
end_date: Date.current + 180, end_date: Date.current + 180,
active: true active: true
@ -62,6 +82,32 @@ step.assign_attributes(
step.save || fail_model!(step) step.save || fail_model!(step)
puts "step id=#{step.id} active=#{step.active}" puts "step id=#{step.id} active=#{step.active}"
pages = Decidim::Component.find_or_initialize_by(
participatory_space: process,
manifest_name: "pages"
)
pages.assign_attributes(
name: t("Background", "Hintergrund", "Contexte"),
published_at: pages.published_at || Time.current,
visible: true,
weight: 0
)
pages.save || fail_model!(pages)
page = Decidim::Pages::Page.find_or_initialize_by(component: pages)
page.body = t(
"<p>ISOC-style public-interest internet: people should inspect how a civic tool works without first handing over an identity.</p>" \
"<p>On this instance the stock cookie banner is hidden; only essential consent is set. The same idea applies to participation spaces: the text of a process is not a privilege of registered users.</p>" \
"<p>This page is part of that argument. It is published on purpose.</p>",
"<p>Internet im öffentlichen Interesse: man soll sehen können, wie ein Beteiligungswerkzeug arbeitet, ohne zuerst eine Identität abzugeben.</p>" \
"<p>Auf dieser Instanz ist der Vorrats-Cookie-Banner ausgeblendet; es gilt nur essential. Dieselbe Idee gilt für Beteiligungsräume: der Text eines Prozesses ist kein Privileg registrierter Konten.</p>" \
"<p>Diese Seite ist absichtlich öffentlich.</p>",
"<p>Internet d'intérêt public : on doit pouvoir voir comment un outil civique fonctionne sans d'abord céder une identité.</p>" \
"<p>Ici, la bannière cookies par défaut est masquée ; seul l'essentiel est posé. Même idée pour les espaces de participation : le texte d'un processus n'est pas un privilège des comptes.</p>" \
"<p>Cette page est publique volontairement.</p>"
)
page.save || fail_model!(page)
puts "pages component id=#{pages.id} page=#{page.id}"
meetings = Decidim::Component.find_or_initialize_by( meetings = Decidim::Component.find_or_initialize_by(
participatory_space: process, participatory_space: process,
manifest_name: "meetings" manifest_name: "meetings"
@ -81,20 +127,38 @@ meeting = Decidim::Meetings::Meeting.find_or_initialize_by(
component: meetings, component: meetings,
author: admin author: admin
) )
if meeting.new_record? || meeting.title.blank? meeting.title = t(
meeting.title = t("Open example meeting", "Offenes Beispieltreffen", "Rencontre exemple ouverte") "Open session: reading without an account",
end "Offene Runde: Lesen ohne Konto",
"Séance ouverte : lire sans compte"
)
meeting.assign_attributes( meeting.assign_attributes(
description: html( description: t(
"Anyone can open this meeting page without an account. It is a demo activity on decidim.hacktivism.ch.", "<p>Public agenda (no registration to read):</p><ol>" \
"Diese Treffen-Seite ist ohne Konto lesbar. Beispielaktivität auf decidim.hacktivism.ch.", "<li>What is already public on hacktivism.ch (Lemmy, Decidim, Castopod).</li>" \
"Cette page de rencontre est lisible sans compte. Activité exemple sur decidim.hacktivism.ch." "<li>Privacy by default — no tracker banner as a gate.</li>" \
"<li>Where login is justified (writing, admin).</li>" \
"</ol><p>Online / public. Signing in is optional.</p>",
"<p>Öffentliche Traktanden (Lesen ohne Anmeldung):</p><ol>" \
"<li>Was auf hacktivism.ch schon öffentlich ist (Lemmy, Decidim, Castopod).</li>" \
"<li>Privatsphäre als Vorgabe — kein Tracker-Banner als Tür.</li>" \
"<li>Wo ein Login gerechtfertigt ist (Schreiben, Admin).</li>" \
"</ol><p>Online / öffentlich. Anmelden ist freiwillig.</p>",
"<p>Ordre du jour public (lecture sans inscription) :</p><ol>" \
"<li>Ce qui est déjà public sur hacktivism.ch (Lemmy, Decidim, Castopod).</li>" \
"<li>Vie privée par défaut — pas de bannière traqueur comme porte.</li>" \
"<li>Où un compte se justifie (écriture, admin).</li>" \
"</ol><p>En ligne / public. Connexion facultative.</p>"
), ),
start_time: meeting.start_time || start_at, start_time: meeting.start_time || start_at,
end_time: meeting.end_time || finish_at, end_time: meeting.end_time || finish_at,
address: "hacktivism", address: "hacktivism.ch",
location: t("Online / public", "Online / öffentlich", "En ligne / public"), location: t("Online, public stream notes", "Online, öffentliche Notizen", "En ligne, notes publiques"),
location_hints: t("No registration required to read.", "Zum Lesen keine Anmeldung.", "Pas d'inscription pour lire."), location_hints: t(
"No ticket. Page stays readable if you do not attend.",
"Kein Ticket. Die Seite bleibt lesbar, wenn man nicht teilnimmt.",
"Pas de billet. La page reste lisible sans y assister."
),
type_of_meeting: "in_person", type_of_meeting: "in_person",
registration_type: "registration_disabled", registration_type: "registration_disabled",
registrations_enabled: false, registrations_enabled: false,
@ -106,6 +170,36 @@ meeting.assign_attributes(
meeting.save || fail_model!(meeting) meeting.save || fail_model!(meeting)
puts "meeting id=#{meeting.id} published=#{meeting.published_at} start=#{meeting.start_time}" puts "meeting id=#{meeting.id} published=#{meeting.published_at} start=#{meeting.start_time}"
proposals = Decidim::Component.find_or_initialize_by(
participatory_space: process,
manifest_name: "proposals"
)
proposals.assign_attributes(
name: t("Proposals", "Vorschläge", "Propositions"),
published_at: proposals.published_at || Time.current,
visible: true,
weight: 2
)
proposals.save || fail_model!(proposals)
proposal = Decidim::Proposals::Proposal.find_or_initialize_by(component: proposals)
if proposal.new_record? || proposal.title.blank?
proposal.title = t(
"Keep public processes readable without login",
"Öffentliche Prozesse ohne Anmeldung lesbar halten",
"Garder les processus publics lisibles sans connexion"
)
proposal.body = t(
"<p>Decide as a standing rule: published process pages, meetings and this proposal list stay readable without an account. Login is only for creating or endorsing proposals, commenting, and administration.</p>",
"<p>Als Regel festhalten: veröffentlichte Prozessseiten, Treffen und diese Vorschlagsliste bleiben ohne Konto lesbar. Login nur zum Erstellen oder Unterstützen von Vorschlägen, Kommentieren und für die Administration.</p>",
"<p>En faire une règle : les pages de processus publiées, les rencontres et cette liste de propositions restent lisibles sans compte. Connexion seulement pour créer ou soutenir, commenter, et pour l'administration.</p>"
)
proposal.add_coauthor(admin)
proposal.published_at = Time.current
end
proposal.save || fail_model!(proposal)
puts "proposal id=#{proposal.id} published=#{proposal.published_at}"
block = Decidim::ContentBlock.find_or_initialize_by( block = Decidim::ContentBlock.find_or_initialize_by(
organization: org, organization: org,
scope_name: "homepage", scope_name: "homepage",
@ -132,8 +226,22 @@ else
isoc.accepted_tos_version = org.tos_version isoc.accepted_tos_version = org.tos_version
isoc.locale = org.default_locale isoc.locale = org.default_locale
isoc.save || fail_model!(isoc) isoc.save || fail_model!(isoc)
avatar_path = ENV["DECIDIM_ISOC_AVATAR"].to_s
if !avatar_path.empty? && File.file?(avatar_path)
isoc.avatar.purge if isoc.avatar.attached?
isoc.avatar.attach(
io: File.open(avatar_path, "rb"),
filename: "isoc-gold.png",
content_type: "image/png"
)
isoc.save || fail_model!(isoc)
puts "isoc avatar attached from #{avatar_path} blob=#{isoc.avatar.blob&.byte_size}"
else
puts "isoc avatar skipped (DECIDIM_ISOC_AVATAR missing)"
end
puts "isoc user id=#{isoc.id} email=#{isoc.email} nick=#{isoc.nickname} admin=#{isoc.admin} confirmed=#{isoc.confirmed_at}" puts "isoc user id=#{isoc.id} email=#{isoc.email} nick=#{isoc.nickname} admin=#{isoc.admin} confirmed=#{isoc.confirmed_at}"
end end
puts "public process: https://#{host}/processes/#{process.slug}" puts "public process: https://#{host}/processes/#{process.slug}"
puts "public meeting: https://#{host}/processes/#{process.slug}/f/#{meetings.id}/meetings/#{meeting.id}" puts "public meeting: https://#{host}/processes/#{process.slug}/f/#{meetings.id}/meetings/#{meeting.id}"
puts "public proposal: https://#{host}/processes/#{process.slug}/f/#{proposals.id}/proposals/#{proposal.id}"

View file

@ -20,6 +20,13 @@ else
fi fi
podman cp "${src}" koopa-decidim:/code/lib/koopa_seed_example.rb podman cp "${src}" koopa-decidim:/code/lib/koopa_seed_example.rb
avatar_host="${DECIDIM_ISOC_AVATAR:-${HOME}/koopa-decidim/assets/img/isoc-gold.png}"
avatar_ctr=""
if [ -f "${avatar_host}" ]; then
avatar_ctr="/tmp/isoc-gold.png"
podman cp "${avatar_host}" "koopa-decidim:${avatar_ctr}"
fi
podman exec \ podman exec \
-e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \ -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \
-e DECIDIM_ADMIN_EMAIL \ -e DECIDIM_ADMIN_EMAIL \
@ -27,5 +34,6 @@ podman exec \
-e DECIDIM_ISOC_PASSWORD \ -e DECIDIM_ISOC_PASSWORD \
-e DECIDIM_ISOC_NAME \ -e DECIDIM_ISOC_NAME \
-e DECIDIM_ISOC_NICKNAME \ -e DECIDIM_ISOC_NICKNAME \
-e DECIDIM_ISOC_AVATAR="${avatar_ctr}" \
koopa-decidim bundle exec rails runner /code/lib/koopa_seed_example.rb koopa-decidim bundle exec rails runner /code/lib/koopa_seed_example.rb
echo "seed done. public: https://decidim.hacktivism.ch/processes/example" echo "seed done. public: https://decidim.hacktivism.ch/processes/example"