From 7cc90d5b07d54184be1de0a37e38c32640608360 Mon Sep 17 00:00:00 2001 From: cos Date: Thu, 30 May 2024 13:30:27 +0200 Subject: Complete waitlist support, used for meetup#47 --- README.md | 42 +++++++++++++++++++++++ src/main.rs | 112 +++++++++++++++++++++++++++++++++--------------------------- 2 files changed, 104 insertions(+), 50 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..00674ff --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +cph.rs attendee +=============== +Creates an A4 tick-off list from a meetup.com organizer export. + + • Generates one file with attendees, and one with those who are waitlisted. + +Used by cph.rs to take attendance on dead-tree as people arrive. + +Prerequisites +------------- +Rust and stuff, of course, and the csv (called xls) export reachable through: + + meetup.com → [event] → # Attendees + Manage → Manage Attendees → Attendee details + +One might also need some tool to convert to a printable file, unless having a +printer which understands the Scalable Vector Graphics format. + +Usage +----- +Simply call: + +``` + cphrs-attendance + inkscape --export-filename= + inkscape --export-filename= +``` + +Future work/Wishlist +-------------------- +Not implemented, but maybe possibly sometime? + + • Supporting also for post-event follow-up features. + • Detection and visualization of RSVP time and when someone got migrated from + the waiting list. + • Integration with [raffle](https://github.com/vanjacosic/raffle)? + • Tablet computer support, to skip the need for manually syncing hard-copies. + Unresearched Idea: Generation of pdf with forms for any iPad/android? + Unresearched Idea: Cursive front-end, for postmarketOS devices? + • OCR/AI support, to automate the hard-copy sync and introduce faults? + +Patches welcome! diff --git a/src/main.rs b/src/main.rs index b92a0eb..3a97efd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,31 +23,7 @@ use { const COL_WIDTH: f64 = 350.0; const ROWS: usize = 35; -fn main() -> Result<()> { - let first_arg = args().nth(1).ok_or_else(|| anyhow!("Missing meetup csv file argument."))?; - let fullpath = first_arg.clone(); - let filename = fullpath.split('/').last().unwrap_or(&fullpath); - let (basename, _) = filename.split_once('.') - .ok_or_else(|| anyhow!("Unexpected filename. Was it downloaded from meetup.com?"))?; - let (event_name, _) = filename.split_once("_sponsored") - .ok_or_else(|| anyhow!("Unexpected filename. Was it downloaded from meetup.com?"))?; - - let mut csv_file = File::open(first_arg)?; - let mut contents = String::new(); - csv_file.read_to_string(&mut contents)?; - - let mut attendees: Vec = vec![]; - let mut waitlisted: Vec = vec![]; - - for (n, row) in contents.split('\n').enumerate() { - let fields: Vec<&str> = row.split('\t').collect(); - match (fields.get(1), fields.get(4)) { - (Some(name), Some(rsvp)) => { }, - _ => eprintln!("Unparsable row {n}: {}", row), - // fields.get(5) "Waiting List" - } - } - +fn gimme_a_form(title: &str, attendees: Vec) -> Document { let page_height = "297mm"; let page_width = "210mm"; let x_crop = 60.047f64; @@ -59,7 +35,7 @@ fn main() -> Result<()> { let title_text = Text::new() .set("x", 1.2 * x_crop) .set("y", 1.2 * y_crop / 2.0) - .add(TextNode::new(event_name.chars().map(|c| if c == '_' { ' '} else { c }).collect::())) + .add(TextNode::new(title.chars().map(|c| if c == '_' { ' '} else { c }).collect::())) .set("style", format!("font-size:{fontsize}px;line-height:1.25;font-family:'{fontfamily}'")); let mut document = Document::new() @@ -69,35 +45,71 @@ fn main() -> Result<()> { .set("xmlns:inkscape", "http://www.inkscape.org/namespaces/inkscape") .add(title_text); - for (n, attendee) in contents.split('\n').enumerate() { - if let Some((name, _)) = attendee.split_once('\t') { - let row = n % ROWS; - let col = n / ROWS; + for (n, name) in attendees.iter().enumerate() { + let row = n % ROWS; + let col = n / ROWS; - let attendee_box = Rectangle::new() - .set("x", 1.2 * x_crop + col as f64 * COL_WIDTH) - .set("y", 1.2 * y_crop + row as f64 * fontsize * 1.2) - .set("style", "fill:#ffffff;stroke:#000000;fill-opacity=0.4;opacity=0.4") - .set("height", fontsize * 1.2) - .set("width", COL_WIDTH); + let attendee_box = Rectangle::new() + .set("x", 1.2 * x_crop + col as f64 * COL_WIDTH) + .set("y", 1.2 * y_crop + row as f64 * fontsize * 1.2) + .set("style", "fill:#ffffff;stroke:#000000;fill-opacity=0.4;opacity=0.4") + .set("height", fontsize * 1.2) + .set("width", COL_WIDTH); - let attendee_text = Text::new() - .set("x", 1.2 * x_crop + col as f64 * COL_WIDTH + 40.0) - .set("y", 1.2 * y_crop + fontsize + row as f64 * fontsize * 1.2) - .add(TextNode::new(name)) - .set("style", format!("font-size:{fontsize}px;line-height:1.25;font-family:'{fontfamily}'")); + let attendee_text = Text::new() + .set("x", 1.2 * x_crop + col as f64 * COL_WIDTH + 40.0) + .set("y", 1.2 * y_crop + fontsize + row as f64 * fontsize * 1.2) + .add(TextNode::new(name)) + .set("style", format!("font-size:{fontsize}px;line-height:1.25;font-family:'{fontfamily}'")); - document = document - .add(attendee_box) - .add(attendee_text); - } else { - if attendee.is_empty() { - continue; - } - return Err(anyhow!("Could not parse: '{attendee}'")); + document = document + .add(attendee_box) + .add(attendee_text); + } + + document +} + +fn main() -> Result<()> { + let first_arg = args().nth(1).ok_or_else(|| anyhow!("Missing meetup csv file argument."))?; + let fullpath = first_arg.clone(); + let filename = fullpath.split('/').last().unwrap_or(&fullpath); + let (basename, _) = filename.split_once('.') + .ok_or_else(|| anyhow!("Unexpected filename. Was it downloaded from meetup.com?"))?; + let (event_name, _) = filename.split_once("_sponsored") + .ok_or_else(|| anyhow!("Unexpected filename. Was it downloaded from meetup.com?"))?; + + let mut csv_file = File::open(first_arg)?; + let mut contents = String::new(); + csv_file.read_to_string(&mut contents)?; + + let mut attendees: Vec = vec![]; + let mut waitlisted: Vec = vec![]; + + for (n, row) in contents.split('\n').enumerate() { + let fields: Vec<&str> = row.split('\t').collect(); + match (fields.first(), fields.get(4)) { + (Some(name), Some(rsvp)) => { + match *rsvp { + "Waiting List" => waitlisted.push(name.to_string()), + "Yes" => attendees.push(name.to_string()), + _ => { + if n == 0 { + continue; + } else { + panic!("Unexpected rsvp on line {}: {rsvp}", n + 1); + } + } + } + }, + _ if row.is_empty() => { /* Empty lines are okay! */ } + _ => eprintln!("Unparsable row {}: '{}'", n + 1, row), } } + let attendee_document = gimme_a_form(event_name, attendees); + svg::save(format!("{basename}.svg"), &attendee_document).unwrap(); + let waitlist_document = gimme_a_form(&format!("{event_name} waiting list"), waitlisted); + svg::save(format!("{basename}-waitlist.svg"), &waitlist_document).unwrap(); - svg::save(format!("{basename}.svg"), &document).unwrap(); Ok(()) } -- cgit v1.2.3