Post recipe form with tags

This commit is contained in:
eraden 2024-11-03 17:39:20 +01:00
parent 8e9849debb
commit 9aa52a3e73
7 changed files with 68 additions and 16 deletions

22
Cargo.lock generated
View File

@ -1173,7 +1173,10 @@ dependencies = [
"askama_actix", "askama_actix",
"chrono", "chrono",
"derive_more 1.0.0", "derive_more 1.0.0",
"encoding_rs",
"entities 0.1.0", "entities 0.1.0",
"futures-core",
"futures-util",
"humantime", "humantime",
"humantime-serde", "humantime-serde",
"migration", "migration",
@ -1184,6 +1187,7 @@ dependencies = [
"sea-orm", "sea-orm",
"serde", "serde",
"serde_json", "serde_json",
"serde_qs",
"tantivy", "tantivy",
"tempfile", "tempfile",
"thiserror", "thiserror",
@ -1558,9 +1562,9 @@ dependencies = [
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.34" version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -4192,6 +4196,20 @@ dependencies = [
"serde", "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]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"

View File

@ -33,6 +33,10 @@ thiserror = "1.0.65"
actix-multipart = "0.7.2" actix-multipart = "0.7.2"
tokio = { version = "1.41.0", features = ["full"] } tokio = { version = "1.41.0", features = ["full"] }
actix-rt = "2.10.0" 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] [build-dependencies]

View File

@ -1,3 +1,5 @@
use std::collections::hash_map::Values;
use crate::types::Page; use crate::types::Page;
pub fn duration(sec: &&i32) -> ::askama::Result<String> { pub fn duration(sec: &&i32) -> ::askama::Result<String> {
@ -16,6 +18,9 @@ pub fn page_class(pair: &(Page, Page)) -> ::askama::Result<String> {
.into()) .into())
} }
pub fn is_checked((id, slice): &(i32, &[i32])) -> ::askama::Result<bool> { pub fn is_checked<'s>((id, slice): &(i32, &[String])) -> ::askama::Result<bool> {
Ok(slice.contains(&id)) Ok(slice
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.any(|s| s == *id))
} }

View File

@ -5,8 +5,11 @@ use actix_files::Files;
use actix_identity::IdentityMiddleware; use actix_identity::IdentityMiddleware;
use actix_session::{storage::RedisSessionStore, SessionMiddleware}; use actix_session::{storage::RedisSessionStore, SessionMiddleware};
use actix_web::cookie::Key; use actix_web::cookie::Key;
use actix_web::{error, HttpResponse};
use actix_web::{web::Data, App, HttpServer}; use actix_web::{web::Data, App, HttpServer};
use sea_orm::Database; use sea_orm::Database;
use serde_qs as qs;
use serde_qs::actix::QsQueryConfig;
use std::str::FromStr; use std::str::FromStr;
use types::Admins; use types::Admins;
@ -119,6 +122,13 @@ async fn main() {
let db = Data::new(db); let db = Data::new(db);
let redis = Data::new(redis); 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 || { HttpServer::new(move || {
// //
App::new() App::new()
@ -133,6 +143,7 @@ async fn main() {
.cookie_name(SESSION_KEY.to_string()) .cookie_name(SESSION_KEY.to_string())
.build(), .build(),
) )
.app_data(qs_config.clone())
.app_data(admins.clone()) .app_data(admins.clone())
.app_data(db.clone()) .app_data(db.clone())
.app_data(redis.clone()) .app_data(redis.clone())

View File

@ -12,7 +12,8 @@ use askama::Template;
use askama_actix::TemplateToResponse; use askama_actix::TemplateToResponse;
use sea_orm::{prelude::*, DatabaseTransaction, QueryOrder, QuerySelect, TransactionTrait}; use sea_orm::{prelude::*, DatabaseTransaction, QueryOrder, QuerySelect, TransactionTrait};
use serde::Deserialize; 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)] #[derive(Debug, Template, derive_more::Deref)]
#[template(path = "recipe_card.jinja")] #[template(path = "recipe_card.jinja")]
@ -71,8 +72,7 @@ struct RecipeForm {
ingeredients: String, ingeredients: String,
steps: String, steps: String,
tags: String, tags: String,
#[serde(default)] selected_tags: Vec<String>,
selected_tags: Vec<i32>,
image_url: String, image_url: String,
time: Option<String>, time: Option<String>,
author: Option<String>, author: Option<String>,
@ -354,7 +354,7 @@ async fn recipe_form(admin: Identity, db: Data<DatabaseConnection>) -> RecipeFor
ingeredients: "".into(), ingeredients: "".into(),
steps: "".into(), steps: "".into(),
tags: "".into(), tags: "".into(),
selected_tags: Vec::new(), selected_tags: Default::default(),
image_url: "".into(), image_url: "".into(),
author: None, author: None,
time: None, time: None,
@ -369,7 +369,8 @@ async fn recipe_form(admin: Identity, db: Data<DatabaseConnection>) -> RecipeFor
#[post("/create")] #[post("/create")]
async fn create_recipe( async fn create_recipe(
admin: Identity, admin: Identity,
form: actix_web::web::Json<RecipeForm>, // form: actix_web::web::Json<RecipeForm>,
form: QsForm<RecipeForm>,
db: Data<DatabaseConnection>, db: Data<DatabaseConnection>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let mut form = form.into_inner(); let mut form = form.into_inner();
@ -446,6 +447,7 @@ async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<i3
tracing::debug!("Selected tags: {:?}", form.selected_tags); tracing::debug!("Selected tags: {:?}", form.selected_tags);
let selected_tags = form let selected_tags = form
.selected_tags; .selected_tags;
let selected_tags = selected_tags.into_iter().filter_map(|s| s.parse().ok()).collect::<Vec<i32>>();
let text_tags = form let text_tags = form
.tags .tags

View File

@ -5,6 +5,7 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/styles.css"> <link rel="stylesheet" href="/styles.css">
<script src="https://unpkg.com/htmx.org@2.0.3"></script>
<title>Cooked</title> <title>Cooked</title>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>

View File

@ -86,9 +86,9 @@
<span>{{ tag.name }}</span> <span>{{ tag.name }}</span>
{% match (tag.id, selected_tags.as_slice())|is_checked %} {% match (tag.id, selected_tags.as_slice())|is_checked %}
{% when true %} {% when true %}
<input type="checkbox" name="selected_tags[]" value="{{tag.id}}" class="hidden" checked /> <input type="checkbox" name="selected_tags[]" value="{{tag.id}}" class="hidden" checked data-type="number[]" />
{% when false %} {% when false %}
<input type="checkbox" name="selected_tags[]" value="{{tag.id}}" class="hidden" /> <input type="checkbox" name="selected_tags[]" value="{{tag.id}}" class="hidden" data-type="number[]" />
{% endmatch %} {% endmatch %}
</label> </label>
{% endfor %} {% endfor %}
@ -121,23 +121,34 @@ document.addEventListener("DOMContentLoaded", (event) => {
.then(({ url }) => image_url.value = url); .then(({ url }) => image_url.value = url);
}); });
const form = body.querySelector('form'); const form = body.querySelector('form');
return;
form.addEventListener('submit', ev => { form.addEventListener('submit', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const p = Array.from(form.elements).reduce((memo, el) => { const p = Array.from(form.elements).reduce((memo, el) => {
const { name, value } = el; const { name, value } = el;
console.log(name);
if (name.endsWith('[]')) { if (name.endsWith('[]')) {
const v = memo[name.replace('[]', '')] || []; const n = name.replace('[]', '');
const v = memo[n] || [];
v.push(value); v.push(value);
memo[name] = v; memo[n] = v;
} else { } else {
memo[name] = value; memo[name] = value;
} }
return memo; return memo;
}, {}); }, {});
fetch(form.action, { method: form.method, body: JSON.stringify(p), headers: { 'Content-Type': 'application/json' } }) async function f() {
.then(res => this.location = res.headers.get('location')) const res = fetch(form.action, { method: form.method, body: JSON.stringify(p), headers: { 'Content-Type': 'application/json' } });
.catch(res => res.text().then(html => body.innerHTML = html )); console.log(res);
if (res.ok) {
res => document.location = res.headers.get('location');
} else {
const html = await res.text();
body.innerHTML = html;
}
}
f();
}); });
}); });
</script> </script>