122 lines
3.5 KiB
Rust
122 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())
|
||
|
|
}
|