aboutsummaryrefslogtreecommitdiff
path: root/src/bin/add-student-x500s.rs
diff options
context:
space:
mode:
authorNathan Ringo <nathan@remexre.com>2024-01-16 00:57:41 -0600
committerNathan Ringo <nathan@remexre.com>2024-01-16 00:57:41 -0600
commited778ab2060c6131caf98231a97873d7ea490d5a (patch)
treebfa6ceca8fe2e209562c1e995c598d80be0e4501 /src/bin/add-student-x500s.rs
parent54f497163f57dacd8d621a2a3c89e1f06ac370d0 (diff)
The start of database functionality.
Diffstat (limited to 'src/bin/add-student-x500s.rs')
-rw-r--r--src/bin/add-student-x500s.rs144
1 files changed, 144 insertions, 0 deletions
diff --git a/src/bin/add-student-x500s.rs b/src/bin/add-student-x500s.rs
new file mode 100644
index 0000000..14023b1
--- /dev/null
+++ b/src/bin/add-student-x500s.rs
@@ -0,0 +1,144 @@
+use anyhow::{bail, Context, Result};
+use clap::{value_parser, ArgAction, Parser};
+use futures::{stream, FutureExt, StreamExt};
+use lambo::config::Config;
+use sqlx::sqlite::SqlitePoolOptions;
+use std::{fs, path::PathBuf};
+use stderrlog::StdErrLog;
+
+#[derive(Debug, Parser)]
+struct Args {
+ /// The path to the lambo configuration file.
+ config_path: PathBuf,
+
+ /// The path to the CSV file, as exported from Canvas's gradebook.
+ csv_path: PathBuf,
+
+ /// Decreases the log level.
+ #[clap(
+ short,
+ long,
+ conflicts_with("verbose"),
+ action = ArgAction::Count,
+ value_parser = value_parser!(u8).range(..=2)
+ )]
+ quiet: u8,
+
+ /// Increases the log level.
+ #[clap(
+ short,
+ long,
+ conflicts_with("quiet"),
+ action = ArgAction::Count,
+ value_parser = value_parser!(u8).range(..=3)
+ )]
+ verbose: u8,
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+ // Parse the arguments.
+ let args = Args::parse();
+
+ // Set up logging.
+ {
+ let mut logger = StdErrLog::new();
+ match args.quiet {
+ 0 => logger.verbosity(1 + args.verbose as usize),
+ 1 => logger.verbosity(0),
+ 2 => logger.quiet(true),
+ // UNREACHABLE: A maximum of two occurrences of quiet are allowed.
+ _ => unreachable!(),
+ };
+ // UNWRAP: No other logger should be set up.
+ logger.show_module_names(true).init().unwrap()
+ }
+
+ // Parse the config file.
+ let config = Config::read_from_file(&args.config_path)?;
+
+ // Connect to the database.
+ let db = SqlitePoolOptions::new()
+ .connect(&config.database_url)
+ .await
+ .with_context(|| format!("failed to connect to database at {:?}", config.database_url))?;
+
+ // Run any necessary migrations.
+ sqlx::migrate!().run(&db).await.with_context(|| {
+ format!(
+ "failed to run migrations on database at {:?}",
+ config.database_url
+ )
+ })?;
+
+ // Read the CSV file.
+ let csv_string = fs::read_to_string(&args.csv_path)
+ .with_context(|| format!("failed to read {:?}", args.csv_path))?;
+
+ // Skip the first two lines; Canvas puts two lines of headers in...
+ let (i, _) = csv_string
+ .match_indices('\n')
+ .nth(1)
+ .context("invalid CSV file (not enough lines)")?;
+ let csv_string = &csv_string[i..];
+
+ // Parse the CSV file.
+ let mut csv = csv::ReaderBuilder::new()
+ .has_headers(false)
+ .from_reader(csv_string.as_bytes())
+ .records()
+ .collect::<Result<Vec<_>, _>>()
+ .with_context(|| format!("failed to parse {:?}", args.csv_path))?;
+
+ // Remove the test student.
+ csv.retain(|record| &record[0] != "Student, Test");
+
+ // Collect all the X.500s, checking that they were of the right form.
+ let x500s = csv
+ .into_iter()
+ .map(|record| {
+ let email = &record[3];
+ if let Some(x500) = email.strip_suffix("@umn.edu") {
+ Ok(x500.to_string())
+ } else {
+ bail!("not a valid UMN email: {:?}", email)
+ }
+ })
+ .collect::<Result<Vec<_>>>()?;
+
+ // Insert them all in the database.
+ //
+ // Looks like sqlx doesn't actually have bulk insert? WTF?
+ //
+ // https://github.com/launchbadge/sqlx/issues/294
+ let db = &db;
+ let errors = stream::iter(x500s)
+ .map(|x500| async move {
+ sqlx::query!(
+ "INSERT OR IGNORE INTO student_x500s (x500) VALUES (?)",
+ x500
+ )
+ .execute(db)
+ .await
+ .context("failed to insert X.500s")
+ })
+ .filter_map(|future| future.map(|r| r.err()))
+ .collect::<Vec<_>>()
+ .await;
+ if !errors.is_empty() {
+ log::error!("encountered {} errors:", errors.len());
+ for error in errors {
+ log::error!("{:?}", error);
+ }
+ bail!("failed to insert X.500s")
+ }
+
+ // Count the number of X.500s.
+ let x500_count = sqlx::query!("SELECT COUNT(x500) as count from student_x500s")
+ .fetch_one(db)
+ .await
+ .context("failed to get a count of X.500s")?;
+ log::info!("We now have {} student X.500s", x500_count.count);
+
+ Ok(())
+}