summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md42
-rw-r--r--src/main.rs112
2 files changed, 104 insertions, 50 deletions
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 <meetup-export.xls>
+ inkscape --export-filename=<attendees.pdf> <meetup-export.svg>
+ inkscape --export-filename=<waitlist.pdf> <meetup-export-waitlist.svg>
+```
+
+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<String> = vec![];
- let mut waitlisted: Vec<String> = 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<String>) -> 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::<String>()))
+ .add(TextNode::new(title.chars().map(|c| if c == '_' { ' '} else { c }).collect::<String>()))
.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<String> = vec![];
+ let mut waitlisted: Vec<String> = 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(())
}