diff --git a/Cargo.lock b/Cargo.lock index 02ac45d..c44015c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1173,7 +1173,10 @@ dependencies = [ "askama_actix", "chrono", "derive_more 1.0.0", + "encoding_rs", "entities 0.1.0", + "futures-core", + "futures-util", "humantime", "humantime-serde", "migration", @@ -1184,6 +1187,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", + "serde_qs", "tantivy", "tempfile", "thiserror", @@ -1558,9 +1562,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -4192,6 +4196,20 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "actix-web", + "futures", + "percent-encoding", + "serde", + "thiserror", + "tracing", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/cooked/Cargo.toml b/cooked/Cargo.toml index 0bf29b4..ca5d89d 100644 --- a/cooked/Cargo.toml +++ b/cooked/Cargo.toml @@ -33,6 +33,10 @@ thiserror = "1.0.65" actix-multipart = "0.7.2" tokio = { version = "1.41.0", features = ["full"] } actix-rt = "2.10.0" +serde_qs = { version = "0.13.0", features = ["actix4", "tracing"] } +futures-core = "0.3.31" +futures-util = "0.3.31" +encoding_rs = "0.8.35" [build-dependencies] diff --git a/cooked/src/filters.rs b/cooked/src/filters.rs index b3a5bef..fb3be73 100644 --- a/cooked/src/filters.rs +++ b/cooked/src/filters.rs @@ -1,3 +1,5 @@ +use std::collections::hash_map::Values; + use crate::types::Page; pub fn duration(sec: &&i32) -> ::askama::Result { @@ -16,6 +18,9 @@ pub fn page_class(pair: &(Page, Page)) -> ::askama::Result { .into()) } -pub fn is_checked((id, slice): &(i32, &[i32])) -> ::askama::Result { - Ok(slice.contains(&id)) +pub fn is_checked<'s>((id, slice): &(i32, &[String])) -> ::askama::Result { + Ok(slice + .iter() + .filter_map(|s| s.parse::().ok()) + .any(|s| s == *id)) } diff --git a/cooked/src/main.rs b/cooked/src/main.rs index c3b2f24..2b7198e 100644 --- a/cooked/src/main.rs +++ b/cooked/src/main.rs @@ -5,8 +5,11 @@ use actix_files::Files; use actix_identity::IdentityMiddleware; use actix_session::{storage::RedisSessionStore, SessionMiddleware}; use actix_web::cookie::Key; +use actix_web::{error, HttpResponse}; use actix_web::{web::Data, App, HttpServer}; use sea_orm::Database; +use serde_qs as qs; +use serde_qs::actix::QsQueryConfig; use std::str::FromStr; use types::Admins; @@ -119,6 +122,13 @@ async fn main() { let db = Data::new(db); let redis = Data::new(redis); + let qs_config = QsQueryConfig::default() + .error_handler(|err, req| { + // <- create custom error response + error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() + }) + .qs_config(qs::Config::new(10, false)); + HttpServer::new(move || { // App::new() @@ -133,6 +143,7 @@ async fn main() { .cookie_name(SESSION_KEY.to_string()) .build(), ) + .app_data(qs_config.clone()) .app_data(admins.clone()) .app_data(db.clone()) .app_data(redis.clone()) diff --git a/cooked/src/routes.rs b/cooked/src/routes.rs index 2f03a12..bd5a129 100644 --- a/cooked/src/routes.rs +++ b/cooked/src/routes.rs @@ -12,7 +12,8 @@ use askama::Template; use askama_actix::TemplateToResponse; use sea_orm::{prelude::*, DatabaseTransaction, QueryOrder, QuerySelect, TransactionTrait}; use serde::Deserialize; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use serde_qs::actix::QsForm; #[derive(Debug, Template, derive_more::Deref)] #[template(path = "recipe_card.jinja")] @@ -71,8 +72,7 @@ struct RecipeForm { ingeredients: String, steps: String, tags: String, - #[serde(default)] - selected_tags: Vec, + selected_tags: Vec, image_url: String, time: Option, author: Option, @@ -354,7 +354,7 @@ async fn recipe_form(admin: Identity, db: Data) -> RecipeFor ingeredients: "".into(), steps: "".into(), tags: "".into(), - selected_tags: Vec::new(), + selected_tags: Default::default(), image_url: "".into(), author: None, time: None, @@ -369,7 +369,8 @@ async fn recipe_form(admin: Identity, db: Data) -> RecipeFor #[post("/create")] async fn create_recipe( admin: Identity, - form: actix_web::web::Json, + // form: actix_web::web::Json, + form: QsForm, db: Data, ) -> Result { let mut form = form.into_inner(); @@ -446,6 +447,7 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result>(); let text_tags = form .tags diff --git a/cooked/templates/base.jinja b/cooked/templates/base.jinja index 9cfebb9..683a6cb 100644 --- a/cooked/templates/base.jinja +++ b/cooked/templates/base.jinja @@ -5,6 +5,7 @@ + Cooked {% block head %}{% endblock %} diff --git a/cooked/templates/recipies/create_form.jinja b/cooked/templates/recipies/create_form.jinja index 6043f58..11b6354 100644 --- a/cooked/templates/recipies/create_form.jinja +++ b/cooked/templates/recipies/create_form.jinja @@ -86,9 +86,9 @@ {{ tag.name }} {% match (tag.id, selected_tags.as_slice())|is_checked %} {% when true %} - + {% when false %} - + {% endmatch %} {% endfor %} @@ -121,23 +121,34 @@ document.addEventListener("DOMContentLoaded", (event) => { .then(({ url }) => image_url.value = url); }); const form = body.querySelector('form'); + return; form.addEventListener('submit', ev => { ev.preventDefault(); ev.stopPropagation(); const p = Array.from(form.elements).reduce((memo, el) => { const { name, value } = el; + console.log(name); if (name.endsWith('[]')) { - const v = memo[name.replace('[]', '')] || []; + const n = name.replace('[]', ''); + const v = memo[n] || []; v.push(value); - memo[name] = v; + memo[n] = v; } else { memo[name] = value; } return memo; }, {}); - fetch(form.action, { method: form.method, body: JSON.stringify(p), headers: { 'Content-Type': 'application/json' } }) - .then(res => this.location = res.headers.get('location')) - .catch(res => res.text().then(html => body.innerHTML = html )); + async function f() { + const res = fetch(form.action, { method: form.method, body: JSON.stringify(p), headers: { 'Content-Type': 'application/json' } }); + console.log(res); + if (res.ok) { + res => document.location = res.headers.get('location'); + } else { + const html = await res.text(); + body.innerHTML = html; + } + } + f(); }); });