mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-15 09:00: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 app.py .
|
||||||
COPY services.py .
|
COPY services.py .
|
||||||
|
COPY forms.py .
|
||||||
|
COPY routes.py .
|
||||||
COPY templates/ templates/
|
COPY templates/ templates/
|
||||||
COPY static/ static/
|
COPY static/ static/
|
||||||
COPY tests/ tests/
|
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 import Flask, render_template, render_template_string, request, redirect, url_for, flash, jsonify, Response
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SubmitField, SelectField
|
from flask_wtf.csrf import CSRFProtect
|
||||||
from wtforms.validators import DataRequired, Regexp
|
|
||||||
import os
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -11,6 +13,7 @@ import services
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import re
|
import re
|
||||||
|
from forms import IgImportForm, ValidationForm
|
||||||
|
|
||||||
# Set up logging
|
# Set up logging
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
@ -20,14 +23,14 @@ app = Flask(__name__)
|
|||||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/instance/fhir_ig.db'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/instance/fhir_ig.db'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
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['API_KEY'] = 'your-api-key-here'
|
||||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True # Enable/disable imposed profile validation
|
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True # Enable/disable UI display of relationships
|
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||||
|
|
||||||
# Ensure directories exist and are writable
|
# Ensure directories exist and are writable
|
||||||
instance_path = '/app/instance'
|
instance_path = '/app/instance'
|
||||||
db_path = os.path.join(instance_path, 'fhir_ig.db')
|
db_path = '/app/instance/fhir_ig.db'
|
||||||
packages_path = app.config['FHIR_PACKAGES_DIR']
|
packages_path = app.config['FHIR_PACKAGES_DIR']
|
||||||
|
|
||||||
logger.debug(f"Instance path: {instance_path}")
|
logger.debug(f"Instance path: {instance_path}")
|
||||||
@ -37,9 +40,9 @@ logger.debug(f"Packages path: {packages_path}")
|
|||||||
try:
|
try:
|
||||||
os.makedirs(instance_path, exist_ok=True)
|
os.makedirs(instance_path, exist_ok=True)
|
||||||
os.makedirs(packages_path, exist_ok=True)
|
os.makedirs(packages_path, exist_ok=True)
|
||||||
os.chmod(instance_path, 0o777)
|
os.chmod(instance_path, 0o755)
|
||||||
os.chmod(packages_path, 0o777)
|
os.chmod(packages_path, 0o755)
|
||||||
logger.debug(f"Directories created: {os.listdir('/app')}")
|
logger.debug(f"Directories created: {os.listdir(os.path.dirname(__file__))}")
|
||||||
logger.debug(f"Instance contents: {os.listdir(instance_path)}")
|
logger.debug(f"Instance contents: {os.listdir(instance_path)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to create directories: {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")
|
logger.warning("Falling back to /tmp/fhir_ig.db")
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
csrf = CSRFProtect(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')
|
|
||||||
|
|
||||||
class ProcessedIg(db.Model):
|
class ProcessedIg(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@ -107,10 +95,11 @@ def import_ig():
|
|||||||
return redirect(url_for('view_igs'))
|
return redirect(url_for('view_igs'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Error downloading IG: {str(e)}", "error")
|
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')
|
@app.route('/view-igs')
|
||||||
def view_igs():
|
def view_igs():
|
||||||
|
form = FlaskForm()
|
||||||
igs = ProcessedIg.query.all()
|
igs = ProcessedIg.query.all()
|
||||||
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
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()):
|
for i, name in enumerate(duplicate_groups.keys()):
|
||||||
group_colors[name] = colors[i % len(colors)]
|
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,
|
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
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)
|
config=app.config)
|
||||||
|
|
||||||
@app.route('/push-igs', methods=['GET', 'POST'])
|
@app.route('/push-igs', methods=['GET', 'POST'])
|
||||||
def push_igs():
|
def push_igs():
|
||||||
|
form = FlaskForm()
|
||||||
igs = ProcessedIg.query.all()
|
igs = ProcessedIg.query.all()
|
||||||
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
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()):
|
for i, name in enumerate(duplicate_groups.keys()):
|
||||||
group_colors[name] = colors[i % len(colors)]
|
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,
|
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
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'],
|
api_key=app.config['API_KEY'],
|
||||||
config=app.config)
|
config=app.config)
|
||||||
|
|
||||||
@app.route('/process-igs', methods=['POST'])
|
@app.route('/process-igs', methods=['POST'])
|
||||||
def process_ig():
|
def process_ig():
|
||||||
filename = request.form.get('filename')
|
form = FlaskForm()
|
||||||
if not filename or not filename.endswith('.tgz'):
|
if form.validate_on_submit():
|
||||||
flash("Invalid package file.", "error")
|
filename = request.form.get('filename')
|
||||||
return redirect(url_for('view_igs'))
|
if not filename or not filename.endswith('.tgz'):
|
||||||
|
flash("Invalid package file.", "error")
|
||||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
return redirect(url_for('view_igs'))
|
||||||
if not os.path.exists(tgz_path):
|
|
||||||
flash(f"Package file not found: {filename}", "error")
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||||
return redirect(url_for('view_igs'))
|
if not os.path.exists(tgz_path):
|
||||||
|
flash(f"Package file not found: {filename}", "error")
|
||||||
try:
|
return redirect(url_for('view_igs'))
|
||||||
last_hyphen_index = filename.rfind('-')
|
|
||||||
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
try:
|
||||||
name = filename[:last_hyphen_index]
|
last_hyphen_index = filename.rfind('-')
|
||||||
version = filename[last_hyphen_index + 1:-4]
|
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
||||||
name = name.replace('_', '.')
|
name = filename[:last_hyphen_index]
|
||||||
else:
|
version = filename[last_hyphen_index + 1:-4]
|
||||||
name = filename[:-4]
|
name = name.replace('_', '.')
|
||||||
version = ''
|
else:
|
||||||
logger.warning(f"Could not parse version from {filename} during processing")
|
name = filename[:-4]
|
||||||
package_info = services.process_package_file(tgz_path)
|
version = ''
|
||||||
processed_ig = ProcessedIg(
|
logger.warning(f"Could not parse version from {filename} during processing")
|
||||||
package_name=name,
|
package_info = services.process_package_file(tgz_path)
|
||||||
version=version,
|
processed_ig = ProcessedIg(
|
||||||
processed_date=datetime.now(),
|
package_name=name,
|
||||||
resource_types_info=package_info['resource_types_info'],
|
version=version,
|
||||||
must_support_elements=package_info.get('must_support_elements'),
|
processed_date=datetime.now(),
|
||||||
examples=package_info.get('examples')
|
resource_types_info=package_info['resource_types_info'],
|
||||||
)
|
must_support_elements=package_info.get('must_support_elements'),
|
||||||
db.session.add(processed_ig)
|
examples=package_info.get('examples')
|
||||||
db.session.commit()
|
)
|
||||||
flash(f"Successfully processed {name}#{version}!", "success")
|
db.session.add(processed_ig)
|
||||||
except Exception as e:
|
db.session.commit()
|
||||||
flash(f"Error processing IG: {str(e)}", "error")
|
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'))
|
return redirect(url_for('view_igs'))
|
||||||
|
|
||||||
@app.route('/delete-ig', methods=['POST'])
|
@app.route('/delete-ig', methods=['POST'])
|
||||||
def delete_ig():
|
def delete_ig():
|
||||||
filename = request.form.get('filename')
|
form = FlaskForm()
|
||||||
if not filename or not filename.endswith('.tgz'):
|
if form.validate_on_submit():
|
||||||
flash("Invalid package file.", "error")
|
filename = request.form.get('filename')
|
||||||
return redirect(url_for('view_igs'))
|
if not filename or not filename.endswith('.tgz'):
|
||||||
|
flash("Invalid package file.", "error")
|
||||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
return redirect(url_for('view_igs'))
|
||||||
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
|
||||||
if os.path.exists(tgz_path):
|
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||||
try:
|
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
||||||
os.remove(tgz_path)
|
if os.path.exists(tgz_path):
|
||||||
if os.path.exists(metadata_path):
|
try:
|
||||||
os.remove(metadata_path)
|
os.remove(tgz_path)
|
||||||
logger.debug(f"Deleted metadata file: {metadata_path}")
|
if os.path.exists(metadata_path):
|
||||||
flash(f"Deleted {filename}", "success")
|
os.remove(metadata_path)
|
||||||
except Exception as e:
|
logger.debug(f"Deleted metadata file: {metadata_path}")
|
||||||
flash(f"Error deleting {filename}: {str(e)}", "error")
|
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:
|
else:
|
||||||
flash(f"File not found: {filename}", "error")
|
flash("CSRF token missing or invalid.", "error")
|
||||||
return redirect(url_for('view_igs'))
|
return redirect(url_for('view_igs'))
|
||||||
|
|
||||||
@app.route('/unload-ig', methods=['POST'])
|
@app.route('/unload-ig', methods=['POST'])
|
||||||
def unload_ig():
|
def unload_ig():
|
||||||
ig_id = request.form.get('ig_id')
|
form = FlaskForm()
|
||||||
if not ig_id:
|
if form.validate_on_submit():
|
||||||
flash("Invalid package ID.", "error")
|
ig_id = request.form.get('ig_id')
|
||||||
return redirect(url_for('view_igs'))
|
if not ig_id:
|
||||||
|
flash("Invalid package ID.", "error")
|
||||||
processed_ig = db.session.get(ProcessedIg, ig_id)
|
return redirect(url_for('view_igs'))
|
||||||
if processed_ig:
|
|
||||||
try:
|
processed_ig = db.session.get(ProcessedIg, ig_id)
|
||||||
db.session.delete(processed_ig)
|
if processed_ig:
|
||||||
db.session.commit()
|
try:
|
||||||
flash(f"Unloaded {processed_ig.package_name}#{processed_ig.version}", "success")
|
db.session.delete(processed_ig)
|
||||||
except Exception as e:
|
db.session.commit()
|
||||||
flash(f"Error unloading package: {str(e)}", "error")
|
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:
|
else:
|
||||||
flash(f"Package not found with ID: {ig_id}", "error")
|
flash("CSRF token missing or invalid.", "error")
|
||||||
return redirect(url_for('view_igs'))
|
return redirect(url_for('view_igs'))
|
||||||
|
|
||||||
@app.route('/view-ig/<int:processed_ig_id>')
|
@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}",
|
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,
|
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,
|
complies_with_profiles=complies_with_profiles, imposed_profiles=imposed_profiles,
|
||||||
config=app.config)
|
config=app.config)
|
||||||
|
|
||||||
@ -418,7 +420,6 @@ def get_package_metadata():
|
|||||||
return jsonify({'dependency_mode': metadata['dependency_mode']})
|
return jsonify({'dependency_mode': metadata['dependency_mode']})
|
||||||
return jsonify({'error': 'Metadata not found'}), 404
|
return jsonify({'error': 'Metadata not found'}), 404
|
||||||
|
|
||||||
# API Endpoint: Import IG Package
|
|
||||||
@app.route('/api/import-ig', methods=['POST'])
|
@app.route('/api/import-ig', methods=['POST'])
|
||||||
def api_import_ig():
|
def api_import_ig():
|
||||||
auth_error = check_api_key()
|
auth_error = check_api_key()
|
||||||
@ -522,7 +523,6 @@ def api_import_ig():
|
|||||||
logger.error(f"Error in api_import_ig: {str(e)}")
|
logger.error(f"Error in api_import_ig: {str(e)}")
|
||||||
return jsonify({"status": "error", "message": f"Error importing package: {str(e)}"}), 500
|
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'])
|
@app.route('/api/push-ig', methods=['POST'])
|
||||||
def api_push_ig():
|
def api_push_ig():
|
||||||
auth_error = check_api_key()
|
auth_error = check_api_key()
|
||||||
@ -597,7 +597,7 @@ def api_push_ig():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Validate against the profile and imposed profiles
|
# 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']:
|
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"
|
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
|
failure_count += 1
|
||||||
@ -643,10 +643,71 @@ def api_push_ig():
|
|||||||
|
|
||||||
return Response(generate_stream(), mimetype='application/x-ndjson')
|
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():
|
with app.app_context():
|
||||||
logger.debug(f"Creating database at: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
logger.debug(f"Creating database at: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
db.create_all()
|
try:
|
||||||
logger.debug("Database initialization complete")
|
db.create_all()
|
||||||
|
logger.debug("Database initialization complete")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to initialize database: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
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 flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SubmitField
|
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Regexp
|
from wtforms.validators import DataRequired, Regexp, Optional
|
||||||
|
|
||||||
class IgImportForm(FlaskForm):
|
class IgImportForm(FlaskForm):
|
||||||
"""Form for specifying an IG package to import."""
|
package_name = StringField('Package Name', validators=[
|
||||||
# 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(),
|
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', validators=[
|
||||||
package_version = StringField('Package Version (e.g., 4.1.0 or current)', validators=[
|
|
||||||
DataRequired(),
|
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",
|
"package_name": "hl7.fhir.au.core",
|
||||||
"version": "1.1.0-preview",
|
"version": "1.1.0-preview",
|
||||||
"dependency_mode": "tree-shaking",
|
"dependency_mode": "recursive",
|
||||||
"imported_dependencies": [
|
"imported_dependencies": [
|
||||||
{
|
{
|
||||||
"name": "hl7.fhir.r4.core",
|
"name": "hl7.fhir.r4.core",
|
||||||
|
@ -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">
|
<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>
|
<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>
|
||||||
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
.badge.bg-danger {
|
.badge.bg-danger {
|
||||||
background-color: #dc3545 !important;
|
background-color: #dc3545 !important;
|
||||||
}
|
}
|
||||||
/* Ensure table rows with custom background colors are not overridden by Bootstrap */
|
|
||||||
.table tr.bg-warning {
|
.table tr.bg-warning {
|
||||||
background-color: #ffc107 !important;
|
background-color: #ffc107 !important;
|
||||||
}
|
}
|
||||||
@ -44,7 +43,6 @@
|
|||||||
.table tr.bg-danger {
|
.table tr.bg-danger {
|
||||||
background-color: #dc3545 !important;
|
background-color: #dc3545 !important;
|
||||||
}
|
}
|
||||||
/* Override Bootstrap's table background to allow custom row colors to show */
|
|
||||||
.table-custom-bg {
|
.table-custom-bg {
|
||||||
--bs-table-bg: transparent !important;
|
--bs-table-bg: transparent !important;
|
||||||
}
|
}
|
||||||
@ -64,9 +62,7 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if packages %}
|
{% if packages %}
|
||||||
<div class="table-responsive">
|
<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 Dependencies</small></p>
|
||||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> Duplicate Dependency with Different Versions</small></p>
|
|
||||||
--------------------------------------------------------------------------------------------------------------------------------------------->
|
|
||||||
<table class="table table-sm table-hover table-custom-bg">
|
<table class="table table-sm table-hover table-custom-bg">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
|
<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>
|
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||||
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||||
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
||||||
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
<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>
|
<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>
|
</form>
|
||||||
@ -106,7 +104,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if duplicate_groups %}
|
{% 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:
|
<p class="mt-2 small text-muted">Duplicate dependencies detected:
|
||||||
{% for name, versions in duplicate_groups.items() %}
|
{% for name, versions in duplicate_groups.items() %}
|
||||||
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
|
{% 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">
|
<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>
|
<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 method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
||||||
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
<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>
|
<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>
|
</form>
|
||||||
|
@ -12,9 +12,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Package Name</th>
|
<th>Package Name</th>
|
||||||
<th>Version</th>
|
<th>Version</th>
|
||||||
<!--------------------------------------------------------------------------------------------------------------if you unhide buttons unhide this--------------------------------------------
|
|
||||||
<th>Actions</th>
|
|
||||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -27,20 +24,6 @@
|
|||||||
<tr {% if duplicate_group %}class="{{ group_colors[name] }}"{% endif %}>
|
<tr {% if duplicate_group %}class="{{ group_colors[name] }}"{% endif %}>
|
||||||
<td>{{ name }}</td>
|
<td>{{ name }}</td>
|
||||||
<td>{{ version }}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -65,6 +48,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2>Push IGs to FHIR Server</h2>
|
<h2>Push IGs to FHIR Server</h2>
|
||||||
<form id="pushIgForm" method="POST">
|
<form id="pushIgForm" method="POST">
|
||||||
|
{{ form.csrf_token }}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="packageSelect" class="form-label">Select Package to Push</label>
|
<label for="packageSelect" class="form-label">Select Package to Push</label>
|
||||||
<select class="form-select" id="packageSelect" name="package_id" required>
|
<select class="form-select" id="packageSelect" name="package_id" required>
|
||||||
@ -115,6 +99,7 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const packageSelect = document.getElementById('packageSelect');
|
const packageSelect = document.getElementById('packageSelect');
|
||||||
const dependencyModeField = document.getElementById('dependencyMode');
|
const dependencyModeField = document.getElementById('dependencyMode');
|
||||||
|
const csrfToken = "{{ form.csrf_token._value() }}";
|
||||||
|
|
||||||
// Update dependency mode when package selection changes
|
// Update dependency mode when package selection changes
|
||||||
packageSelect.addEventListener('change', function() {
|
packageSelect.addEventListener('change', function() {
|
||||||
@ -143,70 +128,135 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (packageSelect.value) {
|
if (packageSelect.value) {
|
||||||
packageSelect.dispatchEvent(new Event('change'));
|
packageSelect.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
|
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const form = event.target;
|
const form = event.target;
|
||||||
const pushButton = document.getElementById('pushButton');
|
const pushButton = document.getElementById('pushButton');
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const packageId = formData.get('package_id');
|
const packageId = formData.get('package_id');
|
||||||
const [packageName, version] = packageId.split('#');
|
const [packageName, version] = packageId.split('#');
|
||||||
const fhirServerUrl = formData.get('fhir_server_url');
|
const fhirServerUrl = formData.get('fhir_server_url');
|
||||||
const includeDependencies = formData.get('include_dependencies') === 'on';
|
const includeDependencies = formData.get('include_dependencies') === 'on';
|
||||||
|
|
||||||
// Disable the button to prevent multiple submissions
|
// Disable the button to prevent multiple submissions
|
||||||
pushButton.disabled = true;
|
pushButton.disabled = true;
|
||||||
pushButton.textContent = 'Pushing...';
|
pushButton.textContent = 'Pushing...';
|
||||||
|
|
||||||
// Clear the console and response area
|
// Clear the console and response area
|
||||||
const liveConsole = document.getElementById('liveConsole');
|
const liveConsole = document.getElementById('liveConsole');
|
||||||
const responseDiv = document.getElementById('pushResponse');
|
const responseDiv = document.getElementById('pushResponse');
|
||||||
liveConsole.innerHTML = '<div>Starting push operation...</div>';
|
liveConsole.innerHTML = '<div>Starting push operation...</div>';
|
||||||
responseDiv.innerHTML = '';
|
responseDiv.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/push-ig', {
|
const response = await fetch('/api/push-ig', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/x-ndjson'
|
'Accept': 'application/x-ndjson',
|
||||||
},
|
'X-CSRFToken': csrfToken
|
||||||
body: JSON.stringify({
|
},
|
||||||
package_name: packageName,
|
body: JSON.stringify({
|
||||||
version: version,
|
package_name: packageName,
|
||||||
fhir_server_url: fhirServerUrl,
|
version: version,
|
||||||
include_dependencies: includeDependencies,
|
fhir_server_url: fhirServerUrl,
|
||||||
api_key: '{{ api_key | safe }}' // Use the API key passed from the server
|
include_dependencies: includeDependencies,
|
||||||
})
|
api_key: '{{ api_key | safe }}'
|
||||||
});
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
throw new Error(errorData.message || 'Failed to push package');
|
throw new Error(errorData.message || 'Failed to push package');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream the response
|
// Stream the response
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
if (done) break;
|
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)
|
for (const line of lines) {
|
||||||
const lines = buffer.split('\n');
|
if (!line.trim()) continue;
|
||||||
buffer = lines.pop(); // Keep the last (possibly incomplete) line in the buffer
|
try {
|
||||||
|
const data = JSON.parse(line);
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
let messageClass = '';
|
||||||
|
let prefix = '';
|
||||||
|
|
||||||
for (const line of lines) {
|
switch (data.type) {
|
||||||
if (!line.trim()) continue; // Skip empty lines
|
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 {
|
try {
|
||||||
const data = JSON.parse(line);
|
const data = JSON.parse(buffer);
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
let messageClass = '';
|
let messageClass = '';
|
||||||
let prefix = '';
|
let prefix = '';
|
||||||
@ -230,7 +280,6 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
|||||||
prefix = '[WARNING]';
|
prefix = '[WARNING]';
|
||||||
break;
|
break;
|
||||||
case 'complete':
|
case 'complete':
|
||||||
// Display the final summary in the response area
|
|
||||||
if (data.data.status === 'success') {
|
if (data.data.status === 'success') {
|
||||||
responseDiv.innerHTML = `
|
responseDiv.innerHTML = `
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
@ -254,7 +303,6 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
// Also log the completion in the console
|
|
||||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
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]');
|
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||||
break;
|
break;
|
||||||
@ -267,103 +315,30 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
|||||||
messageDiv.className = messageClass;
|
messageDiv.className = messageClass;
|
||||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||||
liveConsole.appendChild(messageDiv);
|
liveConsole.appendChild(messageDiv);
|
||||||
|
|
||||||
// Auto-scroll to the bottom
|
|
||||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||||
} catch (parseError) {
|
} 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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -6,13 +6,13 @@
|
|||||||
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
|
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
|
||||||
<div class="col-lg-6 mx-auto">
|
<div class="col-lg-6 mx-auto">
|
||||||
<p class="lead mb-4">
|
<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>
|
</p>
|
||||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import FHIR IG</a>
|
<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('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>
|
</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