phonograph/interim-server/src/routes/forms/form_handler.rs
2025-10-09 08:01:01 +00:00

121 lines
3.5 KiB
Rust

use std::collections::HashMap;
use askama::Template;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse as _, Response},
};
use interim_models::{
field::Field,
field_form_prompt::FieldFormPrompt,
language::Language,
portal::Portal,
presentation::{Presentation, TextInputMode},
};
use interim_pgtypes::{pg_attribute::PgAttribute, pg_class::PgClass};
use serde::Deserialize;
use uuid::Uuid;
use crate::{
Settings,
app::AppDbConn,
errors::{AppError, not_found},
field_info::FormFieldInfo,
workspace_pooler::{RoleAssignment, WorkspacePooler},
};
#[derive(Debug, Deserialize)]
pub(super) struct PathParams {
portal_id: Uuid,
}
pub(super) async fn get(
State(settings): State<Settings>,
State(mut pooler): State<WorkspacePooler>,
AppDbConn(mut app_db): AppDbConn,
Path(PathParams { portal_id }): Path<PathParams>,
) -> Result<Response, AppError> {
let portal = Portal::with_id(portal_id)
.fetch_optional(&mut app_db)
.await?
.ok_or(not_found!("form not found"))?;
// FIXME: auth
// WARNING: This client is connected with full workspace privileges. Even
// more so than usual, the Phonograph server is responsible for ensuring all
// auth checks are performed properly.
//
// TODO: Can this be delegated to a dedicated and less privileged role
// instead?
let mut workspace_client = pooler
.acquire_for(portal.workspace_id, RoleAssignment::Root)
.await?;
let rel = PgClass::with_oid(portal.class_oid)
.fetch_one(&mut workspace_client)
.await?;
let attrs: HashMap<String, PgAttribute> = PgAttribute::all_for_rel(portal.class_oid)
.fetch_all(&mut workspace_client)
.await?
.into_iter()
.map(|value| (value.attname.clone(), value))
.collect();
// TODO: implement with sql join
let mut fields: Vec<FormFieldInfo> = vec![];
for field in Field::belonging_to_portal(portal_id)
.fetch_all(&mut app_db)
.await?
{
let attr = attrs.get(&field.name);
let prompts: HashMap<Language, String> = FieldFormPrompt::belonging_to_field(field.id)
.fetch_all(&mut app_db)
.await?
.into_iter()
.map(|prompt| (prompt.language, prompt.content))
.collect();
fields.push(FormFieldInfo {
field,
column_present: attr.is_some(),
has_default: attr.is_some_and(|value| value.atthasdef),
not_null: attr.is_some_and(|value| value.attnotnull.is_some_and(|notnull| notnull)),
prompts,
})
}
let mut prompts_html: HashMap<String, String> = HashMap::new();
for field in fields.iter() {
// TODO: i18n
let prompt = field
.prompts
.get(&Language::Eng)
.cloned()
.unwrap_or_default();
let prompt_md = markdown::to_html(&prompt);
// TODO: a11y (input labels)
prompts_html.insert(field.field.name.clone(), prompt_md);
}
#[derive(Debug, Template)]
#[template(path = "forms/form_index.html")]
struct ResponseTemplate {
fields: Vec<FormFieldInfo>,
language: Language,
portal: Portal,
prompts_html: HashMap<String, String>,
settings: Settings,
}
Ok(Html(
ResponseTemplate {
fields,
language: Language::Eng,
portal,
prompts_html,
settings,
}
.render()?,
)
.into_response())
}