From ebb691449b86051ebb65572f513996c22e2ca069 Mon Sep 17 00:00:00 2001 From: Sudo-JHare Date: Tue, 22 Apr 2025 13:33:44 +1000 Subject: [PATCH] Incremental Added: Constraints, Bindings, Search param tabs. --- app.py | 241 +++- services.py | 79 ++ templates/cp_view_processed_ig.html | 312 ++++- templates/fhir_ui_operations.html.bak | 1788 ------------------------- 4 files changed, 569 insertions(+), 1851 deletions(-) delete mode 100644 templates/fhir_ui_operations.html.bak diff --git a/app.py b/app.py index cda14ef..b5cf8fc 100644 --- a/app.py +++ b/app.py @@ -401,13 +401,126 @@ def view_ig(processed_ig_id): optional_usage_elements=optional_usage_elements, config=current_app.config) +#---------------------------------------------------------------------------------------OLD backup----------------------------------- +# @app.route('/get-structure') +# def get_structure(): +# package_name = request.args.get('package_name') +# package_version = request.args.get('package_version') +# resource_type = request.args.get('resource_type') +# # Keep view parameter for potential future use or caching, though not used directly in this revised logic +# view = request.args.get('view', 'snapshot') # Default to snapshot view processing + +# if not all([package_name, package_version, resource_type]): +# logger.warning("get_structure: Missing query parameters: package_name=%s, package_version=%s, resource_type=%s", package_name, package_version, resource_type) +# return jsonify({"error": "Missing required query parameters: package_name, package_version, resource_type"}), 400 + +# packages_dir = current_app.config.get('FHIR_PACKAGES_DIR') +# if not packages_dir: +# logger.error("FHIR_PACKAGES_DIR not configured.") +# return jsonify({"error": "Server configuration error: Package directory not set."}), 500 + +# tgz_filename = services.construct_tgz_filename(package_name, package_version) +# tgz_path = os.path.join(packages_dir, tgz_filename) +# sd_data = None +# fallback_used = False +# source_package_id = f"{package_name}#{package_version}" +# logger.debug(f"Attempting to find SD for '{resource_type}' in {tgz_filename}") + +# # --- Fetch SD Data (Keep existing logic including fallback) --- +# if os.path.exists(tgz_path): +# try: +# sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type) +# # Add error handling as before... +# except Exception as e: +# logger.error(f"Unexpected error extracting SD '{resource_type}' from {tgz_path}: {e}", exc_info=True) +# return jsonify({"error": f"Unexpected error reading StructureDefinition: {str(e)}"}), 500 +# else: +# logger.warning(f"Package file not found: {tgz_path}") +# # Try fallback... (keep existing fallback logic) +# if sd_data is None: +# logger.info(f"SD for '{resource_type}' not found in {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.") +# core_package_name, core_package_version = services.CANONICAL_PACKAGE +# core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version) +# core_tgz_path = os.path.join(packages_dir, core_tgz_filename) +# if not os.path.exists(core_tgz_path): +# # Handle missing core package / download if needed... +# logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally.") +# return jsonify({"error": f"SD for '{resource_type}' not found in primary package, and core package is missing."}), 500 +# # (Add download logic here if desired) +# try: +# sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type) +# if sd_data is not None: +# fallback_used = True +# source_package_id = services.CANONICAL_PACKAGE_ID +# logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}.") +# # Add error handling as before... +# except Exception as e: +# logger.error(f"Unexpected error extracting SD '{resource_type}' from fallback {core_tgz_path}: {e}", exc_info=True) +# return jsonify({"error": f"Unexpected error reading fallback StructureDefinition: {str(e)}"}), 500 + +# # --- Check if SD data was found --- +# if not sd_data: +# logger.error(f"SD for '{resource_type}' not found in primary or fallback package.") +# return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404 + +# # --- *** START Backend Modification *** --- +# # Extract snapshot and differential elements +# snapshot_elements = sd_data.get('snapshot', {}).get('element', []) +# differential_elements = sd_data.get('differential', {}).get('element', []) + +# # Create a set of element IDs from the differential for efficient lookup +# # Using element 'id' is generally more reliable than 'path' for matching +# differential_ids = {el.get('id') for el in differential_elements if el.get('id')} +# logger.debug(f"Found {len(differential_ids)} unique IDs in differential.") + +# enriched_elements = [] +# if snapshot_elements: +# logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.") +# for element in snapshot_elements: +# element_id = element.get('id') +# # Add the isInDifferential flag +# element['isInDifferential'] = bool(element_id and element_id in differential_ids) +# enriched_elements.append(element) +# # Clean narrative from enriched elements (should have been done in find_and_extract_sd, but double check) +# enriched_elements = [services.remove_narrative(el) for el in enriched_elements] +# else: +# # Fallback: If no snapshot, log warning. Maybe return differential only? +# # Returning only differential might break frontend filtering logic. +# # For now, return empty, but log clearly. +# logger.warning(f"No snapshot found for {resource_type} in {source_package_id}. Returning empty element list.") +# enriched_elements = [] # Or consider returning differential and handle in JS + +# # --- *** END Backend Modification *** --- + +# # Retrieve must_support_paths from DB (keep existing logic) +# must_support_paths = [] +# processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first() +# if processed_ig and processed_ig.must_support_elements: +# # Use the profile ID (which is likely the resource_type for profiles) as the key +# must_support_paths = processed_ig.must_support_elements.get(resource_type, []) +# logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_type}' from processed IG DB record.") +# else: +# logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}, resource {resource_type}") + + +# # Construct the response +# response_data = { +# 'elements': enriched_elements, # Return the processed list +# 'must_support_paths': must_support_paths, +# 'fallback_used': fallback_used, +# 'source_package': source_package_id +# # Removed raw structure_definition to reduce payload size, unless needed elsewhere +# } + +# # Use Response object for consistent JSON formatting +# return Response(json.dumps(response_data, indent=2), mimetype='application/json') # Use indent=2 for readability if debugging +#----------------------------------------------------------------------------------------------------------------------------------- @app.route('/get-structure') def get_structure(): package_name = request.args.get('package_name') package_version = request.args.get('package_version') - resource_type = request.args.get('resource_type') - # Keep view parameter for potential future use or caching, though not used directly in this revised logic - view = request.args.get('view', 'snapshot') # Default to snapshot view processing + resource_type = request.args.get('resource_type') # This is the StructureDefinition ID/Name + view = request.args.get('view', 'snapshot') # Keep for potential future use if not all([package_name, package_version, resource_type]): logger.warning("get_structure: Missing query parameters: package_name=%s, package_version=%s, resource_type=%s", package_name, package_version, resource_type) @@ -418,101 +531,131 @@ def get_structure(): logger.error("FHIR_PACKAGES_DIR not configured.") return jsonify({"error": "Server configuration error: Package directory not set."}), 500 + # Paths for primary and core packages tgz_filename = services.construct_tgz_filename(package_name, package_version) tgz_path = os.path.join(packages_dir, tgz_filename) + core_package_name, core_package_version = services.CANONICAL_PACKAGE + core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version) + core_tgz_path = os.path.join(packages_dir, core_tgz_filename) + sd_data = None + search_params_data = [] # Initialize search params list fallback_used = False source_package_id = f"{package_name}#{package_version}" + base_resource_type_for_sp = None # Variable to store the base type for SP search + logger.debug(f"Attempting to find SD for '{resource_type}' in {tgz_filename}") - # --- Fetch SD Data (Keep existing logic including fallback) --- - if os.path.exists(tgz_path): + # --- Fetch SD Data (Primary Package) --- + primary_package_exists = os.path.exists(tgz_path) + core_package_exists = os.path.exists(core_tgz_path) + + if primary_package_exists: try: sd_data, _ = services.find_and_extract_sd(tgz_path, resource_type) - # Add error handling as before... + if sd_data: + base_resource_type_for_sp = sd_data.get('type') + logger.debug(f"Determined base resource type '{base_resource_type_for_sp}' from primary SD '{resource_type}'") except Exception as e: - logger.error(f"Unexpected error extracting SD '{resource_type}' from {tgz_path}: {e}", exc_info=True) - return jsonify({"error": f"Unexpected error reading StructureDefinition: {str(e)}"}), 500 - else: - logger.warning(f"Package file not found: {tgz_path}") - # Try fallback... (keep existing fallback logic) + logger.error(f"Unexpected error extracting SD '{resource_type}' from primary package {tgz_path}: {e}", exc_info=True) + sd_data = None # Ensure sd_data is None if extraction failed + + # --- Fallback SD Check (if primary failed or file didn't exist) --- if sd_data is None: - logger.info(f"SD for '{resource_type}' not found in {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.") - core_package_name, core_package_version = services.CANONICAL_PACKAGE - core_tgz_filename = services.construct_tgz_filename(core_package_name, core_package_version) - core_tgz_path = os.path.join(packages_dir, core_tgz_filename) - if not os.path.exists(core_tgz_path): - # Handle missing core package / download if needed... - logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally.") - return jsonify({"error": f"SD for '{resource_type}' not found in primary package, and core package is missing."}), 500 - # (Add download logic here if desired) + logger.info(f"SD for '{resource_type}' not found or failed to load from {source_package_id}. Attempting fallback to {services.CANONICAL_PACKAGE_ID}.") + if not core_package_exists: + logger.error(f"Core package {services.CANONICAL_PACKAGE_ID} not found locally at {core_tgz_path}.") + error_message = f"SD for '{resource_type}' not found in primary package, and core package is missing." if primary_package_exists else f"Primary package {package_name}#{package_version} and core package are missing." + return jsonify({"error": error_message}), 500 if primary_package_exists else 404 + try: sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_type) if sd_data is not None: fallback_used = True source_package_id = services.CANONICAL_PACKAGE_ID - logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}.") - # Add error handling as before... + base_resource_type_for_sp = sd_data.get('type') # Store base type from fallback SD + logger.info(f"Found SD for '{resource_type}' in fallback package {source_package_id}. Base type: '{base_resource_type_for_sp}'") except Exception as e: logger.error(f"Unexpected error extracting SD '{resource_type}' from fallback {core_tgz_path}: {e}", exc_info=True) return jsonify({"error": f"Unexpected error reading fallback StructureDefinition: {str(e)}"}), 500 - # --- Check if SD data was found --- + # --- Check if SD data was ultimately found --- if not sd_data: - logger.error(f"SD for '{resource_type}' not found in primary or fallback package.") + # This case should ideally be covered by the checks above, but as a final safety net: + logger.error(f"SD for '{resource_type}' could not be found in primary or fallback packages.") return jsonify({"error": f"StructureDefinition for '{resource_type}' not found."}), 404 - # --- *** START Backend Modification *** --- - # Extract snapshot and differential elements + # --- Fetch Search Parameters (Primary Package First) --- + if base_resource_type_for_sp and primary_package_exists: + try: + logger.info(f"Fetching SearchParameters for base type '{base_resource_type_for_sp}' from primary package {tgz_path}") + search_params_data = services.find_and_extract_search_params(tgz_path, base_resource_type_for_sp) + except Exception as e: + logger.error(f"Error extracting SearchParameters for '{base_resource_type_for_sp}' from primary package {tgz_path}: {e}", exc_info=True) + search_params_data = [] # Continue with empty list on error + elif not primary_package_exists: + logger.warning(f"Original package {tgz_path} not found, cannot search it for specific SearchParameters.") + elif not base_resource_type_for_sp: + logger.warning(f"Base resource type could not be determined for '{resource_type}', cannot search for SearchParameters.") + + # --- Fetch Search Parameters (Fallback to Core Package if needed) --- + if not search_params_data and base_resource_type_for_sp and core_package_exists: + logger.info(f"No relevant SearchParameters found in primary package for '{base_resource_type_for_sp}'. Searching core package {core_tgz_path}.") + try: + search_params_data = services.find_and_extract_search_params(core_tgz_path, base_resource_type_for_sp) + if search_params_data: + logger.info(f"Found {len(search_params_data)} SearchParameters for '{base_resource_type_for_sp}' in core package.") + except Exception as e: + logger.error(f"Error extracting SearchParameters for '{base_resource_type_for_sp}' from core package {core_tgz_path}: {e}", exc_info=True) + search_params_data = [] # Continue with empty list on error + elif not search_params_data and not core_package_exists: + logger.warning(f"Core package {core_tgz_path} not found, cannot perform fallback search for SearchParameters.") + + + # --- Prepare Snapshot/Differential Elements (Existing Logic) --- snapshot_elements = sd_data.get('snapshot', {}).get('element', []) differential_elements = sd_data.get('differential', {}).get('element', []) - - # Create a set of element IDs from the differential for efficient lookup - # Using element 'id' is generally more reliable than 'path' for matching differential_ids = {el.get('id') for el in differential_elements if el.get('id')} logger.debug(f"Found {len(differential_ids)} unique IDs in differential.") - enriched_elements = [] if snapshot_elements: logger.debug(f"Processing {len(snapshot_elements)} snapshot elements to add isInDifferential flag.") for element in snapshot_elements: element_id = element.get('id') - # Add the isInDifferential flag element['isInDifferential'] = bool(element_id and element_id in differential_ids) enriched_elements.append(element) - # Clean narrative from enriched elements (should have been done in find_and_extract_sd, but double check) enriched_elements = [services.remove_narrative(el) for el in enriched_elements] else: - # Fallback: If no snapshot, log warning. Maybe return differential only? - # Returning only differential might break frontend filtering logic. - # For now, return empty, but log clearly. logger.warning(f"No snapshot found for {resource_type} in {source_package_id}. Returning empty element list.") - enriched_elements = [] # Or consider returning differential and handle in JS + enriched_elements = [] - # --- *** END Backend Modification *** --- - - # Retrieve must_support_paths from DB (keep existing logic) + # --- Retrieve Must Support Paths from DB (Existing Logic - slightly refined key lookup) --- must_support_paths = [] processed_ig = ProcessedIg.query.filter_by(package_name=package_name, version=package_version).first() if processed_ig and processed_ig.must_support_elements: - # Use the profile ID (which is likely the resource_type for profiles) as the key - must_support_paths = processed_ig.must_support_elements.get(resource_type, []) - logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths for '{resource_type}' from processed IG DB record.") + ms_elements_dict = processed_ig.must_support_elements + if resource_type in ms_elements_dict: + must_support_paths = ms_elements_dict[resource_type] + logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths using profile key '{resource_type}' from processed IG DB record.") + elif base_resource_type_for_sp and base_resource_type_for_sp in ms_elements_dict: + must_support_paths = ms_elements_dict[base_resource_type_for_sp] + logger.debug(f"Retrieved {len(must_support_paths)} Must Support paths using base type key '{base_resource_type_for_sp}' from processed IG DB record.") + else: + logger.debug(f"No specific Must Support paths found for keys '{resource_type}' or '{base_resource_type_for_sp}' in processed IG DB.") else: - logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}, resource {resource_type}") + logger.debug(f"No processed IG record or no must_support_elements found in DB for {package_name}#{package_version}") - - # Construct the response + # --- Construct the final response --- response_data = { - 'elements': enriched_elements, # Return the processed list + 'elements': enriched_elements, 'must_support_paths': must_support_paths, + 'search_parameters': search_params_data, # Include potentially populated list 'fallback_used': fallback_used, 'source_package': source_package_id - # Removed raw structure_definition to reduce payload size, unless needed elsewhere } - # Use Response object for consistent JSON formatting - return Response(json.dumps(response_data, indent=2), mimetype='application/json') # Use indent=2 for readability if debugging + # Use Response object for consistent JSON formatting and smaller payload + return Response(json.dumps(response_data, indent=None, separators=(',', ':')), mimetype='application/json') @app.route('/get-example') def get_example(): diff --git a/services.py b/services.py index 5efae24..9a324d3 100644 --- a/services.py +++ b/services.py @@ -1933,6 +1933,85 @@ def process_fhir_input(input_mode, fhir_file, fhir_text, alias_file=None): logger.error(f"Error processing input: {str(e)}", exc_info=True) return None, None, None, f"Error processing input: {str(e)}" +# --- ADD THIS NEW FUNCTION TO services.py --- +def find_and_extract_search_params(tgz_path, base_resource_type): + """ + Finds and extracts SearchParameter resources relevant to a given base resource type + from a FHIR package tgz file. + """ + search_params = [] + if not tgz_path or not os.path.exists(tgz_path): + logger.error(f"Package file not found for SearchParameter extraction: {tgz_path}") + return search_params # Return empty list on error + + logger.debug(f"Searching for SearchParameters based on '{base_resource_type}' in {os.path.basename(tgz_path)}") + try: + with tarfile.open(tgz_path, "r:gz") as tar: + for member in tar: + # Basic filtering for JSON files in package directory, excluding common metadata + if not (member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json')): + continue + if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']: + continue + + fileobj = None + try: + fileobj = tar.extractfile(member) + if fileobj: + content_bytes = fileobj.read() + content_string = content_bytes.decode('utf-8-sig') + data = json.loads(content_string) + + # Check if it's a SearchParameter resource + if isinstance(data, dict) and data.get('resourceType') == 'SearchParameter': + # Check if the SearchParameter applies to the requested base resource type + sp_bases = data.get('base', []) # 'base' is a list of applicable resource types + if base_resource_type in sp_bases: + # Extract relevant information + param_info = { + 'id': data.get('id'), + 'url': data.get('url'), + 'name': data.get('name'), + 'description': data.get('description'), + 'code': data.get('code'), # The actual parameter name used in searches + 'type': data.get('type'), # e.g., token, reference, date + 'expression': data.get('expression'), # FHIRPath expression + 'base': sp_bases, + # NOTE: Conformance (mandatory/optional) usually comes from CapabilityStatement, + # which is not processed here. Add placeholders or leave out for now. + 'conformance': 'N/A', # Placeholder + 'is_mandatory': False # Placeholder + } + search_params.append(param_info) + logger.debug(f"Found relevant SearchParameter: {param_info.get('name')} (ID: {param_info.get('id')}) for base {base_resource_type}") + + # --- Error handling for individual file processing --- + except json.JSONDecodeError as e: + logger.debug(f"Could not parse JSON for SearchParameter in {member.name}, skipping: {e}") + except UnicodeDecodeError as e: + logger.warning(f"Could not decode UTF-8 for SearchParameter in {member.name}, skipping: {e}") + except tarfile.TarError as e: + logger.warning(f"Tar error reading member {member.name} for SearchParameter, skipping: {e}") + except Exception as e: + logger.warning(f"Could not read/parse potential SearchParameter {member.name}, skipping: {e}", exc_info=False) + finally: + if fileobj: + fileobj.close() + + # --- Error handling for opening/reading the tgz file --- + except tarfile.ReadError as e: + logger.error(f"Tar ReadError extracting SearchParameters from {tgz_path}: {e}") + except tarfile.TarError as e: + logger.error(f"TarError extracting SearchParameters from {tgz_path}: {e}") + except FileNotFoundError: + logger.error(f"Package file not found during SearchParameter extraction: {tgz_path}") + except Exception as e: + logger.error(f"Unexpected error extracting SearchParameters from {tgz_path}: {e}", exc_info=True) + + logger.info(f"Found {len(search_params)} SearchParameters relevant to '{base_resource_type}' in {os.path.basename(tgz_path)}") + return search_params +# --- END OF NEW FUNCTION --- + # --- Standalone Test --- if __name__ == '__main__': logger.info("Running services.py directly for testing.") diff --git a/templates/cp_view_processed_ig.html b/templates/cp_view_processed_ig.html index aa5220e..e872dcd 100644 --- a/templates/cp_view_processed_ig.html +++ b/templates/cp_view_processed_ig.html @@ -153,6 +153,15 @@ + + +
@@ -195,6 +204,64 @@
+
+
+

Constraints defined in this profile:

+ + + + + + + + + + + + + +
KeySeverityPathDescriptionExpression (FHIRPath)
No constraints found or data not loaded.
+
+
+
+
+

Terminology bindings defined in this profile:

+ + + + + + + + + + + + +
PathStrengthValueSetDescription
No bindings found or data not loaded.
+
+
+
+
+

Search parameters associated with this profile:

+ + + + + + + + + + + + + +
ParameterTypeConformanceRequirements / Description
Search Parameter data not available. Requires backend implementation.
+
+
@@ -869,8 +936,9 @@ async function fetchStructure(resourceType, view) { } -// --- Populate Structure for All Views - V2 (Builds full tree from snapshot first) --- +// --- Populate Structure for All Views - V4 (Add Search Param Logging) --- async function populateStructure(resourceType) { + // ... (keep existing variable assignments and UI resets at the beginning) ... if (structureTitle) structureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`; if (rawStructureTitle) rawStructureTitle.textContent = `${resourceType} ({{ processed_ig.package_name }}#{{ processed_ig.version }})`; if (structureLoading) structureLoading.style.display = 'block'; @@ -880,33 +948,76 @@ async function populateStructure(resourceType) { if (structureFallbackMessage) { structureFallbackMessage.style.display = 'none'; structureFallbackMessage.textContent = ''; } const views = ['differential', 'snapshot', 'must-support', 'key-elements']; views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = ''; }); - if (rawStructureContent) rawStructureContent.textContent = ''; + const constraintsTableBody = document.getElementById('constraints-table')?.querySelector('tbody'); + const bindingsTableBody = document.getElementById('bindings-table')?.querySelector('tbody'); + const searchParamsTableBody = document.getElementById('searchparams-table')?.querySelector('tbody'); + if (constraintsTableBody) constraintsTableBody.innerHTML = 'Loading...'; + if (bindingsTableBody) bindingsTableBody.innerHTML = 'Loading...'; + if (searchParamsTableBody) searchParamsTableBody.innerHTML = 'Loading...'; + if (rawStructureContent) rawStructureContent.textContent = 'Loading...'; try { - console.log(`PopulateStructure V2: Fetching snapshot for ${resourceType}`); - const snapshotData = await fetchStructure(resourceType, 'snapshot'); - if (!snapshotData || !snapshotData.elements) { throw new Error(`Snapshot data missing for ${resourceType}.`); } - const snapshotElementsCopy = JSON.parse(JSON.stringify(snapshotData.elements)); - const mustSupportPaths = new Set(snapshotData.must_support_paths || []); - const fallbackUsed = snapshotData.fallback_used || false; const fallbackSource = snapshotData.source_package || 'FHIR core'; - if (rawStructureContent) { rawStructureContent.textContent = JSON.stringify(snapshotData, null, 4); if (rawStructureContent.classList.contains('hljs')) { rawStructureContent.classList.remove('hljs'); } hljs.highlightElement(rawStructureContent); if (fallbackUsed && structureFallbackMessage) { /* show fallback */ } } - else { if (rawStructureContent) rawStructureContent.textContent = '(Raw content display element not found)';} + console.log(`PopulateStructure V4: Fetching structure and search params for ${resourceType}`); + const structureBaseUrl = "{{ url_for('get_structure') }}"; + const params = new URLSearchParams({ package_name: "{{ processed_ig.package_name }}", package_version: "{{ processed_ig.version }}", resource_type: resourceType }); + const fetchUrl = `${structureBaseUrl}?${params.toString()}`; + const response = await fetch(fetchUrl); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP error ${response.status}: ${errorText}`); + } + const data = await response.json(); + + if (!data || !data.elements) { throw new Error(`Structure data or elements missing for ${resourceType}.`); } + + const snapshotElementsCopy = JSON.parse(JSON.stringify(data.elements)); + const mustSupportPaths = new Set(data.must_support_paths || []); + const searchParams = data.search_parameters || []; // Get search params + const fallbackUsed = data.fallback_used || false; + const fallbackSource = data.source_package || 'FHIR core'; + + // *** ADDED LOG: Check received search parameters *** + console.log('[populateStructure] Received search_parameters:', searchParams); + // ************************************************* + + // Update Raw JSON View + if (rawStructureContent) { + rawStructureContent.textContent = JSON.stringify(data, null, 4); + if (rawStructureContent.classList.contains('hljs')) { rawStructureContent.classList.remove('hljs'); } + hljs.highlightElement(rawStructureContent); + if (fallbackUsed && structureFallbackMessage) { + structureFallbackMessage.textContent = `Note: Displaying structure from fallback package: ${fallbackSource}`; + structureFallbackMessage.style.display = 'block'; + } + } else { if (rawStructureContent) rawStructureContent.textContent = '(Raw content display element not found)';} + + // Render the Tree Views for (const view of views) { - console.log(`PopulateStructure V2: Rendering view '${view}' for ${resourceType}`); + console.log(`PopulateStructure V4: Rendering view '${view}' for ${resourceType}`); const treeContainer = document.getElementById(`structure-tree-${view}`); if (!treeContainer) { console.error(`Container missing for ${view}`); continue; } - // Use V11 buildTreeData (called within renderStructureTree) await renderStructureTree(resourceType, view, JSON.parse(JSON.stringify(snapshotElementsCopy)), mustSupportPaths); } + + // Render Constraints & Bindings tables + renderConstraintsTable(data.elements); + renderBindingsTable(data.elements); + // Render Search Params table (passing potentially empty array) + renderSearchParametersTable(searchParams); // Pass the retrieved data + } catch (error) { console.error("Error populating structure:", error); - if (structureTreeDifferential) { structureTreeDifferential.innerHTML = `
Error loading structure: ${error.message}
`; } + // ... existing error handling ... + views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = `
Error loading structure: ${error.message}
`; }); + if (constraintsTableBody) constraintsTableBody.innerHTML = `Error loading constraints: ${error.message}`; + if (bindingsTableBody) bindingsTableBody.innerHTML = `Error loading bindings: ${error.message}`; + if (searchParamsTableBody) searchParamsTableBody.innerHTML = `Error loading search params: ${error.message}`; // Show error here too if (rawStructureContent) { rawStructureContent.textContent = `Error: ${error.message}`; } } finally { if (structureLoading) structureLoading.style.display = 'none'; if (rawStructureLoading) rawStructureLoading.style.display = 'none'; } -} // End populateStructure V2 +} // End populateStructure V4 // --- Populate Example Selector Dropdown --- @@ -949,6 +1060,179 @@ function setupCopyButton(buttonElement, sourceElement, tooltipInstance, successT buttonElement.removeEventListener('click', clickHandler); buttonElement.addEventListener('click', clickHandler); return tooltipInstance; } +// --- Function to Render Constraints Table --- +function renderConstraintsTable(elements) { + const tableBody = document.getElementById('constraints-table')?.querySelector('tbody'); + if (!tableBody) { + console.error('Constraints table body not found'); + return; + } + tableBody.innerHTML = ''; // Clear existing rows + let constraintsFound = false; + + if (!elements || !Array.isArray(elements)) { + console.warn("renderConstraintsTable called with invalid elements data:", elements); + elements = []; // Prevent errors later + } + + + elements.forEach(element => { + // Check if element exists and has a constraint property which is an array + if (element && element.constraint && Array.isArray(element.constraint) && element.constraint.length > 0) { + element.constraint.forEach(constraint => { + if (constraint) { // Ensure constraint object itself exists + constraintsFound = true; + const row = tableBody.insertRow(); + row.insertCell().textContent = constraint.key || 'N/A'; + row.insertCell().textContent = constraint.severity || 'N/A'; + row.insertCell().textContent = element.path || 'N/A'; // Path of the element the constraint is on + row.insertCell().textContent = constraint.human || 'N/A'; + const expressionCell = row.insertCell(); + const codeElement = document.createElement('code'); + codeElement.className = 'language-fhirpath'; // Use 'language-fhirpath' if you have a FHIRPath grammar for highlight.js + codeElement.textContent = constraint.expression || 'N/A'; + expressionCell.appendChild(codeElement); + } + }); + } + }); + + if (!constraintsFound) { + const row = tableBody.insertRow(); + const cell = row.insertCell(); + cell.colSpan = 5; // Match number of columns + cell.textContent = 'No constraints defined in this profile.'; + cell.style.textAlign = 'center'; + } + + // Re-run highlight.js for the new code blocks + // Use setTimeout to allow the DOM to update before highlighting + setTimeout(() => { + document.querySelectorAll('#constraints-table code.language-fhirpath').forEach((block) => { + // Remove existing highlight classes if any, before re-highlighting + block.classList.remove('hljs'); + hljs.highlightElement(block); + }); + }, 0); +} + +// --- Function to Render Terminology Bindings Table --- +function renderBindingsTable(elements) { + const tableBody = document.getElementById('bindings-table')?.querySelector('tbody'); + if (!tableBody) { + console.error('Bindings table body not found'); + return; + } + tableBody.innerHTML = ''; // Clear existing rows + let bindingsFound = false; + + if (!elements || !Array.isArray(elements)) { + console.warn("renderBindingsTable called with invalid elements data:", elements); + elements = []; // Prevent errors later + } + + elements.forEach(element => { + // Check if element exists and has a binding property which is an object + if (element && element.binding && typeof element.binding === 'object') { + bindingsFound = true; + const binding = element.binding; + const row = tableBody.insertRow(); + row.insertCell().textContent = element.path || 'N/A'; + row.insertCell().textContent = binding.strength || 'N/A'; + const valueSetCell = row.insertCell(); + if (binding.valueSet) { + const valueSetLink = document.createElement('a'); + valueSetLink.href = binding.valueSet; // Ideally, resolve canonical to actual URL if possible + // Try to show last part of URL as name, or full URL if no '/' + const urlParts = binding.valueSet.split('/'); + valueSetLink.textContent = urlParts.length > 1 ? urlParts[urlParts.length - 1] : binding.valueSet; + valueSetLink.target = '_blank'; // Open in new tab + valueSetLink.title = `Open ValueSet: ${binding.valueSet}`; // Add tooltip + valueSetLink.setAttribute('data-bs-toggle', 'tooltip'); // Enable tooltip + valueSetCell.appendChild(valueSetLink); + // Re-initialize tooltips for dynamically added links + const tooltip = new bootstrap.Tooltip(valueSetLink); + } else { + valueSetCell.textContent = 'N/A'; + } + row.insertCell().textContent = binding.description || 'N/A'; + } + }); + + if (!bindingsFound) { + const row = tableBody.insertRow(); + const cell = row.insertCell(); + cell.colSpan = 4; // Match number of columns + cell.textContent = 'No terminology bindings defined in this profile.'; + cell.style.textAlign = 'center'; + } +} + +// --- Function to Render Search Parameters Table (Add Logging) --- +function renderSearchParametersTable(searchParamsData) { + const tableBody = document.getElementById('searchparams-table')?.querySelector('tbody'); + if (!tableBody) { + console.error('Search Params table body not found'); + return; + } + tableBody.innerHTML = ''; // Clear existing rows + + // *** ADDED LOG: Check data received by rendering function *** + console.log('[renderSearchParametersTable] Received data:', searchParamsData); + // ********************************************************** + + if (!searchParamsData || !Array.isArray(searchParamsData) || searchParamsData.length === 0) { + console.log('[renderSearchParametersTable] No valid search parameter data found, displaying message.'); // Added Log + const row = tableBody.insertRow(); + const cell = row.insertCell(); + cell.colSpan = 4; + cell.textContent = 'No relevant search parameters found for this resource type.'; + cell.style.textAlign = 'center'; + cell.classList.add('text-muted', 'fst-italic'); + return; + } + + // Sort parameters alphabetically by code + searchParamsData.sort((a, b) => (a.code || '').localeCompare(b.code || '')); + + searchParamsData.forEach((param, index) => { + // *** ADDED LOG: Log each parameter being processed *** + console.log(`[renderSearchParametersTable] Processing param ${index + 1}:`, param); + // *************************************************** + + const row = tableBody.insertRow(); + + // Parameter Name (using 'code') + const nameCell = row.insertCell(); + nameCell.innerHTML = `${escapeHtmlAttr(param.code || 'N/A')}`; + + // Type + row.insertCell().textContent = param.type || 'N/A'; + + // Conformance (Placeholder) + const conformanceCell = row.insertCell(); + conformanceCell.textContent = param.conformance || 'N/A'; // Show backend placeholder + // Add highlighting later if backend provides is_mandatory + // if (param.is_mandatory) { ... } + + // Description + const descriptionCell = row.insertCell(); + descriptionCell.innerHTML = `${escapeHtmlAttr(param.description || 'No description available.')}`; + if (param.expression) { + descriptionCell.title = `FHIRPath: ${param.expression}`; + descriptionCell.setAttribute('data-bs-toggle', 'tooltip'); + descriptionCell.setAttribute('data-bs-placement', 'top'); + // Ensure tooltip is initialized for dynamically added elements + const existingTooltip = bootstrap.Tooltip.getInstance(descriptionCell); + if (existingTooltip) { + existingTooltip.dispose(); // Remove old instance if any + } + new bootstrap.Tooltip(descriptionCell); + } + }); + console.log('[renderSearchParametersTable] Finished rendering parameters.'); // Added Log +} // End renderSearchParametersTable + // --- DOMContentLoaded Listener --- document.addEventListener('DOMContentLoaded', function() { diff --git a/templates/fhir_ui_operations.html.bak b/templates/fhir_ui_operations.html.bak deleted file mode 100644 index e9b348a..0000000 --- a/templates/fhir_ui_operations.html.bak +++ /dev/null @@ -1,1788 +0,0 @@ - -
- FHIRFLARE IG Toolkit -

FHIR UI Operations

-
-

- Explore FHIR server operations by selecting resource types or system operations. Toggle between local HAPI or a custom server to interact with FHIR metadata, resources, and server-wide operations. -

- -
-
- -
-
-
FHIR Operations Configuration
-
-
- {{ form.hidden_tag() }} -
- -
- - -
- Toggle to use local HAPI (/fhir proxy) or enter a custom FHIR server URL. -
- -
- - - - -
-
-
- -{% endblock %} \ No newline at end of file