use std::collections::HashMap;
use std::fmt::Write as _;
use crate::core::{inventory, FieldSchema, Join, ModelEntry, ModelSchema, Relation};
use crate::sql::sqlx;
use super::render;
use super::templates::render_template;
use super::urls::AppState;
pub(crate) type FkMap = HashMap<(String, String), String>;
pub(crate) fn lookup_model(state: &AppState, table: &str) -> Option<&'static ModelSchema> {
if !state.is_visible(table) {
return None;
}
inventory::iter::<ModelEntry>
.into_iter()
.find(|e| e.schema.table == table)
.map(|e| e.schema)
}
pub(crate) fn build_fk_joins(state: &AppState, model: &'static ModelSchema) -> Vec<Join> {
let mut joins = Vec::new();
for field in model.scalar_fields() {
let Some(rel) = field.relation else { continue };
let (to, on) = match rel {
Relation::Fk { to, on } | Relation::O2O { to, on } => (to, on),
Relation::M2M { .. } => continue,
};
let Some(target) = lookup_model(state, to) else {
continue;
};
let Some(display_field) = target.display_field() else {
continue;
};
joins.push(Join {
target,
on_local: field.column,
on_remote: on,
alias: field.name,
project: vec![display_field.column],
});
}
joins
}
pub(crate) fn fk_map_from_joined_rows(
state: &AppState,
model: &'static ModelSchema,
rows: &[sqlx::postgres::PgRow],
) -> FkMap {
let mut map: FkMap = HashMap::new();
for field in model.scalar_fields() {
let Some(rel) = field.relation else { continue };
let to = match rel {
Relation::Fk { to, .. } | Relation::O2O { to, .. } => to,
Relation::M2M { .. } => continue,
};
let Some(target) = lookup_model(state, to) else {
continue;
};
let Some(display_field) = target.display_field() else {
continue;
};
for row in rows {
let Some(source) = render::read_value_as_string(row, field) else {
continue;
};
let Some(display) = render::read_joined_value_as_html(row, field.name, display_field)
else {
continue;
};
map.insert((to.to_owned(), source), display);
}
}
map
}
pub(crate) fn pager_suffix(q: Option<&str>, filters: &[(&'static str, String)]) -> String {
let mut out = String::new();
if let Some(qs) = q {
out.push_str("&q=");
out.push_str(&url_encode(qs));
}
for (k, v) in filters {
out.push('&');
out.push_str(k);
out.push('=');
out.push_str(&url_encode(v));
}
out
}
fn url_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for byte in s.bytes() {
let safe = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~');
if safe {
out.push(byte as char);
} else {
let _ = write!(out, "%{byte:02X}");
}
}
out
}
pub(crate) fn render_cell(
row: &sqlx::postgres::PgRow,
field: &FieldSchema,
fk_map: &FkMap,
) -> String {
if let Some(rel) = field.relation {
let to = match rel {
Relation::Fk { to, .. } | Relation::O2O { to, .. } => Some(to),
Relation::M2M { .. } => None,
};
if let Some(to) = to {
let Some(raw_value) = render::read_value_as_string(row, field) else {
return "<em>NULL</em>".to_owned();
};
let raw_esc = render::escape(&raw_value);
let to_esc = render::escape(to);
return match fk_map.get(&(to.to_owned(), raw_value)) {
Some(display) => format!(r#"<a href="/{to_esc}/{raw_esc}">{display}</a>"#),
None => raw_esc,
};
}
}
render::render_value(row, field)
}
pub(crate) fn render_form(
model: &'static ModelSchema,
prefill: Option<&HashMap<String, String>>,
pk_locked: bool,
error_msg: Option<&str>,
) -> String {
let action = if pk_locked {
let pk_field = model.primary_key().expect("pk_locked requires a PK");
let pk_value = prefill
.and_then(|m| m.get(pk_field.name).cloned())
.unwrap_or_default();
format!("/{}/{}", model.table, render::escape(&pk_value))
} else {
format!("/{}", model.table)
};
let title = if pk_locked {
format!("Edit {}", model.name)
} else {
format!("New {}", model.name)
};
let rows_ctx: Vec<serde_json::Value> = model
.scalar_fields()
.map(|f| {
let value = prefill
.and_then(|m| m.get(f.name))
.map_or("", String::as_str);
let extra = if f.primary_key {
" <small>(pk)</small>"
} else if !f.nullable {
" <small>required</small>"
} else {
""
};
serde_json::json!({
"label": f.name,
"extra": extra,
"input": render::render_input(f, value, pk_locked),
})
})
.collect();
render_template(
"form.html",
&serde_json::json!({
"model": { "name": model.name, "table": model.table },
"title": title,
"action": action,
"error": error_msg,
"rows": rows_ctx,
}),
)
}