From 92bcc185e14e423e61b16d2b5fdbf9c32278cbd0 Mon Sep 17 00:00:00 2001 From: Sudo-JHare Date: Thu, 10 Apr 2025 21:43:36 +1000 Subject: [PATCH] V1 package --- Dump/__init__.txt | 28 -- Dump/app.txt | 162 ------- Dump/base.html.txt | 91 ---- Dump/cp_downloaded_igs.html.txt | 135 ------ Dump/cp_view_processed_ig.html.txt | 405 ------------------ Dump/forms.txt | 19 - Dump/index.html.txt | 16 - Dump/routes.txt | 162 ------- Dump/services.txt | 345 --------------- README.md | 216 ++++++++++ app.py | 21 +- instance/fhir_ig.db | Bin 180224 -> 180224 bytes templates/base.html.txt | 65 --- templates/cp_downloaded_igs - Copy.html | 135 ------ templates/cp_downloaded_igs.html | 51 ++- templates/cp_downloaded_igs.html.txt | 145 ------- templates/cp_view_processed_ig - Copy.html | 405 ------------------ templates/cp_view_processed_ig.html | 5 +- templates/cp_view_processed_ig.html.txt | 385 ----------------- .../fhir_ig_importer/import_ig_page.html | 124 ------ 20 files changed, 274 insertions(+), 2641 deletions(-) delete mode 100644 Dump/__init__.txt delete mode 100644 Dump/app.txt delete mode 100644 Dump/base.html.txt delete mode 100644 Dump/cp_downloaded_igs.html.txt delete mode 100644 Dump/cp_view_processed_ig.html.txt delete mode 100644 Dump/forms.txt delete mode 100644 Dump/index.html.txt delete mode 100644 Dump/routes.txt delete mode 100644 Dump/services.txt create mode 100644 README.md delete mode 100644 templates/base.html.txt delete mode 100644 templates/cp_downloaded_igs - Copy.html delete mode 100644 templates/cp_downloaded_igs.html.txt delete mode 100644 templates/cp_view_processed_ig - Copy.html delete mode 100644 templates/cp_view_processed_ig.html.txt delete mode 100644 templates/fhir_ig_importer/import_ig_page.html diff --git a/Dump/__init__.txt b/Dump/__init__.txt deleted file mode 100644 index 8f67ebc..0000000 --- a/Dump/__init__.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Dump/app.txt b/Dump/app.txt deleted file mode 100644 index cfe370f..0000000 --- a/Dump/app.txt +++ /dev/null @@ -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(''' - - - - - FHIR IG Toolkit - - - -

FHIR IG Toolkit

-

Simple tool for importing and viewing FHIR Implementation Guides.

- - - - - ''') - -# 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(''' - - - - - Import FHIR IG - - - -

Import FHIR IG

-
-
- - -
-
- - -
- - -
- {% with messages = get_flashed_messages(with_categories=True) %} - {% if messages %} - {% for category, message in messages %} -

{{ message }}

- {% endfor %} - {% endif %} - {% endwith %} - - - ''') - -# View Downloaded IGs route -@app.route('/view-igs') -def view_igs(): - igs = ProcessedIg.query.all() - return render_template_string(''' - - - - - View Downloaded IGs - - - -

Downloaded FHIR IGs

- - - - - - - - {% for ig in igs %} - - - - - - - {% endfor %} -
Package NameVersionProcessed DateResource Types
{{ ig.package_name }}{{ ig.version }}{{ ig.processed_date }}{{ ig.resource_types_info | length }} types
- - - - ''', igs=igs) - -# Initialize DB -with app.app_context(): - db.create_all() - -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file diff --git a/Dump/base.html.txt b/Dump/base.html.txt deleted file mode 100644 index cdad0d0..0000000 --- a/Dump/base.html.txt +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - {% if title %}{{ title }} - {% endif %}{{ site_name }} - - - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} - {% block content %}{% endblock %} -
- - - - - - - {% block scripts %} - {# Tooltip Initialization Script #} - - {% endblock %} - - \ No newline at end of file diff --git a/Dump/cp_downloaded_igs.html.txt b/Dump/cp_downloaded_igs.html.txt deleted file mode 100644 index 49f8207..0000000 --- a/Dump/cp_downloaded_igs.html.txt +++ /dev/null @@ -1,135 +0,0 @@ -{# app/control_panel/templates/cp_downloaded_igs.html #} -{% extends "base.html" %} - -{% block content %} -
-
-

Manage FHIR Packages

-
- Import More IGs - Back to CP Index -
-
- - {% if error_message %} - - {% endif %} - - {# NOTE: The block calculating processed_ids set using {% set %} was REMOVED from here #} - - {# --- Start Two Column Layout --- #} -
- - {# --- Left Column: Downloaded Packages (Horizontal Buttons) --- #} -
-
-
Downloaded Packages ({{ packages|length }})
-
- {% if packages %} -
- - -

Risk:= Duplicate Dependancy with different versions

- - - - {% 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 --- #} - - - - - - {% endfor %} - -
Package NameVersionActions
- {# --- ADDED: Risk Badge for duplicates --- #} - {% if is_duplicate %} - Duplicate - {% endif %} - {# --- End Add --- #} - {{ pkg.name }} - {{ pkg.version }} {# Actions #} -
- {% if is_processed %} - Processed - {% else %} -
- - - -
- {% endif %} -
- - -
-
-
-
- {% elif not error_message %}

No downloaded FHIR packages found.

{% endif %} -
-
-
{# --- End Left Column --- #} - - - {# --- Right Column: Processed Packages (Vertical Buttons) --- #} -
-
-
Processed Packages ({{ processed_list|length }})
-
- {% if processed_list %} -

MS = Contains Must Support Elements

-
- - - - - - {% for processed_ig in processed_list %} - - - - - - - {% endfor %} - -
Package NameVersionResource TypesActions
{# 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') %} - {{ processed_ig.package_name }} - {{ processed_ig.package_version }} {# Resource Types Cell w/ Badges #} - {% set types_info = processed_ig.resource_types_info %} - {% if types_info %}
{% for type_info in types_info %}{% if type_info.must_support %}{{ type_info.name }}{% else %}{{ type_info.name }}{% endif %}{% endfor %}
{% else %}N/A{% endif %} -
- {# Vertical Button Group #} -
- View -
- - -
-
-
-
- {% elif not error_message %}

No packages recorded as processed yet.

{% endif %} -
-
-
{# --- End Right Column --- #} - -
{# --- End Row --- #} - -
{# End container #} -{% endblock %} - -{# Tooltip JS Initializer should be in base.html #} -{% block scripts %}{{ super() }}{% endblock %} \ No newline at end of file diff --git a/Dump/cp_view_processed_ig.html.txt b/Dump/cp_view_processed_ig.html.txt deleted file mode 100644 index e8cf5f9..0000000 --- a/Dump/cp_view_processed_ig.html.txt +++ /dev/null @@ -1,405 +0,0 @@ -{% extends "base.html" %} - -{% from "_form_helpers.html" import render_field %} - -{% block content %} -
-
-

{{ title }}

- Back to Package List -
- - {% if processed_ig %} -
-
Package Details
-
-
-
Package Name
-
{{ processed_ig.package_name }}
-
Package Version
-
{{ processed_ig.package_version }}
-
Processed At
-
{{ processed_ig.processed_at.strftime('%Y-%m-%d %H:%M:%S UTC') if processed_ig.processed_at else 'N/A' }}
-
Processing Status
-
- {{ processed_ig.status or 'N/A' }} -
-
-
-
- -
-
Resource Types Found / Defined
-
- {% if profile_list or base_list %} -

MS = Contains Must Support Elements

- {% if profile_list %} -
Profiles Defined ({{ profile_list|length }}):
- - {% endif %} - {% if base_list %} -

Examples = - Examples will be displayed when selecting Base Types if contained in the IG

-
Base Resource Types Referenced ({{ base_list|length }}):
- - {% endif %} - {% else %} -

No resource type information extracted or stored.

- {% endif %} -
-
- - - - - - - - {% else %} - - {% endif %} -
- -{% block scripts %} -{{ super() }} - - -{% endblock scripts %} -{% endblock content %} {# Main content block END #} \ No newline at end of file diff --git a/Dump/forms.txt b/Dump/forms.txt deleted file mode 100644 index b6a97d9..0000000 --- a/Dump/forms.txt +++ /dev/null @@ -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') \ No newline at end of file diff --git a/Dump/index.html.txt b/Dump/index.html.txt deleted file mode 100644 index 0cbeb42..0000000 --- a/Dump/index.html.txt +++ /dev/null @@ -1,16 +0,0 @@ -{% extends "base.html" %} {% block content %}
- PAS Logo Placeholder - -

Welcome to {{ site_name }}

-
-

- 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. -

-
- - -
-
-
-{% endblock %} diff --git a/Dump/routes.txt b/Dump/routes.txt deleted file mode 100644 index 3404de2..0000000 --- a/Dump/routes.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Dump/services.txt b/Dump/services.txt deleted file mode 100644 index 85da566..0000000 --- a/Dump/services.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..84a10cc --- /dev/null +++ b/README.md @@ -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). \ No newline at end of file diff --git a/app.py b/app.py index 48a523a..f718c73 100644 --- a/app.py +++ b/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, diff --git a/instance/fhir_ig.db b/instance/fhir_ig.db index 0d4bd907c2fb0050372f8f7b13546a990b359dac..bc0719047716cd8d7ee7821fe086dac003a3dc1e 100644 GIT binary patch delta 681 zcmaLTPixa)90%|xQ!y^gh-_wUr^}wYt!~XjQ!BM)APxktIvHpg0p## z)x$(QSRU}?L9gCM4?FK2_%?R)D!cg2Y&cXaJ zQ`D?I?~8f)0=AlYgg42WM_Y z3Ugq?EZl(WFas$_LIRdy32s3KCczkewrlssC$Q8=L`XE;3P#bD_vTyO+tod7U-!m- z9>L(h>Ep^LyCI>?a7xVdCzB%0WxN#+z+l{3lf&Cco)5v9=S0q9wm3 zAIjaC%6ZiY8-1NOdtyz5h^#!`=+o1~%XWpv$*kJ03Az!VJU-sp(WerlzABS*&!(p1 ndPkLQw!0_(O!D;v`!{%|FMAQReKl9zRNT)}tG}0eoD%&4K*h88 delta 211 zcmYk$Jqp4=5QgDhE!2dtDf~~=jZqVSddr-_8$|E`@c@AZ3&F}@YvC0ng;((|7W$@> znFqdsff-|LW9+NvxI%~x>WGSXE8S=MP`<+`MJ{MdeboEEisQb_(~AlgEUmGEhb0 - - - - - - - {% if title %}{{ title }} - {% endif %}{{ site_name }} - - - - -
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} - {% block content %}{% endblock %} -
- -
-
- {% set current_year = now.year %} - © {{ current_year }} {{ site_name }}. All rights reserved. -
-
- - - - {% block scripts %} - - {% endblock %} - - \ No newline at end of file diff --git a/templates/cp_downloaded_igs - Copy.html b/templates/cp_downloaded_igs - Copy.html deleted file mode 100644 index 49f8207..0000000 --- a/templates/cp_downloaded_igs - Copy.html +++ /dev/null @@ -1,135 +0,0 @@ -{# app/control_panel/templates/cp_downloaded_igs.html #} -{% extends "base.html" %} - -{% block content %} -
-
-

Manage FHIR Packages

- -
- - {% if error_message %} - - {% endif %} - - {# NOTE: The block calculating processed_ids set using {% set %} was REMOVED from here #} - - {# --- Start Two Column Layout --- #} -
- - {# --- Left Column: Downloaded Packages (Horizontal Buttons) --- #} -
-
-
Downloaded Packages ({{ packages|length }})
-
- {% if packages %} -
- - -

Risk:= Duplicate Dependancy with different versions

- - - - {% 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 --- #} - - - - - - {% endfor %} - -
Package NameVersionActions
- {# --- ADDED: Risk Badge for duplicates --- #} - {% if is_duplicate %} - Duplicate - {% endif %} - {# --- End Add --- #} - {{ pkg.name }} - {{ pkg.version }} {# Actions #} -
- {% if is_processed %} - Processed - {% else %} -
- - - -
- {% endif %} -
- - -
-
-
-
- {% elif not error_message %}

No downloaded FHIR packages found.

{% endif %} -
-
-
{# --- End Left Column --- #} - - - {# --- Right Column: Processed Packages (Vertical Buttons) --- #} -
-
-
Processed Packages ({{ processed_list|length }})
-
- {% if processed_list %} -

MS = Contains Must Support Elements

-
- - - - - - {% for processed_ig in processed_list %} - - - - - - - {% endfor %} - -
Package NameVersionResource TypesActions
{# 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') %} - {{ processed_ig.package_name }} - {{ processed_ig.package_version }} {# Resource Types Cell w/ Badges #} - {% set types_info = processed_ig.resource_types_info %} - {% if types_info %}
{% for type_info in types_info %}{% if type_info.must_support %}{{ type_info.name }}{% else %}{{ type_info.name }}{% endif %}{% endfor %}
{% else %}N/A{% endif %} -
- {# Vertical Button Group #} -
- View -
- - -
-
-
-
- {% elif not error_message %}

No packages recorded as processed yet.

{% endif %} -
-
-
{# --- End Right Column --- #} - -
{# --- End Row --- #} - -
{# End container #} -{% endblock %} - -{# Tooltip JS Initializer should be in base.html #} -{% block scripts %}{{ super() }}{% endblock %} \ No newline at end of file diff --git a/templates/cp_downloaded_igs.html b/templates/cp_downloaded_igs.html index 7a99aeb..3b982be 100644 --- a/templates/cp_downloaded_igs.html +++ b/templates/cp_downloaded_igs.html @@ -3,7 +3,7 @@ {% block content %}
FHIRFLARE Ig Toolkit -

Welcome to {{ site_name }}

+

Manage & Process FHIR Packages

This is the starting Point for your Journey through the IG's @@ -16,6 +16,39 @@

+ + +

Manage FHIR Packages

@@ -30,8 +63,10 @@
{% if packages %}
+ + @@ -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' %}
Package NameVersionActions
{{ pkg.name }} @@ -70,12 +105,11 @@
{% if duplicate_groups %} -

Duplicates detected: +

Risk: = Duplicate Dependencies

+

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' %} - {{ name }} ({{ versions|join(', ') }}) - {% endif %} + {% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %} + {{ name }} ({{ versions|join(', ') }}) {% endfor %}

{% else %} @@ -94,6 +128,7 @@
{% if processed_list %}

MS = Contains Must Support Elements

+

Resource Types in the list will be both Profile and Base Type:

diff --git a/templates/cp_downloaded_igs.html.txt b/templates/cp_downloaded_igs.html.txt deleted file mode 100644 index 24fd0cc..0000000 --- a/templates/cp_downloaded_igs.html.txt +++ /dev/null @@ -1,145 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
- PAS Logo Placeholder -

Welcome to {{ site_name }}

-
-

- 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. -

-
-
- -
-
-

Manage FHIR Packages

- -
- -
-
-
-
Downloaded Packages ({{ packages|length }})
-
- {% if packages %} -
-

Risk: Duplicate Dependency with Different Versions

-
- - - - - {% 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' %} - - - - - - {% endfor %} - -
Package NameVersionActions
- {{ pkg.name }} - {% if is_duplicate %} - Duplicate - {% endif %} - {{ pkg.version }} -
- {% if is_processed %} - Processed - {% else %} -
- - -
- {% endif %} -
- - -
-
-
-
- {% if duplicate_groups %} -

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' %} - {{ name }} ({{ versions|join(', ') }}) - {% endif %} - {% endfor %} -

- {% else %} -

No duplicates detected.

- {% endif %} - {% else %} -

No downloaded FHIR packages found.

- {% endif %} -
-
-
- -
-
-
Processed Packages ({{ processed_list|length }})
-
- {% if processed_list %} -

MS = Contains Must Support Elements

-
- - - - - - {% for processed_ig in processed_list %} - - - - - - - {% endfor %} - -
Package NameVersionResource TypesActions
- {{ processed_ig.package_name }} - {{ processed_ig.version }} - {% set types_info = processed_ig.resource_types_info %} - {% if types_info %}
- {% for type_info in types_info %} - {% if type_info.must_support %} - {{ type_info.name }} - {% else %} - {{ type_info.name }} - {% endif %} - {% endfor %} -
{% else %}N/A{% endif %} -
-
- View -
- - -
-
-
-
- {% else %} -

No packages recorded as processed yet.

- {% endif %} -
-
-
-
-
-{% endblock %} - -{% block scripts %} -{{ super() }} -{% endblock %} \ No newline at end of file diff --git a/templates/cp_view_processed_ig - Copy.html b/templates/cp_view_processed_ig - Copy.html deleted file mode 100644 index e8cf5f9..0000000 --- a/templates/cp_view_processed_ig - Copy.html +++ /dev/null @@ -1,405 +0,0 @@ -{% extends "base.html" %} - -{% from "_form_helpers.html" import render_field %} - -{% block content %} -
-
-

{{ title }}

- Back to Package List -
- - {% if processed_ig %} -
-
Package Details
-
-
-
Package Name
-
{{ processed_ig.package_name }}
-
Package Version
-
{{ processed_ig.package_version }}
-
Processed At
-
{{ processed_ig.processed_at.strftime('%Y-%m-%d %H:%M:%S UTC') if processed_ig.processed_at else 'N/A' }}
-
Processing Status
-
- {{ processed_ig.status or 'N/A' }} -
-
-
-
- -
-
Resource Types Found / Defined
-
- {% if profile_list or base_list %} -

MS = Contains Must Support Elements

- {% if profile_list %} -
Profiles Defined ({{ profile_list|length }}):
- - {% endif %} - {% if base_list %} -

Examples = - Examples will be displayed when selecting Base Types if contained in the IG

-
Base Resource Types Referenced ({{ base_list|length }}):
- - {% endif %} - {% else %} -

No resource type information extracted or stored.

- {% endif %} -
-
- - - - - - - - {% else %} - - {% endif %} -
- -{% block scripts %} -{{ super() }} - - -{% endblock scripts %} -{% endblock content %} {# Main content block END #} \ No newline at end of file diff --git a/templates/cp_view_processed_ig.html b/templates/cp_view_processed_ig.html index da0dd31..6e79574 100644 --- a/templates/cp_view_processed_ig.html +++ b/templates/cp_view_processed_ig.html @@ -64,6 +64,7 @@ center">

No profiles defined.

{% endif %} {% if base_list %} +

Examples = - Examples will be displayed when selecting Base Types if contained in the IG

Base Resource Types Referenced ({{ base_list|length }}):
{% for type_info in base_list %} @@ -147,7 +148,7 @@ center"> {% endif %} - + - - - -{% endblock content %} {# Main content block END #} \ No newline at end of file diff --git a/templates/fhir_ig_importer/import_ig_page.html b/templates/fhir_ig_importer/import_ig_page.html deleted file mode 100644 index 47b94cf..0000000 --- a/templates/fhir_ig_importer/import_ig_page.html +++ /dev/null @@ -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 #} -
-

Import FHIR Implementation Guide

- -
-
- {# --- Import Form --- #} -
-
-
Enter Package Details
-
{# Use relative endpoint for blueprint #} - {{ form.hidden_tag() }} {# CSRF token #} -
- {{ render_field(form.package_name, class="form-control" + (" is-invalid" if form.package_name.errors else ""), placeholder="e.g., hl7.fhir.au.base") }} -
-
- {{ render_field(form.package_version, class="form-control" + (" is-invalid" if form.package_version.errors else ""), placeholder="e.g., 4.1.0 or latest") }} -
-
- {{ form.submit(class="btn btn-primary", value="Fetch & Download IG") }} -
-
-
-
- - {# --- Results Section --- #} -
- {# Display Fatal Error if passed #} - {% if fatal_error %} - - {% endif %} - - {# Display results if the results dictionary exists (meaning POST happened) #} - {% if results %} -
-
- {# Use results.requested for package info #} -
Import Results for: {{ results.requested[0] }}#{{ results.requested[1] }}
-
- - {# --- Process Errors --- #} - {% if results.errors %} -
Errors Encountered ({{ results.errors|length }}):
-
    - {% for error in results.errors %} -
  • {{ error }}
  • - {% endfor %} -
- {% endif %} - - {# --- Downloaded Files --- #} -
Downloaded Packages ({{ results.downloaded|length }} / {{ results.processed|length }} processed):
- {# Check results.downloaded dictionary #} - {% if results.downloaded %} -
    - {# Iterate through results.downloaded items #} - {% for (name, version), path in results.downloaded.items()|sort %} -
  • - {{ name }}#{{ version }} - {# Display relative path cleanly #} - {{ path | replace('/app/', '') | replace('\\', '/') }} - Downloaded {# Moved badge to end #} -
  • - {% endfor %} -
-

Files saved in server's `instance/fhir_packages` directory.

- {% else %} -

No packages were successfully downloaded.

- {% endif %} - - - {# --- All Dependencies Found --- #} -
Consolidated Dependencies Found:
- {# 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 %} -

Unique direct dependencies found across all processed packages:

-
- {% for (name, version), _ in unique_deps.items()|sort %} -
{{ name }}
-
{{ version }}
- {% endfor %} -
- {% else %} - - {% endif %} - {# --- End Dependency Result --- #} - -
{# End card-body #} -
{# End card #} - {% endif %} {# End if results #} -
{# End results #} - {# --- End Results Section --- #} - -
-
- {# --- Instructions Panel --- #} -
-
-
Instructions
-

Enter the official FHIR package name (e.g., hl7.fhir.au.base) and the desired version (e.g., 4.1.0, latest).

-

The system will download the package and attempt to list its direct dependencies.

-

Downloads are saved to the server's `instance/fhir_packages` folder.

-
-
-
-
{# End row #} -
{# End container #} -{% endblock %} \ No newline at end of file