mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-07-31 20:25:33 +00:00
Merge pull request #1 from Sudo-JHare/Complies-Imposes
Complies imposes / sample validation
This commit is contained in:
commit
9b904d980f
@ -10,6 +10,8 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY app.py .
|
||||
COPY services.py .
|
||||
COPY forms.py .
|
||||
COPY routes.py .
|
||||
COPY templates/ templates/
|
||||
COPY static/ static/
|
||||
COPY tests/ tests/
|
||||
|
47
README.md
47
README.md
@ -8,10 +8,12 @@ The FHIRFLARE IG Toolkit is a Flask-based web application designed to simplify t
|
||||
|
||||
- **Import IGs**: Download FHIR IG packages and their dependencies from a package registry.
|
||||
- **Manage IGs**: View, process, and delete downloaded IGs, with duplicate detection.
|
||||
- **Process IGs**: Extract resource types, profiles, must-support elements, and examples from IGs.
|
||||
- **Push IGs**: Upload IG resources to a FHIR server with real-time console output.
|
||||
- **API Support**: Provides RESTful API endpoints for importing and pushing IGs.
|
||||
- **Process IGs**: Extract resource types, profiles, must-support elements, examples, and profile relationships (`compliesWithProfile` and `imposeProfile`) from IGs.
|
||||
- **Push IGs**: Upload IG resources to a FHIR server with real-time console output, including validation against imposed profiles.
|
||||
- **Profile Relationships**: Support for `structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile` extensions, with validation and UI display.
|
||||
- **API Support**: Provides RESTful API endpoints for importing and pushing IGs, including profile relationship metadata.
|
||||
- **Live Console**: Displays real-time logs during push operations.
|
||||
- **Configurable Behavior**: Options to enable/disable imposed profile validation and UI display of profile relationships.
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@ -85,18 +87,23 @@ The FHIRFLARE IG Toolkit is built using the following technologies:
|
||||
- Go to the "Manage FHIR Packages" tab to view downloaded IGs.
|
||||
- Process, delete, or view details of IGs. Duplicates are highlighted for resolution.
|
||||
|
||||
3. **Push IGs to a FHIR Server**:
|
||||
3. **View Processed IGs**:
|
||||
- After processing an IG, view its details, including resource types, profiles, must-support elements, examples, and profile relationships (`compliesWithProfile` and `imposeProfile`).
|
||||
- Profile relationships are displayed if enabled via the `DISPLAY_PROFILE_RELATIONSHIPS` configuration.
|
||||
|
||||
4. **Push IGs to a FHIR Server**:
|
||||
- Navigate to the "Push IGs" tab.
|
||||
- Select a package, enter a FHIR server URL (e.g., `http://hapi.fhir.org/baseR4`), and choose whether to include dependencies.
|
||||
- Click "Push to FHIR Server" to upload resources, with progress shown in the live console.
|
||||
- Click "Push to FHIR Server" to upload resources, with validation against imposed profiles (if enabled via `VALIDATE_IMPOSED_PROFILES`) and progress shown in the live console.
|
||||
|
||||
4. **API Usage**:
|
||||
5. **API Usage**:
|
||||
- **Import IG**: `POST /api/import-ig`
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/import-ig \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"package_name": "hl7.fhir.us.core", "version": "3.1.1", "api_key": "your-api-key"}'
|
||||
```
|
||||
Response includes `complies_with_profiles` and `imposed_profiles` if present.
|
||||
- **Push IG**: `POST /api/push-ig`
|
||||
```bash
|
||||
curl -X POST http://localhost:5000/api/push-ig \
|
||||
@ -104,6 +111,18 @@ The FHIRFLARE IG Toolkit is built using the following technologies:
|
||||
-H "Accept: application/x-ndjson" \
|
||||
-d '{"package_name": "hl7.fhir.us.core", "version": "3.1.1", "fhir_server_url": "http://hapi.fhir.org/baseR4", "include_dependencies": true, "api_key": "your-api-key"}'
|
||||
```
|
||||
Resources are validated against imposed profiles before pushing.
|
||||
|
||||
## Configuration Options
|
||||
|
||||
- **`VALIDATE_IMPOSED_PROFILES`**: Set to `True` (default) to validate resources against imposed profiles during the push operation. Set to `False` to skip this validation.
|
||||
```python
|
||||
app.config['VALIDATE_IMPOSED_PROFILES'] = False
|
||||
```
|
||||
- **`DISPLAY_PROFILE_RELATIONSHIPS`**: Set to `True` (default) to display `compliesWithProfile` and `imposeProfile` relationships in the UI. Set to `False` to hide them.
|
||||
```python
|
||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = False
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
@ -147,11 +166,11 @@ The test suite includes 27 test cases covering the following areas:
|
||||
- Import IG page (`/import-ig`): Form rendering and submission (success, failure, invalid input).
|
||||
- Manage IGs page (`/view-igs`): Rendering with and without packages.
|
||||
- Push IGs page (`/push-igs`): Rendering and live console.
|
||||
- View Processed IG page (`/view-ig/<id>`): Rendering processed IG details.
|
||||
- View Processed IG page (`/view-ig/<id>`): Rendering processed IG details, including profile relationships.
|
||||
|
||||
- **API Endpoints**:
|
||||
- `POST /api/import-ig`: Success, invalid API key, missing parameters.
|
||||
- `POST /api/push-ig`: Success, invalid API key, package not found.
|
||||
- `POST /api/import-ig`: Success, invalid API key, missing parameters, profile relationships in response.
|
||||
- `POST /api/push-ig`: Success, invalid API key, package not found, imposed profile validation.
|
||||
- `GET /get-structure`: Fetching structure definitions (success, not found).
|
||||
- `GET /get-example`: Fetching example content (success, invalid path).
|
||||
|
||||
@ -161,7 +180,7 @@ The test suite includes 27 test cases covering the following areas:
|
||||
- Viewing processed IGs: Retrieving and displaying processed IG data.
|
||||
|
||||
- **File Operations**:
|
||||
- Processing IG packages: Extracting data from `.tgz` files.
|
||||
- Processing IG packages: Extracting data from `.tgz` files, including profile relationships.
|
||||
- Deleting IG packages: Removing `.tgz` files from the filesystem.
|
||||
|
||||
- **Security**:
|
||||
@ -219,7 +238,7 @@ test_app.py::TestFHIRFlareIGToolkit::test_get_example_content_invalid_path PASSE
|
||||
|
||||
### Background
|
||||
|
||||
The FHIRFLARE IG Toolkit was developed to address the need for a user-friendly tool to manage FHIR Implementation Guides. The project focuses on providing a seamless experience for importing, processing, and analyzing FHIR packages, with a particular emphasis on handling duplicate dependencies—a common challenge in FHIR development.
|
||||
The FHIRFLARE IG Toolkit was developed to address the need for a user-friendly tool to manage FHIR Implementation Guides. The project focuses on providing a seamless experience for importing, processing, and analyzing FHIR packages, with a particular emphasis on handling duplicate dependencies and profile relationships—a common challenge in FHIR development.
|
||||
|
||||
### Technical Decisions
|
||||
|
||||
@ -227,18 +246,21 @@ The FHIRFLARE IG Toolkit was developed to address the need for a user-friendly t
|
||||
- **SQLite**: Used as the database for simplicity and ease of setup. For production use, consider switching to a more robust database like PostgreSQL.
|
||||
- **Bootstrap**: Integrated for a responsive and professional UI, with custom CSS to handle duplicate package highlighting.
|
||||
- **Docker Support**: Added to simplify deployment and ensure consistency across development and production environments.
|
||||
- **Profile Validation**: Added support for `structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile` to ensure resources comply with required profiles during push operations.
|
||||
|
||||
### Known Issues and Workarounds
|
||||
|
||||
- **Bootstrap CSS Conflicts**: Early versions of the application had issues with Bootstrap’s table background styles (`--bs-table-bg`) overriding custom row colors for duplicate packages. This was resolved by setting `--bs-table-bg` to `transparent` for the affected table (see `templates/cp_downloaded_igs.html`).
|
||||
- **Database Permissions**: The `instance` directory must be writable by the application. If you encounter permission errors, ensure the directory has the correct permissions (`chmod -R 777 instance`).
|
||||
- **Package Parsing**: Some FHIR package filenames may not follow the expected `name-version.tgz` format, leading to parsing issues. The application includes a fallback to treat such files as name-only packages, but this may need further refinement.
|
||||
- **Profile Validation Overhead**: Validating against imposed profiles can increase processing time during push operations. This can be disabled via the `VALIDATE_IMPOSED_PROFILES` configuration if performance is a concern.
|
||||
|
||||
### Future Improvements
|
||||
|
||||
- [ ] **Sorting Versions**: Add sorting for package versions in the "Manage FHIR Packages" view to display them in a consistent order (e.g., ascending or descending).
|
||||
- [ ] **Advanced Duplicate Handling**: Implement options to resolve duplicates (e.g., keep the latest version, merge resources).
|
||||
- [ ] **Production Database**: Support for PostgreSQL or MySQL for better scalability in production environments.
|
||||
- [ ] **Profile Validation Enhancements**: Add more detailed validation reports for imposed profiles, including specific element mismatches.
|
||||
|
||||
**Completed Items** (Removed from the list as they are done):
|
||||
- ~~Testing: Add unit tests using pytest to cover core functionality, especially package processing and database operations.~~ (Implemented in `tests/test_app.py` with 27 test cases covering UI, API, database, and file operations.)
|
||||
@ -267,11 +289,12 @@ Please ensure your code follows the project’s coding style and includes approp
|
||||
- **Database Issues**: If the SQLite database (`instance/fhir_ig.db`) cannot be created, ensure the `instance` directory is writable. You may need to adjust permissions (`chmod -R 777 instance`).
|
||||
- **Package Download Fails**: Verify your internet connection and ensure the package name and version are correct.
|
||||
- **Colors Not Displaying**: If table row colors for duplicates are not showing, inspect the page with browser developer tools (F12) to check for CSS conflicts with Bootstrap.
|
||||
- **Profile Relationships Not Displaying**: Ensure `DISPLAY_PROFILE_RELATIONSHIPS` is set to `True` in the application configuration.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- `app.py`: Main Flask application file.
|
||||
- `services.py`: Business logic for importing, processing, and pushing IGs.
|
||||
- `services.py`: Business logic for importing, processing, and pushing IGs, including profile relationship handling.
|
||||
- `templates/`: HTML templates for the UI.
|
||||
- `instance/`: Directory for SQLite database and downloaded packages.
|
||||
- `tests/`: Directory for test files.
|
||||
|
394
app.py
394
app.py
@ -1,16 +1,19 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
from flask import Flask, render_template, render_template_string, request, redirect, url_for, flash, jsonify, Response
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, SelectField
|
||||
from wtforms.validators import DataRequired, Regexp
|
||||
import os
|
||||
from flask_wtf.csrf import CSRFProtect
|
||||
import tarfile
|
||||
import json
|
||||
from datetime import datetime
|
||||
import services
|
||||
import logging
|
||||
import requests
|
||||
import re # Added for regex validation
|
||||
import re
|
||||
from forms import IgImportForm, ValidationForm
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
@ -20,12 +23,14 @@ app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////app/instance/fhir_ig.db'
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['FHIR_PACKAGES_DIR'] = os.path.join(app.instance_path, 'fhir_packages')
|
||||
app.config['API_KEY'] = 'your-api-key-here' # Hardcoded API key for now; replace with a secure solution
|
||||
app.config['FHIR_PACKAGES_DIR'] = '/app/instance/fhir_packages'
|
||||
app.config['API_KEY'] = 'your-api-key-here'
|
||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||
|
||||
# Ensure directories exist and are writable
|
||||
instance_path = '/app/instance'
|
||||
db_path = os.path.join(instance_path, 'fhir_ig.db')
|
||||
db_path = '/app/instance/fhir_ig.db'
|
||||
packages_path = app.config['FHIR_PACKAGES_DIR']
|
||||
|
||||
logger.debug(f"Instance path: {instance_path}")
|
||||
@ -35,9 +40,9 @@ logger.debug(f"Packages path: {packages_path}")
|
||||
try:
|
||||
os.makedirs(instance_path, exist_ok=True)
|
||||
os.makedirs(packages_path, exist_ok=True)
|
||||
os.chmod(instance_path, 0o777)
|
||||
os.chmod(packages_path, 0o777)
|
||||
logger.debug(f"Directories created: {os.listdir('/app')}")
|
||||
os.chmod(instance_path, 0o755)
|
||||
os.chmod(packages_path, 0o755)
|
||||
logger.debug(f"Directories created: {os.listdir(os.path.dirname(__file__))}")
|
||||
logger.debug(f"Instance contents: {os.listdir(instance_path)}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create directories: {e}")
|
||||
@ -45,22 +50,7 @@ except Exception as e:
|
||||
logger.warning("Falling back to /tmp/fhir_ig.db")
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
class IgImportForm(FlaskForm):
|
||||
package_name = StringField('Package Name (e.g., hl7.fhir.us.core)', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zAZ0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
|
||||
])
|
||||
package_version = StringField('Package Version (e.g., 1.0.0 or current)', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
|
||||
])
|
||||
dependency_mode = SelectField('Dependency Pulling Mode', choices=[
|
||||
('recursive', 'Current Recursive'),
|
||||
('patch-canonical', 'Patch Canonical Versions'),
|
||||
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
|
||||
], default='recursive')
|
||||
submit = SubmitField('Fetch & Download IG')
|
||||
csrf = CSRFProtect(app)
|
||||
|
||||
class ProcessedIg(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -98,7 +88,6 @@ def import_ig():
|
||||
result = services.import_package_and_dependencies(name, version, dependency_mode=dependency_mode)
|
||||
if result['errors'] and not result['downloaded']:
|
||||
error_msg = result['errors'][0]
|
||||
# Simplify the error message by taking the last part after the last colon
|
||||
simplified_msg = error_msg.split(": ")[-1] if ": " in error_msg else error_msg
|
||||
flash(f"Failed to import {name}#{version}: {simplified_msg}", "error - check the name and version!")
|
||||
return redirect(url_for('import_ig'))
|
||||
@ -106,10 +95,11 @@ def import_ig():
|
||||
return redirect(url_for('view_igs'))
|
||||
except Exception as e:
|
||||
flash(f"Error downloading IG: {str(e)}", "error")
|
||||
return render_template('import_ig.html', form=form, site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
return render_template('import_ig.html', form=form, site_name='FHIRFLARE IG Toolkit', now=datetime.now())
|
||||
|
||||
@app.route('/view-igs')
|
||||
def view_igs():
|
||||
form = FlaskForm()
|
||||
igs = ProcessedIg.query.all()
|
||||
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
||||
|
||||
@ -119,24 +109,19 @@ def view_igs():
|
||||
if os.path.exists(packages_dir):
|
||||
for filename in os.listdir(packages_dir):
|
||||
if filename.endswith('.tgz'):
|
||||
# Split on the last hyphen to separate name and version
|
||||
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] # Remove .tgz
|
||||
# Validate that the version looks reasonable (e.g., starts with a digit or is a known keyword)
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
if version[0].isdigit() or version in ('preview', 'current', 'latest'):
|
||||
# Replace underscores with dots to match FHIR package naming convention
|
||||
name = name.replace('_', '.')
|
||||
packages.append({'name': name, 'version': version, 'filename': filename})
|
||||
else:
|
||||
# Fallback: treat as name only, log warning
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
||||
packages.append({'name': name, 'version': version, 'filename': filename})
|
||||
else:
|
||||
# Fallback: treat as name only, log warning
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
||||
@ -156,7 +141,7 @@ def view_igs():
|
||||
# Calculate duplicate_groups
|
||||
duplicate_groups = {}
|
||||
for name, pkgs in duplicate_names.items():
|
||||
if len(pkgs) > 1: # Only include packages with multiple versions
|
||||
if len(pkgs) > 1:
|
||||
duplicate_groups[name] = [pkg['version'] for pkg in pkgs]
|
||||
|
||||
# Precompute group colors
|
||||
@ -165,13 +150,15 @@ def view_igs():
|
||||
for i, name in enumerate(duplicate_groups.keys()):
|
||||
group_colors[name] = colors[i % len(colors)]
|
||||
|
||||
return render_template('cp_downloaded_igs.html', packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
return render_template('cp_downloaded_igs.html', form=form, packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
||||
site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
site_name='FHIRFLARE IG Toolkit', now=datetime.now(),
|
||||
config=app.config)
|
||||
|
||||
@app.route('/push-igs', methods=['GET', 'POST'])
|
||||
def push_igs():
|
||||
form = FlaskForm()
|
||||
igs = ProcessedIg.query.all()
|
||||
processed_ids = {(ig.package_name, ig.version) for ig in igs}
|
||||
|
||||
@ -181,24 +168,19 @@ def push_igs():
|
||||
if os.path.exists(packages_dir):
|
||||
for filename in os.listdir(packages_dir):
|
||||
if filename.endswith('.tgz'):
|
||||
# Split on the last hyphen to separate name and version
|
||||
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] # Remove .tgz
|
||||
# Validate that the version looks reasonable (e.g., starts with a digit or is a known keyword)
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
if version[0].isdigit() or version in ('preview', 'current', 'latest'):
|
||||
# Replace underscores with dots to match FHIR package naming convention
|
||||
name = name.replace('_', '.')
|
||||
packages.append({'name': name, 'version': version, 'filename': filename})
|
||||
else:
|
||||
# Fallback: treat as name only, log warning
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
||||
packages.append({'name': name, 'version': version, 'filename': filename})
|
||||
else:
|
||||
# Fallback: treat as name only, log warning
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename}, treating as name only")
|
||||
@ -218,7 +200,7 @@ def push_igs():
|
||||
# Calculate duplicate_groups
|
||||
duplicate_groups = {}
|
||||
for name, pkgs in duplicate_names.items():
|
||||
if len(pkgs) > 1: # Only include packages with multiple versions
|
||||
if len(pkgs) > 1:
|
||||
duplicate_groups[name] = [pkg['version'] for pkg in pkgs]
|
||||
|
||||
# Precompute group colors
|
||||
@ -227,91 +209,102 @@ def push_igs():
|
||||
for i, name in enumerate(duplicate_groups.keys()):
|
||||
group_colors[name] = colors[i % len(colors)]
|
||||
|
||||
return render_template('cp_push_igs.html', packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
return render_template('cp_push_igs.html', form=form, packages=packages, processed_list=igs,
|
||||
processed_ids=processed_ids, duplicate_names=duplicate_names,
|
||||
duplicate_groups=duplicate_groups, group_colors=group_colors,
|
||||
site_name='FLARE FHIR IG Toolkit', now=datetime.now(),
|
||||
api_key=app.config['API_KEY']) # Pass the API key to the template
|
||||
site_name='FHIRFLARE IG Toolkit', now=datetime.now(),
|
||||
api_key=app.config['API_KEY'],
|
||||
config=app.config)
|
||||
|
||||
@app.route('/process-igs', methods=['POST'])
|
||||
def process_ig():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
if not os.path.exists(tgz_path):
|
||||
flash(f"Package file not found: {filename}", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
try:
|
||||
# Parse name and version from filename
|
||||
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]
|
||||
# Replace underscores with dots to match FHIR package naming convention
|
||||
name = name.replace('_', '.')
|
||||
else:
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename} during processing")
|
||||
package_info = services.process_package_file(tgz_path)
|
||||
processed_ig = ProcessedIg(
|
||||
package_name=name,
|
||||
version=version,
|
||||
processed_date=datetime.now(),
|
||||
resource_types_info=package_info['resource_types_info'],
|
||||
must_support_elements=package_info.get('must_support_elements'),
|
||||
examples=package_info.get('examples')
|
||||
)
|
||||
db.session.add(processed_ig)
|
||||
db.session.commit()
|
||||
flash(f"Successfully processed {name}#{version}!", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error processing IG: {str(e)}", "error")
|
||||
form = FlaskForm()
|
||||
if form.validate_on_submit():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
if not os.path.exists(tgz_path):
|
||||
flash(f"Package file not found: {filename}", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
try:
|
||||
last_hyphen_index = filename.rfind('-')
|
||||
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
||||
name = filename[:last_hyphen_index]
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
name = name.replace('_', '.')
|
||||
else:
|
||||
name = filename[:-4]
|
||||
version = ''
|
||||
logger.warning(f"Could not parse version from {filename} during processing")
|
||||
package_info = services.process_package_file(tgz_path)
|
||||
processed_ig = ProcessedIg(
|
||||
package_name=name,
|
||||
version=version,
|
||||
processed_date=datetime.now(),
|
||||
resource_types_info=package_info['resource_types_info'],
|
||||
must_support_elements=package_info.get('must_support_elements'),
|
||||
examples=package_info.get('examples')
|
||||
)
|
||||
db.session.add(processed_ig)
|
||||
db.session.commit()
|
||||
flash(f"Successfully processed {name}#{version}!", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error processing IG: {str(e)}", "error")
|
||||
else:
|
||||
flash("CSRF token missing or invalid.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
@app.route('/delete-ig', methods=['POST'])
|
||||
def delete_ig():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
||||
if os.path.exists(tgz_path):
|
||||
try:
|
||||
os.remove(tgz_path)
|
||||
if os.path.exists(metadata_path):
|
||||
os.remove(metadata_path)
|
||||
logger.debug(f"Deleted metadata file: {metadata_path}")
|
||||
flash(f"Deleted {filename}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting {filename}: {str(e)}", "error")
|
||||
form = FlaskForm()
|
||||
if form.validate_on_submit():
|
||||
filename = request.form.get('filename')
|
||||
if not filename or not filename.endswith('.tgz'):
|
||||
flash("Invalid package file.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)
|
||||
metadata_path = tgz_path.replace('.tgz', '.metadata.json')
|
||||
if os.path.exists(tgz_path):
|
||||
try:
|
||||
os.remove(tgz_path)
|
||||
if os.path.exists(metadata_path):
|
||||
os.remove(metadata_path)
|
||||
logger.debug(f"Deleted metadata file: {metadata_path}")
|
||||
flash(f"Deleted {filename}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error deleting {filename}: {str(e)}", "error")
|
||||
else:
|
||||
flash(f"File not found: {filename}", "error")
|
||||
else:
|
||||
flash(f"File not found: {filename}", "error")
|
||||
flash("CSRF token missing or invalid.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
@app.route('/unload-ig', methods=['POST'])
|
||||
def unload_ig():
|
||||
ig_id = request.form.get('ig_id')
|
||||
if not ig_id:
|
||||
flash("Invalid package ID.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
processed_ig = db.session.get(ProcessedIg, ig_id)
|
||||
if processed_ig:
|
||||
try:
|
||||
db.session.delete(processed_ig)
|
||||
db.session.commit()
|
||||
flash(f"Unloaded {processed_ig.package_name}#{processed_ig.version}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error unloading package: {str(e)}", "error")
|
||||
form = FlaskForm()
|
||||
if form.validate_on_submit():
|
||||
ig_id = request.form.get('ig_id')
|
||||
if not ig_id:
|
||||
flash("Invalid package ID.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
processed_ig = db.session.get(ProcessedIg, ig_id)
|
||||
if processed_ig:
|
||||
try:
|
||||
db.session.delete(processed_ig)
|
||||
db.session.commit()
|
||||
flash(f"Unloaded {processed_ig.package_name}#{processed_ig.version}", "success")
|
||||
except Exception as e:
|
||||
flash(f"Error unloading package: {str(e)}", "error")
|
||||
else:
|
||||
flash(f"Package not found with ID: {ig_id}", "error")
|
||||
else:
|
||||
flash(f"Package not found with ID: {ig_id}", "error")
|
||||
flash("CSRF token missing or invalid.", "error")
|
||||
return redirect(url_for('view_igs'))
|
||||
|
||||
@app.route('/view-ig/<int:processed_ig_id>')
|
||||
@ -320,9 +313,25 @@ def view_ig(processed_ig_id):
|
||||
profile_list = [t for t in processed_ig.resource_types_info if t.get('is_profile')]
|
||||
base_list = [t for t in processed_ig.resource_types_info if not t.get('is_profile')]
|
||||
examples_by_type = processed_ig.examples or {}
|
||||
|
||||
# Load metadata to get profile relationships
|
||||
package_name = processed_ig.package_name
|
||||
version = processed_ig.version
|
||||
metadata_filename = f"{services.sanitize_filename_part(package_name)}-{services.sanitize_filename_part(version)}.metadata.json"
|
||||
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename)
|
||||
complies_with_profiles = []
|
||||
imposed_profiles = []
|
||||
if os.path.exists(metadata_path):
|
||||
with open(metadata_path, 'r') as f:
|
||||
metadata = json.load(f)
|
||||
complies_with_profiles = metadata.get('complies_with_profiles', [])
|
||||
imposed_profiles = metadata.get('imposed_profiles', [])
|
||||
|
||||
return render_template('cp_view_processed_ig.html', title=f"View {processed_ig.package_name}#{processed_ig.version}",
|
||||
processed_ig=processed_ig, profile_list=profile_list, base_list=base_list,
|
||||
examples_by_type=examples_by_type, site_name='FLARE FHIR IG Toolkit', now=datetime.now())
|
||||
examples_by_type=examples_by_type, site_name='FHIRFLARE IG Toolkit', now=datetime.now(),
|
||||
complies_with_profiles=complies_with_profiles, imposed_profiles=imposed_profiles,
|
||||
config=app.config)
|
||||
|
||||
@app.route('/get-structure')
|
||||
def get_structure_definition():
|
||||
@ -332,7 +341,6 @@ def get_structure_definition():
|
||||
if not all([package_name, package_version, resource_identifier]):
|
||||
return jsonify({"error": "Missing query parameters"}), 400
|
||||
|
||||
# First, try to get the structure definition from the specified package
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(package_name, package_version))
|
||||
sd_data = None
|
||||
fallback_used = False
|
||||
@ -340,13 +348,11 @@ def get_structure_definition():
|
||||
if os.path.exists(tgz_path):
|
||||
sd_data, _ = services.find_and_extract_sd(tgz_path, resource_identifier)
|
||||
|
||||
# If not found, fall back to the core FHIR package (hl7.fhir.r4.core#4.0.1)
|
||||
if sd_data is None:
|
||||
logger.debug(f"Structure definition for '{resource_identifier}' not found in {package_name}#{package_version}, attempting fallback to hl7.fhir.r4.core#4.0.1")
|
||||
core_package_name = "hl7.fhir.r4.core"
|
||||
core_package_version = "4.0.1"
|
||||
|
||||
# Ensure the core package is downloaded
|
||||
core_tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], services._construct_tgz_filename(core_package_name, core_package_version))
|
||||
if not os.path.exists(core_tgz_path):
|
||||
logger.debug(f"Core package {core_package_name}#{core_package_version} not found, attempting to download")
|
||||
@ -359,7 +365,6 @@ def get_structure_definition():
|
||||
logger.error(f"Error downloading core package: {str(e)}")
|
||||
return jsonify({"error": f"SD for '{resource_identifier}' not found in {package_name}#{package_version}, and error downloading core package: {str(e)}"}), 500
|
||||
|
||||
# Try to extract the structure definition from the core package
|
||||
if os.path.exists(core_tgz_path):
|
||||
sd_data, _ = services.find_and_extract_sd(core_tgz_path, resource_identifier)
|
||||
if sd_data is None:
|
||||
@ -415,55 +420,59 @@ def get_package_metadata():
|
||||
return jsonify({'dependency_mode': metadata['dependency_mode']})
|
||||
return jsonify({'error': 'Metadata not found'}), 404
|
||||
|
||||
# API Endpoint: Import IG Package
|
||||
@app.route('/api/import-ig', methods=['POST'])
|
||||
def api_import_ig():
|
||||
# Check API key
|
||||
auth_error = check_api_key()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
# Validate request
|
||||
if not request.is_json:
|
||||
return jsonify({"status": "error", "message": "Request must be JSON"}), 400
|
||||
|
||||
data = request.get_json()
|
||||
package_name = data.get('package_name')
|
||||
version = data.get('version')
|
||||
dependency_mode = data.get('dependency_mode', 'recursive') # Default to recursive
|
||||
dependency_mode = data.get('dependency_mode', 'recursive')
|
||||
|
||||
if not package_name or not version:
|
||||
return jsonify({"status": "error", "message": "Missing package_name or version"}), 400
|
||||
|
||||
# Validate package name and version format using re
|
||||
if not (isinstance(package_name, str) and isinstance(version, str) and
|
||||
re.match(r'^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$', package_name) and
|
||||
re.match(r'^[a-zA-Z0-9\.\-]+$', version)):
|
||||
return jsonify({"status": "error", "message": "Invalid package name or version format"}), 400
|
||||
|
||||
# Validate dependency mode
|
||||
valid_modes = ['recursive', 'patch-canonical', 'tree-shaking']
|
||||
if dependency_mode not in valid_modes:
|
||||
return jsonify({"status": "error", "message": f"Invalid dependency mode: {dependency_mode}. Must be one of {valid_modes}"}), 400
|
||||
|
||||
try:
|
||||
# Import package and dependencies
|
||||
result = services.import_package_and_dependencies(package_name, version, dependency_mode=dependency_mode)
|
||||
if result['errors'] and not result['downloaded']:
|
||||
return jsonify({"status": "error", "message": f"Failed to import {package_name}#{version}: {result['errors'][0]}"}), 500
|
||||
|
||||
# Process the package to get compliesWithProfile and imposeProfile
|
||||
package_filename = f"{services.sanitize_filename_part(package_name)}-{services.sanitize_filename_part(version)}.tgz"
|
||||
package_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], package_filename)
|
||||
complies_with_profiles = []
|
||||
imposed_profiles = []
|
||||
if os.path.exists(package_path):
|
||||
process_result = services.process_package_file(package_path)
|
||||
complies_with_profiles = process_result.get('complies_with_profiles', [])
|
||||
imposed_profiles = process_result.get('imposed_profiles', [])
|
||||
else:
|
||||
logger.warning(f"Package file not found after import: {package_path}")
|
||||
|
||||
# Check for duplicates
|
||||
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'):
|
||||
# Split on the last hyphen to separate name and version
|
||||
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]
|
||||
# Replace underscores with dots to match FHIR package naming convention
|
||||
name = name.replace('_', '.')
|
||||
if version[0].isdigit() or version in ('preview', 'current', 'latest'):
|
||||
packages.append({'name': name, 'version': version, 'filename': filename})
|
||||
@ -476,7 +485,6 @@ def api_import_ig():
|
||||
version = ''
|
||||
packages.append({'name': name, 'version': version, 'filename': filename})
|
||||
|
||||
# Calculate duplicates
|
||||
duplicate_names = {}
|
||||
for pkg in packages:
|
||||
name = pkg['name']
|
||||
@ -490,7 +498,6 @@ def api_import_ig():
|
||||
versions = [pkg['version'] for pkg in pkgs]
|
||||
duplicates.append(f"{name} (exists as {', '.join(versions)})")
|
||||
|
||||
# Deduplicate dependencies
|
||||
seen = set()
|
||||
unique_dependencies = []
|
||||
for dep in result.get('dependencies', []):
|
||||
@ -499,7 +506,6 @@ def api_import_ig():
|
||||
seen.add(dep_str)
|
||||
unique_dependencies.append(dep_str)
|
||||
|
||||
# Prepare response
|
||||
response = {
|
||||
"status": "success",
|
||||
"message": "Package imported successfully",
|
||||
@ -507,6 +513,8 @@ def api_import_ig():
|
||||
"version": version,
|
||||
"dependency_mode": dependency_mode,
|
||||
"dependencies": unique_dependencies,
|
||||
"complies_with_profiles": complies_with_profiles,
|
||||
"imposed_profiles": imposed_profiles,
|
||||
"duplicates": duplicates
|
||||
}
|
||||
return jsonify(response), 200
|
||||
@ -515,15 +523,12 @@ def api_import_ig():
|
||||
logger.error(f"Error in api_import_ig: {str(e)}")
|
||||
return jsonify({"status": "error", "message": f"Error importing package: {str(e)}"}), 500
|
||||
|
||||
# API Endpoint: Push IG to FHIR Server with Streaming
|
||||
@app.route('/api/push-ig', methods=['POST'])
|
||||
def api_push_ig():
|
||||
# Check API key
|
||||
auth_error = check_api_key()
|
||||
if auth_error:
|
||||
return auth_error
|
||||
|
||||
# Validate request
|
||||
if not request.is_json:
|
||||
return jsonify({"status": "error", "message": "Request must be JSON"}), 400
|
||||
|
||||
@ -536,13 +541,11 @@ def api_push_ig():
|
||||
if not all([package_name, version, fhir_server_url]):
|
||||
return jsonify({"status": "error", "message": "Missing package_name, version, or fhir_server_url"}), 400
|
||||
|
||||
# Validate package name and version format using re
|
||||
if not (isinstance(package_name, str) and isinstance(version, str) and
|
||||
re.match(r'^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$', package_name) and
|
||||
re.match(r'^[a-zA-Z0-9\.\-]+$', version)):
|
||||
return jsonify({"status": "error", "message": "Invalid package name or version format"}), 400
|
||||
|
||||
# Check if package exists
|
||||
tgz_filename = services._construct_tgz_filename(package_name, version)
|
||||
tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], tgz_filename)
|
||||
if not os.path.exists(tgz_path):
|
||||
@ -550,21 +553,10 @@ def api_push_ig():
|
||||
|
||||
def generate_stream():
|
||||
try:
|
||||
# Start message
|
||||
yield json.dumps({"type": "start", "message": f"Starting push for {package_name}#{version}..."}) + "\n"
|
||||
|
||||
# Extract resources from the main package
|
||||
resources = []
|
||||
with tarfile.open(tgz_path, "r:gz") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.name.startswith('package/') and member.name.endswith('.json') and not member.name.endswith('package.json'):
|
||||
with tar.extractfile(member) as f:
|
||||
resource_data = json.load(f)
|
||||
if 'resourceType' in resource_data:
|
||||
resources.append(resource_data)
|
||||
|
||||
# If include_dependencies is True, fetch dependencies from metadata
|
||||
pushed_packages = [f"{package_name}#{version}"]
|
||||
packages_to_push = [(package_name, version, tgz_path)]
|
||||
if include_dependencies:
|
||||
yield json.dumps({"type": "progress", "message": "Processing dependencies..."}) + "\n"
|
||||
metadata = services.get_package_metadata(package_name, version)
|
||||
@ -575,36 +567,44 @@ def api_push_ig():
|
||||
dep_tgz_filename = services._construct_tgz_filename(dep_name, dep_version)
|
||||
dep_tgz_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], dep_tgz_filename)
|
||||
if os.path.exists(dep_tgz_path):
|
||||
with tarfile.open(dep_tgz_path, "r:gz") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.name.startswith('package/') and member.name.endswith('.json') and not member.name.endswith('package.json'):
|
||||
with tar.extractfile(member) as f:
|
||||
resource_data = json.load(f)
|
||||
if 'resourceType' in resource_data:
|
||||
resources.append(resource_data)
|
||||
pushed_packages.append(f"{dep_name}#{dep_version}")
|
||||
packages_to_push.append((dep_name, dep_version, dep_tgz_path))
|
||||
yield json.dumps({"type": "progress", "message": f"Added dependency {dep_name}#{dep_version}"}) + "\n"
|
||||
else:
|
||||
yield json.dumps({"type": "warning", "message": f"Dependency {dep_name}#{dep_version} not found, skipping"}) + "\n"
|
||||
|
||||
# Push resources to FHIR server
|
||||
for pkg_name, pkg_version, pkg_path in packages_to_push:
|
||||
with tarfile.open(pkg_path, "r:gz") as tar:
|
||||
for member in tar.getmembers():
|
||||
if member.name.startswith('package/') and member.name.endswith('.json') and not member.name.endswith('package.json'):
|
||||
with tar.extractfile(member) as f:
|
||||
resource_data = json.load(f)
|
||||
if 'resourceType' in resource_data:
|
||||
resources.append((resource_data, pkg_name, pkg_version))
|
||||
|
||||
server_response = []
|
||||
success_count = 0
|
||||
failure_count = 0
|
||||
total_resources = len(resources)
|
||||
yield json.dumps({"type": "progress", "message": f"Found {total_resources} resources to upload"}) + "\n"
|
||||
|
||||
for i, resource in enumerate(resources, 1):
|
||||
pushed_packages = []
|
||||
for i, (resource, pkg_name, pkg_version) in enumerate(resources, 1):
|
||||
resource_type = resource.get('resourceType')
|
||||
resource_id = resource.get('id')
|
||||
if not resource_type or not resource_id:
|
||||
yield json.dumps({"type": "warning", "message": f"Skipping invalid resource at index {i}"}) + "\n"
|
||||
yield json.dumps({"type": "warning", "message": f"Skipping invalid resource at index {i} from {pkg_name}#{pkg_version}"}) + "\n"
|
||||
failure_count += 1
|
||||
continue
|
||||
|
||||
# Validate against the profile and imposed profiles
|
||||
validation_result = services.validate_resource_against_profile(pkg_name, pkg_version, resource, include_dependencies=False)
|
||||
if not validation_result['valid']:
|
||||
yield json.dumps({"type": "error", "message": f"Validation failed for {resource_type}/{resource_id} in {pkg_name}#{pkg_version}: {', '.join(validation_result['errors'])}"}) + "\n"
|
||||
failure_count += 1
|
||||
continue
|
||||
|
||||
# Construct the FHIR server URL for the resource
|
||||
resource_url = f"{fhir_server_url.rstrip('/')}/{resource_type}/{resource_id}"
|
||||
yield json.dumps({"type": "progress", "message": f"Uploading {resource_type}/{resource_id} ({i}/{total_resources})..."}) + "\n"
|
||||
yield json.dumps({"type": "progress", "message": f"Uploading {resource_type}/{resource_id} ({i}/{total_resources}) from {pkg_name}#{pkg_version}..."}) + "\n"
|
||||
|
||||
try:
|
||||
response = requests.put(resource_url, json=resource, headers={'Content-Type': 'application/fhir+json'})
|
||||
@ -612,13 +612,14 @@ def api_push_ig():
|
||||
server_response.append(f"Uploaded {resource_type}/{resource_id} successfully")
|
||||
yield json.dumps({"type": "success", "message": f"Uploaded {resource_type}/{resource_id} successfully"}) + "\n"
|
||||
success_count += 1
|
||||
if f"{pkg_name}#{pkg_version}" not in pushed_packages:
|
||||
pushed_packages.append(f"{pkg_name}#{pkg_version}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = f"Failed to upload {resource_type}/{resource_id}: {str(e)}"
|
||||
server_response.append(error_msg)
|
||||
yield json.dumps({"type": "error", "message": error_msg}) + "\n"
|
||||
failure_count += 1
|
||||
|
||||
# Final summary
|
||||
summary = {
|
||||
"status": "success" if failure_count == 0 else "partial",
|
||||
"message": f"Push completed: {success_count} resources uploaded, {failure_count} failed",
|
||||
@ -642,10 +643,71 @@ def api_push_ig():
|
||||
|
||||
return Response(generate_stream(), mimetype='application/x-ndjson')
|
||||
|
||||
@app.route('/validate-sample', methods=['GET', 'POST'])
|
||||
def validate_sample():
|
||||
form = ValidationForm()
|
||||
validation_report = None
|
||||
packages = []
|
||||
|
||||
# Load available packages
|
||||
packages_dir = app.config['FHIR_PACKAGES_DIR']
|
||||
if os.path.exists(packages_dir):
|
||||
for filename in os.listdir(packages_dir):
|
||||
if filename.endswith('.tgz'):
|
||||
last_hyphen_index = filename.rfind('-')
|
||||
if last_hyphen_index != -1 and filename.endswith('.tgz'):
|
||||
name = filename[:last_hyphen_index]
|
||||
version = filename[last_hyphen_index + 1:-4]
|
||||
name = name.replace('_', '.')
|
||||
try:
|
||||
with tarfile.open(os.path.join(packages_dir, filename), 'r:gz') as tar:
|
||||
package_json = tar.extractfile('package/package.json')
|
||||
pkg_info = json.load(package_json)
|
||||
packages.append({'name': pkg_info['name'], 'version': pkg_info['version']})
|
||||
except Exception as e:
|
||||
logger.warning(f"Error reading package {filename}: {e}")
|
||||
continue
|
||||
|
||||
if form.validate_on_submit():
|
||||
package_name = form.package_name.data
|
||||
version = form.version.data
|
||||
include_dependencies = form.include_dependencies.data
|
||||
mode = form.mode.data
|
||||
try:
|
||||
sample_input = json.loads(form.sample_input.data)
|
||||
if mode == 'single':
|
||||
validation_report = services.validate_resource_against_profile(
|
||||
package_name, version, sample_input, include_dependencies
|
||||
)
|
||||
else: # mode == 'bundle'
|
||||
validation_report = services.validate_bundle_against_profile(
|
||||
package_name, version, sample_input, include_dependencies
|
||||
)
|
||||
flash("Validation completed.", 'success')
|
||||
except json.JSONDecodeError:
|
||||
flash("Invalid JSON format in sample input.", 'error')
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating sample: {e}")
|
||||
flash(f"Error validating sample: {str(e)}", 'error')
|
||||
validation_report = {'valid': False, 'errors': [str(e)], 'warnings': [], 'results': {}}
|
||||
|
||||
return render_template(
|
||||
'validate_sample.html',
|
||||
form=form,
|
||||
packages=packages,
|
||||
validation_report=validation_report,
|
||||
site_name='FHIRFLARE IG Toolkit',
|
||||
now=datetime.now()
|
||||
)
|
||||
|
||||
with app.app_context():
|
||||
logger.debug(f"Creating database at: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
||||
db.create_all()
|
||||
logger.debug("Database initialization complete")
|
||||
try:
|
||||
db.create_all()
|
||||
logger.debug("Database initialization complete")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize database: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
49
forms.py
49
forms.py
@ -1,19 +1,44 @@
|
||||
# app/modules/fhir_ig_importer/forms.py
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField
|
||||
from wtforms.validators import DataRequired, Regexp
|
||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField
|
||||
from wtforms.validators import DataRequired, Regexp, Optional
|
||||
|
||||
class IgImportForm(FlaskForm):
|
||||
"""Form for specifying an IG package to import."""
|
||||
# Basic validation for FHIR package names (e.g., hl7.fhir.r4.core)
|
||||
package_name = StringField('Package Name (e.g., hl7.fhir.au.base)', validators=[
|
||||
package_name = StringField('Package Name', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)+$', message='Invalid package name format.')
|
||||
Regexp(r'^[a-zA-Z0-9][a-zA-Z0-9\-\.]*[a-zA-Z0-9]$', message="Invalid package name format.")
|
||||
])
|
||||
# Basic validation for version (e.g., 4.1.0, current)
|
||||
package_version = StringField('Package Version (e.g., 4.1.0 or current)', validators=[
|
||||
package_version = StringField('Package Version', validators=[
|
||||
DataRequired(),
|
||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message='Invalid version format.')
|
||||
Regexp(r'^[a-zA-Z0-9\.\-]+$', message="Invalid version format. Use alphanumeric characters, dots, or hyphens (e.g., 1.2.3, 1.1.0-preview, current).")
|
||||
])
|
||||
submit = SubmitField('Fetch & Download IG')
|
||||
dependency_mode = SelectField('Dependency Mode', choices=[
|
||||
('recursive', 'Current Recursive'),
|
||||
('patch-canonical', 'Patch Canonical Versions'),
|
||||
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
|
||||
], default='recursive')
|
||||
submit = SubmitField('Import')
|
||||
|
||||
class ValidationForm(FlaskForm):
|
||||
package_name = StringField('Package Name', validators=[DataRequired()])
|
||||
version = StringField('Package Version', validators=[DataRequired()])
|
||||
include_dependencies = BooleanField('Include Dependencies', default=True)
|
||||
mode = SelectField('Validation Mode', choices=[
|
||||
('single', 'Single Resource'),
|
||||
('bundle', 'Bundle')
|
||||
], default='single')
|
||||
sample_input = TextAreaField('Sample Input', validators=[DataRequired()])
|
||||
submit = SubmitField('Validate')
|
||||
|
||||
def validate_json(field, mode):
|
||||
"""Custom validator to ensure input is valid JSON and matches the selected mode."""
|
||||
import json
|
||||
try:
|
||||
data = json.loads(field)
|
||||
if mode == 'single' and not isinstance(data, dict):
|
||||
raise ValueError("Single resource mode requires a JSON object.")
|
||||
if mode == 'bundle' and (not isinstance(data, dict) or data.get('resourceType') != 'Bundle'):
|
||||
raise ValueError("Bundle mode requires a JSON object with resourceType 'Bundle'.")
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError("Invalid JSON format.")
|
||||
except ValueError as e:
|
||||
raise ValueError(str(e))
|
Binary file not shown.
@ -0,0 +1,21 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.au.base",
|
||||
"version": "5.1.0-preview",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1"
|
||||
},
|
||||
{
|
||||
"name": "hl7.terminology.r4",
|
||||
"version": "6.2.0"
|
||||
},
|
||||
{
|
||||
"name": "hl7.fhir.uv.extensions.r4",
|
||||
"version": "5.2.0"
|
||||
}
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.au.base-5.1.0-preview.tgz
Normal file
Binary file not shown.
@ -1,7 +1,7 @@
|
||||
{
|
||||
"package_name": "hl7.fhir.au.core",
|
||||
"version": "1.1.0-preview",
|
||||
"dependency_mode": "tree-shaking",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": [
|
||||
{
|
||||
"name": "hl7.fhir.r4.core",
|
||||
@ -27,5 +27,7 @@
|
||||
"name": "hl7.fhir.uv.ipa",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
],
|
||||
"complies_with_profiles": [],
|
||||
"imposed_profiles": []
|
||||
}
|
@ -2,5 +2,7 @@
|
||||
"package_name": "hl7.fhir.r4.core",
|
||||
"version": "4.0.1",
|
||||
"dependency_mode": "recursive",
|
||||
"imported_dependencies": []
|
||||
"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.
1492
services.py
1492
services.py
File diff suppressed because it is too large
Load Diff
@ -29,6 +29,9 @@
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'push_igs' else '' }}" href="{{ url_for('push_igs') }}"><i class="bi bi-upload me-1"></i> Push IGs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ 'active' if request.endpoint == 'validate_sample' else '' }}" href="{{ url_for('validate_sample') }}"><i class="bi bi-check-circle me-1"></i> Validate FHIR Sample</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,7 +31,6 @@
|
||||
.badge.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
/* Ensure table rows with custom background colors are not overridden by Bootstrap */
|
||||
.table tr.bg-warning {
|
||||
background-color: #ffc107 !important;
|
||||
}
|
||||
@ -44,7 +43,6 @@
|
||||
.table tr.bg-danger {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
/* Override Bootstrap's table background to allow custom row colors to show */
|
||||
.table-custom-bg {
|
||||
--bs-table-bg: transparent !important;
|
||||
}
|
||||
@ -64,9 +62,7 @@
|
||||
<div class="card-body">
|
||||
{% if packages %}
|
||||
<div class="table-responsive">
|
||||
<!-- remove this warning and move it down the bottom-----------------------------------------------------------------------------------------
|
||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> Duplicate Dependency with Different Versions</small></p>
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> = Duplicate Dependencies</small></p>
|
||||
<table class="table table-sm table-hover table-custom-bg">
|
||||
<thead>
|
||||
<tr><th>Package Name</th><th>Version</th><th>Actions</th></tr>
|
||||
@ -90,11 +86,13 @@
|
||||
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
||||
{% else %}
|
||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||
{{ form.csrf_token }}
|
||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
||||
{{ form.csrf_token }}
|
||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
||||
</form>
|
||||
@ -106,7 +104,6 @@
|
||||
</table>
|
||||
</div>
|
||||
{% if duplicate_groups %}
|
||||
<p class="mb-2"><small><span class="badge bg-danger text-light border me-1">Risk:</span> = Duplicate Dependencies</small></p>
|
||||
<p class="mt-2 small text-muted">Duplicate dependencies detected:
|
||||
{% for name, versions in duplicate_groups.items() %}
|
||||
{% set group_color = group_colors[name] if name in group_colors else 'bg-warning' %}
|
||||
@ -158,6 +155,7 @@
|
||||
<div class="btn-group-vertical btn-group-sm w-100">
|
||||
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
|
||||
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
||||
{{ form.csrf_token }}
|
||||
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
||||
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
||||
</form>
|
||||
|
@ -12,9 +12,6 @@
|
||||
<tr>
|
||||
<th>Package Name</th>
|
||||
<th>Version</th>
|
||||
<!--------------------------------------------------------------------------------------------------------------if you unhide buttons unhide this--------------------------------------------
|
||||
<th>Actions</th>
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -27,20 +24,6 @@
|
||||
<tr {% if duplicate_group %}class="{{ group_colors[name] }}"{% endif %}>
|
||||
<td>{{ name }}</td>
|
||||
<td>{{ version }}</td>
|
||||
<!--------------------------------------------------------------------------------------------------------------Dont need the buttons here--------------------------------------------------
|
||||
<td>
|
||||
{% if not processed %}
|
||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||
<input type="hidden" name="filename" value="{{ filename }}">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Process</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete {{ name }}#{{ version }}?');">
|
||||
<input type="hidden" name="filename" value="{{ filename }}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -65,6 +48,7 @@
|
||||
<div class="col-md-6">
|
||||
<h2>Push IGs to FHIR Server</h2>
|
||||
<form id="pushIgForm" method="POST">
|
||||
{{ form.csrf_token }}
|
||||
<div class="mb-3">
|
||||
<label for="packageSelect" class="form-label">Select Package to Push</label>
|
||||
<select class="form-select" id="packageSelect" name="package_id" required>
|
||||
@ -115,6 +99,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const packageSelect = document.getElementById('packageSelect');
|
||||
const dependencyModeField = document.getElementById('dependencyMode');
|
||||
const csrfToken = "{{ form.csrf_token._value() }}";
|
||||
|
||||
// Update dependency mode when package selection changes
|
||||
packageSelect.addEventListener('change', function() {
|
||||
@ -143,70 +128,135 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (packageSelect.value) {
|
||||
packageSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
document.getElementById('pushIgForm').addEventListener('submit', async function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target;
|
||||
const pushButton = document.getElementById('pushButton');
|
||||
const formData = new FormData(form);
|
||||
const packageId = formData.get('package_id');
|
||||
const [packageName, version] = packageId.split('#');
|
||||
const fhirServerUrl = formData.get('fhir_server_url');
|
||||
const includeDependencies = formData.get('include_dependencies') === 'on';
|
||||
const form = event.target;
|
||||
const pushButton = document.getElementById('pushButton');
|
||||
const formData = new FormData(form);
|
||||
const packageId = formData.get('package_id');
|
||||
const [packageName, version] = packageId.split('#');
|
||||
const fhirServerUrl = formData.get('fhir_server_url');
|
||||
const includeDependencies = formData.get('include_dependencies') === 'on';
|
||||
|
||||
// Disable the button to prevent multiple submissions
|
||||
pushButton.disabled = true;
|
||||
pushButton.textContent = 'Pushing...';
|
||||
// Disable the button to prevent multiple submissions
|
||||
pushButton.disabled = true;
|
||||
pushButton.textContent = 'Pushing...';
|
||||
|
||||
// Clear the console and response area
|
||||
const liveConsole = document.getElementById('liveConsole');
|
||||
const responseDiv = document.getElementById('pushResponse');
|
||||
liveConsole.innerHTML = '<div>Starting push operation...</div>';
|
||||
responseDiv.innerHTML = '';
|
||||
// Clear the console and response area
|
||||
const liveConsole = document.getElementById('liveConsole');
|
||||
const responseDiv = document.getElementById('pushResponse');
|
||||
liveConsole.innerHTML = '<div>Starting push operation...</div>';
|
||||
responseDiv.innerHTML = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/push-ig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/x-ndjson'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
package_name: packageName,
|
||||
version: version,
|
||||
fhir_server_url: fhirServerUrl,
|
||||
include_dependencies: includeDependencies,
|
||||
api_key: '{{ api_key | safe }}' // Use the API key passed from the server
|
||||
})
|
||||
});
|
||||
try {
|
||||
const response = await fetch('/api/push-ig', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/x-ndjson',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
package_name: packageName,
|
||||
version: version,
|
||||
fhir_server_url: fhirServerUrl,
|
||||
include_dependencies: includeDependencies,
|
||||
api_key: '{{ api_key | safe }}'
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to push package');
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.message || 'Failed to push package');
|
||||
}
|
||||
|
||||
// Stream the response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
// Stream the response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
// Decode the chunk and append to buffer
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
// Process each line (newline-separated JSON)
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // Keep the last (possibly incomplete) line in the buffer
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = '';
|
||||
let prefix = '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue; // Skip empty lines
|
||||
switch (data.type) {
|
||||
case 'start':
|
||||
case 'progress':
|
||||
messageClass = 'text-info';
|
||||
prefix = '[INFO]';
|
||||
break;
|
||||
case 'success':
|
||||
messageClass = 'text-success';
|
||||
prefix = '[SUCCESS]';
|
||||
break;
|
||||
case 'error':
|
||||
messageClass = 'text-danger';
|
||||
prefix = '[ERROR]';
|
||||
break;
|
||||
case 'warning':
|
||||
messageClass = 'text-warning';
|
||||
prefix = '[WARNING]';
|
||||
break;
|
||||
case 'complete':
|
||||
if (data.data.status === 'success') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else if (data.data.status === 'partial') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<strong>Partial Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> ${data.data.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
||||
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||
break;
|
||||
default:
|
||||
messageClass = 'text-light';
|
||||
prefix = '[INFO]';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = messageClass;
|
||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||
liveConsole.appendChild(messageDiv);
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing streamed message:', parseError, 'Line:', line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
const data = JSON.parse(buffer);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = '';
|
||||
let prefix = '';
|
||||
@ -230,7 +280,6 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
||||
prefix = '[WARNING]';
|
||||
break;
|
||||
case 'complete':
|
||||
// Display the final summary in the response area
|
||||
if (data.data.status === 'success') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
@ -254,7 +303,6 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// Also log the completion in the console
|
||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
||||
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||
break;
|
||||
@ -267,103 +315,30 @@ document.getElementById('pushIgForm').addEventListener('submit', async function(
|
||||
messageDiv.className = messageClass;
|
||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||
liveConsole.appendChild(messageDiv);
|
||||
|
||||
// Auto-scroll to the bottom
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing streamed message:', parseError, 'Line:', line);
|
||||
console.error('Error parsing final streamed message:', parseError, 'Buffer:', buffer);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'text-danger';
|
||||
errorDiv.textContent = `${timestamp} [ERROR] Failed to push package: ${error.message}`;
|
||||
liveConsole.appendChild(errorDiv);
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> Failed to push package: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
pushButton.disabled = false;
|
||||
pushButton.textContent = 'Push to FHIR Server';
|
||||
}
|
||||
|
||||
// Process any remaining buffer
|
||||
if (buffer.trim()) {
|
||||
try {
|
||||
const data = JSON.parse(buffer);
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
let messageClass = '';
|
||||
let prefix = '';
|
||||
|
||||
switch (data.type) {
|
||||
case 'start':
|
||||
case 'progress':
|
||||
messageClass = 'text-info';
|
||||
prefix = '[INFO]';
|
||||
break;
|
||||
case 'success':
|
||||
messageClass = 'text-success';
|
||||
prefix = '[SUCCESS]';
|
||||
break;
|
||||
case 'error':
|
||||
messageClass = 'text-danger';
|
||||
prefix = '[ERROR]';
|
||||
break;
|
||||
case 'warning':
|
||||
messageClass = 'text-warning';
|
||||
prefix = '[WARNING]';
|
||||
break;
|
||||
case 'complete':
|
||||
if (data.data.status === 'success') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else if (data.data.status === 'partial') {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<strong>Partial Success:</strong> ${data.data.message}<br>
|
||||
<strong>Pushed Packages:</strong> ${data.data.pushed_packages.join(', ')}<br>
|
||||
<strong>Server Response:</strong> ${data.data.server_response}
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> ${data.data.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
messageClass = data.data.status === 'success' ? 'text-success' : (data.data.status === 'partial' ? 'text-warning' : 'text-danger');
|
||||
prefix = data.data.status === 'success' ? '[SUCCESS]' : (data.data.status === 'partial' ? '[PARTIAL]' : '[ERROR]');
|
||||
break;
|
||||
default:
|
||||
messageClass = 'text-light';
|
||||
prefix = '[INFO]';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = messageClass;
|
||||
messageDiv.textContent = `${timestamp} ${prefix} ${data.message || (data.data && data.data.message) || 'Unknown message'}`;
|
||||
liveConsole.appendChild(messageDiv);
|
||||
|
||||
// Auto-scroll to the bottom
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
} catch (parseError) {
|
||||
console.error('Error parsing final streamed message:', parseError, 'Buffer:', buffer);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'text-danger';
|
||||
errorDiv.textContent = `${timestamp} [ERROR] Failed to push package: ${error.message}`;
|
||||
liveConsole.appendChild(errorDiv);
|
||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||
|
||||
responseDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> Failed to push package: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
// Re-enable the button
|
||||
pushButton.disabled = false;
|
||||
pushButton.textContent = 'Push to FHIR Server';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -37,6 +37,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if config.DISPLAY_PROFILE_RELATIONSHIPS %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">Profile Relationships</div>
|
||||
<div class="card-body">
|
||||
<h6>Complies With</h6>
|
||||
{% if complies_with_profiles %}
|
||||
<ul>
|
||||
{% for profile in complies_with_profiles %}
|
||||
<li>{{ profile }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted"><em>No profiles declared as compatible.</em></p>
|
||||
{% endif %}
|
||||
|
||||
<h6>Required Dependent Profiles (Must Also Validate Against)</h6>
|
||||
{% if imposed_profiles %}
|
||||
<ul>
|
||||
{% for profile in imposed_profiles %}
|
||||
<li>{{ profile }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="text-muted"><em>No imposed profiles.</em></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">Resource Types Found / Defined</div>
|
||||
<div class="card-body">
|
||||
|
@ -6,13 +6,13 @@
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">Welcome to {{ site_name }}</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Simple tool for importing and viewing FHIR Implementation Guides.
|
||||
Simple tool for importing, viewing, and validating FHIR Implementation Guides.
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import FHIR IG</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">View Downloaded IGs</a>
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IG's</a>
|
||||
|
||||
<a href="{{ url_for('push_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Upload IGs</a>
|
||||
<a href="{{ url_for('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
139
templates/validate_sample.html
Normal file
139
templates/validate_sample.html
Normal file
@ -0,0 +1,139 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="px-4 py-5 my-5 text-center">
|
||||
<img class="d-block mx-auto mb-4" src="{{ url_for('static', filename='FHIRFLARE.png') }}" alt="FHIRFLARE IG Toolkit" width="192" height="192">
|
||||
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
|
||||
<div class="col-lg-6 mx-auto">
|
||||
<p class="lead mb-4">
|
||||
Validate a FHIR resource or bundle against a selected Implementation Guide. (ALPHA - TEST Fhir pathing is complex and the logic is WIP, please report anomalies you find in Github issues alongside your sample json REMOVE PHI)
|
||||
</p>
|
||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary btn-lg px-4 gap-3">Back to Home</a>
|
||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4">Manage FHIR Packages</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="card">
|
||||
<div class="card-header">Validation Form</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" id="validationForm">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<small class="form-text text-muted">
|
||||
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
||||
</small>
|
||||
<div class="mt-2">
|
||||
<select class="form-select" id="packageSelect" onchange="if(this.value) { let parts = this.value.split('#'); document.getElementById('{{ form.package_name.id }}').value = parts[0]; document.getElementById('{{ form.version.id }}').value = parts[1]; }">
|
||||
<option value="">-- Select a Package --</option>
|
||||
{% for pkg in packages %}
|
||||
<option value="{{ pkg.name }}#{{ pkg.version }}">{{ pkg.name }}#{{ pkg.version }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<label for="{{ form.package_name.id }}" class="form-label">Package Name</label>
|
||||
{{ form.package_name(class="form-control") }}
|
||||
{% for error in form.package_name.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
||||
{{ form.version(class="form-control") }}
|
||||
{% for error in form.version.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
|
||||
<div class="form-check">
|
||||
{{ form.include_dependencies(class="form-check-input") }}
|
||||
{{ form.include_dependencies.label(class="form-check-label") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
|
||||
{{ form.mode(class="form-select") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
|
||||
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
||||
{% for error in form.sample_input.errors %}
|
||||
<div class="text-danger">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if validation_report %}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">Validation Report</div>
|
||||
<div class="card-body">
|
||||
<h5>Summary</h5>
|
||||
<p><strong>Valid:</strong> {{ 'Yes' if validation_report.valid else 'No' }}</p>
|
||||
{% if validation_report.errors %}
|
||||
<h6>Errors</h6>
|
||||
<ul class="text-danger">
|
||||
{% for error in validation_report.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if validation_report.warnings %}
|
||||
<h6>Warnings</h6>
|
||||
<ul class="text-warning">
|
||||
{% for warning in validation_report.warnings %}
|
||||
<li>{{ warning }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if validation_report.results %}
|
||||
<h5>Detailed Results</h5>
|
||||
{% for resource_id, result in validation_report.results.items() %}
|
||||
<h6>{{ resource_id }}</h6>
|
||||
<p><strong>Valid:</strong> {{ 'Yes' if result.valid else 'No' }}</p>
|
||||
{% if result.errors %}
|
||||
<ul class="text-danger">
|
||||
{% for error in result.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if result.warnings %}
|
||||
<ul class="text-warning">
|
||||
{% for warning in result.warnings %}
|
||||
<li>{{ warning }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('validationForm');
|
||||
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
||||
|
||||
// Client-side JSON validation preview
|
||||
sampleInput.addEventListener('input', function() {
|
||||
try {
|
||||
JSON.parse(this.value);
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
} catch (e) {
|
||||
this.classList.remove('is-valid');
|
||||
this.classList.add('is-invalid');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user