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

@ -1,16 +1,13 @@
# 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 avatar: DECIDIM_ISOC_AVATAR (PNG path inside the container).
# Usage: bundle exec rails runner /code/lib/koopa_seed_example.rb
def t(en, de, fr)
{ "en" => en, "de" => de, "fr" => fr }
end
def html(en, de, fr)
t("<p>#{en}</p>", "<p>#{de}</p>", "<p>#{fr}</p>")
end
def fail_model!(record)
raise "#{record.class}: #{record.errors.full_messages.join("; ")}"
end
@ -24,21 +21,40 @@ org.update!(force_users_to_authenticate_before_access_organization: false)
slug = "example"
process = Decidim::ParticipatoryProcess.find_or_initialize_by(organization: org, slug: slug)
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(
"Public demo — no account needed to read",
"Öffentliches Beispiel — Lesen ohne Konto",
"Démo publique — lecture sans compte"
"Which self-hosted services stay readable without an account?",
"Welche selbst gehosteten Dienste bleiben ohne Konto lesbar?",
"Quels services auto-hébergés restent lisibles sans compte ?"
),
short_description: html(
"A published participatory process so visitors can see Decidim without signing in.",
"Ein veröffentlichter Beteiligungsprozess, den Besucherinnen ohne Anmeldung lesen können.",
"Un processus participatif publié, lisible sans compte."
short_description: t(
"<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>",
"<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>",
"<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(
"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.",
"Kleines öffentliches Beispiel auf hacktivism. Prozessseite und Treffen sind für alle sichtbar. Anmelden nur zum Mitmachen.",
"Petit exemple public sur hacktivism. La page du processus et la rencontre sont visibles sans compte. La connexion sert uniquement à participer."
description: t(
"<p>Reading civic material should not depend on creating an account. Writing (proposals, comments, votes) can.</p>" \
"<p>Concrete questions:</p><ul>" \
"<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,
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.assign_attributes(
title: t("Open", "Offen", "Ouvert"),
description: html("Example step.", "Beispielphase.", "Étape exemple."),
title: t("Open discussion", "Offene Diskussion", "Discussion ouverte"),
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,
end_date: Date.current + 180,
active: true
@ -62,6 +82,32 @@ step.assign_attributes(
step.save || fail_model!(step)
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(
participatory_space: process,
manifest_name: "meetings"
@ -81,20 +127,38 @@ meeting = Decidim::Meetings::Meeting.find_or_initialize_by(
component: meetings,
author: admin
)
if meeting.new_record? || meeting.title.blank?
meeting.title = t("Open example meeting", "Offenes Beispieltreffen", "Rencontre exemple ouverte")
end
meeting.title = t(
"Open session: reading without an account",
"Offene Runde: Lesen ohne Konto",
"Séance ouverte : lire sans compte"
)
meeting.assign_attributes(
description: html(
"Anyone can open this meeting page without an account. It is a demo activity on decidim.hacktivism.ch.",
"Diese Treffen-Seite ist ohne Konto lesbar. Beispielaktivität auf decidim.hacktivism.ch.",
"Cette page de rencontre est lisible sans compte. Activité exemple sur decidim.hacktivism.ch."
description: t(
"<p>Public agenda (no registration to read):</p><ol>" \
"<li>What is already public on hacktivism.ch (Lemmy, Decidim, Castopod).</li>" \
"<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,
end_time: meeting.end_time || finish_at,
address: "hacktivism",
location: t("Online / public", "Online / öffentlich", "En ligne / public"),
location_hints: t("No registration required to read.", "Zum Lesen keine Anmeldung.", "Pas d'inscription pour lire."),
address: "hacktivism.ch",
location: t("Online, public stream notes", "Online, öffentliche Notizen", "En ligne, notes publiques"),
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",
registration_type: "registration_disabled",
registrations_enabled: false,
@ -106,6 +170,36 @@ meeting.assign_attributes(
meeting.save || fail_model!(meeting)
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(
organization: org,
scope_name: "homepage",
@ -132,8 +226,22 @@ else
isoc.accepted_tos_version = org.tos_version
isoc.locale = org.default_locale
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}"
end
puts "public process: https://#{host}/processes/#{process.slug}"
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}"