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",
"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"

View File

@ -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]

View File

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

View File

@ -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<i32>,
selected_tags: Vec<String>,
image_url: String,
time: Option<String>,
author: Option<String>,
@ -354,7 +354,7 @@ async fn recipe_form(admin: Identity, db: Data<DatabaseConnection>) -> 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<DatabaseConnection>) -> RecipeFor
#[post("/create")]
async fn create_recipe(
admin: Identity,
form: actix_web::web::Json<RecipeForm>,
// form: actix_web::web::Json<RecipeForm>,
form: QsForm<RecipeForm>,
db: Data<DatabaseConnection>,
) -> Result<HttpResponse, Error> {
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);
let selected_tags = form
.selected_tags;
let selected_tags = selected_tags.into_iter().filter_map(|s| s.parse().ok()).collect::<Vec<i32>>();
let text_tags = form
.tags

View File

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

View File

@ -86,9 +86,9 @@
<span>{{ tag.name }}</span>
{% match (tag.id, selected_tags.as_slice())|is_checked %}
{% 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 %}
<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 %}
</label>
{% 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();
});
});
</script>