mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 16:19:59 +00:00
Incremental
Added: Constraints, Bindings, Search param tabs.
This commit is contained in:
parent
454203a583
commit
ebb691449b
241
app.py
241
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():
|
||||
|
79
services.py
79
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.")
|
||||
|
@ -153,6 +153,15 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="key-elements-tab" data-bs-toggle="tab" href="#key-elements" role="tab" aria-controls="key-elements" aria-selected="false">Key Elements</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="constraints-tab" data-bs-toggle="tab" href="#constraints-tab-pane" role="tab">Constraints</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="terminology-tab" data-bs-toggle="tab" href="#terminology-tab-pane" role="tab">Terminology</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" id="searchparams-tab" data-bs-toggle="tab" href="#searchparams-tab-pane" role="tab">Search Params</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content mt-3">
|
||||
<div class="tab-pane fade show active" id="differential" role="tabpanel" aria-labelledby="differential-tab">
|
||||
@ -195,6 +204,64 @@
|
||||
</div>
|
||||
<ul id="structure-tree-key-elements" class="list-group list-group-flush structure-tree-root"></ul>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="constraints-tab-pane" role="tabpanel" aria-labelledby="constraints-tab">
|
||||
<div class="mt-3 table-responsive">
|
||||
<p>Constraints defined in this profile:</p>
|
||||
<table class="table table-sm table-bordered table-striped" id="constraints-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Severity</th>
|
||||
<th>Path</th>
|
||||
<th>Description</th>
|
||||
<th>Expression (FHIRPath)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="5">No constraints found or data not loaded.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="terminology-tab-pane" role="tabpanel" aria-labelledby="terminology-tab">
|
||||
<div class="mt-3 table-responsive">
|
||||
<p>Terminology bindings defined in this profile:</p>
|
||||
<table class="table table-sm table-bordered table-striped" id="bindings-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Path</th>
|
||||
<th>Strength</th>
|
||||
<th>ValueSet</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="4">No bindings found or data not loaded.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="searchparams-tab-pane" role="tabpanel" aria-labelledby="searchparams-tab">
|
||||
<div class="mt-3">
|
||||
<p>Search parameters associated with this profile:</p>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<strong>Note:</strong> SearchParameter resources. This functionality is NEWLY implemented, please report any bugs)
|
||||
</div>
|
||||
<table class="table table-sm table-bordered table-striped" id="searchparams-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Parameter</th>
|
||||
<th>Type</th>
|
||||
<th>Conformance</th>
|
||||
<th>Requirements / Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="4">Search Parameter data not available. Requires backend implementation.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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 = '<tr><td colspan="5" class="text-center text-muted fst-italic">Loading...</td></tr>';
|
||||
if (bindingsTableBody) bindingsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
|
||||
if (searchParamsTableBody) searchParamsTableBody.innerHTML = '<tr><td colspan="4" class="text-center text-muted fst-italic">Loading...</td></tr>';
|
||||
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 = `<div class="alert alert-danger">Error loading structure: ${error.message}</div>`; }
|
||||
// ... existing error handling ...
|
||||
views.forEach(v => { const tc = document.getElementById(`structure-tree-${v}`); if(tc) tc.innerHTML = `<div class="alert alert-danger m-2">Error loading structure: ${error.message}</div>`; });
|
||||
if (constraintsTableBody) constraintsTableBody.innerHTML = `<tr><td colspan="5" class="text-danger text-center">Error loading constraints: ${error.message}</td></tr>`;
|
||||
if (bindingsTableBody) bindingsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading bindings: ${error.message}</td></tr>`;
|
||||
if (searchParamsTableBody) searchParamsTableBody.innerHTML = `<tr><td colspan="4" class="text-danger text-center">Error loading search params: ${error.message}</td></tr>`; // 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 = `<code>${escapeHtmlAttr(param.code || 'N/A')}</code>`;
|
||||
|
||||
// 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 = `<small>${escapeHtmlAttr(param.description || 'No description available.')}</small>`;
|
||||
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() {
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user