Skip to content
Cooperation-orgPublic

About

act — story + calls-to-action app for volunteer orgs: campaign pages (Givebutter/SimpleTip embeds), testimonials as LinkedClaims, share attribution, org-scoped volunteer editing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

act

Deployments and data

A deployment is better off with its own database. Rows carry their own ids, so restoring one deployment's dump into another's database can overwrite live rows. Where deployments do share, move content by importing it from its source rather than by restoring a dump, and check with the project owner first.

Story + calls-to-action app for volunteer orgs. Campaign pages where money is ONE way to help: give (wrapped Givebutter widget, or SimpleTip), mentor, hire, host an event, AMA, subscribe. Testimonials from named-or-anonymous vouchers (LinkedClaim URI per testimony, publish-gated), per-person share links with counted attribution, Instagram story/post card downloads.

First deployment target: VM 513 (voluntask), domain act.raisethevoices.org, for the Raise the Voices JREAS Hub campaign. Design + phased plan: projects repo Active/raise-the-voices/act-design.md; VM handoff: dashboard-vm513-handoff.md.

Run (dev)

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
.venv/bin/python manage.py migrate
.venv/bin/python manage.py seed_jreas        # RTV org + JREAS campaign (draft)
.venv/bin/python manage.py createsuperuser
.venv/bin/python manage.py runserver

Publish the campaign in /admin (Campaigns → jreas → status: published).

Deploy

Live: cooperation.org/letters/ on the civic-actions VM (10.0.0.154) since 2026-09-07; only /letters/, /admin/, /static/, /media/ are routed there. Later: act.raisethevoices.org (VM 513) for the campaign pages.

  • .env from .env.example (mode 600). Postgres: database act on VM 100 (roles act_owner for migrations, act_user for the running app). ACT_BASE_PATH/SCRIPT_NAME empty at a domain root.
  • Python 3.12 venv, pip install -r requirements.txt. WeasyPrint needs libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0.
  • manage.py migrate --check (run migrate with the act_owner credentials if pending), collectstatic. media/ must be writable by the service user (act).
  • deploy/act-retry-mail.service + .timer: confirmation-mail retries (systemctl enable --now act-retry-mail.timer).
  • deploy/act.service (gunicorn 127.0.0.1:8050) and deploy/nginx-cooperation.org.conf (locations added inside the existing cooperation.org vhost) are what is live. deploy/nginx-act.conf is the own-domain variant for act.raisethevoices.org.
  • Deploy a change: git pull, .venv/bin/pip install -r requirements.txt, collectstatic --clear (hashed filenames; nginx caches /static/ for 7 days), tests with ACT_DEBUG=1 manage.py test, migrate --check, sudo systemctl restart act.
  • DNS + Caddy route are host-side actions. Register in cobox app-registry.md.

Deployments

One repo and one database schema, several deployments. ACT_APPS in .env picks the apps a deployment routes and shows in admin: campaigns (story + calls to action, owns /), letters (open letters and petitions at /letters/). Org and volunteer accounts are shared and always visible.

Site ACT_APPS ACT_SITE_ORG Public paths
cooperation.org letters , /letters/, /admin/ (root forwards to /letters/)
raisethevoices.org campaigns,blog,people , (platform: every org's published campaigns) /, /c/<slug>/, /updates/, /blog/, /people/, /admin/
Jreas Coop (domain TBD) campaigns,blog,people jreas-coop / (org home), /c/jreas-coop/, /updates/, /blog/, /people/, /governance/, /admin/
Story Kit: demos.linkedtrust.us/storykit/ (VM 200 demo, database act) artifacts , / → /kit/, /start/, /ways/, /send/, /library/, /collections/, /stories/<slug>/, /admin/

New capabilities (content management, content planning, email campaigns) arrive as further apps in this list.

Story Kit (artifacts)

Raw material from many sources lands in one place per org, then becomes stories. Design: projects repo Ideas/inbound-artifacts/ (the flow) and Ideas/story-kit/ (the app).

  • Start (/start/): a signed-in person names their org (and optionally its website). That creates the Org, makes them its approver, and opens Ways to send (/ways/).
  • Ways in (each makes Artifacts; the source stays the original, we keep a copy):
    • phone or computer: /send/ (camera, video, voice, files, drag and drop). Installed to the home screen, the web app is in the phone's Share menu (manifest.webmanifest share target → /share/).
    • email: each org has an address on its Email source (manage.py set_intake_address <org> <address>); manage.py poll_email reads the intake mailbox over IMAP (Migadu) and files each message under the org it was sent to: attachments become artifacts, the body is the sender's words. Run it on a timer.
    • Google Drive: paste a folder shared "Anyone with the link"; it is copied in the background, subfolders included, Google Docs skipped. Without ACT_GOOGLE_API_KEY it reads Drive's public folder view; with it, the Drive API (adds owner and photo dates). manage.py check_sources re-copies new files.
    • LinkedClaims: claims whose subject is the org's site (GET {LT_API}/api/claims/subject/<url>).
    • a link, with what it shows.
  • One Artifact per item per org (sha256 of the file, or of the URL). The sender's words are kept exactly; photo dates come from EXIF (location tags are never read).
  • What we have (/library/): filter by kind, label, way in, person, dates; label, hide, collect, start a story. Collections are hand-picked sets or a saved filter.
  • Stories: /stories/<slug>/edit/ orders pieces (drag, works by touch), adds captions and paragraphs; publish makes /stories/<slug>/ public. Drafts are visible to the org only. The AI helper ("Draft from this collection") appears only with ANTHROPIC_API_KEY; it uses only the pieces' own words and marks every line it writes until a person edits it.
  • Files: an S3-compatible bucket (Backblaze B2) when ACT_STORAGE_BUCKET is set, else media/. Set ACT_STORAGE_SIGNED_URLS=0 and ACT_STORAGE_PUBLIC_HOST only for a public bucket.

Install Story Kit on its own VM

  1. Checkout, venv, pip install -r requirements.txt (see Deploy above for WeasyPrint libs).
  2. .env (mode 600) from .env.example with ACT_APPS=artifacts, ACT_PUBLIC_URL, and ACT_BASE_PATH/SCRIPT_NAME empty at a domain root. Its own database is better (see "Deployments and data"); ACT_PG_* point at it.
  3. manage.py migrate with the owner role, collectstatic --clear, createsuperuser.
  4. deploy/act.service (gunicorn) and deploy/nginx-act.conf with the VM's domain; keep client_max_body_size 200m for video.
  5. Files: set ACT_STORAGE_* for a B2 bucket, else media/ must be writable by the service user.
  6. Email intake: ACT_INTAKE_IMAP_* for the org's mailbox and a timer running manage.py poll_email (copy deploy/act-retry-mail.timer, change the command).
  7. Sign-in: a LinkedTrust SSO client for <ACT_PUBLIC_URL>/api/v1/auth/linkedtrust/callback (LINKEDTRUST_CLIENT_ID/SECRET). Without it, sign-in is the Django admin login.

Single-org site (ACT_SITE_ORG)

Set ACT_SITE_ORG=<org slug> and the deployment is that org: the wordmark and footer are the org's name, home is campaigns/org_home.html (tagline, about in Markdown, its campaigns, the give rail of its first published campaign with the locked Civic Works disclosure, Latest, People, Governance), and campaigns, posts and people of other orgs 404. Every text field is the org's own words, edited in /admin → Orgs (superuser). A slug with no Org row raises ImproperlyConfigured on / rather than silently falling back.

  • /updates/ and /updates/feed/ (RSS): published campaign updates (receipts, counts) and blog posts as one stream, newest first. Home shows the latest six.
  • /blog/, /blog/<slug>/, /blog/feed/, blog app. Post: Markdown body, optional author (a Person), summary for lists and feed, publish gate (published_at set on publish).
  • /people/, /people/<slug>/, people app. Person: name, role, photo, Markdown bio, links, optional login. Publishes only with consent_on_record ticked and an approver's publish , a name tied to funding can endanger someone; the admin action and the save hook both refuse otherwise. A post's author link appears only while the author's profile is published.
  • /governance/, shown (and linked in nav) only when the Org has governance_text (Markdown, the members' own words) or governance_url. Governance itself is not modelled here: earned governance (reviewed work → voting weight, pie, votes, sortition) is GovKit (Cooperation-org/govkit, dash.workers.vc); governance_url points at the org's page there.

seed_jreas creates org jreas-coop ("Jreas Coop") and the JREAS Hub campaign under it, as draft.

Letters (open letters and petitions)

letters/ app, pages under /letters/<slug>/. A Letter belongs to an Org, is written in Markdown (bold, italics, paragraphs, bullets), and is draft until published in /admin. kind is open letter (counter only) or petition (addressee + goal). Theme: parchment, EB Garamond (bundled, OFL).

Signing: first name, last name, city, country, plus the letter's own fields (Letters → Signature fields: label, kind short / paragraph / checkbox, required, public). A signature counts when the signer has either confirmed their email or drawn a signature (signature_pad, stored as PNG). Email is required to get updates (keep_updated). Same email twice: an unconfirmed one is replaced, a confirmed one is refused.

Confirmation mail goes out over SMTP (EMAIL_HOST, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, DEFAULT_FROM_EMAIL). With EMAIL_HOST unset nothing is sent; every attempt, sent or failed, is an EmailLog row. A failed send is retried automatically 1 h, 6 h and 24 h later (letters.models.RETRY_DELAYS; manage.py retry_confirmations on deploy/act-retry-mail.timer, every 15 min), then it gives up. The schedule and last error live on the Signature. Admin → Signatures → filter "confirmation": confirmed / drawn / approved / sent, waiting / failed, retry scheduled / failed, gave up / no email sent; action "Resend confirmation email" starts over. Approved (checkbox in the list) makes a signature count and show even if the email never confirmed, for the friend who signed but never got the mail.

PDF: /letters/<slug>.pdf is the letter with every counted signature that has on_pdf ticked, ordered by pdf_sort then date. In admin, filter (city, country, …), tick rows, action "PDF of the letter with the selected signatures". WeasyPrint renders the same templates; static and media files are read from disk.

Testimonials are LinkedClaims

Same mechanism as workers.vc (doorway/claims.py) and LinkedClaims/docs/embedding.md:

  1. /c/<slug>/testimony/ embeds <linked-video-recorder> from LT_EMBED; the browser uploads the video to LinkedTrust storage and the returned videoUrl lands in the form's hidden field.
  2. The testimonial sits pending. In admin an approver runs "Sign as LinkedClaim and publish": campaigns/linkedtrust.py POSTs {subject: campaign page, claim: ENDORSES, statement, videoUrl, name only if opted in} to LT_API/api/claims with x-lt-client-id/secret, stores claim_id, linkedclaim_uri, signed_at, or sign_error.
  3. The campaign page renders each signed testimonial as <linked-badge claim-id=…> (badge.js from LT_EMBED); unsigned ones render as plain cards marked "not yet signed".

LT_CLIENT_ID/LT_CLIENT_SECRET are issued per site by whoever runs live.linkedtrust.us; without them signing is refused (an issuer-less claim is permanent damage), publishing still works.

Money boundary

act never touches funds. The give rail embeds the Givebutter widget for the org's Givebutter account (charity of record: Civic Works, receipts and discretion-and-control disclosure are locked text in campaigns/donation_text.py, deliberately not DB-editable; see projects repo Active/raise-the-voices/legal/fundraising-compliance.md) or a <simple-tip> element. Set Org.givebutter_account_id + Campaign.givebutter_campaign_id in admin when the Givebutter campaign exists.

Volunteers

Volunteer logins get is_staff + one VolunteerProfile(org=...) per org they work with (the same people volunteer on letters and on fundraisers); admin queries are scoped to those orgs, Org pickers offer only them, delete is superuser-only, and publishing (campaigns, testimonials, updates, posts, people) is an approver action per org, an approver for org A cannot publish org B's rows even when they can see them. LinkedTrust SSO activates when LINKEDTRUST_CLIENT_ID/SECRET are set (client registered at live.linkedtrust.us, see django-linkedtrust-auth README); Django session login always works as the fallback.

About

act — story + calls-to-action app for volunteer orgs: campaign pages (Givebutter/SimpleTip embeds), testimonials as LinkedClaims, share attribution, org-scoped volunteer editing

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages