Three small things that together make a board work: a link on every
listed record to a page about it (ResourceSpec.link, {id} and {field}
in the address); a state mail whose `to` is a field of the record this
one belongs to ({notice.email}), with that record's fields in the
text, and a lint that the form carries the record field it reaches
through; and value: "{user.email}" filled from the session and hidden,
so someone the site knows is not asked who they are.
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
470338b969
commit
fc998c8592
+85
-1
@@ -207,6 +207,35 @@ impl Question {
|
||||
}
|
||||
}
|
||||
|
||||
/// The signed-in person's own details into the fields that ask for
|
||||
/// them: `value: "{user.email}"` becomes their address and the field
|
||||
/// hidden, since asking someone who is known to say who they are is
|
||||
/// the form doubting them. Without a session the placeholder is
|
||||
/// emptied, and the field asks. Only the whole of a value is filled
|
||||
/// this way; a value that mixes a user placeholder with other text is
|
||||
/// left alone.
|
||||
pub fn fill_from_user(question: &mut Question, user: Option<&crate::auth::User>) {
|
||||
for alt in &mut question.alternatives {
|
||||
for feature in &mut alt.features {
|
||||
for req in &mut feature.requirements {
|
||||
let Some(value) = req.value.as_deref() else { continue };
|
||||
let filled = match value {
|
||||
"{user.email}" => user.and_then(|u| u.email.clone()),
|
||||
"{user.name}" => user.map(|u| u.display_name.clone()),
|
||||
_ => continue,
|
||||
};
|
||||
match filled.filter(|v| !v.trim().is_empty()) {
|
||||
Some(v) => {
|
||||
req.value = Some(v);
|
||||
req.kind = "hidden".to_string();
|
||||
}
|
||||
None => req.value = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An alternative's `action` may carry `{field}` / `{field.sub}`
|
||||
/// placeholders filled from the submitted responses - `/shape/{curve.key}`
|
||||
/// lands on the dynamic page `/shape/[key]` for the key the visitor's
|
||||
@@ -439,6 +468,14 @@ pub struct ResourceSpec {
|
||||
/// didn't declare. `Kv` sources only.
|
||||
#[serde(default)]
|
||||
pub transitions: Vec<Transition>,
|
||||
/// A way from each listed record to a page about it: `to` is the
|
||||
/// page, with `{id}` for the record's id and `{field}` for any of
|
||||
/// its fields, and `label` the words on the link. The other half
|
||||
/// of `type: record`, seen from the list: a notice on a board links
|
||||
/// to the page where a neighbour answers it, and that page's form
|
||||
/// carries the notice's id back in.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub link: Option<RowLink>,
|
||||
/// A jq filter (evaluated via `jaq`, no shell-out) reshaping the
|
||||
/// fetched value before it reaches the frontend - e.g.
|
||||
/// `.[] | {name, url: .html_url}`. `None` returns it as-is.
|
||||
@@ -542,7 +579,10 @@ pub struct Requirement {
|
||||
/// A preset value the field starts with. On a dynamic page the
|
||||
/// URL segment substitutes into `{name}` placeholders here, like
|
||||
/// resource keys - `value: "{key}"` on a hidden field carries the
|
||||
/// page's key into the answer.
|
||||
/// page's key into the answer. `{user.email}` and `{user.name}`
|
||||
/// are the signed-in person's, and a field they fill is carried
|
||||
/// rather than asked (see `fill_from_user`); for a visitor they
|
||||
/// are empty, and the field asks as usual.
|
||||
#[serde(default)]
|
||||
pub value: Option<String>,
|
||||
/// `type: record` only - the bucket the referenced record lives in.
|
||||
@@ -574,6 +614,30 @@ pub struct Requirement {
|
||||
/// listing may ask about a record's own contents: one field, one
|
||||
/// value, compared as strings. Anything more is a query language, and
|
||||
/// a jq filter is already there for shaping what comes back.
|
||||
/// A link from every listed record to a page about it (`ResourceSpec.link`).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RowLink {
|
||||
pub to: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl RowLink {
|
||||
/// The link for one record: `{id}` is the record's id, `{name}`
|
||||
/// any string field of its responses. A placeholder nothing fills
|
||||
/// stays as it is, which is visible, rather than a wrong page.
|
||||
pub fn href(&self, id: &str, responses: &serde_json::Value) -> String {
|
||||
let mut out = self.to.replace("{id}", id);
|
||||
if let Some(fields) = responses.as_object() {
|
||||
for (k, v) in fields {
|
||||
if let Some(s) = v.as_str() {
|
||||
out = out.replace(&format!("{{{k}}}"), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct FieldIs {
|
||||
pub field: String,
|
||||
@@ -1790,6 +1854,26 @@ alternatives:
|
||||
assert_eq!(render_inline_markdown("a\nb"), "a b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_signed_in_person_is_not_asked_who_they_are() {
|
||||
let yaml = "id: /oppslag/[key]\nname: P\nalternatives:\n - name: A\n features:\n - name: F\n requirements:\n - name: email\n type: email\n value: \"{user.email}\"\n - name: navn\n value: \"{user.name}\"\n - name: hva\n type: textarea\n value: \"{user.email} says\"\n";
|
||||
let q: Question = serde_yaml::from_str(yaml).unwrap();
|
||||
let user = crate::auth::User { sub: "s".into(), username: "kari".into(), display_name: "Kari N".into(), groups: vec![], email: Some("kari@vel.no".into()) };
|
||||
let mut signed_in = q.clone();
|
||||
fill_from_user(&mut signed_in, Some(&user));
|
||||
let reqs = &signed_in.alternatives[0].features[0].requirements;
|
||||
assert_eq!((reqs[0].kind.as_str(), reqs[0].value.as_deref()), ("hidden", Some("kari@vel.no")));
|
||||
assert_eq!((reqs[1].kind.as_str(), reqs[1].value.as_deref()), ("hidden", Some("Kari N")));
|
||||
// Mixed text is not a user field, and is left as written.
|
||||
assert_eq!(reqs[2].value.as_deref(), Some("{user.email} says"));
|
||||
// A visitor is asked: the placeholder goes, the field stays.
|
||||
let mut visitor = q.clone();
|
||||
fill_from_user(&mut visitor, None);
|
||||
let reqs = &visitor.alternatives[0].features[0].requirements;
|
||||
assert_eq!((reqs[0].kind.as_str(), reqs[0].value.as_deref()), ("email", None));
|
||||
assert_eq!((reqs[1].kind.as_str(), reqs[1].value.as_deref()), ("text", None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_drops_html_and_unsafe_links() {
|
||||
assert_eq!(render_inline_markdown("x <script>y</script> z"), "x y z");
|
||||
|
||||
Reference in New Issue
Block a user