use std::collections::HashMap; use askama::Template; use axum::{ debug_handler, extract::{Path, State}, response::{Html, IntoResponse}, }; use interim_models::{ field::Field, field_form_prompt::FieldFormPrompt, form_transition::FormTransition, language::Language, portal::Portal, workspace::Workspace, workspace_user_perm::{self, WorkspaceUserPerm}, }; use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; use sqlx::postgres::types::Oid; use strum::IntoEnumIterator as _; use uuid::Uuid; use crate::{ app::{App, AppDbConn}, errors::{AppError, forbidden}, field_info::FormFieldInfo, navigator::{Navigator, NavigatorPage as _}, settings::Settings, user::CurrentUser, workspace_nav::{NavLocation, RelLocation, WorkspaceNav}, workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_utils::{RelationPortalSet, fetch_all_accessible_portals}, }; #[derive(Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, rel_oid: u32, workspace_id: Uuid, } /// HTTP GET handler for the top-level portal form builder page. #[debug_handler(state = App)] pub(super) async fn get( State(settings): State, CurrentUser(user): CurrentUser, AppDbConn(mut app_db): AppDbConn, Path(PathParams { portal_id, rel_oid, workspace_id, }): Path, navigator: Navigator, State(mut pooler): State, ) -> Result { // Check workspace authorization. let workspace_perms = WorkspaceUserPerm::belonging_to_user(user.id) .fetch_all(&mut app_db) .await?; if workspace_perms.iter().all(|p| { p.workspace_id != workspace_id || p.perm != workspace_user_perm::PermissionValue::Connect }) { return Err(forbidden!("access denied to workspace")); } // FIXME ensure workspace corresponds to rel/portal, and that user has // permission to access/alter both as needed. let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; let workspace = Workspace::with_id(portal.workspace_id) .fetch_one(&mut app_db) .await?; let mut workspace_client = pooler .acquire_for(workspace.id, RoleAssignment::User(user.id)) .await?; let attrs: HashMap = PgAttribute::all_for_rel(portal.class_oid) .fetch_all(&mut workspace_client) .await? .into_iter() .map(|attr| (attr.attname.clone(), attr)) .collect(); let fields: Vec = { let fields: Vec = Field::belonging_to_portal(portal.id) .fetch_all(&mut app_db) .await?; let mut field_info: Vec = Vec::with_capacity(fields.len()); for field in fields { let attr = attrs.get(&field.name); let prompts: HashMap = FieldFormPrompt::belonging_to_field(field.id) .fetch_all(&mut app_db) .await? .into_iter() .map(|value| (value.language, value.content)) .collect(); field_info.push(FormFieldInfo { field, column_present: attr.is_some(), has_default: attr.map(|value| value.atthasdef).unwrap_or(false), not_null: attr.and_then(|value| value.attnotnull).unwrap_or_default(), prompts, }); } field_info }; // FIXME: exclude portals user does not have access to, as well as // unnecessary fields let portal_sets = fetch_all_accessible_portals(workspace_id, &mut app_db, &mut workspace_client).await?; #[derive(Template)] #[template(path = "relations_single/form_index.html")] struct ResponseTemplate { fields: Vec, identifier_hints: Vec, languages: Vec, navigator: Navigator, portal: Portal, portals: Vec, settings: Settings, transitions: Vec, workspace_nav: WorkspaceNav, } #[derive(Debug, Serialize)] struct LanguageInfo { code: String, locale_str: String, } #[derive(Debug, Serialize)] struct PortalDisplay { id: Uuid, display_name: String, } Ok(Html( ResponseTemplate { fields, identifier_hints: attrs.keys().cloned().collect(), languages: Language::iter() .map(|value| LanguageInfo { code: value.to_string(), locale_str: value.as_locale_str().to_owned(), }) .collect(), portal, portals: portal_sets .iter() .flat_map(|RelationPortalSet { rel, portals }| { portals.iter().map(|portal| PortalDisplay { id: portal.id, display_name: format!( "{rel_name}: {portal_name}", rel_name = rel.relname, portal_name = portal.name ), }) }) .collect(), transitions: FormTransition::with_source(portal_id) .fetch_all(&mut app_db) .await?, workspace_nav: WorkspaceNav::builder() .navigator(navigator.clone()) .workspace(workspace) .populate_rels(&mut app_db, &mut workspace_client) .await? .current(NavLocation::Rel( Oid(rel_oid), Some(RelLocation::Portal(portal_id)), )) .build()?, navigator, settings, } .render()?, )) }