Mostly working state

This commit is contained in:
2026-07-28 22:05:33 -06:00
parent f497329169
commit f62ce57279
6 changed files with 79 additions and 209 deletions
+35 -7
View File
@@ -1,5 +1,18 @@
require 'yaml'
## Debugging:
# If the response should be filtered and it is not being filtered **and** you
# are geting all the values for that column, check to make sure that you are
# selecting from the allowed tables and columns.
ALLOWED_TABLES = {
"JointInfo" => ["Joint", "Osteokinematics"],
"Joint" => ["Common Name", "Joint"]
}
class DashboardController < ApplicationController
before_action :load_manifest
@@ -13,14 +26,28 @@ class DashboardController < ApplicationController
# GET /dashboard/options?table=Joint&filter_col=joint_id&value=1
def options
table = params[:table].constantize
if params[:filter_col].present?
options = table.where(params[:filter_col] => params[:value]).pluck(params[:pluck_column])
# 1. Define what is allowed to be accessed
table_name = params[:table]
col_name = params[:pluck_column]
# 2. Validate that the table and column exist in your allowlist
if ALLOWED_TABLES.key?(table_name) && ALLOWED_TABLES[table_name].include?(col_name)
table = table_name.constantize
quoted_col = ActiveRecord::Base.connection.quote_column_name(col_name)
if params[:filter_column].present? && ALLOWED_TABLES[table_name].include?(params[:filter_column])
puts "Filtering with 'filter_column'"
options = table.select(Arel.sql(quoted_col)).where(params[:filter_column] => params[:value]).pluck(Arel.sql(quoted_col))
else
puts "***NO FILTERING***"
options = table.distinct.pluck(Arel.sql(quoted_col))
end
render json: options
else
puts table
options = table.distinct.pluck(params[:pluck_column])
render json: { error: "Invalid table or column requested" }, status: :bad_request
end
render json: options
end
# GET /dashboard/data?joint=Acromioclavicular&sub_joint=X
@@ -29,10 +56,11 @@ class DashboardController < ApplicationController
# Use the data_sources from YAML to aggregate data
@manifest['data_sources'].each do |source|
debugger
table = source['table'].constantize
# Find the filter value from the params that matches the filter_column
# We assume the filter_column name in DB matches the filter ID in YAML
filter_val = params[source['filter_column']] || params['joint'] # Fallback to joint
filter_val = params[source['filter_column']]
record = table.find_by(source['filter_column'] => filter_val)
results[source['key']] = record ? record.attributes : {}
+5
View File
@@ -0,0 +1,5 @@
class JointInfo < ApplicationRecord
def readonly?
true
end
end
-5
View File
@@ -1,5 +0,0 @@
class JointsInfo < ApplicationRecord
def readonly?
true
end
end
+3 -176
View File
@@ -18,180 +18,6 @@
.bg-pale-orange { background-color: #FCE4D6; }
</style>
<!-- <script> -->
<!-- document.addEventListener("DOMContentLoaded", () => { -->
<!-- let manifest = {}; -->
<!-- let currentSelections = {}; -->
<!-- -->
<!-- async function init() { -->
<!-- const resp = await fetch('/dashboard'); -->
<!-- manifest = await resp.json(); -->
<!-- renderRootFilters(); -->
<!-- } -->
<!-- -->
<!-- function renderRootFilters() { -->
<!-- const rootFilters = manifest.filters.filter(f => f.depends_on === null); -->
<!-- rootFilters.forEach(f => createDropdown(f)); -->
<!-- } -->
<!-- -->
<!-- function createDropdown(config) { -->
<!-- const container = document.createElement("div"); -->
<!-- container.id = `container_${config.id}`; -->
<!-- -->
<!-- const label = document.createElement("label"); -->
<!-- label.textContent = config.label + ": "; -->
<!-- label.style.fontWeight = "bold"; -->
<!-- -->
<!-- const select = document.createElement("select"); -->
<!-- select.id = config.id; -->
<!-- select.innerHTML = `<option value="">Select...</option>`; -->
<!-- -->
<!-- select.addEventListener("change", async (e) => { -->
<!-- const val = e.target.value; -->
<!-- currentSelections[config.id] = val; -->
<!-- -->
<!-- // 1. Clear dependent filters and data -->
<!-- clearDependents(config.id); -->
<!-- -->
<!-- // 2. Load options for children -->
<!-- const children = manifest.filters.filter(f => f.depends_on === config.id); -->
<!-- children.forEach(child => { -->
<!-- const options = await fetchOptions(child, val); -->
<!-- populateDropdown(child.id, options); -->
<!-- }); -->
<!-- -->
<!-- // 3. Update data grid -->
<!-- updateGrid(); -->
<!-- }); -->
<!-- -->
<!-- container.appendChild(label); -->
<!-- container.appendChild(select); -->
<!-- document.getElementById("filter-area").appendChild(container); -->
<!-- } -->
<!-- -->
<!-- async function fetchOptions(config, value) { -->
<!-- const params = new URLSearchParams({ -->
<!-- table: config.table, -->
<!-- pluck_column: config.pluck_column, -->
<!-- filter_col: config.filter_column, -->
<!-- value: value -->
<!-- }); -->
<!-- const resp = await fetch(`/dashboard/options?${params.toString()}`); -->
<!-- return await resp.json(); -->
<!-- } -->
<!-- -->
<!-- function populateDropdown(id, options) { -->
<!-- const select = document.getElementById(id) || createMissingDropdown(id); -->
<!-- select.innerHTML = `<option value="">Select...</option>`; -->
<!-- options.forEach(opt => { -->
<!-- select.innerHTML += `<option value="${opt}">${opt}</option>`; -->
<!-- }); -->
<!-- // If this is the last dropdown, it should be visible now -->
<!-- const container = document.getElementById(`container_${id}`); -->
<!-- if(container) container.style.display = "block"; -->
<!-- } -->
<!-- -->
<!-- function createMissingDropdown(id) { -->
<!-- const config = manifest.filters.find(f => f.id === id); -->
<!-- const container = document.createElement("div"); -->
<!-- container.id = `container_${id}`; -->
<!-- container.style.display = "none"; // Hidden until parent is selected -->
<!-- -->
<!-- const label = document.createElement("label"); -->
<!-- label.textContent = config.label + ": "; -->
<!-- -->
<!-- const select = document.createElement("select"); -->
<!-- select.id = id; -->
<!-- -->
<!-- select.addEventListener("change", async (e) => { -->
<!-- currentSelections[id] = e.target.value; -->
<!-- clearDependents(id); -->
<!-- const children = manifest.filters.filter(f => f.depends_on === id); -->
<!-- children.forEach(child => { -->
<!-- const opts = await fetchOptions(child, e.target.value); -->
<!-- populateDropdown(child.id, opts); -->
<!-- }); -->
<!-- updateGrid(); -->
<!-- }); -->
<!-- -->
<!-- container.appendChild(label); -->
<!-- container.appendChild(select); -->
<!-- document.getElementById("filter-area").appendChild(container); -->
<!-- return select; -->
<!-- } -->
<!-- -->
<!-- function clearDependents(id) { -->
<!-- // Recursively find all filters that depend on this ID and reset them -->
<!-- manifest.filters.forEach(f => { -->
<!-- if (f.depends_on === id) { -->
<!-- const select = document.getElementById(f.id); -->
<!-- if (select) select.innerHTML = `<option value="">Select...</option>`; -->
<!-- const container = document.getElementById(`container_${f.id}`); -->
<!-- if (container) container.style.display = "none"; -->
<!-- delete currentSelections[f.id]; -->
<!-- clearDependents(f.id); -->
<!-- } -->
<!-- }); -->
<!-- } -->
<!-- -->
<!-- async function updateGrid() { -->
<!-- const params = new URLSearchParams(currentSelections); -->
<!-- const resp = await fetch(`/dashboard/data?${params.toString()}`); -->
<!-- const data = await resp.json(); -->
<!-- renderLayout(data); -->
<!-- } -->
<!-- -->
<!-- function renderLayout(data) { -->
<!-- const area = document.getElementById("results-area"); -->
<!-- area.innerHTML = ""; -->
<!-- -->
<!-- // Group layout by grid name (defaults to 'main') -->
<!-- const grids = {}; -->
<!-- manifest.layout.forEach(item => { -->
<!-- const gridId = item.grid || 'main'; -->
<!-- if (!grids[gridId]) grids[gridId] = []; -->
<!-- grids[gridId].push(item); -->
<!-- }); -->
<!-- -->
<!-- Object.keys(grids).forEach(gridId => { -->
<!-- const table = document.createElement("table"); -->
<!-- table.className = "dyn-grid"; -->
<!-- -->
<!-- // Find max rows/cols to initialize table structure if needed, -->
<!-- // but we can just build rows dynamically. -->
<!-- const layoutItems = grids[gridId]; -->
<!-- const maxRow = Math.max(...layoutItems.map(i => i.row)); -->
<!-- -->
<!-- for (let r = 0; r <= maxRow; r++) { -->
<!-- const tr = document.createElement("tr"); -->
<!-- const rowItems = layoutItems.filter(i => i.row === r); -->
<!-- -->
<!-- rowItems.forEach(item => { -->
<!-- const td = document.createElement("td"); -->
<!-- td.className = item.class; -->
<!-- td.colSpan = item.colspan || 1; -->
<!-- td.rowSpan = item.rowspan || 1; -->
<!-- -->
<!-- if (item.text) { -->
<!-- td.textContent = item.text; -->
<!-- } else if (item.value) { -->
<!-- // Resolve nested JSON path (e.g., "main_info.joint") -->
<!-- const parts = item.value.split('.'); -->
<!-- const val = parts.reduce((obj, key) => (obj && obj[key] !== undefined) ? obj[key] : '', data); -->
<!-- td.textContent = val; -->
<!-- } -->
<!-- tr.appendChild(td); -->
<!-- }); -->
<!-- table.appendChild(tr); -->
<!-- } -->
<!-- area.appendChild(table); -->
<!-- }); -->
<!-- } -->
<!-- -->
<!-- init(); -->
<!-- }); -->
<!-- </script> -->
<script>
document.addEventListener("DOMContentLoaded", () => {
@@ -261,7 +87,8 @@ document.addEventListener("DOMContentLoaded", () => {
async function fetchOptions(config, value) {
var params
if (config.filter_col == undefined) {
if (config.filter_column == undefined) {
console.log(config + "Undefined filter col");
params = new URLSearchParams({
table: config.table,
pluck_column: config.pluck_column,
@@ -270,7 +97,7 @@ document.addEventListener("DOMContentLoaded", () => {
params = new URLSearchParams({
table: config.table,
pluck_column: config.pluck_column,
filter_col: config.filter_column,
filter_column: config.filter_column,
value: value
});
}
+26 -18
View File
@@ -5,36 +5,44 @@ filters:
table: "Joint"
pluck_column: "Joint"
depends_on: null # Root level
# - id: "joint_info"
# label: "joint_info"
# table: "joint_info" # A separate table in SQLite
# pluck_column: "Common Joint Name"
# depends_on: "joint" # Only appears after 'joint' is selected
# filter_column: "Common Joint Name" # Filter SubJoint where joint_id == selected joint value
- id: "joint_info"
label: "Select kinematics"
table: "JointInfo" # A separate table in SQLite
pluck_column: "Osteokinematics"
depends_on: "joint" # Only appears after 'joint' is selected
filter_column: "Joint" # Filter SubJoint where joint_id == selected joint value
# Data Mapping: Defines which tables to hit for the final grid
data_sources:
- table: "Joint"
key: "main_info" # Nested JSON key
filter_column: "joint"
# - table: "Ligament"
# key: "lig_info"
# filter_column: "joint_id"
- table: "JointInfo" # MUST MATCH DATABASE TABLE
key: "joint_details"
filter_column: "Osteokinematics"
# Layout: Defines the visual grid
# colors: dark_blue, light_blue, pale_yellow, pale_green, pale_orange
layout:
- { row: 0, col: 0, colspan: 4, rowspan: 1, text: "Joint Information", class: "bg-dark-blue" }
- { row: 1, col: 0, colspan: 1, rowspan: 1, text: "Anatomical Name", class: "bg-light-blue" }
- { row: 1, col: 1, colspan: 1, rowspan: 1, value: "main_info.joint", class: "bg-pale-yellow" }
- { row: 0, col: 0, colspan: 3, rowspan: 1, text: "Joint Information", class: "bg-dark-blue" }
- { row: 1, col: 0, colspan: 2, rowspan: 1, text: "Anatomical Name", class: "bg-light-blue" }
- { row: 2, col: 0, colspan: 2, rowspan: 1, value: "main_info.Joint", class: "bg-pale-yellow" }
- { row: 1, col: 2, colspan: 1, rowspan: 1, text: "Common Name", class: "bg-light-blue" }
- { row: 1, col: 3, colspan: 1, rowspan: 1, value: "main_info.common_name", class: "bg-pale-yellow" }
- { row: 2, col: 0, colspan: 1, rowspan: 1, text: "Joint Type", class: "bg-light-blue" }
- { row: 2, col: 1, colspan: 1, rowspan: 1, value: "main_info.joint_type", class: "bg-pale-yellow" }
- { row: 2, col: 2, colspan: 1, rowspan: 1, text: "Planes", class: "bg-light-blue" }
- { row: 2, col: 3, colspan: 1, rowspan: 1, value: "main_info.planes", class: "bg-pale-yellow" }
- { row: 3, col: 0, colspan: 4, rowspan: 1, value: "main_info.notes", class: "bg-pale-green" }
- { row: 2, col: 2, colspan: 1, rowspan: 1, value: "main_info.Common Name", class: "bg-pale-yellow" }
- { row: 3, col: 0, colspan: 1, rowspan: 1, text: "Diarthrodial Joint Type", class: "bg-light-blue" }
- { row: 4, col: 0, colspan: 1, rowspan: 1, value: "main_info.Diarthrodial joint", class: "bg-pale-yellow" }
- { row: 3, col: 1, colspan: 1, rowspan: 1, text: "Alternate Name", class: "bg-light-blue" }
- { row: 4, col: 1, colspan: 1, rowspan: 1, value: "main_info.Alternate Name", class: "bg-pale-yellow" }
- { row: 3, col: 2, colspan: 1, rowspan: 1, text: "Planes of Freedom", class: "bg-light-blue" }
- { row: 4, col: 2, colspan: 1, rowspan: 1, value: "main_info.# of Plains", class: "bg-pale-yellow" }
- { row: 5, col: 0, colspan: 1, rowspan: 1, text: "Arthrokinematic Rule", class: "bg-light-blue"}
- { row: 6, col: 0, colspan: 1, rowspan: 1, value: "joint_details.Arthrokinematic Rule", class: "bg-pale-yellow"}
#- { row: 5, col: 0, colspan: 4, rowspan: 1, value: "main_info.notes", class: "bg-pale-green" }
# Ligaments Table (separate grid)
#- { grid: "ligaments", row: 0, col: 0, colspan: 1, rowspan: 1, text: "Ligaments", class: "bg-dark-blue" }
#- { grid: "ligaments", row: 0, col: 1, colspan: 1, rowspan: 1, value: ".list", class: "bg-pale-orange" }
# http://localhost:3000/dashboard/options?table=JointInfo&pluck_column=Osteokinematics&filter_col=undefined&value=Glenohumeral
# http://localhost:3000/dashboard/options?table=JointInfo&pluck_column=Osteokinematics&filter_col=Common%20Joint%20Name&value=Shoulder
+10 -3
View File
@@ -1,7 +1,14 @@
require "test_helper"
class DashboardControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
test "options returns JSON" do
get "/dashboard/options",
params: {
table: 'Joint',
pluck_column: 'Joint'
}
assert_response :success
assert_equal "application/json", response.media_type
end
end