mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 21:29:59 +00:00
commit
05d23aa268
167
app.py
167
app.py
@ -20,7 +20,6 @@ logging.basicConfig(level=logging.DEBUG)
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
# --- Configuration ---
|
|
||||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-fallback-secret-key-here')
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your-fallback-secret-key-here')
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:////app/instance/fhir_ig.db')
|
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:////app/instance/fhir_ig.db')
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
@ -32,7 +31,6 @@ app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
|||||||
# Ensure directories exist and are writable
|
# Ensure directories exist and are writable
|
||||||
instance_path = '/app/instance'
|
instance_path = '/app/instance'
|
||||||
packages_path = app.config['FHIR_PACKAGES_DIR']
|
packages_path = app.config['FHIR_PACKAGES_DIR']
|
||||||
|
|
||||||
logger.debug(f"Instance path configuration: {instance_path}")
|
logger.debug(f"Instance path configuration: {instance_path}")
|
||||||
logger.debug(f"Database URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
logger.debug(f"Database URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
logger.debug(f"Packages path: {packages_path}")
|
logger.debug(f"Packages path: {packages_path}")
|
||||||
@ -49,7 +47,6 @@ except Exception as e:
|
|||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
csrf = CSRFProtect(app)
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
# --- Database Model ---
|
|
||||||
class ProcessedIg(db.Model):
|
class ProcessedIg(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
package_name = db.Column(db.String(128), nullable=False)
|
package_name = db.Column(db.String(128), nullable=False)
|
||||||
@ -61,10 +58,8 @@ class ProcessedIg(db.Model):
|
|||||||
complies_with_profiles = db.Column(db.JSON, nullable=True)
|
complies_with_profiles = db.Column(db.JSON, nullable=True)
|
||||||
imposed_profiles = db.Column(db.JSON, nullable=True)
|
imposed_profiles = db.Column(db.JSON, nullable=True)
|
||||||
optional_usage_elements = db.Column(db.JSON, nullable=True)
|
optional_usage_elements = db.Column(db.JSON, nullable=True)
|
||||||
|
|
||||||
__table_args__ = (db.UniqueConstraint('package_name', 'version', name='uq_package_version'),)
|
__table_args__ = (db.UniqueConstraint('package_name', 'version', name='uq_package_version'),)
|
||||||
|
|
||||||
# --- API Key Middleware ---
|
|
||||||
def check_api_key():
|
def check_api_key():
|
||||||
api_key = request.headers.get('X-API-Key')
|
api_key = request.headers.get('X-API-Key')
|
||||||
if not api_key and request.is_json:
|
if not api_key and request.is_json:
|
||||||
@ -78,47 +73,6 @@ def check_api_key():
|
|||||||
logger.debug("API key validated successfully")
|
logger.debug("API key validated successfully")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- Routes ---
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
return render_template('index.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
|
||||||
|
|
||||||
@app.route('/import-ig', methods=['GET', 'POST'])
|
|
||||||
def import_ig():
|
|
||||||
form = IgImportForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
name = form.package_name.data
|
|
||||||
version = form.package_version.data
|
|
||||||
dependency_mode = form.dependency_mode.data
|
|
||||||
try:
|
|
||||||
result = services.import_package_and_dependencies(name, version, dependency_mode=dependency_mode)
|
|
||||||
if result['errors'] and not result['downloaded']:
|
|
||||||
error_msg = result['errors'][0]
|
|
||||||
simplified_msg = error_msg
|
|
||||||
if "HTTP error" in error_msg and "404" in error_msg:
|
|
||||||
simplified_msg = "Package not found on registry (404). Check name and version."
|
|
||||||
elif "HTTP error" in error_msg:
|
|
||||||
simplified_msg = f"Registry error: {error_msg.split(': ', 1)[-1]}"
|
|
||||||
elif "Connection error" in error_msg:
|
|
||||||
simplified_msg = "Could not connect to the FHIR package registry."
|
|
||||||
flash(f"Failed to import {name}#{version}: {simplified_msg}", "error")
|
|
||||||
logger.error(f"Import failed critically for {name}#{version}: {error_msg}")
|
|
||||||
else:
|
|
||||||
if result['errors']:
|
|
||||||
flash(f"Partially imported {name}#{version} with errors during dependency processing. Check logs.", "warning")
|
|
||||||
for err in result['errors']: logger.warning(f"Import warning for {name}#{version}: {err}")
|
|
||||||
else:
|
|
||||||
flash(f"Successfully downloaded {name}#{version} and dependencies! Mode: {dependency_mode}", "success")
|
|
||||||
return redirect(url_for('view_igs'))
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Unexpected error during IG import: {str(e)}", exc_info=True)
|
|
||||||
flash(f"An unexpected error occurred downloading the IG: {str(e)}", "error")
|
|
||||||
else:
|
|
||||||
for field, errors in form.errors.items():
|
|
||||||
for error in errors:
|
|
||||||
flash(f"Error in {getattr(form, field).label.text}: {error}", "danger")
|
|
||||||
return render_template('import_ig.html', form=form, site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
|
||||||
|
|
||||||
def list_downloaded_packages(packages_dir):
|
def list_downloaded_packages(packages_dir):
|
||||||
packages = []
|
packages = []
|
||||||
errors = []
|
errors = []
|
||||||
@ -159,7 +113,6 @@ def list_downloaded_packages(packages_dir):
|
|||||||
else:
|
else:
|
||||||
logger.warning(f"Skipping package {filename} due to invalid name ('{name}') or version ('{version}')")
|
logger.warning(f"Skipping package {filename} due to invalid name ('{name}') or version ('{version}')")
|
||||||
errors.append(f"Invalid package {filename}: name='{name}', version='{version}'")
|
errors.append(f"Invalid package {filename}: name='{name}', version='{version}'")
|
||||||
# Build duplicate_groups
|
|
||||||
name_counts = {}
|
name_counts = {}
|
||||||
for pkg in packages:
|
for pkg in packages:
|
||||||
name = pkg['name']
|
name = pkg['name']
|
||||||
@ -172,6 +125,47 @@ def list_downloaded_packages(packages_dir):
|
|||||||
logger.debug(f"Duplicate groups: {duplicate_groups}")
|
logger.debug(f"Duplicate groups: {duplicate_groups}")
|
||||||
return packages, errors, duplicate_groups
|
return packages, errors, duplicate_groups
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return render_template('index.html', site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
||||||
|
|
||||||
|
@app.route('/import-ig', methods=['GET', 'POST'])
|
||||||
|
def import_ig():
|
||||||
|
form = IgImportForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
name = form.package_name.data
|
||||||
|
version = form.package_version.data
|
||||||
|
dependency_mode = form.dependency_mode.data
|
||||||
|
try:
|
||||||
|
result = services.import_package_and_dependencies(name, version, dependency_mode=dependency_mode)
|
||||||
|
if result['errors'] and not result['downloaded']:
|
||||||
|
error_msg = result['errors'][0]
|
||||||
|
simplified_msg = error_msg
|
||||||
|
if "HTTP error" in error_msg and "404" in error_msg:
|
||||||
|
simplified_msg = "Package not found on registry (404). Check name and version."
|
||||||
|
elif "HTTP error" in error_msg:
|
||||||
|
simplified_msg = f"Registry error: {error_msg.split(': ', 1)[-1]}"
|
||||||
|
elif "Connection error" in error_msg:
|
||||||
|
simplified_msg = "Could not connect to the FHIR package registry."
|
||||||
|
flash(f"Failed to import {name}#{version}: {simplified_msg}", "error")
|
||||||
|
logger.error(f"Import failed critically for {name}#{version}: {error_msg}")
|
||||||
|
else:
|
||||||
|
if result['errors']:
|
||||||
|
flash(f"Partially imported {name}#{version} with errors during dependency processing. Check logs.", "warning")
|
||||||
|
for err in result['errors']:
|
||||||
|
logger.warning(f"Import warning for {name}#{version}: {err}")
|
||||||
|
else:
|
||||||
|
flash(f"Successfully downloaded {name}#{version} and dependencies! Mode: {dependency_mode}", "success")
|
||||||
|
return redirect(url_for('view_igs'))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error during IG import: {str(e)}", exc_info=True)
|
||||||
|
flash(f"An unexpected error occurred downloading the IG: {str(e)}", "error")
|
||||||
|
else:
|
||||||
|
for field, errors in form.errors.items():
|
||||||
|
for error in errors:
|
||||||
|
flash(f"Error in {getattr(form, field).label.text}: {error}", "danger")
|
||||||
|
return render_template('import_ig.html', form=form, site_name='FHIRFLARE IG Toolkit', now=datetime.datetime.now())
|
||||||
|
|
||||||
@app.route('/view-igs')
|
@app.route('/view-igs')
|
||||||
def view_igs():
|
def view_igs():
|
||||||
form = FlaskForm()
|
form = FlaskForm()
|
||||||
@ -230,6 +224,8 @@ def process_ig():
|
|||||||
try:
|
try:
|
||||||
logger.info(f"Starting processing for {name}#{version} from file {filename}")
|
logger.info(f"Starting processing for {name}#{version} from file {filename}")
|
||||||
package_info = services.process_package_file(tgz_path)
|
package_info = services.process_package_file(tgz_path)
|
||||||
|
if package_info.get('errors'):
|
||||||
|
flash(f"Processing completed with errors for {name}#{version}: {', '.join(package_info['errors'])}", "warning")
|
||||||
optional_usage_dict = {
|
optional_usage_dict = {
|
||||||
info['name']: True
|
info['name']: True
|
||||||
for info in package_info.get('resource_types_info', [])
|
for info in package_info.get('resource_types_info', [])
|
||||||
@ -302,7 +298,8 @@ def delete_ig():
|
|||||||
errors.append(f"Could not delete metadata for {filename}: {e}")
|
errors.append(f"Could not delete metadata for {filename}: {e}")
|
||||||
logger.error(f"Error deleting {metadata_path}: {e}")
|
logger.error(f"Error deleting {metadata_path}: {e}")
|
||||||
if errors:
|
if errors:
|
||||||
for error in errors: flash(error, "error")
|
for error in errors:
|
||||||
|
flash(error, "error")
|
||||||
elif deleted_files:
|
elif deleted_files:
|
||||||
flash(f"Deleted: {', '.join(deleted_files)}", "success")
|
flash(f"Deleted: {', '.join(deleted_files)}", "success")
|
||||||
else:
|
else:
|
||||||
@ -356,9 +353,11 @@ def view_ig(processed_ig_id):
|
|||||||
base_list = [t for t in processed_ig.resource_types_info if not t.get('is_profile')]
|
base_list = [t for t in processed_ig.resource_types_info if not t.get('is_profile')]
|
||||||
examples_by_type = processed_ig.examples or {}
|
examples_by_type = processed_ig.examples or {}
|
||||||
optional_usage_elements = processed_ig.optional_usage_elements or {}
|
optional_usage_elements = processed_ig.optional_usage_elements or {}
|
||||||
logger.debug(f"Optional usage elements for {processed_ig.package_name}#{processed_ig.version}: {optional_usage_elements}")
|
|
||||||
complies_with_profiles = processed_ig.complies_with_profiles or []
|
complies_with_profiles = processed_ig.complies_with_profiles or []
|
||||||
imposed_profiles = processed_ig.imposed_profiles or []
|
imposed_profiles = processed_ig.imposed_profiles or []
|
||||||
|
logger.debug(f"Viewing IG {processed_ig.package_name}#{processed_ig.version}: "
|
||||||
|
f"{len(profile_list)} profiles, {len(base_list)} base resources, "
|
||||||
|
f"{len(optional_usage_elements)} optional elements")
|
||||||
return render_template('cp_view_processed_ig.html',
|
return render_template('cp_view_processed_ig.html',
|
||||||
title=f"View {processed_ig.package_name}#{processed_ig.version}",
|
title=f"View {processed_ig.package_name}#{processed_ig.version}",
|
||||||
processed_ig=processed_ig,
|
processed_ig=processed_ig,
|
||||||
@ -462,6 +461,7 @@ def get_example_content():
|
|||||||
package_version = request.args.get('package_version')
|
package_version = request.args.get('package_version')
|
||||||
example_member_path = request.args.get('filename')
|
example_member_path = request.args.get('filename')
|
||||||
if not all([package_name, package_version, example_member_path]):
|
if not all([package_name, package_version, example_member_path]):
|
||||||
|
logger.warning(f"get_example_content: Missing query parameters: name={package_name}, version={package_version}, path={example_member_path}")
|
||||||
return jsonify({"error": "Missing required query parameters: package_name, package_version, filename"}), 400
|
return jsonify({"error": "Missing required query parameters: package_name, package_version, filename"}), 400
|
||||||
if not example_member_path.startswith('package/') or '..' in example_member_path:
|
if not example_member_path.startswith('package/') or '..' in example_member_path:
|
||||||
logger.warning(f"Invalid example file path requested: {example_member_path}")
|
logger.warning(f"Invalid example file path requested: {example_member_path}")
|
||||||
@ -649,7 +649,8 @@ def api_push_ig():
|
|||||||
for dep in dependencies_to_include:
|
for dep in dependencies_to_include:
|
||||||
dep_name = dep.get('name')
|
dep_name = dep.get('name')
|
||||||
dep_version = dep.get('version')
|
dep_version = dep.get('version')
|
||||||
if not dep_name or not dep_version: continue
|
if not dep_name or not dep_version:
|
||||||
|
continue
|
||||||
dep_tgz_filename = services.construct_tgz_filename(dep_name, dep_version)
|
dep_tgz_filename = services.construct_tgz_filename(dep_name, dep_version)
|
||||||
dep_tgz_path = os.path.join(packages_dir, dep_tgz_filename)
|
dep_tgz_path = os.path.join(packages_dir, dep_tgz_filename)
|
||||||
if os.path.exists(dep_tgz_path):
|
if os.path.exists(dep_tgz_path):
|
||||||
@ -789,83 +790,61 @@ def api_push_ig():
|
|||||||
def validate_sample():
|
def validate_sample():
|
||||||
form = ValidationForm()
|
form = ValidationForm()
|
||||||
validation_report = None
|
validation_report = None
|
||||||
packages_for_template = []
|
packages = []
|
||||||
packages_dir = app.config.get('FHIR_PACKAGES_DIR')
|
packages_dir = app.config['FHIR_PACKAGES_DIR']
|
||||||
if packages_dir:
|
if os.path.exists(packages_dir):
|
||||||
|
for filename in os.listdir(packages_dir):
|
||||||
|
if filename.endswith('.tgz'):
|
||||||
try:
|
try:
|
||||||
all_packages, errors, duplicate_groups = list_downloaded_packages(packages_dir)
|
with tarfile.open(os.path.join(packages_dir, filename), 'r:gz') as tar:
|
||||||
if errors:
|
package_json = tar.extractfile('package/package.json')
|
||||||
flash(f"Warning: Errors encountered while listing packages: {', '.join(errors)}", "warning")
|
if package_json:
|
||||||
filtered_packages = [
|
pkg_info = json.load(package_json)
|
||||||
pkg for pkg in all_packages
|
name = pkg_info.get('name')
|
||||||
if isinstance(pkg.get('name'), str) and pkg.get('name') and
|
version = pkg_info.get('version')
|
||||||
isinstance(pkg.get('version'), str) and pkg.get('version')
|
if name and version:
|
||||||
]
|
packages.append({'name': name, 'version': version})
|
||||||
packages_for_template = sorted([
|
|
||||||
{"id": f"{pkg['name']}#{pkg['version']}", "text": f"{pkg['name']}#{pkg['version']}"}
|
|
||||||
for pkg in filtered_packages
|
|
||||||
], key=lambda x: x['text'])
|
|
||||||
logger.debug(f"Packages for template: {packages_for_template}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to list or process downloaded packages: {e}", exc_info=True)
|
logger.warning(f"Error reading package {filename}: {e}")
|
||||||
flash("Error loading available packages.", "danger")
|
continue
|
||||||
else:
|
|
||||||
flash("FHIR Packages directory not configured.", "danger")
|
|
||||||
logger.error("FHIR_PACKAGES_DIR is not configured in the Flask app.")
|
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
package_name = form.package_name.data
|
package_name = form.package_name.data
|
||||||
version = form.version.data
|
version = form.version.data
|
||||||
include_dependencies = form.include_dependencies.data
|
include_dependencies = form.include_dependencies.data
|
||||||
mode = form.mode.data
|
mode = form.mode.data
|
||||||
sample_input_raw = form.sample_input.data
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sample_input = json.loads(sample_input_raw)
|
sample_input = json.loads(form.sample_input.data)
|
||||||
logger.info(f"Starting validation (mode: {mode}) for {package_name}#{version}, deps: {include_dependencies}")
|
|
||||||
if mode == 'single':
|
if mode == 'single':
|
||||||
validation_report = services.validate_resource_against_profile(
|
validation_report = services.validate_resource_against_profile(
|
||||||
package_name, version, sample_input, include_dependencies
|
package_name, version, sample_input, include_dependencies
|
||||||
)
|
)
|
||||||
elif mode == 'bundle':
|
else:
|
||||||
validation_report = services.validate_bundle_against_profile(
|
validation_report = services.validate_bundle_against_profile(
|
||||||
package_name, version, sample_input, include_dependencies
|
package_name, version, sample_input, include_dependencies
|
||||||
)
|
)
|
||||||
else:
|
flash("Validation completed.", 'success')
|
||||||
flash("Invalid validation mode selected.", "error")
|
|
||||||
validation_report = None
|
|
||||||
if validation_report:
|
|
||||||
flash("Validation completed.", 'info')
|
|
||||||
logger.info(f"Validation Result: Valid={validation_report.get('valid')}, Errors={len(validation_report.get('errors',[]))}, Warnings={len(validation_report.get('warnings',[]))}")
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
flash("Invalid JSON format in sample input.", 'error')
|
flash("Invalid JSON format in sample input.", 'error')
|
||||||
logger.warning("Validation failed: Invalid JSON input.")
|
|
||||||
validation_report = {'valid': False, 'errors': ['Invalid JSON format provided.'], 'warnings': [], 'results': {}}
|
validation_report = {'valid': False, 'errors': ['Invalid JSON format provided.'], 'warnings': [], 'results': {}}
|
||||||
except FileNotFoundError as e:
|
|
||||||
flash(f"Validation Error: Required package file not found for {package_name}#{version}. Please ensure it's downloaded.", 'error')
|
|
||||||
logger.error(f"Validation failed: Package file missing - {e}")
|
|
||||||
validation_report = {'valid': False, 'errors': [f"Required package file not found: {package_name}#{version}"], 'warnings': [], 'results': {}}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error validating sample: {e}", exc_info=True)
|
logger.error(f"Error validating sample: {e}")
|
||||||
flash(f"An unexpected error occurred during validation: {str(e)}", 'error')
|
flash(f"Error validating sample: {str(e)}", 'error')
|
||||||
validation_report = {'valid': False, 'errors': [f'Unexpected error: {str(e)}'], 'warnings': [], 'results': {}}
|
validation_report = {'valid': False, 'errors': [str(e)], 'warnings': [], 'results': {}}
|
||||||
else:
|
else:
|
||||||
for field, errors in form.errors.items():
|
for field, errors in form.errors.items():
|
||||||
field_obj = getattr(form, field, None)
|
field_obj = getattr(form, field, None)
|
||||||
field_label = field_obj.label.text if field_obj and hasattr(field_obj, 'label') else field
|
field_label = field_obj.label.text if field_obj and hasattr(field_obj, 'label') else field
|
||||||
for error in errors:
|
for error in errors:
|
||||||
flash(f"Error in field '{field_label}': {error}", "danger")
|
flash(f"Error in field '{field_label}': {error}", "danger")
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
'validate_sample.html',
|
'validate_sample.html',
|
||||||
form=form,
|
form=form,
|
||||||
packages=packages_for_template,
|
packages=packages,
|
||||||
validation_report=validation_report,
|
validation_report=validation_report,
|
||||||
site_name='FHIRFLARE IG Toolkit',
|
site_name='FHIRFLARE IG Toolkit',
|
||||||
now=datetime.datetime.now()
|
now=datetime.datetime.now()
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- App Initialization ---
|
|
||||||
def create_db():
|
def create_db():
|
||||||
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
try:
|
try:
|
||||||
|
6
forms.py
6
forms.py
@ -1,18 +1,18 @@
|
|||||||
# forms.py
|
# forms.py
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField
|
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Regexp, Optional, ValidationError
|
from wtforms.validators import DataRequired, Regexp, ValidationError
|
||||||
import json
|
import json
|
||||||
|
|
||||||
class IgImportForm(FlaskForm):
|
class IgImportForm(FlaskForm):
|
||||||
package_name = StringField('Package Name', validators=[
|
package_name = StringField('Package Name', validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
Regexp(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$', message="Invalid package name format.")
|
Regexp(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$', message="Invalid package name format.")
|
||||||
])
|
], render_kw={'placeholder': 'e.g., hl7.fhir.au.core'})
|
||||||
package_version = StringField('Package Version', validators=[
|
package_version = StringField('Package Version', validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message="Invalid version format. Use alphanumeric characters, dots, or hyphens (e.g., 1.2.3, 1.1.0-preview, current).")
|
Regexp(r'^[a-zA-Z0-9\.\-]+$', message="Invalid version format. Use alphanumeric characters, dots, or hyphens (e.g., 1.2.3, 1.1.0-preview, current).")
|
||||||
])
|
], render_kw={'placeholder': 'e.g., 1.1.0-preview'})
|
||||||
dependency_mode = SelectField('Dependency Mode', choices=[
|
dependency_mode = SelectField('Dependency Mode', choices=[
|
||||||
('recursive', 'Current Recursive'),
|
('recursive', 'Current Recursive'),
|
||||||
('patch-canonical', 'Patch Canonical Versions'),
|
('patch-canonical', 'Patch Canonical Versions'),
|
||||||
|
Binary file not shown.
BIN
instance/fhir_ig.db.old
Normal file
BIN
instance/fhir_ig.db.old
Normal file
Binary file not shown.
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.au.base",
|
||||||
|
"version": "5.1.0-preview",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.terminology.r4",
|
||||||
|
"version": "6.2.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.uv.extensions.r4",
|
||||||
|
"version": "5.2.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"package_name": "hl7.fhir.au.core",
|
"package_name": "hl7.fhir.au.core",
|
||||||
"version": "1.1.0-preview",
|
"version": "1.1.0-preview",
|
||||||
"dependency_mode": "tree-shaking",
|
"dependency_mode": "recursive",
|
||||||
"imported_dependencies": [
|
"imported_dependencies": [
|
||||||
{
|
{
|
||||||
"name": "hl7.fhir.r4.core",
|
"name": "hl7.fhir.r4.core",
|
||||||
@ -29,6 +29,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"complies_with_profiles": [],
|
"complies_with_profiles": [],
|
||||||
"imposed_profiles": [],
|
"imposed_profiles": []
|
||||||
"timestamp": "2025-04-13T13:43:59.139683+00:00"
|
|
||||||
}
|
}
|
@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
"package_name": "hl7.fhir.r4.core",
|
"package_name": "hl7.fhir.r4.core",
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"dependency_mode": "tree-shaking",
|
"dependency_mode": "recursive",
|
||||||
"imported_dependencies": [],
|
"imported_dependencies": [],
|
||||||
"complies_with_profiles": [],
|
"complies_with_profiles": [],
|
||||||
"imposed_profiles": [],
|
"imposed_profiles": []
|
||||||
"timestamp": "2025-04-13T13:44:17.228391+00:00"
|
|
||||||
}
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.uv.extensions.r4",
|
||||||
|
"version": "5.2.0",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.extensions.r4-5.2.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.extensions.r4-5.2.0.tgz
Normal file
Binary file not shown.
21
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
21
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.uv.ipa",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.terminology.r4",
|
||||||
|
"version": "5.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.uv.smart-app-launch",
|
||||||
|
"version": "2.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.uv.smart-app-launch",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.uv.smart-app-launch",
|
||||||
|
"version": "2.1.0",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.terminology.r4",
|
||||||
|
"version": "5.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.1.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.1.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.terminology.r4",
|
||||||
|
"version": "5.0.0",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.terminology.r4-5.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.terminology.r4-5.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.terminology.r4",
|
||||||
|
"version": "6.2.0",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": []
|
||||||
|
}
|
BIN
instance/fhir_packages/hl7.terminology.r4-6.2.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.terminology.r4-6.2.0.tgz
Normal file
Binary file not shown.
234
services.py
234
services.py
@ -22,7 +22,7 @@ DOWNLOAD_DIR_NAME = "fhir_packages"
|
|||||||
CANONICAL_PACKAGE = ("hl7.fhir.r4.core", "4.0.1")
|
CANONICAL_PACKAGE = ("hl7.fhir.r4.core", "4.0.1")
|
||||||
CANONICAL_PACKAGE_ID = f"{CANONICAL_PACKAGE[0]}#{CANONICAL_PACKAGE[1]}"
|
CANONICAL_PACKAGE_ID = f"{CANONICAL_PACKAGE[0]}#{CANONICAL_PACKAGE[1]}"
|
||||||
|
|
||||||
# Define standard FHIR R4 base types (used selectively)
|
# Define standard FHIR R4 base types
|
||||||
FHIR_R4_BASE_TYPES = {
|
FHIR_R4_BASE_TYPES = {
|
||||||
"Account", "ActivityDefinition", "AdministrableProductDefinition", "AdverseEvent", "AllergyIntolerance",
|
"Account", "ActivityDefinition", "AdministrableProductDefinition", "AdverseEvent", "AllergyIntolerance",
|
||||||
"Appointment", "AppointmentResponse", "AuditEvent", "Basic", "Binary", "BiologicallyDerivedProduct",
|
"Appointment", "AppointmentResponse", "AuditEvent", "Basic", "Binary", "BiologicallyDerivedProduct",
|
||||||
@ -111,13 +111,7 @@ def construct_metadata_filename(name, version):
|
|||||||
return f"{sanitize_filename_part(name)}-{sanitize_filename_part(version)}.metadata.json"
|
return f"{sanitize_filename_part(name)}-{sanitize_filename_part(version)}.metadata.json"
|
||||||
|
|
||||||
def parse_package_filename(filename):
|
def parse_package_filename(filename):
|
||||||
"""
|
"""Parses a standard FHIR package filename into name and version."""
|
||||||
Parses a standard FHIR package filename (e.g., name-version.tgz)
|
|
||||||
into package name and version. Handles common variations.
|
|
||||||
Returns (name, version) tuple, or (None, None) if parsing fails.
|
|
||||||
"""
|
|
||||||
name = None
|
|
||||||
version = None
|
|
||||||
if not filename or not filename.endswith('.tgz'):
|
if not filename or not filename.endswith('.tgz'):
|
||||||
logger.debug(f"Filename '{filename}' does not end with .tgz")
|
logger.debug(f"Filename '{filename}' does not end with .tgz")
|
||||||
return None, None
|
return None, None
|
||||||
@ -133,20 +127,15 @@ def parse_package_filename(filename):
|
|||||||
return name, version
|
return name, version
|
||||||
else:
|
else:
|
||||||
last_hyphen_index = base_name.rfind('-', 0, last_hyphen_index)
|
last_hyphen_index = base_name.rfind('-', 0, last_hyphen_index)
|
||||||
logger.warning(f"Could not confidently parse version from '{filename}'. Treating '{base_name}' as name.")
|
logger.warning(f"Could not parse version from '{filename}'. Treating '{base_name}' as name.")
|
||||||
name = base_name.replace('_', '.')
|
name = base_name.replace('_', '.')
|
||||||
version = ""
|
version = ""
|
||||||
return name, version
|
return name, version
|
||||||
|
|
||||||
def find_and_extract_sd(tgz_path, resource_identifier):
|
def find_and_extract_sd(tgz_path, resource_identifier):
|
||||||
"""
|
"""Helper to find and extract StructureDefinition json from a tgz path."""
|
||||||
Helper to find and extract StructureDefinition json from a given tgz path.
|
|
||||||
Matches by resource ID, Name, or Type defined within the SD file.
|
|
||||||
Returns (sd_data, found_path_in_tar) or (None, None).
|
|
||||||
"""
|
|
||||||
sd_data = None
|
sd_data = None
|
||||||
found_path = None
|
found_path = None
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
if not tgz_path or not os.path.exists(tgz_path):
|
if not tgz_path or not os.path.exists(tgz_path):
|
||||||
logger.error(f"File not found in find_and_extract_sd: {tgz_path}")
|
logger.error(f"File not found in find_and_extract_sd: {tgz_path}")
|
||||||
return None, None
|
return None, None
|
||||||
@ -371,7 +360,6 @@ def process_package_file(tgz_path):
|
|||||||
|
|
||||||
if must_support is True:
|
if must_support is True:
|
||||||
if element_id and element_path:
|
if element_id and element_path:
|
||||||
# Use id for sliced elements to ensure uniqueness
|
|
||||||
ms_path = element_id if slice_name else element_path
|
ms_path = element_id if slice_name else element_path
|
||||||
ms_paths_in_this_sd.add(ms_path)
|
ms_paths_in_this_sd.add(ms_path)
|
||||||
has_ms_in_this_sd = True
|
has_ms_in_this_sd = True
|
||||||
@ -432,7 +420,6 @@ def process_package_file(tgz_path):
|
|||||||
resource_type = data.get('resourceType')
|
resource_type = data.get('resourceType')
|
||||||
if not resource_type: continue
|
if not resource_type: continue
|
||||||
|
|
||||||
# Prioritize meta.profile matches (from oldest version for reliability)
|
|
||||||
profile_meta = data.get('meta', {}).get('profile', [])
|
profile_meta = data.get('meta', {}).get('profile', [])
|
||||||
found_profile_match = False
|
found_profile_match = False
|
||||||
if profile_meta and isinstance(profile_meta, list):
|
if profile_meta and isinstance(profile_meta, list):
|
||||||
@ -446,11 +433,10 @@ def process_package_file(tgz_path):
|
|||||||
break
|
break
|
||||||
elif profile_url in resource_info:
|
elif profile_url in resource_info:
|
||||||
associated_key = profile_url
|
associated_key = profile_url
|
||||||
found_profile_match = True
|
personally_match = True
|
||||||
logger.debug(f"Example {member.name} associated with profile {associated_key} via meta.profile")
|
logger.debug(f"Example {member.name} associated with profile {associated_key} via meta.profile")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Fallback to base type
|
|
||||||
if not found_profile_match:
|
if not found_profile_match:
|
||||||
key_to_use = resource_type
|
key_to_use = resource_type
|
||||||
if key_to_use not in resource_info:
|
if key_to_use not in resource_info:
|
||||||
@ -459,7 +445,6 @@ def process_package_file(tgz_path):
|
|||||||
logger.debug(f"Example {member.name} associated with resource type {associated_key}")
|
logger.debug(f"Example {member.name} associated with resource type {associated_key}")
|
||||||
referenced_types.add(resource_type)
|
referenced_types.add(resource_type)
|
||||||
else:
|
else:
|
||||||
# Handle non-JSON examples (from oldest version)
|
|
||||||
guessed_type = base_filename_lower.split('-')[0].capitalize()
|
guessed_type = base_filename_lower.split('-')[0].capitalize()
|
||||||
guessed_profile_id = base_filename_lower.split('-')[0]
|
guessed_profile_id = base_filename_lower.split('-')[0]
|
||||||
key_to_use = None
|
key_to_use = None
|
||||||
@ -574,16 +559,12 @@ def process_package_file(tgz_path):
|
|||||||
|
|
||||||
# --- Validation Functions ---
|
# --- Validation Functions ---
|
||||||
def navigate_fhir_path(resource, path, extension_url=None):
|
def navigate_fhir_path(resource, path, extension_url=None):
|
||||||
"""
|
"""Navigates a FHIR resource using a FHIRPath-like expression."""
|
||||||
Navigates a FHIR resource using a FHIRPath-like expression.
|
|
||||||
Returns the value(s) at the specified path or None if not found.
|
|
||||||
"""
|
|
||||||
if not resource or not path:
|
if not resource or not path:
|
||||||
return None
|
return None
|
||||||
parts = path.split('.')
|
parts = path.split('.')
|
||||||
current = resource
|
current = resource
|
||||||
for part in parts:
|
for part in parts:
|
||||||
# Handle array indexing (e.g., element[0])
|
|
||||||
match = re.match(r'^(\w+)\[(\d+)\]$', part)
|
match = re.match(r'^(\w+)\[(\d+)\]$', part)
|
||||||
if match:
|
if match:
|
||||||
key, index = match.groups()
|
key, index = match.groups()
|
||||||
@ -596,21 +577,16 @@ def navigate_fhir_path(resource, path, extension_url=None):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
# Handle regular path components
|
|
||||||
if isinstance(current, dict) and part in current:
|
if isinstance(current, dict) and part in current:
|
||||||
current = current[part]
|
current = current[part]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
# Handle extension filtering
|
|
||||||
if extension_url and isinstance(current, list):
|
if extension_url and isinstance(current, list):
|
||||||
current = [item for item in current if item.get('url') == extension_url]
|
current = [item for item in current if item.get('url') == extension_url]
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def validate_resource_against_profile(package_name, version, resource, include_dependencies=True):
|
def validate_resource_against_profile(package_name, version, resource, include_dependencies=True):
|
||||||
"""
|
"""Validates a FHIR resource against a StructureDefinition in the specified package."""
|
||||||
Validates a FHIR resource against a StructureDefinition in the specified package.
|
|
||||||
Returns a dict with 'valid', 'errors', and 'warnings'.
|
|
||||||
"""
|
|
||||||
logger.debug(f"Validating resource {resource.get('resourceType')} against {package_name}#{version}")
|
logger.debug(f"Validating resource {resource.get('resourceType')} against {package_name}#{version}")
|
||||||
result = {'valid': True, 'errors': [], 'warnings': []}
|
result = {'valid': True, 'errors': [], 'warnings': []}
|
||||||
download_dir = _get_download_dir()
|
download_dir = _get_download_dir()
|
||||||
@ -629,14 +605,13 @@ def validate_resource_against_profile(package_name, version, resource, include_d
|
|||||||
# Basic validation: check required elements and Must Support
|
# Basic validation: check required elements and Must Support
|
||||||
elements = sd_data.get('snapshot', {}).get('element', [])
|
elements = sd_data.get('snapshot', {}).get('element', [])
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if element.get('min', 0) > 0:
|
|
||||||
path = element.get('path')
|
path = element.get('path')
|
||||||
|
if element.get('min', 0) > 0:
|
||||||
value = navigate_fhir_path(resource, path)
|
value = navigate_fhir_path(resource, path)
|
||||||
if value is None or (isinstance(value, list) and not value):
|
if value is None or (isinstance(value, list) and not value):
|
||||||
result['valid'] = False
|
result['valid'] = False
|
||||||
result['errors'].append(f"Required element {path} missing")
|
result['errors'].append(f"Required element {path} missing")
|
||||||
if element.get('mustSupport', False):
|
if element.get('mustSupport', False):
|
||||||
path = element.get('path')
|
|
||||||
value = navigate_fhir_path(resource, path)
|
value = navigate_fhir_path(resource, path)
|
||||||
if value is None or (isinstance(value, list) and not value):
|
if value is None or (isinstance(value, list) and not value):
|
||||||
result['warnings'].append(f"Must Support element {path} missing or empty")
|
result['warnings'].append(f"Must Support element {path} missing or empty")
|
||||||
@ -644,10 +619,7 @@ def validate_resource_against_profile(package_name, version, resource, include_d
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def validate_bundle_against_profile(package_name, version, bundle, include_dependencies=True):
|
def validate_bundle_against_profile(package_name, version, bundle, include_dependencies=True):
|
||||||
"""
|
"""Validates a FHIR Bundle against profiles in the specified package."""
|
||||||
Validates a FHIR Bundle against profiles in the specified package.
|
|
||||||
Returns a dict with 'valid', 'errors', 'warnings', and per-resource 'results'.
|
|
||||||
"""
|
|
||||||
logger.debug(f"Validating bundle against {package_name}#{version}")
|
logger.debug(f"Validating bundle against {package_name}#{version}")
|
||||||
result = {
|
result = {
|
||||||
'valid': True,
|
'valid': True,
|
||||||
@ -675,6 +647,74 @@ def validate_bundle_against_profile(package_name, version, bundle, include_depen
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# --- Structure Definition Retrieval ---
|
||||||
|
def get_structure_definition(package_name, version, resource_type):
|
||||||
|
"""Fetches StructureDefinition with slicing support."""
|
||||||
|
download_dir = _get_download_dir()
|
||||||
|
if not download_dir:
|
||||||
|
logger.error("Could not get download directory.")
|
||||||
|
return {'error': 'Download directory not accessible'}
|
||||||
|
|
||||||
|
tgz_filename = construct_tgz_filename(package_name, version)
|
||||||
|
tgz_path = os.path.join(download_dir, tgz_filename)
|
||||||
|
sd_data, sd_path = find_and_extract_sd(tgz_path, resource_type)
|
||||||
|
|
||||||
|
if not sd_data:
|
||||||
|
# Fallback to canonical package
|
||||||
|
canonical_tgz = construct_tgz_filename(*CANONICAL_PACKAGE)
|
||||||
|
canonical_path = os.path.join(download_dir, canonical_tgz)
|
||||||
|
sd_data, sd_path = find_and_extract_sd(canonical_path, resource_type)
|
||||||
|
if sd_data:
|
||||||
|
logger.info(f"Using canonical SD for {resource_type} from {canonical_path}")
|
||||||
|
elements = sd_data.get('snapshot', {}).get('element', [])
|
||||||
|
return {
|
||||||
|
'elements': elements,
|
||||||
|
'must_support_paths': [el['path'] for el in elements if el.get('mustSupport', False)],
|
||||||
|
'slices': [],
|
||||||
|
'fallback_used': True,
|
||||||
|
'source_package': f"{CANONICAL_PACKAGE[0]}#{CANONICAL_PACKAGE[1]}"
|
||||||
|
}
|
||||||
|
logger.error(f"No StructureDefinition found for {resource_type} in {package_name}#{version} or canonical package")
|
||||||
|
return {'error': f"No StructureDefinition for {resource_type}"}
|
||||||
|
|
||||||
|
elements = sd_data.get('snapshot', {}).get('element', [])
|
||||||
|
must_support_paths = []
|
||||||
|
slices = []
|
||||||
|
|
||||||
|
# Process elements for must-support and slicing
|
||||||
|
for element in elements:
|
||||||
|
path = element.get('path', '')
|
||||||
|
element_id = element.get('id', '')
|
||||||
|
slice_name = element.get('sliceName')
|
||||||
|
if element.get('mustSupport', False):
|
||||||
|
ms_path = element_id if slice_name else path
|
||||||
|
must_support_paths.append(ms_path)
|
||||||
|
if 'slicing' in element:
|
||||||
|
slice_info = {
|
||||||
|
'path': path,
|
||||||
|
'sliceName': slice_name,
|
||||||
|
'discriminator': element.get('slicing', {}).get('discriminator', []),
|
||||||
|
'nested_slices': []
|
||||||
|
}
|
||||||
|
# Find nested slices
|
||||||
|
for sub_element in elements:
|
||||||
|
if sub_element['path'].startswith(path + '.') and 'slicing' in sub_element:
|
||||||
|
sub_slice_name = sub_element.get('sliceName')
|
||||||
|
slice_info['nested_slices'].append({
|
||||||
|
'path': sub_element['path'],
|
||||||
|
'sliceName': sub_slice_name,
|
||||||
|
'discriminator': sub_element.get('slicing', {}).get('discriminator', [])
|
||||||
|
})
|
||||||
|
slices.append(slice_info)
|
||||||
|
|
||||||
|
logger.debug(f"StructureDefinition for {resource_type}: {len(elements)} elements, {len(must_support_paths)} must-support paths, {len(slices)} slices")
|
||||||
|
return {
|
||||||
|
'elements': elements,
|
||||||
|
'must_support_paths': sorted(list(set(must_support_paths))),
|
||||||
|
'slices': slices,
|
||||||
|
'fallback_used': False
|
||||||
|
}
|
||||||
|
|
||||||
# --- Other Service Functions ---
|
# --- Other Service Functions ---
|
||||||
def _build_package_index(download_dir):
|
def _build_package_index(download_dir):
|
||||||
"""Builds an index of canonical URLs to package details from .index.json files."""
|
"""Builds an index of canonical URLs to package details from .index.json files."""
|
||||||
@ -721,7 +761,7 @@ def _load_definition(details, download_dir):
|
|||||||
"""Loads a StructureDefinition from package details."""
|
"""Loads a StructureDefinition from package details."""
|
||||||
if not details:
|
if not details:
|
||||||
return None
|
return None
|
||||||
tgz_path = os.path.join(download_dir, _construct_tgz_filename(details['package_name'], details['package_version']))
|
tgz_path = os.path.join(download_dir, construct_tgz_filename(details['package_name'], details['package_version']))
|
||||||
try:
|
try:
|
||||||
with tarfile.open(tgz_path, "r:gz") as tar:
|
with tarfile.open(tgz_path, "r:gz") as tar:
|
||||||
member_path = f"package/{details['filename']}"
|
member_path = f"package/{details['filename']}"
|
||||||
@ -735,7 +775,9 @@ def _load_definition(details, download_dir):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to load definition {details['filename']} from {tgz_path}: {e}")
|
logger.error(f"Failed to load definition {details['filename']} from {tgz_path}: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def download_package(name, version):
|
def download_package(name, version):
|
||||||
|
"""Downloads a single FHIR package."""
|
||||||
download_dir = _get_download_dir()
|
download_dir = _get_download_dir()
|
||||||
if not download_dir: return None, "Download dir error"
|
if not download_dir: return None, "Download dir error"
|
||||||
filename = construct_tgz_filename(name, version)
|
filename = construct_tgz_filename(name, version)
|
||||||
@ -760,6 +802,7 @@ def download_package(name, version):
|
|||||||
return None, f"File write error: {e}"
|
return None, f"File write error: {e}"
|
||||||
|
|
||||||
def extract_dependencies(tgz_path):
|
def extract_dependencies(tgz_path):
|
||||||
|
"""Extracts dependencies from package.json."""
|
||||||
package_json_path = "package/package.json"
|
package_json_path = "package/package.json"
|
||||||
dependencies = {}
|
dependencies = {}
|
||||||
error_message = None
|
error_message = None
|
||||||
@ -782,18 +825,14 @@ def extract_used_types(tgz_path):
|
|||||||
used_types = set()
|
used_types = set()
|
||||||
if not tgz_path or not os.path.exists(tgz_path):
|
if not tgz_path or not os.path.exists(tgz_path):
|
||||||
logger.error(f"Cannot extract used types: File not found at {tgz_path}")
|
logger.error(f"Cannot extract used types: File not found at {tgz_path}")
|
||||||
return used_types # Return empty set
|
return used_types
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with tarfile.open(tgz_path, "r:gz") as tar:
|
with tarfile.open(tgz_path, "r:gz") as tar:
|
||||||
for member in tar:
|
for member in tar:
|
||||||
# Process only JSON files within the 'package/' directory
|
|
||||||
if not (member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json')):
|
if not (member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json')):
|
||||||
continue
|
continue
|
||||||
# Skip metadata files
|
|
||||||
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']:
|
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
fileobj = None
|
fileobj = None
|
||||||
try:
|
try:
|
||||||
fileobj = tar.extractfile(member)
|
fileobj = tar.extractfile(member)
|
||||||
@ -801,71 +840,40 @@ def extract_used_types(tgz_path):
|
|||||||
content_bytes = fileobj.read()
|
content_bytes = fileobj.read()
|
||||||
content_string = content_bytes.decode('utf-8-sig')
|
content_string = content_bytes.decode('utf-8-sig')
|
||||||
data = json.loads(content_string)
|
data = json.loads(content_string)
|
||||||
|
if not isinstance(data, dict): continue
|
||||||
if not isinstance(data, dict): continue # Skip if not a valid JSON object
|
|
||||||
|
|
||||||
resource_type = data.get('resourceType')
|
resource_type = data.get('resourceType')
|
||||||
if not resource_type: continue # Skip if no resourceType
|
if not resource_type: continue
|
||||||
|
|
||||||
# Add the resource type itself
|
|
||||||
used_types.add(resource_type)
|
used_types.add(resource_type)
|
||||||
|
|
||||||
# --- StructureDefinition Specific Extraction ---
|
|
||||||
if resource_type == 'StructureDefinition':
|
if resource_type == 'StructureDefinition':
|
||||||
# Add the type this SD defines/constrains
|
|
||||||
sd_type = data.get('type')
|
sd_type = data.get('type')
|
||||||
if sd_type: used_types.add(sd_type)
|
if sd_type: used_types.add(sd_type)
|
||||||
# Add the base definition type if it's a profile
|
|
||||||
base_def = data.get('baseDefinition')
|
base_def = data.get('baseDefinition')
|
||||||
if base_def:
|
if base_def:
|
||||||
base_type = base_def.split('/')[-1]
|
base_type = base_def.split('/')[-1]
|
||||||
# Avoid adding primitive types like 'Element', 'Resource' etc. if not needed
|
if base_type and base_type[0].isupper(): used_types.add(base_type)
|
||||||
if base_type and base_type[0].isupper():
|
|
||||||
used_types.add(base_type)
|
|
||||||
|
|
||||||
# Extract types from elements (snapshot or differential)
|
|
||||||
elements = data.get('snapshot', {}).get('element', []) or data.get('differential', {}).get('element', [])
|
elements = data.get('snapshot', {}).get('element', []) or data.get('differential', {}).get('element', [])
|
||||||
for element in elements:
|
for element in elements:
|
||||||
if isinstance(element, dict) and 'type' in element:
|
if isinstance(element, dict) and 'type' in element:
|
||||||
for t in element.get('type', []):
|
for t in element.get('type', []):
|
||||||
# Add code (element type)
|
|
||||||
code = t.get('code')
|
code = t.get('code')
|
||||||
if code and code[0].isupper(): used_types.add(code)
|
if code and code[0].isupper(): used_types.add(code)
|
||||||
# Add targetProfile types (Reference targets)
|
|
||||||
for profile_uri in t.get('targetProfile', []):
|
for profile_uri in t.get('targetProfile', []):
|
||||||
if profile_uri:
|
if profile_uri:
|
||||||
profile_type = profile_uri.split('/')[-1]
|
profile_type = profile_uri.split('/')[-1]
|
||||||
if profile_type and profile_type[0].isupper(): used_types.add(profile_type)
|
if profile_type and profile_type[0].isupper(): used_types.add(profile_type)
|
||||||
# Add types from contentReference
|
|
||||||
content_ref = element.get('contentReference')
|
|
||||||
if content_ref and content_ref.startswith('#'):
|
|
||||||
# This usually points to another element path within the same SD
|
|
||||||
# Trying to resolve this fully can be complex.
|
|
||||||
# We might infer types based on the path referenced if needed.
|
|
||||||
pass
|
|
||||||
|
|
||||||
# --- General Resource Type Extraction ---
|
|
||||||
else:
|
else:
|
||||||
# Look for meta.profile for referenced profiles -> add profile type
|
|
||||||
profiles = data.get('meta', {}).get('profile', [])
|
profiles = data.get('meta', {}).get('profile', [])
|
||||||
for profile_uri in profiles:
|
for profile_uri in profiles:
|
||||||
if profile_uri:
|
if profile_uri:
|
||||||
profile_type = profile_uri.split('/')[-1]
|
profile_type = profile_uri.split('/')[-1]
|
||||||
if profile_type and profile_type[0].isupper(): used_types.add(profile_type)
|
if profile_type and profile_type[0].isupper(): used_types.add(profile_type)
|
||||||
|
|
||||||
# ValueSet: Check compose.include.system (often points to CodeSystem)
|
|
||||||
if resource_type == 'ValueSet':
|
if resource_type == 'ValueSet':
|
||||||
for include in data.get('compose', {}).get('include', []):
|
for include in data.get('compose', {}).get('include', []):
|
||||||
system = include.get('system')
|
system = include.get('system')
|
||||||
# Heuristic: If it looks like a FHIR core codesystem URL, extract type
|
|
||||||
if system and system.startswith('http://hl7.org/fhir/'):
|
if system and system.startswith('http://hl7.org/fhir/'):
|
||||||
type_name = system.split('/')[-1]
|
type_name = system.split('/')[-1]
|
||||||
# Check if it looks like a ResourceType
|
if type_name and type_name[0].isupper() and not type_name.startswith('sid'):
|
||||||
if type_name and type_name[0].isupper() and not type_name.startswith('sid'): # Avoid things like sid/us-ssn
|
|
||||||
used_types.add(type_name)
|
used_types.add(type_name)
|
||||||
# Could add more heuristics for other terminology servers
|
|
||||||
|
|
||||||
# CapabilityStatement: Check rest.resource.type and rest.resource.profile
|
|
||||||
if resource_type == 'CapabilityStatement':
|
if resource_type == 'CapabilityStatement':
|
||||||
for rest_item in data.get('rest', []):
|
for rest_item in data.get('rest', []):
|
||||||
for resource_item in rest_item.get('resource', []):
|
for resource_item in rest_item.get('resource', []):
|
||||||
@ -875,60 +883,36 @@ def extract_used_types(tgz_path):
|
|||||||
if profile_uri:
|
if profile_uri:
|
||||||
profile_type = profile_uri.split('/')[-1]
|
profile_type = profile_uri.split('/')[-1]
|
||||||
if profile_type and profile_type[0].isupper(): used_types.add(profile_type)
|
if profile_type and profile_type[0].isupper(): used_types.add(profile_type)
|
||||||
|
|
||||||
|
|
||||||
# --- Generic recursive search for 'reference' fields? ---
|
|
||||||
# This could be expensive. Let's rely on SDs for now.
|
|
||||||
# def find_references(obj):
|
|
||||||
# if isinstance(obj, dict):
|
|
||||||
# for k, v in obj.items():
|
|
||||||
# if k == 'reference' and isinstance(v, str):
|
|
||||||
# ref_type = v.split('/')[0]
|
|
||||||
# if ref_type and ref_type[0].isupper(): used_types.add(ref_type)
|
|
||||||
# else:
|
|
||||||
# find_references(v)
|
|
||||||
# elif isinstance(obj, list):
|
|
||||||
# for item in obj:
|
|
||||||
# find_references(item)
|
|
||||||
# find_references(data)
|
|
||||||
|
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
logger.warning(f"Could not parse JSON in {member.name} for used types: {e}")
|
logger.warning(f"Could not parse JSON in {member.name}: {e}")
|
||||||
except UnicodeDecodeError as e:
|
except UnicodeDecodeError as e:
|
||||||
logger.warning(f"Could not decode {member.name} for used types: {e}")
|
logger.warning(f"Could not decode {member.name}: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not process member {member.name} for used types: {e}")
|
logger.warning(f"Could not process member {member.name}: {e}")
|
||||||
finally:
|
finally:
|
||||||
if fileobj:
|
if fileobj:
|
||||||
fileobj.close()
|
fileobj.close()
|
||||||
|
|
||||||
except tarfile.ReadError as e:
|
except tarfile.ReadError as e:
|
||||||
logger.error(f"Tar ReadError extracting used types from {tgz_path}: {e}")
|
logger.error(f"Tar ReadError extracting used types from {tgz_path}: {e}")
|
||||||
except tarfile.TarError as e:
|
except tarfile.TarError as e:
|
||||||
logger.error(f"TarError extracting used types from {tgz_path}: {e}")
|
logger.error(f"TarError extracting used types from {tgz_path}: {e}")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.error(f"Package file not found for used type extraction: {tgz_path}")
|
logger.error(f"Package file not found: {tgz_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error extracting used types from {tgz_path}: {e}", exc_info=True)
|
logger.error(f"Error extracting used types from {tgz_path}: {e}", exc_info=True)
|
||||||
|
core_non_resource_types = {
|
||||||
# Filter out potential primitives or base types that aren't resources?
|
'string', 'boolean', 'integer', 'decimal', 'uri', 'url', 'canonical', 'base64Binary', 'instant',
|
||||||
# E.g., 'string', 'boolean', 'Element', 'BackboneElement', 'Resource'
|
'date', 'dateTime', 'time', 'code', 'oid', 'id', 'markdown', 'unsignedInt', 'positiveInt', 'xhtml',
|
||||||
core_non_resource_types = {'string', 'boolean', 'integer', 'decimal', 'uri', 'url', 'canonical',
|
'Element', 'BackboneElement', 'Resource', 'DomainResource', 'DataType'
|
||||||
'base64Binary', 'instant', 'date', 'dateTime', 'time', 'code', 'oid', 'id',
|
}
|
||||||
'markdown', 'unsignedInt', 'positiveInt', 'xhtml',
|
|
||||||
'Element', 'BackboneElement', 'Resource', 'DomainResource', 'DataType'}
|
|
||||||
final_used_types = {t for t in used_types if t not in core_non_resource_types and t[0].isupper()}
|
final_used_types = {t for t in used_types if t not in core_non_resource_types and t[0].isupper()}
|
||||||
|
|
||||||
logger.debug(f"Extracted used types from {os.path.basename(tgz_path)}: {final_used_types}")
|
logger.debug(f"Extracted used types from {os.path.basename(tgz_path)}: {final_used_types}")
|
||||||
return final_used_types
|
return final_used_types
|
||||||
|
|
||||||
|
|
||||||
def map_types_to_packages(used_types, all_dependencies, download_dir):
|
def map_types_to_packages(used_types, all_dependencies, download_dir):
|
||||||
"""Maps used types to packages by checking .index.json files for exported types."""
|
"""Maps used types to packages by checking .index.json files."""
|
||||||
type_to_package = {}
|
type_to_package = {}
|
||||||
processed_types = set()
|
processed_types = set()
|
||||||
|
|
||||||
# Load .index.json from each package to map types
|
|
||||||
for (pkg_name, pkg_version), _ in all_dependencies.items():
|
for (pkg_name, pkg_version), _ in all_dependencies.items():
|
||||||
tgz_filename = construct_tgz_filename(pkg_name, pkg_version)
|
tgz_filename = construct_tgz_filename(pkg_name, pkg_version)
|
||||||
tgz_path = os.path.join(download_dir, tgz_filename)
|
tgz_path = os.path.join(download_dir, tgz_filename)
|
||||||
@ -950,30 +934,25 @@ def map_types_to_packages(used_types, all_dependencies, download_dir):
|
|||||||
if sd_name in used_types:
|
if sd_name in used_types:
|
||||||
type_to_package[sd_name] = (pkg_name, pkg_version)
|
type_to_package[sd_name] = (pkg_name, pkg_version)
|
||||||
processed_types.add(sd_name)
|
processed_types.add(sd_name)
|
||||||
logger.debug(f"Mapped type '{sd_name}' to package '{pkg_name}#{pkg_version}' via .index.json")
|
logger.debug(f"Mapped type '{sd_name}' to package '{pkg_name}#{pkg_version}'")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to process .index.json for {pkg_name}#{pkg_version}: {e}")
|
logger.warning(f"Failed to process .index.json for {pkg_name}#{pkg_version}: {e}")
|
||||||
|
|
||||||
# Fallback: Use heuristic matching for unmapped types
|
|
||||||
for t in used_types - processed_types:
|
for t in used_types - processed_types:
|
||||||
for (pkg_name, pkg_version), _ in all_dependencies.items():
|
for (pkg_name, pkg_version), _ in all_dependencies.items():
|
||||||
if t.lower() in pkg_name.lower():
|
if t.lower() in pkg_name.lower():
|
||||||
type_to_package[t] = (pkg_name, pkg_version)
|
type_to_package[t] = (pkg_name, pkg_version)
|
||||||
processed_types.add(t)
|
processed_types.add(t)
|
||||||
logger.debug(f"Fallback: Mapped type '{t}' to package '{pkg_name}#{pkg_version}' via name heuristic")
|
logger.debug(f"Fallback: Mapped type '{t}' to package '{pkg_name}#{pkg_version}'")
|
||||||
break
|
break
|
||||||
|
|
||||||
# Final fallback: Map remaining types to canonical package
|
|
||||||
canonical_name, canonical_version = CANONICAL_PACKAGE
|
canonical_name, canonical_version = CANONICAL_PACKAGE
|
||||||
for t in used_types - processed_types:
|
for t in used_types - processed_types:
|
||||||
type_to_package[t] = CANONICAL_PACKAGE
|
type_to_package[t] = CANONICAL_PACKAGE
|
||||||
logger.debug(f"Fallback: Mapped type '{t}' to canonical package {canonical_name}#{canonical_version}")
|
logger.debug(f"Fallback: Mapped type '{t}' to canonical package {canonical_name}#{canonical_version}")
|
||||||
|
|
||||||
logger.debug(f"Final type-to-package mapping: {type_to_package}")
|
logger.debug(f"Final type-to-package mapping: {type_to_package}")
|
||||||
return type_to_package
|
return type_to_package
|
||||||
|
|
||||||
def import_package_and_dependencies(initial_name, initial_version, dependency_mode='recursive'):
|
def import_package_and_dependencies(initial_name, initial_version, dependency_mode='recursive'):
|
||||||
"""Orchestrates recursive download and dependency extraction based on the dependency mode."""
|
"""Orchestrates recursive download and dependency extraction."""
|
||||||
logger.info(f"Starting import for {initial_name}#{initial_version} with dependency_mode={dependency_mode}")
|
logger.info(f"Starting import for {initial_name}#{initial_version} with dependency_mode={dependency_mode}")
|
||||||
download_dir = _get_download_dir()
|
download_dir = _get_download_dir()
|
||||||
if not download_dir:
|
if not download_dir:
|
||||||
@ -994,17 +973,14 @@ def import_package_and_dependencies(initial_name, initial_version, dependency_mo
|
|||||||
while pending_queue:
|
while pending_queue:
|
||||||
name, version = pending_queue.pop(0)
|
name, version = pending_queue.pop(0)
|
||||||
package_id_tuple = (name, version)
|
package_id_tuple = (name, version)
|
||||||
|
|
||||||
if package_id_tuple in results['processed']:
|
if package_id_tuple in results['processed']:
|
||||||
logger.debug(f"Skipping already processed package: {name}#{version}")
|
logger.debug(f"Skipping already processed package: {name}#{version}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"Processing package from queue: {name}#{version}")
|
logger.info(f"Processing package from queue: {name}#{version}")
|
||||||
save_path, dl_error = download_package(name, version)
|
save_path, dl_error = download_package(name, version)
|
||||||
if dl_error:
|
if dl_error:
|
||||||
results['errors'].append(f"Download failed for {name}#{version}: {dl_error}")
|
results['errors'].append(f"Download failed for {name}#{version}: {dl_error}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
results['downloaded'][package_id_tuple] = save_path
|
results['downloaded'][package_id_tuple] = save_path
|
||||||
dependencies, dep_error = extract_dependencies(save_path)
|
dependencies, dep_error = extract_dependencies(save_path)
|
||||||
if dep_error:
|
if dep_error:
|
||||||
@ -1015,10 +991,8 @@ def import_package_and_dependencies(initial_name, initial_version, dependency_mo
|
|||||||
results['errors'].append(f"Dependency extraction returned critical error for {name}#{version}.")
|
results['errors'].append(f"Dependency extraction returned critical error for {name}#{version}.")
|
||||||
results['processed'].add(package_id_tuple)
|
results['processed'].add(package_id_tuple)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
results['all_dependencies'][package_id_tuple] = dependencies
|
results['all_dependencies'][package_id_tuple] = dependencies
|
||||||
results['processed'].add(package_id_tuple)
|
results['processed'].add(package_id_tuple)
|
||||||
|
|
||||||
current_package_deps = []
|
current_package_deps = []
|
||||||
for dep_name, dep_version in dependencies.items():
|
for dep_name, dep_version in dependencies.items():
|
||||||
if isinstance(dep_name, str) and isinstance(dep_version, str) and dep_name and dep_version:
|
if isinstance(dep_name, str) and isinstance(dep_version, str) and dep_name and dep_version:
|
||||||
@ -1027,22 +1001,17 @@ def import_package_and_dependencies(initial_name, initial_version, dependency_mo
|
|||||||
if dep_tuple not in all_found_dependencies:
|
if dep_tuple not in all_found_dependencies:
|
||||||
all_found_dependencies.add(dep_tuple)
|
all_found_dependencies.add(dep_tuple)
|
||||||
results['dependencies'].append({"name": dep_name, "version": dep_version})
|
results['dependencies'].append({"name": dep_name, "version": dep_version})
|
||||||
|
|
||||||
if dep_tuple not in queued_or_processed_lookup:
|
if dep_tuple not in queued_or_processed_lookup:
|
||||||
should_queue = False
|
should_queue = False
|
||||||
if dependency_mode == 'recursive':
|
if dependency_mode == 'recursive':
|
||||||
should_queue = True
|
should_queue = True
|
||||||
elif dependency_mode == 'patch-canonical' and dep_tuple == CANONICAL_PACKAGE:
|
elif dependency_mode == 'patch-canonical' and dep_tuple == CANONICAL_PACKAGE:
|
||||||
should_queue = True
|
should_queue = True
|
||||||
# Tree-shaking dependencies are handled post-loop for the initial package
|
|
||||||
if should_queue:
|
if should_queue:
|
||||||
logger.debug(f"Adding dependency to queue ({dependency_mode}): {dep_name}#{dep_version}")
|
logger.debug(f"Adding dependency to queue ({dependency_mode}): {dep_name}#{dep_version}")
|
||||||
pending_queue.append(dep_tuple)
|
pending_queue.append(dep_tuple)
|
||||||
queued_or_processed_lookup.add(dep_tuple)
|
queued_or_processed_lookup.add(dep_tuple)
|
||||||
|
|
||||||
save_package_metadata(name, version, dependency_mode, current_package_deps)
|
save_package_metadata(name, version, dependency_mode, current_package_deps)
|
||||||
|
|
||||||
# Tree-shaking: Process dependencies for the initial package
|
|
||||||
if dependency_mode == 'tree-shaking' and package_id_tuple == (initial_name, initial_version):
|
if dependency_mode == 'tree-shaking' and package_id_tuple == (initial_name, initial_version):
|
||||||
logger.info(f"Performing tree-shaking for {initial_name}#{initial_version}")
|
logger.info(f"Performing tree-shaking for {initial_name}#{initial_version}")
|
||||||
used_types = extract_used_types(save_path)
|
used_types = extract_used_types(save_path)
|
||||||
@ -1052,19 +1021,16 @@ def import_package_and_dependencies(initial_name, initial_version, dependency_mo
|
|||||||
if CANONICAL_PACKAGE not in tree_shaken_deps:
|
if CANONICAL_PACKAGE not in tree_shaken_deps:
|
||||||
tree_shaken_deps.add(CANONICAL_PACKAGE)
|
tree_shaken_deps.add(CANONICAL_PACKAGE)
|
||||||
logger.debug(f"Ensuring canonical package {CANONICAL_PACKAGE} for tree-shaking")
|
logger.debug(f"Ensuring canonical package {CANONICAL_PACKAGE} for tree-shaking")
|
||||||
|
|
||||||
for dep_tuple in tree_shaken_deps:
|
for dep_tuple in tree_shaken_deps:
|
||||||
if dep_tuple not in queued_or_processed_lookup:
|
if dep_tuple not in queued_or_processed_lookup:
|
||||||
logger.info(f"Queueing tree-shaken dependency: {dep_tuple[0]}#{dep_tuple[1]}")
|
logger.info(f"Queueing tree-shaken dependency: {dep_tuple[0]}#{dep_tuple[1]}")
|
||||||
pending_queue.append(dep_tuple)
|
pending_queue.append(dep_tuple)
|
||||||
queued_or_processed_lookup.add(dep_tuple)
|
queued_or_processed_lookup.add(dep_tuple)
|
||||||
|
|
||||||
results['dependencies'] = [{"name": d[0], "version": d[1]} for d in all_found_dependencies]
|
results['dependencies'] = [{"name": d[0], "version": d[1]} for d in all_found_dependencies]
|
||||||
logger.info(f"Import finished for {initial_name}#{initial_version}. Processed: {len(results['processed'])}, Downloaded: {len(results['downloaded'])}, Errors: {len(results['errors'])}")
|
logger.info(f"Import finished for {initial_name}#{initial_version}. Processed: {len(results['processed'])}, Downloaded: {len(results['downloaded'])}, Errors: {len(results['errors'])}")
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
# --- Standalone Test ---
|
||||||
# --- Standalone Test/Example Usage ---
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logger.info("Running services.py directly for testing.")
|
logger.info("Running services.py directly for testing.")
|
||||||
class MockFlask:
|
class MockFlask:
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
|
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
|
||||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</a>
|
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</a>
|
||||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4 ">Upload IG's</a>
|
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,7 +52,6 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for pkg in packages %}
|
{% for pkg in packages %}
|
||||||
{% set is_processed = (pkg.name, pkg.version) in processed_ids %}
|
{% set is_processed = (pkg.name, pkg.version) in processed_ids %}
|
||||||
<!---{% set is_duplicate = pkg.name in duplicate_names and duplicate_names[pkg.name]|length > 1 %}--->
|
|
||||||
{% set is_duplicate = pkg.name in duplicate_groups %}
|
{% set is_duplicate = pkg.name in duplicate_groups %}
|
||||||
{% set group_color = group_colors[pkg.name] if (is_duplicate and pkg.name in group_colors) else 'bg-warning' if is_duplicate else '' %}
|
{% set group_color = group_colors[pkg.name] if (is_duplicate and pkg.name in group_colors) else 'bg-warning' if is_duplicate else '' %}
|
||||||
<tr class="{% if group_color %}{{ group_color }} text-dark{% endif %}">
|
<tr class="{% if group_color %}{{ group_color }} text-dark{% endif %}">
|
||||||
@ -109,7 +108,10 @@
|
|||||||
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if processed_list %}
|
{% if processed_list %}
|
||||||
<p class="mb-2"><small><span class="badge bg-warning text-dark border me-1">MS</span> = Contains Must Support Elements</small></p>
|
<p class="mb-2"><small>
|
||||||
|
<span class="badge bg-warning text-dark border me-1">MS</span> = Contains Must Support Elements<br>
|
||||||
|
<span class="badge bg-info text-dark border me-1">Optional MS Ext</span> = Optional Extension with Must Support Sub-Elements
|
||||||
|
</small></p>
|
||||||
<p class="mb-2 small text-muted">Resource Types in the list will be both Profile and Base Type:</p>
|
<p class="mb-2 small text-muted">Resource Types in the list will be both Profile and Base Type:</p>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover">
|
<table class="table table-sm table-hover">
|
||||||
@ -126,7 +128,8 @@
|
|||||||
{% if types_info %}
|
{% if types_info %}
|
||||||
<div class="d-flex flex-wrap gap-1">
|
<div class="d-flex flex-wrap gap-1">
|
||||||
{% for type_info in types_info %}
|
{% for type_info in types_info %}
|
||||||
<span class="badge {% if type_info.must_support %}bg-warning text-dark{% else %}bg-light text-dark{% endif %} border" title="{% if type_info.must_support %}Has Must Support{% endif %}">{{ type_info.name }}</span>
|
<span class="badge {% if type_info.must_support %}{% if type_info.optional_usage %}bg-info{% else %}bg-warning{% endif %} text-dark{% else %}bg-light text-dark{% endif %} border"
|
||||||
|
title="{% if type_info.must_support %}{% if type_info.optional_usage %}Optional Extension with Must Support{% else %}Has Must Support{% endif %}{% endif %}">{{ type_info.name }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -84,24 +84,17 @@
|
|||||||
<a href="#" class="resource-type-link text-decoration-none"
|
<a href="#" class="resource-type-link text-decoration-none"
|
||||||
data-package-name="{{ processed_ig.package_name }}"
|
data-package-name="{{ processed_ig.package_name }}"
|
||||||
data-package-version="{{ processed_ig.version }}"
|
data-package-version="{{ processed_ig.version }}"
|
||||||
data-resource-type="{{ type_info.name }}" {# Use name which is usually the profile ID #}
|
data-resource-type="{{ type_info.name }}"
|
||||||
aria-label="View structure for {{ type_info.name }}">
|
aria-label="View structure for {{ type_info.name }}">
|
||||||
|
|
||||||
{# --- Badge Logic --- #}
|
|
||||||
{% if type_info.must_support %}
|
{% if type_info.must_support %}
|
||||||
{# Check if it's marked as optional_usage (Extension with internal MS) #}
|
|
||||||
{% if optional_usage_elements.get(type_info.name) %}
|
{% if optional_usage_elements.get(type_info.name) %}
|
||||||
{# Blue/Info badge for Optional Extension with MS #}
|
<span class="badge bg-info text-dark border" title="Optional Extension with Must Support sub-elements">{{ type_info.name }}</span>
|
||||||
<span class="badge bg-info text-dark border" title="Optional Extension with Must Support sub-elements (e.g., value[x], type)">{{ type_info.name }}</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Yellow/Warning badge for standard Profile with MS #}
|
|
||||||
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# Default light badge if no Must Support #}
|
|
||||||
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{# --- End Badge Logic --- #}
|
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -115,11 +108,10 @@
|
|||||||
<a href="#" class="resource-type-link text-decoration-none"
|
<a href="#" class="resource-type-link text-decoration-none"
|
||||||
data-package-name="{{ processed_ig.package_name }}"
|
data-package-name="{{ processed_ig.package_name }}"
|
||||||
data-package-version="{{ processed_ig.version }}"
|
data-package-version="{{ processed_ig.version }}"
|
||||||
data-resource-type="{{ type_info.name }}" {# Use name which is the resource type #}
|
data-resource-type="{{ type_info.name }}"
|
||||||
aria-label="View structure for {{ type_info.name }}">
|
aria-label="View structure for {{ type_info.name }}">
|
||||||
{# Base types usually don't have MS directly, but check just in case #}
|
|
||||||
{% if type_info.must_support %}
|
{% if type_info.must_support %}
|
||||||
<span class="badge bg-warning text-dark border" title="Contains Must Support elements (Unusual for base type reference)">{{ type_info.name }}</span>
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -193,7 +185,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@ -220,15 +213,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
|
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
|
||||||
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
|
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
|
||||||
|
|
||||||
// Debugging: Log initial data
|
|
||||||
// console.log("Examples Data:", examplesData);
|
|
||||||
|
|
||||||
// Optional: Debug clicks if needed
|
|
||||||
// document.addEventListener('click', function(event) {
|
|
||||||
// console.log("Click event on:", event.target, "Class:", event.target.className);
|
|
||||||
// }, true);
|
|
||||||
|
|
||||||
// Handle clicks on resource-type-link within resource-type-list containers
|
|
||||||
document.querySelectorAll('.resource-type-list').forEach(container => {
|
document.querySelectorAll('.resource-type-list').forEach(container => {
|
||||||
container.addEventListener('click', function(event) {
|
container.addEventListener('click', function(event) {
|
||||||
const link = event.target.closest('.resource-type-link');
|
const link = event.target.closest('.resource-type-link');
|
||||||
@ -238,7 +222,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
const pkgName = link.dataset.packageName;
|
const pkgName = link.dataset.packageName;
|
||||||
const pkgVersion = link.dataset.packageVersion;
|
const pkgVersion = link.dataset.packageVersion;
|
||||||
const resourceType = link.dataset.resourceType; // This is the Profile ID or Resource Type Name
|
const resourceType = link.dataset.resourceType;
|
||||||
if (!pkgName || !pkgVersion || !resourceType) {
|
if (!pkgName || !pkgVersion || !resourceType) {
|
||||||
console.error("Missing data attributes:", { pkgName, pkgVersion, resourceType });
|
console.error("Missing data attributes:", { pkgName, pkgVersion, resourceType });
|
||||||
return;
|
return;
|
||||||
@ -248,42 +232,35 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||||
console.log("Fetching structure from:", structureFetchUrl);
|
console.log("Fetching structure from:", structureFetchUrl);
|
||||||
|
|
||||||
// Update titles and reset displays
|
|
||||||
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||||
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||||
structureDisplay.innerHTML = ''; // Clear previous structure
|
structureDisplay.innerHTML = '';
|
||||||
rawStructureContent.textContent = ''; // Clear previous raw structure
|
rawStructureContent.textContent = '';
|
||||||
structureLoading.style.display = 'block';
|
structureLoading.style.display = 'block';
|
||||||
rawStructureLoading.style.display = 'block';
|
rawStructureLoading.style.display = 'block';
|
||||||
structureFallbackMessage.style.display = 'none';
|
structureFallbackMessage.style.display = 'none';
|
||||||
structureDisplayWrapper.style.display = 'block'; // Show structure section
|
structureDisplayWrapper.style.display = 'block';
|
||||||
rawStructureWrapper.style.display = 'block'; // Show raw structure section
|
rawStructureWrapper.style.display = 'block';
|
||||||
exampleDisplayWrapper.style.display = 'none'; // Hide examples initially
|
exampleDisplayWrapper.style.display = 'none';
|
||||||
|
|
||||||
// Fetch Structure Definition
|
|
||||||
fetch(structureFetchUrl)
|
fetch(structureFetchUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log("Structure fetch response status:", response.status);
|
console.log("Structure fetch response status:", response.status);
|
||||||
// Try to parse JSON regardless of status to get error message from body
|
|
||||||
return response.json().then(data => ({ ok: response.ok, status: response.status, data }));
|
return response.json().then(data => ({ ok: response.ok, status: response.status, data }));
|
||||||
})
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
// Throw error using message from JSON body if available
|
|
||||||
throw new Error(result.data.error || `HTTP error ${result.status}`);
|
throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||||
}
|
}
|
||||||
console.log("Structure data received:", result.data);
|
console.log("Structure data received:", result.data);
|
||||||
// Display fallback message if needed
|
|
||||||
if (result.data.fallback_used) {
|
if (result.data.fallback_used) {
|
||||||
structureFallbackMessage.style.display = 'block';
|
structureFallbackMessage.style.display = 'block';
|
||||||
structureFallbackMessage.textContent = `Structure definition not found in this IG; using base FHIR definition from ${result.data.source_package}.`;
|
structureFallbackMessage.textContent = `Structure definition not found in this IG; using base FHIR definition from ${result.data.source_package}.`;
|
||||||
} else {
|
} else {
|
||||||
structureFallbackMessage.style.display = 'none';
|
structureFallbackMessage.style.display = 'none';
|
||||||
}
|
}
|
||||||
// Render the tree and raw JSON
|
renderStructureTree(result.data.elements, result.data.must_support_paths || [], resourceType);
|
||||||
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
|
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||||
rawStructureContent.textContent = JSON.stringify(result.data, null, 2); // Pretty print raw SD
|
|
||||||
// Populate examples for the clicked resource/profile ID
|
|
||||||
populateExampleSelector(resourceType);
|
populateExampleSelector(resourceType);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@ -291,108 +268,96 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
structureFallbackMessage.style.display = 'none';
|
structureFallbackMessage.style.display = 'none';
|
||||||
structureDisplay.innerHTML = `<div class="alert alert-danger">Error loading structure: ${error.message}</div>`;
|
structureDisplay.innerHTML = `<div class="alert alert-danger">Error loading structure: ${error.message}</div>`;
|
||||||
rawStructureContent.textContent = `Error loading structure: ${error.message}`;
|
rawStructureContent.textContent = `Error loading structure: ${error.message}`;
|
||||||
// Still try to show examples if structure fails
|
|
||||||
populateExampleSelector(resourceType);
|
populateExampleSelector(resourceType);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// Hide loading spinners
|
|
||||||
structureLoading.style.display = 'none';
|
structureLoading.style.display = 'none';
|
||||||
rawStructureLoading.style.display = 'none';
|
rawStructureLoading.style.display = 'none';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to populate the example dropdown
|
|
||||||
function populateExampleSelector(resourceOrProfileIdentifier) {
|
function populateExampleSelector(resourceOrProfileIdentifier) {
|
||||||
console.log("Populating examples for:", resourceOrProfileIdentifier);
|
console.log("Populating examples for:", resourceOrProfileIdentifier);
|
||||||
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier; // Update example card title
|
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
||||||
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
||||||
console.log("Available examples:", availableExamples);
|
console.log("Available examples:", availableExamples);
|
||||||
// Reset example section
|
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>';
|
||||||
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>'; // Clear previous options
|
exampleContentWrapper.style.display = 'none';
|
||||||
exampleContentWrapper.style.display = 'none'; // Hide content area
|
|
||||||
exampleFilename.textContent = '';
|
exampleFilename.textContent = '';
|
||||||
exampleContentRaw.textContent = '';
|
exampleContentRaw.textContent = '';
|
||||||
exampleContentJson.textContent = '';
|
exampleContentJson.textContent = '';
|
||||||
|
|
||||||
if (availableExamples.length > 0) {
|
if (availableExamples.length > 0) {
|
||||||
// Add new options
|
|
||||||
availableExamples.forEach(filePath => {
|
availableExamples.forEach(filePath => {
|
||||||
const filename = filePath.split('/').pop(); // Get filename from path
|
const filename = filePath.split('/').pop();
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = filePath;
|
option.value = filePath;
|
||||||
option.textContent = filename;
|
option.textContent = filename;
|
||||||
exampleSelect.appendChild(option);
|
exampleSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
exampleSelectorWrapper.style.display = 'block'; // Show dropdown
|
exampleSelectorWrapper.style.display = 'block';
|
||||||
exampleDisplayWrapper.style.display = 'block'; // Show example card
|
exampleDisplayWrapper.style.display = 'block';
|
||||||
} else {
|
} else {
|
||||||
exampleSelectorWrapper.style.display = 'none'; // Hide dropdown if no examples
|
exampleSelectorWrapper.style.display = 'none';
|
||||||
exampleDisplayWrapper.style.display = 'none'; // Hide example card if no examples
|
exampleDisplayWrapper.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listener for example selection change
|
|
||||||
if (exampleSelect) {
|
if (exampleSelect) {
|
||||||
exampleSelect.addEventListener('change', function(event) {
|
exampleSelect.addEventListener('change', function(event) {
|
||||||
const selectedFilePath = this.value;
|
const selectedFilePath = this.value;
|
||||||
console.log("Selected example file:", selectedFilePath);
|
console.log("Selected example file:", selectedFilePath);
|
||||||
// Reset example content areas
|
|
||||||
exampleContentRaw.textContent = '';
|
exampleContentRaw.textContent = '';
|
||||||
exampleContentJson.textContent = '';
|
exampleContentJson.textContent = '';
|
||||||
exampleFilename.textContent = '';
|
exampleFilename.textContent = '';
|
||||||
exampleContentWrapper.style.display = 'none'; // Hide content initially
|
exampleContentWrapper.style.display = 'none';
|
||||||
|
|
||||||
if (!selectedFilePath) return; // Do nothing if "-- Select --" is chosen
|
if (!selectedFilePath) return;
|
||||||
|
|
||||||
// Prepare URL to fetch example content
|
|
||||||
const exampleParams = new URLSearchParams({
|
const exampleParams = new URLSearchParams({
|
||||||
package_name: "{{ processed_ig.package_name }}",
|
package_name: "{{ processed_ig.package_name }}",
|
||||||
package_version: "{{ processed_ig.version }}",
|
package_version: "{{ processed_ig.version }}",
|
||||||
filename: selectedFilePath
|
filename: selectedFilePath
|
||||||
});
|
});
|
||||||
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
const exampleFetchUrl = `${exampleBaseUrl}?${structureParams.toString()}`;
|
||||||
console.log("Fetching example from:", exampleFetchUrl);
|
console.log("Fetching example from:", exampleFetchUrl);
|
||||||
exampleLoading.style.display = 'block'; // Show loading spinner
|
exampleLoading.style.display = 'block';
|
||||||
|
|
||||||
// Fetch example content
|
|
||||||
fetch(exampleFetchUrl)
|
fetch(exampleFetchUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log("Example fetch response status:", response.status);
|
console.log("Example fetch response status:", response.status);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Try to get error text from body
|
|
||||||
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
|
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
|
||||||
}
|
}
|
||||||
return response.text(); // Get content as text
|
return response.text();
|
||||||
})
|
})
|
||||||
.then(content => {
|
.then(content => {
|
||||||
console.log("Example content received."); // Avoid logging potentially large content
|
console.log("Example content received.");
|
||||||
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
||||||
exampleContentRaw.textContent = content; // Display raw content
|
exampleContentRaw.textContent = content;
|
||||||
// Try to parse and pretty-print if it's JSON
|
|
||||||
try {
|
try {
|
||||||
const jsonContent = JSON.parse(content);
|
const jsonContent = JSON.parse(content);
|
||||||
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
|
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Example content is not valid JSON:", e.message);
|
console.warn("Example content is not valid JSON:", e.message);
|
||||||
exampleContentJson.textContent = '(Not valid JSON)'; // Indicate if not JSON
|
exampleContentJson.textContent = '(Not valid JSON)';
|
||||||
}
|
}
|
||||||
exampleContentWrapper.style.display = 'block'; // Show content area
|
exampleContentWrapper.style.display = 'block';
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error fetching example:", error);
|
console.error("Error fetching example:", error);
|
||||||
exampleFilename.textContent = 'Error loading example';
|
exampleFilename.textContent = 'Error loading example';
|
||||||
exampleContentRaw.textContent = `Error: ${error.message}`;
|
exampleContentRaw.textContent = `Error: ${error.message}`;
|
||||||
exampleContentJson.textContent = `Error: ${error.message}`;
|
exampleContentJson.textContent = `Error: ${error.message}`;
|
||||||
exampleContentWrapper.style.display = 'block'; // Show error in content area
|
exampleContentWrapper.style.display = 'block';
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
exampleLoading.style.display = 'none'; // Hide loading spinner
|
exampleLoading.style.display = 'none';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to build the hierarchical data structure for the tree
|
|
||||||
function buildTreeData(elements) {
|
function buildTreeData(elements) {
|
||||||
const treeRoot = { children: {}, element: null, name: 'Root' };
|
const treeRoot = { children: {}, element: null, name: 'Root' };
|
||||||
const nodeMap = { 'Root': treeRoot };
|
const nodeMap = { 'Root': treeRoot };
|
||||||
@ -415,14 +380,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let parentNode = treeRoot;
|
let parentNode = treeRoot;
|
||||||
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
for (let i = 0; i < parts.length; i++) {
|
||||||
const part = parts[i];
|
let part = parts[i];
|
||||||
currentPath = i === 0 ? part : `${currentPath}.${part}`;
|
const isChoiceElement = part.endsWith('[x]');
|
||||||
|
const cleanPart = part.replace(/\[\d+\]/g, '').replace('[x]', '');
|
||||||
|
currentPath = i === 0 ? cleanPart : `${currentPath}.${cleanPart}`;
|
||||||
|
|
||||||
// For extensions, append sliceName to path if present
|
let nodeKey = isChoiceElement ? part : cleanPart;
|
||||||
let nodeKey = part;
|
if (sliceName && i === parts.length - 1) {
|
||||||
if (part === 'extension' && i === parts.length - 1 && sliceName) {
|
nodeKey = sliceName;
|
||||||
nodeKey = `${part}:${sliceName}`;
|
currentPath = id || currentPath;
|
||||||
currentPath = id || currentPath; // Use id for precise matching in extensions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nodeMap[currentPath]) {
|
if (!nodeMap[currentPath]) {
|
||||||
@ -432,17 +398,68 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
name: nodeKey,
|
name: nodeKey,
|
||||||
path: currentPath
|
path: currentPath
|
||||||
};
|
};
|
||||||
let parentPath = i === 0 ? 'Root' : parts.slice(0, i).join('.');
|
let parentPath = i === 0 ? 'Root' : parts.slice(0, i).join('.').replace(/\[\d+\]/g, '').replace('[x]', '');
|
||||||
parentNode = nodeMap[parentPath] || treeRoot;
|
parentNode = nodeMap[parentPath] || treeRoot;
|
||||||
parentNode.children[nodeKey] = newNode;
|
parentNode.children[nodeKey] = newNode;
|
||||||
nodeMap[currentPath] = newNode;
|
nodeMap[currentPath] = newNode;
|
||||||
console.log(`Created node: path=${currentPath}, name=${nodeKey}`);
|
console.log(`Created node: path=${currentPath}, name=${nodeKey}`);
|
||||||
|
|
||||||
|
// Add modifierExtension only for DomainResource, BackboneElement, or explicitly defined
|
||||||
|
if (el.path === cleanPart + '.modifierExtension' ||
|
||||||
|
(el.type && el.type.some(t => t.code === 'DomainResource' || t.code === 'BackboneElement'))) {
|
||||||
|
const modExtPath = `${currentPath}.modifierExtension`;
|
||||||
|
const modExtNode = {
|
||||||
|
children: {},
|
||||||
|
element: {
|
||||||
|
path: modExtPath,
|
||||||
|
id: modExtPath,
|
||||||
|
short: 'Extensions that cannot be ignored',
|
||||||
|
definition: 'May be used to represent additional information that modifies the understanding of the element.',
|
||||||
|
min: 0,
|
||||||
|
max: '*',
|
||||||
|
type: [{ code: 'Extension' }],
|
||||||
|
isModifier: true
|
||||||
|
},
|
||||||
|
name: 'modifierExtension',
|
||||||
|
path: modExtPath
|
||||||
|
};
|
||||||
|
newNode.children['modifierExtension'] = modExtNode;
|
||||||
|
nodeMap[modExtPath] = modExtNode;
|
||||||
|
console.log(`Added modifierExtension: path=${modExtPath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i === parts.length - 1) {
|
if (i === parts.length - 1) {
|
||||||
const targetNode = nodeMap[currentPath];
|
const targetNode = nodeMap[currentPath];
|
||||||
targetNode.element = el;
|
targetNode.element = el;
|
||||||
console.log(`Assigned element to node: path=${currentPath}, id=${id}, sliceName=${sliceName}`);
|
console.log(`Assigned element to node: path=${currentPath}, id=${id}, sliceName=${sliceName}`);
|
||||||
|
|
||||||
|
// Handle choice elements ([x])
|
||||||
|
if (isChoiceElement && el.type && el.type.length > 1) {
|
||||||
|
el.type.forEach(type => {
|
||||||
|
if (type.code === 'dateTime') return; // Skip dateTime as per FHIR convention
|
||||||
|
const typeName = `onset${type.code.charAt(0).toUpperCase() + type.code.slice(1)}`;
|
||||||
|
const typePath = `${currentPath}.${typeName}`;
|
||||||
|
const typeNode = {
|
||||||
|
children: {},
|
||||||
|
element: {
|
||||||
|
path: typePath,
|
||||||
|
id: typePath,
|
||||||
|
short: el.short || typeName,
|
||||||
|
definition: el.definition || `Choice of type ${type.code}`,
|
||||||
|
min: el.min,
|
||||||
|
max: el.max,
|
||||||
|
type: [{ code: type.code }],
|
||||||
|
mustSupport: el.mustSupport || false
|
||||||
|
},
|
||||||
|
name: typeName,
|
||||||
|
path: typePath
|
||||||
|
};
|
||||||
|
targetNode.children[typeName] = typeNode;
|
||||||
|
nodeMap[typePath] = typeNode;
|
||||||
|
console.log(`Added choice type node: path=${typePath}, name=${typeName}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parentNode = nodeMap[currentPath];
|
parentNode = nodeMap[currentPath];
|
||||||
@ -454,8 +471,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return treeData;
|
return treeData;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to render a single node (and its children recursively) as an <li>
|
function renderNodeAsLi(node, mustSupportPathsSet, resourceType, level = 0) {
|
||||||
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
|
|
||||||
if (!node || !node.element) {
|
if (!node || !node.element) {
|
||||||
console.warn("Skipping render for invalid node:", node);
|
console.warn("Skipping render for invalid node:", node);
|
||||||
return '';
|
return '';
|
||||||
@ -470,24 +486,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const definition = el.definition || '';
|
const definition = el.definition || '';
|
||||||
|
|
||||||
console.log(`Rendering node: path=${path}, id=${id}, sliceName=${sliceName}`);
|
console.log(`Rendering node: path=${path}, id=${id}, sliceName=${sliceName}`);
|
||||||
console.log(` MustSupportPathsSet contains path: ${mustSupportPathsSet.has(path)}`);
|
|
||||||
if (id) {
|
|
||||||
console.log(` MustSupportPathsSet contains id: ${mustSupportPathsSet.has(id)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check MS for path, id, or normalized extension path
|
|
||||||
let isMustSupport = mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id));
|
let isMustSupport = mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id));
|
||||||
if (!isMustSupport && path.startsWith('Extension.extension')) {
|
let isOptional = min == 0 && isMustSupport && el.type && el.type.some(t => t.code === 'Extension') && path.includes('extension');
|
||||||
const basePath = path.split(':')[0];
|
|
||||||
const baseId = id ? id.split(':')[0] : null;
|
|
||||||
isMustSupport = mustSupportPathsSet.has(basePath) || (baseId && mustSupportPathsSet.has(baseId));
|
|
||||||
console.log(` Extension check: basePath=${basePath}, baseId=${baseId}, isMustSupport=${isMustSupport}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Final isMustSupport for ${path}: ${isMustSupport}`);
|
|
||||||
|
|
||||||
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
||||||
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning ms-1" title="Must Support"></i>' : '';
|
const mustSupportDisplay = isMustSupport ? `<i class="bi bi-check-circle-fill ${isOptional ? 'text-info' : 'text-warning'} ms-1" title="${isOptional ? 'Optional Must Support Extension' : 'Must Support'}"></i>` : '';
|
||||||
const hasChildren = Object.keys(node.children).length > 0;
|
const hasChildren = Object.keys(node.children).length > 0;
|
||||||
const collapseId = `collapse-${path.replace(/[\.\:\/\[\]\(\)]/g, '-')}`;
|
const collapseId = `collapse-${path.replace(/[\.\:\/\[\]\(\)]/g, '-')}`;
|
||||||
const padding = level * 20;
|
const padding = level * 20;
|
||||||
@ -510,15 +513,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
let childrenHtml = '';
|
let childrenHtml = '';
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
childrenHtml += `<ul class="collapse list-group list-group-flush structure-subtree" id="${collapseId}">`;
|
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
|
||||||
Object.values(node.children).sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name)).forEach(childNode => {
|
Object.values(node.children).sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name)).forEach(childNode => {
|
||||||
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, level + 1);
|
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, resourceType, level + 1);
|
||||||
});
|
});
|
||||||
childrenHtml += `</ul>`;
|
childrenHtml += `</ul>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemHtml = `<li class="${liClass}">`;
|
let itemHtml = `<li class="${liClass}">`;
|
||||||
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" data-bs-target="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-collapse-id="${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||||
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
|
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
|
||||||
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||||
if (hasChildren) {
|
if (hasChildren) {
|
||||||
@ -533,11 +536,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let descriptionTooltipAttrs = '';
|
let descriptionTooltipAttrs = '';
|
||||||
if (definition) {
|
if (definition) {
|
||||||
const escapedDefinition = definition
|
const escapedDefinition = definition
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''')
|
.replace(/'/g, '\'')
|
||||||
.replace(/\n/g, ' ');
|
.replace(/\n/g, ' ');
|
||||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||||
}
|
}
|
||||||
@ -554,19 +557,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter must_support_paths to avoid generic extension paths
|
const mustSupportPathsSet = new Set(mustSupportPaths);
|
||||||
const validMustSupportPaths = mustSupportPaths.filter(path => {
|
|
||||||
if (path.includes('.extension') && !path.includes(':')) {
|
|
||||||
return false; // Exclude generic Patient.extension
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const mustSupportPathsSet = new Set(validMustSupportPaths);
|
|
||||||
console.log("Must Support Paths Set for Tree:", mustSupportPathsSet);
|
console.log("Must Support Paths Set for Tree:", mustSupportPathsSet);
|
||||||
|
|
||||||
let html = '<p><small>' +
|
let html = '<p><small>' +
|
||||||
'<i class="bi bi-check-circle-fill text-warning ms-1"></i> = Must Support (yellow row)<br>' +
|
'<i class="bi bi-check-circle-fill text-warning ms-1"></i> = Must Support (yellow row)<br>' +
|
||||||
'<i class="bi bi-check-circle-fill text-info ms-1"></i> = Optional (blue row)</small></p>';
|
'<i class="bi bi-check-circle-fill text-info ms-1"></i> = Optional Must Support Extension (blue icon)</small></p>';
|
||||||
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">Status</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">Status</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
||||||
html += '<ul class="list-group list-group-flush structure-tree-root">';
|
html += '<ul class="list-group list-group-flush structure-tree-root">';
|
||||||
|
|
||||||
@ -586,35 +582,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize collapse with manual click handling and debouncing
|
||||||
|
let lastClickTime = 0;
|
||||||
|
const debounceDelay = 200; // ms
|
||||||
structureDisplay.querySelectorAll('.collapse-toggle').forEach(toggleEl => {
|
structureDisplay.querySelectorAll('.collapse-toggle').forEach(toggleEl => {
|
||||||
const collapseTargetId = toggleEl.getAttribute('data-bs-target');
|
const collapseId = toggleEl.getAttribute('data-collapse-id');
|
||||||
if (!collapseTargetId) return;
|
if (!collapseId) {
|
||||||
const collapseEl = document.querySelector(collapseTargetId);
|
console.warn("No collapse ID for toggle:", toggleEl.outerHTML);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collapseEl = document.querySelector(`#${collapseId}`);
|
||||||
|
if (!collapseEl) {
|
||||||
|
console.warn("No collapse element found for ID:", collapseId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const toggleIcon = toggleEl.querySelector('.toggle-icon');
|
const toggleIcon = toggleEl.querySelector('.toggle-icon');
|
||||||
|
if (!toggleIcon) {
|
||||||
|
console.warn("No toggle icon for:", collapseId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (collapseEl && toggleIcon) {
|
console.log("Initializing collapse for:", collapseId);
|
||||||
collapseEl.classList.remove('show');
|
// Initialize Bootstrap Collapse
|
||||||
toggleIcon.classList.remove('bi-chevron-down');
|
const bsCollapse = new bootstrap.Collapse(collapseEl, {
|
||||||
toggleIcon.classList.add('bi-chevron-right');
|
toggle: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom click handler with debounce
|
||||||
toggleEl.addEventListener('click', event => {
|
toggleEl.addEventListener('click', event => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const isCurrentlyShown = collapseEl.classList.contains('show');
|
const now = Date.now();
|
||||||
toggleIcon.classList.toggle('bi-chevron-right', isCurrentlyShown);
|
if (now - lastClickTime < debounceDelay) {
|
||||||
toggleIcon.classList.toggle('bi-chevron-down', !isCurrentlyShown);
|
console.log("Debounced click ignored for:", collapseId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastClickTime = now;
|
||||||
|
console.log("Click toggle for:", collapseId, "Current show:", collapseEl.classList.contains('show'));
|
||||||
|
if (collapseEl.classList.contains('show')) {
|
||||||
|
bsCollapse.hide();
|
||||||
|
} else {
|
||||||
|
bsCollapse.show();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
collapseEl.addEventListener('show.bs.collapse', () => {
|
// Update icon and log state
|
||||||
|
collapseEl.addEventListener('show.bs.collapse', event => {
|
||||||
|
console.log("Show collapse triggered for:", event.target.id, "Show class:", event.target.classList.contains('show'));
|
||||||
|
if (toggleIcon) {
|
||||||
toggleIcon.classList.remove('bi-chevron-right');
|
toggleIcon.classList.remove('bi-chevron-right');
|
||||||
toggleIcon.classList.add('bi-chevron-down');
|
toggleIcon.classList.add('bi-chevron-down');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
collapseEl.addEventListener('hide.bs.collapse', () => {
|
collapseEl.addEventListener('hide.bs.collapse', event => {
|
||||||
|
console.log("Hide collapse triggered for:", event.target.id, "Show class:", event.target.classList.contains('show'));
|
||||||
|
if (toggleIcon) {
|
||||||
toggleIcon.classList.remove('bi-chevron-down');
|
toggleIcon.classList.remove('bi-chevron-down');
|
||||||
toggleIcon.classList.add('bi-chevron-right');
|
toggleIcon.classList.add('bi-chevron-right');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
collapseEl.addEventListener('shown.bs.collapse', event => {
|
||||||
|
console.log("Shown collapse completed for:", event.target.id, "Show class:", event.target.classList.contains('show'));
|
||||||
|
});
|
||||||
|
collapseEl.addEventListener('hidden.bs.collapse', event => {
|
||||||
|
console.log("Hidden collapse completed for:", event.target.id, "Show class:", event.target.classList.contains('show'));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("JavaScript error in cp_view_processed_ig.html:", e);
|
console.error("JavaScript error in cp_view_processed_ig.html:", e);
|
||||||
|
577
templates/cp_view_processed_ig_GOOD.html
Normal file
577
templates/cp_view_processed_ig_GOOD.html
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="px-4 py-5 my-5 text-center">
|
||||||
|
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE Ig Toolkit" width="192" height="192">
|
||||||
|
<h1 class="display-5 fw-bold text-body-emphasis">{{ title }}</h1>
|
||||||
|
<div class="col-lg-6 mx-auto">
|
||||||
|
<p class="lead mb-4">
|
||||||
|
View details of the processed FHIR Implementation Guide.
|
||||||
|
</p>
|
||||||
|
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||||
|
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
|
||||||
|
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2><i class="bi bi-info-circle me-2"></i>{{ title }}</h2>
|
||||||
|
<a href="{{ url_for('view_igs') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Package List</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if processed_ig %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Package Details</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-sm-3">Package Name</dt>
|
||||||
|
<dd class="col-sm-9"><code>{{ processed_ig.package_name }}</code></dd>
|
||||||
|
<dt class="col-sm-3">Package Version</dt>
|
||||||
|
<dd class="col-sm-9">{{ processed_ig.version }}</dd>
|
||||||
|
<dt class="col-sm-3">Processed At</dt>
|
||||||
|
<dd class="col-sm-9">{{ processed_ig.processed_date.strftime('%Y-%m-%d %H:%M:%S UTC') }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if config.DISPLAY_PROFILE_RELATIONSHIPS %}
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">Profile Relationships</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h6>Complies With</h6>
|
||||||
|
{% if complies_with_profiles %}
|
||||||
|
<ul>
|
||||||
|
{% for profile in complies_with_profiles %}
|
||||||
|
<li>{{ profile }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted"><em>No profiles declared as compatible.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h6>Required Dependent Profiles (Must Also Validate Against)</h6>
|
||||||
|
{% if imposed_profiles %}
|
||||||
|
<ul>
|
||||||
|
{% for profile in imposed_profiles %}
|
||||||
|
<li>{{ profile }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted"><em>No imposed profiles.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">Resource Types Found / Defined</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if profile_list or base_list %}
|
||||||
|
<p class="mb-2">
|
||||||
|
<small>
|
||||||
|
<span class="badge bg-warning text-dark border me-1" style="font-size: 0.8em;" title="Profile contains Must Support elements">MS</span> = Contains Must Support Elements<br>
|
||||||
|
<span class="badge bg-info text-dark border me-1" style="font-size: 0.8em;" title="An optional Extension which contains Must Support sub-elements (e.g., value[x], type)">Optional MS Ext</span> = Optional Extension with Must Support Sub-Elements
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
{% if profile_list %}
|
||||||
|
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = Examples will be displayed when selecting profile Types if contained in the IG</small></p>
|
||||||
|
<h6>Profiles Defined ({{ profile_list|length }}):</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-1 mb-3 resource-type-list">
|
||||||
|
{% for type_info in profile_list %}
|
||||||
|
<a href="#" class="resource-type-link text-decoration-none"
|
||||||
|
data-package-name="{{ processed_ig.package_name }}"
|
||||||
|
data-package-version="{{ processed_ig.version }}"
|
||||||
|
data-resource-type="{{ type_info.name }}"
|
||||||
|
aria-label="View structure for {{ type_info.name }}">
|
||||||
|
{% if type_info.must_support %}
|
||||||
|
{% if optional_usage_elements.get(type_info.name) %}
|
||||||
|
<span class="badge bg-info text-dark border" title="Optional Extension with Must Support sub-elements">{{ type_info.name }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted"><em>No profiles defined.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if base_list %}
|
||||||
|
<h6>Base Resource Types Referenced ({{ base_list|length }}):</h6>
|
||||||
|
<div class="d-flex flex-wrap gap-1 resource-type-list">
|
||||||
|
{% for type_info in base_list %}
|
||||||
|
<a href="#" class="resource-type-link text-decoration-none"
|
||||||
|
data-package-name="{{ processed_ig.package_name }}"
|
||||||
|
data-package-version="{{ processed_ig.version }}"
|
||||||
|
data-resource-type="{{ type_info.name }}"
|
||||||
|
aria-label="View structure for {{ type_info.name }}">
|
||||||
|
{% if type_info.must_support %}
|
||||||
|
<span class="badge bg-warning text-dark border" title="Contains Must Support elements">{{ type_info.name }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted"><em>No base resource types referenced.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted"><em>No resource type information extracted or stored.</em></p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="structure-display-wrapper" class="mt-4" style="display: none;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Structure Definition for: <code id="structure-title"></code></span>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" onclick="document.getElementById('structure-display-wrapper').style.display='none'; document.getElementById('example-display-wrapper').style.display='none'; document.getElementById('raw-structure-wrapper').style.display='none';"></button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="structure-loading" class="text-center py-3" style="display: none;">
|
||||||
|
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="structure-fallback-message" class="alert alert-info" style="display: none;"></div>
|
||||||
|
<div id="structure-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="raw-structure-wrapper" class="mt-4" style="display: none;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Raw Structure Definition for <code id="raw-structure-title"></code></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="raw-structure-loading" class="text-center py-3" style="display: none;">
|
||||||
|
<div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||||
|
</div>
|
||||||
|
<pre><code id="raw-structure-content" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="example-display-wrapper" class="mt-4" style="display: none;">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Examples for <code id="example-resource-type-title"></code></div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3" id="example-selector-wrapper" style="display: none;">
|
||||||
|
<label for="example-select" class="form-label">Select Example:</label>
|
||||||
|
<select class="form-select form-select-sm" id="example-select"><option selected value="">-- Select --</option></select>
|
||||||
|
</div>
|
||||||
|
<div id="example-loading" class="text-center py-3" style="display: none;">
|
||||||
|
<div class="spinner-border spinner-border-sm text-secondary" role="status"><span class="visually-hidden">Loading...</span></div>
|
||||||
|
</div>
|
||||||
|
<div id="example-content-wrapper" style="display: none;">
|
||||||
|
<h6 id="example-filename" class="mt-2 small text-muted"></h6>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Raw Content</h6>
|
||||||
|
<pre><code id="example-content-raw" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6>Pretty-Printed JSON</h6>
|
||||||
|
<pre><code id="example-content-json" class="p-2 d-block border bg-light" style="max-height: 400px; overflow-y: auto;"></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
try {
|
||||||
|
const structureDisplayWrapper = document.getElementById('structure-display-wrapper');
|
||||||
|
const structureDisplay = document.getElementById('structure-content');
|
||||||
|
const structureTitle = document.getElementById('structure-title');
|
||||||
|
const structureLoading = document.getElementById('structure-loading');
|
||||||
|
const structureFallbackMessage = document.getElementById('structure-fallback-message');
|
||||||
|
const exampleDisplayWrapper = document.getElementById('example-display-wrapper');
|
||||||
|
const exampleSelectorWrapper = document.getElementById('example-selector-wrapper');
|
||||||
|
const exampleSelect = document.getElementById('example-select');
|
||||||
|
const exampleResourceTypeTitle = document.getElementById('example-resource-type-title');
|
||||||
|
const exampleLoading = document.getElementById('example-loading');
|
||||||
|
const exampleContentWrapper = document.getElementById('example-content-wrapper');
|
||||||
|
const exampleFilename = document.getElementById('example-filename');
|
||||||
|
const exampleContentRaw = document.getElementById('example-content-raw');
|
||||||
|
const exampleContentJson = document.getElementById('example-content-json');
|
||||||
|
const rawStructureWrapper = document.getElementById('raw-structure-wrapper');
|
||||||
|
const rawStructureTitle = document.getElementById('raw-structure-title');
|
||||||
|
const rawStructureContent = document.getElementById('raw-structure-content');
|
||||||
|
const rawStructureLoading = document.getElementById('raw-structure-loading');
|
||||||
|
|
||||||
|
const structureBaseUrl = "{{ url_for('get_structure_definition') }}";
|
||||||
|
const exampleBaseUrl = "{{ url_for('get_example_content') }}";
|
||||||
|
|
||||||
|
document.querySelectorAll('.resource-type-list').forEach(container => {
|
||||||
|
container.addEventListener('click', function(event) {
|
||||||
|
const link = event.target.closest('.resource-type-link');
|
||||||
|
if (!link) return;
|
||||||
|
event.preventDefault();
|
||||||
|
console.log("Resource type link clicked:", link.dataset.resourceType);
|
||||||
|
|
||||||
|
const pkgName = link.dataset.packageName;
|
||||||
|
const pkgVersion = link.dataset.packageVersion;
|
||||||
|
const resourceType = link.dataset.resourceType;
|
||||||
|
if (!pkgName || !pkgVersion || !resourceType) {
|
||||||
|
console.error("Missing data attributes:", { pkgName, pkgVersion, resourceType });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
|
||||||
|
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
|
||||||
|
console.log("Fetching structure from:", structureFetchUrl);
|
||||||
|
|
||||||
|
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||||
|
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
|
||||||
|
structureDisplay.innerHTML = '';
|
||||||
|
rawStructureContent.textContent = '';
|
||||||
|
structureLoading.style.display = 'block';
|
||||||
|
rawStructureLoading.style.display = 'block';
|
||||||
|
structureFallbackMessage.style.display = 'none';
|
||||||
|
structureDisplayWrapper.style.display = 'block';
|
||||||
|
rawStructureWrapper.style.display = 'block';
|
||||||
|
exampleDisplayWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
fetch(structureFetchUrl)
|
||||||
|
.then(response => {
|
||||||
|
console.log("Structure fetch response status:", response.status);
|
||||||
|
return response.json().then(data => ({ ok: response.ok, status: response.status, data }));
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(result.data.error || `HTTP error ${result.status}`);
|
||||||
|
}
|
||||||
|
console.log("Structure data received:", result.data);
|
||||||
|
if (result.data.fallback_used) {
|
||||||
|
structureFallbackMessage.style.display = 'block';
|
||||||
|
structureFallbackMessage.textContent = `Structure definition not found in this IG; using base FHIR definition from ${result.data.source_package}.`;
|
||||||
|
} else {
|
||||||
|
structureFallbackMessage.style.display = 'none';
|
||||||
|
}
|
||||||
|
renderStructureTree(result.data.elements, result.data.must_support_paths || [], resourceType);
|
||||||
|
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
|
||||||
|
populateExampleSelector(resourceType);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching structure:", error);
|
||||||
|
structureFallbackMessage.style.display = 'none';
|
||||||
|
structureDisplay.innerHTML = `<div class="alert alert-danger">Error loading structure: ${error.message}</div>`;
|
||||||
|
rawStructureContent.textContent = `Error loading structure: ${error.message}`;
|
||||||
|
populateExampleSelector(resourceType);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
structureLoading.style.display = 'none';
|
||||||
|
rawStructureLoading.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function populateExampleSelector(resourceOrProfileIdentifier) {
|
||||||
|
console.log("Populating examples for:", resourceOrProfileIdentifier);
|
||||||
|
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
|
||||||
|
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
|
||||||
|
console.log("Available examples:", availableExamples);
|
||||||
|
exampleSelect.innerHTML = '<option selected value="">-- Select --</option>';
|
||||||
|
exampleContentWrapper.style.display = 'none';
|
||||||
|
exampleFilename.textContent = '';
|
||||||
|
exampleContentRaw.textContent = '';
|
||||||
|
exampleContentJson.textContent = '';
|
||||||
|
|
||||||
|
if (availableExamples.length > 0) {
|
||||||
|
availableExamples.forEach(filePath => {
|
||||||
|
const filename = filePath.split('/').pop();
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = filePath;
|
||||||
|
option.textContent = filename;
|
||||||
|
exampleSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
exampleSelectorWrapper.style.display = 'block';
|
||||||
|
exampleDisplayWrapper.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
exampleSelectorWrapper.style.display = 'none';
|
||||||
|
exampleDisplayWrapper.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exampleSelect) {
|
||||||
|
exampleSelect.addEventListener('change', function(event) {
|
||||||
|
const selectedFilePath = this.value;
|
||||||
|
console.log("Selected example file:", selectedFilePath);
|
||||||
|
exampleContentRaw.textContent = '';
|
||||||
|
exampleContentJson.textContent = '';
|
||||||
|
exampleFilename.textContent = '';
|
||||||
|
exampleContentWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
if (!selectedFilePath) return;
|
||||||
|
|
||||||
|
const exampleParams = new URLSearchParams({
|
||||||
|
package_name: "{{ processed_ig.package_name }}",
|
||||||
|
package_version: "{{ processed_ig.version }}",
|
||||||
|
filename: selectedFilePath
|
||||||
|
});
|
||||||
|
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
|
||||||
|
console.log("Fetching example from:", exampleFetchUrl);
|
||||||
|
exampleLoading.style.display = 'block';
|
||||||
|
|
||||||
|
fetch(exampleFetchUrl)
|
||||||
|
.then(response => {
|
||||||
|
console.log("Example fetch response status:", response.status);
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then(content => {
|
||||||
|
console.log("Example content received.");
|
||||||
|
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
|
||||||
|
exampleContentRaw.textContent = content;
|
||||||
|
try {
|
||||||
|
const jsonContent = JSON.parse(content);
|
||||||
|
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Example content is not valid JSON:", e.message);
|
||||||
|
exampleContentJson.textContent = '(Not valid JSON)';
|
||||||
|
}
|
||||||
|
exampleContentWrapper.style.display = 'block';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching example:", error);
|
||||||
|
exampleFilename.textContent = 'Error loading example';
|
||||||
|
exampleContentRaw.textContent = `Error: ${error.message}`;
|
||||||
|
exampleContentJson.textContent = `Error: ${error.message}`;
|
||||||
|
exampleContentWrapper.style.display = 'block';
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
exampleLoading.style.display = 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTreeData(elements) {
|
||||||
|
const treeRoot = { children: {}, element: null, name: 'Root' };
|
||||||
|
const nodeMap = { 'Root': treeRoot };
|
||||||
|
|
||||||
|
console.log("Building tree with elements:", elements.length);
|
||||||
|
elements.forEach((el, index) => {
|
||||||
|
const path = el.path;
|
||||||
|
const id = el.id || null;
|
||||||
|
const sliceName = el.sliceName || null;
|
||||||
|
|
||||||
|
console.log(`Element ${index}: path=${path}, id=${id}, sliceName=${sliceName}`);
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
console.warn(`Skipping element ${index} with no path`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = path.split('.');
|
||||||
|
let currentPath = '';
|
||||||
|
let parentNode = treeRoot;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
const cleanPart = part.replace(/\[\d+\]/g, '');
|
||||||
|
currentPath = i === 0 ? cleanPart : `${currentPath}.${cleanPart}`;
|
||||||
|
|
||||||
|
let nodeKey = cleanPart;
|
||||||
|
if (sliceName && i === parts.length - 1) {
|
||||||
|
nodeKey = sliceName;
|
||||||
|
currentPath = id || currentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeMap[currentPath]) {
|
||||||
|
const newNode = {
|
||||||
|
children: {},
|
||||||
|
element: null,
|
||||||
|
name: nodeKey,
|
||||||
|
path: currentPath
|
||||||
|
};
|
||||||
|
let parentPath = i === 0 ? 'Root' : parts.slice(0, i).join('.').replace(/\[\d+\]/g, '');
|
||||||
|
parentNode = nodeMap[parentPath] || treeRoot;
|
||||||
|
parentNode.children[nodeKey] = newNode;
|
||||||
|
nodeMap[currentPath] = newNode;
|
||||||
|
console.log(`Created node: path=${currentPath}, name=${nodeKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i === parts.length - 1) {
|
||||||
|
const targetNode = nodeMap[currentPath];
|
||||||
|
targetNode.element = el;
|
||||||
|
console.log(`Assigned element to node: path=${currentPath}, id=${id}, sliceName=${sliceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
parentNode = nodeMap[currentPath];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const treeData = Object.values(treeRoot.children);
|
||||||
|
console.log("Tree data constructed:", treeData);
|
||||||
|
return treeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNodeAsLi(node, mustSupportPathsSet, resourceType, level = 0) {
|
||||||
|
if (!node || !node.element) {
|
||||||
|
console.warn("Skipping render for invalid node:", node);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const el = node.element;
|
||||||
|
const path = el.path || 'N/A';
|
||||||
|
const id = el.id || null;
|
||||||
|
const sliceName = el.sliceName || null;
|
||||||
|
const min = el.min !== undefined ? el.min : '';
|
||||||
|
const max = el.max || '';
|
||||||
|
const short = el.short || '';
|
||||||
|
const definition = el.definition || '';
|
||||||
|
|
||||||
|
console.log(`Rendering node: path=${path}, id=${id}, sliceName=${sliceName}`);
|
||||||
|
|
||||||
|
let isMustSupport = mustSupportPathsSet.has(path) || (id && mustSupportPathsSet.has(id));
|
||||||
|
// Blue tick boxes only for optional extensions with must-support sub-elements
|
||||||
|
let isOptional = min == 0 && isMustSupport && el.type && el.type.some(t => t.code === 'Extension') && path.includes('extension');
|
||||||
|
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
|
||||||
|
const mustSupportDisplay = isMustSupport ? `<i class="bi bi-check-circle-fill ${isOptional ? 'text-info' : 'text-warning'} ms-1" title="${isOptional ? 'Optional Must Support Extension' : 'Must Support'}"></i>` : '';
|
||||||
|
const hasChildren = Object.keys(node.children).length > 0;
|
||||||
|
const collapseId = `collapse-${path.replace(/[\.\:\/\[\]\(\)]/g, '-')}`;
|
||||||
|
const padding = level * 20;
|
||||||
|
const pathStyle = `padding-left: ${padding}px; white-space: nowrap;`;
|
||||||
|
|
||||||
|
let typeString = 'N/A';
|
||||||
|
if (el.type && el.type.length > 0) {
|
||||||
|
typeString = el.type.map(t => {
|
||||||
|
let s = t.code || '';
|
||||||
|
let profiles = t.targetProfile || t.profile || [];
|
||||||
|
if (profiles.length > 0) {
|
||||||
|
const targetTypes = profiles.map(p => (p || '').split('/').pop()).filter(Boolean).join(', ');
|
||||||
|
if (targetTypes) {
|
||||||
|
s += `(<span class="text-muted fst-italic" title="${profiles.join(', ')}">${targetTypes}</span>)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}).join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
let childrenHtml = '';
|
||||||
|
if (hasChildren) {
|
||||||
|
childrenHtml += `<ul class="collapse list-group list-group-flush structure-subtree" id="${collapseId}">`;
|
||||||
|
Object.values(node.children).sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name)).forEach(childNode => {
|
||||||
|
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, resourceType, level + 1);
|
||||||
|
});
|
||||||
|
childrenHtml += `</ul>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemHtml = `<li class="${liClass}">`;
|
||||||
|
itemHtml += `<div class="row gx-2 align-items-center ${hasChildren ? 'collapse-toggle' : ''}" ${hasChildren ? `data-bs-toggle="collapse" data-bs-target="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
|
||||||
|
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}">`;
|
||||||
|
itemHtml += `<span style="display: inline-block; width: 1.2em; text-align: center;">`;
|
||||||
|
if (hasChildren) {
|
||||||
|
itemHtml += `<i class="bi bi-chevron-right small toggle-icon"></i>`;
|
||||||
|
}
|
||||||
|
itemHtml += `</span>`;
|
||||||
|
itemHtml += `<code class="fw-bold ms-1" title="${path}">${node.name}</code>`;
|
||||||
|
itemHtml += `</div>`;
|
||||||
|
itemHtml += `<div class="col-lg-1 col-md-1 text-center text-muted small"><code>${min}..${max}</code></div>`;
|
||||||
|
itemHtml += `<div class="col-lg-3 col-md-3 text-truncate small">${typeString}</div>`;
|
||||||
|
itemHtml += `<div class="col-lg-1 col-md-1 text-center">${mustSupportDisplay}</div>`;
|
||||||
|
let descriptionTooltipAttrs = '';
|
||||||
|
if (definition) {
|
||||||
|
const escapedDefinition = definition
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/\n/g, ' ');
|
||||||
|
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||||
|
}
|
||||||
|
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short || (definition ? '(definition only)' : '')}</div>`;
|
||||||
|
itemHtml += `</div>`;
|
||||||
|
itemHtml += childrenHtml;
|
||||||
|
itemHtml += `</li>`;
|
||||||
|
return itemHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStructureTree(elements, mustSupportPaths, resourceType) {
|
||||||
|
if (!elements || elements.length === 0) {
|
||||||
|
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found in Structure Definition.</em></p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mustSupportPathsSet = new Set(mustSupportPaths);
|
||||||
|
console.log("Must Support Paths Set for Tree:", mustSupportPathsSet);
|
||||||
|
|
||||||
|
let html = '<p><small>' +
|
||||||
|
'<i class="bi bi-check-circle-fill text-warning ms-1"></i> = Must Support (yellow row)<br>' +
|
||||||
|
'<i class="bi bi-check-circle-fill text-info ms-1"></i> = Optional Must Support Extension (blue icon)</small></p>';
|
||||||
|
html += `<div class="row gx-2 small fw-bold border-bottom mb-1 d-none d-md-flex"><div class="col-lg-4 col-md-3">Path</div><div class="col-lg-1 col-md-1 text-center">Card.</div><div class="col-lg-3 col-md-3">Type(s)</div><div class="col-lg-1 col-md-1 text-center">Status</div><div class="col-lg-3 col-md-4">Description</div></div>`;
|
||||||
|
html += '<ul class="list-group list-group-flush structure-tree-root">';
|
||||||
|
|
||||||
|
const treeData = buildTreeData(elements);
|
||||||
|
treeData.sort((a, b) => (a.element?.path ?? a.name).localeCompare(b.element?.path ?? b.name));
|
||||||
|
treeData.forEach(rootNode => {
|
||||||
|
html += renderNodeAsLi(rootNode, mustSupportPathsSet, resourceType, 0);
|
||||||
|
});
|
||||||
|
html += '</ul>';
|
||||||
|
|
||||||
|
structureDisplay.innerHTML = html;
|
||||||
|
|
||||||
|
const tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||||
|
tooltipTriggerList.forEach(tooltipTriggerEl => {
|
||||||
|
if (tooltipTriggerEl.getAttribute('title')) {
|
||||||
|
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
structureDisplay.querySelectorAll('.collapse-toggle').forEach(toggleEl => {
|
||||||
|
const collapseTargetId = toggleEl.getAttribute('data-bs-target');
|
||||||
|
if (!collapseTargetId) return;
|
||||||
|
const collapseEl = document.querySelector(collapseTargetId);
|
||||||
|
const toggleIcon = toggleEl.querySelector('.toggle-icon');
|
||||||
|
|
||||||
|
if (collapseEl && toggleIcon) {
|
||||||
|
collapseEl.classList.remove('show');
|
||||||
|
toggleIcon.classList.remove('bi-chevron-down');
|
||||||
|
toggleIcon.classList.add('bi-chevron-right');
|
||||||
|
|
||||||
|
toggleEl.addEventListener('click', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const isCurrentlyShown = collapseEl.classList.contains('show');
|
||||||
|
toggleIcon.classList.toggle('bi-chevron-right', isCurrentlyShown);
|
||||||
|
toggleIcon.classList.toggle('bi-chevron-down', !isCurrentlyShown);
|
||||||
|
});
|
||||||
|
|
||||||
|
collapseEl.addEventListener('show.bs.collapse', () => {
|
||||||
|
toggleIcon.classList.remove('bi-chevron-right');
|
||||||
|
toggleIcon.classList.add('bi-chevron-down');
|
||||||
|
});
|
||||||
|
collapseEl.addEventListener('hide.bs.collapse', () => {
|
||||||
|
toggleIcon.classList.remove('bi-chevron-down');
|
||||||
|
toggleIcon.classList.add('bi-chevron-right');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("JavaScript error in cp_view_processed_ig.html:", e);
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
if (body) {
|
||||||
|
const errorDiv = document.createElement('div');
|
||||||
|
errorDiv.className = 'alert alert-danger m-3';
|
||||||
|
errorDiv.textContent = 'A critical error occurred while initializing the page view. Please check the console for details.';
|
||||||
|
body.insertBefore(errorDiv, body.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -27,11 +27,11 @@
|
|||||||
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
||||||
</small>
|
</small>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<select class="form-select" id="packageSelect" name="packageSelect" onchange="updatePackageFields(this)">
|
<select class="form-select" id="packageSelect" onchange="updatePackageFields(this)">
|
||||||
<option value="">-- Select a Package --</option>
|
<option value="">-- Select a Package --</option>
|
||||||
{% if packages %}
|
{% if packages %}
|
||||||
{% for pkg in packages %}
|
{% for pkg in packages %}
|
||||||
<option value="{{ pkg.id }}">{{ pkg.text }}</option>
|
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<option value="" disabled>No packages available</option>
|
<option value="" disabled>No packages available</option>
|
||||||
@ -39,14 +39,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
|
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
|
||||||
{{ form.package_name(class="form-control") }}
|
{{ form.package_name(class="form-control", id=form.package_name.id) }}
|
||||||
{% for error in form.package_name.errors %}
|
{% for error in form.package_name.errors %}
|
||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
||||||
{{ form.version(class="form-control") }}
|
{{ form.version(class="form-control", id=form.version.id) }}
|
||||||
{% for error in form.version.errors %}
|
{% for error in form.version.errors %}
|
||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -125,21 +125,31 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const form = document.getElementById('validationForm');
|
|
||||||
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
|
||||||
const packageSelect = document.getElementById('packageSelect');
|
const packageSelect = document.getElementById('packageSelect');
|
||||||
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
||||||
const versionInput = document.getElementById('{{ form.version.id }}');
|
const versionInput = document.getElementById('{{ form.version.id }}');
|
||||||
|
|
||||||
|
// Debug DOM elements
|
||||||
|
console.log('Package Select:', packageSelect ? 'Found' : 'Missing');
|
||||||
|
console.log('Package Name Input:', packageNameInput ? 'Found' : 'Missing');
|
||||||
|
console.log('Version Input:', versionInput ? 'Found' : 'Missing');
|
||||||
|
|
||||||
// Update package_name and version fields from dropdown
|
// Update package_name and version fields from dropdown
|
||||||
function updatePackageFields(select) {
|
function updatePackageFields(select) {
|
||||||
const value = select.value;
|
const value = select.value;
|
||||||
console.log('Selected package:', value);
|
console.log('Selected package:', value);
|
||||||
|
|
||||||
|
if (!packageNameInput || !versionInput) {
|
||||||
|
console.error('Input fields not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const parts = value.split('#');
|
const [name, version] = value.split('#');
|
||||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
if (name && version) {
|
||||||
packageNameInput.value = parts[0];
|
packageNameInput.value = name;
|
||||||
versionInput.value = parts[1];
|
versionInput.value = version;
|
||||||
|
console.log('Updated fields:', { packageName: name, version });
|
||||||
} else {
|
} else {
|
||||||
console.warn('Invalid package format:', value);
|
console.warn('Invalid package format:', value);
|
||||||
packageNameInput.value = '';
|
packageNameInput.value = '';
|
||||||
@ -148,19 +158,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
} else {
|
} else {
|
||||||
packageNameInput.value = '';
|
packageNameInput.value = '';
|
||||||
versionInput.value = '';
|
versionInput.value = '';
|
||||||
|
console.log('Cleared fields: no package selected');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize dropdown with current form values
|
// Initialize dropdown based on form values
|
||||||
if (packageNameInput.value && versionInput.value) {
|
if (packageNameInput && versionInput && packageNameInput.value && versionInput.value) {
|
||||||
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
||||||
const option = packageSelect.querySelector(`option[value="${currentValue}"]`);
|
if (packageSelect.querySelector(`option[value="${currentValue}"]`)) {
|
||||||
if (option) {
|
|
||||||
packageSelect.value = currentValue;
|
packageSelect.value = currentValue;
|
||||||
|
console.log('Initialized dropdown with:', currentValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug dropdown options
|
||||||
|
console.log('Dropdown options:', Array.from(packageSelect.options).map(opt => opt.value));
|
||||||
|
|
||||||
|
// Attach event listener explicitly
|
||||||
|
if (packageSelect) {
|
||||||
|
packageSelect.addEventListener('change', function() {
|
||||||
|
updatePackageFields(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Client-side JSON validation preview
|
// Client-side JSON validation preview
|
||||||
|
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
||||||
|
if (sampleInput) {
|
||||||
sampleInput.addEventListener('input', function() {
|
sampleInput.addEventListener('input', function() {
|
||||||
try {
|
try {
|
||||||
JSON.parse(this.value);
|
JSON.parse(this.value);
|
||||||
@ -171,9 +194,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
this.classList.add('is-invalid');
|
this.classList.add('is-invalid');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
// Debug dropdown options
|
|
||||||
console.log('Dropdown options:', Array.from(packageSelect.options).map(opt => ({value: opt.value, text: opt.textContent})));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user