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 @@
Constraints defined in this profile:
+Key | +Severity | +Path | +Description | +Expression (FHIRPath) | +
---|---|---|---|---|
No constraints found or data not loaded. |
Terminology bindings defined in this profile:
+Path | +Strength | +ValueSet | +Description | +
---|---|---|---|
No bindings found or data not loaded. |
Search parameters associated with this profile:
+Parameter | +Type | +Conformance | +Requirements / Description | +
---|---|---|---|
Search Parameter data not available. Requires backend implementation. |
${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 @@
-
-- 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. -
- -