mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 13:09:59 +00:00
V1 package
This commit is contained in:
parent
753cc4e6c1
commit
92bcc185e1
@ -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
|
162
Dump/app.txt
162
Dump/app.txt
@ -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)
|
@ -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">© {{ 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>
|
@ -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 %}
|
@ -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, '"').replace(/'/g, ''');
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short}</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 #}
|
@ -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')
|
@ -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 %}
|
162
Dump/routes.txt
162
Dump/routes.txt
@ -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
|
@ -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
216
README.md
Normal 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 don’t 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 it’s 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 it’s 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. You’ll 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 Bootstrap’s 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 server’s 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 project’s 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
21
app.py
@ -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.
@ -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>
|
@ -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 %}
|
@ -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>
|
||||
|
@ -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 %}
|
@ -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, '"').replace(/'/g, ''');
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short}</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 #}
|
@ -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>
|
||||
|
@ -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, '"').replace(/'/g, ''');
|
||||
descriptionTooltipAttrs = `data-bs-toggle="tooltip" data-bs-placement="top" title="${escapedDefinition}"`;
|
||||
}
|
||||
itemHtml += `<div class="col-lg-3 col-md-4 text-muted text-truncate small" ${descriptionTooltipAttrs}>${short}</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 #}
|
@ -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 %}
|
Loading…
x
Reference in New Issue
Block a user