decidim: mirror koopa-decidim setup (9027, docs for /sm showcase)

This commit is contained in:
Hernâni Marques 2026-08-24 08:47:22 +02:00
parent d519fb3c80
commit 9ae0dfa429
No known key found for this signature in database
21 changed files with 998 additions and 2 deletions

View file

@ -0,0 +1,247 @@
# frozen_string_literal: true
# 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 fail_model!(record)
raise "#{record.class}: #{record.errors.full_messages.join("; ")}"
end
host = ENV.fetch("DECIDIM_HOST", "decidim.hacktivism.ch")
org = Decidim::Organization.find_by!(host: host)
admin = Decidim::User.find_by!(email: ENV.fetch("DECIDIM_ADMIN_EMAIL"), organization: org)
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(
"Public digital commons",
"Öffentliche digitale Allmende",
"Communs numériques publics"
),
subtitle: t(
"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: 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: 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,
published_at: process.published_at || Time.current,
private_space: false,
promoted: true,
scopes_enabled: false,
weight: 1
)
process.save || fail_model!(process)
puts "process id=#{process.id} slug=#{process.slug} published=#{process.published_at}"
step = process.steps.find_or_initialize_by(position: 1)
step.assign_attributes(
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
)
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"
)
meetings.assign_attributes(
name: t("Meetings", "Treffen", "Rencontres"),
published_at: meetings.published_at || Time.current,
visible: true,
weight: 1
)
meetings.save || fail_model!(meetings)
puts "meetings component id=#{meetings.id} published=#{meetings.published_at}"
start_at = Time.current.utc.change(hour: 18, min: 0) + 7.days
finish_at = start_at + 2.hours
meeting = Decidim::Meetings::Meeting.find_or_initialize_by(
component: meetings,
author: admin
)
meeting.title = t(
"Open session: reading without an account",
"Offene Runde: Lesen ohne Konto",
"Séance ouverte : lire sans compte"
)
meeting.assign_attributes(
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.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,
private_meeting: false,
transparent: true,
published_at: meeting.published_at || Time.current,
comments_enabled: true
)
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",
manifest_name: "highlighted_processes"
)
block.weight ||= 45
block.published_at ||= Time.current
block.save || fail_model!(block)
puts "homepage highlighted_processes published=#{block.published_at}"
isoc_email = ENV["DECIDIM_ISOC_EMAIL"].to_s.strip
isoc_password = ENV["DECIDIM_ISOC_PASSWORD"].to_s
if isoc_email.empty? || isoc_password.empty?
puts "isoc user skipped (DECIDIM_ISOC_EMAIL / DECIDIM_ISOC_PASSWORD unset)"
else
isoc = Decidim::User.find_or_initialize_by(email: isoc_email, organization: org)
isoc.name = ENV.fetch("DECIDIM_ISOC_NAME", "isoc")
isoc.nickname = ENV.fetch("DECIDIM_ISOC_NICKNAME", "isoc")
isoc.password = isoc_password
isoc.password_confirmation = isoc_password
isoc.admin = false
isoc.confirmed_at ||= Time.current
isoc.tos_agreement = true
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}"