mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 16:19:59 +00:00
376 lines
18 KiB
HTML
376 lines
18 KiB
HTML
<!-- templates/validate_sample.html -->
|
|
{% 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>
|
|
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
|
<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>
|
|
----------------------------------------------------------------remove the buttons----------------------------------------------------->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container mt-4">
|
|
<div class="card">
|
|
<div class="card-header">Validation Form</div>
|
|
<div class="card-body">
|
|
<form 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">
|
|
<option value="">-- Select a Package --</option>
|
|
{% if packages %}
|
|
{% for pkg in packages %}
|
|
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
|
|
{% endfor %}
|
|
{% else %}
|
|
<option value="" disabled>No packages available</option>
|
|
{% endif %}
|
|
</select>
|
|
</div>
|
|
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
|
|
{{ form.package_name(class="form-control", id=form.package_name.id) }}
|
|
{% 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", id=form.version.id) }}
|
|
{% 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>
|
|
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-4" id="validationResult" style="display: none;">
|
|
<div class="card-header">Validation Report</div>
|
|
<div class="card-body" id="validationContent">
|
|
<!-- Results will be populated dynamically -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const packageSelect = document.getElementById('packageSelect');
|
|
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
|
const versionInput = document.getElementById('{{ form.version.id }}');
|
|
const validateButton = document.getElementById('validateButton');
|
|
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
|
const validationResult = document.getElementById('validationResult');
|
|
const validationContent = document.getElementById('validationContent');
|
|
|
|
// Debug DOM elements
|
|
console.log('Package Select:', packageSelect ? 'Found' : 'Missing');
|
|
console.log('Package Name Input:', packageNameInput ? 'Found' : 'Missing');
|
|
console.log('Version Input:', versionInput ? 'Found' : 'Missing');
|
|
|
|
// Update package_name and version fields from dropdown
|
|
function updatePackageFields(select) {
|
|
const value = select.value;
|
|
console.log('Selected package:', value);
|
|
if (!packageNameInput || !versionInput) {
|
|
console.error('Input fields not found');
|
|
return;
|
|
}
|
|
if (value) {
|
|
const [name, version] = value.split('#');
|
|
if (name && version) {
|
|
packageNameInput.value = name;
|
|
versionInput.value = version;
|
|
console.log('Updated fields:', { packageName: name, version });
|
|
} else {
|
|
console.warn('Invalid package format:', value);
|
|
packageNameInput.value = '';
|
|
versionInput.value = '';
|
|
}
|
|
} else {
|
|
packageNameInput.value = '';
|
|
versionInput.value = '';
|
|
console.log('Cleared fields: no package selected');
|
|
}
|
|
}
|
|
|
|
// Initialize dropdown based on form values
|
|
if (packageNameInput && versionInput && packageNameInput.value && versionInput.value) {
|
|
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
|
if (packageSelect.querySelector(`option[value="${currentValue}"]`)) {
|
|
packageSelect.value = currentValue;
|
|
console.log('Initialized dropdown with:', currentValue);
|
|
}
|
|
}
|
|
|
|
// Debug dropdown options
|
|
console.log('Dropdown options:', Array.from(packageSelect.options).map(opt => opt.value));
|
|
|
|
// Attach event listener for package selection
|
|
if (packageSelect) {
|
|
packageSelect.addEventListener('change', function() {
|
|
updatePackageFields(this);
|
|
});
|
|
}
|
|
|
|
// Client-side JSON validation preview
|
|
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');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check HAPI server status
|
|
function checkHapiStatus() {
|
|
return fetch('/fhir/metadata', { method: 'GET' })
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('HAPI server unavailable');
|
|
}
|
|
return true;
|
|
})
|
|
.catch(error => {
|
|
console.warn('HAPI status check failed:', error.message);
|
|
validationContent.innerHTML = '<div class="alert alert-warning">HAPI server unavailable; using local validation.</div>';
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// Sanitize text to prevent XSS
|
|
function sanitizeText(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Validate button handler
|
|
if (validateButton) {
|
|
validateButton.addEventListener('click', function() {
|
|
const packageName = packageNameInput.value;
|
|
const version = versionInput.value;
|
|
const sampleData = sampleInput.value.trim();
|
|
const includeDependencies = document.getElementById('{{ form.include_dependencies.id }}').checked;
|
|
|
|
if (!packageName || !version) {
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = '<div class="alert alert-danger">Please select a package and version.</div>';
|
|
return;
|
|
}
|
|
|
|
if (!sampleData) {
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</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;
|
|
}
|
|
|
|
validationResult.style.display = 'block';
|
|
validationContent.innerHTML = '<div class="text-center py-3"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Validating...</span></div></div>';
|
|
|
|
// Check HAPI status before validation
|
|
checkHapiStatus().then(() => {
|
|
console.log('Sending request to /api/validate-sample with:', {
|
|
package_name: packageName,
|
|
version: version,
|
|
include_dependencies: includeDependencies,
|
|
sample_data_length: sampleData.length
|
|
});
|
|
|
|
fetch('/api/validate-sample', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ form.csrf_token._value() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
package_name: packageName,
|
|
version: version,
|
|
include_dependencies: includeDependencies,
|
|
sample_data: sampleData
|
|
})
|
|
})
|
|
.then(response => {
|
|
console.log('Server response status:', response.status, response.statusText);
|
|
if (!response.ok) {
|
|
return response.text().then(text => {
|
|
console.error('Server response text:', text.substring(0, 200) + (text.length > 200 ? '...' : ''));
|
|
throw new Error(`HTTP error ${response.status}: ${text.substring(0, 100)}...`);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Validation response:', data);
|
|
validationContent.innerHTML = '';
|
|
let hasContent = false;
|
|
|
|
// Display profile information
|
|
if (data.results) {
|
|
const profileDiv = document.createElement('div');
|
|
profileDiv.className = 'mb-3';
|
|
const profiles = new Set();
|
|
Object.values(data.results).forEach(result => {
|
|
if (result.profile) profiles.add(result.profile);
|
|
});
|
|
if (profiles.size > 0) {
|
|
profileDiv.innerHTML = `
|
|
<h5>Validated Against Profile${profiles.size > 1 ? 's' : ''}</h5>
|
|
<ul>
|
|
${Array.from(profiles).map(p => `<li><a href="${sanitizeText(p)}" target="_blank">${sanitizeText(p.split('/').pop())}</a></li>`).join('')}
|
|
</ul>
|
|
`;
|
|
validationContent.appendChild(profileDiv);
|
|
hasContent = true;
|
|
}
|
|
}
|
|
|
|
// Display errors
|
|
if (data.errors && data.errors.length > 0) {
|
|
hasContent = true;
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'alert alert-danger';
|
|
errorDiv.innerHTML = `
|
|
<h5>Errors</h5>
|
|
<ul>
|
|
${data.errors.map(e => `<li>${sanitizeText(typeof e === 'string' ? e : e.message)}</li>`).join('')}
|
|
</ul>
|
|
`;
|
|
validationContent.appendChild(errorDiv);
|
|
}
|
|
|
|
// Display warnings
|
|
if (data.warnings && data.warnings.length > 0) {
|
|
hasContent = true;
|
|
const warningDiv = document.createElement('div');
|
|
warningDiv.className = 'alert alert-warning';
|
|
warningDiv.innerHTML = `
|
|
<h5>Warnings</h5>
|
|
<ul>
|
|
${data.warnings.map(w => {
|
|
const isMustSupport = w.includes('Must Support');
|
|
return `<li>${sanitizeText(typeof w === 'string' ? w : w.message)}${isMustSupport ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}</li>`;
|
|
}).join('')}
|
|
</ul>
|
|
`;
|
|
validationContent.appendChild(warningDiv);
|
|
}
|
|
|
|
// Detailed results with collapsible sections
|
|
if (data.results) {
|
|
hasContent = true;
|
|
const resultsDiv = document.createElement('div');
|
|
resultsDiv.innerHTML = '<h5>Detailed Results</h5>';
|
|
let index = 0;
|
|
for (const [resourceId, result] of Object.entries(data.results)) {
|
|
const collapseId = `result-collapse-${index++}`;
|
|
resultsDiv.innerHTML += `
|
|
<div class="card mb-2">
|
|
<div class="card-header" role="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
|
<h6 class="mb-0">${sanitizeText(resourceId)}</h6>
|
|
<p class="mb-0"><strong>Valid:</strong> ${result.valid ? 'Yes' : 'No'}</p>
|
|
</div>
|
|
<div id="${collapseId}" class="collapse">
|
|
<div class="card-body">
|
|
${result.details && result.details.length > 0 ? `
|
|
<ul>
|
|
${result.details.map(d => `
|
|
<li class="${d.severity === 'error' ? 'text-danger' : d.severity === 'warning' ? 'text-warning' : 'text-info'}">
|
|
${sanitizeText(d.issue)}
|
|
${d.description && d.description !== d.issue ? `<br><small class="text-muted">${sanitizeText(d.description)}</small>` : ''}
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
` : '<p>No additional details.</p>'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
validationContent.appendChild(resultsDiv);
|
|
}
|
|
|
|
// Summary
|
|
const summaryDiv = document.createElement('div');
|
|
summaryDiv.className = 'alert alert-info';
|
|
summaryDiv.innerHTML = `
|
|
<h5>Summary</h5>
|
|
<p>Valid: ${data.valid ? 'Yes' : 'No'}</p>
|
|
<p>Errors: ${data.summary.error_count || 0}, Warnings: ${data.summary.warning_count || 0}</p>
|
|
`;
|
|
validationContent.appendChild(summaryDiv);
|
|
hasContent = true;
|
|
|
|
if (!hasContent && data.valid) {
|
|
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
|
|
}
|
|
|
|
// Initialize Bootstrap collapse and tooltips
|
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
|
|
el.addEventListener('click', () => {
|
|
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
|
target.classList.toggle('show');
|
|
});
|
|
});
|
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
|
new bootstrap.Tooltip(el);
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error('Validation error:', error);
|
|
validationContent.innerHTML = `<div class="alert alert-danger">Error during validation: ${sanitizeText(error.message)}</div>`;
|
|
});
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |