working quote submission (with limits for now)
- must have two lines - can only submit timestamp in system timezone
This commit is contained in:
39
Cargo.lock
generated
39
Cargo.lock
generated
@@ -185,6 +185,31 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-extra"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"axum-core",
|
||||||
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"serde_html_form",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base32"
|
name = "base32"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -934,6 +959,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"axum",
|
"axum",
|
||||||
|
"axum-extra",
|
||||||
"base32",
|
"base32",
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -1289,6 +1315,19 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_html_form"
|
||||||
|
version = "0.2.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.149"
|
version = "1.0.149"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
axum = "0.8.8"
|
axum = "0.8.8"
|
||||||
|
axum-extra = { version = "0.12.5", features = ["form"] }
|
||||||
base32 = "0.5.1"
|
base32 = "0.5.1"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ pub fn pages() -> Router {
|
|||||||
.route("/persons", get(persons::page))
|
.route("/persons", get(persons::page))
|
||||||
.route("/persons/create", post(persons::create))
|
.route("/persons/create", post(persons::create))
|
||||||
.route("/logs", get(logs::page))
|
.route("/logs", get(logs::page))
|
||||||
|
//
|
||||||
.route("/quotes", get(quotes::page))
|
.route("/quotes", get(quotes::page))
|
||||||
.route("/quotes/add", get(quotes::add::page))
|
.route("/quotes/add", get(quotes::add::page))
|
||||||
|
.route("/quotes/add-form", post(quotes::add::form))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn base(title: &str, inner: Markup) -> Markup {
|
pub fn base(title: &str, inner: Markup) -> Markup {
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
|
Json,
|
||||||
extract::Request,
|
extract::Request,
|
||||||
|
http::HeaderMap,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
use axum_extra::extract::Form;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use chrono_tz::Europe::Warsaw;
|
||||||
use maud::{PreEscaped, html};
|
use maud::{PreEscaped, html};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database,
|
database,
|
||||||
error::CompositeError,
|
error::CompositeError,
|
||||||
|
logs::{LogAction, LogEntry},
|
||||||
persons::Name,
|
persons::Name,
|
||||||
users::{User, auth::UserAuthenticate},
|
quotes::Quote,
|
||||||
|
users::{
|
||||||
|
User,
|
||||||
|
auth::{UserAuthRequired, UserAuthenticate},
|
||||||
|
},
|
||||||
web::{components::nav::nav, icons, pages::base},
|
web::{components::nav::nav, icons, pages::base},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -29,7 +41,8 @@ pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
|||||||
span class="text-2xl font-semibold font-lora" {"Quote Maker"}
|
span class="text-2xl font-semibold font-lora" {"Quote Maker"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div class="border border-neutral-200/25 bg-neutral-200/5 rounded-md p-4 flex flex-col" {
|
form method="post" action="/quotes/add-form"
|
||||||
|
class="border border-neutral-200/25 bg-neutral-200/5 rounded-md p-4 flex flex-col" {
|
||||||
@for i in 1..=2 {
|
@for i in 1..=2 {
|
||||||
div class="flex justify-between gap-4" {
|
div class="flex justify-between gap-4" {
|
||||||
div class="flex flex-col flex-1" {
|
div class="flex flex-col flex-1" {
|
||||||
@@ -38,10 +51,6 @@ pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
|||||||
input type="text" name="quoteline" placeholder="They said..." autocomplete="off"
|
input type="text" name="quoteline" placeholder="They said..." autocomplete="off"
|
||||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||||
}
|
}
|
||||||
// label for=(format!("line-{i}")) class="mb-1" {(format!("Quote Line #{i}"))}
|
|
||||||
// input type="text" id=(format!("line-{i}")) name=(format!("line-{i}"))
|
|
||||||
// placeholder=(format!("They said...")) autocomplete="off"
|
|
||||||
// class="px-2 py-1 mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
|
||||||
}
|
}
|
||||||
div class="flex flex-col" {
|
div class="flex flex-col" {
|
||||||
label {
|
label {
|
||||||
@@ -50,18 +59,10 @@ pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
|||||||
class="px-2 py-1.5 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"{
|
class="px-2 py-1.5 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25"{
|
||||||
option {"--"}
|
option {"--"}
|
||||||
@for name in &names {
|
@for name in &names {
|
||||||
option {(name.name)}
|
option value=(name.id.to_string()) {(name.name)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// label for=(format!("who-{i}")) class="mb-1" {(format!("Quote Author #{i}"))}
|
|
||||||
// select id=(format!("line-{i}")) name=(format!("line-{i}")) autocomplete="off"
|
|
||||||
// class="px-2 py-1.5 mb-2 bg-neutral-950/50 rounded border border-neutral-200/25" {
|
|
||||||
// option {"--"}
|
|
||||||
// @for name in &names {
|
|
||||||
// option {(name.name)}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +78,9 @@ pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
|||||||
div class="flex flex-col flex-1" {
|
div class="flex flex-col flex-1" {
|
||||||
label class="w-full" {
|
label class="w-full" {
|
||||||
p class="mb-1" {"Time of utterance"}
|
p class="mb-1" {"Time of utterance"}
|
||||||
input type="text" name="time" autocomplete="off" placeholder="2026-04-05T01:14:05+02:00"
|
input type="hidden" name="time" id="time_hidden";
|
||||||
|
input type="datetime-local" autocomplete="off"
|
||||||
|
onchange="document.getElementById('time_hidden').value = new Date(this.value).toISOString()"
|
||||||
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
class="px-2 py-1 w-full mb-2 bg-neutral-950/50 rounded border border-neutral-200/25";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,3 +103,44 @@ pub async fn page(req: Request) -> Result<Response, CompositeError> {
|
|||||||
)
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct IncomingQuote {
|
||||||
|
#[serde(rename = "quoteline")]
|
||||||
|
lines: Vec<String>,
|
||||||
|
#[serde(rename = "quoteauthor")]
|
||||||
|
authors: Vec<Uuid>,
|
||||||
|
location: String,
|
||||||
|
time: String,
|
||||||
|
context: String,
|
||||||
|
}
|
||||||
|
pub async fn form(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Form(form): Form<IncomingQuote>,
|
||||||
|
) -> Result<Response, CompositeError> {
|
||||||
|
let u = User::authenticate(&headers)?.required()?;
|
||||||
|
let mut conn = database::conn()?;
|
||||||
|
let tx = conn.transaction()?;
|
||||||
|
|
||||||
|
let authors = form
|
||||||
|
.authors
|
||||||
|
.into_iter()
|
||||||
|
.map(|nid| Name::get_by_id(&tx, nid).unwrap());
|
||||||
|
let lines = form.lines.into_iter().zip(authors).collect();
|
||||||
|
let timestamp = DateTime::parse_from_rfc3339(&form.time)
|
||||||
|
.unwrap_or_else(|_| Utc::now().with_timezone(&Warsaw).fixed_offset());
|
||||||
|
let context = match form.context.trim() {
|
||||||
|
"" => None,
|
||||||
|
s => Some(s.to_string()),
|
||||||
|
};
|
||||||
|
let location = match form.location.trim() {
|
||||||
|
"" => None,
|
||||||
|
s => Some(s.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let q = Quote::create(&tx, lines, timestamp, context, location, u.id, false)?;
|
||||||
|
LogEntry::new(&tx, u, LogAction::CreateQuote { id: q.id })?;
|
||||||
|
tx.commit()?;
|
||||||
|
|
||||||
|
Ok(Json(q).into_response())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user