mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-09-17 18:35:02 +00:00
V4.3 - Advanced Validation
V4.3 Advanced Validation added full gamut of options and configuration for fully fleshed out validation
This commit is contained in:
parent
356914f306
commit
92fe759b0f
160
app.py
160
app.py
@ -48,7 +48,9 @@ from services import (
|
|||||||
import_manual_package_and_dependencies,
|
import_manual_package_and_dependencies,
|
||||||
parse_and_list_igs,
|
parse_and_list_igs,
|
||||||
generate_ig_yaml,
|
generate_ig_yaml,
|
||||||
apply_and_validate
|
apply_and_validate,
|
||||||
|
run_validator_cli,
|
||||||
|
uuid
|
||||||
)
|
)
|
||||||
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm, IgYamlForm
|
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm, IgYamlForm
|
||||||
from wtforms import SubmitField
|
from wtforms import SubmitField
|
||||||
@ -70,11 +72,16 @@ app.config['FHIR_PACKAGES_DIR'] = os.path.join(instance_path, 'fhir_packages')
|
|||||||
app.config['API_KEY'] = os.environ.get('API_KEY', 'your-fallback-api-key-here')
|
app.config['API_KEY'] = os.environ.get('API_KEY', 'your-fallback-api-key-here')
|
||||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||||
|
app.config['LATEST_BUILD_URL'] = "http://build.fhir.org/ig/qas.json"
|
||||||
|
app.config['VALIDATION_LOG_DIR'] = os.path.join(app.instance_path, 'validation_logs')
|
||||||
|
os.makedirs(app.config['VALIDATION_LOG_DIR'], exist_ok=True)
|
||||||
app.config['UPLOAD_FOLDER'] = os.path.join(CURRENT_DIR, 'static', 'uploads') # For GoFSH output
|
app.config['UPLOAD_FOLDER'] = os.path.join(CURRENT_DIR, 'static', 'uploads') # For GoFSH output
|
||||||
app.config['APP_BASE_URL'] = os.environ.get('APP_BASE_URL', 'http://localhost:5000')
|
app.config['APP_BASE_URL'] = os.environ.get('APP_BASE_URL', 'http://localhost:5000')
|
||||||
app.config['HAPI_FHIR_URL'] = os.environ.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')
|
app.config['HAPI_FHIR_URL'] = os.environ.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')
|
||||||
CONFIG_PATH = os.environ.get('CONFIG_PATH', '/usr/local/tomcat/conf/application.yaml')
|
CONFIG_PATH = os.environ.get('CONFIG_PATH', '/usr/local/tomcat/conf/application.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Basic Swagger configuration
|
# Basic Swagger configuration
|
||||||
app.config['SWAGGER'] = {
|
app.config['SWAGGER'] = {
|
||||||
'title': 'FHIRFLARE IG Toolkit API',
|
'title': 'FHIRFLARE IG Toolkit API',
|
||||||
@ -1765,6 +1772,157 @@ csrf.exempt(api_push_ig) # Add this line
|
|||||||
# Exempt the entire API blueprint (for routes defined IN services.py, like /api/validate-sample)
|
# Exempt the entire API blueprint (for routes defined IN services.py, like /api/validate-sample)
|
||||||
csrf.exempt(services_bp) # Keep this line for routes defined in the blueprint
|
csrf.exempt(services_bp) # Keep this line for routes defined in the blueprint
|
||||||
|
|
||||||
|
@app.route('/validate-sample', methods=['GET'])
|
||||||
|
def validate_resource_page():
|
||||||
|
"""Renders the validation form page."""
|
||||||
|
form = ValidationForm()
|
||||||
|
# Fetch list of packages to populate the dropdown
|
||||||
|
packages = current_app.config.get('MANUAL_PACKAGE_CACHE', [])
|
||||||
|
return render_template('validate_sample.html', form=form, packages=packages)
|
||||||
|
|
||||||
|
@app.route('/api/validate', methods=['POST'])
|
||||||
|
@swag_from({
|
||||||
|
'tags': ['Validation'],
|
||||||
|
'summary': 'Starts a validation process for a FHIR resource or bundle.',
|
||||||
|
'description': 'Starts a validation task and immediately returns a task ID for polling. The actual validation is run in a separate thread to prevent timeouts.',
|
||||||
|
'security': [{'ApiKeyAuth': []}],
|
||||||
|
'consumes': ['application/json'],
|
||||||
|
'parameters': [
|
||||||
|
{
|
||||||
|
'name': 'validation_payload',
|
||||||
|
'in': 'body',
|
||||||
|
'required': True,
|
||||||
|
'schema': {
|
||||||
|
'type': 'object',
|
||||||
|
'required': ['package_name', 'version', 'fhir_resource'],
|
||||||
|
'properties': {
|
||||||
|
'package_name': {'type': 'string', 'example': 'hl7.fhir.us.core'},
|
||||||
|
'version': {'type': 'string', 'example': '6.1.0'},
|
||||||
|
'fhir_resource': {'type': 'string', 'description': 'A JSON string of the FHIR resource or Bundle to validate.'},
|
||||||
|
'fhir_version': {'type': 'string', 'example': '4.0.1'},
|
||||||
|
'do_native': {'type': 'boolean', 'default': False},
|
||||||
|
'hint_about_must_support': {'type': 'boolean', 'default': False},
|
||||||
|
'assume_valid_rest_references': {'type': 'boolean', 'default': False},
|
||||||
|
'no_extensible_binding_warnings': {'type': 'boolean', 'default': False},
|
||||||
|
'show_times': {'type': 'boolean', 'default': False},
|
||||||
|
'allow_example_urls': {'type': 'boolean', 'default': False},
|
||||||
|
'check_ips_codes': {'type': 'boolean', 'default': False},
|
||||||
|
'allow_any_extensions': {'type': 'boolean', 'default': False},
|
||||||
|
'tx_routing': {'type': 'boolean', 'default': False},
|
||||||
|
'snomed_ct_version': {'type': 'string', 'default': ''},
|
||||||
|
'profiles': {'type': 'string', 'description': 'Comma-separated list of profile URLs.'},
|
||||||
|
'extensions': {'type': 'string', 'description': 'Comma-separated list of extension URLs.'},
|
||||||
|
'terminology_server': {'type': 'string', 'description': 'URL of an alternate terminology server.'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'responses': {
|
||||||
|
'202': {'description': 'Validation process started successfully, returns a task_id.'},
|
||||||
|
'400': {'description': 'Invalid request (e.g., missing fields, invalid JSON).'}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def validate_resource():
|
||||||
|
"""API endpoint to run validation on a FHIR resource."""
|
||||||
|
try:
|
||||||
|
# Get data from the JSON payload
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid JSON: No data received.'}), 400
|
||||||
|
|
||||||
|
fhir_resource_text = data.get('fhir_resource')
|
||||||
|
fhir_version = data.get('fhir_version')
|
||||||
|
package_name = data.get('package_name')
|
||||||
|
version = data.get('version')
|
||||||
|
profiles = data.get('profiles')
|
||||||
|
extensions = data.get('extensions')
|
||||||
|
terminology_server = data.get('terminology_server')
|
||||||
|
snomed_ct_version = data.get('snomed_ct_version')
|
||||||
|
|
||||||
|
# Build the list of IGs from the package name and version
|
||||||
|
igs = [f"{package_name}#{version}"] if package_name and version else []
|
||||||
|
|
||||||
|
flags = {
|
||||||
|
'doNative': data.get('do_native'),
|
||||||
|
'hintAboutMustSupport': data.get('hint_about_must_support'),
|
||||||
|
'assumeValidRestReferences': data.get('assume_valid_rest_references'),
|
||||||
|
'noExtensibleBindingWarnings': data.get('no_extensible_binding_warnings'),
|
||||||
|
'showTimes': data.get('show_times'),
|
||||||
|
'allowExampleUrls': data.get('allow_example_urls'),
|
||||||
|
'checkIpsCodes': data.get('check_ips_codes'),
|
||||||
|
'allowAnyExtensions': data.get('allow_any_extensions'),
|
||||||
|
'txRouting': data.get('tx_routing'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check for empty resource
|
||||||
|
if not fhir_resource_text:
|
||||||
|
return jsonify({'status': 'error', 'message': 'No FHIR resource provided.'}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Generate a unique task ID
|
||||||
|
task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# Store a pending status in the global cache
|
||||||
|
with services.cache_lock:
|
||||||
|
services.validation_results_cache[task_id] = {"status": "pending"}
|
||||||
|
|
||||||
|
# Start the validation process in a new thread
|
||||||
|
app_context = app.app_context()
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=services.validate_in_thread,
|
||||||
|
args=(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version),
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
logger.info(f"Validation task {task_id} started in background thread.")
|
||||||
|
|
||||||
|
return jsonify({"status": "accepted", "message": "Validation started", "task_id": task_id}), 202
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting validation thread: {e}", exc_info=True)
|
||||||
|
return jsonify({'status': 'error', 'message': f'An internal server error occurred while starting the validation: {e}'}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Validation API error: {e}")
|
||||||
|
return jsonify({'status': 'error', 'message': f'An internal server error occurred: {e}'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/check-validation-status/<task_id>', methods=['GET'])
|
||||||
|
@swag_from({
|
||||||
|
'tags': ['Validation'],
|
||||||
|
'summary': 'Check the status of a validation task.',
|
||||||
|
'description': 'Polls for the result of a validation task by its ID. Returns the full validation report when the task is complete.',
|
||||||
|
'security': [{'ApiKeyAuth': []}],
|
||||||
|
'parameters': [
|
||||||
|
{'name': 'task_id', 'in': 'path', 'type': 'string', 'required': True, 'description': 'The unique ID of the validation task.'}
|
||||||
|
],
|
||||||
|
'responses': {
|
||||||
|
'200': {'description': 'Returns the full validation report if the task is complete.'},
|
||||||
|
'202': {'description': 'The validation task is still in progress.'},
|
||||||
|
'404': {'description': 'The specified task ID was not found.'}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
def check_validation_status(task_id):
|
||||||
|
"""
|
||||||
|
This GET endpoint is polled by the frontend to check the status of a validation task.
|
||||||
|
"""
|
||||||
|
with services.cache_lock:
|
||||||
|
task_result = services.validation_results_cache.get(task_id)
|
||||||
|
|
||||||
|
if task_result is None:
|
||||||
|
return jsonify({"status": "error", "message": "Task not found."}), 404
|
||||||
|
|
||||||
|
if task_result["status"] == "pending":
|
||||||
|
return jsonify({"status": "pending", "message": "Validation is still in progress."}), 202
|
||||||
|
|
||||||
|
# Remove the result from the cache after it has been retrieved to prevent a memory leak
|
||||||
|
with services.cache_lock:
|
||||||
|
del services.validation_results_cache[task_id]
|
||||||
|
|
||||||
|
return jsonify({"status": "complete", "result": task_result["result"]}), 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_db():
|
def create_db():
|
||||||
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||||
try:
|
try:
|
||||||
|
@ -17,5 +17,5 @@ services:
|
|||||||
- NODE_PATH=/usr/lib/node_modules
|
- NODE_PATH=/usr/lib/node_modules
|
||||||
- APP_MODE=standalone
|
- APP_MODE=standalone
|
||||||
- APP_BASE_URL=http://localhost:5000
|
- APP_BASE_URL=http://localhost:5000
|
||||||
- HAPI_FHIR_URL=https://fhir.hl7.org.au/aucore/fhir/DEFAULT/
|
- HAPI_FHIR_URL=https://smile.sparked-fhir.com/aucore/fhir/DEFAULT/
|
||||||
command: supervisord -c /etc/supervisord.conf
|
command: supervisord -c /etc/supervisord.conf
|
||||||
|
55
forms.py
55
forms.py
@ -312,3 +312,58 @@ class IgYamlForm(FlaskForm):
|
|||||||
"""Form to select IGs for YAML generation."""
|
"""Form to select IGs for YAML generation."""
|
||||||
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
|
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
|
||||||
generate_yaml = SubmitField('Generate YAML')
|
generate_yaml = SubmitField('Generate YAML')
|
||||||
|
|
||||||
|
# --- New ValidationForm class for the new page ---
|
||||||
|
class ValidationForm(FlaskForm):
|
||||||
|
"""Form for validating a single FHIR resource with various options."""
|
||||||
|
# Added fields to match the HTML template
|
||||||
|
package_name = StringField('Package Name', validators=[Optional()])
|
||||||
|
version = StringField('Package Version', validators=[Optional()])
|
||||||
|
|
||||||
|
# Main content fields
|
||||||
|
fhir_resource = TextAreaField('FHIR Resource (JSON or XML)', validators=[InputRequired()],
|
||||||
|
render_kw={'placeholder': 'Paste your FHIR JSON here...'})
|
||||||
|
fhir_version = SelectField('FHIR Version', choices=[
|
||||||
|
('4.0.1', 'R4 (4.0.1)'),
|
||||||
|
('3.0.2', 'STU3 (3.0.2)'),
|
||||||
|
('5.0.0', 'R5 (5.0.0)')
|
||||||
|
], default='4.0.1', validators=[DataRequired()])
|
||||||
|
|
||||||
|
# Flags and options from validator settings.pdf
|
||||||
|
do_native = BooleanField('Native Validation (doNative)')
|
||||||
|
hint_about_must_support = BooleanField('Must Support (hintAboutMustSupport)')
|
||||||
|
assume_valid_rest_references = BooleanField('Assume Valid Rest References (assumeValidRestReferences)')
|
||||||
|
no_extensible_binding_warnings = BooleanField('Extensible Binding Warnings (noExtensibleBindingWarnings)')
|
||||||
|
show_times = BooleanField('Show Times (-show-times)')
|
||||||
|
allow_example_urls = BooleanField('Allow Example URLs (-allow-example-urls)')
|
||||||
|
check_ips_codes = BooleanField('Check IPS Codes (-check-ips-codes)')
|
||||||
|
allow_any_extensions = BooleanField('Allow Any Extensions (-allow-any-extensions)')
|
||||||
|
tx_routing = BooleanField('Show Terminology Routing (-tx-routing)')
|
||||||
|
|
||||||
|
# SNOMED CT options
|
||||||
|
snomed_ct_version = SelectField('Select SNOMED Version', choices=[
|
||||||
|
('', '-- No selection --'),
|
||||||
|
('intl', 'International edition (900000000000207008)'),
|
||||||
|
('us', 'US edition (731000124108)'),
|
||||||
|
('uk', 'United Kingdom Edition (999000041000000102)'),
|
||||||
|
('es', 'Spanish Language Edition (449081005)'),
|
||||||
|
('nl', 'Netherlands Edition (11000146104)'),
|
||||||
|
('ca', 'Canadian Edition (20611000087101)'),
|
||||||
|
('dk', 'Danish Edition (554471000005108)'),
|
||||||
|
('se', 'Swedish Edition (45991000052106)'),
|
||||||
|
('au', 'Australian Edition (32506021000036107)'),
|
||||||
|
('be', 'Belgium Edition (11000172109)')
|
||||||
|
], validators=[Optional()])
|
||||||
|
|
||||||
|
# Text input fields
|
||||||
|
profiles = StringField('Profiles',
|
||||||
|
validators=[Optional()],
|
||||||
|
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'})
|
||||||
|
extensions = StringField('Extensions',
|
||||||
|
validators=[Optional()],
|
||||||
|
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace'})
|
||||||
|
terminology_server = StringField('Terminology Server',
|
||||||
|
validators=[Optional()],
|
||||||
|
render_kw={'placeholder': 'e.g. http://tx.fhir.org'})
|
||||||
|
|
||||||
|
submit = SubmitField('Validate')
|
204
services.py
204
services.py
@ -198,6 +198,203 @@ def safe_parse_version(v_str):
|
|||||||
logger.error(f"Unexpected error in safe_parse_version for '{v_str}': {e}")
|
logger.error(f"Unexpected error in safe_parse_version for '{v_str}': {e}")
|
||||||
return pkg_version.parse("0.0.0a0") # Fallback
|
return pkg_version.parse("0.0.0a0") # Fallback
|
||||||
|
|
||||||
|
# --- New Function to run the FHIR Validator CLI ---
|
||||||
|
def run_validator_cli(resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version, app_config):
|
||||||
|
"""
|
||||||
|
Constructs and runs the Java FHIR Validator CLI command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource_text (str): The FHIR resource/bundle as a string.
|
||||||
|
fhir_version (str): The FHIR version to validate against.
|
||||||
|
igs (list): List of selected IG canonical URLs.
|
||||||
|
profiles (str): Comma-separated list of profile URLs.
|
||||||
|
extensions (str): Comma-separated list of extension URLs.
|
||||||
|
terminology_server (str): The URL of the terminology server.
|
||||||
|
flags (dict): Dictionary of boolean flags for validation.
|
||||||
|
snomed_ct_version (str): The SNOMED CT version to use.
|
||||||
|
app_config (dict): The Flask application configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: A dictionary containing the validation output and status.
|
||||||
|
"""
|
||||||
|
temp_dir = tempfile.mkdtemp(dir=app_config['VALIDATION_LOG_DIR'])
|
||||||
|
|
||||||
|
# Store the temp directory in the app config for the download endpoint to find it.
|
||||||
|
app_config['VALIDATION_TEMP_DIR'] = temp_dir
|
||||||
|
|
||||||
|
temp_resource_path = os.path.join(temp_dir, f'resource_{uuid.uuid4()}.json')
|
||||||
|
try:
|
||||||
|
with open(temp_resource_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(resource_text)
|
||||||
|
except Exception as e:
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
return {'status': 'error', 'message': f'Failed to write temporary resource file: {e}'}
|
||||||
|
|
||||||
|
output_json_path = os.path.join(temp_dir, 'validation-report.json')
|
||||||
|
output_html_path = os.path.join(temp_dir, 'validation-report.html')
|
||||||
|
|
||||||
|
command = [
|
||||||
|
'java',
|
||||||
|
'-jar', os.environ.get('JAVA_VALIDATOR_PATH', '/app/validator_cli/validator_cli.jar'),
|
||||||
|
temp_resource_path,
|
||||||
|
'-version', fhir_version,
|
||||||
|
'-output', output_json_path,
|
||||||
|
'-html-output', output_html_path,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add flags
|
||||||
|
if flags.get('doNative'):
|
||||||
|
command.append('-do-native')
|
||||||
|
if flags.get('hintAboutMustSupport'):
|
||||||
|
command.append('-hint-about-must-support')
|
||||||
|
if flags.get('assumeValidRestReferences'):
|
||||||
|
command.append('-assume-valid-rest-references')
|
||||||
|
if flags.get('noExtensibleBindingWarnings'):
|
||||||
|
command.append('-no-extensible-binding-warnings')
|
||||||
|
if flags.get('showTimes'):
|
||||||
|
command.append('-show-times')
|
||||||
|
if flags.get('allowExampleUrls'):
|
||||||
|
command.append('-allow-example-urls')
|
||||||
|
if flags.get('checkIpsCodes'):
|
||||||
|
command.append('-check-ips-codes')
|
||||||
|
if flags.get('allowAnyExtensions'):
|
||||||
|
command.append('-allow-any-extensions')
|
||||||
|
if flags.get('txRouting'):
|
||||||
|
command.append('-tx-routing')
|
||||||
|
|
||||||
|
# Add other options
|
||||||
|
if igs:
|
||||||
|
for ig in igs:
|
||||||
|
command.extend(['-ig', ig])
|
||||||
|
if profiles:
|
||||||
|
command.extend(['-profile', profiles])
|
||||||
|
if extensions:
|
||||||
|
command.extend(['-extension', extensions])
|
||||||
|
if terminology_server:
|
||||||
|
command.extend(['-tx', terminology_server])
|
||||||
|
if snomed_ct_version:
|
||||||
|
command.extend(['-sct', snomed_ct_version])
|
||||||
|
|
||||||
|
logger.info(f"Running validator with command: {' '.join(command)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
command,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
timeout=300,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = result.stdout
|
||||||
|
stderr = result.stderr
|
||||||
|
|
||||||
|
logger.debug(f"Validator stdout:\n{stdout}")
|
||||||
|
logger.debug(f"Validator stderr:\n{stderr}")
|
||||||
|
|
||||||
|
# Check for errors in the stdout and stderr
|
||||||
|
if result.returncode != 0 and not os.path.exists(output_json_path):
|
||||||
|
return_dict = {
|
||||||
|
'status': 'error',
|
||||||
|
'output': stderr or "Validation process failed with an unknown error.",
|
||||||
|
'file_paths': {},
|
||||||
|
'errors': [stderr or "Validation process failed with an unknown error."],
|
||||||
|
'warnings': [],
|
||||||
|
'summary': {'error_count': 1, 'warning_count': 0},
|
||||||
|
'results': {}
|
||||||
|
}
|
||||||
|
return return_dict
|
||||||
|
|
||||||
|
# Now, try to read the JSON file produced by the validator
|
||||||
|
if os.path.exists(output_json_path):
|
||||||
|
with open(output_json_path, 'r', encoding='utf-8') as f:
|
||||||
|
json_report = json.load(f)
|
||||||
|
|
||||||
|
# Process the JSON report to get a structured summary
|
||||||
|
error_count = 0
|
||||||
|
warning_count = 0
|
||||||
|
errors_list = []
|
||||||
|
warnings_list = []
|
||||||
|
detailed_results = {}
|
||||||
|
is_valid = True
|
||||||
|
|
||||||
|
for issue in json_report.get('issue', []):
|
||||||
|
severity = issue.get('severity')
|
||||||
|
diagnostics = issue.get('diagnostics') or issue.get('details', {}).get('text') or 'No details provided.'
|
||||||
|
resource_expression = issue.get('expression', ['unknown'])[0]
|
||||||
|
|
||||||
|
# Determine the resource ID and type from the expression, e.g., "AllergyIntolerance" or "AllergyIntolerance[0].extension[0]"
|
||||||
|
resource_id = resource_expression.split('[')[0].split('.')[0]
|
||||||
|
|
||||||
|
if resource_id not in detailed_results:
|
||||||
|
detailed_results[resource_id] = {'valid': True, 'details': []}
|
||||||
|
|
||||||
|
detail = {
|
||||||
|
'issue': diagnostics,
|
||||||
|
'severity': severity,
|
||||||
|
'description': issue.get('details', {}).get('text', diagnostics)
|
||||||
|
}
|
||||||
|
detailed_results[resource_id]['details'].append(detail)
|
||||||
|
|
||||||
|
if severity == 'error':
|
||||||
|
error_count += 1
|
||||||
|
errors_list.append(diagnostics)
|
||||||
|
is_valid = False
|
||||||
|
detailed_results[resource_id]['valid'] = False
|
||||||
|
elif severity == 'warning':
|
||||||
|
warning_count += 1
|
||||||
|
warnings_list.append(diagnostics)
|
||||||
|
|
||||||
|
# Determine the overall status
|
||||||
|
status = 'success'
|
||||||
|
if error_count > 0:
|
||||||
|
status = 'error'
|
||||||
|
elif warning_count > 0:
|
||||||
|
status = 'warning'
|
||||||
|
|
||||||
|
# Build the final return dictionary
|
||||||
|
return_dict = {
|
||||||
|
'status': status,
|
||||||
|
'valid': is_valid,
|
||||||
|
'output': stdout,
|
||||||
|
'file_paths': {
|
||||||
|
'json': output_json_path,
|
||||||
|
'html': output_html_path
|
||||||
|
},
|
||||||
|
'errors': errors_list,
|
||||||
|
'warnings': warnings_list,
|
||||||
|
'summary': {
|
||||||
|
'error_count': error_count,
|
||||||
|
'warning_count': warning_count
|
||||||
|
},
|
||||||
|
'results': detailed_results
|
||||||
|
}
|
||||||
|
return return_dict
|
||||||
|
else:
|
||||||
|
# Fallback if validator completed but didn't produce a file
|
||||||
|
return_dict = {
|
||||||
|
'status': 'error',
|
||||||
|
'valid': False,
|
||||||
|
'output': stdout,
|
||||||
|
'file_paths': {},
|
||||||
|
'errors': ["Validator failed to produce the expected output files."],
|
||||||
|
'warnings': [],
|
||||||
|
'summary': {'error_count': 1, 'warning_count': 0},
|
||||||
|
'results': {}
|
||||||
|
}
|
||||||
|
return return_dict
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {'status': 'error', 'message': 'Java command or validator JAR not found. Please check paths.'}
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return {'status': 'error', 'message': 'Validation timed out.'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': 'error', 'message': f'An unexpected error occurred during validation: {e}'}
|
||||||
|
finally:
|
||||||
|
# NOTE: We no longer delete the temporary directory here.
|
||||||
|
# It is the responsibility of the `clear-validation-results` endpoint.
|
||||||
|
pass
|
||||||
|
|
||||||
# --- MODIFIED FUNCTION with Enhanced Logging ---
|
# --- MODIFIED FUNCTION with Enhanced Logging ---
|
||||||
def get_additional_registries():
|
def get_additional_registries():
|
||||||
"""Fetches the list of additional FHIR IG registries from the master feed."""
|
"""Fetches the list of additional FHIR IG registries from the master feed."""
|
||||||
@ -1108,15 +1305,16 @@ validation_results_cache = {}
|
|||||||
# Thread lock to safely manage access to the cache
|
# Thread lock to safely manage access to the cache
|
||||||
cache_lock = threading.Lock()
|
cache_lock = threading.Lock()
|
||||||
|
|
||||||
def validate_in_thread(app_context, task_id, package_name, version, sample_data):
|
def validate_in_thread(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version):
|
||||||
"""
|
"""
|
||||||
Wrapper function to run the blocking validator in a separate thread.
|
Wrapper function to run the blocking validator in a separate thread.
|
||||||
It acquires an app context and then calls the main validation logic.
|
It acquires an app context and then calls the main validation logic.
|
||||||
"""
|
"""
|
||||||
with app_context:
|
with app_context:
|
||||||
logger.info(f"Starting validation thread for task ID: {task_id}")
|
logger.info(f"Starting validation thread for task ID: {task_id}")
|
||||||
# Call the core blocking validation function
|
|
||||||
result = run_java_validator(package_name, version, sample_data)
|
# Call the core blocking validation function, passing all arguments
|
||||||
|
result = run_validator_cli(fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version, current_app.config)
|
||||||
|
|
||||||
# Safely update the shared cache with the final result
|
# Safely update the shared cache with the final result
|
||||||
with cache_lock:
|
with cache_lock:
|
||||||
|
@ -22,8 +22,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Validation Form</div>
|
<div class="card-header">Validation Form</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="validationForm">
|
<form id="validationForm" method="POST" action="/api/validate">
|
||||||
{{ form.hidden_tag() }}
|
{{ form.csrf_token }}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<small class="form-text text-muted">
|
<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>).
|
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
||||||
@ -46,6 +47,7 @@
|
|||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
||||||
{{ form.version(class="form-control", id=form.version.id) }}
|
{{ form.version(class="form-control", id=form.version.id) }}
|
||||||
@ -53,35 +55,110 @@
|
|||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<!-- New Fields from ValidationForm -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
|
<label for="fhir_resource" class="form-label">FHIR Resource (JSON or XML)</label>
|
||||||
|
{{ form.fhir_resource(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="fhir_version" class="form-label">FHIR Version</label>
|
||||||
|
{{ form.fhir_version(class="form-select") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h5>Validation Flags</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
{{ form.include_dependencies(class="form-check-input") }}
|
{{ form.do_native(class="form-check-input") }}
|
||||||
{{ form.include_dependencies.label(class="form-check-label") }}
|
<label class="form-check-label" for="do_native">{{ form.do_native.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.hint_about_must_support(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="hint_about_must_support">{{ form.hint_about_must_support.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.assume_valid_rest_references(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="assume_valid_rest_references">{{ form.assume_valid_rest_references.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.no_extensible_binding_warnings(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="no_extensible_binding_warnings">{{ form.no_extensible_binding_warnings.label }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="col-md-6">
|
||||||
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
|
<div class="form-check">
|
||||||
{{ form.mode(class="form-select") }}
|
{{ form.show_times(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="show_times">{{ form.show_times.label }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="form-check">
|
||||||
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
|
{{ form.allow_example_urls(class="form-check-input") }}
|
||||||
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
<label class="form-check-label" for="allow_example_urls">{{ form.allow_example_urls.label }}</label>
|
||||||
{% for error in form.sample_input.errors %}
|
|
||||||
<div class="text-danger">{{ error }}</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between">
|
<div class="form-check">
|
||||||
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
|
{{ form.check_ips_codes(class="form-check-input") }}
|
||||||
<button type="button" class="btn btn-secondary d-none" id="clearResultsButton">Clear Results</button>
|
<label class="form-check-label" for="check_ips_codes">{{ form.check_ips_codes.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.allow_any_extensions(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="allow_any_extensions">{{ form.allow_any_extensions.label }}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
{{ form.tx_routing(class="form-check-input") }}
|
||||||
|
<label class="form-check-label" for="tx_routing">{{ form.tx_routing.label }}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<h5>Other Settings</h5>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="profiles" class="form-label">{{ form.profiles.label }}</label>
|
||||||
|
{{ form.profiles(class="form-control") }}
|
||||||
|
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="extensions" class="form-label">{{ form.extensions.label }}</label>
|
||||||
|
{{ form.extensions(class="form-control") }}
|
||||||
|
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="terminology_server" class="form-label">{{ form.terminology_server.label }}</label>
|
||||||
|
{{ form.terminology_server(class="form-control") }}
|
||||||
|
<small class="form-text text-muted">e.g., http://tx.fhir.org</small>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="snomed_ct_version" class="form-label">{{ form.snomed_ct_version.label }}</label>
|
||||||
|
{{ form.snomed_ct_version(class="form-select") }}
|
||||||
|
<small class="form-text text-muted">Select a SNOMED CT edition.</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<button type="submit" id="validateButton" class="btn btn-primary btn-lg">
|
||||||
|
Validate Resource
|
||||||
|
</button>
|
||||||
|
<button type="button" id="clearResultsButton" class="btn btn-secondary btn-lg d-none">
|
||||||
|
Clear Results
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt-4" id="validationResult" style="display: none;">
|
<div id="validationResult" class="card mt-4" style="display: none;">
|
||||||
<div class="card-header">Validation Report</div>
|
<div class="card-header">Validation Results</div>
|
||||||
<div class="card-body" id="validationContent">
|
<div class="card-body" id="validationContent">
|
||||||
|
<!-- Results will be displayed here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -94,6 +171,7 @@
|
|||||||
const spinnerOverlay = document.getElementById('spinner-overlay');
|
const spinnerOverlay = document.getElementById('spinner-overlay');
|
||||||
const spinnerMessage = document.getElementById('spinner-message');
|
const spinnerMessage = document.getElementById('spinner-message');
|
||||||
const spinnerContainer = document.getElementById('spinner-animation');
|
const spinnerContainer = document.getElementById('spinner-animation');
|
||||||
|
const perPage = 10;
|
||||||
|
|
||||||
// Function to load the Lottie animation based on theme
|
// Function to load the Lottie animation based on theme
|
||||||
function loadLottieAnimation(theme) {
|
function loadLottieAnimation(theme) {
|
||||||
@ -136,17 +214,18 @@
|
|||||||
|
|
||||||
// --- Frontend logic starts here
|
// --- Frontend logic starts here
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const packageSelect = document.getElementById('packageSelect');
|
const form = document.getElementById('validationForm');
|
||||||
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
|
||||||
const versionInput = document.getElementById('{{ form.version.id }}');
|
|
||||||
const validateButton = document.getElementById('validateButton');
|
const validateButton = document.getElementById('validateButton');
|
||||||
const clearResultsButton = document.getElementById('clearResultsButton');
|
const clearResultsButton = document.getElementById('clearResultsButton');
|
||||||
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
|
||||||
const validationResult = document.getElementById('validationResult');
|
const validationResult = document.getElementById('validationResult');
|
||||||
const validationContent = document.getElementById('validationContent');
|
const validationContent = document.getElementById('validationContent');
|
||||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
const perPage = 10;
|
const packageSelect = document.getElementById('packageSelect');
|
||||||
|
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
||||||
|
const versionInput = document.getElementById('{{ form.version.id }}');
|
||||||
|
const fhirResourceInput = document.getElementById('fhir_resource');
|
||||||
|
|
||||||
|
// Function to update package fields when a package is selected from the dropdown
|
||||||
function updatePackageFields(select) {
|
function updatePackageFields(select) {
|
||||||
const value = select.value;
|
const value = select.value;
|
||||||
if (!packageNameInput || !versionInput) return;
|
if (!packageNameInput || !versionInput) return;
|
||||||
@ -165,37 +244,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (packageNameInput.value && versionInput.value) {
|
// Sanitize text to prevent XSS
|
||||||
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
|
||||||
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
|
|
||||||
if (optionExists) {
|
|
||||||
packageSelect.value = currentValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packageSelect) {
|
|
||||||
packageSelect.addEventListener('change', function() {
|
|
||||||
updatePackageFields(this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sampleInput) {
|
|
||||||
sampleInput.addEventListener('input', function() {
|
|
||||||
try {
|
|
||||||
if (this.value.trim()) {
|
|
||||||
JSON.parse(this.value);
|
|
||||||
this.classList.remove('is-invalid');
|
|
||||||
this.classList.add('is-valid');
|
|
||||||
} else {
|
|
||||||
this.classList.remove('is-valid', 'is-invalid');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.classList.remove('is-valid');
|
|
||||||
this.classList.add('is-invalid');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeText(text) {
|
function sanitizeText(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
@ -226,7 +275,7 @@
|
|||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
||||||
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
||||||
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
ul.appendChild(li);
|
ul.appendChild(li);
|
||||||
@ -246,7 +295,7 @@
|
|||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
||||||
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
||||||
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
remainingUl.appendChild(li);
|
remainingUl.appendChild(li);
|
||||||
@ -275,7 +324,7 @@
|
|||||||
clearResultsButton.classList.remove('d-none');
|
clearResultsButton.classList.remove('d-none');
|
||||||
|
|
||||||
if (data.file_paths) {
|
if (data.file_paths) {
|
||||||
// Fix: Extract just the filename to construct the download URL
|
// Use the paths returned from the server to create download links
|
||||||
const jsonFileName = data.file_paths.json.split('/').pop();
|
const jsonFileName = data.file_paths.json.split('/').pop();
|
||||||
const htmlFileName = data.file_paths.html.split('/').pop();
|
const htmlFileName = data.file_paths.html.split('/').pop();
|
||||||
const downloadDiv = document.createElement('div');
|
const downloadDiv = document.createElement('div');
|
||||||
@ -398,10 +447,11 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function runBlockingValidation() {
|
function runBlockingValidation(e) {
|
||||||
const packageName = packageNameInput.value;
|
e.preventDefault(); // This is the crucial line to prevent default form submission
|
||||||
const version = versionInput.value;
|
const packageName = document.getElementById('{{ form.package_name.id }}').value;
|
||||||
const sampleData = sampleInput.value.trim();
|
const version = document.getElementById('{{ form.version.id }}').value;
|
||||||
|
const sampleData = document.getElementById('fhir_resource').value.trim();
|
||||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
||||||
|
|
||||||
if (!packageName || !version) {
|
if (!packageName || !version) {
|
||||||
@ -412,31 +462,40 @@
|
|||||||
|
|
||||||
if (!sampleData) {
|
if (!sampleData) {
|
||||||
validationResult.style.display = 'block';
|
validationResult.style.display = 'block';
|
||||||
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
|
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON or XML.</div>';
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
JSON.parse(sampleData);
|
|
||||||
} catch (e) {
|
|
||||||
validationResult.style.display = 'block';
|
|
||||||
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
|
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
|
||||||
|
|
||||||
fetch('/api/validate-sample', {
|
// Collect all form data including the new fields
|
||||||
|
const data = {
|
||||||
|
package_name: packageName,
|
||||||
|
version: version,
|
||||||
|
fhir_resource: sampleData,
|
||||||
|
fhir_version: document.getElementById('fhir_version').value,
|
||||||
|
do_native: document.getElementById('do_native').checked,
|
||||||
|
hint_about_must_support: document.getElementById('hint_about_must_support').checked,
|
||||||
|
assume_valid_rest_references: document.getElementById('assume_valid_rest_references').checked,
|
||||||
|
no_extensible_binding_warnings: document.getElementById('no_extensible_binding_warnings').checked,
|
||||||
|
show_times: document.getElementById('show_times').checked,
|
||||||
|
allow_example_urls: document.getElementById('allow_example_urls').checked,
|
||||||
|
check_ips_codes: document.getElementById('check_ips_codes').checked,
|
||||||
|
allow_any_extensions: document.getElementById('allow_any_extensions').checked,
|
||||||
|
tx_routing: document.getElementById('tx_routing').checked,
|
||||||
|
snomed_ct_version: document.getElementById('snomed_ct_version').value,
|
||||||
|
profiles: document.getElementById('profiles').value,
|
||||||
|
extensions: document.getElementById('extensions').value,
|
||||||
|
terminology_server: document.getElementById('terminology_server').value
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/api/validate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-CSRFToken': csrfToken
|
'X-CSRFToken': csrfToken
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(data)
|
||||||
package_name: packageName,
|
|
||||||
version: version,
|
|
||||||
sample_data: sampleData
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.status === 202) {
|
if (response.status === 202) {
|
||||||
@ -462,9 +521,9 @@
|
|||||||
toggleSpinner(false);
|
toggleSpinner(false);
|
||||||
console.error('Validation failed:', error);
|
console.error('Validation failed:', error);
|
||||||
validationResult.style.display = 'block';
|
validationResult.style.display = 'block';
|
||||||
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred: ${sanitizeText(error.message)}</div>`;
|
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
clearResultsButton.addEventListener('click', function() {
|
clearResultsButton.addEventListener('click', function() {
|
||||||
fetch('/api/clear-validation-results', {
|
fetch('/api/clear-validation-results', {
|
||||||
@ -487,9 +546,40 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (packageSelect) {
|
||||||
|
packageSelect.addEventListener('change', function() {
|
||||||
|
updatePackageFields(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (validateButton) {
|
if (validateButton) {
|
||||||
validateButton.addEventListener('click', function() {
|
validateButton.addEventListener('click', function(e) {
|
||||||
runBlockingValidation();
|
runBlockingValidation(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packageNameInput.value && versionInput.value) {
|
||||||
|
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
||||||
|
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
|
||||||
|
if (optionExists) {
|
||||||
|
packageSelect.value = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fhirResourceInput) {
|
||||||
|
fhirResourceInput.addEventListener('input', function() {
|
||||||
|
try {
|
||||||
|
if (this.value.trim()) {
|
||||||
|
JSON.parse(this.value);
|
||||||
|
this.classList.remove('is-invalid');
|
||||||
|
this.classList.add('is-valid');
|
||||||
|
} else {
|
||||||
|
this.classList.remove('is-valid', 'is-invalid');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.classList.remove('is-valid');
|
||||||
|
this.classList.add('is-invalid');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user