improved htmlerror ergonomics

This commit is contained in:
Per Stark
2025-01-21 22:48:11 +01:00
parent c953dffe56
commit 1532ada09f
14 changed files with 126 additions and 85 deletions

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,7 @@ use zettle_db::{
documentation::index::show_documentation_index,
gdpr::{accept_gdpr, deny_gdpr},
index::index_handler,
ingress::{process_ingress_form, show_ingress_form},
ingress_form::{process_ingress_form, show_ingress_form},
ingress_tasks::{delete_task, show_queue_tasks},
privacy_policy::show_privacy_policy,
search_result::search_result_handler,
@@ -175,7 +175,7 @@ fn html_routes(
.route("/signout", get(sign_out_user))
.route("/signin", get(show_signin_form).post(authenticate_user))
.route(
"/ingress",
"/ingress-form",
get(show_ingress_form).post(process_ingress_form),
)
.route("/queue", get(show_queue_tasks))

View File

@@ -109,6 +109,26 @@ impl IntoResponse for ApiError {
(status, Json(body)).into_response()
}
}
pub type TemplateResult<T> = Result<T, HtmlError>;
// Helper trait for converting to HtmlError with templates
pub trait IntoHtmlError {
fn with_template(self, templates: Arc<AutoReloader>) -> HtmlError;
}
// // Implement for AppError
impl IntoHtmlError for AppError {
fn with_template(self, templates: Arc<AutoReloader>) -> HtmlError {
HtmlError::new(self, templates)
}
}
// // Implement for minijinja::Error directly
impl IntoHtmlError for minijinja::Error {
fn with_template(self, templates: Arc<AutoReloader>) -> HtmlError {
HtmlError::from_template_error(self, templates)
}
}
#[derive(Clone)]
pub struct ErrorContext {
#[allow(dead_code)]
@@ -129,7 +149,6 @@ pub enum HtmlError {
Template(String, Arc<AutoReloader>),
}
// Implement From<ApiError> for HtmlError
impl HtmlError {
pub fn new(error: AppError, templates: Arc<AutoReloader>) -> Self {
match error {

View File

@@ -35,8 +35,7 @@ pub async fn show_account_page(
AccountData::template_name(),
AccountData { user },
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
)?;
Ok(output.into_response())
}
@@ -70,8 +69,7 @@ pub async fn set_api_key(
"api_key_section",
AccountData { user: updated_user },
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
)?;
Ok(output.into_response())
}

View File

@@ -24,8 +24,7 @@ pub async fn show_documentation_index(
user: auth.current_user,
},
state.templates.clone(),
)
.map_err(|e| HtmlError::from_template_error(e, state.templates.clone()))?;
)?;
Ok(output.into_response())
}

View File

@@ -6,7 +6,7 @@ use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
error::{AppError, HtmlError},
error::HtmlError,
page_data,
server::{routes::html::render_template, AppState},
storage::types::user::User,
@@ -57,8 +57,7 @@ pub async fn index_handler(
user: auth.current_user,
},
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
)?;
Ok(output.into_response())
}

View File

@@ -6,25 +6,20 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
use futures::{future::try_join_all, TryFutureExt};
use serde::Serialize;
use surrealdb::{engine::any::Any, Surreal};
use tempfile::NamedTempFile;
use tracing::info;
use crate::{
error::{AppError, HtmlError},
error::{AppError, HtmlError, IntoHtmlError},
ingress::types::ingress_input::{create_ingress_objects, IngressInput},
page_data,
server::AppState,
storage::types::{file_info::FileInfo, user::User},
};
use super::render_template;
#[derive(Serialize)]
struct PageData {
// name: String,
}
pub async fn show_ingress_form(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
@@ -33,8 +28,7 @@ pub async fn show_ingress_form(
return Ok(Redirect::to("/").into_response());
}
let output = render_template("ingress_form.html", PageData {}, state.templates.clone())
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
let output = render_template("ingress_form.html", {}, state.templates.clone())?;
Ok(output.into_response())
}
@@ -49,15 +43,47 @@ pub struct IngressParams {
pub files: Vec<FieldData<NamedTempFile>>,
}
page_data!(IngressFormData, "ingress_form.html", {
instructions: String,
content: String,
});
pub async fn process_ingress_form(
State(state): State<AppState>,
auth: AuthSession<User, String, SessionSurrealPool<Any>, Surreal<Any>>,
TypedMultipart(input): TypedMultipart<IngressParams>,
) -> Result<impl IntoResponse, HtmlError> {
let user = match auth.current_user {
Some(user) => user,
None => return Ok(Redirect::to("/").into_response()),
};
let user = auth.current_user.ok_or_else(|| {
AppError::Auth("You must be signed in".to_string()).with_template(state.templates.clone())
})?;
// let user = match auth.current_user {
// Some(user) => user,
// None => {
// return Err(HtmlError::new(
// AppError::Auth("You must be signed in".to_string()),
// state.templates,
// ))
// }
// };
if input.content.clone().is_some_and(|c| c.len() < 2) && input.files.is_empty() {
let output = render_template(
IngressFormData::template_name(),
IngressFormData {
instructions: input.instructions.clone(),
content: input.content.clone().unwrap(),
},
state.templates.clone(),
)?;
return Ok(output.into_response());
// return Ok((
// StatusCode::UNAUTHORIZED,
// Html("Invalid input, make sure you fill in either content or add files"),
// )
// .into_response());
}
info!("{:?}", input);
@@ -88,5 +114,8 @@ pub async fn process_ingress_form(
.map_err(AppError::from)
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
Ok(Html("SuccessBRO!").into_response())
Ok(Html(
"<a class='btn btn-primary' hx-get='/ingress-form' hx-swap='outerHTML'>Add Content</a>",
)
.into_response())
}

View File

@@ -1,5 +1,5 @@
use crate::{
error::{AppError, HtmlError},
error::HtmlError,
page_data,
server::AppState,
storage::types::{job::Job, user::User},
@@ -35,8 +35,7 @@ pub async fn show_queue_tasks(
ShowQueueTasks::template_name(),
ShowQueueTasks { jobs, user },
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
)?;
Ok(rendered.into_response())
}

View File

@@ -3,11 +3,13 @@ use std::sync::Arc;
use axum::response::Html;
use minijinja_autoreload::AutoReloader;
use crate::error::{HtmlError, IntoHtmlError};
pub mod account;
pub mod documentation;
pub mod gdpr;
pub mod index;
pub mod ingress;
pub mod ingress_form;
pub mod ingress_tasks;
pub mod privacy_policy;
pub mod search_result;
@@ -19,21 +21,26 @@ pub trait PageData {
fn template_name() -> &'static str;
}
// Helper function for render_template
pub fn render_template<T>(
template_name: &str,
context: T,
templates: Arc<AutoReloader>,
) -> Result<Html<String>, minijinja::Error>
) -> Result<Html<String>, HtmlError>
where
T: serde::Serialize,
{
let env = templates.acquire_env()?;
let tmpl = env.get_template(template_name)?;
let env = templates
.acquire_env()
.map_err(|e| e.with_template(templates.clone()))?;
let tmpl = env
.get_template(template_name)
.map_err(|e| e.with_template(templates.clone()))?;
let context = minijinja::Value::from_serialize(&context);
let output = tmpl.render(context)?;
Ok(output.into())
let output = tmpl
.render(context)
.map_err(|e| e.with_template(templates.clone()))?;
Ok(Html(output))
}
pub fn render_block<T>(
@@ -41,15 +48,23 @@ pub fn render_block<T>(
block: &str,
context: T,
templates: Arc<AutoReloader>,
) -> Result<Html<String>, minijinja::Error>
) -> Result<Html<String>, HtmlError>
where
T: serde::Serialize,
{
let env = templates.acquire_env()?;
let tmpl = env.get_template(template_name)?;
let env = templates
.acquire_env()
.map_err(|e| e.with_template(templates.clone()))?;
let tmpl = env
.get_template(template_name)
.map_err(|e| e.with_template(templates.clone()))?;
let context = minijinja::Value::from_serialize(&context);
let output = tmpl.eval_to_state(context)?.render_block(block)?;
let output = tmpl
.eval_to_state(context)
.map_err(|e| e.with_template(templates.clone()))?
.render_block(block)
.map_err(|e| e.with_template(templates.clone()))?;
Ok(output.into())
}

View File

@@ -24,8 +24,7 @@ pub async fn show_privacy_policy(
user: auth.current_user,
},
state.templates.clone(),
)
.map_err(|e| HtmlError::from_template_error(e, state.templates.clone()))?;
)?;
Ok(output.into_response())
}

View File

@@ -9,12 +9,7 @@ use axum_session_auth::AuthSession;
use axum_session_surreal::SessionSurrealPool;
use surrealdb::{engine::any::Any, Surreal};
use crate::{
error::{AppError, HtmlError},
page_data,
server::AppState,
storage::types::user::User,
};
use crate::{error::HtmlError, page_data, server::AppState, storage::types::user::User};
use super::{render_block, render_template};
@@ -41,14 +36,12 @@ pub async fn show_signin_form(
"body",
ShowSignInForm {},
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?,
)?,
false => render_template(
ShowSignInForm::template_name(),
ShowSignInForm {},
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?,
)?,
};
Ok(output.into_response())

View File

@@ -11,11 +11,7 @@ use serde::{Deserialize, Serialize};
use surrealdb::{engine::any::Any, Surreal};
use tracing::info;
use crate::{
error::{AppError, HtmlError},
server::AppState,
storage::types::user::User,
};
use crate::{error::HtmlError, server::AppState, storage::types::user::User};
use super::{render_block, render_template};
@@ -44,14 +40,12 @@ pub async fn show_signup_form(
"body",
PageData {},
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?,
)?,
false => render_template(
"auth/signup_form.html",
PageData {},
state.templates.clone(),
)
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?,
)?,
};
Ok(output.into_response())

View File

@@ -19,11 +19,11 @@
</div>
<!-- Quick Actions - Modified button to link to ingress form -->
<div class="card bg-base-100 shadow-xl">
<div class="card bg-base-100 shadow-xl mt-4">
<div class="card-body">
<h2 class="card-title">Quick Actions</h2>
<div class="flex gap-4">
<a class="btn btn-primary" href="/ingress" hx-boost="true">Add Content</a>
<a class="btn btn-primary" hx-get="/ingress-form" hx-swap="outerHTML">Add Content</a>
</div>
</div>
</div>

View File

@@ -1,22 +1,19 @@
{% extends "body_base.html" %}
{% block main %}
<div class="flex justify-center grow mt-2 sm:mt-4">
<div class="container">
<div class="card">
<form class="space-y-2" hx-post="/ingress-form" enctype="multipart/form-data">
<h1 class="text-2xl">Add content to the database </h1>
<form class="space-y-2" hx-post="/ingress" enctype="multipart/form-data">
<label class="label label-text">Instructions</label>
<textarea name="instructions" class="textarea w-full input-bordered"
placeholder="Enter instructions for the AI here, help it understand what its seeing or how it should relate to the database"></textarea>
<label class="label label-text">Content (optional)</label>
<textarea name="content" class="textarea w-full input-bordered" placeholder="Additional content"></textarea>
<label class="label label-text">Category</label>
<input type="text" name="category" class="input input-bordered" placeholder="Category for ingress">
<label class="label label-text">Instructions</label>
<textarea name="instructions" class="textarea w-full input-bordered"
placeholder="Enter instructions for the AI here, help it understand what its seeing or how it should relate to the database">{{instructions
}}</textarea>
<label class="label label-text">Content (optional)</label>
<textarea name="content" class="textarea w-full input-bordered" placeholder="Additional content">{{content
}}</textarea>
<label class="label label-text">Category</label>
<input type="text" name="category" class="input input-bordered" placeholder="Category for ingress">
<label class="label label-text">Files</label>
<input type="file" name="files" multiple class="file-input file-input-bordered" />
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div id="ingress-result"></div>
</div>
</div>
{% endblock %}
<label class="label label-text">Files</label>
<input type="file" name="files" multiple class="file-input file-input-bordered" />
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<div id="ingress-result"></div>
</div>