build: the plan's contact may be a group, which then answers for every page

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-30 08:18:30 +02:00
co-authored by Claude Fable 5.1
parent 894f228b8f
commit 67250af445
2 changed files with 31 additions and 2 deletions
+3 -1
View File
@@ -12,7 +12,9 @@
# igjen.
business: Tomter Vel
language: no
contact: { name: Styret i Tomter Vel, email: post@tomtervel.no }
# Styret svarer for sidene som gruppe: ingen konto for innboksen, og
# hvert styremedlem kan foreslå endringer på dem.
contact: { name: Styret i Tomter Vel, email: post@tomtervel.no, group: styret }
# Avsenderen for all e-post fra siden (gdo på kasse leverer); det
# kasses postfix kan sende for i dag.
mail: { from: "Tomter Vel <tomtervel@klingenbergbygg.no>", reply_to: post@tomtervel.no }
+28 -1
View File
@@ -156,6 +156,11 @@ pub struct Door {
pub struct Contact {
pub name: String,
pub email: String,
/// The group the contact is: a board's inbox, not a person. Then
/// portal makes no account for the address, and every member of
/// the group may suggest changes to the pages it answers for.
#[serde(default)]
pub group: Option<String>,
}
/// Who the site's mail comes from: portal sends nothing without it,
@@ -343,6 +348,13 @@ fn is_id(s: &str) -> bool {
impl Plan {
pub fn validate(&self) -> anyhow::Result<()> {
// A contact that is a group has to be one with a desk, or
// nobody could sign in as it.
if let Some(group) = &self.contact.group {
if !groups(self).contains(group) {
bail!("contact: group {group:?} has no desk: it must handle, watch or move something");
}
}
let mut doors = BTreeSet::new();
for door in &self.doors {
if !is_id(&door.id) || !doors.insert(&door.id) {
@@ -1591,7 +1603,10 @@ fn form_alternative(plan: &Plan, need: &PlanNeed, w: &Value, action: &str) -> Va
/// Every file of the content repo, from the plan and a set of words.
pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BTreeMap<String, String>> {
let mut files: BTreeMap<String, Value> = BTreeMap::new();
let responsible = json!({ "name": plan.contact.name, "contact": plan.contact.email });
let mut responsible = json!({ "name": plan.contact.name, "contact": plan.contact.email });
if let Some(group) = &plan.contact.group {
responsible["group"] = json!(group);
}
let mut site = json!({ "title": plan.business, "lang": plan.language });
if let Some(mail) = &plan.mail {
@@ -2909,6 +2924,18 @@ mod invite_tests {
const BOARD: &str = "business: Vel\ncontact: { name: S, email: s@v.example }\nmail: { from: v@v.example }\nlanguage: no\nneeds:\n - id: notice\n who: en nabo som har noe å låne ut\n wants: et oppslag naboene ser\n lands_in: oppslag\n handled_by: redaktor\n stages: [open, tatt_ned]\n done_when: [tatt_ned]\n moves_by: { tatt_ned: [submitter, redaktor] }\n mails: { open: s@v.example }\n watched_by: [public]\n public_states: [open]\n max_fields: 3\n collects:\n - { name: email, type: email }\n - { name: hva, type: textarea }\n - id: answer\n who: en nabo som kan låne ut det\n wants: at den som la ut oppslaget får vite det\n about: notice\n lands_in: svar\n handled_by: redaktor\n stages: [open, lest]\n done_when: [lest]\n mails: { open: owner }\n max_fields: 1\n collects:\n - { name: email, type: email }\n";
#[test]
fn a_contact_that_is_a_group_answers_for_every_page_as_that_group() {
let plan: Plan = serde_yaml::from_str(&BOARD.replace("contact: { name: S, email: s@v.example }", "contact: { name: S, email: s@v.example, group: redaktor }")).unwrap();
plan.validate().unwrap();
let files = render(&plan, &fallback_words(&plan)).unwrap();
let front: Value = serde_yaml::from_str(&files["questions/index.yaml"]).unwrap();
assert_eq!(front["responsible"]["group"], "redaktor");
assert_eq!(front["responsible"]["contact"], "s@v.example");
let nobody: Plan = serde_yaml::from_str(&BOARD.replace("contact: { name: S, email: s@v.example }", "contact: { name: S, email: s@v.example, group: nobody }")).unwrap();
assert!(nobody.validate().unwrap_err().to_string().contains("no desk"));
}
#[test]
fn a_need_about_another_is_answered_from_the_boards_own_rows() {
let plan: Plan = serde_yaml::from_str(BOARD).unwrap();