build: a door may point to a members-only need; signed-in people are not asked who they are; a granted group does not self-invite
The vel: paid membership grants the medlemmer group, and a committee seat is offered on the bidra door and taken on the members' page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GT37Z1Xtfd9pUuQtMTg6Yt
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
67250af445
commit
28ef235906
@@ -82,9 +82,13 @@ needs:
|
||||
# Medlemmet melder seg ut selv, gjennom sin egen lenke.
|
||||
moves_by: { utmeldt: [submitter, kasserer] }
|
||||
mails: { open: submitter, betalt: submitter }
|
||||
# Betalt kontingent er medlemskapet: husstanden får konto og plass
|
||||
# blant medlemmene i samme trekk, og det som er medlemmenes eget
|
||||
# (komitéplass) ligger bak innloggingen.
|
||||
grants: { betalt: medlemmer }
|
||||
max_fields: 3
|
||||
collects:
|
||||
- { name: navn }
|
||||
- { name: name }
|
||||
- { name: adresse }
|
||||
- { name: email, type: email }
|
||||
- { name: telefon, type: tel, optional: true }
|
||||
@@ -172,11 +176,14 @@ needs:
|
||||
- { name: telefon, type: tel }
|
||||
- { name: hva, type: select, multiple: true, options: [17. mai, Juletregranstenning, Skøytedag, Dugnad på elvestien, Det som trengs] }
|
||||
|
||||
# Komitéplass er for medlemmer: døra viser veien, skjemaet ligger på
|
||||
# medlemmenes side bak innloggingen.
|
||||
- id: komite
|
||||
who: en som vil sitte i en komité
|
||||
who: et medlem som vil sitte i en komité
|
||||
wants: en plass i den komiteen som driver med det de bryr seg om
|
||||
lands_in: komitesoknader
|
||||
door: bidra
|
||||
enters_as: medlemmer
|
||||
handled_by: styret
|
||||
stages: [open, plassert, avslatt]
|
||||
stages_text: { open: "mottatt", plassert: "plassert i en komité", avslatt: "avslått" }
|
||||
|
||||
+44
-14
@@ -182,7 +182,9 @@ pub struct PlanNeed {
|
||||
#[serde(default)]
|
||||
pub enters_as: Option<String>,
|
||||
/// The door this public need sits behind; none means straight on
|
||||
/// the front door.
|
||||
/// the front door. With `enters_as` too, the door carries a card
|
||||
/// that leads to the group's page, where the form is: a seat on a
|
||||
/// committee is offered to everyone and open to members.
|
||||
#[serde(default)]
|
||||
pub door: Option<String>,
|
||||
/// One answer per record of another need: a neighbour answering a
|
||||
@@ -420,9 +422,8 @@ impl Plan {
|
||||
if !doors.contains(door) {
|
||||
bail!(at(&format!("door {door:?} is not one of the plan's doors")));
|
||||
}
|
||||
if need.enters_as.is_some() {
|
||||
bail!(at("a door is a public way in; a need with enters_as has its group's page instead"));
|
||||
}
|
||||
// With enters_as as well, the door only points the way:
|
||||
// the form sits on the group's page, behind sign-in.
|
||||
}
|
||||
if let Some(about) = &need.about {
|
||||
let Some(parent) = self.needs.iter().find(|n| &n.id == about) else {
|
||||
@@ -1273,7 +1274,11 @@ fn groups(plan: &Plan) -> BTreeSet<String> {
|
||||
/// must have a desk to carry it on.
|
||||
fn inviters(plan: &Plan, group: &str) -> BTreeSet<String> {
|
||||
let desks = groups(plan);
|
||||
std::iter::once(group.to_string())
|
||||
// A group a case grants is entered by that case, not by a member
|
||||
// waving somebody in: members do not make members.
|
||||
let granted = plan.needs.iter().any(|n| n.grants.values().any(|g| g == group));
|
||||
(if granted { None } else { Some(group.to_string()) })
|
||||
.into_iter()
|
||||
.chain(plan.invites_by.iter().cloned())
|
||||
.chain(plan.groups.iter().filter(|g| g.id == group).flat_map(|g| g.invited_by.iter().cloned()))
|
||||
.filter(|g| desks.contains(g))
|
||||
@@ -1540,9 +1545,13 @@ fn form_alternative(plan: &Plan, need: &PlanNeed, w: &Value, action: &str) -> Va
|
||||
if f.multiple { r["multiple"] = json!(true); }
|
||||
let hint = s(&w["hints"][&f.name]).trim();
|
||||
if !hint.is_empty() && f.kind != "select" { r["placeholder"] = json!(hint); }
|
||||
// Answering a record is done by neighbours as well as members:
|
||||
// someone signed in is not asked for what the site knows.
|
||||
if parent.is_some() && f.name == "email" && f.kind == "email" { r["value"] = json!("{user.email}"); }
|
||||
// Someone signed in is not asked for what the site knows: on a
|
||||
// group's page they always are, and answering a record they
|
||||
// may be.
|
||||
if parent.is_some() || need.enters_as.is_some() {
|
||||
if f.kind == "email" && matches!(f.name.as_str(), "email" | "epost" | "e-post") { r["value"] = json!("{user.email}"); }
|
||||
if f.kind == "text" && matches!(f.name.as_str(), "name" | "navn") { r["value"] = json!("{user.name}"); }
|
||||
}
|
||||
r
|
||||
}).collect();
|
||||
// The record this answer is about, carried in from the page's own
|
||||
@@ -1754,7 +1763,17 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
||||
"name": s(&w["question"]),
|
||||
"description": s(&w["description"]),
|
||||
"responsible": responsible,
|
||||
"alternatives": plan.needs_behind(&door.id).iter().map(|n| form_alternative(plan, n, &w["needs"][&n.id], &public_followup(n))).collect::<Vec<_>>(),
|
||||
"alternatives": plan.needs_behind(&door.id).iter().map(|n| match &n.enters_as {
|
||||
None => form_alternative(plan, n, &w["needs"][&n.id], &public_followup(n)),
|
||||
// The way to the group's page: portal shows the
|
||||
// sign-in gate there to whoever is not in the group.
|
||||
Some(group) => json!({
|
||||
"name": s(&w["needs"][&n.id]["heading"]),
|
||||
"description": s(&w["needs"][&n.id]["description"]),
|
||||
"action": internal_page(n, group),
|
||||
"consequence": [s(&w["needs"][&n.id]["button"])],
|
||||
}),
|
||||
}).collect::<Vec<_>>(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -2029,7 +2048,7 @@ fn words_prompt(plan: &Plan, path: &str, fallback: &Value) -> String {
|
||||
} else if path.starts_with("questions/status/") {
|
||||
status_purpose.as_str()
|
||||
} else if plan.doors.iter().any(|d| door_path(d) == path) {
|
||||
"a page one kind of visitor reaches from the front door. `question` is what this page asks them; under `needs`, each entry is one alternative they can pick, with the form behind it."
|
||||
"a page one kind of visitor reaches from the front door. `question` is what this page asks them; under `needs`, each entry is one alternative they can pick, with the form behind it - or, for a need that is members' only, the card that leads to the members' page, where the form is behind sign-in: say so in its description."
|
||||
} else if path.ends_with("/index.yaml") {
|
||||
"a desk: the page one role inside the business works from. Under `buckets`, each entry is a list of cases; `moves` are the buttons that move a case from one stage to the next (key `from->to`), so label each with what the person has just done. Under `links` are pages this role can open. Under `invites`, each entry is a form that invites a person into that group - they get an account and a one-time link - and `invited` is the list of invites sent, where `again` is the button that sends an invite whose link ran out once more. `suggestions`, on the desk that has it, is the list of changes the people responsible for a page have suggested: `looking`, `changed` and `declined` are its buttons."
|
||||
} else if path.ends_with("/[key].yaml") {
|
||||
@@ -2547,12 +2566,23 @@ mod tests {
|
||||
assert_eq!(needs["needs"][2]["max_steps"], 1);
|
||||
assert_eq!(plan.front_door_count(), 2);
|
||||
|
||||
// A door nobody is behind, a door on an internal need, and a
|
||||
// door that is also a group are refused.
|
||||
// A door nobody is behind, and a door that is also a group, are
|
||||
// refused. A door on a need that enters as a group is the way
|
||||
// to that group's page: a card on the door, the form behind
|
||||
// sign-in, and the spec entering on the group's page.
|
||||
let lonely: Plan = serde_yaml::from_str(&yaml.replace(" door: your-building\n", "")).unwrap();
|
||||
assert!(lonely.validate().unwrap_err().to_string().contains("leads nowhere"));
|
||||
let internal: Plan = serde_yaml::from_str(&yaml.replacen(" door: your-building\n", " door: your-building\n enters_as: pms\n", 1)).unwrap();
|
||||
assert!(internal.validate().unwrap_err().to_string().contains("public way in"));
|
||||
let members: Plan = serde_yaml::from_str(&yaml.replacen(" door: your-building\n", " door: your-building\n enters_as: pms\n", 1)).unwrap();
|
||||
members.validate().unwrap();
|
||||
let files = render(&members, &fallback_words(&members)).unwrap();
|
||||
let door: Value = serde_yaml::from_str(&files["questions/your-building/index.yaml"]).unwrap();
|
||||
assert_eq!(door["alternatives"][0]["action"], "/pms/survey");
|
||||
assert!(door["alternatives"][0].get("record_as").is_none());
|
||||
let page: Value = serde_yaml::from_str(&files["questions/pms/survey.yaml"]).unwrap();
|
||||
assert_eq!(page["alternatives"][0]["record_as"], "enquiries");
|
||||
let needs: Value = serde_yaml::from_str(&files["needs.yaml"]).unwrap();
|
||||
assert_eq!(needs["needs"][0]["enters"], "/pms/survey");
|
||||
assert_eq!(needs["needs"][0]["enters_as"], "pms");
|
||||
let clash: Plan = serde_yaml::from_str(&yaml.replace("your-building", "pms")).unwrap();
|
||||
assert!(clash.validate().unwrap_err().to_string().contains("also a group"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user