Post recipe form with tags
This commit is contained in:
parent
8e9849debb
commit
9aa52a3e73
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -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"
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user