mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 00:40:00 +00:00
Added Fhir Sample validation (alpha)
This commit is contained in:
parent
9b7b8344de
commit
afafcd0a22
@ -10,6 +10,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
COPY services.py .
|
||||
COPY forms.py .
|
||||
COPY routes.py .
|
||||
COPY templates/ templates/
|
||||
COPY static/ static/
|
||||
COPY tests/ tests/
|
||||
|
265
app.py
265
app.py
@ -1,9 +1,11 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
from flask import Flask, render_template, render_template_string, request, redirect, url_for, flash, jsonify, Response
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, SelectField
|
||||
from wtforms.validators import DataRequired, Regexp
|
||||
import os
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
import tarfile
|
||||
import json
|
||||
from datetime import datetime
|
||||
@ -11,6 +13,7 @@ import services
|
||||
import logging
|
||||
import requests
|
||||
import re
|
||||
from forms import IgImportForm, ValidationForm
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
@ -20,14 +23,14 @@ app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/instance/fhir_ig.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['FHIR_PACKAGES_DIR'] = os.path.join(app.instance_path, 'fhir_packages')
|
||||
app.config['FHIR_PACKAGES_DIR'] = '/app/instance/fhir_packages'
|
||||
app.config['API_KEY'] = 'your-api-key-here'
|
||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True # Enable/disable imposed profile validation
|
||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True # Enable/disable UI display of relationships
|
||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||
|
||||
# Ensure directories exist and are writable
|
||||
instance_path = '/app/instance'
|
||||
db_path = os.path.join(instance_path, 'fhir_ig.db')
|
||||
db_path = '/app/instance/fhir_ig.db'
|
||||
packages_path = app.config['FHIR_PACKAGES_DIR']
|
||||
|
||||
logger.debug(f"Instance path: {instance_path}")
|
||||
@ -37,9 +40,9 @@ logger.debug(f"Packages path: {packages_path}")
|
||||
try:
|
||||
os.makedirs(instance_path, exist_ok=True)
|
||||
os.makedirs(packages_path, exist_ok=True)
|
||||
os.chmod(instance_path, 0o777)
|
||||
os.chmod(packages_path, 0o777)
|
||||
logger.debug(f"Directories created: {os.listdir('/app')}")
|
||||
os.chmod(instance_path, 0o755)
|
||||
os.chmod(packages_path, 0o755)
|
||||
logger.debug(f"Directories created: {os.listdir(os.path.dirname(__file__))}")
|
||||
logger.debug(f"Instance contents: {os.listdir(instance_path)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directories: {e}")
|
||||
@ -47,22 +50,7 @@ except Exception as e:
|
||||
logger.warning("Falling back to /tmp/fhir_ig.db")
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
class IgImportForm(FlaskForm):
|
||||
package_name = StringField('Package Name (e.g., hl7.fhir.us.core)', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zAZ0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
|
||||
])
|
||||
package_version = StringField('Package Version (e.g., 1.0.0 or current)', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
|
||||
])
|
||||
dependency_mode = SelectField('Dependency Pulling Mode', choices=[
|
||||
('recursive', 'Current Recursive'),
|
||||
('patch-canonical', 'Patch Canonical Versions'),
|
||||
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
|
||||
], default='recursive')
|
||||
submit = SubmitField('Fetch & Download IG')
|
||||
csrf = CSRFProtect(app)
|
||||
|
||||
class ProcessedIg(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -107,10 +95,11 @@ def import_ig():
|
||||
return redirect(url_for('view_igs'))
|
||||
except Exception as e:
|
||||
flash(f"Error downloading IG: {str(e)}", "error")
|
||||
return render_template('import_ig.html', form=form, site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
return render_template('import_ig.html', form=form, site_name='FHIRFLARE IG Toolkit', now=datetime.now())
|
||||
|
||||
@app.route('/view-igs')
|
||||
def view_igs():
|
||||
form = FlaskForm()
|
||||
igs = ProcessedIg.query.all()
|
||||
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
||||
|
||||
@ -161,14 +150,15 @@ def view_igs():
|
||||
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,
|
||||
return render_template('cp_downloaded_igs.html', form=form, packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
||||
site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
||||
site_name='FHIRFLARE IG Toolkit', now=datetime.now(),
|
||||
config=app.config)
|
||||
|
||||
@app.route('/push-igs', methods=['GET', 'POST'])
|
||||
def push_igs():
|
||||
form = FlaskForm()
|
||||
igs = ProcessedIg.query.all()
|
||||
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
||||
|
||||
@ -219,90 +209,102 @@ def push_igs():
|
||||
for i, name in enumerate(duplicate_groups.keys()):
|
||||
group_colors[name] = colors[i % len(colors)]
|
||||
|
||||
return render_template('cp_push_igs.html', packages=packages, processed_list=igs,
|
||||
return render_template('cp_push_igs.html', form=form, packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
||||
site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
||||
site_name='FHIRFLARE IG Toolkit', now=datetime.now(),
|
||||
api_key=app.config['API_KEY'],
|
||||
config=app.config)
|
||||
|
||||
@app.route('/process-igs', methods=['POST'])
|
||||
def process_ig():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
if not os.path.exists(tgz_path):
|
||||
flash(f"Package file not found: {filename}", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
try:
|
||||
last_hyphen_index = filename.rfind('-')
|
||||
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
||||
name = filename[:last_hyphen_index]
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
name = name.replace('_', '.')
|
||||
else:
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename} during processing")
|
||||
package_info = services.process_package_file(tgz_path)
|
||||
processed_ig = ProcessedIg(
|
||||
package_name=name,
|
||||
version=version,
|
||||
processed_date=datetime.now(),
|
||||
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 processed {name}#{version}!", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error processing IG: {str(e)}", "error")
|
||||
form = FlaskForm()
|
||||
if form.validate_on_submit():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
if not os.path.exists(tgz_path):
|
||||
flash(f"Package file not found: {filename}", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
try:
|
||||
last_hyphen_index = filename.rfind('-')
|
||||
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
||||
name = filename[:last_hyphen_index]
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
name = name.replace('_', '.')
|
||||
else:
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename} during processing")
|
||||
package_info = services.process_package_file(tgz_path)
|
||||
processed_ig = ProcessedIg(
|
||||
package_name=name,
|
||||
version=version,
|
||||
processed_date=datetime.now(),
|
||||
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 processed {name}#{version}!", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error processing IG: {str(e)}", "error")
|
||||
else:
|
||||
flash("CSRF token missing or invalid.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
@app.route('/delete-ig', methods=['POST'])
|
||||
def delete_ig():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
||||
if os.path.exists(tgz_path):
|
||||
try:
|
||||
os.remove(tgz_path)
|
||||
if os.path.exists(metadata_path):
|
||||
os.remove(metadata_path)
|
||||
logger.debug(f"Deleted metadata file: {metadata_path}")
|
||||
flash(f"Deleted {filename}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting {filename}: {str(e)}", "error")
|
||||
form = FlaskForm()
|
||||
if form.validate_on_submit():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
||||
if os.path.exists(tgz_path):
|
||||
try:
|
||||
os.remove(tgz_path)
|
||||
if os.path.exists(metadata_path):
|
||||
os.remove(metadata_path)
|
||||
logger.debug(f"Deleted metadata file: {metadata_path}")
|
||||
flash(f"Deleted {filename}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting {filename}: {str(e)}", "error")
|
||||
else:
|
||||
flash(f"File not found: {filename}", "error")
|
||||
else:
|
||||
flash(f"File not found: {filename}", "error")
|
||||
flash("CSRF token missing or invalid.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
@app.route('/unload-ig', methods=['POST'])
|
||||
def unload_ig():
|
||||
ig_id = request.form.get('ig_id')
|
||||
if not ig_id:
|
||||
flash("Invalid package ID.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
processed_ig = db.session.get(ProcessedIg, ig_id)
|
||||
if processed_ig:
|
||||
try:
|
||||
db.session.delete(processed_ig)
|
||||
db.session.commit()
|
||||
flash(f"Unloaded {processed_ig.package_name}#{processed_ig.version}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error unloading package: {str(e)}", "error")
|
||||
form = FlaskForm()
|
||||
if form.validate_on_submit():
|
||||
ig_id = request.form.get('ig_id')
|
||||
if not ig_id:
|
||||
flash("Invalid package ID.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
processed_ig = db.session.get(ProcessedIg, ig_id)
|
||||
if processed_ig:
|
||||
try:
|
||||
db.session.delete(processed_ig)
|
||||
db.session.commit()
|
||||
flash(f"Unloaded {processed_ig.package_name}#{processed_ig.version}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error unloading package: {str(e)}", "error")
|
||||
else:
|
||||
flash(f"Package not found with ID: {ig_id}", "error")
|
||||
else:
|
||||
flash(f"Package not found with ID: {ig_id}", "error")
|
||||
flash("CSRF token missing or invalid.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
@app.route('/view-ig/<int:processed_ig_id>')
|
||||
@ -327,7 +329,7 @@ def view_ig(processed_ig_id):
|
||||
|
||||
return render_template('cp_view_processed_ig.html', title=f"View {processed_ig.package_name}#{processed_ig.version}",
|
||||
processed_ig=processed_ig, profile_list=profile_list, base_list=base_list,
|
||||
examples_by_type=examples_by_type, site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
||||
examples_by_type=examples_by_type, site_name='FHIRFLARE IG Toolkit', now=datetime.now(),
|
||||
complies_with_profiles=complies_with_profiles, imposed_profiles=imposed_profiles,
|
||||
config=app.config)
|
||||
|
||||
@ -418,7 +420,6 @@ def get_package_metadata():
|
||||
return jsonify({'dependency_mode': metadata['dependency_mode']})
|
||||
return jsonify({'error': 'Metadata not found'}), 404
|
||||
|
||||
# API Endpoint: Import IG Package
|
||||
@app.route('/api/import-ig', methods=['POST'])
|
||||
def api_import_ig():
|
||||
auth_error = check_api_key()
|
||||
@ -522,7 +523,6 @@ def api_import_ig():
|
||||
logger.error(f"Error in api_import_ig: {str(e)}")
|
||||
return jsonify({"status": "error", "message": f"Error importing package: {str(e)}"}), 500
|
||||
|
||||
# API Endpoint: Push IG to FHIR Server with Streaming
|
||||
@app.route('/api/push-ig', methods=['POST'])
|
||||
def api_push_ig():
|
||||
auth_error = check_api_key()
|
||||
@ -597,7 +597,7 @@ def api_push_ig():
|
||||
continue
|
||||
|
||||
# Validate against the profile and imposed profiles
|
||||
validation_result = services.validate_resource_against_profile(resource, pkg_name, pkg_version, resource_type)
|
||||
validation_result = services.validate_resource_against_profile(pkg_name, pkg_version, resource, include_dependencies=False)
|
||||
if not validation_result['valid']:
|
||||
yield json.dumps({"type": "error", "message": f"Validation failed for {resource_type}/{resource_id} in {pkg_name}#{pkg_version}: {', '.join(validation_result['errors'])}"}) + "\n"
|
||||
failure_count += 1
|
||||
@ -643,10 +643,71 @@ def api_push_ig():
|
||||
|
||||
return Response(generate_stream(), mimetype='application/x-ndjson')
|
||||
|
||||
@app.route('/validate-sample', methods=['GET', 'POST'])
|
||||
def validate_sample():
|
||||
form = ValidationForm()
|
||||
validation_report = None
|
||||
packages = []
|
||||
|
||||
# Load available packages
|
||||
packages_dir = app.config['FHIR_PACKAGES_DIR']
|
||||
if os.path.exists(packages_dir):
|
||||
for filename in os.listdir(packages_dir):
|
||||
if filename.endswith('.tgz'):
|
||||
last_hyphen_index = filename.rfind('-')
|
||||
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
||||
name = filename[:last_hyphen_index]
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
name = name.replace('_', '.')
|
||||
try:
|
||||
with tarfile.open(os.path.join(packages_dir, filename), 'r:gz') as tar:
|
||||
package_json = tar.extractfile('package/package.json')
|
||||
pkg_info = json.load(package_json)
|
||||
packages.append({'name': pkg_info['name'], 'version': pkg_info['version']})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading package {filename}: {e}")
|
||||
continue
|
||||
|
||||
if form.validate_on_submit():
|
||||
package_name = form.package_name.data
|
||||
version = form.version.data
|
||||
include_dependencies = form.include_dependencies.data
|
||||
mode = form.mode.data
|
||||
try:
|
||||
sample_input = json.loads(form.sample_input.data)
|
||||
if mode == 'single':
|
||||
validation_report = services.validate_resource_against_profile(
|
||||
package_name, version, sample_input, include_dependencies
|
||||
)
|
||||
else: # mode == 'bundle'
|
||||
validation_report = services.validate_bundle_against_profile(
|
||||
package_name, version, sample_input, include_dependencies
|
||||
)
|
||||
flash("Validation completed.", 'success')
|
||||
except json.JSONDecodeError:
|
||||
flash("Invalid JSON format in sample input.", 'error')
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating sample: {e}")
|
||||
flash(f"Error validating sample: {str(e)}", 'error')
|
||||
validation_report = {'valid': False, 'errors': [str(e)], 'warnings': [], 'results': {}}
|
||||
|
||||
return render_template(
|
||||
'validate_sample.html',
|
||||
form=form,
|
||||
packages=packages,
|
||||
validation_report=validation_report,
|
||||
site_name='FHIRFLARE IG Toolkit',
|
||||
now=datetime.now()
|
||||
)
|
||||
|
||||
with app.app_context():
|
||||
logger.debug(f"Creating database at: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||
db.create_all()
|
||||
logger.debug("Database initialization complete")
|
||||
try:
|
||||
db.create_all()
|
||||
logger.debug("Database initialization complete")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
49
forms.py
49
forms.py
@ -1,19 +1,44 @@
|
||||
# app/modules/fhir_ig_importer/forms.py
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.validators import DataRequired, Regexp
|
||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired, Regexp, Optional
|
||||
|
||||
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=[
|
||||
package_name = StringField('Package Name', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
|
||||
Regexp(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$', message="Invalid package name format.")
|
||||
])
|
||||
# Basic validation for version (e.g., 4.1.0, current)
|
||||
package_version = StringField('Package Version (e.g., 4.1.0 or current)', validators=[
|
||||
package_version = StringField('Package Version', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
|
||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message="Invalid version format. Use alphanumeric characters, dots, or hyphens (e.g., 1.2.3, 1.1.0-preview, current).")
|
||||
])
|
||||
submit = SubmitField('Fetch & Download IG')
|
||||
dependency_mode = SelectField('Dependency Mode', choices=[
|
||||
('recursive', 'Current Recursive'),
|
||||
('patch-canonical', 'Patch Canonical Versions'),
|
||||
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
|
||||
], default='recursive')
|
||||
submit = SubmitField('Import')
|
||||
|
||||
class ValidationForm(FlaskForm):
|
||||
package_name = StringField('Package Name', validators=[DataRequired()])
|
||||
version = StringField('Package Version', validators=[DataRequired()])
|
||||
include_dependencies = BooleanField('Include Dependencies', default=True)
|
||||
mode = SelectField('Validation Mode', choices=[
|
||||
('single', 'Single Resource'),
|
||||
('bundle', 'Bundle')
|
||||
], default='single')
|
||||
sample_input = TextAreaField('Sample Input', validators=[DataRequired()])
|
||||
submit = SubmitField('Validate')
|
||||
|
||||
def validate_json(field, mode):
|
||||
"""Custom validator to ensure input is valid JSON and matches the selected mode."""
|
||||
import json
|
||||
try:
|
||||
data = json.loads(field)
|
||||
if mode == 'single' and not isinstance(data, dict):
|
||||
raise ValueError("Single resource mode requires a JSON object.")
|
||||
if mode == 'bundle' and (not isinstance(data, dict) or data.get('resourceType') != 'Bundle'):
|
||||
raise ValueError("Bundle mode requires a JSON object with resourceType 'Bundle'.")
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError("Invalid JSON format.")
|
||||
except ValueError as e:
|
||||
raise ValueError(str(e))
|
Binary file not shown.
@ -0,0 +1,21 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.au.base",
|
||||
"version": "5.1.0-preview",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "6.2.0"
|
||||
},
|
||||
{
|
||||
"name": "hl7.fhir.uv.extensions.r4",
|
||||
"version": "5.2.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.au.core",
|
||||
"version": "1.1.0-preview",
|
||||
"dependency_mode": "tree-shaking",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.extensions.r4",
|
||||
"version": "5.2.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
21
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
21
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.ipa",
|
||||
"version": "1.0.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "5.0.0"
|
||||
},
|
||||
{
|
||||
"name": "hl7.fhir.uv.smart-app-launch",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,13 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.smart-app-launch",
|
||||
"version": "2.0.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,17 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.uv.smart-app-launch",
|
||||
"version": "2.1.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "5.0.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.1.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.smart-app-launch-2.1.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,13 @@
|
||||
{
|
||||
"package_name": "hl7.terminology.r4",
|
||||
"version": "5.0.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.terminology.r4-5.0.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.terminology.r4-5.0.0.tgz
Normal file
Binary file not shown.
@ -0,0 +1,13 @@
|
||||
{
|
||||
"package_name": "hl7.terminology.r4",
|
||||
"version": "6.2.0",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.terminology.r4-6.2.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.terminology.r4-6.2.0.tgz
Normal file
Binary file not shown.
1544
services.py
1544
services.py
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="bi bi-upload me-1"></i> Push IGs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'validate_sample' else '' }}" href="{{ url_for('validate_sample') }}"><i class="bi bi-check-circle me-1"></i> Validate FHIR Sample</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,7 +31,6 @@
|
||||
.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;
|
||||
}
|
||||
@ -44,7 +43,6 @@
|
||||
.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;
|
||||
}
|
||||
@ -64,9 +62,7 @@
|
||||
<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>
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> = Duplicate Dependencies</small></p>
|
||||
<table class="table table-sm table-hover table-custom-bg">
|
||||
<thead>
|
||||
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
|
||||
@ -90,11 +86,13 @@
|
||||
<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;">
|
||||
{{ form.csrf_token }}
|
||||
<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;">
|
||||
{{ form.csrf_token }}
|
||||
<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>
|
||||
@ -106,7 +104,6 @@
|
||||
</table>
|
||||
</div>
|
||||
{% if duplicate_groups %}
|
||||
<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() %}
|
||||
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
|
||||
@ -158,6 +155,7 @@
|
||||
<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;">
|
||||
{{ form.csrf_token }}
|
||||
<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>
|
||||
|
@ -12,9 +12,6 @@
|
||||
<tr>
|
||||
<th>Package Name</th>
|
||||
<th>Version</th>
|
||||
<!--------------------------------------------------------------------------------------------------------------if you unhide buttons unhide this--------------------------------------------
|
||||
<th>Actions</th>
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -27,20 +24,6 @@
|
||||
<tr {% if duplicate_group %}class="{{ group_colors[name] }}"{% endif %}>
|
||||
<td>{{ name }}</td>
|
||||
<td>{{ version }}</td>
|
||||
<!--------------------------------------------------------------------------------------------------------------Dont need the buttons here--------------------------------------------------
|
||||
<td>
|
||||
{% if not processed %}
|
||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="filename" value="{{ filename }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Process</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete {{ name }}#{{ version }}?');">
|
||||
<input type="hidden" name="filename" value="{{ filename }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -65,6 +48,7 @@
|
||||
<div class="col-md-6">
|
||||
<h2>Push IGs to FHIR Server</h2>
|
||||
<form id="pushIgForm" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<div class="mb-3">
|
||||
<label for="packageSelect" class="form-label">Select Package to Push</label>
|
||||
<select class="form-select" id="packageSelect" name="package_id" required>
|
||||
@ -115,6 +99,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const packageSelect = document.getElementById('packageSelect');
|
||||
const dependencyModeField = document.getElementById('dependencyMode');
|
||||
const csrfToken = "{{ form.csrf_token._value() }}";
|
||||
|
||||
// Update dependency mode when package selection changes
|
||||
packageSelect.addEventListener('change', function() {
|
||||
@ -143,70 +128,135 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (packageSelect.value) {
|
||||
packageSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const pushButton = document.getElementById('pushButton');
|
||||
const formData = new FormData(form);
|
||||
const packageId = formData.get('package_id');
|
||||
const [packageName, version] = packageId.split('#');
|
||||
const fhirServerUrl = formData.get('fhir_server_url');
|
||||
const includeDependencies = formData.get('include_dependencies') === 'on';
|
||||
const form = event.target;
|
||||
const pushButton = document.getElementById('pushButton');
|
||||
const formData = new FormData(form);
|
||||
const packageId = formData.get('package_id');
|
||||
const [packageName, version] = packageId.split('#');
|
||||
const fhirServerUrl = formData.get('fhir_server_url');
|
||||
const includeDependencies = formData.get('include_dependencies') === 'on';
|
||||
|
||||
// Disable the button to prevent multiple submissions
|
||||
pushButton.disabled = true;
|
||||
pushButton.textContent = 'Pushing...';
|
||||
// Disable the button to prevent multiple submissions
|
||||
pushButton.disabled = true;
|
||||
pushButton.textContent = 'Pushing...';
|
||||
|
||||
// Clear the console and response area
|
||||
const liveConsole = document.getElementById('liveConsole');
|
||||
const responseDiv = document.getElementById('pushResponse');
|
||||
liveConsole.innerHTML = '<div>Starting push operation...</div>';
|
||||
responseDiv.innerHTML = '';
|
||||
// Clear the console and response area
|
||||
const liveConsole = document.getElementById('liveConsole');
|
||||
const responseDiv = document.getElementById('pushResponse');
|
||||
liveConsole.innerHTML = '<div>Starting push operation...</div>';
|
||||
responseDiv.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/push-ig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/x-ndjson'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
package_name: packageName,
|
||||
version: version,
|
||||
fhir_server_url: fhirServerUrl,
|
||||
include_dependencies: includeDependencies,
|
||||
api_key: '{{ api_key | safe }}' // Use the API key passed from the server
|
||||
})
|
||||
});
|
||||
try {
|
||||
const response = await fetch('/api/push-ig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/x-ndjson',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
package_name: packageName,
|
||||
version: version,
|
||||
fhir_server_url: fhirServerUrl,
|
||||
include_dependencies: includeDependencies,
|
||||
api_key: '{{ api_key | safe }}'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to push package');
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to push package');
|
||||
}
|
||||
|
||||
// Stream the response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
// Stream the response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// Decode the chunk and append to buffer
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
// Process each line (newline-separated JSON)
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // Keep the last (possibly incomplete) line in the buffer
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = '';
|
||||
let prefix = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue; // Skip empty lines
|
||||
switch (data.type) {
|
||||
case 'start':
|
||||
case 'progress':
|
||||
messageClass = 'text-info';
|
||||
prefix = '[INFO]';
|
||||
break;
|
||||
case 'success':
|
||||
messageClass = 'text-success';
|
||||
prefix = '[SUCCESS]';
|
||||
break;
|
||||
case 'error':
|
||||
messageClass = 'text-danger';
|
||||
prefix = '[ERROR]';
|
||||
break;
|
||||
case 'warning':
|
||||
messageClass = 'text-warning';
|
||||
prefix = '[WARNING]';
|
||||
break;
|
||||
case 'complete':
|
||||
if (data.data.status === 'success') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else if (data.data.status === 'partial') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<strong>Partial Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> ${data.data.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
||||
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||
break;
|
||||
default:
|
||||
messageClass = 'text-light';
|
||||
prefix = '[INFO]';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = messageClass;
|
||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||
liveConsole.appendChild(messageDiv);
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing streamed message:', parseError, 'Line:', line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
const data = JSON.parse(buffer);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = '';
|
||||
let prefix = '';
|
||||
@ -230,7 +280,6 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
||||
prefix = '[WARNING]';
|
||||
break;
|
||||
case 'complete':
|
||||
// Display the final summary in the response area
|
||||
if (data.data.status === 'success') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
@ -254,7 +303,6 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// Also log the completion in the console
|
||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
||||
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||
break;
|
||||
@ -267,103 +315,30 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
||||
messageDiv.className = messageClass;
|
||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||
liveConsole.appendChild(messageDiv);
|
||||
|
||||
// Auto-scroll to the bottom
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing streamed message:', parseError, 'Line:', line);
|
||||
console.error('Error parsing final streamed message:', parseError, 'Buffer:', buffer);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'text-danger';
|
||||
errorDiv.textContent = `${timestamp} [ERROR] Failed to push package: ${error.message}`;
|
||||
liveConsole.appendChild(errorDiv);
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> Failed to push package: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
pushButton.disabled = false;
|
||||
pushButton.textContent = 'Push to FHIR Server';
|
||||
}
|
||||
|
||||
// Process any remaining buffer
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(buffer);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = '';
|
||||
let prefix = '';
|
||||
|
||||
switch (data.type) {
|
||||
case 'start':
|
||||
case 'progress':
|
||||
messageClass = 'text-info';
|
||||
prefix = '[INFO]';
|
||||
break;
|
||||
case 'success':
|
||||
messageClass = 'text-success';
|
||||
prefix = '[SUCCESS]';
|
||||
break;
|
||||
case 'error':
|
||||
messageClass = 'text-danger';
|
||||
prefix = '[ERROR]';
|
||||
break;
|
||||
case 'warning':
|
||||
messageClass = 'text-warning';
|
||||
prefix = '[WARNING]';
|
||||
break;
|
||||
case 'complete':
|
||||
if (data.data.status === 'success') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else if (data.data.status === 'partial') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<strong>Partial Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> ${data.data.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
||||
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||
break;
|
||||
default:
|
||||
messageClass = 'text-light';
|
||||
prefix = '[INFO]';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = messageClass;
|
||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||
liveConsole.appendChild(messageDiv);
|
||||
|
||||
// Auto-scroll to the bottom
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing final streamed message:', parseError, 'Buffer:', buffer);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'text-danger';
|
||||
errorDiv.textContent = `${timestamp} [ERROR] Failed to push package: ${error.message}`;
|
||||
liveConsole.appendChild(errorDiv);
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> Failed to push package: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
// Re-enable the button
|
||||
pushButton.disabled = false;
|
||||
pushButton.textContent = 'Push to FHIR Server';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -6,13 +6,13 @@
|
||||
<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">
|
||||
Simple tool for importing and viewing FHIR Implementation Guides.
|
||||
Simple tool for importing, viewing, and validating FHIR Implementation Guides.
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import FHIR IG</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">View Downloaded IGs</a>
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
||||
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IGs</a>
|
||||
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
139
templates/validate_sample.html
Normal file
139
templates/validate_sample.html
Normal file
@ -0,0 +1,139 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Validate a FHIR resource or bundle against a selected Implementation Guide. (ALPHA - TEST Fhir pathing is complex and the logic is WIP, please report anomalies you find in Github issues alongside your sample json REMOVE PHI)
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Validation Form</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" id="validationForm">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<small class="form-text text-muted">
|
||||
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
||||
</small>
|
||||
<div class="mt-2">
|
||||
<select class="form-select" id="packageSelect" onchange="if(this.value) { let parts = this.value.split('#'); document.getElementById('{{ form.package_name.id }}').value = parts[0]; document.getElementById('{{ form.version.id }}').value = parts[1]; }">
|
||||
<option value="">-- Select a Package --</option>
|
||||
{% for pkg in packages %}
|
||||
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
|
||||
{{ form.package_name(class="form-control") }}
|
||||
{% for error in form.package_name.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
||||
{{ form.version(class="form-control") }}
|
||||
{% for error in form.version.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
|
||||
<div class="form-check">
|
||||
{{ form.include_dependencies(class="form-check-input") }}
|
||||
{{ form.include_dependencies.label(class="form-check-label") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
|
||||
{{ form.mode(class="form-select") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
|
||||
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
||||
{% for error in form.sample_input.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if validation_report %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">Validation Report</div>
|
||||
<div class="card-body">
|
||||
<h5>Summary</h5>
|
||||
<p><strong>Valid:</strong> {{ 'Yes' if validation_report.valid else 'No' }}</p>
|
||||
{% if validation_report.errors %}
|
||||
<h6>Errors</h6>
|
||||
<ul class="text-danger">
|
||||
{% for error in validation_report.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if validation_report.warnings %}
|
||||
<h6>Warnings</h6>
|
||||
<ul class="text-warning">
|
||||
{% for warning in validation_report.warnings %}
|
||||
<li>{{ warning }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if validation_report.results %}
|
||||
<h5>Detailed Results</h5>
|
||||
{% for resource_id, result in validation_report.results.items() %}
|
||||
<h6>{{ resource_id }}</h6>
|
||||
<p><strong>Valid:</strong> {{ 'Yes' if result.valid else 'No' }}</p>
|
||||
{% if result.errors %}
|
||||
<ul class="text-danger">
|
||||
{% for error in result.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if result.warnings %}
|
||||
<ul class="text-warning">
|
||||
{% for warning in result.warnings %}
|
||||
<li>{{ warning }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('validationForm');
|
||||
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
||||
|
||||
// Client-side JSON validation preview
|
||||
sampleInput.addEventListener('input', function() {
|
||||
try {
|
||||
JSON.parse(this.value);
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
} catch (e) {
|
||||
this.classList.remove('is-valid');
|
||||
this.classList.add('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user