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",
|
"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"
|
||||||
|
@ -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]
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user