From 4896223b838634ea8ebacb08505f68fc9e381d4d Mon Sep 17 00:00:00 2001 From: Joe Ardent Date: Sat, 1 Jul 2023 15:42:01 -0700 Subject: [PATCH] add a bunch of stuff for adding and seeing watches --- src/db_id.rs | 4 +- src/import_utils.rs | 97 ++++++++++++++++++++++++++++++ src/lib.rs | 6 +- src/signup.rs | 2 +- src/test_utils.rs | 2 +- src/watches/handlers.rs | 46 +++++++++----- src/watches/mod.rs | 20 ++++-- templates/get_watch_page.html | 2 +- templates/my_watches_page.html | 2 +- templates/search_watches_page.html | 2 +- 10 files changed, 153 insertions(+), 30 deletions(-) create mode 100644 src/import_utils.rs diff --git a/src/db_id.rs b/src/db_id.rs index a535dfc..0a18f0c 100644 --- a/src/db_id.rs +++ b/src/db_id.rs @@ -32,7 +32,7 @@ impl DbId { Self(Ulid::new()) } - pub fn from_str(s: &str) -> Result { + pub fn from_string(s: &str) -> Result { let id = Ulid::from_string(s)?; Ok(id.into()) } @@ -58,7 +58,7 @@ impl Display for DbId { impl Debug for DbId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("DbId").field(&self.bytes()).finish() + f.debug_tuple("DbId").field(&self.as_string()).finish() } } diff --git a/src/import_utils.rs b/src/import_utils.rs new file mode 100644 index 0000000..82ea9a5 --- /dev/null +++ b/src/import_utils.rs @@ -0,0 +1,97 @@ +use sqlx::{query, query_scalar, SqlitePool}; + +use crate::{db_id::DbId, Watch}; + +const USER_EXISTS_QUERY: &str = "select count(*) from witches where id = $1"; +const ADD_WATCH_QUERY: &str = "insert into watches (id, title, kind, metadata_url, length, release_date, added_by) values ($1, $2, $3, $4, $5, $6, $7)"; + +const OMEGA_ID: u128 = u128::MAX; + +//-************************************************************************ +// utility functions for building CLI tools, currently just for benchmarking +//-************************************************************************ +pub async fn add_watch(db_pool: &SqlitePool, watch: &Watch) { + if query(ADD_WATCH_QUERY) + .bind(watch.id) + .bind(&watch.title) + .bind(watch.kind) + .bind(&watch.metadata_url) + .bind(watch.length) + .bind(watch.release_date) + .bind(watch.added_by) + .execute(db_pool) + .await + .is_ok() + { + println!("{}", watch.id); + } else { + eprintln!("failed to add \"{}\"", watch.title); + } +} + +pub async fn add_user( + db_pool: &SqlitePool, + username: &str, + displayname: Option<&str>, + email: Option<&str>, + id: Option, +) { + let pwhash = "you shall not password"; + let id: DbId = id.unwrap_or_else(DbId::new); + if query(crate::signup::CREATE_QUERY) + .bind(id) + .bind(username) + .bind(displayname) + .bind(email) + .bind(pwhash) + .execute(db_pool) + .await + .is_ok() + { + println!("{id}"); + } else { + eprintln!("failed to add user \"{username}\""); + } +} + +pub async fn ensure_omega(db_pool: &SqlitePool) -> DbId { + if !check_omega_exists(db_pool).await { + add_user( + db_pool, + "The Omega User", + Some("I am the end of all watches."), + None, + Some(OMEGA_ID.into()), + ) + .await + } + OMEGA_ID.into() +} + +async fn check_omega_exists(db_pool: &SqlitePool) -> bool { + let id: DbId = OMEGA_ID.into(); + let count = query_scalar(USER_EXISTS_QUERY) + .bind(id) + .fetch_one(db_pool) + .await + .unwrap_or(0); + dbg!(count); + count > 0 +} + +//-************************************************************************ +//tests +//-************************************************************************ + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn ensure_omega_user() { + let p = crate::db::get_db_pool().await; + assert!(!check_omega_exists(&p).await); + ensure_omega(&p).await; + assert!(check_omega_exists(&p).await); + } +} diff --git a/src/lib.rs b/src/lib.rs index d6e89aa..5a9185a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,11 @@ extern crate justerror; #[cfg(test)] pub mod test_utils; -/// This is used in the bin crate and in tests. +/// Some public interfaces for interacting with the database outside of the web +/// app pub use db::get_db_pool; +pub use db_id::DbId; +pub mod import_utils; // everything else is private to the crate mod db; @@ -19,7 +22,6 @@ mod util; mod watches; // things we want in the crate namespace -use db_id::DbId; use optional_optional_user::OptionalOptionalUser; use templates::*; use users::User; diff --git a/src/signup.rs b/src/signup.rs index e586874..2e8352c 100644 --- a/src/signup.rs +++ b/src/signup.rs @@ -126,7 +126,7 @@ pub async fn get_signup_success( State(pool): State, ) -> Response { let id = id.trim(); - let id = DbId::from_str(id).unwrap_or_default(); + let id = DbId::from_string(id).unwrap_or_default(); let user: User = { query_as(ID_QUERY) .bind(id) diff --git a/src/test_utils.rs b/src/test_utils.rs index 079be4e..1f5d146 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -11,7 +11,7 @@ pub fn get_test_user() -> User { username: "test_user".to_string(), // corresponding to a password of "a": pwhash: "$argon2id$v=19$m=19456,t=2,p=1$GWsCH1w5RYaP9WWmq+xw0g$hmOEqC+MU+vnEk3bOdkoE+z01mOmmOeX08XyPyjqua8".to_string(), - id: DbId::from_str("00041061050R3GG28A1C60T3GF").unwrap(), + id: DbId::from_string("00041061050R3GG28A1C60T3GF").unwrap(), displayname: Some("Test User".to_string()), ..Default::default() } diff --git a/src/watches/handlers.rs b/src/watches/handlers.rs index 43e9403..beb101f 100644 --- a/src/watches/handlers.rs +++ b/src/watches/handlers.rs @@ -80,8 +80,10 @@ pub struct PostAddNewWatch { pub title: String, #[serde(default)] pub private: bool, - pub kind: Option, + pub kind: ShowKind, + #[serde(default, deserialize_with = "empty_string_as_none")] pub year: Option, // need a date-picker or something + #[serde(default, deserialize_with = "empty_string_as_none")] pub metadata_url: Option, #[serde(default)] pub watched_already: bool, @@ -114,6 +116,14 @@ pub async fn post_add_new_watch( { let watch_id = DbId::new(); let witch_watch_id = DbId::new(); + let release_date = form.year.map(|year| match year.trim().parse::() { + Ok(year) => { + let years = (year - 1970) as i64; + let days = (years as f32 * 365.2425) as i64; + Some(days * 24 * 60 * 60) + } + Err(_) => None, + }); let mut tx = pool .begin() .await @@ -122,7 +132,7 @@ pub async fn post_add_new_watch( .bind(watch_id) .bind(&form.title) .bind(form.kind) - .bind(&form.year) + .bind(release_date) .bind(form.metadata_url) .bind(user.id) .execute(&mut tx) @@ -148,8 +158,7 @@ pub async fn post_add_new_watch( tracing::error!("Got error: {err}"); WatchAddErrorKind::UnknownDBError })?; - let id = watch_id.to_string(); - let location = format!("/watch/{id}"); + let location = format!("/watch/{watch_id}"); Ok(Redirect::to(&location)) } } else { @@ -177,12 +186,9 @@ pub async fn get_watch( "".to_string() }; let id = id.trim(); - let id = DbId::from_str(id).unwrap_or_default(); - let watch: Option = query_as(GET_WATCH_QUERY) - .bind(id) - .fetch_one(&pool) - .await - .ok(); + let id = DbId::from_string(id).unwrap_or_default(); + let q = query_as(GET_WATCH_QUERY).bind(id); + let watch: Option = q.fetch_one(&pool).await.ok(); GetWatchPage { watch, @@ -213,22 +219,30 @@ pub async fn get_search_watch( ) -> impl IntoResponse { let user = auth.current_user; - let search = if search.0 != EMPTY_SEARCH_QUERY_STRUCT { + let (search_string, qstring) = if search.0 != EMPTY_SEARCH_QUERY_STRUCT { let s = search.0; - format!("{s:?}") + let q = if let Some(title) = &s.title { + title + } else if let Some(search) = &s.search { + search + } else { + "" + }; + (format!("{s:?}"), format!("%{}%", q.trim())) } else { - "".to_string() + ("".to_string(), "%".to_string()) }; // until tantivy search - let watches: Vec = query_as("select * from watches") + let watches: Vec = query_as("select * from watches where title like ?") + .bind(&qstring) .fetch_all(&pool) .await - .unwrap_or_default(); + .unwrap(); SearchWatchesPage { watches, user, - search, + search: search_string, } } diff --git a/src/watches/mod.rs b/src/watches/mod.rs index 915c69c..abdb92b 100644 --- a/src/watches/mod.rs +++ b/src/watches/mod.rs @@ -8,7 +8,7 @@ pub mod templates; #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, sqlx::Type, )] -#[repr(i64)] +#[repr(i32)] pub enum ShowKind { Movie = 0, Series = 1, @@ -67,12 +67,22 @@ impl From for ShowKind { )] pub struct Watch { pub id: DbId, - pub kind: ShowKind, pub title: String, + pub kind: ShowKind, pub metadata_url: Option, pub length: Option, pub release_date: Option, - added_by: DbId, // this shouldn't be exposed to randos - created_at: i64, - last_updated: i64, + pub added_by: DbId, // this shouldn't be exposed to randos in the application +} + +impl Watch { + pub fn year(&self) -> Option { + if let Some(year) = self.release_date { + let date = chrono::NaiveDateTime::from_timestamp_opt(year, 0)?; + let year = format!("{}", date.format("%Y")); + Some(year) + } else { + None + } + } } diff --git a/templates/get_watch_page.html b/templates/get_watch_page.html index 4af344f..71dee07 100644 --- a/templates/get_watch_page.html +++ b/templates/get_watch_page.html @@ -12,7 +12,7 @@ {% when Some with (watch) %}
- {{watch.title}} -- {% call m::get_or_default(watch.release_date, "when??") %} + {{watch.title}} -- {% call m::get_or_default(watch.year(), "when??") %}
{% else %} diff --git a/templates/my_watches_page.html b/templates/my_watches_page.html index 4222a7b..894db5a 100644 --- a/templates/my_watches_page.html +++ b/templates/my_watches_page.html @@ -17,7 +17,7 @@ Hello, {{ usr.username }}! It's nice to see you.
    {% for watch in watches %} -
  • {{watch.title}} -- {% call m::get_or_default(watch.release_date, "when??") %}:
  • +
  • {{watch.title}} -- {% call m::get_or_default(watch.year(), "when??") %}:
  • {% endfor %}
diff --git a/templates/search_watches_page.html b/templates/search_watches_page.html index fe3bb8e..37b03b1 100644 --- a/templates/search_watches_page.html +++ b/templates/search_watches_page.html @@ -12,7 +12,7 @@
    {% for watch in watches %} -
  • {{watch.title}} -- {% call m::get_or_default(watch.release_date, "when??") %}:
  • +
  • {{watch.title}} -- {% call m::get_or_default(watch.year(), "when??") -%}: {{watch.kind}}
  • {% endfor %}