V1 package

This commit is contained in:
Joshua Hare 2025-04-10 21:43:36 +10:00
parent 753cc4e6c1
commit 92bcc185e1
20 changed files with 274 additions and 2641 deletions

View File

@ -1,28 +0,0 @@
# app/modules/fhir_ig_importer/__init__.py
from flask import Blueprint
# --- Module Metadata ---
metadata = {
'module_id': 'fhir_ig_importer', # Matches folder name
'display_name': 'FHIR IG Importer',
'description': 'Imports FHIR Implementation Guide packages from a registry.',
'version': '0.1.0',
# No main nav items, will be accessed via Control Panel
'nav_items': []
}
# --- End Module Metadata ---
# Define Blueprint
# We'll mount this under the control panel later
bp = Blueprint(
metadata['module_id'],
__name__,
template_folder='templates',
# Define a URL prefix if mounting standalone, but we'll likely register
# it under /control-panel via app/__init__.py later
# url_prefix='/fhir-importer'
)
# Import routes after creating blueprint
from . import routes, forms # Import forms too

View File

@ -1,162 +0,0 @@
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
import os
import services # Assuming your existing services module for FHIR IG handling
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///instance/fhir_ig.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['FHIR_PACKAGES_DIR'] = os.path.join(app.instance_path, 'fhir_packages')
os.makedirs(app.config['FHIR_PACKAGES_DIR'], exist_ok=True)
db = SQLAlchemy(app)
# Simplified ProcessedIg model (no user-related fields)
class ProcessedIg(db.Model):
id = db.Column(db.Integer, primary_key=True)
package_name = db.Column(db.String(128), nullable=False)
version = db.Column(db.String(32), nullable=False)
processed_date = db.Column(db.DateTime, nullable=False)
resource_types_info = db.Column(db.JSON, nullable=False) # List of resource type metadata
must_support_elements = db.Column(db.JSON, nullable=True) # Dict of MS elements
examples = db.Column(db.JSON, nullable=True) # Dict of example filepaths
# Landing page with two buttons
@app.route('/')
def index():
return render_template_string('''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FHIR IG Toolkit</title>
<style>
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
.button { padding: 15px 30px; margin: 10px; font-size: 16px; }
</style>
</head>
<body>
<h1>FHIR IG Toolkit</h1>
<p>Simple tool for importing and viewing FHIR Implementation Guides.</p>
<a href="{{ url_for('import_ig') }}"><button class="button">Import FHIR IG</button></a>
<a href="{{ url_for('view_igs') }}"><button class="button">View Downloaded IGs</button></a>
</body>
</html>
''')
# Import IG route
@app.route('/import-ig', methods=['GET', 'POST'])
def import_ig():
if request.method == 'POST':
name = request.form.get('name')
version = request.form.get('version', 'latest')
try:
# Call your existing service to download package and dependencies
result = services.import_package_and_dependencies(name, version, app.config['FHIR_PACKAGES_DIR'])
downloaded_files = result.get('downloaded', [])
for file_path in downloaded_files:
# Process each downloaded package
package_info = services.process_package_file(file_path)
processed_ig = ProcessedIg(
package_name=package_info['name'],
version=package_info['version'],
processed_date=package_info['processed_date'],
resource_types_info=package_info['resource_types_info'],
must_support_elements=package_info.get('must_support_elements'),
examples=package_info.get('examples')
)
db.session.add(processed_ig)
db.session.commit()
flash(f"Successfully imported {name} {version} and dependencies!", "success")
return redirect(url_for('view_igs'))
except Exception as e:
flash(f"Error importing IG: {str(e)}", "error")
return redirect(url_for('import_ig'))
return render_template_string('''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Import FHIR IG</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.form { max-width: 400px; margin: 0 auto; }
.field { margin: 10px 0; }
input[type="text"] { width: 100%; padding: 5px; }
.button { padding: 10px 20px; }
.message { color: {% if category == "success" %}green{% else %}red{% endif %}; }
</style>
</head>
<body>
<h1>Import FHIR IG</h1>
<form class="form" method="POST">
<div class="field">
<label>Package Name:</label>
<input type="text" name="name" placeholder="e.g., hl7.fhir.us.core" required>
</div>
<div class="field">
<label>Version (optional):</label>
<input type="text" name="version" placeholder="e.g., 1.0.0 or latest">
</div>
<button class="button" type="submit">Import</button>
<a href="{{ url_for('index') }}"><button class="button" type="button">Back</button></a>
</form>
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
<p class="message">{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</body>
</html>
''')
# View Downloaded IGs route
@app.route('/view-igs')
def view_igs():
igs = ProcessedIg.query.all()
return render_template_string('''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>View Downloaded IGs</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
table { width: 80%; margin: 20px auto; border-collapse: collapse; }
th, td { padding: 10px; border: 1px solid #ddd; text-align: left; }
th { background-color: #f2f2f2; }
.button { padding: 10px 20px; }
</style>
</head>
<body>
<h1>Downloaded FHIR IGs</h1>
<table>
<tr>
<th>Package Name</th>
<th>Version</th>
<th>Processed Date</th>
<th>Resource Types</th>
</tr>
{% for ig in igs %}
<tr>
<td>{{ ig.package_name }}</td>
<td>{{ ig.version }}</td>
<td>{{ ig.processed_date }}</td>
<td>{{ ig.resource_types_info | length }} types</td>
</tr>
{% endfor %}
</table>
<a href="{{ url_for('index') }}"><button class="button">Back</button></a>
</body>
</html>
''', igs=igs)
# Initialize DB
with app.app_context():
db.create_all()
if __name__ == '__main__':
app.run(debug=True)

View File

@ -1,91 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
</head>
<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('core.index') }}">{{ site_name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
{# FIX: Added check for request.endpoint #}
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint == 'core.index' else '' }}" aria-current="page" href="{{ url_for('core.index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
</li>
{% if module_menu_items %}
{% for item in module_menu_items %}
<li class="nav-item">
{# FIX: Added check for request.endpoint #}
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint == item.endpoint else '' }}" href="{{ url_for(item.endpoint) }}">
<i class="{{ item.icon | default('bi bi-box-seam me-1') }}"></i> {{ item.title }}
</a>
</li>
{% endfor %}
{% endif %}
</ul>
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
{% if current_user.role == 'admin' %}
<li class="nav-item">
{# FIX: Added check for request.endpoint #}
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint.startswith('control_panel.') else '' }}" href="{{ url_for('control_panel.index') }}">
<i class="bi bi-gear me-1"></i> Control Panel
</a>
</li>
{% endif %}
<li class="nav-item"><span class="navbar-text me-2"><i class="bi bi-person-circle me-1"></i> {{ current_user.username }}</span></li>
<li class="nav-item"><a class="nav-link" href="{{ url_for('auth.logout') }}"><i class="bi bi-box-arrow-right me-1"></i> Logout</a></li>
{% else %}
<li class="nav-item">
{# FIX: Added check for request.endpoint #}
<a class="nav-link {{ 'active' if request.endpoint and request.endpoint == 'auth.login' else '' }}" href="{{ url_for('auth.login') }}"><i class="bi bi-box-arrow-in-right me-1"></i> Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<main class="container flex-grow-1">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category or 'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
{% set current_year = now.year %} <span class="text-muted">&copy; {{ current_year }} {{ site_name }}. All rights reserved.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}
{# Tooltip Initialization Script #}
<script>
document.addEventListener('DOMContentLoaded', function () {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
});
</script>
{% endblock %}
</body>
</html>

View File

@ -1,135 +0,0 @@
{# app/control_panel/templates/cp_downloaded_igs.html #}
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
<div>
<a href="{{ url_for('fhir_ig_importer.import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
<a href="{{ url_for('control_panel.index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to CP Index</a>
</div>
</div>
{% if error_message %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">Error</h5>
{{ error_message }}
</div>
{% endif %}
{# NOTE: The block calculating processed_ids set using {% set %} was REMOVED from here #}
{# --- Start Two Column Layout --- #}
<div class="row g-4">
{# --- Left Column: Downloaded Packages (Horizontal Buttons) --- #}
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
<div class="card-body">
{% if packages %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<p class="mb-2"><small><span class="badge bg-danger text-dark border me-1">Risk:</span>= Duplicate Dependancy with different versions</small></p>
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
</thead>
<tbody>
{% for pkg in packages %}
{% set is_processed = (pkg.name, pkg.version) in processed_ids %}
{# --- ADDED: Check for duplicate name --- #}
{% set is_duplicate = pkg.name in duplicate_names %}
{# --- ADDED: Assign row class based on duplicate group --- #}
<tr class="{{ duplicate_groups.get(pkg.name, '') if is_duplicate else '' }}">
<td>
{# --- ADDED: Risk Badge for duplicates --- #}
{% if is_duplicate %}
<span class="badge bg-danger mb-1 d-block">Duplicate</span>
{% endif %}
{# --- End Add --- #}
<code>{{ pkg.name }}</code>
</td>
<td>{{ pkg.version }}</td>
<td> {# Actions #}
<div class="btn-group btn-group-sm" role="group">
{% if is_processed %}
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
{% else %}
<form action="{{ url_for('control_panel.process_ig') }}" method="POST" style="display: inline-block;">
<input type="hidden" name="package_name" value="{{ pkg.name }}">
<input type="hidden" name="package_version" value="{{ pkg.version }}">
<button type="submit" class="btn btn-outline-primary" title="Mark as processed"><i class="bi bi-gear"></i> Process</button>
</form>
{% endif %}
<form action="{{ url_for('control_panel.delete_ig_file') }}" method="POST" style="display: inline-block;" onsubmit="return confirm('Delete file \'{{ pkg.filename }}\'?');">
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-danger" title="Delete File"><i class="bi bi-trash"></i> Delete</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif not error_message %}<p class="text-muted">No downloaded FHIR packages found.</p>{% endif %}
</div>
</div>
</div>{# --- End Left Column --- #}
{# --- Right Column: Processed Packages (Vertical Buttons) --- #}
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
<div class="card-body">
{% 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>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr><th>Package Name</th><th>Version</th><th>Resource Types</th><th>Actions</th></tr>
</thead>
<tbody>
{% for processed_ig in processed_list %}
<tr>
<td>{# Tooltip for Processed At / Status #}
{% set tooltip_title_parts = [] %}
{% if processed_ig.processed_at %}{% set _ = tooltip_title_parts.append("Processed: " + processed_ig.processed_at.strftime('%Y-%m-%d %H:%M')) %}{% endif %}
{% if processed_ig.status %}{% set _ = tooltip_title_parts.append("Status: " + processed_ig.status) %}{% endif %}
{% set tooltip_text = tooltip_title_parts | join('\n') %}
<code data-bs-toggle="tooltip" data-bs-placement="top" title="{{ tooltip_text }}">{{ processed_ig.package_name }}</code>
</td>
<td>{{ processed_ig.package_version }}</td>
<td> {# Resource Types Cell w/ Badges #}
{% set types_info = processed_ig.resource_types_info %}
{% if types_info %}<div class="d-flex flex-wrap gap-1">{% for type_info in types_info %}{% if type_info.must_support %}<span class="badge bg-warning text-dark border" title="Has Must Support">{{ type_info.name }}</span>{% else %}<span class="badge bg-light text-dark border">{{ type_info.name }}</span>{% endif %}{% endfor %}</div>{% else %}<small class="text-muted">N/A</small>{% endif %}
</td>
<td>
{# Vertical Button Group #}
<div class="btn-group-vertical btn-group-sm w-100" role="group" aria-label="Processed Package Actions for {{ processed_ig.package_name }}">
<a href="{{ url_for('control_panel.view_processed_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
<form action="{{ url_for('control_panel.unload_ig') }}" method="POST" onsubmit="return confirm('Unload record for \'{{ processed_ig.package_name }}#{{ processed_ig.package_version }}\'?');">
<input type="hidden" name="processed_ig_id" value="{{ processed_ig.id }}">
<button type="submit" class="btn btn-outline-warning w-100" title="Unload/Remove Processed Record"><i class="bi bi-x-lg"></i> Unload</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif not error_message %}<p class="text-muted">No packages recorded as processed yet.</p>{% endif %}
</div>
</div>
</div>{# --- End Right Column --- #}
</div>{# --- End Row --- #}
</div>{# End container #}
{% endblock %}
{# Tooltip JS Initializer should be in base.html #}
{% block scripts %}{{ super() }}{% endblock %}

View File

@ -1,405 +0,0 @@
{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<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('control_panel.list_downloaded_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.package_version }}</dd>
<dt class="col-sm-3">Processed At</dt>
<dd class="col-sm-9">{{ processed_ig.processed_at.strftime('%Y-%m-%d %H:%M:%S UTC') if processed_ig.processed_at else 'N/A' }}</dd>
<dt class="col-sm-3">Processing Status</dt>
<dd class="col-sm-9">
<span class="badge rounded-pill text-bg-{{ 'success' if processed_ig.status == 'processed' else ('warning' if processed_ig.status == 'processed_with_errors' else 'secondary') }}">{{ processed_ig.status or 'N/A' }}</span>
</dd>
</dl>
</div>
</div>
<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">MS</span> = Contains Must Support Elements</small></p>
{% if profile_list %}
<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.package_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>
{% endif %}
{% if base_list %}
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = - Examples will be displayed when selecting Base Types if contained in the IG</small></p>
<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.package_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>
{% 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 Details View" 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-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>
{% block scripts %}
{{ super() }}
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
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 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 = "/control-panel/fhir/get-structure";
const exampleBaseUrl = "/control-panel/fhir/get-example";
let currentPkgName = null;
let currentPkgVersion = null;
let currentRawStructureData = null;
document.body.addEventListener('click', function(event) {
const link = event.target.closest('.resource-type-link');
if (!link) return;
event.preventDefault();
currentPkgName = link.dataset.packageName;
currentPkgVersion = link.dataset.packageVersion;
const resourceType = link.dataset.resourceType;
if (!currentPkgName || !currentPkgVersion || !resourceType) return;
const structureParams = new URLSearchParams({
package_name: currentPkgName,
package_version: currentPkgVersion,
resource_type: resourceType
});
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
structureTitle.textContent = `${resourceType} (${currentPkgName}#${currentPkgVersion})`;
rawStructureTitle.textContent = `${resourceType} (${currentPkgName}#${currentPkgVersion})`;
structureDisplay.innerHTML = '';
rawStructureContent.textContent = '';
structureLoading.style.display = 'block';
rawStructureLoading.style.display = 'block';
structureDisplayWrapper.style.display = 'block';
rawStructureWrapper.style.display = 'block';
exampleDisplayWrapper.style.display = 'none';
fetch(structureFetchUrl)
.then(response => 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}`);
currentRawStructureData = result.data;
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
populateExampleSelector(resourceType);
})
.catch(error => {
console.error('Error fetching structure:', error);
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
rawStructureContent.textContent = `Error: ${error.message}`;
})
.finally(() => {
structureLoading.style.display = 'none';
rawStructureLoading.style.display = 'none';
});
});
function populateExampleSelector(resourceOrProfileIdentifier) {
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
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;
exampleContentRaw.textContent = '';
exampleContentJson.textContent = '';
exampleFilename.textContent = '';
exampleContentWrapper.style.display = 'none';
if (!selectedFilePath || !currentPkgName || !currentPkgVersion) return;
const exampleParams = new URLSearchParams({
package_name: currentPkgName,
package_version: currentPkgVersion,
filename: selectedFilePath
});
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
console.log("Fetching example from:", exampleFetchUrl);
exampleLoading.style.display = 'block';
fetch(exampleFetchUrl)
.then(response => {
if (!response.ok) {
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
}
return response.text();
})
.then(content => {
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
exampleContentRaw.textContent = content;
try {
const jsonContent = JSON.parse(content);
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
} catch (e) {
exampleContentJson.textContent = 'Not valid JSON';
}
exampleContentWrapper.style.display = 'block';
})
.catch(error => {
exampleFilename.textContent = 'Error';
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};
elements.forEach(el => {
const path = el.path;
if (!path) return;
const parts = path.split('.');
let currentPath = '';
let parentNode = treeRoot;
for(let i = 0; i < parts.length; i++) {
const part = parts[i];
const currentPartName = part.includes('[') ? part.substring(0, part.indexOf('[')) : part;
currentPath = i === 0 ? currentPartName : `${currentPath}.${part}`;
if (!nodeMap[currentPath]) {
const newNode = { children: {}, element: null, name: part, path: currentPath };
parentNode.children[part] = newNode;
nodeMap[currentPath] = newNode;
}
if (i === parts.length - 1) {
nodeMap[currentPath].element = el;
}
parentNode = nodeMap[currentPath];
}
});
return Object.values(treeRoot.children);
}
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
if (!node || !node.element) return '';
const el = node.element;
const path = el.path || 'N/A';
const min = el.min !== undefined ? el.min : '';
const max = el.max || '';
const short = el.short || '';
const definition = el.definition || '';
const isMustSupport = mustSupportPathsSet.has(path);
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></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 || '';
if (t.targetProfile && t.targetProfile.length > 0) {
const targetTypes = t.targetProfile.map(p => p.split('/').pop());
s += `(<span class="text-muted">${targetTypes.join('|')}</span>)`;
}
return s;
}).join(' | ');
}
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
let childrenHtml = '';
if (hasChildren) {
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
Object.values(node.children).sort((a,b) => a.name.localeCompare(b.name)).forEach(childNode => {
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, 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" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><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><code class="fw-bold ms-1">${node.name}</code></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, '&quot;').replace(/'/g, '&apos;');
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}</div>`;
itemHtml += `</div>`;
itemHtml += childrenHtml;
itemHtml += `</li>`;
return itemHtml;
}
function renderStructureTree(elements, mustSupportPaths) {
if (!elements || elements.length === 0) {
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found.</em></p>';
return;
}
const mustSupportPathsSet = new Set(mustSupportPaths || []);
console.log("Rendering tree. MS Set:", mustSupportPathsSet);
const treeData = buildTreeData(elements);
let html = '<p><small><span class="badge bg-warning text-dark border me-1">MS</span> = Must Support (Row Highlighted)</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">MS</div><div class="col-lg-3 col-md-4">Description</div></div>`;
html += '<ul class="list-group list-group-flush">';
treeData.sort((a,b) => a.name.localeCompare(b.name)).forEach(rootNode => {
html += renderNodeAsLi(rootNode, mustSupportPathsSet, 0);
});
html += '</ul>';
structureDisplay.innerHTML = html;
var tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
collapseEl.addEventListener('show.bs.collapse', event => {
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
});
collapseEl.addEventListener('hide.bs.collapse', event => {
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
});
});
}
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
});
</script>
{% endblock scripts %}
{% endblock content %} {# Main content block END #}

View File

@ -1,19 +0,0 @@
# app/modules/fhir_ig_importer/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Regexp
class IgImportForm(FlaskForm):
"""Form for specifying an IG package to import."""
# Basic validation for FHIR package names (e.g., hl7.fhir.r4.core)
package_name = StringField('Package Name (e.g., hl7.fhir.au.base)', validators=[
DataRequired(),
Regexp(r'^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
])
# Basic validation for version (e.g., 4.1.0, current)
package_version = StringField('Package Version (e.g., 4.1.0 or current)', validators=[
DataRequired(),
Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
])
submit = SubmitField('Fetch & Download IG')

View File

@ -1,16 +0,0 @@
{% extends "base.html" %} {% block content %} <div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="https://placehold.co/72x57/0d6efd/white?text=PAS" alt="PAS Logo Placeholder" width="72" height="57">
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
This is the starting point for our modular Patient Administration System.
We will build upon this core framework by adding modules through the control panel.
</p>
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
<button type="button" class="btn btn-primary btn-lg px-4 gap-3">Primary button</button>
<button type="button" class="btn btn-outline-secondary btn-lg px-4">Secondary</button>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,162 +0,0 @@
# app/modules/fhir_ig_importer/routes.py
import requests
import os
import tarfile # Needed for find_and_extract_sd
import gzip
import json
import io
import re
from flask import (render_template, redirect, url_for, flash, request,
current_app, jsonify, send_file)
from flask_login import login_required
from app.decorators import admin_required
from werkzeug.utils import secure_filename
from . import bp
from .forms import IgImportForm
# Import the services module
from . import services
# Import ProcessedIg model for get_structure_definition
from app.models import ProcessedIg
from app import db
# --- Helper: Find/Extract SD ---
# Moved from services.py to be local to routes that use it, or keep in services and call services.find_and_extract_sd
def find_and_extract_sd(tgz_path, resource_identifier):
"""Helper to find and extract SD json from a given tgz path by ID, Name, or Type."""
sd_data = None; found_path = None; logger = current_app.logger # Use current_app logger
if not tgz_path or not os.path.exists(tgz_path): logger.error(f"File not found in find_and_extract_sd: {tgz_path}"); return None, None
try:
with tarfile.open(tgz_path, "r:gz") as tar:
logger.debug(f"Searching for SD matching '{resource_identifier}' in {os.path.basename(tgz_path)}")
for member in tar:
if member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json'):
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']: continue
fileobj = None
try:
fileobj = tar.extractfile(member)
if fileobj:
content_bytes = fileobj.read(); content_string = content_bytes.decode('utf-8-sig'); data = json.loads(content_string)
if isinstance(data, dict) and data.get('resourceType') == 'StructureDefinition':
sd_id = data.get('id'); sd_name = data.get('name'); sd_type = data.get('type')
if resource_identifier == sd_type or resource_identifier == sd_id or resource_identifier == sd_name:
sd_data = data; found_path = member.name; logger.info(f"Found matching SD for '{resource_identifier}' at path: {found_path}"); break
except Exception as e: logger.warning(f"Could not read/parse potential SD {member.name}: {e}")
finally:
if fileobj: fileobj.close()
if sd_data is None: logger.warning(f"SD matching '{resource_identifier}' not found within archive {os.path.basename(tgz_path)}")
except Exception as e: logger.error(f"Error reading archive {tgz_path} in find_and_extract_sd: {e}", exc_info=True); raise
return sd_data, found_path
# --- End Helper ---
# --- Route for the main import page ---
@bp.route('/import-ig', methods=['GET', 'POST'])
@login_required
@admin_required
def import_ig():
"""Handles FHIR IG recursive download using services."""
form = IgImportForm()
template_context = {"title": "Import FHIR IG", "form": form, "results": None }
if form.validate_on_submit():
package_name = form.package_name.data; package_version = form.package_version.data
template_context.update(package_name=package_name, package_version=package_version)
flash(f"Starting full import for {package_name}#{package_version}...", "info"); current_app.logger.info(f"Calling import service for: {package_name}#{package_version}")
try:
# Call the CORRECT orchestrator service function
import_results = services.import_package_and_dependencies(package_name, package_version)
template_context["results"] = import_results
# Flash summary messages
dl_count = len(import_results.get('downloaded', {})); proc_count = len(import_results.get('processed', set())); error_count = len(import_results.get('errors', []))
if dl_count > 0: flash(f"Downloaded/verified {dl_count} package file(s).", "success")
if proc_count < dl_count and dl_count > 0 : flash(f"Dependency data extraction failed for {dl_count - proc_count} package(s).", "warning")
if error_count > 0: flash(f"{error_count} total error(s) occurred.", "danger")
elif dl_count == 0 and error_count == 0: flash("No packages needed downloading or initial package failed.", "info")
elif error_count == 0: flash("Import process completed successfully.", "success")
except Exception as e:
fatal_error = f"Critical unexpected error during import: {e}"; template_context["fatal_error"] = fatal_error; current_app.logger.error(f"Critical import error: {e}", exc_info=True); flash(fatal_error, "danger")
return render_template('fhir_ig_importer/import_ig_page.html', **template_context)
return render_template('fhir_ig_importer/import_ig_page.html', **template_context)
# --- Route to get StructureDefinition elements ---
@bp.route('/get-structure')
@login_required
@admin_required
def get_structure_definition():
"""API endpoint to fetch SD elements and pre-calculated Must Support paths."""
package_name = request.args.get('package_name'); package_version = request.args.get('package_version'); resource_identifier = request.args.get('resource_type')
error_response_data = {"elements": [], "must_support_paths": []}
if not all([package_name, package_version, resource_identifier]): error_response_data["error"] = "Missing query parameters"; return jsonify(error_response_data), 400
current_app.logger.info(f"Request for structure: {package_name}#{package_version} / {resource_identifier}")
# Find the primary package file
package_dir_name = 'fhir_packages'; download_dir = os.path.join(current_app.instance_path, package_dir_name)
# Use service helper for consistency
filename = services._construct_tgz_filename(package_name, package_version)
tgz_path = os.path.join(download_dir, filename)
if not os.path.exists(tgz_path): error_response_data["error"] = f"Package file not found: {filename}"; return jsonify(error_response_data), 404
sd_data = None; found_path = None; error_msg = None
try:
# Call the local helper function correctly
sd_data, found_path = find_and_extract_sd(tgz_path, resource_identifier)
# Fallback check
if sd_data is None:
core_pkg_name = "hl7.fhir.r4.core"; core_pkg_version = "4.0.1" # TODO: Make dynamic
core_filename = services._construct_tgz_filename(core_pkg_name, core_pkg_version)
core_tgz_path = os.path.join(download_dir, core_filename)
if os.path.exists(core_tgz_path):
current_app.logger.info(f"Trying fallback search in {core_pkg_name}...")
sd_data, found_path = find_and_extract_sd(core_tgz_path, resource_identifier) # Call local helper
else: current_app.logger.warning(f"Core package {core_tgz_path} not found.")
except Exception as e:
error_msg = f"Error searching package(s): {e}"; current_app.logger.error(error_msg, exc_info=True); error_response_data["error"] = error_msg; return jsonify(error_response_data), 500
if sd_data is None: error_msg = f"SD for '{resource_identifier}' not found."; error_response_data["error"] = error_msg; return jsonify(error_response_data), 404
# Extract elements
elements = sd_data.get('snapshot', {}).get('element', [])
if not elements: elements = sd_data.get('differential', {}).get('element', [])
# Fetch pre-calculated Must Support paths from DB
must_support_paths = [];
try:
stmt = db.select(ProcessedIg).filter_by(package_name=package_name, package_version=package_version); processed_ig_record = db.session.scalar(stmt)
if processed_ig_record: all_ms_paths_dict = processed_ig_record.must_support_elements; must_support_paths = all_ms_paths_dict.get(resource_identifier, [])
else: current_app.logger.warning(f"No ProcessedIg record found for {package_name}#{package_version}")
except Exception as e: current_app.logger.error(f"Error fetching MS paths from DB: {e}", exc_info=True)
current_app.logger.info(f"Returning {len(elements)} elements for {resource_identifier} from {found_path or 'Unknown File'}")
return jsonify({"elements": elements, "must_support_paths": must_support_paths})
# --- Route to get raw example file content ---
@bp.route('/get-example')
@login_required
@admin_required
def get_example_content():
# ... (Function remains the same as response #147) ...
package_name = request.args.get('package_name'); package_version = request.args.get('package_version'); example_member_path = request.args.get('filename')
if not all([package_name, package_version, example_member_path]): return jsonify({"error": "Missing query parameters"}), 400
current_app.logger.info(f"Request for example: {package_name}#{package_version} / {example_member_path}")
package_dir_name = 'fhir_packages'; download_dir = os.path.join(current_app.instance_path, package_dir_name)
pkg_filename = services._construct_tgz_filename(package_name, package_version) # Use service helper
tgz_path = os.path.join(download_dir, pkg_filename)
if not os.path.exists(tgz_path): return jsonify({"error": f"Package file not found: {pkg_filename}"}), 404
# Basic security check on member path
safe_member_path = secure_filename(example_member_path.replace("package/","")) # Allow paths within package/
if not example_member_path.startswith('package/') or '..' in example_member_path: return jsonify({"error": "Invalid example file path."}), 400
try:
with tarfile.open(tgz_path, "r:gz") as tar:
try: example_member = tar.getmember(example_member_path) # Use original path here
except KeyError: return jsonify({"error": f"Example file '{example_member_path}' not found."}), 404
example_fileobj = tar.extractfile(example_member)
if not example_fileobj: return jsonify({"error": "Could not extract example file."}), 500
try: content_bytes = example_fileobj.read()
finally: example_fileobj.close()
return content_bytes # Return raw bytes
except tarfile.TarError as e: err_msg = f"Error reading {tgz_path}: {e}"; current_app.logger.error(err_msg); return jsonify({"error": err_msg}), 500
except Exception as e: err_msg = f"Unexpected error getting example {example_member_path}: {e}"; current_app.logger.error(err_msg, exc_info=True); return jsonify({"error": err_msg}), 500

View File

@ -1,345 +0,0 @@
# app/modules/fhir_ig_importer/services.py
import requests
import os
import tarfile
import gzip
import json
import io
import re
import logging
from flask import current_app
from collections import defaultdict
# Constants
FHIR_REGISTRY_BASE_URL = "https://packages.fhir.org"
DOWNLOAD_DIR_NAME = "fhir_packages"
# --- Helper Functions ---
def _get_download_dir():
"""Gets the absolute path to the download directory, creating it if needed."""
logger = logging.getLogger(__name__)
instance_path = None # Initialize
try:
# --- FIX: Indent code inside try block ---
instance_path = current_app.instance_path
logger.debug(f"Using instance path from current_app: {instance_path}")
except RuntimeError:
# --- FIX: Indent code inside except block ---
logger.warning("No app context for instance_path, constructing relative path.")
instance_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'instance'))
logger.debug(f"Constructed instance path: {instance_path}")
# This part depends on instance_path being set above
if not instance_path:
logger.error("Fatal Error: Could not determine instance path.")
return None
download_dir = os.path.join(instance_path, DOWNLOAD_DIR_NAME)
try:
# --- FIX: Indent code inside try block ---
os.makedirs(download_dir, exist_ok=True)
return download_dir
except OSError as e:
# --- FIX: Indent code inside except block ---
logger.error(f"Fatal Error creating dir {download_dir}: {e}", exc_info=True)
return None
def sanitize_filename_part(text): # Public version
"""Basic sanitization for name/version parts of filename."""
# --- FIX: Indent function body ---
safe_text = "".join(c if c.isalnum() or c in ['.', '-'] else '_' for c in text)
safe_text = re.sub(r'_+', '_', safe_text) # Uses re
safe_text = safe_text.strip('_-.')
return safe_text if safe_text else "invalid_name"
def _construct_tgz_filename(name, version):
"""Constructs the standard filename using the sanitized parts."""
# --- FIX: Indent function body ---
return f"{sanitize_filename_part(name)}-{sanitize_filename_part(version)}.tgz"
def find_and_extract_sd(tgz_path, resource_identifier): # Public version
"""Helper to find and extract SD json from a given tgz path by ID, Name, or Type."""
# --- FIX: Ensure consistent indentation ---
sd_data = None
found_path = None
logger = logging.getLogger(__name__)
if not tgz_path or not os.path.exists(tgz_path):
logger.error(f"File not found in find_and_extract_sd: {tgz_path}")
return None, None
try:
with tarfile.open(tgz_path, "r:gz") as tar:
logger.debug(f"Searching for SD matching '{resource_identifier}' in {os.path.basename(tgz_path)}")
for member in tar:
if not (member.isfile() and member.name.startswith('package/') and member.name.lower().endswith('.json')):
continue
if os.path.basename(member.name).lower() in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']:
continue
fileobj = None
try:
fileobj = tar.extractfile(member)
if fileobj:
content_bytes = fileobj.read()
content_string = content_bytes.decode('utf-8-sig')
data = json.loads(content_string)
if isinstance(data, dict) and data.get('resourceType') == 'StructureDefinition':
sd_id = data.get('id')
sd_name = data.get('name')
sd_type = data.get('type')
# Match if requested identifier matches ID, Name, or Base Type
if resource_identifier == sd_type or resource_identifier == sd_id or resource_identifier == sd_name:
sd_data = data
found_path = member.name
logger.info(f"Found matching SD for '{resource_identifier}' at path: {found_path}")
break # Stop searching once found
except Exception as e:
# Log issues reading/parsing individual files but continue search
logger.warning(f"Could not read/parse potential SD {member.name}: {e}")
finally:
if fileobj: fileobj.close() # Ensure resource cleanup
if sd_data is None:
logger.warning(f"SD matching '{resource_identifier}' not found within archive {os.path.basename(tgz_path)}")
except tarfile.TarError as e:
logger.error(f"TarError reading {tgz_path} in find_and_extract_sd: {e}")
raise tarfile.TarError(f"Error reading package archive: {e}") from e
except FileNotFoundError as e:
logger.error(f"FileNotFoundError reading {tgz_path} in find_and_extract_sd: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error in find_and_extract_sd for {tgz_path}: {e}", exc_info=True)
raise
return sd_data, found_path
# --- Core Service Functions ---
def download_package(name, version):
""" Downloads a single FHIR package. Returns (save_path, error_message) """
# --- FIX: Ensure consistent indentation ---
logger = logging.getLogger(__name__)
download_dir = _get_download_dir()
if not download_dir:
return None, "Could not get/create download directory."
package_id = f"{name}#{version}"
package_url = f"{FHIR_REGISTRY_BASE_URL}/{name}/{version}"
filename = _construct_tgz_filename(name, version) # Uses public sanitize via helper
save_path = os.path.join(download_dir, filename)
if os.path.exists(save_path):
logger.info(f"Exists: {filename}")
return save_path, None
logger.info(f"Downloading: {package_id} -> {filename}")
try:
with requests.get(package_url, stream=True, timeout=90) as r:
r.raise_for_status()
with open(save_path, 'wb') as f:
logger.debug(f"Opened {save_path} for writing.")
for chunk in r.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
logger.info(f"Success: Downloaded {filename}")
return save_path, None
except requests.exceptions.RequestException as e:
err_msg = f"Download error for {package_id}: {e}"; logger.error(err_msg); return None, err_msg
except OSError as e:
err_msg = f"File save error for {filename}: {e}"; logger.error(err_msg); return None, err_msg
except Exception as e:
err_msg = f"Unexpected download error for {package_id}: {e}"; logger.error(err_msg, exc_info=True); return None, err_msg
def extract_dependencies(tgz_path):
""" Extracts dependencies dict from package.json. Returns (dep_dict or None on error, error_message) """
# --- FIX: Ensure consistent indentation ---
logger = logging.getLogger(__name__)
package_json_path = "package/package.json"
dependencies = {}
error_message = None
if not tgz_path or not os.path.exists(tgz_path):
return None, f"File not found at {tgz_path}"
try:
with tarfile.open(tgz_path, "r:gz") as tar:
package_json_member = tar.getmember(package_json_path)
package_json_fileobj = tar.extractfile(package_json_member)
if package_json_fileobj:
try:
package_data = json.loads(package_json_fileobj.read().decode('utf-8-sig'))
dependencies = package_data.get('dependencies', {})
finally:
package_json_fileobj.close()
else:
raise FileNotFoundError(f"Could not extract {package_json_path}")
except KeyError:
error_message = f"'{package_json_path}' not found in {os.path.basename(tgz_path)}.";
logger.warning(error_message) # OK if missing
except (json.JSONDecodeError, UnicodeDecodeError) as e:
error_message = f"Parse error in {package_json_path}: {e}"; logger.error(error_message); dependencies = None # Parsing failed
except (tarfile.TarError, FileNotFoundError) as e:
error_message = f"Archive error {os.path.basename(tgz_path)}: {e}"; logger.error(error_message); dependencies = None # Archive read failed
except Exception as e:
error_message = f"Unexpected error extracting deps: {e}"; logger.error(error_message, exc_info=True); dependencies = None
return dependencies, error_message
# --- Recursive Import Orchestrator ---
def import_package_and_dependencies(initial_name, initial_version):
"""Orchestrates recursive download and dependency extraction."""
# --- FIX: Ensure consistent indentation ---
logger = logging.getLogger(__name__)
logger.info(f"Starting recursive import for {initial_name}#{initial_version}")
results = {'requested': (initial_name, initial_version), 'processed': set(), 'downloaded': {}, 'all_dependencies': {}, 'errors': [] }
pending_queue = [(initial_name, initial_version)]
processed_lookup = set()
while pending_queue:
name, version = pending_queue.pop(0)
package_id_tuple = (name, version)
if package_id_tuple in processed_lookup:
continue
logger.info(f"Processing: {name}#{version}")
processed_lookup.add(package_id_tuple)
save_path, dl_error = download_package(name, version)
if dl_error:
error_msg = f"Download failed for {name}#{version}: {dl_error}"
results['errors'].append(error_msg)
if package_id_tuple == results['requested']:
logger.error("Aborting import: Initial package download failed.")
break
else:
continue
else: # Download OK
results['downloaded'][package_id_tuple] = save_path
# --- Correctly indented block ---
dependencies, dep_error = extract_dependencies(save_path)
if dep_error:
results['errors'].append(f"Dependency extraction failed for {name}#{version}: {dep_error}")
elif dependencies is not None:
results['all_dependencies'][package_id_tuple] = dependencies
results['processed'].add(package_id_tuple)
logger.debug(f"Dependencies for {name}#{version}: {list(dependencies.keys())}")
for dep_name, dep_version in dependencies.items():
if isinstance(dep_name, str) and isinstance(dep_version, str) and dep_name and dep_version:
dep_tuple = (dep_name, dep_version)
if dep_tuple not in processed_lookup:
if dep_tuple not in pending_queue:
pending_queue.append(dep_tuple)
logger.debug(f"Added to queue: {dep_name}#{dep_version}")
else:
logger.warning(f"Skipping invalid dependency '{dep_name}': '{dep_version}' in {name}#{version}")
# --- End Correctly indented block ---
proc_count=len(results['processed']); dl_count=len(results['downloaded']); err_count=len(results['errors'])
logger.info(f"Import finished. Processed: {proc_count}, Downloaded/Verified: {dl_count}, Errors: {err_count}")
return results
# --- Package File Content Processor (V6.2 - Fixed MS path handling) ---
def process_package_file(tgz_path):
""" Extracts types, profile status, MS elements, and examples from a downloaded .tgz package (Single Pass). """
logger = logging.getLogger(__name__)
logger.info(f"Processing package file details (V6.2 Logic): {tgz_path}")
results = {'resource_types_info': [], 'must_support_elements': {}, 'examples': {}, 'errors': [] }
resource_info = defaultdict(lambda: {'name': None, 'type': None, 'is_profile': False, 'ms_flag': False, 'ms_paths': set(), 'examples': set()})
if not tgz_path or not os.path.exists(tgz_path):
results['errors'].append(f"Package file not found: {tgz_path}"); return results
try:
with tarfile.open(tgz_path, "r:gz") as tar:
for member in tar:
if not member.isfile() or not member.name.startswith('package/') or not member.name.lower().endswith(('.json', '.xml', '.html')): continue
member_name_lower = member.name.lower(); base_filename_lower = os.path.basename(member_name_lower); fileobj = None
if base_filename_lower in ['package.json', '.index.json', 'validation-summary.json', 'validation-oo.json']: continue
is_example = member.name.startswith('package/example/') or 'example' in base_filename_lower
is_json = member_name_lower.endswith('.json')
try: # Process individual member
if is_json:
fileobj = tar.extractfile(member);
if not fileobj: continue
content_bytes = fileobj.read(); content_string = content_bytes.decode('utf-8-sig'); data = json.loads(content_string)
if not isinstance(data, dict) or 'resourceType' not in data: continue
resource_type = data['resourceType']; entry_key = resource_type; is_sd = False
if resource_type == 'StructureDefinition':
is_sd = True; profile_id = data.get('id') or data.get('name'); sd_type = data.get('type'); sd_base = data.get('baseDefinition'); is_profile_sd = bool(sd_base);
if not profile_id or not sd_type: logger.warning(f"SD missing ID or Type: {member.name}"); continue
entry_key = profile_id
entry = resource_info[entry_key]; entry.setdefault('type', resource_type) # Ensure type exists
if is_sd:
entry['name'] = entry_key; entry['type'] = sd_type; entry['is_profile'] = is_profile_sd;
if not entry.get('sd_processed'):
has_ms = False; ms_paths_for_sd = set()
for element_list in [data.get('snapshot', {}).get('element', []), data.get('differential', {}).get('element', [])]:
for element in element_list:
if isinstance(element, dict) and element.get('mustSupport') is True:
# --- FIX: Check path safely ---
element_path = element.get('path')
if element_path: # Only add if path exists
ms_paths_for_sd.add(element_path)
has_ms = True # Mark MS found if we added a path
else:
logger.warning(f"Found mustSupport=true without path in element of {entry_key}")
# --- End FIX ---
if ms_paths_for_sd: entry['ms_paths'] = ms_paths_for_sd # Store the set of paths
if has_ms: entry['ms_flag'] = True; logger.debug(f" Found MS elements in {entry_key}") # Use boolean flag
entry['sd_processed'] = True # Mark MS check done
elif is_example: # JSON Example
key_to_use = None; profile_meta = data.get('meta', {}).get('profile', [])
if profile_meta and isinstance(profile_meta, list):
for profile_url in profile_meta: profile_id_from_meta = profile_url.split('/')[-1];
if profile_id_from_meta in resource_info: key_to_use = profile_id_from_meta; break
if not key_to_use: key_to_use = resource_type
if key_to_use not in resource_info: resource_info[key_to_use].update({'name': key_to_use, 'type': resource_type})
resource_info[key_to_use]['examples'].add(member.name)
elif is_example: # XML/HTML examples
# ... (XML/HTML example association logic) ...
guessed_type = base_filename_lower.split('-')[0].capitalize(); guessed_profile_id = base_filename_lower.split('-')[0]; key_to_use = None
if guessed_profile_id in resource_info: key_to_use = guessed_profile_id
elif guessed_type in resource_info: key_to_use = guessed_type
if key_to_use: resource_info[key_to_use]['examples'].add(member.name)
else: logger.warning(f"Could not associate non-JSON example {member.name}")
except Exception as e: logger.warning(f"Could not process member {member.name}: {e}", exc_info=False)
finally:
if fileobj: fileobj.close()
# -- End Member Loop --
# --- Final formatting moved INSIDE the main try block ---
final_list = []; final_ms_elements = {}; final_examples = {}
logger.debug(f"Formatting results from resource_info keys: {list(resource_info.keys())}")
for key, info in resource_info.items():
display_name = info.get('name') or key; base_type = info.get('type')
if display_name or base_type:
logger.debug(f" Formatting item '{display_name}': type='{base_type}', profile='{info.get('is_profile', False)}', ms_flag='{info.get('ms_flag', False)}'")
final_list.append({'name': display_name, 'type': base_type, 'is_profile': info.get('is_profile', False), 'must_support': info.get('ms_flag', False)}) # Ensure 'must_support' key uses 'ms_flag'
if info['ms_paths']: final_ms_elements[display_name] = sorted(list(info['ms_paths']))
if info['examples']: final_examples[display_name] = sorted(list(info['examples']))
else: logger.warning(f"Skipping formatting for key: {key}")
results['resource_types_info'] = sorted(final_list, key=lambda x: (not x.get('is_profile', False), x.get('name', '')))
results['must_support_elements'] = final_ms_elements
results['examples'] = final_examples
# --- End formatting moved inside ---
except Exception as e:
err_msg = f"Error processing package file {tgz_path}: {e}"; logger.error(err_msg, exc_info=True); results['errors'].append(err_msg)
# Logging counts
final_types_count = len(results['resource_types_info']); ms_count = sum(1 for r in results['resource_types_info'] if r['must_support']); total_ms_paths = sum(len(v) for v in results['must_support_elements'].values()); total_examples = sum(len(v) for v in results['examples'].values())
logger.info(f"V6.2 Extraction: {final_types_count} items ({ms_count} MS; {total_ms_paths} MS paths; {total_examples} examples) from {os.path.basename(tgz_path)}")
return results

216
README.md Normal file
View File

@ -0,0 +1,216 @@
# FHIRFLARE IG Toolkit
## Overview
FHIRFLARE IG Toolkit is a Flask-based web application designed to simplify the management of FHIR Implementation Guides (IGs). It allows users to import, process, view, and manage FHIR packages, with features to handle duplicate dependencies and visualize processed IGs. The toolkit is built to assist developers, researchers, and healthcare professionals working with FHIR standards.
### Key Features
- **Import FHIR Packages**: Download FHIR IGs and their dependencies by specifying package names and versions.
- **Manage Duplicates**: Detect and highlight duplicate packages with different versions, using color-coded indicators.
- **Process IGs**: Extract and process FHIR resources, including structure definitions, must-support elements, and examples.
- **View Details**: Explore processed IGs with detailed views of resource types and examples.
- **Database Integration**: Store processed IGs in a SQLite database for persistence.
- **User-Friendly Interface**: Built with Bootstrap for a responsive and intuitive UI.
## Prerequisites
Before setting up the project, ensure you have the following installed:
- **Python 3.8+**
- **Docker** (optional, for containerized deployment)
- **pip** (Python package manager)
## Setup Instructions
### 1. Clone the Repository
```bash
git clone https://github.com/your-username/FLARE-FHIR-IG-Toolkit.git
cd FLARE-FHIR-IG-Toolkit
```
### 2. Create a Virtual Environment
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
### 3. Install Dependencies
Install the required Python packages using `pip`:
```bash
pip install -r requirements.txt
```
If you dont have a `requirements.txt` file, you can install the core dependencies manually:
```bash
pip install flask flask-sqlalchemy flask-wtf
```
### 4. Set Up the Instance Directory
The application uses an `instance` directory to store the SQLite database and FHIR packages. Create this directory and set appropriate permissions:
```bash
mkdir instance
mkdir instance/fhir_packages
chmod -R 777 instance # Ensure the directory is writable
```
### 5. Initialize the Database
The application uses a SQLite database (`instance/fhir_ig.db`) to store processed IGs. The database is automatically created when you first run the application, but you can also initialize it manually:
```bash
python -c "from app import db; db.create_all()"
```
### 6. Run the Application Locally
Start the Flask development server:
```bash
python app.py
```
The application will be available at `http://localhost:5000`.
### 7. (Optional) Run with Docker
If you prefer to run the application in a Docker container:
```bash
docker build -t flare-fhir-ig-toolkit .
docker run -p 5000:5000 -v $(pwd)/instance:/app/instance flare-fhir-ig-toolkit
```
The application will be available at `http://localhost:5000`.
## Database Management
The application uses a SQLite database (`instance/fhir_ig.db`) to store processed IGs. Below are steps to create, purge, and recreate the database.
### Creating the Database
The database is automatically created when you first run the application. To create it manually:
```bash
python -c "from app import db; db.create_all()"
```
This will create `instance/fhir_ig.db` with the necessary tables.
### Purging the Database
To purge the database (delete all data while keeping the schema):
1. Stop the application if its running.
2. Run the following command to drop all tables and recreate them:
```bash
python -c "from app import db; db.drop_all(); db.create_all()"
```
3. Alternatively, you can delete the database file and recreate it:
```bash
rm instance/fhir_ig.db
python -c "from app import db; db.create_all()"
```
### Recreating the Database
To completely recreate the database (e.g., after making schema changes):
1. Stop the application if its running.
2. Delete the existing database file:
```bash
rm instance/fhir_ig.db
```
3. Recreate the database:
```bash
python -c "from app import db; db.create_all()"
```
4. Restart the application:
```bash
python app.py
```
**Note**: Ensure the `instance` directory has write permissions (`chmod -R 777 instance`) to avoid permission errors when creating or modifying the database.
## Usage
### Importing FHIR Packages
1. Navigate to the "Import IGs" page (`/import-ig`).
2. Enter the package name (e.g., `hl7.fhir.us.core`) and version (e.g., `1.0.0` or `current`).
3. Click "Fetch & Download IG" to download the package and its dependencies.
4. Youll be redirected to the "Manage FHIR Packages" page to view the downloaded packages.
### Managing FHIR Packages
- **View Downloaded Packages**: The "Manage FHIR Packages" page (`/view-igs`) lists all downloaded packages.
- **Handle Duplicates**: Duplicate packages with different versions are highlighted with color-coded rows (e.g., yellow for one group, light blue for another).
- **Process Packages**: Click "Process" to extract and store package details in the database.
- **Delete Packages**: Click "Delete" to remove a package from the filesystem.
### Viewing Processed IGs
- Processed packages are listed in the "Processed Packages" section.
- Click "View" to see detailed information about a processed IG, including resource types and examples.
- Click "Unload" to remove a processed IG from the database.
## Project Structure
```
FLARE-FHIR-IG-Toolkit/
├── app.py # Main Flask application
├── instance/ # Directory for SQLite database and FHIR packages
│ ├── fhir_ig.db # SQLite database
│ └── fhir_packages/ # Directory for downloaded FHIR packages
├── static/ # Static files (e.g., favicon.ico, FHIRFLARE.png)
│ ├── FHIRFLARE.png
│ └── favicon.ico
├── templates/ # HTML templates
│ ├── base.html
│ ├── cp_downloaded_igs.html
│ ├── cp_view_processed_ig.html
│ ├── import_ig.html
│ └── index.html
├── services.py # Helper functions for processing FHIR packages
└── README.md # Project documentation
```
## Development Notes
### Background
The FHIRFLARE IG Toolkit was developed to address the need for a user-friendly tool to manage FHIR Implementation Guides. The project focuses on providing a seamless experience for importing, processing, and analyzing FHIR packages, with a particular emphasis on handling duplicate dependencies—a common challenge in FHIR development.
### Technical Decisions
- **Flask**: Chosen for its lightweight and flexible nature, making it ideal for a small to medium-sized web application.
- **SQLite**: Used as the database for simplicity and ease of setup. For production use, consider switching to a more robust database like PostgreSQL.
- **Bootstrap**: Integrated for a responsive and professional UI, with custom CSS to handle duplicate package highlighting.
- **Docker Support**: Added to simplify deployment and ensure consistency across development and production environments.
### Known Issues and Workarounds
- **Bootstrap CSS Conflicts**: Early versions of the application had issues with Bootstraps table background styles (`--bs-table-bg`) overriding custom row colors for duplicate packages. This was resolved by setting `--bs-table-bg` to `transparent` for the affected table (see `templates/cp_downloaded_igs.html`).
- **Database Permissions**: The `instance` directory must be writable by the application. If you encounter permission errors, ensure the directory has the correct permissions (`chmod -R 777 instance`).
- **Package Parsing**: Some FHIR package filenames may not follow the expected `name-version.tgz` format, leading to parsing issues. The application includes a fallback to treat such files as name-only packages, but this may need further refinement.
### Future Improvements
- **Sorting Versions**: Add sorting for package versions in the "Manage FHIR Packages" view to display them in a consistent order (e.g., ascending or descending).
- **Advanced Duplicate Handling**: Implement options to resolve duplicates (e.g., keep the latest version, merge resources).
- **Production Database**: Support for PostgreSQL or MySQL for better scalability in production environments.
- **Testing**: Add unit tests using `pytest` to cover core functionality, especially package processing and database operations.
- **Inbound API for IG Packages**: Develop API endpoints to allow external tools to push IG packages to FHIRFLARE. The API should automatically resolve dependencies, return a list of dependencies, and identify any duplicate dependencies. For example:
- Endpoint: `POST /api/import-ig`
- Request: `{ "package_name": "hl7.fhir.us.core", "version": "1.0.0" }`
- Response: `{ "status": "success", "dependencies": ["hl7.fhir.r4.core#4.0.1"], "duplicates": ["hl7.fhir.r4.core#4.0.1 (already exists as 5.0.0)"] }`
- **Outbound API for Pushing IGs to FHIR Servers**: Create an outbound API to push a chosen IG (with its dependencies) to a FHIR server, or allow pushing a single IG without dependencies. The API should process the servers responses and provide feedback. For example:
- Endpoint: `POST /api/push-ig`
- Request: `{ "package_name": "hl7.fhir.us.core", "version": "1.0.0", "fhir_server_url": "https://fhir-server.example.com", "include_dependencies": true }`
- Response: `{ "status": "success", "pushed_packages": ["hl7.fhir.us.core#1.0.0", "hl7.fhir.r4.core#4.0.1"], "server_response": "Resources uploaded successfully" }`
- **Far-Distant Improvements**:
- **Cache Service for IGs**: Implement a cache service to store all IGs, allowing for quick querying of package metadata without reprocessing. This could use an in-memory store like Redis to improve performance.
- **Database Index Optimization**: Modify the database structure to use a composite index on `package_name` and `version` (e.g., `ProcessedIg.package_name + ProcessedIg.version` as a unique key). This would allow the `/view-igs` page and API endpoints to directly query specific packages (e.g., `/api/ig/hl7.fhir.us.core/1.0.0`) without scanning the entire table.
## Contributing
Contributions are welcome! To contribute:
1. Fork the repository.
2. Create a new branch (`git checkout -b feature/your-feature`).
3. Make your changes and commit them (`git commit -m "Add your feature"`).
4. Push to your branch (`git push origin feature/your-feature`).
5. Open a Pull Request.
Please ensure your code follows the projects coding style and includes appropriate tests.
## Troubleshooting
- **Database Issues**: If the SQLite database (`instance/fhir_ig.db`) cannot be created, ensure the `instance` directory is writable. You may need to adjust permissions (`chmod -R 777 instance`).
- **Package Download Fails**: Verify your internet connection and ensure the package name and version are correct.
- **Colors Not Displaying**: If table row colors for duplicates are not showing, inspect the page with browser developer tools (F12) to check for CSS conflicts with Bootstrap.
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Contact
For questions or support, please open an issue on GitHub or contact the maintainers at [your-email@example.com](mailto:your-email@example.com).

21
app.py
View File

@ -116,22 +116,25 @@ def view_igs():
else:
logger.warning(f"Packages directory not found: {packages_dir}")
# Calculate duplicate_names
duplicate_names = {}
duplicate_groups = {}
for pkg in packages:
name = pkg['name']
if name in duplicate_names:
duplicate_names[name].append(pkg)
duplicate_groups.setdefault(name, []).append(pkg['version'])
else:
duplicate_names[name] = [pkg]
if name not in duplicate_names:
duplicate_names[name] = []
duplicate_names[name].append(pkg)
# Calculate duplicate_groups
duplicate_groups = {}
for name, pkgs in duplicate_names.items():
if len(pkgs) > 1: # Only include packages with multiple versions
duplicate_groups[name] = [pkg['version'] for pkg in pkgs]
# Precompute group colors
colors = ['bg-warning', 'bg-info', 'bg-success', 'bg-danger']
group_colors = {}
for i, name in enumerate(duplicate_groups):
if len(duplicate_groups[name]) > 1: # Only color duplicates
group_colors[name] = colors[i % len(colors)]
for i, name in enumerate(duplicate_groups.keys()):
group_colors[name] = colors[i % len(colors)]
return render_template('cp_downloaded_igs.html', packages=packages, processed_list=igs,
processed_ids=processed_ids, duplicate_names=duplicate_names,

Binary file not shown.

View File

@ -1,65 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<title>{% if title %}{{ title }} - {% endif %}{{ site_name }}</title>
</head>
<body class="d-flex flex-column min-vh-100">
<nav class="navbar navbar-expand-lg navbar-dark bg-primary mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}">{{ site_name }}</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'index' else '' }}" aria-current="page" href="{{ url_for('index') }}"><i class="bi bi-house-door me-1"></i> Home</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="bi bi-journal-arrow-down me-1"></i> Manage FHIR Packages</a>
</li>
<li class="nav-item">
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="bi bi-download me-1"></i> Import IGs</a>
</li>
</ul>
</div>
</div>
</nav>
<main class="container flex-grow-1">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category or 'info' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
{% set current_year = now.year %}
<span class="text-muted">© {{ current_year }} {{ site_name }}. All rights reserved.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) })
});
</script>
{% endblock %}
</body>
</html>

View File

@ -1,135 +0,0 @@
{# app/control_panel/templates/cp_downloaded_igs.html #}
{% extends "base.html" %}
{% block content %}
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
<div>
<a href="{{ url_for('fhir_ig_importer.import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
<a href="{{ url_for('control_panel.index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to CP Index</a>
</div>
</div>
{% if error_message %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">Error</h5>
{{ error_message }}
</div>
{% endif %}
{# NOTE: The block calculating processed_ids set using {% set %} was REMOVED from here #}
{# --- Start Two Column Layout --- #}
<div class="row g-4">
{# --- Left Column: Downloaded Packages (Horizontal Buttons) --- #}
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
<div class="card-body">
{% if packages %}
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<p class="mb-2"><small><span class="badge bg-danger text-dark border me-1">Risk:</span>= Duplicate Dependancy with different versions</small></p>
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
</thead>
<tbody>
{% for pkg in packages %}
{% set is_processed = (pkg.name, pkg.version) in processed_ids %}
{# --- ADDED: Check for duplicate name --- #}
{% set is_duplicate = pkg.name in duplicate_names %}
{# --- ADDED: Assign row class based on duplicate group --- #}
<tr class="{{ duplicate_groups.get(pkg.name, '') if is_duplicate else '' }}">
<td>
{# --- ADDED: Risk Badge for duplicates --- #}
{% if is_duplicate %}
<span class="badge bg-danger mb-1 d-block">Duplicate</span>
{% endif %}
{# --- End Add --- #}
<code>{{ pkg.name }}</code>
</td>
<td>{{ pkg.version }}</td>
<td> {# Actions #}
<div class="btn-group btn-group-sm" role="group">
{% if is_processed %}
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
{% else %}
<form action="{{ url_for('control_panel.process_ig') }}" method="POST" style="display: inline-block;">
<input type="hidden" name="package_name" value="{{ pkg.name }}">
<input type="hidden" name="package_version" value="{{ pkg.version }}">
<button type="submit" class="btn btn-outline-primary" title="Mark as processed"><i class="bi bi-gear"></i> Process</button>
</form>
{% endif %}
<form action="{{ url_for('control_panel.delete_ig_file') }}" method="POST" style="display: inline-block;" onsubmit="return confirm('Delete file \'{{ pkg.filename }}\'?');">
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-danger" title="Delete File"><i class="bi bi-trash"></i> Delete</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif not error_message %}<p class="text-muted">No downloaded FHIR packages found.</p>{% endif %}
</div>
</div>
</div>{# --- End Left Column --- #}
{# --- Right Column: Processed Packages (Vertical Buttons) --- #}
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
<div class="card-body">
{% 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>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr><th>Package Name</th><th>Version</th><th>Resource Types</th><th>Actions</th></tr>
</thead>
<tbody>
{% for processed_ig in processed_list %}
<tr>
<td>{# Tooltip for Processed At / Status #}
{% set tooltip_title_parts = [] %}
{% if processed_ig.processed_at %}{% set _ = tooltip_title_parts.append("Processed: " + processed_ig.processed_at.strftime('%Y-%m-%d %H:%M')) %}{% endif %}
{% if processed_ig.status %}{% set _ = tooltip_title_parts.append("Status: " + processed_ig.status) %}{% endif %}
{% set tooltip_text = tooltip_title_parts | join('\n') %}
<code data-bs-toggle="tooltip" data-bs-placement="top" title="{{ tooltip_text }}">{{ processed_ig.package_name }}</code>
</td>
<td>{{ processed_ig.package_version }}</td>
<td> {# Resource Types Cell w/ Badges #}
{% set types_info = processed_ig.resource_types_info %}
{% if types_info %}<div class="d-flex flex-wrap gap-1">{% for type_info in types_info %}{% if type_info.must_support %}<span class="badge bg-warning text-dark border" title="Has Must Support">{{ type_info.name }}</span>{% else %}<span class="badge bg-light text-dark border">{{ type_info.name }}</span>{% endif %}{% endfor %}</div>{% else %}<small class="text-muted">N/A</small>{% endif %}
</td>
<td>
{# Vertical Button Group #}
<div class="btn-group-vertical btn-group-sm w-100" role="group" aria-label="Processed Package Actions for {{ processed_ig.package_name }}">
<a href="{{ url_for('control_panel.view_processed_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
<form action="{{ url_for('control_panel.unload_ig') }}" method="POST" onsubmit="return confirm('Unload record for \'{{ processed_ig.package_name }}#{{ processed_ig.package_version }}\'?');">
<input type="hidden" name="processed_ig_id" value="{{ processed_ig.id }}">
<button type="submit" class="btn btn-outline-warning w-100" title="Unload/Remove Processed Record"><i class="bi bi-x-lg"></i> Unload</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif not error_message %}<p class="text-muted">No packages recorded as processed yet.</p>{% endif %}
</div>
</div>
</div>{# --- End Right Column --- #}
</div>{# --- End Row --- #}
</div>{# End container #}
{% endblock %}
{# Tooltip JS Initializer should be in base.html #}
{% block scripts %}{{ super() }}{% endblock %}

View File

@ -3,7 +3,7 @@
{% 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">Welcome to {{ site_name }}</h1>
<h1 class="display-5 fw-bold text-body-emphasis">Manage & Process FHIR Packages</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
This is the starting Point for your Journey through the IG's
@ -16,6 +16,39 @@
</div>
<div class="container mt-4">
<!-- Add custom CSS for badge and table row colors -->
<style>
.badge.bg-warning {
background-color: #ffc107 !important;
}
.badge.bg-info {
background-color: #0dcaf0 !important;
}
.badge.bg-secondary {
background-color: #6c757d !important;
}
.badge.bg-danger {
background-color: #dc3545 !important;
}
/* Ensure table rows with custom background colors are not overridden by Bootstrap */
.table tr.bg-warning {
background-color: #ffc107 !important;
}
.table tr.bg-info {
background-color: #0dcaf0 !important;
}
.table tr.bg-secondary {
background-color: #6c757d !important;
}
.table tr.bg-danger {
background-color: #dc3545 !important;
}
/* Override Bootstrap's table background to allow custom row colors to show */
.table-custom-bg {
--bs-table-bg: transparent !important;
}
</style>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
<div>
@ -30,8 +63,10 @@
<div class="card-body">
{% if packages %}
<div class="table-responsive">
<!-- remove this warning and move it down the bottom-----------------------------------------------------------------------------------------
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> Duplicate Dependency with Different Versions</small></p>
<table class="table table-sm table-hover">
--------------------------------------------------------------------------------------------------------------------------------------------->
<table class="table table-sm table-hover table-custom-bg">
<thead>
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
</thead>
@ -39,7 +74,7 @@
{% for pkg in packages %}
{% 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 group_color = group_colors[pkg.name] if is_duplicate else 'bg-light' %}
{% set group_color = group_colors[pkg.name] if (is_duplicate and pkg.name in group_colors) else 'bg-warning' if is_duplicate else 'bg-light' %}
<tr class="{% if is_duplicate %}{{ group_color }} text-dark{% endif %}">
<td>
<code>{{ pkg.name }}</code>
@ -70,12 +105,11 @@
</table>
</div>
{% if duplicate_groups %}
<p class="mt-2 small text-muted">Duplicates detected:
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> = Duplicate Dependencies</small></p>
<p class="mt-2 small text-muted">Duplicate dependencies detected:
{% for name, versions in duplicate_groups.items() %}
{% if versions|length > 1 %}
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
<span class="badge {{ group_color }} text-dark">{{ name }} ({{ versions|join(', ') }})</span>
{% endif %}
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
<span class="badge {{ group_color }} text-dark">{{ name }} ({{ versions|join(', ') }})</span>
{% endfor %}
</p>
{% else %}
@ -94,6 +128,7 @@
<div class="card-body">
{% 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 text-muted">Resource Types in the list will be both Profile and Base Type:
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>

View File

@ -1,145 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="px-4 py-5 my-5 text-center">
<img class="d-block mx-auto mb-4" src="https://placehold.co/72x57/0d6efd/white?text=PAS" alt="PAS Logo Placeholder" width="72" height="57">
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
<div class="col-lg-6 mx-auto">
<p class="lead mb-4">
This is the starting point for our modular Patient Administration System.
We will build upon this core framework by adding modules through the control panel.
</p>
</div>
</div>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
<div>
<a href="{{ url_for('import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
<a href="{{ url_for('index') }}" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Home</a>
</div>
</div>
<div class="row g-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
<div class="card-body">
{% if packages %}
<div class="table-responsive">
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> Duplicate Dependency with Different Versions</small></p>
<table class="table table-sm table-hover">
<thead>
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
</thead>
<tbody>
{% for pkg in packages %}
{% 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 group_color = group_colors[pkg.name] if is_duplicate else 'bg-light' %}
<tr class="{% if is_duplicate %}{{ group_color }} text-dark{% endif %}">
<td>
<code>{{ pkg.name }}</code>
{% if is_duplicate %}
<span class="badge bg-danger text-light ms-1">Duplicate</span>
{% endif %}
</td>
<td>{{ pkg.version }}</td>
<td>
<div class="btn-group btn-group-sm">
{% if is_processed %}
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
{% else %}
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
<input type="hidden" name="filename" value="{{ pkg.filename }}">
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if duplicate_groups %}
<p class="mt-2 small text-muted">Duplicates detected:
{% for name, versions in duplicate_groups.items() %}
{% if versions|length > 1 %}
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
<span class="badge {{ group_color }} text-dark">{{ name }} ({{ versions|join(', ') }})</span>
{% endif %}
{% endfor %}
</p>
{% else %}
<p class="mt-2 small text-muted">No duplicates detected.</p>
{% endif %}
{% else %}
<p class="text-muted">No downloaded FHIR packages found.</p>
{% endif %}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
<div class="card-body">
{% 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>
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr><th>Package Name</th><th>Version</th><th>Resource Types</th><th>Actions</th></tr>
</thead>
<tbody>
{% for processed_ig in processed_list %}
<tr>
<td>
<code>{{ processed_ig.package_name }}</code>
</td>
<td>{{ processed_ig.version }}</td>
<td>
{% set types_info = processed_ig.resource_types_info %}
{% if types_info %}<div class="d-flex flex-wrap gap-1">
{% for type_info in types_info %}
{% if type_info.must_support %}
<span class="badge bg-warning text-dark border" title="Has Must Support">{{ type_info.name }}</span>
{% else %}
<span class="badge bg-light text-dark border">{{ type_info.name }}</span>
{% endif %}
{% endfor %}
</div>{% else %}<small class="text-muted">N/A</small>{% endif %}
</td>
<td>
<div class="btn-group-vertical btn-group-sm w-100">
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No packages recorded as processed yet.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{% endblock %}

View File

@ -1,405 +0,0 @@
{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<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('control_panel.list_downloaded_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.package_version }}</dd>
<dt class="col-sm-3">Processed At</dt>
<dd class="col-sm-9">{{ processed_ig.processed_at.strftime('%Y-%m-%d %H:%M:%S UTC') if processed_ig.processed_at else 'N/A' }}</dd>
<dt class="col-sm-3">Processing Status</dt>
<dd class="col-sm-9">
<span class="badge rounded-pill text-bg-{{ 'success' if processed_ig.status == 'processed' else ('warning' if processed_ig.status == 'processed_with_errors' else 'secondary') }}">{{ processed_ig.status or 'N/A' }}</span>
</dd>
</dl>
</div>
</div>
<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">MS</span> = Contains Must Support Elements</small></p>
{% if profile_list %}
<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.package_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>
{% endif %}
{% if base_list %}
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = - Examples will be displayed when selecting Base Types if contained in the IG</small></p>
<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.package_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>
{% 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 Details View" 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-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>
{% block scripts %}
{{ super() }}
<script>const examplesData = {{ examples_by_type | tojson | safe }};</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
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 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 = "/control-panel/fhir/get-structure";
const exampleBaseUrl = "/control-panel/fhir/get-example";
let currentPkgName = null;
let currentPkgVersion = null;
let currentRawStructureData = null;
document.body.addEventListener('click', function(event) {
const link = event.target.closest('.resource-type-link');
if (!link) return;
event.preventDefault();
currentPkgName = link.dataset.packageName;
currentPkgVersion = link.dataset.packageVersion;
const resourceType = link.dataset.resourceType;
if (!currentPkgName || !currentPkgVersion || !resourceType) return;
const structureParams = new URLSearchParams({
package_name: currentPkgName,
package_version: currentPkgVersion,
resource_type: resourceType
});
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
structureTitle.textContent = `${resourceType} (${currentPkgName}#${currentPkgVersion})`;
rawStructureTitle.textContent = `${resourceType} (${currentPkgName}#${currentPkgVersion})`;
structureDisplay.innerHTML = '';
rawStructureContent.textContent = '';
structureLoading.style.display = 'block';
rawStructureLoading.style.display = 'block';
structureDisplayWrapper.style.display = 'block';
rawStructureWrapper.style.display = 'block';
exampleDisplayWrapper.style.display = 'none';
fetch(structureFetchUrl)
.then(response => 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}`);
currentRawStructureData = result.data;
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
populateExampleSelector(resourceType);
})
.catch(error => {
console.error('Error fetching structure:', error);
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
rawStructureContent.textContent = `Error: ${error.message}`;
})
.finally(() => {
structureLoading.style.display = 'none';
rawStructureLoading.style.display = 'none';
});
});
function populateExampleSelector(resourceOrProfileIdentifier) {
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
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;
exampleContentRaw.textContent = '';
exampleContentJson.textContent = '';
exampleFilename.textContent = '';
exampleContentWrapper.style.display = 'none';
if (!selectedFilePath || !currentPkgName || !currentPkgVersion) return;
const exampleParams = new URLSearchParams({
package_name: currentPkgName,
package_version: currentPkgVersion,
filename: selectedFilePath
});
const exampleFetchUrl = `${exampleBaseUrl}?${exampleParams.toString()}`;
console.log("Fetching example from:", exampleFetchUrl);
exampleLoading.style.display = 'block';
fetch(exampleFetchUrl)
.then(response => {
if (!response.ok) {
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
}
return response.text();
})
.then(content => {
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
exampleContentRaw.textContent = content;
try {
const jsonContent = JSON.parse(content);
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
} catch (e) {
exampleContentJson.textContent = 'Not valid JSON';
}
exampleContentWrapper.style.display = 'block';
})
.catch(error => {
exampleFilename.textContent = 'Error';
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};
elements.forEach(el => {
const path = el.path;
if (!path) return;
const parts = path.split('.');
let currentPath = '';
let parentNode = treeRoot;
for(let i = 0; i < parts.length; i++) {
const part = parts[i];
const currentPartName = part.includes('[') ? part.substring(0, part.indexOf('[')) : part;
currentPath = i === 0 ? currentPartName : `${currentPath}.${part}`;
if (!nodeMap[currentPath]) {
const newNode = { children: {}, element: null, name: part, path: currentPath };
parentNode.children[part] = newNode;
nodeMap[currentPath] = newNode;
}
if (i === parts.length - 1) {
nodeMap[currentPath].element = el;
}
parentNode = nodeMap[currentPath];
}
});
return Object.values(treeRoot.children);
}
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
if (!node || !node.element) return '';
const el = node.element;
const path = el.path || 'N/A';
const min = el.min !== undefined ? el.min : '';
const max = el.max || '';
const short = el.short || '';
const definition = el.definition || '';
const isMustSupport = mustSupportPathsSet.has(path);
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></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 || '';
if (t.targetProfile && t.targetProfile.length > 0) {
const targetTypes = t.targetProfile.map(p => p.split('/').pop());
s += `(<span class="text-muted">${targetTypes.join('|')}</span>)`;
}
return s;
}).join(' | ');
}
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
let childrenHtml = '';
if (hasChildren) {
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
Object.values(node.children).sort((a,b) => a.name.localeCompare(b.name)).forEach(childNode => {
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, 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" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><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><code class="fw-bold ms-1">${node.name}</code></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, '&quot;').replace(/'/g, '&apos;');
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}</div>`;
itemHtml += `</div>`;
itemHtml += childrenHtml;
itemHtml += `</li>`;
return itemHtml;
}
function renderStructureTree(elements, mustSupportPaths) {
if (!elements || elements.length === 0) {
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found.</em></p>';
return;
}
const mustSupportPathsSet = new Set(mustSupportPaths || []);
console.log("Rendering tree. MS Set:", mustSupportPathsSet);
const treeData = buildTreeData(elements);
let html = '<p><small><span class="badge bg-warning text-dark border me-1">MS</span> = Must Support (Row Highlighted)</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">MS</div><div class="col-lg-3 col-md-4">Description</div></div>`;
html += '<ul class="list-group list-group-flush">';
treeData.sort((a,b) => a.name.localeCompare(b.name)).forEach(rootNode => {
html += renderNodeAsLi(rootNode, mustSupportPathsSet, 0);
});
html += '</ul>';
structureDisplay.innerHTML = html;
var tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
collapseEl.addEventListener('show.bs.collapse', event => {
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
});
collapseEl.addEventListener('hide.bs.collapse', event => {
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
});
});
}
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) });
});
</script>
{% endblock scripts %}
{% endblock content %} {# Main content block END #}

View File

@ -64,6 +64,7 @@ center">
<p class="text-muted"><em>No profiles defined.</em></p>
{% endif %}
{% if base_list %}
<p class="mb-2"><small><span class="badge bg-primary text-light border me-1">Examples</span> = - Examples will be displayed when selecting Base Types if contained in the IG</small></p>
<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 %}
@ -147,7 +148,7 @@ center">
<div class="alert alert-warning" role="alert">Processed IG details not available.</div>
{% endif %}
<!-- Debugging: Display raw data for verification -->
<!-- Debugging: Display raw data for verification ------------------------------------------------------------------------------
<div class="mt-4">
<details>
<summary class="text-muted small">Debug: Raw Data (Click to Expand)</summary>
@ -156,7 +157,7 @@ center">
</details>
</div>
</div>
-------------------------------------------------------------------------------------------------------------DEBUG end -->
<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>

View File

@ -1,385 +0,0 @@
{% extends "base.html" %}
{% from "_form_helpers.html" import render_field %}
{% block content %}
<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>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
</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>
<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">MS</span> = Contains Must Support Elements</small></p>
{% if profile_list %}
<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 %}
<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>
{% 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>
{% 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-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() {
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 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.body.addEventListener('click', function(event) {
const link = event.target.closest('.resource-type-link');
if (!link) return;
event.preventDefault();
const pkgName = link.dataset.packageName;
const pkgVersion = link.dataset.packageVersion;
const resourceType = link.dataset.resourceType;
if (!pkgName || !pkgVersion || !resourceType) return;
const structureParams = new URLSearchParams({ package_name: pkgName, package_version: pkgVersion, resource_type: resourceType });
const structureFetchUrl = `${structureBaseUrl}?${structureParams.toString()}`;
structureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
rawStructureTitle.textContent = `${resourceType} (${pkgName}#${pkgVersion})`;
structureDisplay.innerHTML = '';
rawStructureContent.textContent = '';
structureLoading.style.display = 'block';
rawStructureLoading.style.display = 'block';
structureDisplayWrapper.style.display = 'block';
rawStructureWrapper.style.display = 'block';
exampleDisplayWrapper.style.display = 'none';
fetch(structureFetchUrl)
.then(response => 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}`);
renderStructureTree(result.data.elements, result.data.must_support_paths || []);
rawStructureContent.textContent = JSON.stringify(result.data, null, 2);
populateExampleSelector(resourceType);
})
.catch(error => {
structureDisplay.innerHTML = `<div class="alert alert-danger">Error: ${error.message}</div>`;
rawStructureContent.textContent = `Error: ${error.message}`;
})
.finally(() => {
structureLoading.style.display = 'none';
rawStructureLoading.style.display = 'none';
});
});
function populateExampleSelector(resourceOrProfileIdentifier) {
exampleResourceTypeTitle.textContent = resourceOrProfileIdentifier;
const availableExamples = examplesData[resourceOrProfileIdentifier] || [];
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;
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()}`;
exampleLoading.style.display = 'block';
fetch(exampleFetchUrl)
.then(response => {
if (!response.ok) {
return response.text().then(errText => { throw new Error(errText || `HTTP error ${response.status}`); });
}
return response.text();
})
.then(content => {
exampleFilename.textContent = `Source: ${selectedFilePath.split('/').pop()}`;
exampleContentRaw.textContent = content;
try {
const jsonContent = JSON.parse(content);
exampleContentJson.textContent = JSON.stringify(jsonContent, null, 2);
} catch (e) {
exampleContentJson.textContent = 'Not valid JSON';
}
exampleContentWrapper.style.display = 'block';
})
.catch(error => {
exampleFilename.textContent = 'Error';
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};
elements.forEach(el => {
const path = el.path;
if (!path) return;
const parts = path.split('.');
let currentPath = '';
let parentNode = treeRoot;
for(let i = 0; i < parts.length; i++) {
const part = parts[i];
const currentPartName = part.includes('[') ? part.substring(0, part.indexOf('[')) : part;
currentPath = i === 0 ? currentPartName : `${currentPath}.${part}`;
if (!nodeMap[currentPath]) {
const newNode = { children: {}, element: null, name: part, path: currentPath };
parentNode.children[part] = newNode;
nodeMap[currentPath] = newNode;
}
if (i === parts.length - 1) {
nodeMap[currentPath].element = el;
}
parentNode = nodeMap[currentPath];
}
});
return Object.values(treeRoot.children);
}
function renderNodeAsLi(node, mustSupportPathsSet, level = 0) {
if (!node || !node.element) return '';
const el = node.element;
const path = el.path || 'N/A';
const min = el.min !== undefined ? el.min : '';
const max = el.max || '';
const short = el.short || '';
const definition = el.definition || '';
const isMustSupport = mustSupportPathsSet.has(path);
const mustSupportDisplay = isMustSupport ? '<i class="bi bi-check-circle-fill text-warning"></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 || '';
if (t.targetProfile && t.targetProfile.length > 0) {
const targetTypes = t.targetProfile.map(p => p.split('/').pop());
s += `(<span class="text-muted">${targetTypes.join('|')}</span>)`;
}
return s;
}).join(' | ');
}
const liClass = isMustSupport ? 'list-group-item py-1 px-2 list-group-item-warning' : 'list-group-item py-1 px-2';
let childrenHtml = '';
if (hasChildren) {
childrenHtml += `<ul class="collapse list-group list-group-flush" id="${collapseId}">`;
Object.values(node.children).sort((a,b) => a.name.localeCompare(b.name)).forEach(childNode => {
childrenHtml += renderNodeAsLi(childNode, mustSupportPathsSet, 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" href="#${collapseId}" role="button" aria-expanded="false" aria-controls="${collapseId}"` : ''}>`;
itemHtml += `<div class="col-lg-4 col-md-3 text-truncate" style="${pathStyle}"><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><code class="fw-bold ms-1">${node.name}</code></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, '&quot;').replace(/'/g, '&apos;');
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}</div>`;
itemHtml += `</div>`;
itemHtml += childrenHtml;
itemHtml += `</li>`;
return itemHtml;
}
function renderStructureTree(elements, mustSupportPaths) {
if (!elements || elements.length === 0) {
structureDisplay.innerHTML = '<p class="text-muted"><em>No elements found.</em></p>';
return;
}
const mustSupportPathsSet = new Set(mustSupportPaths || []);
let html = '<p><small><span class="badge bg-warning text-dark border me-1">MS</span> = Must Support (Row Highlighted)</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">MS</div><div class="col-lg-3 col-md-4">Description</div></div>`;
html += '<ul class="list-group list-group-flush">';
buildTreeData(elements).sort((a,b) => a.name.localeCompare(b.name)).forEach(rootNode => {
html += renderNodeAsLi(rootNode, mustSupportPathsSet, 0);
});
html += '</ul>';
structureDisplay.innerHTML = html;
var tooltipTriggerList = [].slice.call(structureDisplay.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
structureDisplay.querySelectorAll('.collapse').forEach(collapseEl => {
collapseEl.addEventListener('show.bs.collapse', event => {
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-right', 'bi-chevron-down');
});
collapseEl.addEventListener('hide.bs.collapse', event => {
event.target.previousElementSibling.querySelector('.toggle-icon')?.classList.replace('bi-chevron-down', 'bi-chevron-right');
});
});
}
});
</script>
</body>
{% endblock content %} {# Main content block END #}

View File

@ -1,124 +0,0 @@
{# app/modules/fhir_ig_importer/templates/fhir_ig_importer/import_ig_page.html #}
{% extends "base.html" %} {# Or your control panel base template "cp_base.html" #}
{% from "_form_helpers.html" import render_field %} {# Assumes you have this macro #}
{% block content %} {# Or your specific CP content block #}
<div class="container mt-4">
<h2 class="mb-4"><i class="bi bi-bootstrap-reboot me-2"></i>Import FHIR Implementation Guide</h2>
<div class="row">
<div class="col-md-8">
{# --- Import Form --- #}
<div class="card mb-4">
<div class="card-body">
<h5 class="card-title">Enter Package Details</h5>
<form method="POST" action="{{ url_for('.import_ig') }}" novalidate> {# Use relative endpoint for blueprint #}
{{ form.hidden_tag() }} {# CSRF token #}
<div class="mb-3">
{{ render_field(form.package_name, class="form-control" + (" is-invalid" if form.package_name.errors else ""), placeholder="e.g., hl7.fhir.au.base") }}
</div>
<div class="mb-3">
{{ render_field(form.package_version, class="form-control" + (" is-invalid" if form.package_version.errors else ""), placeholder="e.g., 4.1.0 or latest") }}
</div>
<div class="mb-3">
{{ form.submit(class="btn btn-primary", value="Fetch & Download IG") }}
</div>
</form>
</div>
</div>
{# --- Results Section --- #}
<div id="results">
{# Display Fatal Error if passed #}
{% if fatal_error %}
<div class="alert alert-danger" role="alert">
<h5 class="alert-heading">Critical Error during Import</h5>
{{ fatal_error }}
</div>
{% endif %}
{# Display results if the results dictionary exists (meaning POST happened) #}
{% if results %}
<div class="card">
<div class="card-body">
{# Use results.requested for package info #}
<h5 class="card-title">Import Results for: <code>{{ results.requested[0] }}#{{ results.requested[1] }}</code></h5>
<hr>
{# --- Process Errors --- #}
{% if results.errors %}
<h6 class="mt-4 text-danger">Errors Encountered ({{ results.errors|length }}):</h6>
<ul class="list-group list-group-flush mb-3">
{% for error in results.errors %}
<li class="list-group-item list-group-item-danger py-1">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{# --- Downloaded Files --- #}
<h6 class="mt-4">Downloaded Packages ({{ results.downloaded|length }} / {{ results.processed|length }} processed):</h6>
{# Check results.downloaded dictionary #}
{% if results.downloaded %}
<ul class="list-group list-group-flush mb-3">
{# Iterate through results.downloaded items #}
{% for (name, version), path in results.downloaded.items()|sort %}
<li class="list-group-item py-1 d-flex justify-content-between align-items-center">
<code>{{ name }}#{{ version }}</code>
{# Display relative path cleanly #}
<small class="text-muted ms-2">{{ path | replace('/app/', '') | replace('\\', '/') }}</small>
<span class="badge bg-success rounded-pill ms-auto">Downloaded</span> {# Moved badge to end #}
</li>
{% endfor %}
</ul>
<p><small>Files saved in server's `instance/fhir_packages` directory.</small></p>
{% else %}
<p><small>No packages were successfully downloaded.</small></p>
{% endif %}
{# --- All Dependencies Found --- #}
<h6 class="mt-4">Consolidated Dependencies Found:</h6>
{# Calculate unique_deps based on results.all_dependencies #}
{% set unique_deps = {} %}
{% for pkg_id, deps_dict in results.all_dependencies.items() %}
{% for dep_name, dep_version in deps_dict.items() %}
{% set _ = unique_deps.update({(dep_name, dep_version): true}) %}
{% endfor %}
{% endfor %}
{% if unique_deps %}
<p><small>Unique direct dependencies found across all processed packages:</small></p>
<dl class="row">
{% for (name, version), _ in unique_deps.items()|sort %}
<dt class="col-sm-4 text-muted"><code>{{ name }}</code></dt>
<dd class="col-sm-8">{{ version }}</dd>
{% endfor %}
</dl>
{% else %}
<div class="alert alert-secondary" role="alert" style="padding: 0.5rem 1rem;">
No explicit dependencies found in any processed package metadata.
</div>
{% endif %}
{# --- End Dependency Result --- #}
</div> {# End card-body #}
</div> {# End card #}
{% endif %} {# End if results #}
</div> {# End results #}
{# --- End Results Section --- #}
</div>
<div class="col-md-4">
{# --- Instructions Panel --- #}
<div class="card bg-light">
<div class="card-body">
<h5 class="card-title">Instructions</h5>
<p class="card-text">Enter the official FHIR package name (e.g., <code>hl7.fhir.au.base</code>) and the desired version (e.g., <code>4.1.0</code>, <code>latest</code>).</p>
<p class="card-text">The system will download the package and attempt to list its direct dependencies.</p>
<p class="card-text"><small>Downloads are saved to the server's `instance/fhir_packages` folder.</small></p>
</div>
</div>
</div>
</div> {# End row #}
</div> {# End container #}
{% endblock %}