mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-12-14 14:55:15 +00:00
Compare commits
No commits in common. "main" and "V2.0" have entirely different histories.
23
.github/ct/chart-schema.yaml
vendored
23
.github/ct/chart-schema.yaml
vendored
@ -1,23 +0,0 @@
|
|||||||
name: str()
|
|
||||||
home: str()
|
|
||||||
version: str()
|
|
||||||
apiVersion: str()
|
|
||||||
appVersion: any(str(), num(), required=False)
|
|
||||||
type: str()
|
|
||||||
dependencies: any(required=False)
|
|
||||||
description: str()
|
|
||||||
keywords: list(str(), required=False)
|
|
||||||
sources: list(str(), required=False)
|
|
||||||
maintainers: list(include('maintainer'), required=False)
|
|
||||||
icon: str(required=False)
|
|
||||||
engine: str(required=False)
|
|
||||||
condition: str(required=False)
|
|
||||||
tags: str(required=False)
|
|
||||||
deprecated: bool(required=False)
|
|
||||||
kubeVersion: str(required=False)
|
|
||||||
annotations: map(str(), str(), required=False)
|
|
||||||
---
|
|
||||||
maintainer:
|
|
||||||
name: str()
|
|
||||||
email: str(required=False)
|
|
||||||
url: str(required=False)
|
|
||||||
15
.github/ct/config.yaml
vendored
15
.github/ct/config.yaml
vendored
@ -1,15 +0,0 @@
|
|||||||
debug: true
|
|
||||||
remote: origin
|
|
||||||
chart-yaml-schema: .github/ct/chart-schema.yaml
|
|
||||||
validate-maintainers: false
|
|
||||||
validate-chart-schema: true
|
|
||||||
validate-yaml: true
|
|
||||||
check-version-increment: true
|
|
||||||
chart-dirs:
|
|
||||||
- charts
|
|
||||||
helm-extra-args: --timeout 300s
|
|
||||||
upgrade: true
|
|
||||||
skip-missing-values: true
|
|
||||||
release-label: release
|
|
||||||
release-name-template: "helm-v{{ .Version }}"
|
|
||||||
target-branch: master
|
|
||||||
84
.github/workflows/build-images.yaml
vendored
84
.github/workflows/build-images.yaml
vendored
@ -1,84 +0,0 @@
|
|||||||
name: Build Container Images
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "image/v*"
|
|
||||||
paths-ignore:
|
|
||||||
- "charts/**"
|
|
||||||
pull_request:
|
|
||||||
branches: [master]
|
|
||||||
paths-ignore:
|
|
||||||
- "charts/**"
|
|
||||||
env:
|
|
||||||
IMAGES: docker.io/hapiproject/hapi
|
|
||||||
PLATFORMS: linux/amd64,linux/arm64/v8
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Container meta for default (distroless) image
|
|
||||||
id: docker_meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.IMAGES }}
|
|
||||||
tags: |
|
|
||||||
type=match,pattern=image/(.*),group=1,enable=${{github.event_name != 'pull_request'}}
|
|
||||||
|
|
||||||
|
|
||||||
- name: Container meta for tomcat image
|
|
||||||
id: docker_tomcat_meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.IMAGES }}
|
|
||||||
tags: |
|
|
||||||
type=match,pattern=image/(.*),group=1,enable=${{github.event_name != 'pull_request'}}
|
|
||||||
flavor: |
|
|
||||||
suffix=-tomcat,onlatest=true
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
|
|
||||||
- name: Build and push default (distroless) image
|
|
||||||
id: docker_build
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
target: default
|
|
||||||
|
|
||||||
- name: Build and push tomcat image
|
|
||||||
id: docker_build_tomcat
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.docker_tomcat_meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.docker_tomcat_meta.outputs.labels }}
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
target: tomcat
|
|
||||||
41
.github/workflows/chart-release.yaml
vendored
41
.github/workflows/chart-release.yaml
vendored
@ -1,41 +0,0 @@
|
|||||||
name: Release Charts
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- "charts/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Add workspace as safe directory
|
|
||||||
run: |
|
|
||||||
git config --global --add safe.directory /__w/FHIRFLARE-IG-Toolkit/FHIRFLARE-IG-Toolkit
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Configure Git
|
|
||||||
run: |
|
|
||||||
git config user.name "$GITHUB_ACTOR"
|
|
||||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Update dependencies
|
|
||||||
run: find charts/ ! -path charts/ -maxdepth 1 -type d -exec helm dependency update {} \;
|
|
||||||
|
|
||||||
- name: Add Helm Repositories
|
|
||||||
run: |
|
|
||||||
helm repo add hapifhir https://hapifhir.github.io/hapi-fhir-jpaserver-starter/
|
|
||||||
helm repo update
|
|
||||||
|
|
||||||
- name: Run chart-releaser
|
|
||||||
uses: helm/chart-releaser-action@be16258da8010256c6e82849661221415f031968 # v1.5.0
|
|
||||||
with:
|
|
||||||
config: .github/ct/config.yaml
|
|
||||||
env:
|
|
||||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
73
.github/workflows/chart-test.yaml
vendored
73
.github/workflows/chart-test.yaml
vendored
@ -1,73 +0,0 @@
|
|||||||
name: Lint and Test Charts
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- "charts/**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
container: quay.io/helmpack/chart-testing:v3.11.0@sha256:f2fd21d30b64411105c7eafb1862783236a219d29f2292219a09fe94ca78ad2a
|
|
||||||
steps:
|
|
||||||
- name: Install helm-docs
|
|
||||||
working-directory: /tmp
|
|
||||||
env:
|
|
||||||
HELM_DOCS_URL: https://github.com/norwoodj/helm-docs/releases/download/v1.14.2/helm-docs_1.14.2_Linux_x86_64.tar.gz
|
|
||||||
run: |
|
|
||||||
curl -LSs $HELM_DOCS_URL | tar xz && \
|
|
||||||
mv ./helm-docs /usr/local/bin/helm-docs && \
|
|
||||||
chmod +x /usr/local/bin/helm-docs && \
|
|
||||||
helm-docs --version
|
|
||||||
|
|
||||||
- name: Add workspace as safe directory
|
|
||||||
run: |
|
|
||||||
git config --global --add safe.directory /__w/hapi-fhir-jpaserver-starter/hapi-fhir-jpaserver-starter
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Check if documentation is up-to-date
|
|
||||||
run: helm-docs && git diff --exit-code HEAD
|
|
||||||
|
|
||||||
- name: Run chart-testing (lint)
|
|
||||||
run: ct lint --config .github/ct/config.yaml
|
|
||||||
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
k8s-version: [1.30.8, 1.31.4, 1.32.0]
|
|
||||||
needs:
|
|
||||||
- lint
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up chart-testing
|
|
||||||
uses: helm/chart-testing-action@e6669bcd63d7cb57cb4380c33043eebe5d111992 # v2.6.1
|
|
||||||
|
|
||||||
- name: Run chart-testing (list-changed)
|
|
||||||
id: list-changed
|
|
||||||
run: |
|
|
||||||
changed=$(ct list-changed --config .github/ct/config.yaml)
|
|
||||||
if [[ -n "$changed" ]]; then
|
|
||||||
echo "::set-output name=changed::true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create k8s Kind Cluster
|
|
||||||
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
|
|
||||||
if: ${{ steps.list-changed.outputs.changed == 'true' }}
|
|
||||||
with:
|
|
||||||
cluster_name: kind-cluster-k8s-${{ matrix.k8s-version }}
|
|
||||||
node_image: kindest/node:v${{ matrix.k8s-version }}
|
|
||||||
|
|
||||||
- name: Run chart-testing (install)
|
|
||||||
run: ct install --config .github/ct/config.yaml
|
|
||||||
if: ${{ steps.list-changed.outputs.changed == 'true' }}
|
|
||||||
58
.github/workflows/docker-publish.yml
vendored
58
.github/workflows/docker-publish.yml
vendored
@ -1,58 +0,0 @@
|
|||||||
# This workflow builds and pushes a multi-architecture Docker image to GitHub Container Registry (ghcr.io).
|
|
||||||
#
|
|
||||||
# The Docker meta step is required because GitHub repository names can contain uppercase letters, but Docker image tags must be lowercase.
|
|
||||||
# The docker/metadata-action@v5 normalizes the repository name to lowercase, ensuring the build and push steps use a valid image tag.
|
|
||||||
#
|
|
||||||
# This workflow builds for both AMD64 and ARM64 architectures using Docker Buildx and QEMU emulation.
|
|
||||||
|
|
||||||
name: Build and Push Docker image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- '*' # This will run the workflow on any branch
|
|
||||||
workflow_dispatch: # This enables manual triggering
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ghcr.io/${{ github.repository }}
|
|
||||||
|
|
||||||
- name: Set normalized image name
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.ref_name }}" == "main" ]]; then
|
|
||||||
echo "IMAGE_NAME=$(echo ${{ steps.meta.outputs.tags }} | sed 's/:main/:latest/')" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "IMAGE_NAME=${{ steps.meta.outputs.tags }}" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and push multi-architecture Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./docker/Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.IMAGE_NAME }}
|
|
||||||
40
.github/workflows/jekyll.yml
vendored
40
.github/workflows/jekyll.yml
vendored
@ -1,40 +0,0 @@
|
|||||||
name: Build and Deploy Jekyll site
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main, master ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: website
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Setup Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
ruby-version: '3.2'
|
|
||||||
bundler-cache: true
|
|
||||||
working-directory: website
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
bundle install
|
|
||||||
|
|
||||||
- name: Build site
|
|
||||||
run: bundle exec jekyll build
|
|
||||||
|
|
||||||
- name: Deploy to gh_pages branch
|
|
||||||
uses: peaceiris/actions-gh-pages@v4
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
publish_dir: ./website/_site
|
|
||||||
publish_branch: gh-pages
|
|
||||||
keep_files: true
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,7 +0,0 @@
|
|||||||
/logs/
|
|
||||||
/.pydevproject
|
|
||||||
/__pycache__/
|
|
||||||
/myenv/
|
|
||||||
/tmp/
|
|
||||||
/Gemfile.lock
|
|
||||||
/_site/
|
|
||||||
23
.project
23
.project
@ -1,23 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<projectDescription>
|
|
||||||
<name>FHIRFLARE-IG-Toolkit</name>
|
|
||||||
<comment></comment>
|
|
||||||
<projects>
|
|
||||||
</projects>
|
|
||||||
<buildSpec>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.python.pydev.PyDevBuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
<buildCommand>
|
|
||||||
<name>org.eclipse.wst.validation.validationbuilder</name>
|
|
||||||
<arguments>
|
|
||||||
</arguments>
|
|
||||||
</buildCommand>
|
|
||||||
</buildSpec>
|
|
||||||
<natures>
|
|
||||||
<nature>org.eclipse.wst.jsdt.core.jsNature</nature>
|
|
||||||
<nature>org.python.pydev.pythonNature</nature>
|
|
||||||
</natures>
|
|
||||||
</projectDescription>
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<classpath>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.JRE_CONTAINER"/>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.wst.jsdt.launching.baseBrowserLibrary"/>
|
|
||||||
<classpathentry kind="src" path=""/>
|
|
||||||
<classpathentry kind="output" path=""/>
|
|
||||||
</classpath>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
org.eclipse.wst.jsdt.launching.JRE_CONTAINER
|
|
||||||
@ -1 +0,0 @@
|
|||||||
Global
|
|
||||||
@ -2,130 +2,57 @@
|
|||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
REM --- Configuration ---
|
REM --- Configuration ---
|
||||||
set REPO_URL_HAPI=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
|
set REPO_URL=https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git
|
||||||
set REPO_URL_CANDLE=https://github.com/FHIR/fhir-candle.git
|
set CLONE_DIR=hapi-fhir-jpaserver
|
||||||
set CLONE_DIR_HAPI=hapi-fhir-jpaserver
|
|
||||||
set CLONE_DIR_CANDLE=fhir-candle
|
|
||||||
set SOURCE_CONFIG_DIR=hapi-fhir-setup
|
set SOURCE_CONFIG_DIR=hapi-fhir-setup
|
||||||
set CONFIG_FILE=application.yaml
|
set CONFIG_FILE=application.yaml
|
||||||
|
|
||||||
REM --- Define Paths ---
|
REM --- Define Paths ---
|
||||||
set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
|
set SOURCE_CONFIG_PATH=..\%SOURCE_CONFIG_DIR%\target\classes\%CONFIG_FILE%
|
||||||
set DEST_CONFIG_PATH=%CLONE_DIR_HAPI%\target\classes\%CONFIG_FILE%
|
set DEST_CONFIG_PATH=%CLONE_DIR%\target\classes\%CONFIG_FILE%
|
||||||
|
|
||||||
REM --- NEW: Define a variable for the custom FHIR URL and server type ---
|
REM === CORRECTED: Prompt for Version ===
|
||||||
set "CUSTOM_FHIR_URL_VAL="
|
|
||||||
set "SERVER_TYPE="
|
|
||||||
set "CANDLE_FHIR_VERSION="
|
|
||||||
|
|
||||||
REM === MODIFIED: Prompt for Installation Mode ===
|
|
||||||
:GetModeChoice
|
:GetModeChoice
|
||||||
SET "APP_MODE=" REM Clear the variable first
|
SET "APP_MODE=" REM Clear the variable first
|
||||||
echo.
|
|
||||||
echo Select Installation Mode:
|
echo Select Installation Mode:
|
||||||
echo 1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet needed)
|
echo 1. Standalone (Includes local HAPI FHIR Server - Requires Git & Maven)
|
||||||
echo 2. Custom URL (Uses a custom FHIR Server - No Git/Maven/Dotnet needed)
|
echo 2. Lite (Excludes local HAPI FHIR Server - No Git/Maven needed)
|
||||||
echo 3. Hapi (Includes local HAPI FHIR Server - Requires Git & Maven)
|
CHOICE /C 12 /N /M "Enter your choice (1 or 2):"
|
||||||
echo 4. Candle (Includes local FHIR Candle Server - Requires Git & Dotnet)
|
|
||||||
CHOICE /C 1234 /N /M "Enter your choice (1, 2, 3, or 4):"
|
|
||||||
|
|
||||||
IF ERRORLEVEL 4 (
|
|
||||||
SET APP_MODE=standalone
|
|
||||||
SET SERVER_TYPE=candle
|
|
||||||
goto :GetCandleFhirVersion
|
|
||||||
)
|
|
||||||
IF ERRORLEVEL 3 (
|
|
||||||
SET APP_MODE=standalone
|
|
||||||
SET SERVER_TYPE=hapi
|
|
||||||
goto :ModeSet
|
|
||||||
)
|
|
||||||
IF ERRORLEVEL 2 (
|
IF ERRORLEVEL 2 (
|
||||||
SET APP_MODE=standalone
|
|
||||||
goto :GetCustomUrl
|
|
||||||
)
|
|
||||||
IF ERRORLEVEL 1 (
|
|
||||||
SET APP_MODE=lite
|
SET APP_MODE=lite
|
||||||
goto :ModeSet
|
goto :ModeSet
|
||||||
)
|
)
|
||||||
|
IF ERRORLEVEL 1 (
|
||||||
|
SET APP_MODE=standalone
|
||||||
|
goto :ModeSet
|
||||||
|
)
|
||||||
REM If somehow neither was chosen (e.g., Ctrl+C), loop back
|
REM If somehow neither was chosen (e.g., Ctrl+C), loop back
|
||||||
echo Invalid input. Please try again.
|
echo Invalid input. Please try again.
|
||||||
goto :GetModeChoice
|
goto :GetModeChoice
|
||||||
|
|
||||||
:GetCustomUrl
|
|
||||||
set "CONFIRMED_URL="
|
|
||||||
:PromptUrlLoop
|
|
||||||
echo.
|
|
||||||
set /p "CUSTOM_URL_INPUT=Please enter the custom FHIR server URL: "
|
|
||||||
echo.
|
|
||||||
echo You entered: !CUSTOM_URL_INPUT!
|
|
||||||
set /p "CONFIRM_URL=Is this URL correct? (Y/N): "
|
|
||||||
if /i "!CONFIRM_URL!" EQU "Y" (
|
|
||||||
set "CONFIRMED_URL=!CUSTOM_URL_INPUT!"
|
|
||||||
goto :ConfirmUrlLoop
|
|
||||||
) else (
|
|
||||||
goto :PromptUrlLoop
|
|
||||||
)
|
|
||||||
:ConfirmUrlLoop
|
|
||||||
echo.
|
|
||||||
echo Please re-enter the URL to confirm it is correct:
|
|
||||||
set /p "CUSTOM_URL_INPUT=Re-enter URL: "
|
|
||||||
if /i "!CUSTOM_URL_INPUT!" EQU "!CONFIRMED_URL!" (
|
|
||||||
set "CUSTOM_FHIR_URL_VAL=!CUSTOM_URL_INPUT!"
|
|
||||||
echo.
|
|
||||||
echo Custom URL confirmed: !CUSTOM_FHIR_URL_VAL!
|
|
||||||
goto :ModeSet
|
|
||||||
) else (
|
|
||||||
echo.
|
|
||||||
echo URLs do not match. Please try again.
|
|
||||||
goto :PromptUrlLoop
|
|
||||||
)
|
|
||||||
|
|
||||||
:GetCandleFhirVersion
|
|
||||||
echo.
|
|
||||||
echo Select the FHIR version for the Candle server:
|
|
||||||
echo 1. R4 (4.0)
|
|
||||||
echo 2. R4B (4.3)
|
|
||||||
echo 3. R5 (5.0)
|
|
||||||
CHOICE /C 123 /N /M "Enter your choice (1, 2, or 3):"
|
|
||||||
IF ERRORLEVEL 3 (
|
|
||||||
SET CANDLE_FHIR_VERSION=r5
|
|
||||||
goto :ModeSet
|
|
||||||
)
|
|
||||||
IF ERRORLEVEL 2 (
|
|
||||||
SET CANDLE_FHIR_VERSION=r4b
|
|
||||||
goto :ModeSet
|
|
||||||
)
|
|
||||||
IF ERRORLEVEL 1 (
|
|
||||||
SET CANDLE_FHIR_VERSION=r4
|
|
||||||
goto :ModeSet
|
|
||||||
)
|
|
||||||
echo Invalid input. Please try again.
|
|
||||||
goto :GetCandleFhirVersion
|
|
||||||
|
|
||||||
:ModeSet
|
:ModeSet
|
||||||
IF "%APP_MODE%"=="" (
|
IF "%APP_MODE%"=="" (
|
||||||
echo Invalid choice detected after checks. Exiting.
|
echo Invalid choice detected after checks. Exiting.
|
||||||
goto :eof
|
goto :eof
|
||||||
)
|
)
|
||||||
echo Selected Mode: %APP_MODE%
|
echo Selected Mode: %APP_MODE%
|
||||||
echo Server Type: %SERVER_TYPE%
|
|
||||||
echo.
|
echo.
|
||||||
REM === END MODIFICATION ===
|
REM === END CORRECTION ===
|
||||||
|
|
||||||
|
|
||||||
REM === Conditionally Execute Server Setup ===
|
REM === Conditionally Execute HAPI Setup ===
|
||||||
IF "%SERVER_TYPE%"=="hapi" (
|
IF "%APP_MODE%"=="standalone" (
|
||||||
echo Running Hapi server setup...
|
echo Running Standalone setup including HAPI FHIR...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM --- Step 0: Clean up previous clone (optional) ---
|
REM --- Step 0: Clean up previous clone (optional) ---
|
||||||
echo Checking for existing directory: %CLONE_DIR_HAPI%
|
echo Checking for existing directory: %CLONE_DIR%
|
||||||
if exist "%CLONE_DIR_HAPI%" (
|
if exist "%CLONE_DIR%" (
|
||||||
echo Found existing directory, removing it...
|
echo Found existing directory, removing it...
|
||||||
rmdir /s /q "%CLONE_DIR_HAPI%"
|
rmdir /s /q "%CLONE_DIR%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to remove existing directory: %CLONE_DIR_HAPI%
|
echo ERROR: Failed to remove existing directory: %CLONE_DIR%
|
||||||
goto :error
|
goto :error
|
||||||
)
|
)
|
||||||
echo Existing directory removed.
|
echo Existing directory removed.
|
||||||
@ -135,8 +62,8 @@ IF "%SERVER_TYPE%"=="hapi" (
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM --- Step 1: Clone the HAPI FHIR server repository ---
|
REM --- Step 1: Clone the HAPI FHIR server repository ---
|
||||||
echo Cloning repository: %REPO_URL_HAPI% into %CLONE_DIR_HAPI%...
|
echo Cloning repository: %REPO_URL% into %CLONE_DIR%...
|
||||||
git clone "%REPO_URL_HAPI%" "%CLONE_DIR_HAPI%"
|
git clone "%REPO_URL%" "%CLONE_DIR%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to clone repository. Check Git installation and network connection.
|
echo ERROR: Failed to clone repository. Check Git installation and network connection.
|
||||||
goto :error
|
goto :error
|
||||||
@ -145,10 +72,10 @@ IF "%SERVER_TYPE%"=="hapi" (
|
|||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM --- Step 2: Navigate into the cloned directory ---
|
REM --- Step 2: Navigate into the cloned directory ---
|
||||||
echo Changing directory to %CLONE_DIR_HAPI%...
|
echo Changing directory to %CLONE_DIR%...
|
||||||
cd "%CLONE_DIR_HAPI%"
|
cd "%CLONE_DIR%"
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo ERROR: Failed to change directory to %CLONE_DIR_HAPI%.
|
echo ERROR: Failed to change directory to %CLONE_DIR%.
|
||||||
goto :error
|
goto :error
|
||||||
)
|
)
|
||||||
echo Current directory: %CD%
|
echo Current directory: %CD%
|
||||||
@ -191,141 +118,45 @@ IF "%SERVER_TYPE%"=="hapi" (
|
|||||||
echo Current directory: %CD%
|
echo Current directory: %CD%
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
) ELSE IF "%SERVER_TYPE%"=="candle" (
|
|
||||||
echo Running FHIR Candle server setup...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM --- Step 0: Clean up previous clone (optional) ---
|
|
||||||
echo Checking for existing directory: %CLONE_DIR_CANDLE%
|
|
||||||
if exist "%CLONE_DIR_CANDLE%" (
|
|
||||||
echo Found existing directory, removing it...
|
|
||||||
rmdir /s /q "%CLONE_DIR_CANDLE%"
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo ERROR: Failed to remove existing directory: %CLONE_DIR_CANDLE%
|
|
||||||
goto :error
|
|
||||||
)
|
|
||||||
echo Existing directory removed.
|
|
||||||
) else (
|
|
||||||
echo Directory does not exist, proceeding with clone.
|
|
||||||
)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM --- Step 1: Clone the FHIR Candle server repository ---
|
|
||||||
echo Cloning repository: %REPO_URL_CANDLE% into %CLONE_DIR_CANDLE%...
|
|
||||||
git clone "%REPO_URL_CANDLE%" "%CLONE_DIR_CANDLE%"
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo ERROR: Failed to clone repository. Check Git and Dotnet installation and network connection.
|
|
||||||
goto :error
|
|
||||||
)
|
|
||||||
echo Repository cloned successfully.
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM --- Step 2: Navigate into the cloned directory ---
|
|
||||||
echo Changing directory to %CLONE_DIR_CANDLE%...
|
|
||||||
cd "%CLONE_DIR_CANDLE%"
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo ERROR: Failed to change directory to %CLONE_DIR_CANDLE%.
|
|
||||||
goto :error
|
|
||||||
)
|
|
||||||
echo Current directory: %CD%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM --- Step 3: Build the FHIR Candle server using Dotnet ---
|
|
||||||
echo ===> "Starting Dotnet build (Step 3)...""
|
|
||||||
dotnet publish -c Release -f net9.0 -o publish
|
|
||||||
echo ===> Dotnet command finished. Checking error level...
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo ERROR: Dotnet build failed. Check Dotnet SDK installation.
|
|
||||||
cd ..
|
|
||||||
goto :error
|
|
||||||
)
|
|
||||||
echo Dotnet build completed successfully. ErrorLevel: %errorlevel%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM --- Step 4: Navigate back to the parent directory ---
|
|
||||||
echo ===> "Changing directory back (Step 4)..."
|
|
||||||
cd ..
|
|
||||||
if errorlevel 1 (
|
|
||||||
echo ERROR: Failed to change back to the parent directory. ErrorLevel: %errorlevel%
|
|
||||||
goto :error
|
|
||||||
)
|
|
||||||
echo Current directory: %CD%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
) ELSE (
|
) ELSE (
|
||||||
echo Running Lite setup, skipping server build...
|
echo Running Lite setup, skipping HAPI FHIR build...
|
||||||
REM Ensure the server directories don't exist in Lite mode
|
REM Ensure the hapi-fhir-jpaserver directory doesn't exist or is empty if Lite mode is chosen after a standalone attempt
|
||||||
if exist "%CLONE_DIR_HAPI%" (
|
if exist "%CLONE_DIR%" (
|
||||||
echo Found existing HAPI directory in Lite mode. Removing it to avoid build issues...
|
echo Found existing HAPI directory in Lite mode. Removing it to avoid build issues...
|
||||||
rmdir /s /q "%CLONE_DIR_HAPI%"
|
rmdir /s /q "%CLONE_DIR%"
|
||||||
)
|
)
|
||||||
if exist "%CLONE_DIR_CANDLE%" (
|
REM Create empty target directories expected by Dockerfile COPY, even if not used
|
||||||
echo Found existing Candle directory in Lite mode. Removing it to avoid build issues...
|
mkdir "%CLONE_DIR%\target\classes" 2> nul
|
||||||
rmdir /s /q "%CLONE_DIR_CANDLE%"
|
mkdir "%CLONE_DIR%\custom" 2> nul
|
||||||
)
|
REM Create a placeholder empty WAR file to satisfy Dockerfile COPY
|
||||||
REM Create empty placeholder files to satisfy Dockerfile COPY commands in Lite mode.
|
echo. > "%CLONE_DIR%\target\ROOT.war"
|
||||||
mkdir "%CLONE_DIR_HAPI%\target\classes" 2> nul
|
echo. > "%CLONE_DIR%\target\classes\application.yaml"
|
||||||
mkdir "%CLONE_DIR_HAPI%\custom" 2> nul
|
|
||||||
echo. > "%CLONE_DIR_HAPI%\target\ROOT.war"
|
|
||||||
echo. > "%CLONE_DIR_HAPI%\target\classes\application.yaml"
|
|
||||||
mkdir "%CLONE_DIR_CANDLE%\publish" 2> nul
|
|
||||||
echo. > "%CLONE_DIR_CANDLE%\publish\fhir-candle.dll"
|
|
||||||
echo Placeholder files created for Lite mode build.
|
echo Placeholder files created for Lite mode build.
|
||||||
echo.
|
echo.
|
||||||
)
|
)
|
||||||
|
|
||||||
REM === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL ===
|
REM === Modify docker-compose.yml to set APP_MODE ===
|
||||||
echo Updating docker-compose.yml with APP_MODE=%APP_MODE% and HAPI_FHIR_URL...
|
echo Updating docker-compose.yml with APP_MODE=%APP_MODE%...
|
||||||
(
|
(
|
||||||
echo version: '3.8'
|
echo version: '3.8'
|
||||||
echo services:
|
echo services:
|
||||||
echo fhirflare:
|
echo fhirflare:
|
||||||
echo build:
|
echo build:
|
||||||
echo context: .
|
echo context: .
|
||||||
IF "%SERVER_TYPE%"=="hapi" (
|
echo dockerfile: Dockerfile
|
||||||
echo dockerfile: Dockerfile.hapi
|
|
||||||
) ELSE IF "%SERVER_TYPE%"=="candle" (
|
|
||||||
echo dockerfile: Dockerfile.candle
|
|
||||||
) ELSE (
|
|
||||||
echo dockerfile: Dockerfile.lite
|
|
||||||
)
|
|
||||||
echo ports:
|
echo ports:
|
||||||
IF "%SERVER_TYPE%"=="candle" (
|
|
||||||
echo - "5000:5000"
|
echo - "5000:5000"
|
||||||
echo - "5001:5826"
|
echo - "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
||||||
) ELSE (
|
|
||||||
echo - "5000:5000"
|
|
||||||
echo - "8080:8080"
|
|
||||||
)
|
|
||||||
echo volumes:
|
echo volumes:
|
||||||
echo - ./instance:/app/instance
|
echo - ./instance:/app/instance
|
||||||
echo - ./static/uploads:/app/static/uploads
|
echo - ./static/uploads:/app/static/uploads
|
||||||
IF "%SERVER_TYPE%"=="hapi" (
|
|
||||||
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
echo - ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||||
)
|
|
||||||
echo - ./logs:/app/logs
|
echo - ./logs:/app/logs
|
||||||
IF "%SERVER_TYPE%"=="hapi" (
|
|
||||||
echo - ./hapi-fhir-jpaserver/target/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
|
|
||||||
echo - ./hapi-fhir-jpaserver/target/classes/application.yaml:/usr/local/tomcat/conf/application.yaml
|
|
||||||
) ELSE IF "%SERVER_TYPE%"=="candle" (
|
|
||||||
echo - ./fhir-candle/publish/:/app/fhir-candle-publish/
|
|
||||||
)
|
|
||||||
echo environment:
|
echo environment:
|
||||||
echo - FLASK_APP=app.py
|
echo - FLASK_APP=app.py
|
||||||
echo - FLASK_ENV=development
|
echo - FLASK_ENV=development
|
||||||
echo - NODE_PATH=/usr/lib/node_modules
|
echo - NODE_PATH=/usr/lib/node_modules
|
||||||
echo - APP_MODE=%APP_MODE%
|
echo - APP_MODE=%APP_MODE%
|
||||||
echo - APP_BASE_URL=http://localhost:5000
|
|
||||||
IF DEFINED CUSTOM_FHIR_URL_VAL (
|
|
||||||
echo - HAPI_FHIR_URL=!CUSTOM_FHIR_URL_VAL!
|
|
||||||
) ELSE (
|
|
||||||
IF "%SERVER_TYPE%"=="candle" (
|
|
||||||
echo - HAPI_FHIR_URL=http://localhost:5826/fhir/%CANDLE_FHIR_VERSION%
|
|
||||||
echo - ASPNETCORE_URLS=http://0.0.0.0:5826
|
|
||||||
) ELSE (
|
|
||||||
echo - HAPI_FHIR_URL=http://localhost:8080/fhir
|
|
||||||
)
|
|
||||||
)
|
|
||||||
echo command: supervisord -c /etc/supervisord.conf
|
echo command: supervisord -c /etc/supervisord.conf
|
||||||
) > docker-compose.yml.tmp
|
) > docker-compose.yml.tmp
|
||||||
|
|
||||||
|
|||||||
24
Dockerfile
24
Dockerfile
@ -3,30 +3,17 @@ FROM tomcat:10.1-jdk17
|
|||||||
|
|
||||||
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip python3-venv curl coreutils git \
|
python3 python3-pip python3-venv curl coreutils \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ADDED: Install the Dotnet SDK for the FHIR Candle server
|
|
||||||
# This makes the image a universal base for either server type
|
|
||||||
RUN apt-get update && apt-get install -y dotnet-sdk-6.0
|
|
||||||
|
|
||||||
# Install specific versions of GoFSH and SUSHI
|
# Install specific versions of GoFSH and SUSHI
|
||||||
|
# REMOVED pip install fhirpath from this line
|
||||||
RUN npm install -g gofsh fsh-sushi
|
RUN npm install -g gofsh fsh-sushi
|
||||||
|
|
||||||
# ADDED: Download the latest HL7 FHIR Validator CLI
|
|
||||||
RUN mkdir -p /app/validator_cli
|
|
||||||
WORKDIR /app/validator_cli
|
|
||||||
# Download the validator JAR and a separate checksum file for verification
|
|
||||||
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
|
||||||
|
|
||||||
# Set permissions for the downloaded file
|
|
||||||
RUN chmod 755 validator_cli.jar
|
|
||||||
|
|
||||||
# Change back to the main app directory for the next steps
|
|
||||||
WORKDIR /app
|
|
||||||
# Set up Python environment
|
# Set up Python environment
|
||||||
|
WORKDIR /app
|
||||||
RUN python3 -m venv /app/venv
|
RUN python3 -m venv /app/venv
|
||||||
ENV PATH="/app/venv/bin:$PATH"
|
ENV PATH="/app/venv/bin:$PATH"
|
||||||
|
|
||||||
@ -57,9 +44,6 @@ COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application
|
|||||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
||||||
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
||||||
|
|
||||||
# ADDED: Copy pre-built Candle DLL files
|
|
||||||
COPY fhir-candle/publish/ /app/fhir-candle-publish/
|
|
||||||
|
|
||||||
# Install supervisord
|
# Install supervisord
|
||||||
RUN pip install supervisor
|
RUN pip install supervisor
|
||||||
|
|
||||||
@ -67,7 +51,7 @@ RUN pip install supervisor
|
|||||||
COPY supervisord.conf /etc/supervisord.conf
|
COPY supervisord.conf /etc/supervisord.conf
|
||||||
|
|
||||||
# Expose ports
|
# Expose ports
|
||||||
EXPOSE 5000 8080 5001
|
EXPOSE 5000 8080
|
||||||
|
|
||||||
# Start supervisord
|
# Start supervisord
|
||||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
||||||
@ -1,64 +0,0 @@
|
|||||||
# Base image with Python and Dotnet
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:9.0
|
|
||||||
|
|
||||||
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
python3 python3-pip python3-venv \
|
|
||||||
default-jre-headless \
|
|
||||||
curl coreutils git \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install specific versions of GoFSH and SUSHI
|
|
||||||
RUN npm install -g gofsh fsh-sushi
|
|
||||||
|
|
||||||
# ADDED: Download the latest HL7 FHIR Validator CLI
|
|
||||||
RUN mkdir -p /app/validator_cli
|
|
||||||
WORKDIR /app/validator_cli
|
|
||||||
# Download the validator JAR and a separate checksum file for verification
|
|
||||||
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
|
||||||
|
|
||||||
# Set permissions for the downloaded file
|
|
||||||
RUN chmod 755 validator_cli.jar
|
|
||||||
|
|
||||||
# Change back to the main app directory for the next steps
|
|
||||||
WORKDIR /app
|
|
||||||
# Set up Python environment
|
|
||||||
RUN python3 -m venv /app/venv
|
|
||||||
ENV PATH="/app/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
|
||||||
RUN pip uninstall -y fhirpath || true
|
|
||||||
# ADDED: Install the new fhirpathpy library
|
|
||||||
RUN pip install --no-cache-dir fhirpathpy
|
|
||||||
|
|
||||||
# Copy Flask files
|
|
||||||
COPY requirements.txt .
|
|
||||||
# Install requirements (including Pydantic - check version compatibility if needed)
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
COPY app.py .
|
|
||||||
COPY services.py .
|
|
||||||
COPY forms.py .
|
|
||||||
COPY package.py .
|
|
||||||
COPY templates/ templates/
|
|
||||||
COPY static/ static/
|
|
||||||
COPY tests/ tests/
|
|
||||||
|
|
||||||
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
|
|
||||||
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
|
|
||||||
|
|
||||||
# Copy pre-built Candle DLL files
|
|
||||||
COPY fhir-candle/publish/ /app/fhir-candle-publish/
|
|
||||||
|
|
||||||
# Install supervisord
|
|
||||||
RUN pip install supervisor
|
|
||||||
|
|
||||||
# Configure supervisord
|
|
||||||
COPY supervisord.conf /etc/supervisord.conf
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 5000 5001
|
|
||||||
|
|
||||||
# Start supervisord
|
|
||||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
# Base image with Python, Java, and Maven
|
|
||||||
FROM tomcat:10.1-jdk17
|
|
||||||
|
|
||||||
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
python3 python3-pip python3-venv curl coreutils git maven \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install specific versions of GoFSH and SUSHI
|
|
||||||
RUN npm install -g gofsh fsh-sushi
|
|
||||||
|
|
||||||
# ADDED: Download the latest HL7 FHIR Validator CLI
|
|
||||||
RUN mkdir -p /app/validator_cli
|
|
||||||
WORKDIR /app/validator_cli
|
|
||||||
# Download the validator JAR and a separate checksum file for verification
|
|
||||||
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
|
||||||
|
|
||||||
# Set permissions for the downloaded file
|
|
||||||
RUN chmod 755 validator_cli.jar
|
|
||||||
|
|
||||||
# Change back to the main app directory for the next steps
|
|
||||||
WORKDIR /app
|
|
||||||
# Set up Python environment
|
|
||||||
RUN python3 -m venv /app/venv
|
|
||||||
ENV PATH="/app/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
|
||||||
RUN pip uninstall -y fhirpath || true
|
|
||||||
# ADDED: Install the new fhirpathpy library
|
|
||||||
RUN pip install --no-cache-dir fhirpathpy
|
|
||||||
|
|
||||||
# Copy Flask files
|
|
||||||
COPY requirements.txt .
|
|
||||||
# Install requirements (including Pydantic - check version compatibility if needed)
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
COPY app.py .
|
|
||||||
COPY services.py .
|
|
||||||
COPY forms.py .
|
|
||||||
COPY package.py .
|
|
||||||
COPY templates/ templates/
|
|
||||||
COPY static/ static/
|
|
||||||
COPY tests/ tests/
|
|
||||||
|
|
||||||
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
|
|
||||||
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
|
|
||||||
|
|
||||||
# Copy pre-built HAPI WAR and configuration
|
|
||||||
COPY hapi-fhir-jpaserver/target/ROOT.war /usr/local/tomcat/webapps/
|
|
||||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/conf/
|
|
||||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application.yaml
|
|
||||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
|
||||||
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
|
||||||
|
|
||||||
# Install supervisord
|
|
||||||
RUN pip install supervisor
|
|
||||||
|
|
||||||
# Configure supervisord
|
|
||||||
COPY supervisord.conf /etc/supervisord.conf
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 5000 8080
|
|
||||||
|
|
||||||
# Start supervisord
|
|
||||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
# Base image with Python and Node.js
|
|
||||||
FROM python:3.9-slim
|
|
||||||
|
|
||||||
# Install JRE and other dependencies for the validator
|
|
||||||
# We need to install `default-jre-headless` to get a minimal Java Runtime Environment
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
default-jre-headless \
|
|
||||||
curl coreutils git \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install specific versions of GoFSH and SUSHI
|
|
||||||
RUN npm install -g gofsh fsh-sushi
|
|
||||||
|
|
||||||
# ADDED: Download the latest HL7 FHIR Validator CLI
|
|
||||||
RUN mkdir -p /app/validator_cli
|
|
||||||
WORKDIR /app/validator_cli
|
|
||||||
# Download the validator JAR and a separate checksum file for verification
|
|
||||||
RUN curl -L -o validator_cli.jar "https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
|
||||||
|
|
||||||
# Set permissions for the downloaded file
|
|
||||||
RUN chmod 755 validator_cli.jar
|
|
||||||
|
|
||||||
# Change back to the main app directory for the next steps
|
|
||||||
WORKDIR /app
|
|
||||||
# Set up Python environment
|
|
||||||
RUN python3 -m venv /app/venv
|
|
||||||
ENV PATH="/app/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
|
||||||
RUN pip uninstall -y fhirpath || true
|
|
||||||
# ADDED: Install the new fhirpathpy library
|
|
||||||
RUN pip install --no-cache-dir fhirpathpy
|
|
||||||
|
|
||||||
# Copy Flask files
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
COPY app.py .
|
|
||||||
COPY services.py .
|
|
||||||
COPY forms.py .
|
|
||||||
COPY package.py .
|
|
||||||
COPY templates/ templates/
|
|
||||||
COPY static/ static/
|
|
||||||
COPY tests/ tests/
|
|
||||||
|
|
||||||
# Ensure necessary directories are writable
|
|
||||||
RUN mkdir -p /app/static/uploads /app/logs && chmod 777 /app/static/uploads /app/logs
|
|
||||||
|
|
||||||
# Install supervisord
|
|
||||||
RUN pip install supervisor
|
|
||||||
|
|
||||||
# Configure supervisord
|
|
||||||
COPY supervisord.conf /etc/supervisord.conf
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Start supervisord
|
|
||||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
|
||||||
448
README.md
448
README.md
@ -1,69 +1,25 @@
|
|||||||
# FHIRFLARE IG Toolkit
|
# FHIRFLARE IG Toolkit
|
||||||

|
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) and test data. It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, converting FHIR resources to FHIR Shorthand (FSH), uploading complex test data sets with dependency management, and retrieving/splitting FHIR bundles. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
|
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) and test data. It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, converting FHIR resources to FHIR Shorthand (FSH), and uploading complex test data sets with dependency management. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
|
||||||
|
|
||||||
The application can run in four modes:
|
The application can run in two modes:
|
||||||
|
|
||||||
|
* **Standalone:** Includes a Dockerized Flask frontend, SQLite database, and an embedded HAPI FHIR server for local validation and interaction.
|
||||||
* Installation Modes (Lite, Custom URL, Hapi, and Candle)
|
* **Lite:** Includes only the Dockerized Flask frontend and SQLite database, excluding the local HAPI FHIR server. Requires connection to external FHIR servers for certain features.
|
||||||
This toolkit now offers four primary installation modes, configured via a simple command-line prompt during setup:
|
|
||||||
|
|
||||||
Lite Mode
|
|
||||||
|
|
||||||
Includes the Flask frontend, SQLite database, and core tooling (GoFSH, SUSHI) without an embedded FHIR server.
|
|
||||||
|
|
||||||
Requires you to provide a URL for an external FHIR server when using features like the FHIR API Explorer and validation.
|
|
||||||
|
|
||||||
Does not require Git, Maven, or .NET for setup.
|
|
||||||
|
|
||||||
Ideal for users who will always connect to an existing external FHIR server.
|
|
||||||
|
|
||||||
Custom URL Mode
|
|
||||||
|
|
||||||
Similar to Lite Mode, but prompts you for a specific external FHIR server URL to use as the default for the toolkit.
|
|
||||||
|
|
||||||
The application will be pre-configured to use your custom URL for all FHIR-related operations.
|
|
||||||
|
|
||||||
Does not require Git, Maven, or .NET for setup.
|
|
||||||
|
|
||||||
Hapi Mode
|
|
||||||
|
|
||||||
Includes the full FHIRFLARE toolkit and an embedded HAPI FHIR server.
|
|
||||||
|
|
||||||
The docker-compose configuration will proxy requests to the internal HAPI server, which is accessible via http://localhost:8080/fhir.
|
|
||||||
|
|
||||||
Requires Git and Maven during the initial build process to compile the HAPI FHIR server.
|
|
||||||
|
|
||||||
Ideal for users who want a self-contained, offline development and testing environment.
|
|
||||||
|
|
||||||
Candle Mode
|
|
||||||
|
|
||||||
Includes the full FHIRFLARE toolkit and an embedded FHIR Candle server.
|
|
||||||
|
|
||||||
The docker-compose configuration will proxy requests to the internal Candle server, which is accessible via http://localhost:5001/fhir/<version>.
|
|
||||||
|
|
||||||
Requires Git and the .NET SDK during the initial build process.
|
|
||||||
|
|
||||||
Ideal for users who want to use the .NET-based FHIR server for development and testing.
|
|
||||||
|
|
||||||
## Installation Modes (Lite vs. Standalone)
|
## Installation Modes (Lite vs. Standalone)
|
||||||
|
|
||||||
This toolkit offers two primary installation modes to suit different needs:
|
This toolkit offers two primary installation modes to suit different needs:
|
||||||
|
|
||||||
* **Standalone Version - Hapi / Candle:**
|
* **Standalone Version:**
|
||||||
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
|
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
|
||||||
* Allows for local FHIR resource validation using HAPI FHIR's capabilities.
|
* Allows for local FHIR resource validation using HAPI FHIR's capabilities.
|
||||||
* Enables the "Use Local HAPI" option in the FHIR API Explorer and FHIR UI Operations pages, proxying requests to the internal HAPI server (`http://localhost:8080/fhir`).
|
* Enables the "Use Local HAPI" option in the FHIR API Explorer and FHIR UI Operations pages, proxying requests to the internal HAPI server (`http://localhost:8080/fhir`).
|
||||||
* Requires Git and Maven during the initial build process (via the `.bat` script or manual steps) to prepare the HAPI FHIR server.
|
* Requires Git and Maven during the initial build process (via the `.bat` script or manual steps) to prepare the HAPI FHIR server.
|
||||||
* Ideal for users who want a self-contained environment for development and testing or who don't have readily available external FHIR servers.
|
* Ideal for users who want a self-contained environment for development and testing or who don't have readily available external FHIR servers.
|
||||||
|
|
||||||
* **Standalone Version - Custom Mode:**
|
|
||||||
* Includes the full FHIRFLARE Toolkit application **and** point the Environmet variable at your chosen custom fhir url endpoint allowing for Custom setups
|
|
||||||
|
|
||||||
|
|
||||||
* **Lite Version:**
|
* **Lite Version:**
|
||||||
* Includes the FHIRFLARE Toolkit application **without** the embedded HAPI FHIR server.
|
* Includes the FHIRFLARE Toolkit application **without** the embedded HAPI FHIR server.
|
||||||
* Requires users to provide URLs for external FHIR servers when using features like the FHIR API Explorer and FHIR UI Operations pages. The "Use Local HAPI" option will be disabled in the UI.
|
* Requires users to provide URLs for external FHIR servers when using features like the FHIR API Explorer and FHIR UI Operations pages. The "Use Local HAPI" option will be disabled in the UI.
|
||||||
@ -74,12 +30,6 @@ This toolkit offers two primary installation modes to suit different needs:
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `current`) and dependency pulling modes (Recursive, Patch Canonical, Tree Shaking).
|
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `current`) and dependency pulling modes (Recursive, Patch Canonical, Tree Shaking).
|
||||||
* **Enhanced Package Search and Import:**
|
|
||||||
* Interactive page (`/search-and-import`) to search for FHIR IG packages from configured registries.
|
|
||||||
* Displays package details, version history, dependencies, and dependents.
|
|
||||||
* Utilizes a local database cache (`CachedPackage`) for faster subsequent searches.
|
|
||||||
* Background task to refresh the package cache from registries (`/api/refresh-cache-task`).
|
|
||||||
* Direct import from search results.
|
|
||||||
* **Manage IGs:** View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
|
* **Manage IGs:** View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
|
||||||
* **Process IGs:** Extract resource types, profiles, must-support elements, examples, and profile relationships (`structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile`).
|
* **Process IGs:** Extract resource types, profiles, must-support elements, examples, and profile relationships (`structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile`).
|
||||||
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature). *Note: Lite version uses local SD checks only.*
|
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature). *Note: Lite version uses local SD checks only.*
|
||||||
@ -105,19 +55,9 @@ This toolkit offers two primary installation modes to suit different needs:
|
|||||||
* Handles large numbers of files using a custom form parser.
|
* Handles large numbers of files using a custom form parser.
|
||||||
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI (configurable).
|
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI (configurable).
|
||||||
* **FSH Converter:** Convert FHIR JSON/XML resources to FHIR Shorthand (FSH) using GoFSH, with advanced options (Package context, Output styles, Log levels, FHIR versions, Fishing Trip, Dependencies, Indentation, Meta Profile handling, Alias File, No Alias). Includes a waiting spinner.
|
* **FSH Converter:** Convert FHIR JSON/XML resources to FHIR Shorthand (FSH) using GoFSH, with advanced options (Package context, Output styles, Log levels, FHIR versions, Fishing Trip, Dependencies, Indentation, Meta Profile handling, Alias File, No Alias). Includes a waiting spinner.
|
||||||
* **Retrieve and Split Bundles:**
|
|
||||||
* Retrieve specified resource types as bundles from a FHIR server.
|
|
||||||
* Optionally fetch referenced resources, either individually or as full bundles for each referenced type.
|
|
||||||
* Split uploaded ZIP files containing bundles into individual resource JSON files.
|
|
||||||
* Download retrieved/split resources as a ZIP archive.
|
|
||||||
* Streaming progress log via the UI for retrieval operations.
|
|
||||||
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (simple GET/POST/PUT/DELETE) and "FHIR UI Operations" (Swagger-like interface based on CapabilityStatement). *Note: Lite version requires custom server URLs.*
|
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (simple GET/POST/PUT/DELETE) and "FHIR UI Operations" (Swagger-like interface based on CapabilityStatement). *Note: Lite version requires custom server URLs.*
|
||||||
* **HAPI FHIR Configuration (Standalone Mode):**
|
* **API Support:** RESTful API endpoints for importing, pushing, retrieving metadata, validating, and uploading test data.
|
||||||
* A dedicated page (`/config-hapi`) to view and edit the `application.yaml` configuration for the embedded HAPI FHIR server.
|
* **Live Console:** Real-time logs for push, validation, upload test data, and FSH conversion operations.
|
||||||
* Allows modification of HAPI FHIR properties directly from the UI.
|
|
||||||
* Option to restart the HAPI FHIR server (Tomcat) to apply changes.
|
|
||||||
* **API Support:** RESTful API endpoints for importing, pushing, retrieving metadata, validating, uploading test data, and retrieving/splitting bundles.
|
|
||||||
* **Live Console:** Real-time logs for push, validation, upload test data, FSH conversion, and bundle retrieval operations.
|
|
||||||
* **Configurable Behavior:** Control validation modes, display options via `app.config`.
|
* **Configurable Behavior:** Control validation modes, display options via `app.config`.
|
||||||
* **Theming:** Supports light and dark modes.
|
* **Theming:** Supports light and dark modes.
|
||||||
|
|
||||||
@ -161,9 +101,8 @@ docker run -d \
|
|||||||
-v ./logs:/app/logs \
|
-v ./logs:/app/logs \
|
||||||
--name fhirflare-lite \
|
--name fhirflare-lite \
|
||||||
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
|
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
|
||||||
Standalone Version (Includes local HAPI FHIR):
|
|
||||||
|
|
||||||
Bash
|
Standalone Version (Includes local HAPI FHIR):
|
||||||
|
|
||||||
# Pull the latest Standalone image
|
# Pull the latest Standalone image
|
||||||
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
||||||
@ -180,6 +119,7 @@ docker run -d \
|
|||||||
-v ./logs:/app/logs \
|
-v ./logs:/app/logs \
|
||||||
--name fhirflare-standalone \
|
--name fhirflare-standalone \
|
||||||
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
||||||
|
|
||||||
Building from Source (Developers)
|
Building from Source (Developers)
|
||||||
Using Windows .bat Scripts (Standalone Version Only):
|
Using Windows .bat Scripts (Standalone Version Only):
|
||||||
|
|
||||||
@ -187,252 +127,258 @@ First Time Setup:
|
|||||||
|
|
||||||
Run Build and Run for first time.bat:
|
Run Build and Run for first time.bat:
|
||||||
|
|
||||||
Code snippet
|
|
||||||
|
|
||||||
cd "<project folder>"
|
cd "<project folder>"
|
||||||
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
|
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
|
||||||
copy .\\hapi-fhir-Setup\\target\\classes\\application.yaml .\\hapi-fhir-jpaserver\\target\\classes\\application.yaml
|
copy .\hapi-fhir-Setup\target\classes\application.yaml .\hapi-fhir-jpaserver\target\classes\application.yaml
|
||||||
mvn clean package -DskipTests=true -Pboot
|
mvn clean package -DskipTests=true -Pboot
|
||||||
docker-compose build --no-cache
|
docker-compose build --no-cache
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers.
|
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers.
|
||||||
|
|
||||||
Subsequent Runs:
|
Subsequent Runs:
|
||||||
|
|
||||||
Run Run.bat:
|
Run Run.bat:
|
||||||
|
|
||||||
Code snippet
|
|
||||||
|
|
||||||
cd "<project folder>"
|
cd "<project folder>"
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080).
|
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080).
|
||||||
|
|
||||||
Access the Application:
|
Access the Application:
|
||||||
|
|
||||||
Flask UI: http://localhost:5000
|
Flask UI: http://localhost:5000
|
||||||
|
|
||||||
HAPI FHIR server: http://localhost:8080
|
HAPI FHIR server: http://localhost:8080
|
||||||
|
|
||||||
Manual Setup (Linux/MacOS/Windows):
|
Manual Setup (Linux/MacOS/Windows):
|
||||||
|
|
||||||
Preparation (Standalone Version Only):
|
Preparation (Standalone Version Only):
|
||||||
|
|
||||||
Bash
|
|
||||||
|
|
||||||
cd <project folder>
|
cd <project folder>
|
||||||
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
|
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
|
||||||
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml
|
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml
|
||||||
Build:
|
|
||||||
|
|
||||||
Bash
|
Build:
|
||||||
|
|
||||||
# Build HAPI FHIR (Standalone Version Only)
|
# Build HAPI FHIR (Standalone Version Only)
|
||||||
mvn clean package -DskipTests=true -Pboot
|
mvn clean package -DskipTests=true -Pboot
|
||||||
|
|
||||||
# Build Docker Image (Specify APP_MODE=lite in docker-compose.yml for Lite version)
|
# Build Docker Image (Specify APP_MODE=lite in docker-compose.yml for Lite version)
|
||||||
docker-compose build --no-cache
|
docker-compose build --no-cache
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
Bash
|
|
||||||
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
Access the Application:
|
Access the Application:
|
||||||
|
|
||||||
Flask UI: http://localhost:5000
|
Flask UI: http://localhost:5000
|
||||||
|
|
||||||
HAPI FHIR server (Standalone only): http://localhost:8080
|
HAPI FHIR server (Standalone only): http://localhost:8080
|
||||||
|
|
||||||
Local Development (Without Docker):
|
Local Development (Without Docker):
|
||||||
|
|
||||||
Clone the Repository:
|
Clone the Repository:
|
||||||
|
|
||||||
Bash
|
|
||||||
|
|
||||||
git clone [https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git](https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git)
|
git clone [https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git](https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git)
|
||||||
cd FHIRFLARE-IG-Toolkit
|
cd FHIRFLARE-IG-Toolkit
|
||||||
Install Dependencies:
|
|
||||||
|
|
||||||
Bash
|
Install Dependencies:
|
||||||
|
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
Install Node.js, GoFSH, and SUSHI (for FSH Converter):
|
|
||||||
|
|
||||||
Bash
|
Install Node.js, GoFSH, and SUSHI (for FSH Converter):
|
||||||
|
|
||||||
# Example for Debian/Ubuntu
|
# Example for Debian/Ubuntu
|
||||||
curl -fsSL [https://deb.nodesource.com/setup_18.x](https://deb.nodesource.com/setup_18.x) | sudo bash -
|
curl -fsSL [https://deb.nodesource.com/setup_18.x](https://deb.nodesource.com/setup_18.x) | sudo bash -
|
||||||
sudo apt-get install -y nodejs
|
sudo apt-get install -y nodejs
|
||||||
# Install globally
|
# Install globally
|
||||||
npm install -g gofsh fsh-sushi
|
npm install -g gofsh fsh-sushi
|
||||||
Set Environment Variables:
|
|
||||||
|
|
||||||
Bash
|
Set Environment Variables:
|
||||||
|
|
||||||
export FLASK_SECRET_KEY='your-secure-secret-key'
|
export FLASK_SECRET_KEY='your-secure-secret-key'
|
||||||
export API_KEY='your-api-key'
|
export API_KEY='your-api-key'
|
||||||
# Optional: Set APP_MODE to 'lite' if desired
|
# Optional: Set APP_MODE to 'lite' if desired
|
||||||
# export APP_MODE='lite'
|
# export APP_MODE='lite'
|
||||||
Initialize Directories:
|
|
||||||
|
|
||||||
Bash
|
Initialize Directories:
|
||||||
|
|
||||||
mkdir -p instance static/uploads logs
|
mkdir -p instance static/uploads logs
|
||||||
# Ensure write permissions if needed
|
# Ensure write permissions if needed
|
||||||
# chmod -R 777 instance static/uploads logs
|
# chmod -R 777 instance static/uploads logs
|
||||||
Run the Application:
|
|
||||||
|
|
||||||
Bash
|
Run the Application:
|
||||||
|
|
||||||
export FLASK_APP=app.py
|
export FLASK_APP=app.py
|
||||||
flask run
|
flask run
|
||||||
|
|
||||||
Access at http://localhost:5000.
|
Access at http://localhost:5000.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
Import an IG
|
Import an IG
|
||||||
### Search, View Details, and Import Packages
|
Navigate to Import IG (/import-ig).
|
||||||
Navigate to **Search and Import Packages** (`/search-and-import`).
|
|
||||||
1. The page will load a list of available FHIR Implementation Guide packages from a local cache or by fetching from configured registries.
|
|
||||||
* A loading animation and progress messages are shown if fetching from registries.
|
|
||||||
* The timestamp of the last cache update is displayed.
|
|
||||||
2. Use the search bar to filter packages by name or author.
|
|
||||||
3. Packages are paginated for easier Browse.
|
|
||||||
4. For each package, you can:
|
|
||||||
* View its latest official and absolute versions.
|
|
||||||
* Click on the package name to navigate to a **detailed view** (`/package-details/<name>`) showing:
|
|
||||||
* Comprehensive metadata (author, FHIR version, canonical URL, description).
|
|
||||||
* A full list of available versions with publication dates.
|
|
||||||
* Declared dependencies.
|
|
||||||
* Other packages that depend on it (dependents).
|
|
||||||
* Version history (logs).
|
|
||||||
* Directly import a specific version using the "Import" button on the search page or the details page.
|
|
||||||
5. **Cache Management:**
|
|
||||||
* A "Clear & Refresh Cache" button is available to trigger a background task (`/api/refresh-cache-task`) that clears the local database and in-memory cache and fetches the latest package information from all configured registries. Progress is shown via a live log.
|
|
||||||
|
|
||||||
Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
|
Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
|
||||||
|
|
||||||
Choose a dependency mode:
|
Choose a dependency mode:
|
||||||
|
|
||||||
Current Recursive: Import all dependencies listed in package.json recursively.
|
Current Recursive: Import all dependencies listed in package.json recursively.
|
||||||
|
|
||||||
Patch Canonical Versions: Import only canonical FHIR packages (e.g., hl7.fhir.r4.core).
|
Patch Canonical Versions: Import only canonical FHIR packages (e.g., hl7.fhir.r4.core).
|
||||||
|
|
||||||
Tree Shaking: Import only dependencies containing resources actually used by the main package.
|
Tree Shaking: Import only dependencies containing resources actually used by the main package.
|
||||||
|
|
||||||
Click Import to download the package and dependencies.
|
Click Import to download the package and dependencies.
|
||||||
|
|
||||||
Manage IGs
|
Manage IGs
|
||||||
Go to Manage FHIR Packages (/view-igs) to view downloaded and processed IGs.
|
Go to Manage FHIR Packages (/view-igs) to view downloaded and processed IGs.
|
||||||
|
|
||||||
Actions:
|
Actions:
|
||||||
|
|
||||||
Process: Extract metadata (resource types, profiles, must-support elements, examples).
|
Process: Extract metadata (resource types, profiles, must-support elements, examples).
|
||||||
|
|
||||||
Unload: Remove processed IG data from the database.
|
Unload: Remove processed IG data from the database.
|
||||||
|
|
||||||
Delete: Remove package files from the filesystem.
|
Delete: Remove package files from the filesystem.
|
||||||
|
|
||||||
Duplicates are highlighted for resolution.
|
Duplicates are highlighted for resolution.
|
||||||
|
|
||||||
View Processed IGs
|
View Processed IGs
|
||||||
After processing, view IG details (/view-ig/<id>), including:
|
After processing, view IG details (/view-ig/<id>), including:
|
||||||
|
|
||||||
Resource types and profiles.
|
Resource types and profiles.
|
||||||
|
|
||||||
Must-support elements and examples.
|
Must-support elements and examples.
|
||||||
|
|
||||||
Profile relationships (compliesWithProfile, imposeProfile) if enabled (DISPLAY_PROFILE_RELATIONSHIPS).
|
Profile relationships (compliesWithProfile, imposeProfile) if enabled (DISPLAY_PROFILE_RELATIONSHIPS).
|
||||||
|
|
||||||
Interactive StructureDefinition viewer (Differential, Snapshot, Must Support, Key Elements, Constraints, Terminology, Search Params).
|
Interactive StructureDefinition viewer (Differential, Snapshot, Must Support, Key Elements, Constraints, Terminology, Search Params).
|
||||||
|
|
||||||
Validate FHIR Resources/Bundles
|
Validate FHIR Resources/Bundles
|
||||||
Navigate to Validate FHIR Sample (/validate-sample).
|
Navigate to Validate FHIR Sample (/validate-sample).
|
||||||
|
|
||||||
Select a package (e.g., hl7.fhir.au.core#1.1.0-preview).
|
Select a package (e.g., hl7.fhir.au.core#1.1.0-preview).
|
||||||
|
|
||||||
Choose Single Resource or Bundle mode.
|
Choose Single Resource or Bundle mode.
|
||||||
|
|
||||||
Paste or upload FHIR JSON/XML (e.g., a Patient resource).
|
Paste or upload FHIR JSON/XML (e.g., a Patient resource).
|
||||||
Submit to view validation errors/warnings. Note: Alpha feature; report issues to GitHub (remove PHI).
|
|
||||||
|
Submit to view validation errors/warnings.
|
||||||
|
|
||||||
|
Note: Alpha feature; report issues to GitHub (remove PHI).
|
||||||
|
|
||||||
Push IGs to a FHIR Server
|
Push IGs to a FHIR Server
|
||||||
Go to Push IGs (/push-igs).
|
Go to Push IGs (/push-igs).
|
||||||
|
|
||||||
Select a downloaded package.
|
Select a downloaded package.
|
||||||
|
|
||||||
Enter the Target FHIR Server URL.
|
Enter the Target FHIR Server URL.
|
||||||
|
|
||||||
Configure Authentication (None, Bearer Token).
|
Configure Authentication (None, Bearer Token).
|
||||||
|
|
||||||
Choose options: Include Dependencies, Force Upload (skips comparison check), Dry Run, Verbose Log.
|
Choose options: Include Dependencies, Force Upload (skips comparison check), Dry Run, Verbose Log.
|
||||||
|
|
||||||
Optionally filter by Resource Types (comma-separated) or Skip Specific Files (paths within package, comma/newline separated).
|
Optionally filter by Resource Types (comma-separated) or Skip Specific Files (paths within package, comma/newline separated).
|
||||||
|
|
||||||
Click Push to FHIR Server to upload resources. Canonical resources are checked before upload. Identical resources are skipped unless Force Upload is checked.
|
Click Push to FHIR Server to upload resources. Canonical resources are checked before upload. Identical resources are skipped unless Force Upload is checked.
|
||||||
|
|
||||||
Monitor progress in the live console.
|
Monitor progress in the live console.
|
||||||
|
|
||||||
Upload Test Data
|
Upload Test Data
|
||||||
Navigate to Upload Test Data (/upload-test-data).
|
Navigate to Upload Test Data (/upload-test-data).
|
||||||
|
|
||||||
Enter the Target FHIR Server URL.
|
Enter the Target FHIR Server URL.
|
||||||
|
|
||||||
Configure Authentication (None, Bearer Token).
|
Configure Authentication (None, Bearer Token).
|
||||||
|
|
||||||
Select one or more .json, .xml files, or a single .zip file containing test resources.
|
Select one or more .json, .xml files, or a single .zip file containing test resources.
|
||||||
|
|
||||||
Optionally check Validate Resources Before Upload? and select a Validation Profile Package.
|
Optionally check Validate Resources Before Upload? and select a Validation Profile Package.
|
||||||
|
|
||||||
Choose Upload Mode:
|
Choose Upload Mode:
|
||||||
|
|
||||||
Individual Resources: Uploads each resource one by one in dependency order.
|
Individual Resources: Uploads each resource one by one in dependency order.
|
||||||
|
|
||||||
Transaction Bundle: Uploads all resources in a single transaction.
|
Transaction Bundle: Uploads all resources in a single transaction.
|
||||||
|
|
||||||
Optionally check Use Conditional Upload (Individual Mode Only)? to use If-Match headers for updates.
|
Optionally check Use Conditional Upload (Individual Mode Only)? to use If-Match headers for updates.
|
||||||
|
|
||||||
Choose Error Handling:
|
Choose Error Handling:
|
||||||
|
|
||||||
Stop on First Error: Halts the process if any validation or upload fails.
|
Stop on First Error: Halts the process if any validation or upload fails.
|
||||||
|
|
||||||
Continue on Error: Reports errors but attempts to process/upload remaining resources.
|
Continue on Error: Reports errors but attempts to process/upload remaining resources.
|
||||||
|
|
||||||
Click Upload and Process. The tool parses files, optionally validates, analyzes dependencies, topologically sorts resources, and uploads them according to selected options.
|
Click Upload and Process. The tool parses files, optionally validates, analyzes dependencies, topologically sorts resources, and uploads them according to selected options.
|
||||||
|
|
||||||
Monitor progress in the streaming log output.
|
Monitor progress in the streaming log output.
|
||||||
|
|
||||||
Convert FHIR to FSH
|
Convert FHIR to FSH
|
||||||
Navigate to FSH Converter (/fsh-converter).
|
Navigate to FSH Converter (/fsh-converter).
|
||||||
|
|
||||||
Optionally select a package for context (e.g., hl7.fhir.au.core#1.1.0-preview).
|
Optionally select a package for context (e.g., hl7.fhir.au.core#1.1.0-preview).
|
||||||
|
|
||||||
Choose input mode:
|
Choose input mode:
|
||||||
|
|
||||||
Upload File: Upload a FHIR JSON/XML file.
|
Upload File: Upload a FHIR JSON/XML file.
|
||||||
|
|
||||||
Paste Text: Paste FHIR JSON/XML content.
|
Paste Text: Paste FHIR JSON/XML content.
|
||||||
|
|
||||||
Configure options:
|
Configure options:
|
||||||
|
|
||||||
Output Style: file-per-definition, group-by-fsh-type, group-by-profile, single-file.
|
Output Style: file-per-definition, group-by-fsh-type, group-by-profile, single-file.
|
||||||
|
|
||||||
Log Level: error, warn, info, debug.
|
Log Level: error, warn, info, debug.
|
||||||
|
|
||||||
FHIR Version: R4, R4B, R5, or auto-detect.
|
FHIR Version: R4, R4B, R5, or auto-detect.
|
||||||
|
|
||||||
Fishing Trip: Enable round-trip validation with SUSHI, generating a comparison report.
|
Fishing Trip: Enable round-trip validation with SUSHI, generating a comparison report.
|
||||||
|
|
||||||
Dependencies: Specify additional packages (e.g., hl7.fhir.us.core@6.1.0, one per line).
|
Dependencies: Specify additional packages (e.g., hl7.fhir.us.core@6.1.0, one per line).
|
||||||
|
|
||||||
Indent Rules: Enable context path indentation for readable FSH.
|
Indent Rules: Enable context path indentation for readable FSH.
|
||||||
|
|
||||||
Meta Profile: Choose only-one, first, or none for meta.profile handling.
|
Meta Profile: Choose only-one, first, or none for meta.profile handling.
|
||||||
|
|
||||||
Alias File: Upload an FSH file with aliases (e.g., $MyAlias = http://example.org).
|
Alias File: Upload an FSH file with aliases (e.g., $MyAlias = http://example.org).
|
||||||
|
|
||||||
No Alias: Disable automatic alias generation.
|
No Alias: Disable automatic alias generation.
|
||||||
|
|
||||||
Click Convert to FSH to generate and display FSH output, with a waiting spinner (light/dark theme) during processing.
|
Click Convert to FSH to generate and display FSH output, with a waiting spinner (light/dark theme) during processing.
|
||||||
|
|
||||||
If Fishing Trip is enabled, view the comparison report via the "Click here for SUSHI Validation" badge button.
|
If Fishing Trip is enabled, view the comparison report via the "Click here for SUSHI Validation" badge button.
|
||||||
|
|
||||||
Download the result as a .fsh file.
|
Download the result as a .fsh file.
|
||||||
Retrieve and Split Bundles
|
|
||||||
Navigate to Retrieve/Split Data (/retrieve-split-data).
|
|
||||||
|
|
||||||
Retrieve Bundles from Server:
|
|
||||||
|
|
||||||
Enter the FHIR Server URL (defaults to the proxy if empty).
|
|
||||||
Select one or more Resource Types to retrieve (e.g., Patient, Observation).
|
|
||||||
Optionally check Fetch Referenced Resources.
|
|
||||||
If checked, further optionally check Fetch Full Reference Bundles to retrieve entire bundles for each referenced type (e.g., all Patients if a Patient is referenced) instead of individual resources by ID.
|
|
||||||
Click Retrieve Bundles.
|
|
||||||
Monitor progress in the streaming log. A ZIP file containing the retrieved bundles/resources will be prepared for download.
|
|
||||||
Split Uploaded Bundles:
|
|
||||||
|
|
||||||
Upload a ZIP file containing FHIR bundles (JSON format).
|
|
||||||
Click Split Bundles.
|
|
||||||
A ZIP file containing individual resources extracted from the bundles will be prepared for download.
|
|
||||||
Explore FHIR Operations
|
Explore FHIR Operations
|
||||||
Navigate to FHIR UI Operations (/fhir-ui-operations).
|
Navigate to FHIR UI Operations (/fhir-ui-operations).
|
||||||
|
|
||||||
Toggle between local HAPI (/fhir) or a custom FHIR server.
|
Toggle between local HAPI (/fhir) or a custom FHIR server.
|
||||||
Click Fetch Metadata to load the server’s CapabilityStatement.
|
|
||||||
Select a resource type (e.g., Patient, Observation) or System to view operations:
|
|
||||||
System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
|
|
||||||
Resource operations: GET Patient/:id, POST Observation/_search, etc.
|
|
||||||
Use Try it out to input parameters or request bodies, then Execute to view results in JSON, XML, or narrative formats.
|
|
||||||
|
|
||||||
### Configure Embedded HAPI FHIR Server (Standalone Mode)
|
Click Fetch Metadata to load the server’s CapabilityStatement.
|
||||||
For users running the **Standalone version**, which includes an embedded HAPI FHIR server.
|
|
||||||
1. Navigate to **Configure HAPI FHIR** (`/config-hapi`).
|
Select a resource type (e.g., Patient, Observation) or System to view operations:
|
||||||
2. The page displays the content of the HAPI FHIR server's `application.yaml` file.
|
|
||||||
3. You can edit the configuration directly in the text area.
|
System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
|
||||||
* *Caution: Incorrect modifications can break the HAPI FHIR server.*
|
|
||||||
4. Click **Save Configuration** to apply your changes to the `application.yaml` file.
|
Resource operations: GET Patient/:id, POST Observation/_search, etc.
|
||||||
5. Click **Restart Tomcat** to restart the HAPI FHIR server and load the new configuration. The restart process may take a few moments.
|
|
||||||
|
Use Try it out to input parameters or request bodies, then Execute to view results in JSON, XML, or narrative formats.
|
||||||
|
|
||||||
API Usage
|
API Usage
|
||||||
Import IG
|
Import IG
|
||||||
Bash
|
|
||||||
|
|
||||||
curl -X POST http://localhost:5000/api/import-ig \
|
curl -X POST http://localhost:5000/api/import-ig \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "X-API-Key: your-api-key" \
|
-H "X-API-Key: your-api-key" \
|
||||||
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "dependency_mode": "recursive"}'
|
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "dependency_mode": "recursive"}'
|
||||||
|
|
||||||
Returns complies_with_profiles, imposed_profiles, and duplicate_packages_present info.
|
Returns complies_with_profiles, imposed_profiles, and duplicate_packages_present info.
|
||||||
|
|
||||||
### Refresh Package Cache (Background Task)
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/refresh-cache-task \
|
|
||||||
-H "X-API-Key: your-api-key"
|
|
||||||
|
|
||||||
Push IG
|
Push IG
|
||||||
Bash
|
|
||||||
|
|
||||||
curl -X POST http://localhost:5000/api/push-ig \
|
curl -X POST http://localhost:5000/api/push-ig \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Accept: application/x-ndjson" \
|
-H "Accept: application/x-ndjson" \
|
||||||
@ -447,11 +393,10 @@ curl -X POST http://localhost:5000/api/push-ig \
|
|||||||
"verbose": false,
|
"verbose": false,
|
||||||
"auth_type": "none"
|
"auth_type": "none"
|
||||||
}'
|
}'
|
||||||
|
|
||||||
Returns a streaming NDJSON response with progress and final summary.
|
Returns a streaming NDJSON response with progress and final summary.
|
||||||
|
|
||||||
Upload Test Data
|
Upload Test Data
|
||||||
Bash
|
|
||||||
|
|
||||||
curl -X POST http://localhost:5000/api/upload-test-data \
|
curl -X POST http://localhost:5000/api/upload-test-data \
|
||||||
-H "X-API-Key: your-api-key" \
|
-H "X-API-Key: your-api-key" \
|
||||||
-H "Accept: application/x-ndjson" \
|
-H "Accept: application/x-ndjson" \
|
||||||
@ -465,29 +410,10 @@ curl -X POST http://localhost:5000/api/upload-test-data \
|
|||||||
-F "use_conditional_uploads=true" \
|
-F "use_conditional_uploads=true" \
|
||||||
-F "test_data_files=@/path/to/your/patient.json" \
|
-F "test_data_files=@/path/to/your/patient.json" \
|
||||||
-F "test_data_files=@/path/to/your/observations.zip"
|
-F "test_data_files=@/path/to/your/observations.zip"
|
||||||
Returns a streaming NDJSON response with progress and final summary. Uses multipart/form-data for file uploads.
|
|
||||||
|
|
||||||
Retrieve Bundles
|
Returns a streaming NDJSON response with progress and final summary.
|
||||||
Bash
|
|
||||||
|
|
||||||
curl -X POST http://localhost:5000/api/retrieve-bundles \
|
Uses multipart/form-data for file uploads.
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-H "Accept: application/x-ndjson" \
|
|
||||||
-F "fhir_server_url=http://your-fhir-server/fhir" \
|
|
||||||
-F "resources=Patient" \
|
|
||||||
-F "resources=Observation" \
|
|
||||||
-F "validate_references=true" \
|
|
||||||
-F "fetch_reference_bundles=false"
|
|
||||||
Returns a streaming NDJSON response with progress. The X-Zip-Path header in the final response part will contain the path to download the ZIP archive (e.g., /tmp/retrieved_bundles_datetime.zip).
|
|
||||||
|
|
||||||
Split Bundles
|
|
||||||
Bash
|
|
||||||
|
|
||||||
curl -X POST http://localhost:5000/api/split-bundles \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-H "Accept: application/x-ndjson" \
|
|
||||||
-F "split_bundle_zip_path=@/path/to/your/bundles.zip"
|
|
||||||
Returns a streaming NDJSON response. The X-Zip-Path header in the final response part will contain the path to download the ZIP archive of split resources.
|
|
||||||
|
|
||||||
Validate Resource/Bundle
|
Validate Resource/Bundle
|
||||||
Not yet exposed via API; use the UI at /validate-sample.
|
Not yet exposed via API; use the UI at /validate-sample.
|
||||||
@ -496,131 +422,180 @@ Configuration Options
|
|||||||
Located in app.py:
|
Located in app.py:
|
||||||
|
|
||||||
VALIDATE_IMPOSED_PROFILES: (Default: True) Validates resources against imposed profiles during push.
|
VALIDATE_IMPOSED_PROFILES: (Default: True) Validates resources against imposed profiles during push.
|
||||||
|
|
||||||
DISPLAY_PROFILE_RELATIONSHIPS: (Default: True) Shows compliesWithProfile and imposeProfile in the UI.
|
DISPLAY_PROFILE_RELATIONSHIPS: (Default: True) Shows compliesWithProfile and imposeProfile in the UI.
|
||||||
|
|
||||||
FHIR_PACKAGES_DIR: (Default: /app/instance/fhir_packages) Stores .tgz packages and metadata.
|
FHIR_PACKAGES_DIR: (Default: /app/instance/fhir_packages) Stores .tgz packages and metadata.
|
||||||
|
|
||||||
UPLOAD_FOLDER: (Default: /app/static/uploads) Stores GoFSH output files and FSH comparison reports.
|
UPLOAD_FOLDER: (Default: /app/static/uploads) Stores GoFSH output files and FSH comparison reports.
|
||||||
|
|
||||||
SECRET_KEY: Required for CSRF protection and sessions. Set via environment variable or directly.
|
SECRET_KEY: Required for CSRF protection and sessions. Set via environment variable or directly.
|
||||||
|
|
||||||
API_KEY: Required for API authentication. Set via environment variable or directly.
|
API_KEY: Required for API authentication. Set via environment variable or directly.
|
||||||
|
|
||||||
MAX_CONTENT_LENGTH: (Default: Flask default) Max size for HTTP request body (e.g., 16 * 1024 * 1024 for 16MB). Important for large uploads.
|
MAX_CONTENT_LENGTH: (Default: Flask default) Max size for HTTP request body (e.g., 16 * 1024 * 1024 for 16MB). Important for large uploads.
|
||||||
|
|
||||||
MAX_FORM_PARTS: (Default: Werkzeug default, often 1000) Default max number of form parts. Overridden for /api/upload-test-data by CustomFormDataParser.
|
MAX_FORM_PARTS: (Default: Werkzeug default, often 1000) Default max number of form parts. Overridden for /api/upload-test-data by CustomFormDataParser.
|
||||||
|
|
||||||
### Get HAPI FHIR Configuration (Standalone Mode)
|
|
||||||
```bash
|
|
||||||
curl -X GET http://localhost:5000/api/config \
|
|
||||||
-H "X-API-Key: your-api-key"
|
|
||||||
|
|
||||||
Save HAPI FHIR Configuration:
|
|
||||||
curl -X POST http://localhost:5000/api/config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-d '{"your_yaml_key": "your_value", ...}' # Send the full YAML content as JSON
|
|
||||||
|
|
||||||
Restart HAPI FHIR Server:
|
|
||||||
curl -X POST http://localhost:5000/api/restart-tomcat \
|
|
||||||
-H "X-API-Key: your-api-key"
|
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
The project includes a test suite covering UI, API, database, file operations, and security.
|
The project includes a test suite covering UI, API, database, file operations, and security.
|
||||||
|
|
||||||
Test Prerequisites:
|
Test Prerequisites:
|
||||||
|
|
||||||
pytest: For running tests.
|
pytest: For running tests.
|
||||||
pytest-mock: For mocking dependencies. Install: pip install pytest pytest-mock
|
|
||||||
Running Tests:
|
|
||||||
|
|
||||||
Bash
|
pytest-mock: For mocking dependencies.
|
||||||
|
|
||||||
|
Install: pip install pytest pytest-mock
|
||||||
|
|
||||||
|
Running Tests:
|
||||||
|
|
||||||
cd <project folder>
|
cd <project folder>
|
||||||
pytest tests/test_app.py -v
|
pytest tests/test_app.py -v
|
||||||
|
|
||||||
Test Coverage:
|
Test Coverage:
|
||||||
|
|
||||||
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter, Upload Test Data, Retrieve/Split Data.
|
UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter, Upload Test Data.
|
||||||
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example, POST /api/upload-test-data, POST /api/retrieve-bundles, POST /api/split-bundles.
|
|
||||||
|
API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example, POST /api/upload-test-data.
|
||||||
|
|
||||||
Database: IG processing, unloading, viewing.
|
Database: IG processing, unloading, viewing.
|
||||||
File Operations: Package processing, deletion, FSH output, ZIP handling.
|
|
||||||
|
File Operations: Package processing, deletion, FSH output.
|
||||||
|
|
||||||
Security: CSRF protection, flash messages, secret key.
|
Security: CSRF protection, flash messages, secret key.
|
||||||
|
|
||||||
FSH Converter: Form submission, file/text input, GoFSH execution, Fishing Trip comparison.
|
FSH Converter: Form submission, file/text input, GoFSH execution, Fishing Trip comparison.
|
||||||
|
|
||||||
Upload Test Data: Parsing, dependency graph, sorting, upload modes, validation, conditional uploads.
|
Upload Test Data: Parsing, dependency graph, sorting, upload modes, validation, conditional uploads.
|
||||||
|
|
||||||
Development Notes
|
Development Notes
|
||||||
Background
|
Background
|
||||||
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, flexible versioning, improved IG pushing, dependency-aware test data uploading, and bundle retrieval/splitting, making it a versatile platform for FHIR developers.
|
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, flexible versioning, improved IG pushing, and dependency-aware test data uploading, making it a versatile platform for FHIR developers.
|
||||||
|
|
||||||
Technical Decisions
|
Technical Decisions
|
||||||
Flask: Lightweight and flexible for web development.
|
Flask: Lightweight and flexible for web development.
|
||||||
|
|
||||||
SQLite: Simple for development; consider PostgreSQL for production.
|
SQLite: Simple for development; consider PostgreSQL for production.
|
||||||
|
|
||||||
Bootstrap 5.3.3: Responsive UI with custom styling.
|
Bootstrap 5.3.3: Responsive UI with custom styling.
|
||||||
|
|
||||||
Lottie-Web: Renders themed animations for FSH conversion waiting spinner.
|
Lottie-Web: Renders themed animations for FSH conversion waiting spinner.
|
||||||
|
|
||||||
GoFSH/SUSHI: Integrated via Node.js for advanced FSH conversion and round-trip validation.
|
GoFSH/SUSHI: Integrated via Node.js for advanced FSH conversion and round-trip validation.
|
||||||
|
|
||||||
Docker: Ensures consistent deployment with Flask and HAPI FHIR.
|
Docker: Ensures consistent deployment with Flask and HAPI FHIR.
|
||||||
|
|
||||||
Flexible Versioning: Supports non-standard IG versions (e.g., -preview, -ballot).
|
Flexible Versioning: Supports non-standard IG versions (e.g., -preview, -ballot).
|
||||||
Live Console/Streaming: Real-time feedback for complex operations (Push, Upload Test Data, FSH, Retrieve Bundles).
|
|
||||||
|
Live Console/Streaming: Real-time feedback for complex operations (Push, Upload Test Data, FSH).
|
||||||
|
|
||||||
Validation: Alpha feature with ongoing FHIRPath improvements.
|
Validation: Alpha feature with ongoing FHIRPath improvements.
|
||||||
|
|
||||||
Dependency Management: Uses topological sort for Upload Test Data feature.
|
Dependency Management: Uses topological sort for Upload Test Data feature.
|
||||||
|
|
||||||
Form Parsing: Uses custom Werkzeug parser for Upload Test Data to handle large numbers of files.
|
Form Parsing: Uses custom Werkzeug parser for Upload Test Data to handle large numbers of files.
|
||||||
|
|
||||||
Recent Updates
|
Recent Updates
|
||||||
* Enhanced package search page with caching, detailed views (dependencies, dependents, version history), and background cache refresh.
|
|
||||||
Upload Test Data Enhancements (April 2025):
|
Upload Test Data Enhancements (April 2025):
|
||||||
|
|
||||||
Added optional Pre-Upload Validation against selected IG profiles.
|
Added optional Pre-Upload Validation against selected IG profiles.
|
||||||
|
|
||||||
Added optional Conditional Uploads (GET + POST/PUT w/ If-Match) for individual mode.
|
Added optional Conditional Uploads (GET + POST/PUT w/ If-Match) for individual mode.
|
||||||
|
|
||||||
Implemented robust XML parsing using fhir.resources library (when available).
|
Implemented robust XML parsing using fhir.resources library (when available).
|
||||||
|
|
||||||
Fixed 413 Request Entity Too Large errors for large file counts using a custom Werkzeug FormDataParser.
|
Fixed 413 Request Entity Too Large errors for large file counts using a custom Werkzeug FormDataParser.
|
||||||
|
|
||||||
Path: templates/upload_test_data.html, app.py, services.py, forms.py.
|
Path: templates/upload_test_data.html, app.py, services.py, forms.py.
|
||||||
|
|
||||||
Push IG Enhancements (April 2025):
|
Push IG Enhancements (April 2025):
|
||||||
|
|
||||||
Added semantic comparison to skip uploading identical resources.
|
Added semantic comparison to skip uploading identical resources.
|
||||||
|
|
||||||
Added "Force Upload" option to bypass comparison.
|
Added "Force Upload" option to bypass comparison.
|
||||||
|
|
||||||
Improved handling of canonical resources (search before PUT/POST).
|
Improved handling of canonical resources (search before PUT/POST).
|
||||||
|
|
||||||
Added filtering by specific files to skip during push.
|
Added filtering by specific files to skip during push.
|
||||||
|
|
||||||
More detailed summary report in stream response.
|
More detailed summary report in stream response.
|
||||||
|
|
||||||
Path: templates/cp_push_igs.html, app.py, services.py.
|
Path: templates/cp_push_igs.html, app.py, services.py.
|
||||||
|
|
||||||
Waiting Spinner for FSH Converter (April 2025):
|
Waiting Spinner for FSH Converter (April 2025):
|
||||||
|
|
||||||
Added a themed (light/dark) Lottie animation spinner during FSH execution.
|
Added a themed (light/dark) Lottie animation spinner during FSH execution.
|
||||||
|
|
||||||
Path: templates/fsh_converter.html, static/animations/, static/js/lottie-web.min.js.
|
Path: templates/fsh_converter.html, static/animations/, static/js/lottie-web.min.js.
|
||||||
|
|
||||||
Advanced FSH Converter (April 2025):
|
Advanced FSH Converter (April 2025):
|
||||||
|
|
||||||
Added support for GoFSH advanced options: --fshing-trip, --dependency, --indent, --meta-profile, --alias-file, --no-alias.
|
Added support for GoFSH advanced options: --fshing-trip, --dependency, --indent, --meta-profile, --alias-file, --no-alias.
|
||||||
|
|
||||||
Displays Fishing Trip comparison reports.
|
Displays Fishing Trip comparison reports.
|
||||||
|
|
||||||
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
|
Path: templates/fsh_converter.html, app.py, services.py, forms.py.
|
||||||
(New) Retrieve and Split Data (May 2025):
|
|
||||||
Added UI and API for retrieving bundles from a FHIR server by resource type.
|
(Keep older updates listed here if desired)
|
||||||
Added options to fetch referenced resources (individually or as full type bundles).
|
|
||||||
Added functionality to split uploaded ZIP files of bundles into individual resources.
|
|
||||||
Streaming log for retrieval and ZIP download for results.
|
|
||||||
Paths: templates/retrieve_split_data.html, app.py, services.py, forms.py.
|
|
||||||
Known Issues and Workarounds
|
Known Issues and Workarounds
|
||||||
Favicon 404: Clear browser cache or verify /app/static/favicon.ico.
|
Favicon 404: Clear browser cache or verify /app/static/favicon.ico.
|
||||||
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
|
||||||
Import Fails: Check package name/version and connectivity.
|
|
||||||
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
|
|
||||||
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
|
|
||||||
Permissions: Ensure instance/ and static/uploads/ are writable.
|
|
||||||
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation.
|
|
||||||
Upload Test Data XML Parsing: Relies on fhir.resources library for full validation; basic parsing used as fallback. Complex XML structures might not be fully analyzed for dependencies with basic parsing. Prefer JSON for reliable dependency analysis.
|
|
||||||
413 Request Entity Too Large: Primarily handled by CustomFormDataParser for /api/upload-test-data. Check the parser's max_form_parts limit if still occurring. MAX_CONTENT_LENGTH in app.py controls overall size. Reverse proxy limits (client_max_body_size in Nginx) might also apply.
|
|
||||||
|
|
||||||
|
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
||||||
|
|
||||||
|
Import Fails: Check package name/version and connectivity.
|
||||||
|
|
||||||
|
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
|
||||||
|
|
||||||
|
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
|
||||||
|
|
||||||
|
Permissions: Ensure instance/ and static/uploads/ are writable.
|
||||||
|
|
||||||
|
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation.
|
||||||
|
|
||||||
|
Upload Test Data XML Parsing: Relies on fhir.resources library for full validation; basic parsing used as fallback. Complex XML structures might not be fully analyzed for dependencies with basic parsing. Prefer JSON for reliable dependency analysis.
|
||||||
|
|
||||||
|
413 Request Entity Too Large: Primarily handled by CustomFormDataParser for /api/upload-test-data. Check the parser's max_form_parts limit if still occurring. MAX_CONTENT_LENGTH in app.py controls overall size. Reverse proxy limits (client_max_body_size in Nginx) might also apply.
|
||||||
|
|
||||||
Future Improvements
|
Future Improvements
|
||||||
Upload Test Data: Improve XML parsing further (direct XML->fhir.resource object if possible), add visual progress bar, add upload order preview, implement transaction bundle size splitting, add 'Clear Target Server' option (with confirmation).
|
Upload Test Data: Improve XML parsing further (direct XML->fhir.resource object if possible), add visual progress bar, add upload order preview, implement transaction bundle size splitting, add 'Clear Target Server' option (with confirmation).
|
||||||
|
|
||||||
Validation: Enhance FHIRPath for complex constraints; add API endpoint.
|
Validation: Enhance FHIRPath for complex constraints; add API endpoint.
|
||||||
|
|
||||||
Sorting: Sort IG versions in /view-igs (e.g., ascending).
|
Sorting: Sort IG versions in /view-igs (e.g., ascending).
|
||||||
|
|
||||||
Duplicate Resolution: Options to keep latest version or merge resources.
|
Duplicate Resolution: Options to keep latest version or merge resources.
|
||||||
|
|
||||||
Production Database: Support PostgreSQL.
|
Production Database: Support PostgreSQL.
|
||||||
|
|
||||||
Error Reporting: Detailed validation error paths in the UI.
|
Error Reporting: Detailed validation error paths in the UI.
|
||||||
|
|
||||||
FSH Enhancements: Add API endpoint for FSH conversion; support inline instance construction.
|
FSH Enhancements: Add API endpoint for FSH conversion; support inline instance construction.
|
||||||
|
|
||||||
FHIR Operations: Add complex parameter support (e.g., /$diff with left/right).
|
FHIR Operations: Add complex parameter support (e.g., /$diff with left/right).
|
||||||
Retrieve/Split Data: Add option to filter resources during retrieval (e.g., by date, specific IDs).
|
|
||||||
Completed Items
|
Completed Items
|
||||||
Testing suite with basic coverage.
|
Testing suite with basic coverage.
|
||||||
|
|
||||||
API endpoints for POST /api/import-ig and POST /api/push-ig.
|
API endpoints for POST /api/import-ig and POST /api/push-ig.
|
||||||
|
|
||||||
Flexible versioning (-preview, -ballot).
|
Flexible versioning (-preview, -ballot).
|
||||||
|
|
||||||
CSRF fixes for forms.
|
CSRF fixes for forms.
|
||||||
|
|
||||||
Resource validation UI (alpha).
|
Resource validation UI (alpha).
|
||||||
|
|
||||||
FSH Converter with advanced GoFSH features and waiting spinner.
|
FSH Converter with advanced GoFSH features and waiting spinner.
|
||||||
|
|
||||||
Push IG enhancements (force upload, semantic comparison, canonical handling, skip files).
|
Push IG enhancements (force upload, semantic comparison, canonical handling, skip files).
|
||||||
|
|
||||||
Upload Test Data feature with dependency sorting, multiple upload modes, pre-upload validation, conditional uploads, robust XML parsing, and fix for large file counts.
|
Upload Test Data feature with dependency sorting, multiple upload modes, pre-upload validation, conditional uploads, robust XML parsing, and fix for large file counts.
|
||||||
Retrieve and Split Data functionality with reference fetching and ZIP download.
|
|
||||||
Far-Distant Improvements
|
Far-Distant Improvements
|
||||||
Cache Service: Use Redis for IG metadata caching.
|
Cache Service: Use Redis for IG metadata caching.
|
||||||
Database Optimization: Composite index on ProcessedIg.package_name and ProcessedIg.version.
|
|
||||||
|
|
||||||
|
Database Optimization: Composite index on ProcessedIg.package_name and ProcessedIg.version.
|
||||||
|
|
||||||
Directory Structure
|
Directory Structure
|
||||||
FHIRFLARE-IG-Toolkit/
|
FHIRFLARE-IG-Toolkit/
|
||||||
@ -633,7 +608,7 @@ FHIRFLARE-IG-Toolkit/
|
|||||||
├── README.md # Project documentation
|
├── README.md # Project documentation
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
├── Run.bat # Windows script for running Docker
|
├── Run.bat # Windows script for running Docker
|
||||||
├── services.py # Logic for IG import, processing, validation, pushing, FSH conversion, test data upload, retrieve/split
|
├── services.py # Logic for IG import, processing, validation, pushing, FSH conversion, test data upload
|
||||||
├── supervisord.conf # Supervisor configuration
|
├── supervisord.conf # Supervisor configuration
|
||||||
├── hapi-fhir-Setup/
|
├── hapi-fhir-Setup/
|
||||||
│ ├── README.md # HAPI FHIR setup instructions
|
│ ├── README.md # HAPI FHIR setup instructions
|
||||||
@ -674,31 +649,38 @@ FHIRFLARE-IG-Toolkit/
|
|||||||
│ ├── fsh_converter.html # UI for FSH conversion
|
│ ├── fsh_converter.html # UI for FSH conversion
|
||||||
│ ├── import_ig.html # UI for importing IGs
|
│ ├── import_ig.html # UI for importing IGs
|
||||||
│ ├── index.html # Homepage
|
│ ├── index.html # Homepage
|
||||||
│ ├── retrieve_split_data.html # UI for Retrieve and Split Data
|
|
||||||
│ ├── upload_test_data.html # UI for Uploading Test Data
|
│ ├── upload_test_data.html # UI for Uploading Test Data
|
||||||
│ ├── validate_sample.html # UI for validating resources/bundles
|
│ ├── validate_sample.html # UI for validating resources/bundles
|
||||||
│ ├── config_hapi.html # UI for HAPI FHIR Configuration
|
|
||||||
│ └── _form_helpers.html # Form helper macros
|
│ └── _form_helpers.html # Form helper macros
|
||||||
├── tests/
|
├── tests/
|
||||||
│ └── test_app.py # Test suite
|
│ └── test_app.py # Test suite
|
||||||
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources (if Standalone)
|
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources (if Standalone)
|
||||||
|
|
||||||
Contributing
|
|
||||||
Fork the repository.
|
|
||||||
Create a feature branch (git checkout -b feature/your-feature).
|
|
||||||
Commit changes (git commit -m "Add your feature").
|
|
||||||
Push to your branch (git push origin feature/your-feature).
|
|
||||||
Open a Pull Request.
|
|
||||||
Ensure code follows PEP 8 and includes tests in tests/test_app.py.
|
|
||||||
|
|
||||||
Troubleshooting
|
## Contributing
|
||||||
Favicon 404: Clear browser cache or verify /app/static/favicon.ico: docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
|
|
||||||
CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
* Fork the repository.
|
||||||
Import Fails: Check package name/version and connectivity.
|
* Create a feature branch (`git checkout -b feature/your-feature`).
|
||||||
Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
|
* Commit changes (`git commit -m "Add your feature"`).
|
||||||
Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
|
* Push to your branch (`git push origin feature/your-feature`).
|
||||||
Permissions: Ensure instance/ and static/uploads/ are writable: chmod -R 777 instance static/uploads logs
|
* Open a Pull Request.
|
||||||
GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation: docker exec -it <container_name> sushi --version
|
|
||||||
413 Request Entity Too Large: Increase MAX_CONTENT_LENGTH and MAX_FORM_PARTS in app.py. If using a reverse proxy (e.g., Nginx), increase its client_max_body_size setting as well. Ensure the application/container is fully restarted/rebuilt.
|
Ensure code follows PEP 8 and includes tests in `tests/test_app.py`.
|
||||||
License
|
|
||||||
Licensed under the Apache 2.0 License. See LICENSE.md for details.
|
## Troubleshooting
|
||||||
|
|
||||||
|
* **Favicon 404:** Clear browser cache or verify `/app/static/favicon.ico`:
|
||||||
|
`docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico`
|
||||||
|
* **CSRF Errors:** Set `FLASK_SECRET_KEY` and ensure `{{ form.hidden_tag() }}` in forms.
|
||||||
|
* **Import Fails:** Check package name/version and connectivity.
|
||||||
|
* **Validation Accuracy:** Alpha feature; report issues to GitHub (remove PHI).
|
||||||
|
* **Package Parsing:** Non-standard `.tgz` filenames may parse incorrectly. Fallback uses name-only parsing.
|
||||||
|
* **Permissions:** Ensure `instance/` and `static/uploads/` are writable:
|
||||||
|
`chmod -R 777 instance static/uploads logs`
|
||||||
|
* **GoFSH/SUSHI Errors:** Check `./logs/flask_err.log` for `ERROR:services:GoFSH failed`. Ensure valid FHIR inputs and SUSHI installation:
|
||||||
|
`docker exec -it <container_name> sushi --version`
|
||||||
|
* **413 Request Entity Too Large:** Increase `MAX_CONTENT_LENGTH` and `MAX_FORM_PARTS` in `app.py`. If using a reverse proxy (e.g., Nginx), increase its `client_max_body_size` setting as well. Ensure the application/container is fully restarted/rebuilt.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the Apache 2.0 License. See `LICENSE.md` for details.
|
||||||
|
|||||||
1
charts/.gitignore
vendored
1
charts/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/hapi-fhir-jpaserver-0.20.0.tgz
|
|
||||||
1
charts/fhirflare-ig-toolkit/.gitignore
vendored
1
charts/fhirflare-ig-toolkit/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/rendered/
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: fhirflare-ig-toolkit
|
|
||||||
version: 0.5.0
|
|
||||||
description: Helm chart for deploying the fhirflare-ig-toolkit application
|
|
||||||
type: application
|
|
||||||
appVersion: "latest"
|
|
||||||
icon: https://github.com/jgsuess/FHIRFLARE-IG-Toolkit/raw/main/static/FHIRFLARE.png
|
|
||||||
keywords:
|
|
||||||
- fhir
|
|
||||||
- healthcare
|
|
||||||
- ig-toolkit
|
|
||||||
- implementation-guide
|
|
||||||
home: https://github.com/jgsuess/FHIRFLARE-IG-Toolkit
|
|
||||||
maintainers:
|
|
||||||
- name: Jörn Guy Süß
|
|
||||||
email: jgsuess@gmail.com
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "fhirflare-ig-toolkit.name" -}}
|
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified app name.
|
|
||||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
||||||
If release name contains chart name it will be used as a full name.
|
|
||||||
*/}}
|
|
||||||
{{- define "fhirflare-ig-toolkit.fullname" -}}
|
|
||||||
{{- if .Values.fullnameOverride }}
|
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
||||||
{{- if contains $name .Release.Name }}
|
|
||||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create chart name and version as used by the chart label.
|
|
||||||
*/}}
|
|
||||||
{{- define "fhirflare-ig-toolkit.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels
|
|
||||||
*/}}
|
|
||||||
{{- define "fhirflare-ig-toolkit.labels" -}}
|
|
||||||
helm.sh/chart: {{ include "fhirflare-ig-toolkit.chart" . }}
|
|
||||||
{{ include "fhirflare-ig-toolkit.selectorLabels" . }}
|
|
||||||
{{- if .Chart.AppVersion }}
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
||||||
{{- end }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Selector labels
|
|
||||||
*/}}
|
|
||||||
{{- define "fhirflare-ig-toolkit.selectorLabels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "fhirflare-ig-toolkit.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the name of the service account to use
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.serviceAccountName" -}}
|
|
||||||
{{- if .Values.serviceAccount.create }}
|
|
||||||
{{- default (include "hapi-fhir-jpaserver.fullname" .) .Values.serviceAccount.name }}
|
|
||||||
{{- else }}
|
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified postgresql name.
|
|
||||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.postgresql.fullname" -}}
|
|
||||||
{{- $name := default "postgresql" .Values.postgresql.nameOverride -}}
|
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Get the Postgresql credentials secret name.
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.postgresql.secretName" -}}
|
|
||||||
{{- if .Values.postgresql.enabled -}}
|
|
||||||
{{- if .Values.postgresql.auth.existingSecret -}}
|
|
||||||
{{- printf "%s" .Values.postgresql.auth.existingSecret -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- printf "%s" (include "hapi-fhir-jpaserver.postgresql.fullname" .) -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- else }}
|
|
||||||
{{- if .Values.externalDatabase.existingSecret -}}
|
|
||||||
{{- printf "%s" .Values.externalDatabase.existingSecret -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{ printf "%s-%s" (include "hapi-fhir-jpaserver.fullname" .) "external-db" }}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Get the Postgresql credentials secret key.
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.postgresql.secretKey" -}}
|
|
||||||
{{- if .Values.postgresql.enabled -}}
|
|
||||||
{{- if .Values.postgresql.auth.username -}}
|
|
||||||
{{- printf "%s" .Values.postgresql.auth.secretKeys.userPasswordKey -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- printf "%s" .Values.postgresql.auth.secretKeys.adminPasswordKey -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- else }}
|
|
||||||
{{- if .Values.externalDatabase.existingSecret -}}
|
|
||||||
{{- printf "%s" .Values.externalDatabase.existingSecretKey -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- printf "postgres-password" -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Add environment variables to configure database values
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.database.host" -}}
|
|
||||||
{{- ternary (include "hapi-fhir-jpaserver.postgresql.fullname" .) .Values.externalDatabase.host .Values.postgresql.enabled -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Add environment variables to configure database values
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.database.user" -}}
|
|
||||||
{{- if .Values.postgresql.enabled -}}
|
|
||||||
{{- printf "%s" .Values.postgresql.auth.username | default "postgres" -}}
|
|
||||||
{{- else -}}
|
|
||||||
{{- printf "%s" .Values.externalDatabase.user -}}
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Add environment variables to configure database values
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.database.name" -}}
|
|
||||||
{{- ternary .Values.postgresql.auth.database .Values.externalDatabase.database .Values.postgresql.enabled -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Add environment variables to configure database values
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.database.port" -}}
|
|
||||||
{{- ternary "5432" .Values.externalDatabase.port .Values.postgresql.enabled -}}
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the JDBC URL from the host, port and database name.
|
|
||||||
*/}}
|
|
||||||
{{- define "hapi-fhir-jpaserver.database.jdbcUrl" -}}
|
|
||||||
{{- $host := (include "hapi-fhir-jpaserver.database.host" .) -}}
|
|
||||||
{{- $port := (include "hapi-fhir-jpaserver.database.port" .) -}}
|
|
||||||
{{- $name := (include "hapi-fhir-jpaserver.database.name" .) -}}
|
|
||||||
{{- $appName := .Release.Name -}}
|
|
||||||
{{ printf "jdbc:postgresql://%s:%d/%s?ApplicationName=%s" $host (int $port) $name $appName }}
|
|
||||||
{{- end -}}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "fhirflare-ig-toolkit.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "fhirflare-ig-toolkit.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.replicaCount | default 1 }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "fhirflare-ig-toolkit.selectorLabels" . | nindent 6 }}
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
{{- include "fhirflare-ig-toolkit.selectorLabels" . | nindent 8 }}
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
args: ["supervisord", "-c", "/etc/supervisord.conf"]
|
|
||||||
env:
|
|
||||||
- name: APP_BASE_URL
|
|
||||||
value: {{ .Values.config.appBaseUrl | default "http://localhost:5000" | quote }}
|
|
||||||
- name: APP_MODE
|
|
||||||
value: {{ .Values.config.appMode | default "lite" | quote }}
|
|
||||||
- name: FLASK_APP
|
|
||||||
value: {{ .Values.config.flaskApp | default "app.py" | quote }}
|
|
||||||
- name: FLASK_ENV
|
|
||||||
value: {{ .Values.config.flaskEnv | default "development" | quote }}
|
|
||||||
- name: HAPI_FHIR_URL
|
|
||||||
value: {{ .Values.config.externalHapiServerUrl | default "http://external-hapi-fhir:8080/fhir" | quote }}
|
|
||||||
- name: NODE_PATH
|
|
||||||
value: {{ .Values.config.nodePath | default "/usr/lib/node_modules" | quote }}
|
|
||||||
- name: TMPDIR
|
|
||||||
value: "/tmp-dir"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.port | default 5000 }}
|
|
||||||
protocol: TCP
|
|
||||||
volumeMounts:
|
|
||||||
- name: logs
|
|
||||||
mountPath: /app/logs
|
|
||||||
- name: tmp-dir
|
|
||||||
mountPath: /tmp-dir
|
|
||||||
{{- with .Values.resources }}
|
|
||||||
resources:
|
|
||||||
{{- toYaml . | nindent 12 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.livenessProbe }}
|
|
||||||
livenessProbe:
|
|
||||||
{{- toYaml . | nindent 12 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.readinessProbe }}
|
|
||||||
readinessProbe:
|
|
||||||
{{- toYaml . | nindent 12 }}
|
|
||||||
{{- end }}
|
|
||||||
volumes:
|
|
||||||
- name: logs
|
|
||||||
emptyDir: {}
|
|
||||||
- name: tmp-dir
|
|
||||||
emptyDir: {}
|
|
||||||
# Always require Intel 64-bit architecture nodes
|
|
||||||
nodeSelector:
|
|
||||||
kubernetes.io/arch: amd64
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
# Merge with user-defined nodeSelectors if any
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
{{- if .Values.ingress.enabled -}}
|
|
||||||
{{- $fullName := include "fhirflare-ig-toolkit.fullname" . -}}
|
|
||||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion }}
|
|
||||||
apiVersion: networking.k8s.io/v1beta1
|
|
||||||
{{- else }}
|
|
||||||
apiVersion: extensions/v1beta1
|
|
||||||
{{- end }}
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: {{ $fullName }}
|
|
||||||
labels:
|
|
||||||
{{- include "fhirflare-ig-toolkit.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.ingress.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
rules:
|
|
||||||
- http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion }}
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: {{ $fullName }}
|
|
||||||
port:
|
|
||||||
number: {{ .Values.service.port | default 5000 }}
|
|
||||||
{{- else }}
|
|
||||||
backend:
|
|
||||||
serviceName: {{ $fullName }}
|
|
||||||
servicePort: {{ .Values.service.port | default 5000 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "fhirflare-ig-toolkit.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "fhirflare-ig-toolkit.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type | default "ClusterIP" }}
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
port: {{ .Values.service.port | default 5000 }}
|
|
||||||
targetPort: {{ .Values.service.port | default 5000 }}
|
|
||||||
protocol: TCP
|
|
||||||
{{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }}
|
|
||||||
nodePort: {{ .Values.service.nodePort }}
|
|
||||||
{{- end }}
|
|
||||||
selector:
|
|
||||||
{{- include "fhirflare-ig-toolkit.selectorLabels" . | nindent 4 }}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: "{{ .Release.Name }}-fhirflare-test-endpoint"
|
|
||||||
labels:
|
|
||||||
helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
|
||||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
app.kubernetes.io/component: tests
|
|
||||||
annotations:
|
|
||||||
"helm.sh/hook": test
|
|
||||||
spec:
|
|
||||||
restartPolicy: Never
|
|
||||||
containers:
|
|
||||||
- name: test-fhirflare-endpoint
|
|
||||||
image: curlimages/curl:8.12.1
|
|
||||||
command: ["curl", "--fail-with-body", "--retry", "5", "--retry-delay", "10"]
|
|
||||||
args: ["http://fhirflare:5000"]
|
|
||||||
securityContext:
|
|
||||||
allowPrivilegeEscalation: false
|
|
||||||
capabilities:
|
|
||||||
drop:
|
|
||||||
- ALL
|
|
||||||
privileged: false
|
|
||||||
readOnlyRootFilesystem: true
|
|
||||||
runAsGroup: 65534
|
|
||||||
runAsNonRoot: true
|
|
||||||
runAsUser: 65534
|
|
||||||
seccompProfile:
|
|
||||||
type: RuntimeDefault
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 150m
|
|
||||||
ephemeral-storage: 2Gi
|
|
||||||
memory: 192Mi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
ephemeral-storage: 50Mi
|
|
||||||
memory: 128Mi
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
# Default values for fhirflare-ig-toolkit
|
|
||||||
replicaCount: 1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/jgsuess/fhirflare-ig-toolkit
|
|
||||||
pullPolicy: Always
|
|
||||||
tag: "latest"
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
|
||||||
nameOverride: ""
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
# FHIRflare specific configuration
|
|
||||||
config:
|
|
||||||
# Application mode: "lite" means using external HAPI server, "standalone" means running with embedded HAPI server
|
|
||||||
appMode: "lite"
|
|
||||||
# URL for the external HAPI FHIR server when in lite mode
|
|
||||||
externalHapiServerUrl: "http://external-hapi-fhir:8080/fhir"
|
|
||||||
appBaseUrl: "http://localhost:5000"
|
|
||||||
flaskApp: "app.py"
|
|
||||||
flaskEnv: "development"
|
|
||||||
nodePath: "/usr/lib/node_modules"
|
|
||||||
|
|
||||||
service:
|
|
||||||
type: ClusterIP
|
|
||||||
port: 5000
|
|
||||||
nodePort: null
|
|
||||||
|
|
||||||
podAnnotations: {}
|
|
||||||
|
|
||||||
# podSecurityContext:
|
|
||||||
# fsGroup: 65532
|
|
||||||
# fsGroupChangePolicy: OnRootMismatch
|
|
||||||
# runAsNonRoot: true
|
|
||||||
# runAsGroup: 65532
|
|
||||||
# runAsUser: 65532
|
|
||||||
# seccompProfile:
|
|
||||||
# type: RuntimeDefault
|
|
||||||
|
|
||||||
# securityContext:
|
|
||||||
# allowPrivilegeEscalation: false
|
|
||||||
# capabilities:
|
|
||||||
# drop:
|
|
||||||
# - ALL
|
|
||||||
# privileged: false
|
|
||||||
# readOnlyRootFilesystem: true
|
|
||||||
# runAsGroup: 65532
|
|
||||||
# runAsNonRoot: true
|
|
||||||
# runAsUser: 65532
|
|
||||||
# seccompProfile:
|
|
||||||
# type: RuntimeDefault
|
|
||||||
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 512Mi
|
|
||||||
ephemeral-storage: 1Gi
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
ephemeral-storage: 100Mi
|
|
||||||
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 30
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 6
|
|
||||||
successThreshold: 1
|
|
||||||
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 6
|
|
||||||
successThreshold: 1
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
tolerations: []
|
|
||||||
affinity: {}
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
# -- whether to create a primitive Ingress to expose the FHIR server HTTP endpoint
|
|
||||||
enabled: false
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
# FHIRFLARE-IG-Toolkit Installation Script
|
|
||||||
#
|
|
||||||
# Description:
|
|
||||||
# This script installs the FHIRFLARE-IG-Toolkit Helm chart into a Kubernetes cluster.
|
|
||||||
# It adds the FHIRFLARE-IG-Toolkit Helm repository and then installs the chart
|
|
||||||
# in the 'flare' namespace, creating the namespace if it doesn't exist.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./install.sh
|
|
||||||
#
|
|
||||||
# Requirements:
|
|
||||||
# - Helm (v3+)
|
|
||||||
# - kubectl configured with access to your Kubernetes cluster
|
|
||||||
#
|
|
||||||
|
|
||||||
# Add the FHIRFLARE-IG-Toolkit Helm repository
|
|
||||||
helm repo add flare https://jgsuess.github.io/FHIRFLARE-IG-Toolkit/
|
|
||||||
|
|
||||||
# Install the FHIRFLARE-IG-Toolkit chart in the 'flare' namespace
|
|
||||||
|
|
||||||
helm install flare/fhirflare-ig-toolkit --namespace flare --create-namespace --generate-name --set hapi-fhir-jpaserver.postgresql.primary.persistence.storageClass=gp2 --atomic
|
|
||||||
@ -3,19 +3,18 @@ services:
|
|||||||
fhirflare:
|
fhirflare:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.lite
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
- "8080:8080"
|
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
||||||
volumes:
|
volumes:
|
||||||
- ./instance:/app/instance
|
- ./instance:/app/instance
|
||||||
- ./static/uploads:/app/static/uploads
|
- ./static/uploads:/app/static/uploads
|
||||||
|
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
environment:
|
environment:
|
||||||
- FLASK_APP=app.py
|
- FLASK_APP=app.py
|
||||||
- FLASK_ENV=development
|
- FLASK_ENV=development
|
||||||
- NODE_PATH=/usr/lib/node_modules
|
- NODE_PATH=/usr/lib/node_modules
|
||||||
- APP_MODE=standalone
|
- APP_MODE=lite
|
||||||
- APP_BASE_URL=http://localhost:5000
|
|
||||||
- HAPI_FHIR_URL=https://smile.sparked-fhir.com/aucore/fhir/DEFAULT/
|
|
||||||
command: supervisord -c /etc/supervisord.conf
|
command: supervisord -c /etc/supervisord.conf
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
# This docker-compose file uses ephemeral Docker named volumes for all data storage.
|
|
||||||
# These volumes persist only as long as the Docker volumes exist and are deleted if you run `docker-compose down -v`.
|
|
||||||
# No data is stored on the host filesystem. If you want persistent storage, replace these with host mounts.
|
|
||||||
services:
|
|
||||||
fhirflare-standalone:
|
|
||||||
image: ${FHIRFLARE_IMAGE:-ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest}
|
|
||||||
container_name: fhirflare-standalone
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- fhirflare-instance:/app/instance
|
|
||||||
- fhirflare-uploads:/app/static/uploads
|
|
||||||
- fhirflare-h2-data:/app/h2-data
|
|
||||||
- fhirflare-logs:/app/logs
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
fhirflare-instance:
|
|
||||||
fhirflare-uploads:
|
|
||||||
fhirflare-h2-data:
|
|
||||||
fhirflare-logs:
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Stop and remove all containers defined in the Docker Compose file,
|
|
||||||
# along with any anonymous volumes attached to them.
|
|
||||||
docker compose down --volumes
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Run Docker Compose
|
|
||||||
|
|
||||||
docker compose up --detach --force-recreate --renew-anon-volumes --always-recreate-deps
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
hapi.fhir:
|
|
||||||
ig_runtime_upload_enabled: false
|
|
||||||
narrative_enabled: true
|
|
||||||
logical_urls:
|
|
||||||
- http://terminology.hl7.org/*
|
|
||||||
- https://terminology.hl7.org/*
|
|
||||||
- http://snomed.info/*
|
|
||||||
- https://snomed.info/*
|
|
||||||
- http://unitsofmeasure.org/*
|
|
||||||
- https://unitsofmeasure.org/*
|
|
||||||
- http://loinc.org/*
|
|
||||||
- https://loinc.org/*
|
|
||||||
cors:
|
|
||||||
allow_Credentials: true
|
|
||||||
allowed_origin:
|
|
||||||
- '*'
|
|
||||||
tester.home.name: FHIRFLARE Tester
|
|
||||||
inline_resource_storage_below_size: 4000
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
services:
|
|
||||||
fhirflare:
|
|
||||||
image: ${FHIRFLARE_IMAGE:-ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest}
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
# Ephemeral Docker named volumes for all data storage. No data is stored on the host filesystem.
|
|
||||||
volumes:
|
|
||||||
- fhirflare-instance:/app/instance
|
|
||||||
- fhirflare-uploads:/app/static/uploads
|
|
||||||
- fhirflare-h2-data:/app/h2-data
|
|
||||||
- fhirflare-logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- FLASK_APP=app.py
|
|
||||||
- FLASK_ENV=development
|
|
||||||
- NODE_PATH=/usr/lib/node_modules
|
|
||||||
- APP_MODE=lite
|
|
||||||
- APP_BASE_URL=http://localhost:5000
|
|
||||||
- HAPI_FHIR_URL=http://fhir:8080/fhir
|
|
||||||
command: supervisord -c /etc/supervisord.conf
|
|
||||||
|
|
||||||
fhir:
|
|
||||||
container_name: hapi
|
|
||||||
image: "hapiproject/hapi:v8.2.0-1"
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
configs:
|
|
||||||
- source: hapi
|
|
||||||
target: /app/config/application.yaml
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: "postgres:17.2-bookworm"
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: admin
|
|
||||||
POSTGRES_USER: admin
|
|
||||||
POSTGRES_DB: hapi
|
|
||||||
volumes:
|
|
||||||
- ./hapi.postgress.data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
configs:
|
|
||||||
hapi:
|
|
||||||
file: ./application.yaml
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
fhirflare-instance:
|
|
||||||
fhirflare-uploads:
|
|
||||||
fhirflare-h2-data:
|
|
||||||
fhirflare-logs:
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Stop and remove all containers defined in the Docker Compose file,
|
|
||||||
# along with any anonymous volumes attached to them.
|
|
||||||
docker compose down --volumes
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# FHIRFLARE IG Toolkit
|
|
||||||
|
|
||||||
This directory provides scripts and configuration to start and stop a FHIRFLARE instance with an attached HAPI FHIR server using Docker Compose.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- To start the FHIRFLARE toolkit and HAPI server:
|
|
||||||
```sh
|
|
||||||
./docker-compose/up.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- To stop and remove the containers and volumes:
|
|
||||||
```sh
|
|
||||||
./docker-compose/down.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The web interface will be available at [http://localhost:5000](http://localhost:5000) and the HAPI FHIR server at [http://localhost:8080/fhir](http://localhost:8080/fhir).
|
|
||||||
|
|
||||||
For more details, see the configuration files in this directory.
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Run Docker Compose
|
|
||||||
|
|
||||||
docker compose up --detach --force-recreate --renew-anon-volumes --always-recreate-deps
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
services:
|
|
||||||
fhirflare:
|
|
||||||
image: ${FHIRFLARE_IMAGE:-ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest}
|
|
||||||
ports:
|
|
||||||
- "5000:5000"
|
|
||||||
# Ephemeral Docker named volumes for all data storage. No data is stored on the host filesystem.
|
|
||||||
volumes:
|
|
||||||
- fhirflare-instance:/app/instance
|
|
||||||
- fhirflare-uploads:/app/static/uploads
|
|
||||||
- fhirflare-h2-data:/app/h2-data
|
|
||||||
- fhirflare-logs:/app/logs
|
|
||||||
environment:
|
|
||||||
- FLASK_APP=app.py
|
|
||||||
- FLASK_ENV=development
|
|
||||||
- NODE_PATH=/usr/lib/node_modules
|
|
||||||
- APP_MODE=lite
|
|
||||||
- APP_BASE_URL=http://localhost:5000
|
|
||||||
- HAPI_FHIR_URL=https://cdr.fhirlab.net/fhir
|
|
||||||
command: supervisord -c /etc/supervisord.conf
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
fhirflare-instance:
|
|
||||||
fhirflare-uploads:
|
|
||||||
fhirflare-h2-data:
|
|
||||||
fhirflare-logs:
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Stop and remove all containers defined in the Docker Compose file,
|
|
||||||
# along with any anonymous volumes attached to them.
|
|
||||||
docker compose down --volumes
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
# FHIRFLARE IG Toolkit
|
|
||||||
|
|
||||||
This directory provides scripts and configuration to start and stop a FHIRFLARE instance with an attached HAPI FHIR server using Docker Compose.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
- To start the FHIRFLARE toolkit and HAPI server:
|
|
||||||
```sh
|
|
||||||
./docker-compose/up.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
- To stop and remove the containers and volumes:
|
|
||||||
```sh
|
|
||||||
./docker-compose/down.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The web interface will be available at [http://localhost:5000](http://localhost:5000) and the HAPI FHIR server at [http://localhost:8080/fhir](http://localhost:8080/fhir).
|
|
||||||
|
|
||||||
For more details, see the configuration files in this directory.
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Run Docker Compose
|
|
||||||
|
|
||||||
docker compose up --detach --force-recreate --renew-anon-volumes --always-recreate-deps
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Dockerfile for FHIRFLARE-IG-Toolkit (Optimized for Python/Flask)
|
|
||||||
#
|
|
||||||
# This Dockerfile builds a container for the FHIRFLARE-IG-Toolkit application.
|
|
||||||
#
|
|
||||||
# Key Features:
|
|
||||||
# - Uses python:3.11-slim as the base image for a minimal, secure Python runtime.
|
|
||||||
# - Installs Node.js and global NPM packages (gofsh, fsh-sushi) for FHIR IG tooling.
|
|
||||||
# - Sets up a Python virtual environment and installs all Python dependencies.
|
|
||||||
# - Installs and configures Supervisor to manage the Flask app and related processes.
|
|
||||||
# - Copies all necessary application code, templates, static files, and configuration.
|
|
||||||
# - Exposes ports 5000 (Flask) and 8080 (optional, for compatibility).
|
|
||||||
# - Entrypoint runs Supervisor for process management.
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - The Dockerfile is optimized for Python. Tomcat/Java is not included.
|
|
||||||
# - Node.js is only installed if needed for FHIR IG tooling.
|
|
||||||
# - The image is suitable for development and production with minimal changes.
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Optimized Dockerfile for Python (Flask)
|
|
||||||
FROM python:3.11-slim AS base
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
curl \
|
|
||||||
coreutils \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Optional: Install Node.js if needed for GoFSH/SUSHI
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
|
||||||
&& npm install -g gofsh fsh-sushi \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Set workdir
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy requirements and install Python dependencies
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN python -m venv /app/venv \
|
|
||||||
&& . /app/venv/bin/activate \
|
|
||||||
&& pip install --upgrade pip \
|
|
||||||
&& pip install --no-cache-dir -r requirements.txt \
|
|
||||||
&& pip uninstall -y fhirpath || true \
|
|
||||||
&& pip install --no-cache-dir fhirpathpy \
|
|
||||||
&& pip install supervisor
|
|
||||||
|
|
||||||
# Copy application files
|
|
||||||
COPY app.py .
|
|
||||||
COPY services.py .
|
|
||||||
COPY forms.py .
|
|
||||||
COPY package.py .
|
|
||||||
COPY templates/ templates/
|
|
||||||
COPY static/ static/
|
|
||||||
COPY tests/ tests/
|
|
||||||
COPY supervisord.conf /etc/supervisord.conf
|
|
||||||
|
|
||||||
# Expose ports
|
|
||||||
EXPOSE 5000 8080
|
|
||||||
|
|
||||||
# Set environment
|
|
||||||
ENV PATH="/app/venv/bin:$PATH"
|
|
||||||
|
|
||||||
# Start supervisord
|
|
||||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Build FHIRFLARE-IG-Toolkit Docker image
|
|
||||||
|
|
||||||
# Build the image using the Dockerfile in the docker directory
|
|
||||||
docker build -f Dockerfile -t fhirflare-ig-toolkit:latest ..
|
|
||||||
|
|
||||||
echo "Docker image built successfully"
|
|
||||||
282
forms.py
282
forms.py
@ -1,63 +1,69 @@
|
|||||||
# forms.py
|
# forms.py
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField, PasswordField, SelectMultipleField
|
from wtforms import StringField, SelectField, TextAreaField, BooleanField, SubmitField, FileField
|
||||||
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired
|
from wtforms.validators import DataRequired, Regexp, ValidationError, URL, Optional, InputRequired
|
||||||
from flask import request
|
from flask import request # Import request for file validation in FSHConverterForm
|
||||||
import json
|
import json
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging # Import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__) # Setup logger if needed elsewhere
|
||||||
|
|
||||||
|
# Existing form classes (IgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm) remain unchanged
|
||||||
|
# Only providing RetrieveSplitDataForm
|
||||||
class RetrieveSplitDataForm(FlaskForm):
|
class RetrieveSplitDataForm(FlaskForm):
|
||||||
"""Form for retrieving FHIR bundles and splitting them into individual resources."""
|
"""Form for retrieving FHIR bundles and splitting them into individual resources."""
|
||||||
fhir_server_url = StringField('FHIR Server URL', validators=[URL(), Optional()],
|
fhir_server_url = StringField('FHIR Server URL', validators=[URL(), Optional()],
|
||||||
render_kw={'placeholder': 'e.g., https://hapi.fhir.org/baseR4'})
|
render_kw={'placeholder': 'e.g., https://hapi.fhir.org/baseR4'})
|
||||||
auth_type = SelectField('Authentication Type (for Custom URL)', choices=[
|
|
||||||
('none', 'None'),
|
validate_references = BooleanField('Fetch Referenced Resources', default=False, # Changed label slightly
|
||||||
('bearerToken', 'Bearer Token'),
|
|
||||||
('basicAuth', 'Basic Authentication')
|
|
||||||
], default='none', validators=[Optional()])
|
|
||||||
auth_token = StringField('Bearer Token', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
|
|
||||||
basic_auth_username = StringField('Username', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Basic Auth Username'})
|
|
||||||
basic_auth_password = PasswordField('Password', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Basic Auth Password'})
|
|
||||||
validate_references = BooleanField('Fetch Referenced Resources', default=False,
|
|
||||||
description="If checked, fetches resources referenced by the initial bundles.")
|
description="If checked, fetches resources referenced by the initial bundles.")
|
||||||
|
|
||||||
|
# --- NEW FIELD ---
|
||||||
fetch_reference_bundles = BooleanField('Fetch Full Reference Bundles (instead of individual resources)', default=False,
|
fetch_reference_bundles = BooleanField('Fetch Full Reference Bundles (instead of individual resources)', default=False,
|
||||||
description="Requires 'Fetch Referenced Resources'. Fetches e.g. /Patient instead of Patient/id for each reference.",
|
description="Requires 'Fetch Referenced Resources'. Fetches e.g. /Patient instead of Patient/id for each reference.",
|
||||||
render_kw={'data-dependency': 'validate_references'})
|
render_kw={'data-dependency': 'validate_references'}) # Add data attribute for JS
|
||||||
|
# --- END NEW FIELD ---
|
||||||
|
|
||||||
split_bundle_zip = FileField('Upload Bundles to Split (ZIP)', validators=[Optional()],
|
split_bundle_zip = FileField('Upload Bundles to Split (ZIP)', validators=[Optional()],
|
||||||
render_kw={'accept': '.zip'})
|
render_kw={'accept': '.zip'})
|
||||||
submit_retrieve = SubmitField('Retrieve Bundles')
|
submit_retrieve = SubmitField('Retrieve Bundles')
|
||||||
submit_split = SubmitField('Split Bundles')
|
submit_split = SubmitField('Split Bundles')
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self, extra_validators=None):
|
||||||
|
"""Custom validation for RetrieveSplitDataForm."""
|
||||||
if not super().validate(extra_validators):
|
if not super().validate(extra_validators):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# --- NEW VALIDATION LOGIC ---
|
||||||
|
# Ensure fetch_reference_bundles is only checked if validate_references is also checked
|
||||||
if self.fetch_reference_bundles.data and not self.validate_references.data:
|
if self.fetch_reference_bundles.data and not self.validate_references.data:
|
||||||
self.fetch_reference_bundles.errors.append('Cannot fetch full reference bundles unless "Fetch Referenced Resources" is also checked.')
|
self.fetch_reference_bundles.errors.append('Cannot fetch full reference bundles unless "Fetch Referenced Resources" is also checked.')
|
||||||
return False
|
return False
|
||||||
if self.auth_type.data == 'bearerToken' and self.submit_retrieve.data and not self.auth_token.data:
|
# --- END NEW VALIDATION LOGIC ---
|
||||||
self.auth_token.errors.append('Bearer Token is required when Bearer Token authentication is selected.')
|
|
||||||
return False
|
# Validate based on which submit button was pressed
|
||||||
if self.auth_type.data == 'basicAuth' and self.submit_retrieve.data:
|
if self.submit_retrieve.data:
|
||||||
if not self.basic_auth_username.data:
|
# No specific validation needed here now, handled by URL validator and JS
|
||||||
self.basic_auth_username.errors.append('Username is required for Basic Authentication.')
|
pass
|
||||||
return False
|
elif self.submit_split.data:
|
||||||
if not self.basic_auth_password.data:
|
# Need to check bundle source radio button selection in backend/JS,
|
||||||
self.basic_auth_password.errors.append('Password is required for Basic Authentication.')
|
# but validate file if 'upload' is selected.
|
||||||
return False
|
# This validation might need refinement based on how source is handled.
|
||||||
|
# Assuming 'split_bundle_zip' is only required if 'upload' source is chosen.
|
||||||
|
pass # Basic validation done by Optional() and file type checks below
|
||||||
|
|
||||||
|
# Validate file uploads (keep existing)
|
||||||
if self.split_bundle_zip.data:
|
if self.split_bundle_zip.data:
|
||||||
if not self.split_bundle_zip.data.filename.lower().endswith('.zip'):
|
if not self.split_bundle_zip.data.filename.lower().endswith('.zip'):
|
||||||
self.split_bundle_zip.errors.append('File must be a ZIP file.')
|
self.split_bundle_zip.errors.append('File must be a ZIP file.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Existing forms (IgImportForm, ValidationForm) remain unchanged
|
||||||
class IgImportForm(FlaskForm):
|
class IgImportForm(FlaskForm):
|
||||||
"""Form for importing Implementation Guides."""
|
"""Form for importing Implementation Guides."""
|
||||||
package_name = StringField('Package Name', validators=[
|
package_name = StringField('Package Name', validators=[
|
||||||
@ -75,62 +81,6 @@ class IgImportForm(FlaskForm):
|
|||||||
], default='recursive')
|
], default='recursive')
|
||||||
submit = SubmitField('Import')
|
submit = SubmitField('Import')
|
||||||
|
|
||||||
class ManualIgImportForm(FlaskForm):
|
|
||||||
"""Form for manual importing Implementation Guides via file or URL."""
|
|
||||||
import_mode = SelectField('Import Mode', choices=[
|
|
||||||
('file', 'Upload File'),
|
|
||||||
('url', 'From URL')
|
|
||||||
], default='file', validators=[DataRequired()])
|
|
||||||
tgz_file = FileField('IG Package File (.tgz)', validators=[Optional()],
|
|
||||||
render_kw={'accept': '.tgz'})
|
|
||||||
tgz_url = StringField('IG Package URL', validators=[Optional(), URL()],
|
|
||||||
render_kw={'placeholder': 'e.g., https://example.com/hl7.fhir.au.core-1.1.0-preview.tgz'})
|
|
||||||
dependency_mode = SelectField('Dependency Mode', choices=[
|
|
||||||
('recursive', 'Current Recursive'),
|
|
||||||
('patch-canonical', 'Patch Canonical Versions'),
|
|
||||||
('tree-shaking', 'Tree Shaking (Only Used Dependencies)')
|
|
||||||
], default='recursive')
|
|
||||||
resolve_dependencies = BooleanField('Resolve Dependencies', default=True,
|
|
||||||
render_kw={'class': 'form-check-input'})
|
|
||||||
submit = SubmitField('Import')
|
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
|
||||||
if not super().validate(extra_validators):
|
|
||||||
return False
|
|
||||||
mode = self.import_mode.data
|
|
||||||
has_file = request and request.files and self.tgz_file.name in request.files and request.files[self.tgz_file.name].filename != ''
|
|
||||||
has_url = bool(self.tgz_url.data) # Convert to boolean: True if non-empty string
|
|
||||||
|
|
||||||
# Ensure exactly one input method is used
|
|
||||||
inputs_provided = sum([has_file, has_url])
|
|
||||||
if inputs_provided != 1:
|
|
||||||
if inputs_provided == 0:
|
|
||||||
self.import_mode.errors.append('Please provide input for one import method (File or URL).')
|
|
||||||
else:
|
|
||||||
self.import_mode.errors.append('Please use only one import method at a time.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Validate based on import mode
|
|
||||||
if mode == 'file':
|
|
||||||
if not has_file:
|
|
||||||
self.tgz_file.errors.append('A .tgz file is required for File import.')
|
|
||||||
return False
|
|
||||||
if not self.tgz_file.data.filename.lower().endswith('.tgz'):
|
|
||||||
self.tgz_file.errors.append('File must be a .tgz file.')
|
|
||||||
return False
|
|
||||||
elif mode == 'url':
|
|
||||||
if not has_url:
|
|
||||||
self.tgz_url.errors.append('A valid URL is required for URL import.')
|
|
||||||
return False
|
|
||||||
if not self.tgz_url.data.lower().endswith('.tgz'):
|
|
||||||
self.tgz_url.errors.append('URL must point to a .tgz file.')
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
self.import_mode.errors.append('Invalid import mode selected.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
class ValidationForm(FlaskForm):
|
class ValidationForm(FlaskForm):
|
||||||
"""Form for validating FHIR samples."""
|
"""Form for validating FHIR samples."""
|
||||||
package_name = StringField('Package Name', validators=[DataRequired()])
|
package_name = StringField('Package Name', validators=[DataRequired()])
|
||||||
@ -142,6 +92,7 @@ class ValidationForm(FlaskForm):
|
|||||||
], default='single')
|
], default='single')
|
||||||
sample_input = TextAreaField('Sample Input', validators=[
|
sample_input = TextAreaField('Sample Input', validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
|
# Removed lambda validator for simplicity, can be added back if needed
|
||||||
])
|
])
|
||||||
submit = SubmitField('Validate')
|
submit = SubmitField('Validate')
|
||||||
|
|
||||||
@ -166,7 +117,7 @@ class FSHConverterForm(FlaskForm):
|
|||||||
('info', 'Info'),
|
('info', 'Info'),
|
||||||
('debug', 'Debug')
|
('debug', 'Debug')
|
||||||
], validators=[DataRequired()])
|
], validators=[DataRequired()])
|
||||||
fhir_version = SelectField('FHIR Version', choices=[
|
fhir_version = SelectField('FHIR Version', choices=[ # Corrected label
|
||||||
('', 'Auto-detect'),
|
('', 'Auto-detect'),
|
||||||
('4.0.1', 'R4'),
|
('4.0.1', 'R4'),
|
||||||
('4.3.0', 'R4B'),
|
('4.3.0', 'R4B'),
|
||||||
@ -185,185 +136,116 @@ class FSHConverterForm(FlaskForm):
|
|||||||
submit = SubmitField('Convert to FSH')
|
submit = SubmitField('Convert to FSH')
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self, extra_validators=None):
|
||||||
|
"""Custom validation for FSH Converter Form."""
|
||||||
|
# Run default validators first
|
||||||
if not super().validate(extra_validators):
|
if not super().validate(extra_validators):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check file/text input based on mode
|
||||||
|
# Need to check request.files for file uploads as self.fhir_file.data might be None during initial POST validation
|
||||||
has_file_in_request = request and request.files and self.fhir_file.name in request.files and request.files[self.fhir_file.name].filename != ''
|
has_file_in_request = request and request.files and self.fhir_file.name in request.files and request.files[self.fhir_file.name].filename != ''
|
||||||
if self.input_mode.data == 'file' and not has_file_in_request:
|
if self.input_mode.data == 'file' and not has_file_in_request:
|
||||||
|
# If it's not in request.files, check if data is already populated (e.g., on re-render after error)
|
||||||
if not self.fhir_file.data:
|
if not self.fhir_file.data:
|
||||||
self.fhir_file.errors.append('File is required when input mode is Upload File.')
|
self.fhir_file.errors.append('File is required when input mode is Upload File.')
|
||||||
return False
|
return False
|
||||||
if self.input_mode.data == 'text' and not self.fhir_text.data:
|
if self.input_mode.data == 'text' and not self.fhir_text.data:
|
||||||
self.fhir_text.errors.append('Text input is required when input mode is Paste Text.')
|
self.fhir_text.errors.append('Text input is required when input mode is Paste Text.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Validate text input format
|
||||||
if self.input_mode.data == 'text' and self.fhir_text.data:
|
if self.input_mode.data == 'text' and self.fhir_text.data:
|
||||||
try:
|
try:
|
||||||
content = self.fhir_text.data.strip()
|
content = self.fhir_text.data.strip()
|
||||||
if not content: pass
|
if not content: # Empty text is technically valid but maybe not useful
|
||||||
elif content.startswith('{'): json.loads(content)
|
pass # Allow empty text for now
|
||||||
elif content.startswith('<'): ET.fromstring(content)
|
elif content.startswith('{'):
|
||||||
|
json.loads(content)
|
||||||
|
elif content.startswith('<'):
|
||||||
|
ET.fromstring(content) # Basic XML check
|
||||||
else:
|
else:
|
||||||
|
# If content exists but isn't JSON or XML, it's an error
|
||||||
self.fhir_text.errors.append('Text input must be valid JSON or XML.')
|
self.fhir_text.errors.append('Text input must be valid JSON or XML.')
|
||||||
return False
|
return False
|
||||||
except (json.JSONDecodeError, ET.ParseError):
|
except (json.JSONDecodeError, ET.ParseError):
|
||||||
self.fhir_text.errors.append('Invalid JSON or XML format.')
|
self.fhir_text.errors.append('Invalid JSON or XML format.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Validate dependency format
|
||||||
if self.dependencies.data:
|
if self.dependencies.data:
|
||||||
for dep in self.dependencies.data.splitlines():
|
for dep in self.dependencies.data.splitlines():
|
||||||
dep = dep.strip()
|
dep = dep.strip()
|
||||||
|
# Allow versions like 'current', 'dev', etc. but require package@version format
|
||||||
if dep and not re.match(r'^[a-zA-Z0-9\-\.]+@[a-zA-Z0-9\.\-]+$', dep):
|
if dep and not re.match(r'^[a-zA-Z0-9\-\.]+@[a-zA-Z0-9\.\-]+$', dep):
|
||||||
self.dependencies.errors.append(f'Invalid dependency format: "{dep}". Use package@version (e.g., hl7.fhir.us.core@6.1.0).')
|
self.dependencies.errors.append(f'Invalid dependency format: "{dep}". Use package@version (e.g., hl7.fhir.us.core@6.1.0).')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Validate alias file extension (optional, basic check)
|
||||||
|
# Check request.files for alias file as well
|
||||||
has_alias_file_in_request = request and request.files and self.alias_file.name in request.files and request.files[self.alias_file.name].filename != ''
|
has_alias_file_in_request = request and request.files and self.alias_file.name in request.files and request.files[self.alias_file.name].filename != ''
|
||||||
alias_file_data = self.alias_file.data or (request.files.get(self.alias_file.name) if request else None)
|
alias_file_data = self.alias_file.data or (request.files.get(self.alias_file.name) if request else None)
|
||||||
|
|
||||||
if alias_file_data and alias_file_data.filename:
|
if alias_file_data and alias_file_data.filename:
|
||||||
if not alias_file_data.filename.lower().endswith('.fsh'):
|
if not alias_file_data.filename.lower().endswith('.fsh'):
|
||||||
self.alias_file.errors.append('Alias file should have a .fsh extension.')
|
self.alias_file.errors.append('Alias file should have a .fsh extension.')
|
||||||
|
# return False # Might be too strict, maybe just warn?
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TestDataUploadForm(FlaskForm):
|
class TestDataUploadForm(FlaskForm):
|
||||||
"""Form for uploading FHIR test data."""
|
"""Form for uploading FHIR test data."""
|
||||||
fhir_server_url = StringField('Target FHIR Server URL', validators=[DataRequired(), URL()],
|
fhir_server_url = StringField('Target FHIR Server URL', validators=[DataRequired(), URL()],
|
||||||
render_kw={'placeholder': 'e.g., http://localhost:8080/fhir'})
|
render_kw={'placeholder': 'e.g., http://localhost:8080/fhir'})
|
||||||
|
|
||||||
auth_type = SelectField('Authentication Type', choices=[
|
auth_type = SelectField('Authentication Type', choices=[
|
||||||
('none', 'None'),
|
('none', 'None'),
|
||||||
('bearerToken', 'Bearer Token'),
|
('bearerToken', 'Bearer Token')
|
||||||
('basic', 'Basic Authentication')
|
|
||||||
], default='none')
|
], default='none')
|
||||||
|
|
||||||
auth_token = StringField('Bearer Token', validators=[Optional()],
|
auth_token = StringField('Bearer Token', validators=[Optional()],
|
||||||
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
|
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
|
||||||
username = StringField('Username', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Basic Auth Username'})
|
|
||||||
password = PasswordField('Password', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Basic Auth Password'})
|
|
||||||
test_data_file = FileField('Select Test Data File(s)', validators=[InputRequired("Please select at least one file.")],
|
test_data_file = FileField('Select Test Data File(s)', validators=[InputRequired("Please select at least one file.")],
|
||||||
render_kw={'multiple': True, 'accept': '.json,.xml,.zip'})
|
render_kw={'multiple': True, 'accept': '.json,.xml,.zip'})
|
||||||
|
|
||||||
validate_before_upload = BooleanField('Validate Resources Before Upload?', default=False,
|
validate_before_upload = BooleanField('Validate Resources Before Upload?', default=False,
|
||||||
description="Validate resources against selected package profile before uploading.")
|
description="Validate resources against selected package profile before uploading.")
|
||||||
validation_package_id = SelectField('Validation Profile Package (Optional)',
|
validation_package_id = SelectField('Validation Profile Package (Optional)',
|
||||||
choices=[('', '-- Select Package for Validation --')],
|
choices=[('', '-- Select Package for Validation --')],
|
||||||
validators=[Optional()],
|
validators=[Optional()],
|
||||||
description="Select the processed IG package to use for validation.")
|
description="Select the processed IG package to use for validation.")
|
||||||
|
|
||||||
upload_mode = SelectField('Upload Mode', choices=[
|
upload_mode = SelectField('Upload Mode', choices=[
|
||||||
('individual', 'Individual Resources'),
|
('individual', 'Individual Resources'), # Simplified label
|
||||||
('transaction', 'Transaction Bundle')
|
('transaction', 'Transaction Bundle') # Simplified label
|
||||||
], default='individual')
|
], default='individual')
|
||||||
|
|
||||||
|
# --- NEW FIELD for Conditional Upload ---
|
||||||
use_conditional_uploads = BooleanField('Use Conditional Upload (Individual Mode Only)?', default=True,
|
use_conditional_uploads = BooleanField('Use Conditional Upload (Individual Mode Only)?', default=True,
|
||||||
description="If checked, checks resource existence (GET) and uses If-Match (PUT) or creates (PUT). If unchecked, uses simple PUT for all.")
|
description="If checked, checks resource existence (GET) and uses If-Match (PUT) or creates (PUT). If unchecked, uses simple PUT for all.")
|
||||||
|
# --- END NEW FIELD ---
|
||||||
|
|
||||||
error_handling = SelectField('Error Handling', choices=[
|
error_handling = SelectField('Error Handling', choices=[
|
||||||
('stop', 'Stop on First Error'),
|
('stop', 'Stop on First Error'),
|
||||||
('continue', 'Continue on Error')
|
('continue', 'Continue on Error')
|
||||||
], default='stop')
|
], default='stop')
|
||||||
|
|
||||||
submit = SubmitField('Upload and Process')
|
submit = SubmitField('Upload and Process')
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
def validate(self, extra_validators=None):
|
||||||
if not super().validate(extra_validators):
|
"""Custom validation for Test Data Upload Form."""
|
||||||
return False
|
if not super().validate(extra_validators): return False
|
||||||
if self.validate_before_upload.data and not self.validation_package_id.data:
|
if self.validate_before_upload.data and not self.validation_package_id.data:
|
||||||
self.validation_package_id.errors.append('Please select a package to validate against when pre-upload validation is enabled.')
|
self.validation_package_id.errors.append('Please select a package to validate against when pre-upload validation is enabled.')
|
||||||
return False
|
return False
|
||||||
|
# Add check: Conditional uploads only make sense for individual mode
|
||||||
if self.use_conditional_uploads.data and self.upload_mode.data == 'transaction':
|
if self.use_conditional_uploads.data and self.upload_mode.data == 'transaction':
|
||||||
self.use_conditional_uploads.errors.append('Conditional Uploads only apply to the "Individual Resources" mode.')
|
self.use_conditional_uploads.errors.append('Conditional Uploads only apply to the "Individual Resources" mode.')
|
||||||
return False
|
# We might allow this combination but warn the user it has no effect,
|
||||||
if self.auth_type.data == 'bearerToken' and not self.auth_token.data:
|
# or enforce it here. Let's enforce for clarity.
|
||||||
self.auth_token.errors.append('Bearer Token is required when Bearer Token authentication is selected.')
|
# return False # Optional: Make this a hard validation failure
|
||||||
return False
|
# Or just let it pass and ignore the flag in the backend for transaction mode.
|
||||||
if self.auth_type.data == 'basic':
|
pass # Let it pass for now, backend will ignore if mode is transaction
|
||||||
if not self.username.data:
|
|
||||||
self.username.errors.append('Username is required for Basic Authentication.')
|
|
||||||
return False
|
|
||||||
if not self.password.data:
|
|
||||||
self.password.errors.append('Password is required for Basic Authentication.')
|
|
||||||
return False
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class FhirRequestForm(FlaskForm):
|
|
||||||
fhir_server_url = StringField('FHIR Server URL', validators=[URL(), Optional()],
|
|
||||||
render_kw={'placeholder': 'e.g., https://hapi.fhir.org/baseR4'})
|
|
||||||
auth_type = SelectField('Authentication Type (for Custom URL)', choices=[
|
|
||||||
('none', 'None'),
|
|
||||||
('bearerToken', 'Bearer Token'),
|
|
||||||
('basicAuth', 'Basic Authentication')
|
|
||||||
], default='none', validators=[Optional()])
|
|
||||||
auth_token = StringField('Bearer Token', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Bearer Token', 'type': 'password'})
|
|
||||||
basic_auth_username = StringField('Username', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Basic Auth Username'})
|
|
||||||
basic_auth_password = PasswordField('Password', validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'Enter Basic Auth Password'})
|
|
||||||
submit = SubmitField('Send Request')
|
|
||||||
|
|
||||||
def validate(self, extra_validators=None):
|
|
||||||
if not super().validate(extra_validators):
|
|
||||||
return False
|
|
||||||
if self.fhir_server_url.data:
|
|
||||||
if self.auth_type.data == 'bearerToken' and not self.auth_token.data:
|
|
||||||
self.auth_token.errors.append('Bearer Token is required when Bearer Token authentication is selected for a custom URL.')
|
|
||||||
return False
|
|
||||||
if self.auth_type.data == 'basicAuth':
|
|
||||||
if not self.basic_auth_username.data:
|
|
||||||
self.basic_auth_username.errors.append('Username is required for Basic Authentication with a custom URL.')
|
|
||||||
return False
|
|
||||||
if not self.basic_auth_password.data:
|
|
||||||
self.basic_auth_password.errors.append('Password is required for Basic Authentication with a custom URL.')
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
class IgYamlForm(FlaskForm):
|
|
||||||
"""Form to select IGs for YAML generation."""
|
|
||||||
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
|
|
||||||
generate_yaml = SubmitField('Generate YAML')
|
|
||||||
|
|
||||||
# --- New ValidationForm class for the new page ---
|
|
||||||
class ValidationForm(FlaskForm):
|
|
||||||
"""Form for validating a single FHIR resource with various options."""
|
|
||||||
# Added fields to match the HTML template
|
|
||||||
package_name = StringField('Package Name', validators=[Optional()])
|
|
||||||
version = StringField('Package Version', validators=[Optional()])
|
|
||||||
|
|
||||||
# Main content fields
|
|
||||||
fhir_resource = TextAreaField('FHIR Resource (JSON or XML)', validators=[InputRequired()],
|
|
||||||
render_kw={'placeholder': 'Paste your FHIR JSON here...'})
|
|
||||||
fhir_version = SelectField('FHIR Version', choices=[
|
|
||||||
('4.0.1', 'R4 (4.0.1)'),
|
|
||||||
('3.0.2', 'STU3 (3.0.2)'),
|
|
||||||
('5.0.0', 'R5 (5.0.0)')
|
|
||||||
], default='4.0.1', validators=[DataRequired()])
|
|
||||||
|
|
||||||
# Flags and options from validator settings.pdf
|
|
||||||
do_native = BooleanField('Native Validation (doNative)')
|
|
||||||
hint_about_must_support = BooleanField('Must Support (hintAboutMustSupport)')
|
|
||||||
assume_valid_rest_references = BooleanField('Assume Valid Rest References (assumeValidRestReferences)')
|
|
||||||
no_extensible_binding_warnings = BooleanField('Extensible Binding Warnings (noExtensibleBindingWarnings)')
|
|
||||||
show_times = BooleanField('Show Times (-show-times)')
|
|
||||||
allow_example_urls = BooleanField('Allow Example URLs (-allow-example-urls)')
|
|
||||||
check_ips_codes = BooleanField('Check IPS Codes (-check-ips-codes)')
|
|
||||||
allow_any_extensions = BooleanField('Allow Any Extensions (-allow-any-extensions)')
|
|
||||||
tx_routing = BooleanField('Show Terminology Routing (-tx-routing)')
|
|
||||||
|
|
||||||
# SNOMED CT options
|
|
||||||
snomed_ct_version = SelectField('Select SNOMED Version', choices=[
|
|
||||||
('', '-- No selection --'),
|
|
||||||
('intl', 'International edition (900000000000207008)'),
|
|
||||||
('us', 'US edition (731000124108)'),
|
|
||||||
('uk', 'United Kingdom Edition (999000041000000102)'),
|
|
||||||
('es', 'Spanish Language Edition (449081005)'),
|
|
||||||
('nl', 'Netherlands Edition (11000146104)'),
|
|
||||||
('ca', 'Canadian Edition (20611000087101)'),
|
|
||||||
('dk', 'Danish Edition (554471000005108)'),
|
|
||||||
('se', 'Swedish Edition (45991000052106)'),
|
|
||||||
('au', 'Australian Edition (32506021000036107)'),
|
|
||||||
('be', 'Belgium Edition (11000172109)')
|
|
||||||
], validators=[Optional()])
|
|
||||||
|
|
||||||
# Text input fields
|
|
||||||
profiles = StringField('Profiles',
|
|
||||||
validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'})
|
|
||||||
extensions = StringField('Extensions',
|
|
||||||
validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace'})
|
|
||||||
terminology_server = StringField('Terminology Server',
|
|
||||||
validators=[Optional()],
|
|
||||||
render_kw={'placeholder': 'e.g. http://tx.fhir.org'})
|
|
||||||
|
|
||||||
submit = SubmitField('Validate')
|
|
||||||
Binary file not shown.
BIN
instance/fhir_ig.db.old
Normal file
BIN
instance/fhir_ig.db.old
Normal file
Binary file not shown.
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:17.475734+00:00"
|
||||||
|
}
|
||||||
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.
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.au.core",
|
||||||
|
"version": "1.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"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.au.base",
|
||||||
|
"version": "5.1.0-preview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.uv.smart-app-launch",
|
||||||
|
"version": "2.1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hl7.fhir.uv.ipa",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:15.067826+00:00"
|
||||||
|
}
|
||||||
BIN
instance/fhir_packages/hl7.fhir.au.core-1.1.0-preview.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.au.core-1.1.0-preview.tgz
Normal file
Binary file not shown.
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"package_name": "hl7.fhir.r4.core",
|
||||||
|
"version": "4.0.1",
|
||||||
|
"dependency_mode": "recursive",
|
||||||
|
"imported_dependencies": [],
|
||||||
|
"complies_with_profiles": [],
|
||||||
|
"imposed_profiles": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:16.477868+00:00"
|
||||||
|
}
|
||||||
BIN
instance/fhir_packages/hl7.fhir.r4.core-4.0.1.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.r4.core-4.0.1.tgz
Normal file
Binary file not shown.
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:17.363719+00:00"
|
||||||
|
}
|
||||||
BIN
instance/fhir_packages/hl7.fhir.uv.extensions.r4-5.2.0.tgz
Normal file
BIN
instance/fhir_packages/hl7.fhir.uv.extensions.r4-5.2.0.tgz
Normal file
Binary file not shown.
22
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
22
instance/fhir_packages/hl7.fhir.uv.ipa-1.0.0.metadata.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:17.590266+00:00"
|
||||||
|
}
|
||||||
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,14 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:18.256800+00:00"
|
||||||
|
}
|
||||||
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,18 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:17.529611+00:00"
|
||||||
|
}
|
||||||
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,14 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:18.216757+00:00"
|
||||||
|
}
|
||||||
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,14 @@
|
|||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"timestamp": "2025-05-04T12:29:17.148041+00:00"
|
||||||
|
}
|
||||||
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.
6
instance/hapi-h2-data/fhir.lock.db
Normal file
6
instance/hapi-h2-data/fhir.lock.db
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#FileLock
|
||||||
|
#Sun May 04 12:29:20 UTC 2025
|
||||||
|
server=172.18.0.2\:34351
|
||||||
|
hostName=1913c9e2ec9b
|
||||||
|
method=file
|
||||||
|
id=1969b45b76c42f20115290bfabb203a60dc75365e9d
|
||||||
BIN
instance/hapi-h2-data/fhir.mv.db
Normal file
BIN
instance/hapi-h2-data/fhir.mv.db
Normal file
Binary file not shown.
213193
instance/hapi-h2-data/fhir.trace.db
Normal file
213193
instance/hapi-h2-data/fhir.trace.db
Normal file
File diff suppressed because it is too large
Load Diff
@ -11,4 +11,3 @@ Flask-Migrate==4.1.0
|
|||||||
cachetools
|
cachetools
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
feedparser==6.0.11
|
feedparser==6.0.11
|
||||||
flasgger
|
|
||||||
|
|||||||
2113
services.py
2113
services.py
File diff suppressed because it is too large
Load Diff
395
setup_linux.sh
395
setup_linux.sh
@ -1,395 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# --- Configuration ---
|
|
||||||
REPO_URL_HAPI="https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git"
|
|
||||||
REPO_URL_CANDLE="https://github.com/FHIR/fhir-candle.git"
|
|
||||||
CLONE_DIR_HAPI="hapi-fhir-jpaserver"
|
|
||||||
CLONE_DIR_CANDLE="fhir-candle"
|
|
||||||
SOURCE_CONFIG_DIR="hapi-fhir-Setup"
|
|
||||||
CONFIG_FILE="application.yaml"
|
|
||||||
|
|
||||||
# --- Define Paths ---
|
|
||||||
SOURCE_CONFIG_PATH="../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
|
|
||||||
DEST_CONFIG_PATH="${CLONE_DIR_HAPI}/target/classes/${CONFIG_FILE}"
|
|
||||||
|
|
||||||
APP_MODE=""
|
|
||||||
CUSTOM_FHIR_URL_VAL=""
|
|
||||||
SERVER_TYPE=""
|
|
||||||
CANDLE_FHIR_VERSION=""
|
|
||||||
|
|
||||||
# --- Error Handling Function ---
|
|
||||||
handle_error() {
|
|
||||||
echo "------------------------------------"
|
|
||||||
echo "An error occurred: $1"
|
|
||||||
echo "Script aborted."
|
|
||||||
echo "------------------------------------"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# === MODIFIED: Prompt for Installation Mode ===
|
|
||||||
get_mode_choice() {
|
|
||||||
echo ""
|
|
||||||
echo "Select Installation Mode:"
|
|
||||||
echo "1. Lite (Excludes local HAPI FHIR Server - No Git/Maven/Dotnet needed)"
|
|
||||||
echo "2. Custom URL (Uses a custom FHIR Server - No Git/Maven/Dotnet needed)"
|
|
||||||
echo "3. Hapi (Includes local HAPI FHIR Server - Requires Git & Maven)"
|
|
||||||
echo "4. Candle (Includes local FHIR Candle Server - Requires Git & Dotnet)"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
read -r -p "Enter your choice (1, 2, 3, or 4): " choice
|
|
||||||
case "$choice" in
|
|
||||||
1)
|
|
||||||
APP_MODE="lite"
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
APP_MODE="standalone"
|
|
||||||
get_custom_url_prompt
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
APP_MODE="standalone"
|
|
||||||
SERVER_TYPE="hapi"
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
4)
|
|
||||||
APP_MODE="standalone"
|
|
||||||
SERVER_TYPE="candle"
|
|
||||||
get_candle_fhir_version
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Invalid input. Please try again."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
echo "Selected Mode: $APP_MODE"
|
|
||||||
echo "Server Type: $SERVER_TYPE"
|
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
# === NEW: Prompt for Custom URL ===
|
|
||||||
get_custom_url_prompt() {
|
|
||||||
local confirmed_url=""
|
|
||||||
while true; do
|
|
||||||
echo
|
|
||||||
read -r -p "Please enter the custom FHIR server URL: " custom_url_input
|
|
||||||
echo
|
|
||||||
echo "You entered: $custom_url_input"
|
|
||||||
read -r -p "Is this URL correct? (Y/N): " confirm_url
|
|
||||||
if [[ "$confirm_url" =~ ^[Yy]$ ]]; then
|
|
||||||
confirmed_url="$custom_url_input"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "URL not confirmed. Please re-enter."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
echo
|
|
||||||
read -r -p "Please re-enter the URL to confirm it is correct: " custom_url_input
|
|
||||||
if [ "$custom_url_input" = "$confirmed_url" ]; then
|
|
||||||
CUSTOM_FHIR_URL_VAL="$custom_url_input"
|
|
||||||
echo
|
|
||||||
echo "Custom URL confirmed: $CUSTOM_FHIR_URL_VAL"
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo
|
|
||||||
echo "URLs do not match. Please try again."
|
|
||||||
confirmed_url="$custom_url_input"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# === NEW: Prompt for Candle FHIR version ===
|
|
||||||
get_candle_fhir_version() {
|
|
||||||
echo ""
|
|
||||||
echo "Select the FHIR version for the Candle server:"
|
|
||||||
echo "1. R4 (4.0)"
|
|
||||||
echo "2. R4B (4.3)"
|
|
||||||
echo "3. R5 (5.0)"
|
|
||||||
while true; do
|
|
||||||
read -r -p "Enter your choice (1, 2, or 3): " choice
|
|
||||||
case "$choice" in
|
|
||||||
1)
|
|
||||||
CANDLE_FHIR_VERSION=r4
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
2)
|
|
||||||
CANDLE_FHIR_VERSION=r4b
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
3)
|
|
||||||
CANDLE_FHIR_VERSION=r5
|
|
||||||
break
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Invalid input. Please try again."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Call the function to get mode choice
|
|
||||||
get_mode_choice
|
|
||||||
|
|
||||||
# === Conditionally Execute Server Setup ===
|
|
||||||
case "$SERVER_TYPE" in
|
|
||||||
"hapi")
|
|
||||||
echo "Running Hapi server setup..."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 0: Clean up previous clone (optional) ---
|
|
||||||
echo "Checking for existing directory: $CLONE_DIR_HAPI"
|
|
||||||
if [ -d "$CLONE_DIR_HAPI" ]; then
|
|
||||||
echo "Found existing directory, removing it..."
|
|
||||||
rm -rf "$CLONE_DIR_HAPI"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Failed to remove existing directory: $CLONE_DIR_HAPI"
|
|
||||||
fi
|
|
||||||
echo "Existing directory removed."
|
|
||||||
else
|
|
||||||
echo "Directory does not exist, proceeding with clone."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 1: Clone the HAPI FHIR server repository ---
|
|
||||||
echo "Cloning repository: $REPO_URL_HAPI into $CLONE_DIR_HAPI..."
|
|
||||||
git clone "$REPO_URL_HAPI" "$CLONE_DIR_HAPI"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Failed to clone repository. Check Git installation and network connection."
|
|
||||||
fi
|
|
||||||
echo "Repository cloned successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 2: Navigate into the cloned directory ---
|
|
||||||
echo "Changing directory to $CLONE_DIR_HAPI..."
|
|
||||||
cd "$CLONE_DIR_HAPI" || handle_error "Failed to change directory to $CLONE_DIR_HAPI."
|
|
||||||
echo "Current directory: $(pwd)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 3: Build the HAPI server using Maven ---
|
|
||||||
echo "===> Starting Maven build (Step 3)..."
|
|
||||||
mvn clean package -DskipTests=true -Pboot
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "ERROR: Maven build failed."
|
|
||||||
cd ..
|
|
||||||
handle_error "Maven build process resulted in an error."
|
|
||||||
fi
|
|
||||||
echo "Maven build completed successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 4: Copy the configuration file ---
|
|
||||||
echo "===> Starting file copy (Step 4)..."
|
|
||||||
echo "Copying configuration file..."
|
|
||||||
INITIAL_SCRIPT_DIR=$(pwd)
|
|
||||||
ABSOLUTE_SOURCE_CONFIG_PATH="${INITIAL_SCRIPT_DIR}/../${SOURCE_CONFIG_DIR}/target/classes/${CONFIG_FILE}"
|
|
||||||
|
|
||||||
echo "Source: $ABSOLUTE_SOURCE_CONFIG_PATH"
|
|
||||||
echo "Destination: target/classes/$CONFIG_FILE"
|
|
||||||
|
|
||||||
if [ ! -f "$ABSOLUTE_SOURCE_CONFIG_PATH" ]; then
|
|
||||||
echo "WARNING: Source configuration file not found at $ABSOLUTE_SOURCE_CONFIG_PATH."
|
|
||||||
echo "The script will continue, but the server might use default configuration."
|
|
||||||
else
|
|
||||||
cp "$ABSOLUTE_SOURCE_CONFIG_PATH" "target/classes/"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "WARNING: Failed to copy configuration file. Check if the source file exists and permissions."
|
|
||||||
echo "The script will continue, but the server might use default configuration."
|
|
||||||
else
|
|
||||||
echo "Configuration file copied successfully."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 5: Navigate back to the parent directory ---
|
|
||||||
echo "===> Changing directory back (Step 5)..."
|
|
||||||
cd .. || handle_error "Failed to change back to the parent directory."
|
|
||||||
echo "Current directory: $(pwd)"
|
|
||||||
echo
|
|
||||||
;;
|
|
||||||
|
|
||||||
"candle")
|
|
||||||
echo "Running FHIR Candle server setup..."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 0: Clean up previous clone (optional) ---
|
|
||||||
echo "Checking for existing directory: $CLONE_DIR_CANDLE"
|
|
||||||
if [ -d "$CLONE_DIR_CANDLE" ]; then
|
|
||||||
echo "Found existing directory, removing it..."
|
|
||||||
rm -rf "$CLONE_DIR_CANDLE"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Failed to remove existing directory: $CLONE_DIR_CANDLE"
|
|
||||||
fi
|
|
||||||
echo "Existing directory removed."
|
|
||||||
else
|
|
||||||
echo "Directory does not exist, proceeding with clone."
|
|
||||||
fi
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 1: Clone the FHIR Candle server repository ---
|
|
||||||
echo "Cloning repository: $REPO_URL_CANDLE into $CLONE_DIR_CANDLE..."
|
|
||||||
git clone "$REPO_URL_CANDLE" "$CLONE_DIR_CANDLE"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Failed to clone repository. Check Git and Dotnet SDK installation and network connection."
|
|
||||||
fi
|
|
||||||
echo "Repository cloned successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 2: Navigate into the cloned directory ---
|
|
||||||
echo "Changing directory to $CLONE_DIR_CANDLE..."
|
|
||||||
cd "$CLONE_DIR_CANDLE" || handle_error "Failed to change directory to $CLONE_DIR_CANDLE."
|
|
||||||
echo "Current directory: $(pwd)"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 3: Build the FHIR Candle server using Dotnet ---
|
|
||||||
echo "===> Starting Dotnet build (Step 3)..."
|
|
||||||
dotnet publish -c Release -f net9.0 -o publish
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Dotnet build failed. Check Dotnet SDK installation."
|
|
||||||
fi
|
|
||||||
echo "Dotnet build completed successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 4: Navigate back to the parent directory ---
|
|
||||||
echo "===> Changing directory back (Step 4)..."
|
|
||||||
cd .. || handle_error "Failed to change back to the parent directory."
|
|
||||||
echo "Current directory: $(pwd)"
|
|
||||||
echo
|
|
||||||
;;
|
|
||||||
|
|
||||||
*) # APP_MODE is Lite, no SERVER_TYPE
|
|
||||||
echo "Running Lite setup, skipping server build..."
|
|
||||||
if [ -d "$CLONE_DIR_HAPI" ]; then
|
|
||||||
echo "Found existing HAPI directory in Lite mode. Removing it to avoid build issues..."
|
|
||||||
rm -rf "$CLONE_DIR_HAPI"
|
|
||||||
fi
|
|
||||||
if [ -d "$CLONE_DIR_CANDLE" ]; then
|
|
||||||
echo "Found existing Candle directory in Lite mode. Removing it to avoid build issues..."
|
|
||||||
rm -rf "$CLONE_DIR_CANDLE"
|
|
||||||
fi
|
|
||||||
mkdir -p "${CLONE_DIR_HAPI}/target/classes"
|
|
||||||
mkdir -p "${CLONE_DIR_HAPI}/custom"
|
|
||||||
touch "${CLONE_DIR_HAPI}/target/ROOT.war"
|
|
||||||
touch "${CLONE_DIR_HAPI}/target/classes/application.yaml"
|
|
||||||
mkdir -p "${CLONE_DIR_CANDLE}/publish"
|
|
||||||
touch "${CLONE_DIR_CANDLE}/publish/fhir-candle.dll"
|
|
||||||
echo "Placeholder files and directories created for Lite mode build."
|
|
||||||
echo
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# === MODIFIED: Update docker-compose.yml to set APP_MODE and HAPI_FHIR_URL and DOCKERFILE ===
|
|
||||||
echo "Updating docker-compose.yml with APP_MODE=$APP_MODE and HAPI_FHIR_URL..."
|
|
||||||
DOCKER_COMPOSE_TMP="docker-compose.yml.tmp"
|
|
||||||
DOCKER_COMPOSE_ORIG="docker-compose.yml"
|
|
||||||
|
|
||||||
HAPI_URL_TO_USE="https://fhir.hl7.org.au/aucore/fhir/DEFAULT/"
|
|
||||||
if [ -n "$CUSTOM_FHIR_URL_VAL" ]; then
|
|
||||||
HAPI_URL_TO_USE="$CUSTOM_FHIR_URL_VAL"
|
|
||||||
elif [ "$SERVER_TYPE" = "candle" ]; then
|
|
||||||
HAPI_URL_TO_USE="http://localhost:5826/fhir/${CANDLE_FHIR_VERSION}"
|
|
||||||
else
|
|
||||||
HAPI_URL_TO_USE="http://localhost:8080/fhir"
|
|
||||||
fi
|
|
||||||
|
|
||||||
DOCKERFILE_TO_USE="Dockerfile.lite"
|
|
||||||
if [ "$SERVER_TYPE" = "hapi" ]; then
|
|
||||||
DOCKERFILE_TO_USE="Dockerfile.hapi"
|
|
||||||
elif [ "$SERVER_TYPE" = "candle" ]; then
|
|
||||||
DOCKERFILE_TO_USE="Dockerfile.candle"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat << EOF > "$DOCKER_COMPOSE_TMP"
|
|
||||||
version: '3.8'
|
|
||||||
services:
|
|
||||||
fhirflare:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ${DOCKERFILE_TO_USE}
|
|
||||||
ports:
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ "$SERVER_TYPE" = "candle" ]; then
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
- "5000:5000"
|
|
||||||
- "5001:5826"
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
- "5000:5000"
|
|
||||||
- "8080:8080"
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
volumes:
|
|
||||||
- ./instance:/app/instance
|
|
||||||
- ./static/uploads:/app/static/uploads
|
|
||||||
- ./logs:/app/logs
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ "$SERVER_TYPE" = "hapi" ]; then
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
|
||||||
- ./hapi-fhir-jpaserver/target/ROOT.war:/usr/local/tomcat/webapps/ROOT.war
|
|
||||||
- ./hapi-fhir-jpaserver/target/classes/application.yaml:/usr/local/tomcat/conf/application.yaml
|
|
||||||
EOF
|
|
||||||
elif [ "$SERVER_TYPE" = "candle" ]; then
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
- ./fhir-candle/publish/:/app/fhir-candle-publish/
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
environment:
|
|
||||||
- FLASK_APP=app.py
|
|
||||||
- FLASK_ENV=development
|
|
||||||
- NODE_PATH=/usr/lib/node_modules
|
|
||||||
- APP_MODE=${APP_MODE}
|
|
||||||
- APP_BASE_URL=http://localhost:5000
|
|
||||||
- HAPI_FHIR_URL=${HAPI_URL_TO_USE}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ "$SERVER_TYPE" = "candle" ]; then
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
- ASPNETCORE_URLS=http://0.0.0.0:5826
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat << EOF >> "$DOCKER_COMPOSE_TMP"
|
|
||||||
command: supervisord -c /etc/supervisord.conf
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [ ! -f "$DOCKER_COMPOSE_TMP" ]; then
|
|
||||||
handle_error "Failed to create temporary docker-compose file ($DOCKER_COMPOSE_TMP)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Replace the original docker-compose.yml
|
|
||||||
mv "$DOCKER_COMPOSE_TMP" "$DOCKER_COMPOSE_ORIG"
|
|
||||||
echo "docker-compose.yml updated successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 6: Build Docker images ---
|
|
||||||
echo "===> Starting Docker build (Step 6)..."
|
|
||||||
docker-compose build --no-cache
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Docker Compose build failed. Check Docker installation and docker-compose.yml file."
|
|
||||||
fi
|
|
||||||
echo "Docker images built successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
# --- Step 7: Start Docker containers ---
|
|
||||||
echo "===> Starting Docker containers (Step 7)..."
|
|
||||||
docker-compose up -d
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
handle_error "Docker Compose up failed. Check Docker installation and container configurations."
|
|
||||||
fi
|
|
||||||
echo "Docker containers started successfully."
|
|
||||||
echo
|
|
||||||
|
|
||||||
echo "===================================="
|
|
||||||
echo "Script finished successfully! (Mode: $APP_MODE)"
|
|
||||||
echo "===================================="
|
|
||||||
exit 0
|
|
||||||
File diff suppressed because one or more lines are too long
@ -3,12 +3,12 @@ body {
|
|||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply overflow and height constraints only for fire animation page
|
/* Apply overflow and height constraints only for fire animation page */
|
||||||
body.fire-animation-page {
|
body.fire-animation-page {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
/* Fire animation overlay */
|
/* Fire animation overlay */
|
||||||
.fire-on {
|
.fire-on {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@ -34,17 +34,3 @@ stdout_logfile_backups=5
|
|||||||
stderr_logfile=/app/logs/tomcat_err.log
|
stderr_logfile=/app/logs/tomcat_err.log
|
||||||
stderr_logfile_maxbytes=10MB
|
stderr_logfile_maxbytes=10MB
|
||||||
stderr_logfile_backups=5
|
stderr_logfile_backups=5
|
||||||
|
|
||||||
[program:candle]
|
|
||||||
command=dotnet /app/fhir-candle-publish/fhir-candle.dll
|
|
||||||
directory=/app/fhir-candle-publish
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
startsecs=10
|
|
||||||
stopwaitsecs=10
|
|
||||||
stdout_logfile=/app/logs/candle.log
|
|
||||||
stdout_logfile_maxbytes=10MB
|
|
||||||
stdout_logfile_backups=5
|
|
||||||
stderr_logfile=/app/logs/candle_err.log
|
|
||||||
stderr_logfile_maxbytes=10MB
|
|
||||||
stderr_logfile_backups=5
|
|
||||||
|
|||||||
@ -19,11 +19,11 @@
|
|||||||
<p>Whether you're downloading the latest IG versions, checking compliance, converting resources to FHIR Shorthand (FSH), or pushing guides to a test server, this toolkit aims to be an essential companion.</p>
|
<p>Whether you're downloading the latest IG versions, checking compliance, converting resources to FHIR Shorthand (FSH), or pushing guides to a test server, this toolkit aims to be an essential companion.</p>
|
||||||
{% if app_mode == 'lite' %}
|
{% if app_mode == 'lite' %}
|
||||||
<div class="alert alert-info" role="alert">
|
<div class="alert alert-info" role="alert">
|
||||||
<i class="bi bi-info-circle-fill me-2"></i>You are currently running the <strong>Lite Version</strong> of the toolkit. This version excludes the built-in FHIR server depending and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
|
<i class="bi bi-info-circle-fill me-2"></i>You are currently running the <strong>Lite Version</strong> of the toolkit. This version excludes the built-in HAPI FHIR server and relies on external FHIR servers for functionalities like the API Explorer and Operations UI. Validation uses local StructureDefinition checks.
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-success" role="alert">
|
<div class="alert alert-success" role="alert">
|
||||||
<i class="bi bi-check-circle-fill me-2"></i>You are currently running the <strong>Standalone Version</strong> of the toolkit, which includes a built-in FHIR server based on your build choice (accessible via the `/fhir` proxy) for local validation and exploration, if you configured the Server ENV variable this could also be whatever customer server you have pointed the local ENV variable at allowing you to use any fhir server of your choice.
|
<i class="bi bi-check-circle-fill me-2"></i>You are currently running the <strong>Standalone Version</strong> of the toolkit, which includes a built-in HAPI FHIR server (accessible via the `/fhir` proxy) for local validation and exploration.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<li><strong>FHIR Server Interaction:</strong>
|
<li><strong>FHIR Server Interaction:</strong>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Push processed IGs (including dependencies) to a target FHIR server with real-time console feedback.</li>
|
<li>Push processed IGs (including dependencies) to a target FHIR server with real-time console feedback.</li>
|
||||||
<li>Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (GET/POST/PUT/DELETE) and "FHIR UI Operations" pages, supporting both the local server (in Standalone mode) and external servers.</li>
|
<li>Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (GET/POST/PUT/DELETE) and "FHIR UI Operations" pages, supporting both the local HAPI server (in Standalone mode) and external servers.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li><strong>FHIR Shorthand (FSH) Conversion:</strong> Convert FHIR JSON or XML resources to FSH using the integrated GoFSH tool. Offers advanced options like context package selection, various output styles, FHIR version selection, dependency loading, alias file usage, and round-trip validation ("Fishing Trip") with SUSHI. Includes a loading indicator during conversion.</li>
|
<li><strong>FHIR Shorthand (FSH) Conversion:</strong> Convert FHIR JSON or XML resources to FSH using the integrated GoFSH tool. Offers advanced options like context package selection, various output styles, FHIR version selection, dependency loading, alias file usage, and round-trip validation ("Fishing Trip") with SUSHI. Includes a loading indicator during conversion.</li>
|
||||||
@ -48,7 +48,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><strong>Backend:</strong> Python with the Flask web framework and SQLAlchemy for database interaction (SQLite).</li>
|
<li><strong>Backend:</strong> Python with the Flask web framework and SQLAlchemy for database interaction (SQLite).</li>
|
||||||
<li><strong>Frontend:</strong> HTML, Bootstrap 5 for styling, and JavaScript for interactivity and dynamic content loading. Uses Lottie-Web for animations.</li>
|
<li><strong>Frontend:</strong> HTML, Bootstrap 5 for styling, and JavaScript for interactivity and dynamic content loading. Uses Lottie-Web for animations.</li>
|
||||||
<li><strong>FHIR Tooling:</strong> Integrates GoFSH and SUSHI (via Node.js) for FSH conversion and validation. Utilizes the Local FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
|
<li><strong>FHIR Tooling:</strong> Integrates GoFSH and SUSHI (via Node.js) for FSH conversion and validation. Utilizes the HAPI FHIR server (in Standalone mode) for robust FHIR validation and operations.</li>
|
||||||
<li><strong>Deployment:</strong> Runs within a Docker container managed by Docker Compose and Supervisor, ensuring a consistent environment.</li>
|
<li><strong>Deployment:</strong> Runs within a Docker container managed by Docker Compose and Supervisor, ensuring a consistent environment.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" xintegrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css" rel="stylesheet" />
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css" rel="stylesheet" />
|
||||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fire-animation.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/fire-animation.css') }}">
|
||||||
@ -769,9 +769,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {% if request.path == '/search-and-import' %}active{% endif %}" href="{{ url_for('search_and_import') }}"><i class="fas fa-download me-1"></i> Search and Import</a>
|
<a class="nav-link {% if request.path == '/search-and-import' %}active{% endif %}" href="{{ url_for('search_and_import') }}"><i class="fas fa-download me-1"></i> Search and Import</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<!-- <li class="nav-item">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'manual_import_ig' else '' }}" href="{{ url_for('manual_import_ig') }}"><i class="fas fa-folder-open me-1"></i> Manual Import</a>
|
<a class="nav-link {{ 'active' if request.endpoint == 'import_ig' else '' }}" href="{{ url_for('import_ig') }}"><i class="fas fa-download me-1"></i> Import IGs</a>
|
||||||
</li>
|
</li> -->
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="fas fa-folder-open me-1"></i> Manage FHIR Packages</a>
|
<a class="nav-link {{ 'active' if request.endpoint == 'view_igs' else '' }}" href="{{ url_for('view_igs') }}"><i class="fas fa-folder-open me-1"></i> Manage FHIR Packages</a>
|
||||||
</li>
|
</li>
|
||||||
@ -796,14 +796,11 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
|
<a class="nav-link {{ 'active' if request.endpoint == 'fsh_converter' else '' }}" href="{{ url_for('fsh_converter') }}"><i class="fas fa-file-code me-1"></i> FSH Converter</a>
|
||||||
</li>
|
</li>
|
||||||
<!-- <li class="nav-item">
|
{% if app_mode != 'lite' %}
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'ig_configurator' else '' }}" href="{{ url_for('ig_configurator') }}"><i class="fas fa-wrench me-1"></i> IG Valid-Conf Gen</a>
|
|
||||||
</li> -->
|
|
||||||
<!-- {% if app_mode != 'lite' %}
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'config_hapi' else '' }}" href="{{ url_for('config_hapi') }}"><i class="fas fa-cog me-1"></i> Configure HAPI</a>
|
<a class="nav-link {{ 'active' if request.endpoint == 'config_hapi' else '' }}" href="{{ url_for('config_hapi') }}"><i class="fas fa-cog me-1"></i> Configure HAPI</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %} -->
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="navbar-controls d-flex align-items-center">
|
<div class="navbar-controls d-flex align-items-center">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
@ -820,6 +817,7 @@
|
|||||||
|
|
||||||
<main class="flex-grow-1">
|
<main class="flex-grow-1">
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
|
<!-- Flashed Messages Section -->
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
@ -854,7 +852,6 @@
|
|||||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/issues/new/choose" class="text-danger text-decoration-none" aria-label="FHIRFLARE support"><i class="fas fa-exclamation-circle me-1"></i> Raise an Issue</a>
|
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/issues/new/choose" class="text-danger text-decoration-none" aria-label="FHIRFLARE support"><i class="fas fa-exclamation-circle me-1"></i> Raise an Issue</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-right">
|
<div class="footer-right">
|
||||||
<a class="nav-link {{ 'active' if request.endpoint == 'flasgger.apidocs' else '' }}" href="{{ url_for('flasgger.apidocs') }}" aria-label="API Documentation"><i class="fas fa-book-open me-1"></i> API Docs</a>
|
|
||||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/discussions" target="_blank" rel="noreferrer" aria-label="Project Discussion">Project Discussions</a>
|
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/discussions" target="_blank" rel="noreferrer" aria-label="Project Discussion">Project Discussions</a>
|
||||||
<a href="https://github.com/Sudo-JHare" aria-label="Developer">Developer</a>
|
<a href="https://github.com/Sudo-JHare" aria-label="Developer">Developer</a>
|
||||||
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/blob/main/LICENSE.md" aria-label="License">License</a>
|
<a href="https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit/blob/main/LICENSE.md" aria-label="License">License</a>
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
<!-----------------------------------------------------------------remove the buttons-----------------------------------------------------
|
||||||
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
<div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
|
||||||
<a href="{{ url_for('search_and_import') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
|
<a href="{{ url_for('import_ig') }}" class="btn btn-primary btn-lg px-4 gap-3">Import IGs</a>
|
||||||
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</a>
|
<a href="{{ url_for('view_igs') }}" class="btn btn-outline-secondary btn-lg px-4 disabled">Manage FHIR Packages</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 IG's</a>
|
||||||
</div>
|
</div>
|
||||||
@ -34,13 +34,13 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
|
<h2><i class="bi bi-journal-arrow-down me-2"></i>Manage FHIR Packages</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('search_and_import') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
|
<a href="{{ url_for('import_ig') }}" class="btn btn-success"><i class="bi bi-download me-1"></i> Import More IGs</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- LEFT: Downloaded Packages -->
|
<!-- LEFT: Downloaded Packages -->
|
||||||
<div class="col-md-5">
|
<div class="col-md-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
|
<div class="card-header"><i class="bi bi-folder-symlink me-1"></i> Downloaded Packages ({{ packages|length }})</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -65,20 +65,20 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>{{ pkg.version }}</td>
|
<td>{{ pkg.version }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group-vertical btn-group-sm w-80">
|
<div class="btn-group btn-group-sm">
|
||||||
{% if is_processed %}
|
{% if is_processed %}
|
||||||
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
<span class="btn btn-success disabled"><i class="bi bi-check-lg"></i> Processed</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('process_ig') }}" style="display:inline;">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||||
<button type="submit" class="btn btn-outline-primary w-80"><i class="bi bi-gear"></i> Process</button>
|
<button type="submit" class="btn btn-outline-primary"><i class="bi bi-gear"></i> Process</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('delete_ig') }}" style="display:inline;">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
<input type="hidden" name="filename" value="{{ pkg.filename }}">
|
||||||
<button type="submit" class="btn btn-outline-danger w-80" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
<button type="submit" class="btn btn-outline-danger" onclick="return confirm('Are you sure you want to delete {{ pkg.name }}#{{ pkg.version }}?');"><i class="bi bi-trash"></i> Delete</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -105,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT: Processed Packages -->
|
<!-- RIGHT: Processed Packages -->
|
||||||
<div class="col-md-7">
|
<div class="col-md-6">
|
||||||
<div class="card h-100">
|
<div class="card h-100">
|
||||||
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
<div class="card-header"><i class="bi bi-check-circle me-1"></i> Processed Packages ({{ processed_list|length }})</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@ -139,12 +139,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group-vertical btn-group-sm w-80">
|
<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-80" title="View Details"><i class="bi bi-search"></i> View</a>
|
<a href="{{ url_for('view_ig', processed_ig_id=processed_ig.id) }}" class="btn btn-outline-info w-100" title="View Details"><i class="bi bi-search"></i> View</a>
|
||||||
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
<form method="POST" action="{{ url_for('unload_ig') }}" style="display:inline;">
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }}
|
||||||
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
<input type="hidden" name="ig_id" value="{{ processed_ig.id }}">
|
||||||
<button type="submit" class="btn btn-outline-warning w-80 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
<button type="submit" class="btn btn-outline-warning w-100 mt-1" onclick="return confirm('Are you sure you want to unload {{ processed_ig.package_name }}#{{ processed_ig.version }}?');"><i class="bi bi-x-lg"></i> Unload</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{# Import form helpers for CSRF token and field rendering #}
|
{# Import form helpers if needed, e.g., for CSRF token #}
|
||||||
{% from "_form_helpers.html" import render_field %}
|
{% from "_form_helpers.html" import render_field %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -48,10 +48,11 @@
|
|||||||
<p class="text-muted">No packages downloaded yet. Use the "Import IG" tab.</p>
|
<p class="text-muted">No packages downloaded yet. Use the "Import IG" tab.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Push Response Area #}
|
{# --- MOVED: Push Response Area --- #}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h4><i class="bi bi-file-earmark-text me-2"></i>Push Report</h4>
|
<h4><i class="bi bi-file-earmark-text me-2"></i>Push Report</h4>
|
||||||
|
{# --- NEW: Report Action Buttons --- #}
|
||||||
<div id="reportActions" style="display: none;">
|
<div id="reportActions" style="display: none;">
|
||||||
<button id="copyReportBtn" class="btn btn-sm btn-outline-secondary me-2" title="Copy Report Text">
|
<button id="copyReportBtn" class="btn btn-sm btn-outline-secondary me-2" title="Copy Report Text">
|
||||||
<i class="bi bi-clipboard"></i> Copy
|
<i class="bi bi-clipboard"></i> Copy
|
||||||
@ -60,9 +61,11 @@
|
|||||||
<i class="bi bi-download"></i> Download
|
<i class="bi bi-download"></i> Download
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{# --- END NEW --- #}
|
||||||
</div>
|
</div>
|
||||||
<div id="pushResponse" class="border p-3 rounded bg-light" style="min-height: 100px;">
|
<div id="pushResponse" class="border p-3 rounded bg-light" style="min-height: 100px;">
|
||||||
<span class="text-muted">Report summary will appear here after pushing...</span>
|
<span class="text-muted">Report summary will appear here after pushing...</span>
|
||||||
|
{# Flash messages can still appear here if needed #}
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
@ -75,13 +78,15 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# --- END MOVED --- #}
|
||||||
|
|
||||||
</div>{# End Left Column #}
|
</div>{# End Left Column #}
|
||||||
|
|
||||||
{# Right Column: Push IGs Form and Console #}
|
{# Right Column: Push IGs Form and Console #}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2><i class="bi bi-upload me-2"></i>Push IGs to FHIR Server</h2>
|
<h2><i class="bi bi-upload me-2"></i>Push IGs to FHIR Server</h2>
|
||||||
<form id="pushIgForm">
|
<form id="pushIgForm">
|
||||||
{{ form.csrf_token if form else '' }}
|
{{ form.csrf_token if form else '' }} {# Use form passed from route #}
|
||||||
|
|
||||||
{# Package Selection #}
|
{# Package Selection #}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -106,31 +111,21 @@
|
|||||||
<input type="url" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., http://localhost:8080/fhir" required>
|
<input type="url" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., http://localhost:8080/fhir" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Authentication Section #}
|
{# --- RESTRUCTURED: Auth and Checkboxes --- #}
|
||||||
<div class="row g-3 mb-3 align-items-end">
|
<div class="row g-3 mb-3 align-items-end">
|
||||||
|
{# Authentication Dropdown & Token Input #}
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<label for="authType" class="form-label">Authentication</label>
|
<label for="authType" class="form-label">Authentication</label>
|
||||||
<select class="form-select" id="authType" name="auth_type">
|
<select class="form-select" id="authType" name="auth_type">
|
||||||
<option value="none" selected>None</option>
|
<option value="none" selected>None</option>
|
||||||
<option value="apiKey">Toolkit API Key (Internal)</option>
|
<option value="apiKey">Toolkit API Key (Internal)</option>
|
||||||
<option value="bearerToken">Bearer Token</option>
|
<option value="bearerToken">Bearer Token</option>
|
||||||
<option value="basic">Basic Authentication</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7" id="authInputsGroup" style="display: none;">
|
<div class="col-md-7" id="authTokenGroup" style="display: none;">
|
||||||
{# Bearer Token Input #}
|
|
||||||
<div id="bearerTokenInput" style="display: none;">
|
|
||||||
<label for="authToken" class="form-label">Bearer Token</label>
|
<label for="authToken" class="form-label">Bearer Token</label>
|
||||||
<input type="password" class="form-control" id="authToken" name="auth_token" placeholder="Enter Bearer Token">
|
<input type="password" class="form-control" id="authToken" name="auth_token" placeholder="Enter Bearer Token">
|
||||||
</div>
|
</div>
|
||||||
{# Basic Auth Inputs #}
|
|
||||||
<div id="basicAuthInputs" style="display: none;">
|
|
||||||
<label for="username" class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control mb-2" id="username" name="username" placeholder="Enter Basic Auth Username">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Basic Auth Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Checkboxes Row #}
|
{# Checkboxes Row #}
|
||||||
@ -159,10 +154,12 @@
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="verbose" name="verbose">
|
<input type="checkbox" class="form-check-input" id="verbose" name="verbose">
|
||||||
<label class="form-check-label" for="verbose">Verbose Log</label>
|
<label class="form-check-label" for="verbose">Verbose Log</label>
|
||||||
<small class="form-text text-muted d-block">Show detailed log.</small>
|
<small class="form-text text-muted d-block">Show detailed Log.</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# --- END RESTRUCTURED --- #}
|
||||||
|
|
||||||
|
|
||||||
{# Resource Type Filter #}
|
{# Resource Type Filter #}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@ -188,6 +185,7 @@
|
|||||||
<span class="text-muted">Console output will appear here...</span>
|
<span class="text-muted">Console output will appear here...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div> {# End Right Column #}
|
</div> {# End Right Column #}
|
||||||
</div> {# End row #}
|
</div> {# End row #}
|
||||||
</div> {# End container-fluid #}
|
</div> {# End container-fluid #}
|
||||||
@ -200,17 +198,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const pushIgForm = document.getElementById('pushIgForm');
|
const pushIgForm = document.getElementById('pushIgForm');
|
||||||
const pushButton = document.getElementById('pushButton');
|
const pushButton = document.getElementById('pushButton');
|
||||||
const liveConsole = document.getElementById('liveConsole');
|
const liveConsole = document.getElementById('liveConsole');
|
||||||
const responseDiv = document.getElementById('pushResponse');
|
const responseDiv = document.getElementById('pushResponse'); // Area for final report
|
||||||
const reportActions = document.getElementById('reportActions');
|
const reportActions = document.getElementById('reportActions'); // Container for report buttons
|
||||||
const copyReportBtn = document.getElementById('copyReportBtn');
|
const copyReportBtn = document.getElementById('copyReportBtn'); // New copy button
|
||||||
const downloadReportBtn = document.getElementById('downloadReportBtn');
|
const downloadReportBtn = document.getElementById('downloadReportBtn'); // New download button
|
||||||
const authTypeSelect = document.getElementById('authType');
|
const authTypeSelect = document.getElementById('authType');
|
||||||
const authInputsGroup = document.getElementById('authInputsGroup');
|
const authTokenGroup = document.getElementById('authTokenGroup');
|
||||||
const bearerTokenInput = document.getElementById('bearerTokenInput');
|
|
||||||
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
|
||||||
const authTokenInput = document.getElementById('authToken');
|
const authTokenInput = document.getElementById('authToken');
|
||||||
const usernameInput = document.getElementById('username');
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
const resourceTypesFilterInput = document.getElementById('resourceTypesFilter');
|
const resourceTypesFilterInput = document.getElementById('resourceTypesFilter');
|
||||||
const skipFilesFilterInput = document.getElementById('skipFilesFilter');
|
const skipFilesFilterInput = document.getElementById('skipFilesFilter');
|
||||||
const dryRunCheckbox = document.getElementById('dryRun');
|
const dryRunCheckbox = document.getElementById('dryRun');
|
||||||
@ -232,7 +226,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (packageSelect && dependencyModeField) {
|
if (packageSelect && dependencyModeField) {
|
||||||
packageSelect.addEventListener('change', function() {
|
packageSelect.addEventListener('change', function() {
|
||||||
const packageId = this.value;
|
const packageId = this.value;
|
||||||
dependencyModeField.value = '';
|
dependencyModeField.value = ''; // Clear on change
|
||||||
if (packageId) {
|
if (packageId) {
|
||||||
const [packageName, version] = packageId.split('#');
|
const [packageName, version] = packageId.split('#');
|
||||||
fetch(`/get-package-metadata?package_name=${packageName}&version=${version}`)
|
fetch(`/get-package-metadata?package_name=${packageName}&version=${version}`)
|
||||||
@ -246,32 +240,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (packageSelect.value) { packageSelect.dispatchEvent(new Event('change')); }
|
if (packageSelect.value) { packageSelect.dispatchEvent(new Event('change')); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/Hide Auth Inputs
|
// Show/Hide Bearer Token Input
|
||||||
if (authTypeSelect && authInputsGroup && bearerTokenInput && basicAuthInputs) {
|
if (authTypeSelect && authTokenGroup) {
|
||||||
authTypeSelect.addEventListener('change', function() {
|
authTypeSelect.addEventListener('change', function() {
|
||||||
authInputsGroup.style.display = (this.value === 'bearerToken' || this.value === 'basic') ? 'block' : 'none';
|
authTokenGroup.style.display = this.value === 'bearerToken' ? 'block' : 'none';
|
||||||
bearerTokenInput.style.display = this.value === 'bearerToken' ? 'block' : 'none';
|
|
||||||
basicAuthInputs.style.display = this.value === 'basic' ? 'block' : 'none';
|
|
||||||
// Clear inputs when switching
|
|
||||||
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
|
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
|
||||||
if (this.value !== 'basic' && usernameInput) usernameInput.value = '';
|
|
||||||
if (this.value !== 'basic' && passwordInput) passwordInput.value = '';
|
|
||||||
});
|
});
|
||||||
authInputsGroup.style.display = (authTypeSelect.value === 'bearerToken' || authTypeSelect.value === 'basic') ? 'block' : 'none';
|
authTokenGroup.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
|
||||||
bearerTokenInput.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
|
|
||||||
basicAuthInputs.style.display = authTypeSelect.value === 'basic' ? 'block' : 'none';
|
|
||||||
} else {
|
|
||||||
console.error("Auth elements not found.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Report Action Button Listeners
|
// --- NEW: Report Action Button Listeners ---
|
||||||
if (copyReportBtn && responseDiv) {
|
if (copyReportBtn && responseDiv) {
|
||||||
copyReportBtn.addEventListener('click', () => {
|
copyReportBtn.addEventListener('click', () => {
|
||||||
const reportAlert = responseDiv.querySelector('.alert');
|
const reportAlert = responseDiv.querySelector('.alert'); // Get the alert div inside
|
||||||
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : '';
|
const reportText = reportAlert ? reportAlert.innerText || reportAlert.textContent : ''; // Get text content
|
||||||
if (reportText && navigator.clipboard) {
|
if (reportText && navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(reportText)
|
navigator.clipboard.writeText(reportText)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
// Optional: Provide feedback (e.g., change button text/icon)
|
||||||
const originalIcon = copyReportBtn.innerHTML;
|
const originalIcon = copyReportBtn.innerHTML;
|
||||||
copyReportBtn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
copyReportBtn.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
||||||
setTimeout(() => { copyReportBtn.innerHTML = originalIcon; }, 2000);
|
setTimeout(() => { copyReportBtn.innerHTML = originalIcon; }, 2000);
|
||||||
@ -306,27 +292,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url); // Clean up
|
||||||
} else {
|
} else {
|
||||||
alert('No report content found to download.');
|
alert('No report content found to download.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// --- END NEW ---
|
||||||
|
|
||||||
// Form Submission
|
|
||||||
|
// --- Form Submission ---
|
||||||
if (pushIgForm) {
|
if (pushIgForm) {
|
||||||
pushIgForm.addEventListener('submit', async function(event) {
|
pushIgForm.addEventListener('submit', async function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Get form values
|
// Get form values (with null checks for elements)
|
||||||
const packageId = packageSelect ? packageSelect.value : null;
|
const packageId = packageSelect ? packageSelect.value : null;
|
||||||
const fhirServerUrl = fhirServerUrlInput ? fhirServerUrlInput.value.trim() : null;
|
const fhirServerUrl = fhirServerUrlInput ? fhirServerUrlInput.value.trim() : null;
|
||||||
if (!packageId || !fhirServerUrl) { alert('Please select package and enter FHIR Server URL.'); return; }
|
if (!packageId || !fhirServerUrl) { alert('Please select package and enter FHIR Server URL.'); return; }
|
||||||
const [packageName, version] = packageId.split('#');
|
const [packageName, version] = packageId.split('#');
|
||||||
const auth_type = authTypeSelect ? authTypeSelect.value : 'none';
|
const auth_type = authTypeSelect ? authTypeSelect.value : 'none';
|
||||||
const auth_token = (auth_type === 'bearerToken' && authTokenInput) ? authTokenInput.value : null;
|
const auth_token = (auth_type === 'bearerToken' && authTokenInput) ? authTokenInput.value : null;
|
||||||
const username = (auth_type === 'basic' && usernameInput) ? usernameInput.value.trim() : null;
|
|
||||||
const password = (auth_type === 'basic' && passwordInput) ? passwordInput.value : null;
|
|
||||||
const resource_types_filter_raw = resourceTypesFilterInput ? resourceTypesFilterInput.value.trim() : '';
|
const resource_types_filter_raw = resourceTypesFilterInput ? resourceTypesFilterInput.value.trim() : '';
|
||||||
const resource_types_filter = resource_types_filter_raw ? resource_types_filter_raw.split(',').map(s => s.trim()).filter(s => s) : null;
|
const resource_types_filter = resource_types_filter_raw ? resource_types_filter_raw.split(',').map(s => s.trim()).filter(s => s) : null;
|
||||||
const skip_files_raw = skipFilesFilterInput ? skipFilesFilterInput.value.trim() : '';
|
const skip_files_raw = skipFilesFilterInput ? skipFilesFilterInput.value.trim() : '';
|
||||||
@ -336,23 +322,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const include_dependencies = includeDependenciesCheckbox ? includeDependenciesCheckbox.checked : true;
|
const include_dependencies = includeDependenciesCheckbox ? includeDependenciesCheckbox.checked : true;
|
||||||
const force_upload = forceUploadCheckbox ? forceUploadCheckbox.checked : false;
|
const force_upload = forceUploadCheckbox ? forceUploadCheckbox.checked : false;
|
||||||
|
|
||||||
// Validate Basic Auth inputs
|
// UI Updates & API Key
|
||||||
if (auth_type === 'basic') {
|
if (pushButton) { pushButton.disabled = true; pushButton.textContent = 'Processing...'; }
|
||||||
if (!username) { alert('Please enter a username for Basic Authentication.'); return; }
|
if (liveConsole) { liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting ${dry_run ? 'DRY RUN ' : ''}${force_upload ? 'FORCE ' : ''}push for ${packageName}#${version}...</div>`; }
|
||||||
if (!password) { alert('Please enter a password for Basic Authentication.'); return; }
|
if (responseDiv) { responseDiv.innerHTML = '<span class="text-muted">Processing...</span>'; } // Clear previous report
|
||||||
}
|
if (reportActions) { reportActions.style.display = 'none'; } // Hide report buttons initially
|
||||||
|
const internalApiKey = {{ api_key | default("") | tojson }}; // Use tojson filter
|
||||||
// UI Updates
|
|
||||||
if (pushButton) {
|
|
||||||
pushButton.disabled = true;
|
|
||||||
pushButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Processing...';
|
|
||||||
}
|
|
||||||
if (liveConsole) {
|
|
||||||
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting ${dry_run ? 'DRY RUN ' : ''}${force_upload ? 'FORCE ' : ''}push for ${packageName}#${version}...</div>`;
|
|
||||||
}
|
|
||||||
if (responseDiv) { responseDiv.innerHTML = '<span class="text-muted">Processing...</span>'; }
|
|
||||||
if (reportActions) { reportActions.style.display = 'none'; }
|
|
||||||
const internalApiKey = {{ api_key | default("") | tojson }};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// API Fetch
|
// API Fetch
|
||||||
@ -360,19 +335,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
package_name: packageName,
|
package_name: packageName, version: version, fhir_server_url: fhirServerUrl,
|
||||||
version: version,
|
include_dependencies: include_dependencies, auth_type: auth_type, auth_token: auth_token,
|
||||||
fhir_server_url: fhirServerUrl,
|
resource_types_filter: resource_types_filter, skip_files: skip_files,
|
||||||
include_dependencies: include_dependencies,
|
dry_run: dry_run, verbose: isVerboseChecked, force_upload: force_upload
|
||||||
auth_type: auth_type,
|
|
||||||
auth_token: auth_token,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
resource_types_filter: resource_types_filter,
|
|
||||||
skip_files: skip_files,
|
|
||||||
dry_run: dry_run,
|
|
||||||
verbose: isVerboseChecked,
|
|
||||||
force_upload: force_upload
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -395,28 +361,25 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
|
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
|
||||||
let messageClass = 'text-light'; let prefix = '[INFO]'; let shouldDisplay = false;
|
let messageClass = 'text-light'; let prefix = '[INFO]'; let shouldDisplay = false;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) { // Determine if message should display based on verbose
|
||||||
case 'start': case 'error': case 'complete': shouldDisplay = true; break;
|
case 'start': case 'error': case 'complete': shouldDisplay = true; break;
|
||||||
case 'success': case 'warning': case 'info': case 'progress': if (isVerboseChecked) { shouldDisplay = true; } break;
|
case 'success': case 'warning': case 'info': case 'progress': if (isVerboseChecked) { shouldDisplay = true; } break;
|
||||||
default: if (isVerboseChecked) { shouldDisplay = true; console.warn("Unknown type:", data.type); prefix = '[UNKNOWN]'; } break;
|
default: if (isVerboseChecked) { shouldDisplay = true; console.warn("Unknown type:", data.type); prefix = '[UNKNOWN]'; } break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDisplay && liveConsole) {
|
if (shouldDisplay && liveConsole) { // Set prefix/class and append to console
|
||||||
if(data.type==='error'){prefix='[ERROR]';messageClass='text-danger';}
|
if(data.type==='error'){prefix='[ERROR]';messageClass='text-danger';}
|
||||||
else if(data.type==='complete'){const s=data.data?.status||'info';if(s==='success'){prefix='[SUCCESS]';messageClass='text-success';}else if(s==='partial'){prefix='[PARTIAL]';messageClass='text-warning';}else{prefix='[ERROR]';messageClass='text-danger';}}
|
else if(data.type==='complete'){const s=data.data?.status||'info';if(s==='success'){prefix='[SUCCESS]';messageClass='text-success';}else if(s==='partial'){prefix='[PARTIAL]';messageClass='text-warning';}else{prefix='[ERROR]';messageClass='text-danger';}}
|
||||||
else if(data.type==='start'){prefix='[START]';messageClass='text-info';}
|
else if(data.type==='start'){prefix='[START]';messageClass='text-info';}
|
||||||
else if(data.type==='success'){prefix='[SUCCESS]';messageClass='text-success';}
|
else if(data.type==='success'){prefix='[SUCCESS]';messageClass='text-success';}else if(data.type==='warning'){prefix='[WARNING]';messageClass='text-warning';}else if(data.type==='info'){prefix='[INFO]';messageClass='text-info';}else{prefix='[PROGRESS]';messageClass='text-light';}
|
||||||
else if(data.type==='warning'){prefix='[WARNING]';messageClass='text-warning';}
|
|
||||||
else if(data.type==='info'){prefix='[INFO]';messageClass='text-info';}
|
|
||||||
else{prefix='[PROGRESS]';messageClass='text-light';}
|
|
||||||
|
|
||||||
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
|
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
|
||||||
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
|
const messageText = (data.type === 'complete' && data.data) ? data.data.message : data.message;
|
||||||
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(messageText) || 'Empty message.'}`;
|
messageDiv.textContent = `${timestamp} ${prefix} ${sanitizeText(messageText) || 'Empty message.'}`;
|
||||||
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
|
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.type === 'complete' && responseDiv) {
|
if (data.type === 'complete' && responseDiv) { // Update final summary box
|
||||||
const summaryData = data.data || {}; let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
|
const summaryData = data.data || {}; let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
|
||||||
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
|
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
|
||||||
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
|
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
|
||||||
@ -428,74 +391,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
|
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
|
||||||
|
|
||||||
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
|
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
|
||||||
if (reportActions) { reportActions.style.display = 'block'; }
|
if (reportActions) { reportActions.style.display = 'block'; } // Show report buttons
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('Stream parse error:', parseError);
|
|
||||||
if (liveConsole) {
|
|
||||||
const errDiv = document.createElement('div');
|
|
||||||
errDiv.className = 'text-danger';
|
|
||||||
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] Stream parse error: ${sanitizeText(parseError.message)}`;
|
|
||||||
liveConsole.appendChild(errDiv);
|
|
||||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (parseError) { /* (Handle JSON parse errors) */ console.error('Stream parse error:', parseError); if(liveConsole){/*...add error to console...*/} }
|
||||||
|
} // end for loop
|
||||||
|
} // end while loop
|
||||||
|
|
||||||
// Process Final Buffer
|
// Process Final Buffer (if any)
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
try {
|
try { /* (Parsing logic for final buffer, similar to above) */ }
|
||||||
const data = JSON.parse(buffer.trim());
|
catch (parseError) { /* (Handle final buffer parse error) */ }
|
||||||
if (data.type === 'complete' && responseDiv) {
|
|
||||||
// Same summary rendering as above
|
|
||||||
const summaryData = data.data || {};
|
|
||||||
let alertClass = 'alert-info'; let statusText = 'Info'; let pushedPkgs = 'None'; let failHtml = ''; let skipHtml = '';
|
|
||||||
const isDryRun = summaryData.dry_run || false; const isForceUpload = summaryData.force_upload || false;
|
|
||||||
const typeFilterUsed = summaryData.resource_types_filter ? summaryData.resource_types_filter.join(', ') : 'All';
|
|
||||||
const fileFilterUsed = summaryData.skip_files_filter ? summaryData.skip_files_filter.join(', ') : 'None';
|
|
||||||
|
|
||||||
if (summaryData.pushed_packages_summary?.length > 0) { pushedPkgs = summaryData.pushed_packages_summary.map(p => `${sanitizeText(p.id)} (${sanitizeText(p.resource_count)} resources)`).join('<br>'); }
|
|
||||||
if (summaryData.status === 'success') { alertClass = 'alert-success'; statusText = 'Success';} else if (summaryData.status === 'partial') { alertClass = 'alert-warning'; statusText = 'Partial Success'; } else { alertClass = 'alert-danger'; statusText = 'Error'; }
|
|
||||||
if (summaryData.failed_details?.length > 0) { failHtml = '<hr><strong>Failures:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.failed_details.forEach(f => {failHtml += `<li><strong>${sanitizeText(f.resource)}:</strong> ${sanitizeText(f.error)}</li>`;}); failHtml += '</ul>';}
|
|
||||||
if (summaryData.skipped_details?.length > 0) { skipHtml = '<hr><strong>Skipped:</strong><ul class="list-unstyled" style="font-size: 0.9em; max-height: 150px; overflow-y: auto; padding-left: 1em;">'; summaryData.skipped_details.forEach(s => {skipHtml += `<li><strong>${sanitizeText(s.resource)}:</strong> ${sanitizeText(s.reason)}</li>`;}); skipHtml += '</ul>';}
|
|
||||||
|
|
||||||
responseDiv.innerHTML = `<div class="alert ${alertClass} mt-0"><strong>${isDryRun?'[DRY RUN] ':''}${isForceUpload?'[FORCE] ':''}${statusText}:</strong> ${sanitizeText(summaryData.message)||'Complete.'}<hr><strong>Target:</strong> ${sanitizeText(summaryData.target_server)}<br><strong>Package:</strong> ${sanitizeText(summaryData.package_name)}#${sanitizeText(summaryData.version)}<br><strong>Config:</strong> Deps=${summaryData.included_dependencies?'Yes':'No'}, Types=${sanitizeText(typeFilterUsed)}, SkipFiles=${sanitizeText(fileFilterUsed)}, DryRun=${isDryRun?'Yes':'No'}, Force=${isForceUpload?'Yes':'No'}, Verbose=${isVerboseChecked?'Yes':'No'}<br><strong>Stats:</strong> Attempt=${sanitizeText(summaryData.resources_attempted)}, Success=${sanitizeText(summaryData.success_count)}, Fail=${sanitizeText(summaryData.failure_count)}, Skip=${sanitizeText(summaryData.skipped_count)}<br><strong>Pushed Pkgs:</strong><br><div style="padding-left:15px;">${pushedPkgs}</div>${failHtml}${skipHtml}</div>`;
|
|
||||||
if (reportActions) { reportActions.style.display = 'block'; }
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
console.error('Final buffer parse error:', parseError);
|
|
||||||
if (liveConsole) {
|
|
||||||
const errDiv = document.createElement('div');
|
|
||||||
errDiv.className = 'text-danger';
|
|
||||||
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] Final buffer parse error: ${sanitizeText(parseError.message)}`;
|
|
||||||
liveConsole.appendChild(errDiv);
|
|
||||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) { // Handle overall fetch/network errors
|
||||||
console.error("Push operation failed:", error);
|
console.error("Push operation failed:", error);
|
||||||
if (liveConsole) {
|
if (liveConsole) { /* ... add error to console ... */ }
|
||||||
const errDiv = document.createElement('div');
|
if (responseDiv) { responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message || error)}</div>`; }
|
||||||
errDiv.className = 'text-danger';
|
if (reportActions) { reportActions.style.display = 'none'; } // Hide buttons on error
|
||||||
errDiv.textContent = `${new Date().toLocaleTimeString()} [ERROR] ${sanitizeText(error.message || error)}`;
|
} finally { // Re-enable button
|
||||||
liveConsole.appendChild(errDiv);
|
if (pushButton) { pushButton.disabled = false; pushButton.textContent = 'Push to FHIR Server'; }
|
||||||
liveConsole.scrollTop = liveConsole.scrollHeight;
|
|
||||||
}
|
}
|
||||||
if (responseDiv) {
|
}); // End form submit listener
|
||||||
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message || error)}</div>`;
|
|
||||||
}
|
|
||||||
if (reportActions) { reportActions.style.display = 'none'; }
|
|
||||||
} finally {
|
|
||||||
if (pushButton) {
|
|
||||||
pushButton.disabled = false;
|
|
||||||
pushButton.innerHTML = '<i class="bi bi-cloud-upload me-2"></i>Push to FHIR Server';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else { console.error("Push IG Form element not found."); }
|
} else { console.error("Push IG Form element not found."); }
|
||||||
});
|
}); // End DOMContentLoaded listener
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -8,6 +8,13 @@
|
|||||||
<p class="lead mb-4">
|
<p class="lead mb-4">
|
||||||
Interact with FHIR servers using GET, POST, PUT, or DELETE requests. Toggle between local HAPI or a custom server to explore resources or perform searches.
|
Interact with FHIR servers using GET, POST, PUT, or DELETE requests. Toggle between local HAPI or a custom server to explore resources or perform searches.
|
||||||
</p>
|
</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('validate_sample') }}" class="btn btn-outline-secondary btn-lg px-4">Validate FHIR Sample</a>
|
||||||
|
<a href="{{ url_for('fhir_ui_operations') }}" class="btn btn-outline-secondary btn-lg px-4">FHIR UI Operations</a>
|
||||||
|
</div>
|
||||||
|
-------------------------------------------------------------------------------------------------------------------------------------------->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -21,37 +28,12 @@
|
|||||||
<label class="form-label">FHIR Server</label>
|
<label class="form-label">FHIR Server</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
||||||
<span id="toggleLabel">Use Local Server</span>
|
<span id="toggleLabel">Use Local HAPI</span>
|
||||||
</button>
|
</button>
|
||||||
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
|
<input type="text" class="form-control" id="fhirServerUrl" name="fhir_server_url" placeholder="e.g., https://hapi.fhir.org/baseR4" style="display: none;" aria-describedby="fhirServerHelp">
|
||||||
</div>
|
</div>
|
||||||
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (http://localhost:8080/fhir) or enter a custom FHIR server URL.</small>
|
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (http://localhost:8080/fhir) or enter a custom FHIR server URL.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3" id="authSection" style="display: none;">
|
|
||||||
<label class="form-label">Authentication</label>
|
|
||||||
<div class="row g-3 align-items-end">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<select class="form-select" id="authType" name="auth_type">
|
|
||||||
<option value="none" selected>None</option>
|
|
||||||
<option value="bearer">Bearer Token</option>
|
|
||||||
<option value="basic">Basic Authentication</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7" id="authInputsGroup" style="display: none;">
|
|
||||||
<div id="bearerTokenInput" style="display: none;">
|
|
||||||
<label for="bearerToken" class="form-label">Bearer Token</label>
|
|
||||||
<input type="password" class="form-control" id="bearerToken" name="bearer_token" placeholder="Enter Bearer Token">
|
|
||||||
</div>
|
|
||||||
<div id="basicAuthInputs" style="display: none;">
|
|
||||||
<label for="username" class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control mb-2" id="username" name="username" placeholder="Enter Username">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="fhirPath" class="form-label">FHIR Path</label>
|
<label for="fhirPath" class="form-label">FHIR Path</label>
|
||||||
<input type="text" class="form-control" id="fhirPath" name="fhir_path" placeholder="e.g., Patient/wang-li" required aria-describedby="fhirPathHelp">
|
<input type="text" class="form-control" id="fhirPath" name="fhir_path" placeholder="e.g., Patient/wang-li" required aria-describedby="fhirPathHelp">
|
||||||
@ -62,10 +44,13 @@
|
|||||||
<div class="d-flex gap-2 flex-wrap">
|
<div class="d-flex gap-2 flex-wrap">
|
||||||
<input type="radio" class="btn-check" name="method" id="get" value="GET" checked>
|
<input type="radio" class="btn-check" name="method" id="get" value="GET" checked>
|
||||||
<label class="btn btn-outline-success" for="get"><span class="badge bg-success">GET</span></label>
|
<label class="btn btn-outline-success" for="get"><span class="badge bg-success">GET</span></label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" name="method" id="post" value="POST">
|
<input type="radio" class="btn-check" name="method" id="post" value="POST">
|
||||||
<label class="btn btn-outline-primary" for="post"><span class="badge bg-primary">POST</span></label>
|
<label class="btn btn-outline-primary" for="post"><span class="badge bg-primary">POST</span></label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" name="method" id="put" value="PUT">
|
<input type="radio" class="btn-check" name="method" id="put" value="PUT">
|
||||||
<label class="btn btn-outline-warning" for="put"><span class="badge bg-warning text-dark">PUT</span></label>
|
<label class="btn btn-outline-warning" for="put"><span class="badge bg-warning text-dark">PUT</span></label>
|
||||||
|
|
||||||
<input type="radio" class="btn-check" name="method" id="delete" value="DELETE">
|
<input type="radio" class="btn-check" name="method" id="delete" value="DELETE">
|
||||||
<label class="btn btn-outline-danger" for="delete"><span class="badge bg-danger">DELETE</span></label>
|
<label class="btn btn-outline-danger" for="delete"><span class="badge bg-danger">DELETE</span></label>
|
||||||
</div>
|
</div>
|
||||||
@ -123,66 +108,68 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
const copyRequestBodyButton = document.getElementById('copyRequestBody');
|
||||||
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
const copyResponseHeadersButton = document.getElementById('copyResponseHeaders');
|
||||||
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
const copyResponseBodyButton = document.getElementById('copyResponseBody');
|
||||||
const authSection = document.getElementById('authSection');
|
|
||||||
const authTypeSelect = document.getElementById('authType');
|
|
||||||
const authInputsGroup = document.getElementById('authInputsGroup');
|
|
||||||
const bearerTokenInput = document.getElementById('bearerTokenInput');
|
|
||||||
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
|
||||||
const usernameInput = document.getElementById('username');
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
|
|
||||||
// Basic check for critical elements
|
// Basic check for critical elements
|
||||||
if (!form || !sendButton || !fhirPathInput || !responseCard || !toggleServerButton || !fhirServerUrlInput || !responseStatus || !responseHeaders || !responseBody || !toggleLabel) {
|
if (!form || !sendButton || !fhirPathInput || !responseCard || !toggleServerButton || !fhirServerUrlInput || !responseStatus || !responseHeaders || !responseBody || !toggleLabel) {
|
||||||
console.error("One or more critical UI elements could not be found. Script execution halted.");
|
console.error("One or more critical UI elements could not be found. Script execution halted.");
|
||||||
alert("Error initializing UI components. Please check the console.");
|
alert("Error initializing UI components. Please check the console.");
|
||||||
return;
|
return; // Stop script execution
|
||||||
}
|
}
|
||||||
console.log("All critical elements checked/found.");
|
console.log("All critical elements checked/found.");
|
||||||
|
|
||||||
// --- State Variable ---
|
// --- State Variable ---
|
||||||
|
// Default assumes standalone, will be forced otherwise by appMode check below
|
||||||
let useLocalHapi = true;
|
let useLocalHapi = true;
|
||||||
|
|
||||||
// --- Get App Mode from Flask Context ---
|
// --- Get App Mode from Flask Context ---
|
||||||
|
// Ensure this variable is correctly passed from Flask using the context_processor
|
||||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||||
console.log('App Mode Detected:', appMode);
|
console.log('App Mode Detected:', appMode);
|
||||||
|
|
||||||
// --- Helper Functions ---
|
// --- DEFINE HELPER FUNCTIONS ---
|
||||||
|
|
||||||
|
// Validates request body, returns null on error, otherwise returns body string or empty string
|
||||||
function validateRequestBody(method, path) {
|
function validateRequestBody(method, path) {
|
||||||
if (!requestBodyInput || !jsonError) return (method === 'POST' || method === 'PUT') ? '' : undefined;
|
if (!requestBodyInput || !jsonError) return (method === 'POST' || method === 'PUT') ? '' : undefined;
|
||||||
const bodyValue = requestBodyInput.value.trim();
|
const bodyValue = requestBodyInput.value.trim();
|
||||||
requestBodyInput.classList.remove('is-invalid');
|
requestBodyInput.classList.remove('is-invalid'); // Reset validation
|
||||||
jsonError.style.display = 'none';
|
jsonError.style.display = 'none';
|
||||||
|
|
||||||
if (!bodyValue) return '';
|
if (!bodyValue) return ''; // Empty body is valid for POST/PUT
|
||||||
|
|
||||||
const isSearch = path && path.endsWith('_search');
|
const isSearch = path && path.endsWith('_search');
|
||||||
const isJson = bodyValue.startsWith('{') || bodyValue.startsWith('[');
|
const isJson = bodyValue.startsWith('{') || bodyValue.startsWith('[');
|
||||||
const isXml = bodyValue.startsWith('<');
|
const isXml = bodyValue.startsWith('<');
|
||||||
const isForm = !isJson && !isXml;
|
const isForm = !isJson && !isXml;
|
||||||
|
|
||||||
if (method === 'POST' && isSearch && isForm) {
|
if (method === 'POST' && isSearch && isForm) { // POST Search with form params
|
||||||
return bodyValue;
|
return bodyValue;
|
||||||
} else if (method === 'POST' || method === 'PUT') {
|
} else if (method === 'POST' || method === 'PUT') { // Other POST/PUT expect JSON/XML
|
||||||
if (isJson) {
|
if (isJson) {
|
||||||
try { JSON.parse(bodyValue); return bodyValue; }
|
try { JSON.parse(bodyValue); return bodyValue; }
|
||||||
catch (e) { jsonError.textContent = `Invalid JSON: ${e.message}`; }
|
catch (e) { jsonError.textContent = `Invalid JSON: ${e.message}`; }
|
||||||
} else if (isXml) {
|
} else if (isXml) {
|
||||||
|
// Basic XML check is difficult in JS, accept it for now
|
||||||
|
// Backend or target server will validate fully
|
||||||
return bodyValue;
|
return bodyValue;
|
||||||
} else {
|
} else { // Neither JSON nor XML, and not a POST search form
|
||||||
jsonError.textContent = 'Request body must be valid JSON or XML for PUT/POST (unless using POST _search with form parameters).';
|
jsonError.textContent = 'Request body must be valid JSON or XML for PUT/POST (unless using POST _search with form parameters).';
|
||||||
}
|
}
|
||||||
requestBodyInput.classList.add('is-invalid');
|
requestBodyInput.classList.add('is-invalid');
|
||||||
jsonError.style.display = 'block';
|
jsonError.style.display = 'block';
|
||||||
return null;
|
return null; // Indicate validation error
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined; // Indicate no body should be sent for GET/DELETE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleans path for proxying
|
||||||
function cleanFhirPath(path) {
|
function cleanFhirPath(path) {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
|
// Remove optional leading 'r4/' or 'fhir/', then trim slashes
|
||||||
return path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
return path.replace(/^(r4\/|fhir\/)*/i, '').replace(/^\/+|\/+$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copies text to clipboard
|
||||||
async function copyToClipboard(text, button) {
|
async function copyToClipboard(text, button) {
|
||||||
if (text === null || text === undefined || !button || !navigator.clipboard) return;
|
if (text === null || text === undefined || !button || !navigator.clipboard) return;
|
||||||
try {
|
try {
|
||||||
@ -190,205 +177,140 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const originalIcon = button.innerHTML;
|
const originalIcon = button.innerHTML;
|
||||||
button.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
button.innerHTML = '<i class="bi bi-check-lg"></i> Copied!';
|
||||||
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 1500);
|
setTimeout(() => { if(button.isConnected) button.innerHTML = originalIcon; }, 1500);
|
||||||
} catch (err) { console.error('Copy failed:', err); }
|
} catch (err) { console.error('Copy failed:', err); /* Optionally alert user */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates the UI elements related to the server toggle button/input
|
||||||
function updateServerToggleUI() {
|
function updateServerToggleUI() {
|
||||||
if (appMode === 'lite') {
|
if (appMode === 'lite') {
|
||||||
useLocalHapi = false;
|
// LITE MODE: Force Custom URL, disable toggle
|
||||||
|
useLocalHapi = false; // Force state
|
||||||
toggleServerButton.disabled = true;
|
toggleServerButton.disabled = true;
|
||||||
toggleServerButton.classList.add('disabled');
|
toggleServerButton.classList.add('disabled');
|
||||||
toggleServerButton.style.pointerEvents = 'none';
|
toggleServerButton.style.pointerEvents = 'none'; // Make unclickable
|
||||||
toggleServerButton.setAttribute('aria-disabled', 'true');
|
toggleServerButton.setAttribute('aria-disabled', 'true');
|
||||||
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
|
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
|
||||||
toggleLabel.textContent = 'Use Custom URL';
|
toggleLabel.textContent = 'Use Custom URL'; // Always show this label
|
||||||
fhirServerUrlInput.style.display = 'block';
|
fhirServerUrlInput.style.display = 'block'; // Always show input
|
||||||
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
|
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
|
||||||
fhirServerUrlInput.required = true;
|
fhirServerUrlInput.required = true; // Make required in lite mode
|
||||||
authSection.style.display = 'block';
|
|
||||||
} else {
|
} else {
|
||||||
|
// STANDALONE MODE: Allow toggle
|
||||||
toggleServerButton.disabled = false;
|
toggleServerButton.disabled = false;
|
||||||
toggleServerButton.classList.remove('disabled');
|
toggleServerButton.classList.remove('disabled');
|
||||||
toggleServerButton.style.pointerEvents = 'auto';
|
toggleServerButton.style.pointerEvents = 'auto';
|
||||||
toggleServerButton.removeAttribute('aria-disabled');
|
toggleServerButton.removeAttribute('aria-disabled');
|
||||||
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
|
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
|
||||||
toggleLabel.textContent = useLocalHapi ? 'Using Local Server' : 'Using Custom URL';
|
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
|
||||||
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
||||||
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
||||||
fhirServerUrlInput.required = !useLocalHapi;
|
fhirServerUrlInput.required = !useLocalHapi; // Required only if custom is selected
|
||||||
authSection.style.display = useLocalHapi ? 'none' : 'block';
|
|
||||||
}
|
}
|
||||||
fhirServerUrlInput.classList.remove('is-invalid');
|
fhirServerUrlInput.classList.remove('is-invalid'); // Clear validation state on toggle
|
||||||
updateAuthInputsUI();
|
|
||||||
console.log(`UI Updated: useLocalHapi=${useLocalHapi}, Button Disabled=${toggleServerButton.disabled}, Input Visible=${fhirServerUrlInput.style.display !== 'none'}, Input Required=${fhirServerUrlInput.required}`);
|
console.log(`UI Updated: useLocalHapi=${useLocalHapi}, Button Disabled=${toggleServerButton.disabled}, Input Visible=${fhirServerUrlInput.style.display !== 'none'}, Input Required=${fhirServerUrlInput.required}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateAuthInputsUI() {
|
// Toggles the server selection state (only effective in Standalone mode)
|
||||||
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
|
|
||||||
const authType = authTypeSelect.value;
|
|
||||||
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
|
|
||||||
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
|
|
||||||
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
|
|
||||||
if (authType !== 'bearer' && bearerTokenInput) bearerTokenInput.value = '';
|
|
||||||
if (authType !== 'basic' && usernameInput) usernameInput.value = '';
|
|
||||||
if (authType !== 'basic' && passwordInput) passwordInput.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleServer() {
|
function toggleServer() {
|
||||||
if (appMode === 'lite') {
|
if (appMode === 'lite') {
|
||||||
console.log("Toggle ignored: Lite mode active.");
|
console.log("Toggle ignored: Lite mode active.");
|
||||||
return;
|
return; // Do nothing in lite mode
|
||||||
}
|
}
|
||||||
useLocalHapi = !useLocalHapi;
|
useLocalHapi = !useLocalHapi;
|
||||||
if (useLocalHapi && fhirServerUrlInput) {
|
if (useLocalHapi && fhirServerUrlInput) {
|
||||||
fhirServerUrlInput.value = '';
|
fhirServerUrlInput.value = ''; // Clear custom URL when switching to local
|
||||||
}
|
}
|
||||||
updateServerToggleUI();
|
updateServerToggleUI(); // Update UI based on new state
|
||||||
updateAuthInputsUI();
|
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local HAPI' : 'Custom URL'}`);
|
||||||
console.log(`Server toggled: Now using ${useLocalHapi ? 'Local Server' : 'Custom URL'}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updates visibility of the Request Body textarea based on selected HTTP method
|
||||||
function updateRequestBodyVisibility() {
|
function updateRequestBodyVisibility() {
|
||||||
const selectedMethod = document.querySelector('input[name="method"]:checked')?.value;
|
const selectedMethod = document.querySelector('input[name="method"]:checked')?.value;
|
||||||
if (!requestBodyGroup || !selectedMethod) return;
|
if (!requestBodyGroup || !selectedMethod) return;
|
||||||
const showBody = (selectedMethod === 'POST' || selectedMethod === 'PUT');
|
const showBody = (selectedMethod === 'POST' || selectedMethod === 'PUT');
|
||||||
requestBodyGroup.style.display = showBody ? 'block' : 'none';
|
requestBodyGroup.style.display = showBody ? 'block' : 'none';
|
||||||
if (!showBody) {
|
if (!showBody) { // Clear body and errors if body not needed
|
||||||
if (requestBodyInput) requestBodyInput.value = '';
|
if (requestBodyInput) requestBodyInput.value = '';
|
||||||
if (jsonError) jsonError.style.display = 'none';
|
if (jsonError) jsonError.style.display = 'none';
|
||||||
if (requestBodyInput) requestBodyInput.classList.remove('is-invalid');
|
if (requestBodyInput) requestBodyInput.classList.remove('is-invalid');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Initial Setup ---
|
// --- INITIAL SETUP & MODE CHECK ---
|
||||||
updateServerToggleUI();
|
updateServerToggleUI(); // Set initial UI based on detected mode and default state
|
||||||
updateRequestBodyVisibility();
|
updateRequestBodyVisibility(); // Set initial visibility based on default method (GET)
|
||||||
|
|
||||||
// --- Event Listeners ---
|
// --- ATTACH EVENT LISTENERS ---
|
||||||
toggleServerButton.addEventListener('click', toggleServer);
|
toggleServerButton.addEventListener('click', toggleServer);
|
||||||
if (methodRadios) { methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); }); }
|
if (methodRadios) { methodRadios.forEach(radio => { radio.addEventListener('change', updateRequestBodyVisibility); }); }
|
||||||
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value )); }
|
if (requestBodyInput && fhirPathInput) { requestBodyInput.addEventListener('input', () => validateRequestBody( document.querySelector('input[name="method"]:checked')?.value, fhirPathInput.value )); }
|
||||||
if (copyRequestBodyButton && requestBodyInput) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBodyInput.value, copyRequestBodyButton)); }
|
if (copyRequestBodyButton && requestBodyInput) { copyRequestBodyButton.addEventListener('click', () => copyToClipboard(requestBodyInput.value, copyRequestBodyButton)); }
|
||||||
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
|
if (copyResponseHeadersButton && responseHeaders) { copyResponseHeadersButton.addEventListener('click', () => copyToClipboard(responseHeaders.textContent, copyResponseHeadersButton)); }
|
||||||
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
|
if (copyResponseBodyButton && responseBody) { copyResponseBodyButton.addEventListener('click', () => copyToClipboard(responseBody.textContent, copyResponseBodyButton)); }
|
||||||
if (authTypeSelect) { authTypeSelect.addEventListener('change', updateAuthInputsUI); }
|
|
||||||
|
|
||||||
// --- Send Request Button Listener ---
|
// --- Send Request Button Listener ---
|
||||||
sendButton.addEventListener('click', async function() {
|
sendButton.addEventListener('click', async function() {
|
||||||
console.log("Send Request button clicked.");
|
console.log("Send Request button clicked.");
|
||||||
sendButton.disabled = true;
|
// --- UI Reset ---
|
||||||
sendButton.textContent = 'Sending...';
|
sendButton.disabled = true; sendButton.textContent = 'Sending...';
|
||||||
responseCard.style.display = 'none';
|
responseCard.style.display = 'none';
|
||||||
responseStatus.textContent = '';
|
responseStatus.textContent = ''; responseHeaders.textContent = ''; responseBody.textContent = '';
|
||||||
responseHeaders.textContent = '';
|
responseStatus.className = 'badge'; // Reset badge class
|
||||||
responseBody.textContent = '';
|
fhirServerUrlInput.classList.remove('is-invalid'); // Reset validation
|
||||||
responseStatus.className = 'badge';
|
|
||||||
fhirServerUrlInput.classList.remove('is-invalid');
|
|
||||||
if(requestBodyInput) requestBodyInput.classList.remove('is-invalid');
|
if(requestBodyInput) requestBodyInput.classList.remove('is-invalid');
|
||||||
if(jsonError) jsonError.style.display = 'none';
|
if(jsonError) jsonError.style.display = 'none';
|
||||||
if (bearerTokenInput) bearerTokenInput.classList.remove('is-invalid');
|
|
||||||
if (usernameInput) usernameInput.classList.remove('is-invalid');
|
|
||||||
if (passwordInput) passwordInput.classList.remove('is-invalid');
|
|
||||||
|
|
||||||
// --- Get Values ---
|
// --- Get Values ---
|
||||||
const path = fhirPathInput.value.trim();
|
const path = fhirPathInput.value.trim();
|
||||||
const method = document.querySelector('input[name="method"]:checked')?.value;
|
const method = document.querySelector('input[name="method"]:checked')?.value;
|
||||||
const customUrl = fhirServerUrlInput.value.trim();
|
const customUrl = fhirServerUrlInput.value.trim();
|
||||||
let body = undefined;
|
let body = undefined;
|
||||||
const authType = authTypeSelect ? authTypeSelect.value : 'none';
|
|
||||||
const bearerToken = document.getElementById('bearerToken').value.trim();
|
|
||||||
const username = usernameInput ? usernameInput.value.trim() : '';
|
|
||||||
const password = passwordInput ? passwordInput.value : '';
|
|
||||||
|
|
||||||
// --- Basic Input Validation ---
|
// --- Basic Input Validation ---
|
||||||
if (!path) {
|
if (!path) { alert('Please enter a FHIR Path.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||||
alert('Please enter a FHIR Path.');
|
if (!method) { alert('Please select a Request Type.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||||
sendButton.disabled = false;
|
if (!useLocalHapi && !customUrl) { // Custom URL mode needs a URL
|
||||||
sendButton.textContent = 'Send Request';
|
alert('Please enter a custom FHIR Server URL.'); fhirServerUrlInput.classList.add('is-invalid'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (!method) {
|
if (!useLocalHapi && customUrl) { // Validate custom URL format
|
||||||
alert('Please select a Request Type.');
|
|
||||||
sendButton.disabled = false;
|
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!useLocalHapi && !customUrl) {
|
|
||||||
alert('Please enter a custom FHIR Server URL.');
|
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
|
||||||
sendButton.disabled = false;
|
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!useLocalHapi && customUrl) {
|
|
||||||
try { new URL(customUrl); }
|
try { new URL(customUrl); }
|
||||||
catch (_) {
|
catch (_) { alert('Invalid custom FHIR Server URL format.'); fhirServerUrlInput.classList.add('is-invalid'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return; }
|
||||||
alert('Invalid custom FHIR Server URL format.');
|
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
|
||||||
sendButton.disabled = false;
|
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Validate Authentication ---
|
// --- Validate & Get Body (if needed) ---
|
||||||
if (!useLocalHapi) {
|
|
||||||
if (authType === 'bearer' && !bearerToken) {
|
|
||||||
alert('Please enter a Bearer Token.');
|
|
||||||
document.getElementById('bearerToken').classList.add('is-invalid');
|
|
||||||
sendButton.disabled = false;
|
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (authType === 'basic' && (!username || !password)) {
|
|
||||||
alert('Please enter both Username and Password for Basic Authentication.');
|
|
||||||
if (!username) usernameInput.classList.add('is-invalid');
|
|
||||||
if (!password) passwordInput.classList.add('is-invalid');
|
|
||||||
sendButton.disabled = false;
|
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Validate & Get Body ---
|
|
||||||
if (method === 'POST' || method === 'PUT') {
|
if (method === 'POST' || method === 'PUT') {
|
||||||
body = validateRequestBody(method, path);
|
body = validateRequestBody(method, path);
|
||||||
if (body === null) {
|
if (body === null) { // null indicates validation error
|
||||||
alert('Request body contains invalid JSON/Format.');
|
alert('Request body contains invalid JSON/Format.'); sendButton.disabled = false; sendButton.textContent = 'Send Request'; return;
|
||||||
sendButton.disabled = false;
|
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
// If body is empty string, ensure it's treated as such for fetch
|
||||||
if (body === '') body = '';
|
if (body === '') body = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Determine Fetch URL and Headers ---
|
// --- Determine Fetch URL and Headers ---
|
||||||
const cleanedPath = cleanFhirPath(path);
|
const cleanedPath = cleanFhirPath(path);
|
||||||
const finalFetchUrl = '/fhir/' + cleanedPath;
|
const finalFetchUrl = '/fhir/' + cleanedPath; // Always send to the backend proxy endpoint
|
||||||
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
|
const headers = { 'Accept': 'application/fhir+json, application/fhir+xml;q=0.9, */*;q=0.8' };
|
||||||
|
|
||||||
|
// Determine Content-Type if body exists
|
||||||
if (body !== undefined) {
|
if (body !== undefined) {
|
||||||
if (body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
|
if (body.trim().startsWith('{')) { headers['Content-Type'] = 'application/fhir+json'; }
|
||||||
else if (body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
|
else if (body.trim().startsWith('<')) { headers['Content-Type'] = 'application/fhir+xml'; }
|
||||||
else if (method === 'POST' && path.endsWith('_search') && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
|
else if (method === 'POST' && path.endsWith('_search') && body && !body.trim().startsWith('{') && !body.trim().startsWith('<')) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; }
|
||||||
else if (body) { headers['Content-Type'] = 'application/fhir+json'; }
|
else if (body) { headers['Content-Type'] = 'application/fhir+json'; } // Default if unknown but present
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Custom Target Header if needed
|
||||||
if (!useLocalHapi && customUrl) {
|
if (!useLocalHapi && customUrl) {
|
||||||
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, '');
|
headers['X-Target-FHIR-Server'] = customUrl.replace(/\/+$/, ''); // Send custom URL without trailing slash
|
||||||
console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
|
console.log("Adding header X-Target-FHIR-Server:", headers['X-Target-FHIR-Server']);
|
||||||
if (authType === 'bearer') {
|
|
||||||
headers['Authorization'] = `Bearer ${bearerToken}`;
|
|
||||||
console.log("Adding header Authorization: Bearer <truncated>");
|
|
||||||
} else if (authType === 'basic') {
|
|
||||||
const credentials = btoa(`${username}:${password}`);
|
|
||||||
headers['Authorization'] = `Basic ${credentials}`;
|
|
||||||
console.log("Adding header Authorization: Basic <redacted>");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add CSRF token ONLY if sending to local proxy (for modifying methods)
|
||||||
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
||||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
|
const csrfToken = csrfTokenInput ? csrfTokenInput.value : null;
|
||||||
|
// Include DELETE method for CSRF check
|
||||||
if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && csrfToken) {
|
if (useLocalHapi && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method) && csrfToken) {
|
||||||
headers['X-CSRFToken'] = csrfToken;
|
headers['X-CSRFToken'] = csrfToken;
|
||||||
console.log("CSRF Token added for local request.");
|
console.log("CSRF Token added for local request.");
|
||||||
@ -397,7 +319,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Executing Fetch: Method=${method}, URL=${finalFetchUrl}, LocalHAPI=${useLocalHapi}`);
|
console.log(`Executing Fetch: Method=${method}, URL=${finalFetchUrl}, LocalHAPI=${useLocalHapi}`);
|
||||||
console.log("Request Headers:", { ...headers, Authorization: headers.Authorization ? '<redacted>' : undefined });
|
console.log("Request Headers:", headers);
|
||||||
if (body !== undefined) console.log("Request Body (first 300 chars):", (body || '').substring(0, 300) + ((body?.length ?? 0) > 300 ? "..." : ""));
|
if (body !== undefined) console.log("Request Body (first 300 chars):", (body || '').substring(0, 300) + ((body?.length ?? 0) > 300 ? "..." : ""));
|
||||||
|
|
||||||
// --- Make the Fetch Request ---
|
// --- Make the Fetch Request ---
|
||||||
@ -405,9 +327,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const response = await fetch(finalFetchUrl, {
|
const response = await fetch(finalFetchUrl, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: body
|
body: body // Will be undefined for GET/DELETE
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Process Response ---
|
||||||
responseCard.style.display = 'block';
|
responseCard.style.display = 'block';
|
||||||
responseStatus.textContent = `${response.status} ${response.statusText}`;
|
responseStatus.textContent = `${response.status} ${response.statusText}`;
|
||||||
responseStatus.className = `badge ${response.ok ? 'bg-success' : 'bg-danger'}`;
|
responseStatus.className = `badge ${response.ok ? 'bg-success' : 'bg-danger'}`;
|
||||||
@ -420,10 +343,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let responseBodyText = await response.text();
|
let responseBodyText = await response.text();
|
||||||
let displayBody = responseBodyText;
|
let displayBody = responseBodyText;
|
||||||
|
|
||||||
|
// Attempt to pretty-print JSON
|
||||||
if (responseContentType.includes('json') && responseBodyText.trim()) {
|
if (responseContentType.includes('json') && responseBodyText.trim()) {
|
||||||
try { displayBody = JSON.stringify(JSON.parse(responseBodyText), null, 2); }
|
try { displayBody = JSON.stringify(JSON.parse(responseBodyText), null, 2); }
|
||||||
catch (e) { console.warn("Failed to pretty-print JSON response:", e); }
|
catch (e) { console.warn("Failed to pretty-print JSON response:", e); /* Show raw text */ }
|
||||||
} else if (responseContentType.includes('xml') && responseBodyText.trim()) {
|
}
|
||||||
|
// Attempt to pretty-print XML (basic indentation)
|
||||||
|
else if (responseContentType.includes('xml') && responseBodyText.trim()) {
|
||||||
try {
|
try {
|
||||||
let formattedXml = '';
|
let formattedXml = '';
|
||||||
let indent = 0;
|
let indent = 0;
|
||||||
@ -431,32 +357,34 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
for (let i = 0; i < xmlLines.length; i++) {
|
for (let i = 0; i < xmlLines.length; i++) {
|
||||||
const node = xmlLines[i];
|
const node = xmlLines[i];
|
||||||
if (!node || node.trim().length === 0) continue;
|
if (!node || node.trim().length === 0) continue;
|
||||||
if (node.match(/^<\/\w/)) indent--;
|
if (node.match(/^<\/\w/)) indent--; // Closing tag
|
||||||
formattedXml += ' '.repeat(Math.max(0, indent)) + node.trim() + '\n';
|
formattedXml += ' '.repeat(Math.max(0, indent)) + node.trim() + '\n';
|
||||||
if (node.match(/^<\w/) && !node.match(/\/>$/) && !node.match(/^<\?/)) indent++;
|
if (node.match(/^<\w/) && !node.match(/\/>$/) && !node.match(/^<\?/)) indent++; // Opening tag
|
||||||
}
|
}
|
||||||
displayBody = formattedXml.trim();
|
displayBody = formattedXml.trim();
|
||||||
} catch (e) { console.warn("Basic XML formatting failed:", e); }
|
} catch(e) { console.warn("Basic XML formatting failed:", e); /* Show raw text */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
responseBody.textContent = displayBody;
|
responseBody.textContent = displayBody;
|
||||||
|
|
||||||
|
// Highlight response body if Prism.js is available
|
||||||
if (typeof Prism !== 'undefined') {
|
if (typeof Prism !== 'undefined') {
|
||||||
responseBody.className = 'border p-2 bg-light';
|
responseBody.className = 'border p-2 bg-light'; // Reset base classes
|
||||||
if (responseContentType.includes('json')) {
|
if (responseContentType.includes('json')) {
|
||||||
responseBody.classList.add('language-json');
|
responseBody.classList.add('language-json');
|
||||||
} else if (responseContentType.includes('xml')) {
|
} else if (responseContentType.includes('xml')) {
|
||||||
responseBody.classList.add('language-xml');
|
responseBody.classList.add('language-xml');
|
||||||
}
|
} // Add other languages if needed
|
||||||
Prism.highlightElement(responseBody);
|
Prism.highlightElement(responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
console.error('Fetch error:', error);
|
||||||
responseCard.style.display = 'block';
|
responseCard.style.display = 'block';
|
||||||
responseStatus.textContent = 'Network Error';
|
responseStatus.textContent = `Network Error`;
|
||||||
responseStatus.className = 'badge bg-danger';
|
responseStatus.className = 'badge bg-danger';
|
||||||
responseHeaders.textContent = 'N/A';
|
responseHeaders.textContent = 'N/A';
|
||||||
|
// Provide a more informative error message
|
||||||
let errorDetail = `Error: ${error.message}\n\n`;
|
let errorDetail = `Error: ${error.message}\n\n`;
|
||||||
if (useLocalHapi) {
|
if (useLocalHapi) {
|
||||||
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server and the local HAPI FHIR server (at http://localhost:8080/fhir) are running.`;
|
errorDetail += `Could not connect to the FHIRFLARE proxy at ${finalFetchUrl}. Ensure the toolkit server and the local HAPI FHIR server (at http://localhost:8080/fhir) are running.`;
|
||||||
@ -465,10 +393,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
responseBody.textContent = errorDetail;
|
responseBody.textContent = errorDetail;
|
||||||
} finally {
|
} finally {
|
||||||
sendButton.disabled = false;
|
sendButton.disabled = false; sendButton.textContent = 'Send Request';
|
||||||
sendButton.textContent = 'Send Request';
|
|
||||||
}
|
}
|
||||||
});
|
}); // End sendButton listener
|
||||||
});
|
|
||||||
|
}); // End DOMContentLoaded Listener
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,40 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mt-4">
|
|
||||||
<h2>IG Configuration Generator</h2>
|
|
||||||
<p>Select the Implementation Guides you want to load into Local FHIR for validation. This will generate the YAML configuration required for the <code>application.yaml</code> file.</p>
|
|
||||||
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">Select IGs</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="POST">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">IGs and their dependencies</label>
|
|
||||||
{% for choice in form.igs.choices %}
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="checkbox" name="igs" value="{{ choice[0] }}" id="ig-{{ choice[0] }}">
|
|
||||||
<label class="form-check-label" for="ig-{{ choice[0] }}">{{ choice[1] }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{{ form.generate_yaml(class="btn btn-primary") }}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if yaml_output %}
|
|
||||||
<div class="card mt-4">
|
|
||||||
<div class="card-header">Generated <code>application.yaml</code> Excerpt</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<p>Copy the following configuration block and paste it into the `hapi.fhir` section of your HAPI server's <code>application.yaml</code> file.</p>
|
|
||||||
<pre><code class="language-yaml">{{ yaml_output }}</code></pre>
|
|
||||||
<div class="alert alert-warning mt-3">
|
|
||||||
<strong>Important:</strong> After updating the <code>application.yaml</code> file, you must **restart your HAPI server** for the changes to take effect.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,194 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% from "_form_helpers.html" import render_field %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
{% include "_flash_messages.html" %}
|
|
||||||
|
|
||||||
<h1 class="mt-4 mb-3">Import FHIR Implementation Guides</h1>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<p class="lead">Import new FHIR Implementation Guides to the system via file or URL.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<h2>Import a New IG</h2>
|
|
||||||
<form id="manual-import-ig-form" method="POST" action="{{ url_for('manual_import_ig') }}" enctype="multipart/form-data">
|
|
||||||
{{ form.hidden_tag() }}
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ render_field(form.import_mode, class="form-select") }}
|
|
||||||
</div>
|
|
||||||
<div id="file-field" class="{% if form.import_mode.data != 'file' %}d-none{% endif %}">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="tgz_file" class="form-label">IG Package File (.tgz)</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="file" name="tgz_file" id="tgz_file" accept=".tgz" class="form-control custom-file-input">
|
|
||||||
<span id="file-label" class="custom-file-label">No file selected</span>
|
|
||||||
</div>
|
|
||||||
{% if form.tgz_file.errors %}
|
|
||||||
<div class="form-error">
|
|
||||||
{% for error in form.tgz_file.errors %}
|
|
||||||
<p>{{ error }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="url-field" class="{% if form.import_mode.data != 'url' %}d-none{% endif %}">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ render_field(form.tgz_url, class="form-control url-input") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ render_field(form.dependency_mode, class="form-select") }}
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
{{ form.resolve_dependencies(type="checkbox", class="form-check-input") }}
|
|
||||||
<label class="form-check-label" for="resolve_dependencies">Resolve Dependencies</label>
|
|
||||||
{% if form.resolve_dependencies.errors %}
|
|
||||||
<div class="form-error">
|
|
||||||
{% for error in form.resolve_dependencies.errors %}
|
|
||||||
<p>{{ error }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="d-grid gap-2 d-sm-flex mt-3">
|
|
||||||
{{ form.submit(class="btn btn-success", id="submit-btn") }}
|
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">Back</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.form-error {
|
|
||||||
color: #dc3545;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
.custom-file-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #495057;
|
|
||||||
background-color: #fff;
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.custom-file-input::-webkit-file-upload-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.custom-file-input::before {
|
|
||||||
content: 'Choose File';
|
|
||||||
display: inline-block;
|
|
||||||
background: #007bff;
|
|
||||||
color: #fff;
|
|
||||||
padding: 0.375rem 0.75rem;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
.custom-file-input:hover::before {
|
|
||||||
background: #0056b3;
|
|
||||||
}
|
|
||||||
.custom-file-label {
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: middle;
|
|
||||||
max-width: 60%;
|
|
||||||
}
|
|
||||||
#file-field, #url-field {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#file-field:not(.d-none), #url-field:not(.d-none) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
console.debug('DOMContentLoaded fired for manual_import_ig.html');
|
|
||||||
|
|
||||||
const importForm = document.getElementById('manual-import-ig-form');
|
|
||||||
const importModeSelect = document.querySelector('#import_mode');
|
|
||||||
const fileField = document.querySelector('#file-field');
|
|
||||||
const urlField = document.querySelector('#url-field');
|
|
||||||
const fileInput = document.querySelector('#tgz_file');
|
|
||||||
const fileLabel = document.querySelector('#file-label');
|
|
||||||
const urlInput = document.querySelector('#tgz_url');
|
|
||||||
|
|
||||||
// Debug DOM elements
|
|
||||||
console.debug('importForm:', !!importForm);
|
|
||||||
console.debug('importModeSelect:', !!importModeSelect);
|
|
||||||
console.debug('fileField:', !!fileField);
|
|
||||||
console.debug('urlField:', !!urlField);
|
|
||||||
console.debug('fileInput:', !!fileInput);
|
|
||||||
console.debug('fileLabel:', !!fileLabel);
|
|
||||||
console.debug('urlInput:', !!urlInput);
|
|
||||||
|
|
||||||
function toggleFields() {
|
|
||||||
if (!importModeSelect || !fileField || !urlField) {
|
|
||||||
console.error('Required elements for toggling not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mode = importModeSelect.value;
|
|
||||||
console.debug(`Toggling fields for mode: ${mode}`);
|
|
||||||
fileField.classList.toggle('d-none', mode !== 'file');
|
|
||||||
urlField.classList.toggle('d-none', mode !== 'url');
|
|
||||||
console.debug(`file-field d-none: ${fileField.classList.contains('d-none')}, url-field d-none: ${urlField.classList.contains('d-none')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFileLabel() {
|
|
||||||
if (fileInput && fileLabel) {
|
|
||||||
const fileName = fileInput.files.length > 0 ? fileInput.files[0].name : 'No file selected';
|
|
||||||
fileLabel.textContent = fileName;
|
|
||||||
console.debug(`Updated file label to: ${fileName}`);
|
|
||||||
} else {
|
|
||||||
console.warn('File input or label not found');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importModeSelect) {
|
|
||||||
importModeSelect.addEventListener('change', function() {
|
|
||||||
console.debug('Import mode changed to:', importModeSelect.value);
|
|
||||||
toggleFields();
|
|
||||||
});
|
|
||||||
toggleFields();
|
|
||||||
} else {
|
|
||||||
console.error('Import mode select not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInput) {
|
|
||||||
fileInput.addEventListener('change', function() {
|
|
||||||
console.debug('File input changed');
|
|
||||||
updateFileLabel();
|
|
||||||
});
|
|
||||||
updateFileLabel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlInput) {
|
|
||||||
urlInput.addEventListener('input', function() {
|
|
||||||
console.debug(`URL input value: ${urlInput.value}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (importForm) {
|
|
||||||
importForm.addEventListener('submit', function(evt) {
|
|
||||||
console.debug('Form submission triggered');
|
|
||||||
if (!importForm.checkValidity()) {
|
|
||||||
evt.preventDefault();
|
|
||||||
console.error('Form validation failed');
|
|
||||||
importForm.reportValidity();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="px-4 py-5 my-5 text-center">
|
<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">Retrieve & Split Data</h1>
|
<h1 class="display-5 fw-bold text-body-emphasis">Retrieve & Split Data</h1>
|
||||||
<div class="col-lg-8 mx-auto">
|
<div class="col-lg-8 mx-auto">
|
||||||
<p class="lead mb-4">
|
<p class="lead mb-4">
|
||||||
@ -10,6 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -26,60 +28,39 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form id="retrieveForm" method="POST">
|
<form id="retrieveForm" method="POST" enctype="multipart/form-data">
|
||||||
{{ form.csrf_token(id='retrieve_csrf_token') }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-bold">FHIR Server</label>
|
<label class="form-label fw-bold">FHIR Server</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
<button type="button" class="btn btn-outline-primary" id="toggleServer">
|
||||||
<span id="toggleLabel">Use Local Server</span>
|
<span id="toggleLabel">Use Local HAPI</span>
|
||||||
</button>
|
</button>
|
||||||
|
{# Ensure the input field uses the form object's data #}
|
||||||
{{ form.fhir_server_url(class="form-control", id="fhirServerUrl", style="display: none;", placeholder="e.g., https://fhir.hl7.org.au/aucore/fhir/DEFAULT", **{'aria-describedby': 'fhirServerHelp'}) }}
|
{{ form.fhir_server_url(class="form-control", id="fhirServerUrl", style="display: none;", placeholder="e.g., https://fhir.hl7.org.au/aucore/fhir/DEFAULT", **{'aria-describedby': 'fhirServerHelp'}) }}
|
||||||
</div>
|
</div>
|
||||||
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local Fhir Server(/fhir proxy) or enter a custom FHIR server URL.</small>
|
<small id="fhirServerHelp" class="form-text text-muted">Toggle to use local HAPI (/fhir proxy) or enter a custom FHIR server URL.</small>
|
||||||
</div>
|
</div>
|
||||||
{# Authentication Section (Shown for Custom URL) #}
|
|
||||||
<div class="mb-3" id="authSection" style="display: none;">
|
{# --- Checkbox Row --- #}
|
||||||
<label class="form-label fw-bold">Authentication</label>
|
|
||||||
<div class="row g-3 align-items-end">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<select class="form-select" id="authType" name="auth_type">
|
|
||||||
<option value="none" selected>None</option>
|
|
||||||
<option value="bearer">Bearer Token</option>
|
|
||||||
<option value="basic">Basic Authentication</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7" id="authInputsGroup" style="display: none;">
|
|
||||||
{# Bearer Token Input #}
|
|
||||||
<div id="bearerTokenInput" style="display: none;">
|
|
||||||
<label for="bearerToken" class="form-label">Bearer Token</label>
|
|
||||||
<input type="password" class="form-control" id="bearerToken" name="bearer_token" placeholder="Enter Bearer Token">
|
|
||||||
</div>
|
|
||||||
{# Basic Auth Inputs #}
|
|
||||||
<div id="basicAuthInputs" style="display: none;">
|
|
||||||
<label for="username" class="form-label">Username</label>
|
|
||||||
<input type="text" class="form-control mb-2" id="username" name="username" placeholder="Enter Username">
|
|
||||||
<label for="password" class="form-label">Password</label>
|
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Enter Password">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Select authentication method for the custom FHIR server.</small>
|
|
||||||
</div>
|
|
||||||
{# Checkbox Row #}
|
|
||||||
<div class="row g-3 mb-3 align-items-center">
|
<div class="row g-3 mb-3 align-items-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
{# Render Fetch Referenced Resources checkbox using the macro #}
|
||||||
{{ render_field(form.validate_references, id='validate_references_checkbox') }}
|
{{ render_field(form.validate_references, id='validate_references_checkbox') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6" id="fetchReferenceBundlesGroup" style="display: none;">
|
<div class="col-md-6" id="fetchReferenceBundlesGroup" style="display: none;"> {# Initially hidden #}
|
||||||
|
{# Render NEW Fetch Full Reference Bundles checkbox using the macro #}
|
||||||
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
|
{{ render_field(form.fetch_reference_bundles, id='fetch_reference_bundles_checkbox') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# --- End Checkbox Row --- #}
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
|
<button type="button" class="btn btn-primary mb-3" id="fetchMetadata">Fetch Metadata</button>
|
||||||
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
|
<div class="banner3 mt-3" id="resourceTypes" style="display: none;">
|
||||||
<h5>Resource Types</h5>
|
<h5>Resource Types</h5>
|
||||||
<div class="d-flex flex-wrap gap-2" id="resourceButtons"></div>
|
<div class="d-flex flex-wrap gap-2" id="resourceButtons"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{# Render SubmitField using the form object #}
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="retrieveButton" name="submit_retrieve">
|
<button type="submit" class="btn btn-primary btn-lg w-100" id="retrieveButton" name="submit_retrieve">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
||||||
<i class="bi bi-arrow-down-circle me-2"></i>{{ form.submit_retrieve.label.text }}
|
<i class="bi bi-arrow-down-circle me-2"></i>{{ form.submit_retrieve.label.text }}
|
||||||
@ -98,13 +79,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
|
<h4 class="my-0 fw-normal"><i class="bi bi-scissors me-2"></i>Split Bundles</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="splitForm" method="POST" enctype="multipart/form-data">
|
<form id="splitForm" method="POST" enctype="multipart/form-data">
|
||||||
{{ form.csrf_token(id='split_csrf_token') }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label fw-bold">Bundle Source</label>
|
<label class="form-label fw-bold">Bundle Source</label>
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
@ -116,7 +98,9 @@
|
|||||||
<label class="form-check-label" for="useRetrievedBundles">Use Retrieved Bundles{% if not session.get('retrieve_params', {}).get('bundle_zip_path', None) %} (No bundles retrieved in this session){% endif %}</label>
|
<label class="form-check-label" for="useRetrievedBundles">Use Retrieved Bundles{% if not session.get('retrieve_params', {}).get('bundle_zip_path', None) %} (No bundles retrieved in this session){% endif %}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# Render FileField using the macro #}
|
||||||
{{ render_field(form.split_bundle_zip, class="form-control") }}
|
{{ render_field(form.split_bundle_zip, class="form-control") }}
|
||||||
|
{# Render SubmitField using the form object #}
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="splitButton" name="submit_split">
|
<button type="submit" class="btn btn-primary btn-lg w-100" id="splitButton" name="submit_split">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
||||||
<i class="bi bi-arrow-right-circle me-2"></i>{{ form.submit_split.label.text }}
|
<i class="bi bi-arrow-right-circle me-2"></i>{{ form.submit_split.label.text }}
|
||||||
@ -136,6 +120,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// --- DOM Element References ---
|
// --- DOM Element References ---
|
||||||
@ -159,13 +144,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const validateReferencesCheckbox = document.getElementById('validate_references_checkbox');
|
const validateReferencesCheckbox = document.getElementById('validate_references_checkbox');
|
||||||
const fetchReferenceBundlesGroup = document.getElementById('fetchReferenceBundlesGroup');
|
const fetchReferenceBundlesGroup = document.getElementById('fetchReferenceBundlesGroup');
|
||||||
const fetchReferenceBundlesCheckbox = document.getElementById('fetch_reference_bundles_checkbox');
|
const fetchReferenceBundlesCheckbox = document.getElementById('fetch_reference_bundles_checkbox');
|
||||||
const authSection = document.getElementById('authSection');
|
|
||||||
const authTypeSelect = document.getElementById('authType');
|
|
||||||
const authInputsGroup = document.getElementById('authInputsGroup');
|
|
||||||
const bearerTokenInput = document.getElementById('bearerTokenInput');
|
|
||||||
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
|
||||||
const usernameInput = document.getElementById('username');
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
// --- State & Config Variables ---
|
// --- State & Config Variables ---
|
||||||
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
const appMode = '{{ app_mode | default("standalone") | lower }}';
|
||||||
const apiKey = '{{ api_key }}';
|
const apiKey = '{{ api_key }}';
|
||||||
@ -174,9 +153,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
let retrieveZipPath = null;
|
let retrieveZipPath = null;
|
||||||
let splitZipPath = null;
|
let splitZipPath = null;
|
||||||
let fetchedMetadataCache = null;
|
let fetchedMetadataCache = null;
|
||||||
let localHapiBaseUrl = ''; // To store the fetched URL
|
|
||||||
// --- Helper Functions ---
|
// --- Helper Functions ---
|
||||||
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
||||||
|
|
||||||
// --- UI Update Functions ---
|
// --- UI Update Functions ---
|
||||||
function updateBundleSourceUI() {
|
function updateBundleSourceUI() {
|
||||||
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
const selectedSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
||||||
@ -187,66 +167,36 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
splitBundleZipInput.required = selectedSource === 'upload';
|
splitBundleZipInput.required = selectedSource === 'upload';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function fetchLocalUrlAndSetUI() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/get-local-server-url');
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch local server URL.');
|
|
||||||
const data = await response.json();
|
|
||||||
localHapiBaseUrl = data.url.replace(/\/+$/, ''); // Normalize by removing trailing slashes
|
|
||||||
console.log(`Local HAPI URL fetched: ${localHapiBaseUrl}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
localHapiBaseUrl = '/fhir'; // Fallback to proxy
|
|
||||||
alert('Could not fetch local HAPI server URL. Falling back to proxy.');
|
|
||||||
} finally {
|
|
||||||
updateServerToggleUI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function updateServerToggleUI() {
|
function updateServerToggleUI() {
|
||||||
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton || !authSection) return;
|
if (!toggleLabel || !fhirServerUrlInput || !toggleServerButton) return;
|
||||||
|
|
||||||
if (appMode === 'lite') {
|
if (appMode === 'lite') {
|
||||||
useLocalHapi = false;
|
useLocalHapi = false;
|
||||||
toggleServerButton.disabled = true;
|
toggleServerButton.disabled = true;
|
||||||
toggleServerButton.classList.add('disabled');
|
toggleServerButton.classList.add('disabled');
|
||||||
toggleServerButton.style.pointerEvents = 'none';
|
toggleServerButton.style.pointerEvents = 'none !important';
|
||||||
toggleServerButton.setAttribute('aria-disabled', 'true');
|
toggleServerButton.setAttribute('aria-disabled', 'true');
|
||||||
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
|
toggleServerButton.title = "Local HAPI server is unavailable in Lite mode";
|
||||||
toggleLabel.textContent = 'Use Custom URL';
|
toggleLabel.textContent = 'Use Custom URL';
|
||||||
fhirServerUrlInput.style.display = 'block';
|
fhirServerUrlInput.style.display = 'block';
|
||||||
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
|
fhirServerUrlInput.placeholder = "Enter FHIR Base URL (Local HAPI unavailable)";
|
||||||
fhirServerUrlInput.required = true;
|
fhirServerUrlInput.required = true;
|
||||||
authSection.style.display = 'block';
|
|
||||||
} else {
|
} else {
|
||||||
toggleServerButton.disabled = false;
|
toggleServerButton.disabled = false;
|
||||||
toggleServerButton.classList.remove('disabled');
|
toggleServerButton.classList.remove('disabled');
|
||||||
toggleServerButton.style.pointerEvents = 'auto';
|
toggleServerButton.style.pointerEvents = 'auto';
|
||||||
toggleServerButton.removeAttribute('aria-disabled');
|
toggleServerButton.removeAttribute('aria-disabled');
|
||||||
toggleLabel.textContent = useLocalHapi ? 'Use Local Server' : 'Use Custom URL';
|
toggleServerButton.title = "Toggle between Local HAPI and Custom URL";
|
||||||
|
toggleLabel.textContent = useLocalHapi ? 'Using Local HAPI' : 'Using Custom URL';
|
||||||
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
fhirServerUrlInput.style.display = useLocalHapi ? 'none' : 'block';
|
||||||
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
fhirServerUrlInput.placeholder = "e.g., https://hapi.fhir.org/baseR4";
|
||||||
fhirServerUrlInput.required = !useLocalHapi;
|
fhirServerUrlInput.required = !useLocalHapi;
|
||||||
authSection.style.display = useLocalHapi ? 'none' : 'block';
|
|
||||||
}
|
}
|
||||||
fhirServerUrlInput.classList.remove('is-invalid');
|
fhirServerUrlInput.classList.remove('is-invalid');
|
||||||
updateAuthInputsUI();
|
|
||||||
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
|
console.log(`Server toggle UI updated: useLocalHapi=${useLocalHapi}, customUrl=${fhirServerUrlInput.value}`);
|
||||||
}
|
}
|
||||||
function updateAuthInputsUI() {
|
|
||||||
if (!authTypeSelect || !authInputsGroup || !bearerTokenInput || !basicAuthInputs) return;
|
|
||||||
const authType = authTypeSelect.value;
|
|
||||||
authInputsGroup.style.display = (authType === 'bearer' || authType === 'basic') ? 'block' : 'none';
|
|
||||||
bearerTokenInput.style.display = authType === 'bearer' ? 'block' : 'none';
|
|
||||||
basicAuthInputs.style.display = authType === 'basic' ? 'block' : 'none';
|
|
||||||
if (authType !== 'bearer') {
|
|
||||||
// Correctly reference the input element by its ID
|
|
||||||
const tokenInput = document.getElementById('bearerToken');
|
|
||||||
if (tokenInput) tokenInput.value = '';
|
|
||||||
}
|
|
||||||
if (authType !== 'basic') {
|
|
||||||
if (usernameInput) usernameInput.value = '';
|
|
||||||
if (passwordInput) passwordInput.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function toggleFetchReferenceBundles() {
|
function toggleFetchReferenceBundles() {
|
||||||
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
|
if (validateReferencesCheckbox && fetchReferenceBundlesGroup) {
|
||||||
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
|
fetchReferenceBundlesGroup.style.display = validateReferencesCheckbox.checked ? 'block' : 'none';
|
||||||
@ -257,40 +207,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
console.warn("Could not find checkbox elements needed for toggling.");
|
console.warn("Could not find checkbox elements needed for toggling.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Event Listeners ---
|
// --- Event Listeners ---
|
||||||
bundleSourceRadios.forEach(radio => {
|
bundleSourceRadios.forEach(radio => {
|
||||||
radio.addEventListener('change', updateBundleSourceUI);
|
radio.addEventListener('change', updateBundleSourceUI);
|
||||||
});
|
});
|
||||||
|
|
||||||
toggleServerButton.addEventListener('click', () => {
|
toggleServerButton.addEventListener('click', () => {
|
||||||
if (appMode === 'lite') return;
|
if (appMode === 'lite') return;
|
||||||
useLocalHapi = !useLocalHapi;
|
useLocalHapi = !useLocalHapi;
|
||||||
if (useLocalHapi) fhirServerUrlInput.value = '';
|
if (useLocalHapi) fhirServerUrlInput.value = '';
|
||||||
updateServerToggleUI();
|
updateServerToggleUI();
|
||||||
updateAuthInputsUI();
|
|
||||||
resourceTypesDiv.style.display = 'none';
|
resourceTypesDiv.style.display = 'none';
|
||||||
fetchedMetadataCache = null;
|
fetchedMetadataCache = null;
|
||||||
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
|
console.log(`Server toggled: useLocalHapi=${useLocalHapi}`);
|
||||||
});
|
});
|
||||||
if (authTypeSelect) {
|
|
||||||
authTypeSelect.addEventListener('change', updateAuthInputsUI);
|
|
||||||
}
|
|
||||||
if (validateReferencesCheckbox) {
|
if (validateReferencesCheckbox) {
|
||||||
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
|
validateReferencesCheckbox.addEventListener('change', toggleFetchReferenceBundles);
|
||||||
} else {
|
} else {
|
||||||
console.warn("Validate references checkbox not found.");
|
console.warn("Validate references checkbox not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchMetadataButton.addEventListener('click', async () => {
|
fetchMetadataButton.addEventListener('click', async () => {
|
||||||
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
|
resourceButtonsContainer.innerHTML = '<span class="text-muted">Fetching...</span>';
|
||||||
resourceTypesDiv.style.display = 'block';
|
resourceTypesDiv.style.display = 'block';
|
||||||
let customUrl = null;
|
const customUrl = useLocalHapi ? null : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
|
||||||
if (!useLocalHapi) {
|
|
||||||
customUrl = fhirServerUrlInput.value.trim().replace(/\/+$/, ''); // Normalize custom URL
|
if (!useLocalHapi && !customUrl) {
|
||||||
if (!customUrl) {
|
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
fhirServerUrlInput.classList.add('is-invalid');
|
||||||
alert('Please enter a valid FHIR server URL.');
|
alert('Please enter a valid FHIR server URL.');
|
||||||
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
|
resourceButtonsContainer.innerHTML = '<span class="text-danger">Error: Custom URL required.</span>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!useLocalHapi) {
|
||||||
try { new URL(customUrl); } catch (_) {
|
try { new URL(customUrl); } catch (_) {
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
fhirServerUrlInput.classList.add('is-invalid');
|
||||||
alert('Invalid custom URL format.');
|
alert('Invalid custom URL format.');
|
||||||
@ -299,32 +249,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fhirServerUrlInput.classList.remove('is-invalid');
|
fhirServerUrlInput.classList.remove('is-invalid');
|
||||||
|
|
||||||
fetchMetadataButton.disabled = true;
|
fetchMetadataButton.disabled = true;
|
||||||
fetchMetadataButton.textContent = 'Fetching...';
|
fetchMetadataButton.textContent = 'Fetching...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build URL and headers based on the toggle state
|
const fetchUrl = '/fhir/metadata'; // Always target proxy
|
||||||
let fetchUrl;
|
|
||||||
const headers = { 'Accept': 'application/fhir+json' };
|
const headers = { 'Accept': 'application/fhir+json' };
|
||||||
if (useLocalHapi) {
|
if (!useLocalHapi && customUrl) {
|
||||||
if (!localHapiBaseUrl) {
|
headers['X-Target-FHIR-Server'] = customUrl;
|
||||||
throw new Error("Local HAPI URL not available. Please refresh.");
|
console.log(`Workspaceing metadata via proxy with X-Target-FHIR-Server: ${customUrl}`);
|
||||||
}
|
|
||||||
fetchUrl = `${localHapiBaseUrl}/metadata`; // localHapiBaseUrl is already normalized
|
|
||||||
console.log(`Fetching metadata directly from local URL: ${fetchUrl}`);
|
|
||||||
} else {
|
} else {
|
||||||
fetchUrl = `${customUrl}/metadata`;
|
console.log("Fetching metadata via proxy for local HAPI server");
|
||||||
// Add authentication headers for the direct request to the custom server
|
|
||||||
const authType = authTypeSelect.value;
|
|
||||||
if (authType === 'bearer' && document.getElementById('bearerToken') && document.getElementById('bearerToken').value) {
|
|
||||||
headers['Authorization'] = `Bearer ${document.getElementById('bearerToken').value}`;
|
|
||||||
} else if (authType === 'basic' && usernameInput && passwordInput && usernameInput.value && passwordInput.value) {
|
|
||||||
const credentials = btoa(`${usernameInput.value}:${passwordInput.value}`);
|
|
||||||
headers['Authorization'] = `Basic ${credentials}`;
|
|
||||||
}
|
}
|
||||||
console.log(`Fetching metadata directly from custom URL: ${fetchUrl}`);
|
console.log(`Proxy Fetch URL: ${fetchUrl}`);
|
||||||
}
|
console.log(`Request Headers sent TO PROXY: ${JSON.stringify(headers)}`);
|
||||||
console.log(`Request Headers sent: ${JSON.stringify(headers)}`);
|
|
||||||
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
|
const response = await fetch(fetchUrl, { method: 'GET', headers: headers });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
let errorText = `Request failed with status ${response.status} ${response.statusText}`;
|
let errorText = `Request failed with status ${response.status} ${response.statusText}`;
|
||||||
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
|
try { const errorBody = await response.text(); errorText += ` - Body: ${errorBody.substring(0, 200)}...`; } catch (e) {}
|
||||||
@ -334,6 +276,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
|
console.log("Metadata received:", JSON.stringify(data, null, 2).substring(0, 500));
|
||||||
fetchedMetadataCache = data;
|
fetchedMetadataCache = data;
|
||||||
|
|
||||||
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
|
const resourceTypes = data.rest?.[0]?.resource?.map(r => r.type)?.filter(Boolean) || [];
|
||||||
resourceButtonsContainer.innerHTML = '';
|
resourceButtonsContainer.innerHTML = '';
|
||||||
if (!resourceTypes.length) {
|
if (!resourceTypes.length) {
|
||||||
@ -364,6 +307,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fetchMetadataButton.textContent = 'Fetch Metadata';
|
fetchMetadataButton.textContent = 'Fetch Metadata';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
retrieveForm.addEventListener('submit', async (e) => {
|
retrieveForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const spinner = retrieveButton.querySelector('.spinner-border');
|
const spinner = retrieveButton.querySelector('.spinner-border');
|
||||||
@ -374,28 +318,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
downloadRetrieveButton.style.display = 'none';
|
downloadRetrieveButton.style.display = 'none';
|
||||||
retrieveZipPath = null;
|
retrieveZipPath = null;
|
||||||
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
|
retrieveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle retrieval...</div>`;
|
||||||
|
|
||||||
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
|
const selectedResources = Array.from(resourceButtonsContainer.querySelectorAll('.btn.active')).map(btn => btn.dataset.resource);
|
||||||
if (!selectedResources.length) {
|
if (!selectedResources.length) {
|
||||||
alert('Please select at least one resource type.');
|
alert('Please select at least one resource type.');
|
||||||
retrieveButton.disabled = false;
|
retrieveButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentFhirServerUrl = useLocalHapi ? localHapiBaseUrl : fhirServerUrlInput.value.trim().replace(/\/+$/, '');
|
|
||||||
if (!currentFhirServerUrl) {
|
|
||||||
alert('FHIR Server URL is required.');
|
|
||||||
fhirServerUrlInput.classList.add('is-invalid');
|
|
||||||
retrieveButton.disabled = false;
|
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
|
const csrfTokenInput = retrieveForm.querySelector('input[name="csrf_token"]');
|
||||||
if(csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
if(csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
||||||
selectedResources.forEach(res => formData.append('resources', res));
|
selectedResources.forEach(res => formData.append('resources', res));
|
||||||
formData.append('fhir_server_url', currentFhirServerUrl);
|
|
||||||
if (validateReferencesCheckbox) {
|
if (validateReferencesCheckbox) {
|
||||||
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
|
formData.append('validate_references', validateReferencesCheckbox.checked ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
@ -408,45 +342,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} else {
|
} else {
|
||||||
formData.append('fetch_reference_bundles', 'false');
|
formData.append('fetch_reference_bundles', 'false');
|
||||||
}
|
}
|
||||||
if (!useLocalHapi && authTypeSelect) {
|
|
||||||
const authType = authTypeSelect.value;
|
const currentFhirServerUrl = useLocalHapi ? '/fhir' : fhirServerUrlInput.value.trim();
|
||||||
formData.append('auth_type', authType);
|
if (!useLocalHapi && !currentFhirServerUrl) {
|
||||||
if (authType === 'bearer') {
|
alert('Custom FHIR Server URL is required.'); fhirServerUrlInput.classList.add('is-invalid');
|
||||||
const bearerToken = document.getElementById('bearerToken').value.trim();
|
retrieveButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
|
||||||
if (!bearerToken) {
|
|
||||||
alert('Please enter a Bearer Token.');
|
|
||||||
retrieveButton.disabled = false;
|
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formData.append('bearer_token', bearerToken);
|
|
||||||
} else if (authType === 'basic' && usernameInput && passwordInput) {
|
|
||||||
if (!usernameInput.value || !passwordInput.value) {
|
|
||||||
alert('Please enter both Username and Password for Basic Authentication.');
|
|
||||||
retrieveButton.disabled = false;
|
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formData.append('username', usernameInput.value);
|
|
||||||
formData.append('password', passwordInput.value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
formData.append('fhir_server_url', currentFhirServerUrl);
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Accept': 'application/x-ndjson',
|
'Accept': 'application/x-ndjson',
|
||||||
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
||||||
'X-API-Key': apiKey
|
'X-API-Key': apiKey
|
||||||
};
|
};
|
||||||
console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}, AuthType: ${formData.get('auth_type')}`);
|
console.log(`Submitting retrieve request. Server: ${currentFhirServerUrl}, ValidateRefs: ${formData.get('validate_references')}, FetchRefBundles: ${formData.get('fetch_reference_bundles')}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
|
const response = await fetch('/api/retrieve-bundles', { method: 'POST', headers: headers, body: formData });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
|
const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response.' }));
|
||||||
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
|
throw new Error(`HTTP ${response.status}: ${errorData.message || 'Unknown error during retrieval'}`);
|
||||||
}
|
}
|
||||||
retrieveZipPath = response.headers.get('X-Zip-Path');
|
retrieveZipPath = response.headers.get('X-Zip-Path');
|
||||||
console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
|
console.log(`Received X-Zip-Path: ${retrieveZipPath}`);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
@ -461,18 +381,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
const data = JSON.parse(line);
|
const data = JSON.parse(line);
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
let messageClass = 'text-info';
|
let messageClass = 'text-info'; let prefix = '[INFO]';
|
||||||
let prefix = '[INFO]';
|
|
||||||
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
|
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
|
||||||
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
|
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
|
||||||
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
||||||
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
|
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
|
||||||
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
|
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
|
||||||
|
|
||||||
const messageDiv = document.createElement('div');
|
const messageDiv = document.createElement('div');
|
||||||
messageDiv.className = messageClass;
|
messageDiv.className = messageClass;
|
||||||
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
|
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
|
||||||
retrieveConsole.appendChild(messageDiv);
|
retrieveConsole.appendChild(messageDiv);
|
||||||
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
|
retrieveConsole.scrollTop = retrieveConsole.scrollHeight;
|
||||||
|
|
||||||
if (data.type === 'complete' && retrieveZipPath) {
|
if (data.type === 'complete' && retrieveZipPath) {
|
||||||
const completeData = data.data || {};
|
const completeData = data.data || {};
|
||||||
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
|
const bundlesFound = (completeData.total_initial_bundles > 0) || (completeData.fetched_individual_references > 0) || (completeData.fetched_type_bundles > 0);
|
||||||
@ -489,7 +410,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
} catch (e) { console.error('Stream parse error:', e, 'Line:', line); }
|
} catch (e) { console.error('Stream parse error:', e, 'Line:', line); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (buffer.trim()) { /* Handle final buffer */ }
|
if (buffer.trim()) { /* Handle final buffer if necessary */ }
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Retrieval error:', e);
|
console.error('Retrieval error:', e);
|
||||||
const ts = new Date().toLocaleTimeString();
|
const ts = new Date().toLocaleTimeString();
|
||||||
@ -501,17 +423,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadRetrieveButton.addEventListener('click', () => {
|
downloadRetrieveButton.addEventListener('click', () => {
|
||||||
if (retrieveZipPath) {
|
if (retrieveZipPath) {
|
||||||
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
|
const filename = retrieveZipPath.split('/').pop() || 'retrieved_bundles.zip';
|
||||||
const downloadUrl = `/tmp/${filename}`;
|
const downloadUrl = `/tmp/${filename}`;
|
||||||
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
|
console.log(`Attempting to download ZIP from Flask endpoint: ${downloadUrl}`);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
link.download = 'retrieved_bundles.zip';
|
link.download = 'retrieved_bundles.zip';
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
|
const csrfToken = retrieveForm.querySelector('input[name="csrf_token"]')?.value || '';
|
||||||
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
|
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
|
||||||
@ -529,6 +454,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
alert("Download error: No file path available.");
|
alert("Download error: No file path available.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
splitForm.addEventListener('submit', async (e) => {
|
splitForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const spinner = splitButton.querySelector('.spinner-border');
|
const spinner = splitButton.querySelector('.spinner-border');
|
||||||
@ -539,17 +465,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
downloadSplitButton.style.display = 'none';
|
downloadSplitButton.style.display = 'none';
|
||||||
splitZipPath = null;
|
splitZipPath = null;
|
||||||
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
|
splitConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting bundle splitting...</div>`;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
|
const csrfTokenInput = splitForm.querySelector('input[name="csrf_token"]');
|
||||||
if(csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
if(csrfTokenInput) formData.append('csrf_token', csrfTokenInput.value);
|
||||||
|
|
||||||
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
const bundleSource = Array.from(bundleSourceRadios).find(radio => radio.checked)?.value;
|
||||||
|
|
||||||
if (bundleSource === 'upload') {
|
if (bundleSource === 'upload') {
|
||||||
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
|
if (!splitBundleZipInput || splitBundleZipInput.files.length === 0) {
|
||||||
alert('Please select a ZIP file to upload for splitting.');
|
alert('Please select a ZIP file to upload for splitting.');
|
||||||
splitButton.disabled = false;
|
splitButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
formData.append('split_bundle_zip', splitBundleZipInput.files[0]);
|
formData.append('split_bundle_zip', splitBundleZipInput.files[0]);
|
||||||
console.log("Splitting uploaded file:", splitBundleZipInput.files[0].name);
|
console.log("Splitting uploaded file:", splitBundleZipInput.files[0].name);
|
||||||
@ -557,70 +483,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
formData.append('split_bundle_zip_path', sessionZipPath);
|
formData.append('split_bundle_zip_path', sessionZipPath);
|
||||||
console.log("Splitting retrieved bundle path:", sessionZipPath);
|
console.log("Splitting retrieved bundle path:", sessionZipPath);
|
||||||
} else if (bundleSource === 'retrieved' && !sessionZipPath){
|
} else if (bundleSource === 'retrieved' && !sessionZipPath){
|
||||||
|
// Check if the retrieveZipPath from the *current* run exists
|
||||||
if (retrieveZipPath) {
|
if (retrieveZipPath) {
|
||||||
formData.append('split_bundle_zip_path', retrieveZipPath);
|
formData.append('split_bundle_zip_path', retrieveZipPath);
|
||||||
console.log("Splitting retrieve bundle path from current run:", retrieveZipPath);
|
console.log("Splitting retrieve bundle path from current run:", retrieveZipPath);
|
||||||
} else {
|
} else {
|
||||||
alert('No bundle source available. Please retrieve bundles first or upload a ZIP.');
|
alert('No bundle source available. Please retrieve bundles first or upload a ZIP.');
|
||||||
splitButton.disabled = false;
|
splitButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('No bundle source selected.');
|
alert('No bundle source selected.');
|
||||||
splitButton.disabled = false;
|
splitButton.disabled = false; if (spinner) spinner.style.display = 'none'; if (icon) icon.style.display = 'inline-block'; return;
|
||||||
if (spinner) spinner.style.display = 'none';
|
|
||||||
if (icon) icon.style.display = 'inline-block';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
'Accept': 'application/x-ndjson',
|
'Accept': 'application/x-ndjson',
|
||||||
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
'X-CSRFToken': csrfTokenInput ? csrfTokenInput.value : '',
|
||||||
'X-API-Key': apiKey
|
'X-API-Key': apiKey
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
|
const response = await fetch('/api/split-bundles', { method: 'POST', headers: headers, body: formData });
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorMsg = await response.text();
|
const errorMsg = await response.text();
|
||||||
let detail = errorMsg;
|
let detail = errorMsg; try { const errorJson = JSON.parse(errorMsg); detail = errorJson.message || errorJson.error || JSON.stringify(errorJson); } catch(e) {}
|
||||||
try { const errorJson = JSON.parse(errorMsg); detail = errorJson.message || errorJson.error || JSON.stringify(errorJson); } catch (e) {}
|
|
||||||
throw new Error(`HTTP ${response.status}: ${detail}`);
|
throw new Error(`HTTP ${response.status}: ${detail}`);
|
||||||
}
|
}
|
||||||
splitZipPath = response.headers.get('X-Zip-Path');
|
splitZipPath = response.headers.get('X-Zip-Path');
|
||||||
console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
|
console.log(`Received X-Zip-Path for split: ${splitZipPath}`);
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read(); if (done) break;
|
||||||
if (done) break;
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
buffer += decoder.decode(value, { stream: true });
|
||||||
const lines = buffer.split('\n');
|
const lines = buffer.split('\n'); buffer = lines.pop() || '';
|
||||||
buffer = lines.pop() || '';
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(line);
|
const data = JSON.parse(line);
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
let messageClass = 'text-info';
|
let messageClass = 'text-info'; let prefix = '[INFO]';
|
||||||
let prefix = '[INFO]';
|
|
||||||
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
|
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
|
||||||
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
||||||
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
|
else if (data.type === 'progress') { prefix = '[PROGRESS]'; messageClass = 'text-light'; }
|
||||||
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
|
else if (data.type === 'complete') { prefix = '[COMPLETE]'; messageClass = 'text-primary'; }
|
||||||
const messageDiv = document.createElement('div');
|
const messageDiv = document.createElement('div'); messageDiv.className = messageClass; messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`; splitConsole.appendChild(messageDiv); splitConsole.scrollTop = splitConsole.scrollHeight;
|
||||||
messageDiv.className = messageClass;
|
if (data.type === 'complete' && splitZipPath) { downloadSplitButton.style.display = 'block'; }
|
||||||
messageDiv.innerHTML = `${timestamp} ${prefix} ${sanitizeText(data.message)}`;
|
|
||||||
splitConsole.appendChild(messageDiv);
|
|
||||||
splitConsole.scrollTop = splitConsole.scrollHeight;
|
|
||||||
if (data.type === 'complete' && splitZipPath) {
|
|
||||||
downloadSplitButton.style.display = 'block';
|
|
||||||
}
|
|
||||||
} catch (e) { console.error('Stream parse error:', e, 'Line:', line); }
|
} catch (e) { console.error('Stream parse error:', e, 'Line:', line); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (buffer.trim()) { /* Handle final buffer */ }
|
if (buffer.trim()) { /* Handle final buffer */ }
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Splitting error:', e);
|
console.error('Splitting error:', e);
|
||||||
const ts = new Date().toLocaleTimeString();
|
const ts = new Date().toLocaleTimeString();
|
||||||
@ -632,17 +549,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
if (icon) icon.style.display = 'inline-block';
|
if (icon) icon.style.display = 'inline-block';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadSplitButton.addEventListener('click', () => {
|
downloadSplitButton.addEventListener('click', () => {
|
||||||
if (splitZipPath) {
|
if (splitZipPath) {
|
||||||
const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
|
const filename = splitZipPath.split('/').pop() || 'split_resources.zip';
|
||||||
const downloadUrl = `/tmp/${filename}`;
|
const downloadUrl = `/tmp/${filename}`;
|
||||||
console.log(`Attempting to download split ZIP from Flask endpoint: ${downloadUrl}`);
|
console.log(`Attempting to download split ZIP from Flask endpoint: ${downloadUrl}`);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a'); link.href = downloadUrl; link.download = 'split_resources.zip'; document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||||||
link.href = downloadUrl;
|
|
||||||
link.download = 'split_resources.zip';
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const csrfToken = splitForm.querySelector('input[name="csrf_token"]')?.value || '';
|
const csrfToken = splitForm.querySelector('input[name="csrf_token"]')?.value || '';
|
||||||
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
|
fetch('/clear-session', { method: 'POST', headers: { 'X-CSRFToken': csrfToken }})
|
||||||
@ -656,10 +569,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
alert("Download error: No file path available.");
|
alert("Download error: No file path available.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Initial Setup Calls ---
|
// --- Initial Setup Calls ---
|
||||||
updateBundleSourceUI();
|
updateBundleSourceUI();
|
||||||
fetchLocalUrlAndSetUI();
|
updateServerToggleUI();
|
||||||
toggleFetchReferenceBundles();
|
toggleFetchReferenceBundles(); // Initial call for the new checkbox
|
||||||
});
|
|
||||||
|
}); // End DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% from "_form_helpers.html" import render_field %}
|
{% from "_form_helpers.html" import render_field %} {# Assuming you have this helper #}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="px-4 py-5 my-5 text-center">
|
<div class="px-4 py-5 my-5 text-center">
|
||||||
@ -20,6 +20,7 @@
|
|||||||
<h4 class="my-0 fw-normal"><i class="bi bi-cloud-upload me-2"></i>Upload Configuration</h4>
|
<h4 class="my-0 fw-normal"><i class="bi bi-cloud-upload me-2"></i>Upload Configuration</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
{# --- Display WTForms validation errors --- #}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<p><strong>Please correct the following errors:</strong></p>
|
<p><strong>Please correct the following errors:</strong></p>
|
||||||
@ -30,52 +31,57 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{# --- End Error Display --- #}
|
||||||
|
|
||||||
<form id="uploadTestDataForm" method="POST" enctype="multipart/form-data">
|
<form id="uploadTestDataForm" method="POST" enctype="multipart/form-data"> {# Use POST, JS will handle submission #}
|
||||||
{{ form.csrf_token }}
|
{{ form.csrf_token }} {# Render CSRF token #}
|
||||||
|
|
||||||
{{ render_field(form.fhir_server_url, class="form-control form-control-lg") }}
|
{{ render_field(form.fhir_server_url, class="form-control form-control-lg") }}
|
||||||
|
|
||||||
|
{# Authentication Row #}
|
||||||
<div class="row g-3 mb-3 align-items-end">
|
<div class="row g-3 mb-3 align-items-end">
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
{{ render_field(form.auth_type, class="form-select") }}
|
{{ render_field(form.auth_type, class="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-7" id="authInputsGroup" style="display: none;">
|
<div class="col-md-7" id="authTokenGroup" style="display: none;">
|
||||||
<div id="bearerTokenInput" style="display: none;">
|
|
||||||
{{ render_field(form.auth_token, class="form-control") }}
|
{{ render_field(form.auth_token, class="form-control") }}
|
||||||
</div>
|
</div>
|
||||||
<div id="basicAuthInputs" style="display: none;">
|
|
||||||
{{ render_field(form.username, class="form-control mb-2", placeholder="Username") }}
|
|
||||||
{{ render_field(form.password, class="form-control", placeholder="Password", type="password") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# File Input #}
|
||||||
{{ render_field(form.test_data_file, class="form-control") }}
|
{{ render_field(form.test_data_file, class="form-control") }}
|
||||||
<small class="form-text text-muted">Select one or more .json, .xml files, or a single .zip file containing them.</small>
|
<small class="form-text text-muted">Select one or more .json, .xml files, or a single .zip file containing them.</small>
|
||||||
|
|
||||||
|
{# --- Validation Options --- #}
|
||||||
<div class="row g-3 mt-3 mb-3 align-items-center">
|
<div class="row g-3 mt-3 mb-3 align-items-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
{# Render BooleanField using the macro #}
|
||||||
{{ render_field(form.validate_before_upload) }}
|
{{ render_field(form.validate_before_upload) }}
|
||||||
<small class="form-text text-muted">It is suggested to not validate against more than 500 files</small>
|
<small class="form-text text-muted">It is suggested to not validate against more than 500 files</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6" id="validationPackageGroup" style="display: none;">
|
<div class="col-md-6" id="validationPackageGroup" style="display: none;">
|
||||||
|
{# Render SelectField using the macro #}
|
||||||
{{ render_field(form.validation_package_id, class="form-select") }}
|
{{ render_field(form.validation_package_id, class="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{# --- END Validation Options --- #}
|
||||||
|
|
||||||
|
{# Upload Mode/Error Handling/Conditional Row #}
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
{{ render_field(form.upload_mode, class="form-select") }}
|
{{ render_field(form.upload_mode, class="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 d-flex align-items-end">
|
{# --- Conditional Upload Checkbox --- #}
|
||||||
|
<div class="col-md-4 d-flex align-items-end"> {# Use flex alignment #}
|
||||||
{{ render_field(form.use_conditional_uploads) }}
|
{{ render_field(form.use_conditional_uploads) }}
|
||||||
</div>
|
</div>
|
||||||
|
{# --- END --- #}
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
{{ render_field(form.error_handling, class="form-select") }}
|
{{ render_field(form.error_handling, class="form-select") }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="uploadButton">
|
<button type="submit" class="btn btn-primary btn-lg w-100" id="uploadButton">
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
|
||||||
<i class="bi bi-arrow-up-circle me-2"></i>Upload and Process
|
<i class="bi bi-arrow-up-circle me-2"></i>Upload and Process
|
||||||
@ -84,26 +90,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Results Area #}
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="my-0 fw-normal"><i class="bi bi-terminal me-2"></i>Processing Log & Results</h4>
|
<h4 class="my-0 fw-normal"><i class="bi bi-terminal me-2"></i>Processing Log & Results</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
{# Live Console for Streaming Output #}
|
||||||
<div id="liveConsole" class="border p-3 rounded bg-dark text-light mb-3" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
|
<div id="liveConsole" class="border p-3 rounded bg-dark text-light mb-3" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">
|
||||||
<span class="text-muted">Processing output will appear here...</span>
|
<span class="text-muted">Processing output will appear here...</span>
|
||||||
</div>
|
</div>
|
||||||
|
{# Final Summary Report Area #}
|
||||||
<div id="uploadResponse" class="mt-3">
|
<div id="uploadResponse" class="mt-3">
|
||||||
|
{# Final summary message appears here #}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div> {# End Col #}
|
||||||
</div>
|
</div> {# End Row #}
|
||||||
|
</div> {# End Container #}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }} {# Include scripts from base.html #}
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const form = document.getElementById('uploadTestDataForm');
|
const form = document.getElementById('uploadTestDataForm');
|
||||||
@ -112,74 +123,64 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const liveConsole = document.getElementById('liveConsole');
|
const liveConsole = document.getElementById('liveConsole');
|
||||||
const responseDiv = document.getElementById('uploadResponse');
|
const responseDiv = document.getElementById('uploadResponse');
|
||||||
const authTypeSelect = document.getElementById('auth_type');
|
const authTypeSelect = document.getElementById('auth_type');
|
||||||
const authInputsGroup = document.getElementById('authInputsGroup');
|
const authTokenGroup = document.getElementById('authTokenGroup');
|
||||||
const bearerTokenInput = document.getElementById('bearerTokenInput');
|
|
||||||
const basicAuthInputs = document.getElementById('basicAuthInputs');
|
|
||||||
const authTokenInput = document.getElementById('auth_token');
|
const authTokenInput = document.getElementById('auth_token');
|
||||||
const usernameInput = document.getElementById('username');
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
const fileInput = document.getElementById('test_data_file');
|
const fileInput = document.getElementById('test_data_file');
|
||||||
const validateCheckbox = document.getElementById('validate_before_upload');
|
const validateCheckbox = document.getElementById('validate_before_upload');
|
||||||
const validationPackageGroup = document.getElementById('validationPackageGroup');
|
const validationPackageGroup = document.getElementById('validationPackageGroup');
|
||||||
const validationPackageSelect = document.getElementById('validation_package_id');
|
const validationPackageSelect = document.getElementById('validation_package_id');
|
||||||
const uploadModeSelect = document.getElementById('upload_mode');
|
const uploadModeSelect = document.getElementById('upload_mode'); // Get upload mode select
|
||||||
const conditionalUploadCheckbox = document.getElementById('use_conditional_uploads');
|
const conditionalUploadCheckbox = document.getElementById('use_conditional_uploads'); // Get conditional checkbox
|
||||||
|
|
||||||
|
// --- Helper: Sanitize text ---
|
||||||
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
const sanitizeText = (str) => str ? String(str).replace(/</g, "<").replace(/>/g, ">") : "";
|
||||||
|
|
||||||
if (authTypeSelect && authInputsGroup && bearerTokenInput && basicAuthInputs) {
|
// --- Event Listener: Show/Hide Auth Token ---
|
||||||
|
if (authTypeSelect && authTokenGroup) {
|
||||||
authTypeSelect.addEventListener('change', function() {
|
authTypeSelect.addEventListener('change', function() {
|
||||||
authInputsGroup.style.display = (this.value === 'bearerToken' || this.value === 'basic') ? 'block' : 'none';
|
authTokenGroup.style.display = this.value === 'bearerToken' ? 'block' : 'none';
|
||||||
bearerTokenInput.style.display = this.value === 'bearerToken' ? 'block' : 'none';
|
|
||||||
basicAuthInputs.style.display = this.value === 'basic' ? 'block' : 'none';
|
|
||||||
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
|
if (this.value !== 'bearerToken' && authTokenInput) authTokenInput.value = '';
|
||||||
if (this.value !== 'basic' && usernameInput) usernameInput.value = '';
|
|
||||||
if (this.value !== 'basic' && passwordInput) passwordInput.value = '';
|
|
||||||
});
|
});
|
||||||
authInputsGroup.style.display = (authTypeSelect.value === 'bearerToken' || authTypeSelect.value === 'basic') ? 'block' : 'none';
|
authTokenGroup.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none'; // Initial state
|
||||||
bearerTokenInput.style.display = authTypeSelect.value === 'bearerToken' ? 'block' : 'none';
|
} else { console.error("Auth elements not found."); }
|
||||||
basicAuthInputs.style.display = authTypeSelect.value === 'basic' ? 'block' : 'none';
|
|
||||||
} else {
|
|
||||||
console.error("Auth elements not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// --- Event Listener: Show/Hide Validation Package Dropdown ---
|
||||||
if (validateCheckbox && validationPackageGroup) {
|
if (validateCheckbox && validationPackageGroup) {
|
||||||
const toggleValidationPackage = () => {
|
const toggleValidationPackage = () => {
|
||||||
validationPackageGroup.style.display = validateCheckbox.checked ? 'block' : 'none';
|
validationPackageGroup.style.display = validateCheckbox.checked ? 'block' : 'none';
|
||||||
};
|
};
|
||||||
validateCheckbox.addEventListener('change', toggleValidationPackage);
|
validateCheckbox.addEventListener('change', toggleValidationPackage);
|
||||||
toggleValidationPackage();
|
toggleValidationPackage(); // Initial state
|
||||||
} else {
|
} else { console.error("Validation checkbox or package group not found."); }
|
||||||
console.error("Validation checkbox or package group not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// --- Event Listener: Enable/Disable Conditional Upload Checkbox ---
|
||||||
if (uploadModeSelect && conditionalUploadCheckbox) {
|
if (uploadModeSelect && conditionalUploadCheckbox) {
|
||||||
const toggleConditionalCheckbox = () => {
|
const toggleConditionalCheckbox = () => {
|
||||||
|
// Enable checkbox only if mode is 'individual'
|
||||||
conditionalUploadCheckbox.disabled = (uploadModeSelect.value !== 'individual');
|
conditionalUploadCheckbox.disabled = (uploadModeSelect.value !== 'individual');
|
||||||
|
// Optional: Uncheck if disabled
|
||||||
if (conditionalUploadCheckbox.disabled) {
|
if (conditionalUploadCheckbox.disabled) {
|
||||||
conditionalUploadCheckbox.checked = false;
|
conditionalUploadCheckbox.checked = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
uploadModeSelect.addEventListener('change', toggleConditionalCheckbox);
|
uploadModeSelect.addEventListener('change', toggleConditionalCheckbox);
|
||||||
toggleConditionalCheckbox();
|
toggleConditionalCheckbox(); // Initial state
|
||||||
} else {
|
} else { console.error("Upload mode select or conditional upload checkbox not found."); }
|
||||||
console.error("Upload mode select or conditional upload checkbox not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- Event Listener: Form Submission ---
|
||||||
if (form && uploadButton && spinner && liveConsole && responseDiv && fileInput && validateCheckbox && validationPackageSelect && conditionalUploadCheckbox) {
|
if (form && uploadButton && spinner && liveConsole && responseDiv && fileInput && validateCheckbox && validationPackageSelect && conditionalUploadCheckbox) {
|
||||||
form.addEventListener('submit', async function(event) {
|
form.addEventListener('submit', async function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log("Form submitted");
|
console.log("Form submitted");
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
if (!fileInput.files || fileInput.files.length === 0) { alert('Please select at least one file.'); return; }
|
if (!fileInput.files || fileInput.files.length === 0) { alert('Please select at least one file.'); return; }
|
||||||
const fhirServerUrl = document.getElementById('fhir_server_url').value.trim();
|
const fhirServerUrl = document.getElementById('fhir_server_url').value.trim();
|
||||||
if (!fhirServerUrl) { alert('Please enter the Target FHIR Server URL.'); return; }
|
if (!fhirServerUrl) { alert('Please enter the Target FHIR Server URL.'); return; }
|
||||||
if (validateCheckbox.checked && !validationPackageSelect.value) { alert('Please select a package for validation.'); return; }
|
if (validateCheckbox.checked && !validationPackageSelect.value) { alert('Please select a package for validation.'); return; }
|
||||||
if (authTypeSelect.value === 'basic') {
|
|
||||||
if (!usernameInput.value.trim()) { alert('Please enter a username for Basic Authentication.'); return; }
|
|
||||||
if (!passwordInput.value) { alert('Please enter a password for Basic Authentication.'); return; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// UI Updates
|
||||||
uploadButton.disabled = true;
|
uploadButton.disabled = true;
|
||||||
if(spinner) spinner.style.display = 'inline-block';
|
if(spinner) spinner.style.display = 'inline-block';
|
||||||
const uploadIcon = uploadButton.querySelector('i');
|
const uploadIcon = uploadButton.querySelector('i');
|
||||||
@ -187,30 +188,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting upload process...</div>`;
|
liveConsole.innerHTML = `<div>${new Date().toLocaleTimeString()} [INFO] Starting upload process...</div>`;
|
||||||
responseDiv.innerHTML = '';
|
responseDiv.innerHTML = '';
|
||||||
|
|
||||||
|
// Prepare FormData
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('fhir_server_url', fhirServerUrl);
|
formData.append('fhir_server_url', fhirServerUrl);
|
||||||
formData.append('auth_type', authTypeSelect.value);
|
formData.append('auth_type', authTypeSelect.value);
|
||||||
if (authTypeSelect.value === 'bearerToken' && authTokenInput) {
|
if (authTypeSelect.value === 'bearerToken' && authTokenInput) { formData.append('auth_token', authTokenInput.value); }
|
||||||
formData.append('auth_token', authTokenInput.value);
|
|
||||||
} else if (authTypeSelect.value === 'basic') {
|
|
||||||
formData.append('username', usernameInput.value);
|
|
||||||
formData.append('password', passwordInput.value);
|
|
||||||
}
|
|
||||||
formData.append('upload_mode', uploadModeSelect.value);
|
formData.append('upload_mode', uploadModeSelect.value);
|
||||||
formData.append('error_handling', document.getElementById('error_handling').value);
|
formData.append('error_handling', document.getElementById('error_handling').value);
|
||||||
formData.append('validate_before_upload', validateCheckbox.checked ? 'true' : 'false');
|
formData.append('validate_before_upload', validateCheckbox.checked ? 'true' : 'false');
|
||||||
if (validateCheckbox.checked) { formData.append('validation_package_id', validationPackageSelect.value); }
|
if (validateCheckbox.checked) { formData.append('validation_package_id', validationPackageSelect.value); }
|
||||||
formData.append('use_conditional_uploads', conditionalUploadCheckbox.checked ? 'true' : 'false');
|
formData.append('use_conditional_uploads', conditionalUploadCheckbox.checked ? 'true' : 'false'); // Add new field
|
||||||
|
|
||||||
for (let i = 0; i < fileInput.files.length; i++) {
|
// Append files
|
||||||
formData.append('test_data_files', fileInput.files[i]);
|
for (let i = 0; i < fileInput.files.length; i++) { formData.append('test_data_files', fileInput.files[i]); }
|
||||||
}
|
|
||||||
|
|
||||||
|
// CSRF token and API key
|
||||||
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
const csrfTokenInput = form.querySelector('input[name="csrf_token"]');
|
||||||
const csrfToken = csrfTokenInput ? csrfTokenInput.value : "";
|
const csrfToken = csrfTokenInput ? csrfTokenInput.value : "";
|
||||||
const internalApiKey = {{ api_key | default("") | tojson }};
|
const internalApiKey = {{ api_key | default("") | tojson }};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// --- API Call ---
|
||||||
const response = await fetch('/api/upload-test-data', {
|
const response = await fetch('/api/upload-test-data', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
|
headers: { 'Accept': 'application/x-ndjson', 'X-CSRFToken': csrfToken, 'X-API-Key': internalApiKey },
|
||||||
@ -224,6 +222,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
if (!response.body) { throw new Error("Response body missing."); }
|
if (!response.body) { throw new Error("Response body missing."); }
|
||||||
|
|
||||||
|
// --- Process Streaming Response ---
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
let buffer = '';
|
let buffer = '';
|
||||||
@ -239,6 +238,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
|
const data = JSON.parse(line); const timestamp = new Date().toLocaleTimeString();
|
||||||
let messageClass = 'text-info'; let prefix = '[INFO]';
|
let messageClass = 'text-info'; let prefix = '[INFO]';
|
||||||
|
|
||||||
|
// Handle message types including validation
|
||||||
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
|
if (data.type === 'error') { prefix = '[ERROR]'; messageClass = 'text-danger'; }
|
||||||
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
else if (data.type === 'success') { prefix = '[SUCCESS]'; messageClass = 'text-success'; }
|
||||||
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
|
else if (data.type === 'warning') { prefix = '[WARNING]'; messageClass = 'text-warning'; }
|
||||||
@ -248,12 +248,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
else if (data.type === 'validation_warning') { prefix = '[VALIDATION]'; messageClass = 'text-warning'; }
|
else if (data.type === 'validation_warning') { prefix = '[VALIDATION]'; messageClass = 'text-warning'; }
|
||||||
else if (data.type === 'validation_error') { prefix = '[VALIDATION]'; messageClass = 'text-danger'; }
|
else if (data.type === 'validation_error') { prefix = '[VALIDATION]'; messageClass = 'text-danger'; }
|
||||||
|
|
||||||
|
// Append to console
|
||||||
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
|
const messageDiv = document.createElement('div'); messageDiv.className = messageClass;
|
||||||
let messageText = sanitizeText(data.message) || '...';
|
let messageText = sanitizeText(data.message) || '...';
|
||||||
if (data.details) { messageText += ` <small>(${sanitizeText(data.details)})</small>`; }
|
if (data.details) { messageText += ` <small>(${sanitizeText(data.details)})</small>`; }
|
||||||
messageDiv.innerHTML = `${timestamp} ${prefix} ${messageText}`;
|
messageDiv.innerHTML = `${timestamp} ${prefix} ${messageText}`;
|
||||||
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
|
liveConsole.appendChild(messageDiv); liveConsole.scrollTop = liveConsole.scrollHeight;
|
||||||
|
|
||||||
|
// Handle final summary
|
||||||
if (data.type === 'complete' && data.data) {
|
if (data.type === 'complete' && data.data) {
|
||||||
const summary = data.data;
|
const summary = data.data;
|
||||||
let alertClass = 'alert-secondary';
|
let alertClass = 'alert-secondary';
|
||||||
@ -274,15 +276,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
${summary.errors?.length > 0 ? '<br><strong>Details:</strong><ul>' + summary.errors.map(e => `<li>${sanitizeText(e)}</li>`).join('') + '</ul>' : ''}
|
${summary.errors?.length > 0 ? '<br><strong>Details:</strong><ul>' + summary.errors.map(e => `<li>${sanitizeText(e)}</li>`).join('') + '</ul>' : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
} catch (parseError) { console.error('Stream parse error:', parseError, 'Line:', line); }
|
} catch (parseError) { console.error('Stream parse error:', parseError, 'Line:', line); /* ... log error to console ... */ }
|
||||||
}
|
} // end for line
|
||||||
}
|
} // end while
|
||||||
|
|
||||||
|
// Process final buffer (if needed)
|
||||||
if (buffer.trim()) {
|
if (buffer.trim()) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(buffer.trim());
|
const data = JSON.parse(buffer.trim());
|
||||||
if (data.type === 'complete' && data.data) { /* ... update summary ... */ }
|
if (data.type === 'complete' && data.data) { /* ... update summary ... */ }
|
||||||
} catch (parseError) { console.error('Final buffer parse error:', parseError); }
|
} catch (parseError) { console.error('Final buffer parse error:', parseError); /* ... log error to console ... */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -290,13 +293,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message)}</div>`;
|
responseDiv.innerHTML = `<div class="alert alert-danger mt-3"><strong>Error:</strong> ${sanitizeText(error.message)}</div>`;
|
||||||
const ts = new Date().toLocaleTimeString(); const errDiv = document.createElement('div'); errDiv.className = 'text-danger'; errDiv.textContent = `${ts} [CLIENT_ERROR] ${sanitizeText(error.message)}`; liveConsole.appendChild(errDiv);
|
const ts = new Date().toLocaleTimeString(); const errDiv = document.createElement('div'); errDiv.className = 'text-danger'; errDiv.textContent = `${ts} [CLIENT_ERROR] ${sanitizeText(error.message)}`; liveConsole.appendChild(errDiv);
|
||||||
} finally {
|
} finally {
|
||||||
|
// Re-enable button
|
||||||
uploadButton.disabled = false;
|
uploadButton.disabled = false;
|
||||||
if(spinner) spinner.style.display = 'none';
|
if(spinner) spinner.style.display = 'none';
|
||||||
const uploadIcon = uploadButton.querySelector('i');
|
const uploadIcon = uploadButton.querySelector('i');
|
||||||
if (uploadIcon) uploadIcon.style.display = 'inline-block';
|
if (uploadIcon) uploadIcon.style.display = 'inline-block'; // Show icon
|
||||||
}
|
}
|
||||||
});
|
}); // End submit listener
|
||||||
} else { console.error("Could not find all required elements for form submission."); }
|
} else { console.error("Could not find all required elements for form submission."); }
|
||||||
});
|
|
||||||
|
}); // End DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
<!-- templates/validate_sample.html -->
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -6,15 +7,14 @@
|
|||||||
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
|
<h1 class="display-5 fw-bold text-body-emphasis">Validate FHIR Sample</h1>
|
||||||
<div class="col-lg-6 mx-auto">
|
<div class="col-lg-6 mx-auto">
|
||||||
<p class="lead mb-4">
|
<p class="lead mb-4">
|
||||||
Validate a FHIR resource or bundle against a selected Implementation Guide.
|
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>
|
</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>
|
</div>
|
||||||
</div>
|
----------------------------------------------------------------remove the buttons----------------------------------------------------->
|
||||||
|
|
||||||
<div id="spinner-overlay" class="d-none position-fixed top-0 start-0 w-100 h-100 bg-dark bg-opacity-50 d-flex align-items-center justify-content-center" style="z-index: 1050;">
|
|
||||||
<div class="text-center text-white">
|
|
||||||
<div id="spinner-animation" style="width: 200px; height: 200px;"></div>
|
|
||||||
<p class="mt-3" id="spinner-message">Waiting, don't leave this page...</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -22,9 +22,8 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Validation Form</div>
|
<div class="card-header">Validation Form</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="validationForm" method="POST" action="/api/validate">
|
<form id="validationForm">
|
||||||
{{ form.csrf_token }}
|
{{ form.hidden_tag() }}
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<small class="form-text text-muted">
|
<small class="form-text text-muted">
|
||||||
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
|
||||||
@ -47,7 +46,6 @@
|
|||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
|
||||||
{{ form.version(class="form-control", id=form.version.id) }}
|
{{ form.version(class="form-control", id=form.version.id) }}
|
||||||
@ -55,195 +53,131 @@
|
|||||||
<div class="text-danger">{{ error }}</div>
|
<div class="text-danger">{{ error }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
|
||||||
|
|
||||||
<!-- New Fields from ValidationForm -->
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="fhir_resource" class="form-label">FHIR Resource (JSON or XML)</label>
|
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
|
||||||
{{ form.fhir_resource(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="fhir_version" class="form-label">FHIR Version</label>
|
|
||||||
{{ form.fhir_version(class="form-select") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-4">
|
|
||||||
|
|
||||||
<h5>Validation Flags</h5>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
{{ form.do_native(class="form-check-input") }}
|
{{ form.include_dependencies(class="form-check-input") }}
|
||||||
<label class="form-check-label" for="do_native">{{ form.do_native.label }}</label>
|
{{ form.include_dependencies.label(class="form-check-label") }}
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
{{ form.hint_about_must_support(class="form-check-input") }}
|
|
||||||
<label class="form-check-label" for="hint_about_must_support">{{ form.hint_about_must_support.label }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
{{ form.assume_valid_rest_references(class="form-check-input") }}
|
|
||||||
<label class="form-check-label" for="assume_valid_rest_references">{{ form.assume_valid_rest_references.label }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
{{ form.no_extensible_binding_warnings(class="form-check-input") }}
|
|
||||||
<label class="form-check-label" for="no_extensible_binding_warnings">{{ form.no_extensible_binding_warnings.label }}</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
|
||||||
{{ form.show_times(class="form-check-input") }}
|
{{ form.mode(class="form-select") }}
|
||||||
<label class="form-check-label" for="show_times">{{ form.show_times.label }}</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check">
|
<div class="mb-3">
|
||||||
{{ form.allow_example_urls(class="form-check-input") }}
|
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
|
||||||
<label class="form-check-label" for="allow_example_urls">{{ form.allow_example_urls.label }}</label>
|
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
|
||||||
</div>
|
{% for error in form.sample_input.errors %}
|
||||||
<div class="form-check">
|
<div class="text-danger">{{ error }}</div>
|
||||||
{{ form.check_ips_codes(class="form-check-input") }}
|
{% endfor %}
|
||||||
<label class="form-check-label" for="check_ips_codes">{{ form.check_ips_codes.label }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
{{ form.allow_any_extensions(class="form-check-input") }}
|
|
||||||
<label class="form-check-label" for="allow_any_extensions">{{ form.allow_any_extensions.label }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
{{ form.tx_routing(class="form-check-input") }}
|
|
||||||
<label class="form-check-label" for="tx_routing">{{ form.tx_routing.label }}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="my-4">
|
|
||||||
|
|
||||||
<h5>Other Settings</h5>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="profiles" class="form-label">{{ form.profiles.label }}</label>
|
|
||||||
{{ form.profiles(class="form-control") }}
|
|
||||||
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="extensions" class="form-label">{{ form.extensions.label }}</label>
|
|
||||||
{{ form.extensions(class="form-control") }}
|
|
||||||
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="terminology_server" class="form-label">{{ form.terminology_server.label }}</label>
|
|
||||||
{{ form.terminology_server(class="form-control") }}
|
|
||||||
<small class="form-text text-muted">e.g., http://tx.fhir.org</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="snomed_ct_version" class="form-label">{{ form.snomed_ct_version.label }}</label>
|
|
||||||
{{ form.snomed_ct_version(class="form-select") }}
|
|
||||||
<small class="form-text text-muted">Select a SNOMED CT edition.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 text-center">
|
|
||||||
<button type="submit" id="validateButton" class="btn btn-primary btn-lg">
|
|
||||||
Validate Resource
|
|
||||||
</button>
|
|
||||||
<button type="button" id="clearResultsButton" class="btn btn-secondary btn-lg d-none">
|
|
||||||
Clear Results
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="validationResult" class="card mt-4" style="display: none;">
|
<div class="card mt-4" id="validationResult" style="display: none;">
|
||||||
<div class="card-header">Validation Results</div>
|
<div class="card-header">Validation Report</div>
|
||||||
<div class="card-body" id="validationContent">
|
<div class="card-body" id="validationContent">
|
||||||
<!-- Results will be displayed here -->
|
<!-- Results will be populated dynamically -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/lottie.min.js') }}"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// --- Global Variables and Helper Functions ---
|
|
||||||
let currentLottieAnimation = null;
|
|
||||||
let pollInterval = null;
|
|
||||||
const spinnerOverlay = document.getElementById('spinner-overlay');
|
|
||||||
const spinnerMessage = document.getElementById('spinner-message');
|
|
||||||
const spinnerContainer = document.getElementById('spinner-animation');
|
|
||||||
const perPage = 10;
|
|
||||||
|
|
||||||
// Function to load the Lottie animation based on theme
|
|
||||||
function loadLottieAnimation(theme) {
|
|
||||||
if (currentLottieAnimation) {
|
|
||||||
currentLottieAnimation.destroy();
|
|
||||||
currentLottieAnimation = null;
|
|
||||||
}
|
|
||||||
const animationPath = (theme === 'dark')
|
|
||||||
? '{{ url_for('static', filename='animations/Validation abstract.json') }}'
|
|
||||||
: '{{ url_for('static', filename='animations/Validation abstract.json') }}';
|
|
||||||
|
|
||||||
try {
|
|
||||||
currentLottieAnimation = lottie.loadAnimation({
|
|
||||||
container: spinnerContainer,
|
|
||||||
renderer: 'svg',
|
|
||||||
loop: true,
|
|
||||||
autoplay: true,
|
|
||||||
path: animationPath
|
|
||||||
});
|
|
||||||
} catch(lottieError) {
|
|
||||||
console.error("Error loading Lottie animation:", lottieError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to show/hide the spinner with a custom message
|
|
||||||
function toggleSpinner(show, message = "Running validator...") {
|
|
||||||
if (show) {
|
|
||||||
spinnerMessage.textContent = message;
|
|
||||||
spinnerOverlay.classList.remove('d-none');
|
|
||||||
const currentTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
|
||||||
loadLottieAnimation(currentTheme);
|
|
||||||
} else {
|
|
||||||
spinnerOverlay.classList.add('d-none');
|
|
||||||
if (currentLottieAnimation) {
|
|
||||||
currentLottieAnimation.destroy();
|
|
||||||
currentLottieAnimation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Frontend logic starts here
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const form = document.getElementById('validationForm');
|
|
||||||
const validateButton = document.getElementById('validateButton');
|
|
||||||
const clearResultsButton = document.getElementById('clearResultsButton');
|
|
||||||
const validationResult = document.getElementById('validationResult');
|
|
||||||
const validationContent = document.getElementById('validationContent');
|
|
||||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
|
||||||
const packageSelect = document.getElementById('packageSelect');
|
const packageSelect = document.getElementById('packageSelect');
|
||||||
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
|
||||||
const versionInput = document.getElementById('{{ form.version.id }}');
|
const versionInput = document.getElementById('{{ form.version.id }}');
|
||||||
const fhirResourceInput = document.getElementById('fhir_resource');
|
const validateButton = document.getElementById('validateButton');
|
||||||
|
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
|
||||||
|
const validationResult = document.getElementById('validationResult');
|
||||||
|
const validationContent = document.getElementById('validationContent');
|
||||||
|
|
||||||
// Function to update package fields when a package is selected from the dropdown
|
// 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) {
|
function updatePackageFields(select) {
|
||||||
const value = select.value;
|
const value = select.value;
|
||||||
if (!packageNameInput || !versionInput) return;
|
console.log('Selected package:', value);
|
||||||
|
if (!packageNameInput || !versionInput) {
|
||||||
|
console.error('Input fields not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (value) {
|
if (value) {
|
||||||
const [name, version] = value.split('#');
|
const [name, version] = value.split('#');
|
||||||
if (name && version) {
|
if (name && version) {
|
||||||
packageNameInput.value = name;
|
packageNameInput.value = name;
|
||||||
versionInput.value = version;
|
versionInput.value = version;
|
||||||
|
console.log('Updated fields:', { packageName: name, version });
|
||||||
} else {
|
} else {
|
||||||
|
console.warn('Invalid package format:', value);
|
||||||
packageNameInput.value = '';
|
packageNameInput.value = '';
|
||||||
versionInput.value = '';
|
versionInput.value = '';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
packageNameInput.value = '';
|
packageNameInput.value = '';
|
||||||
versionInput.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
|
// Sanitize text to prevent XSS
|
||||||
function sanitizeText(text) {
|
function sanitizeText(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
@ -251,109 +185,126 @@
|
|||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tidy up the display by paginating results
|
// Validate button handler
|
||||||
function renderResultList(container, items, title, isWarnings = false) {
|
if (validateButton) {
|
||||||
container.innerHTML = '';
|
validateButton.addEventListener('click', function() {
|
||||||
if (items.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrapper = document.createElement('div');
|
if (!sampleData) {
|
||||||
const listId = `list-${title.toLowerCase()}`;
|
validationResult.style.display = 'block';
|
||||||
|
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
|
||||||
let initialItems = items.slice(0, perPage);
|
return;
|
||||||
let remainingItems = items.slice(perPage);
|
|
||||||
|
|
||||||
const titleElement = document.createElement('h5');
|
|
||||||
titleElement.textContent = title;
|
|
||||||
wrapper.appendChild(titleElement);
|
|
||||||
|
|
||||||
const ul = document.createElement('ul');
|
|
||||||
ul.className = 'list-unstyled';
|
|
||||||
initialItems.forEach(item => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.innerHTML = `
|
|
||||||
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
|
||||||
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
|
||||||
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
ul.appendChild(li);
|
|
||||||
});
|
|
||||||
wrapper.appendChild(ul);
|
|
||||||
|
|
||||||
if (remainingItems.length > 0) {
|
|
||||||
const toggleButton = document.createElement('a');
|
|
||||||
toggleButton.href = '#';
|
|
||||||
toggleButton.className = 'btn btn-sm btn-link text-decoration-none';
|
|
||||||
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
|
|
||||||
|
|
||||||
const remainingUl = document.createElement('ul');
|
|
||||||
remainingUl.className = 'list-unstyled d-none';
|
|
||||||
remainingItems.forEach(item => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.innerHTML = `
|
|
||||||
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
|
|
||||||
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
|
|
||||||
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
remainingUl.appendChild(li);
|
|
||||||
});
|
|
||||||
|
|
||||||
toggleButton.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (remainingUl.classList.contains('d-none')) {
|
|
||||||
remainingUl.classList.remove('d-none');
|
|
||||||
toggleButton.textContent = 'Show Less';
|
|
||||||
} else {
|
|
||||||
remainingUl.classList.add('d-none');
|
|
||||||
toggleButton.textContent = `Show More (${remainingItems.length} more)`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
wrapper.appendChild(toggleButton);
|
|
||||||
wrapper.appendChild(remainingUl);
|
|
||||||
}
|
|
||||||
container.appendChild(wrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayFinalReport(data) {
|
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 = '';
|
validationContent.innerHTML = '';
|
||||||
let hasContent = false;
|
let hasContent = false;
|
||||||
|
|
||||||
clearResultsButton.classList.remove('d-none');
|
// Display profile information
|
||||||
|
if (data.results) {
|
||||||
if (data.file_paths) {
|
const profileDiv = document.createElement('div');
|
||||||
// Use the paths returned from the server to create download links
|
profileDiv.className = 'mb-3';
|
||||||
const jsonFileName = data.file_paths.json.split('/').pop();
|
const profiles = new Set();
|
||||||
const htmlFileName = data.file_paths.html.split('/').pop();
|
Object.values(data.results).forEach(result => {
|
||||||
const downloadDiv = document.createElement('div');
|
if (result.profile) profiles.add(result.profile);
|
||||||
downloadDiv.className = 'alert alert-info d-flex justify-content-between align-items-center';
|
});
|
||||||
downloadDiv.innerHTML = `
|
if (profiles.size > 0) {
|
||||||
<span>Download Raw Reports:</span>
|
profileDiv.innerHTML = `
|
||||||
<div>
|
<h5>Validated Against Profile${profiles.size > 1 ? 's' : ''}</h5>
|
||||||
<a href="/download/${jsonFileName}" class="btn btn-sm btn-secondary me-2" download>Download JSON</a>
|
<ul>
|
||||||
<a href="/download/${htmlFileName}" class="btn btn-sm btn-secondary" download>Download HTML</a>
|
${Array.from(profiles).map(p => `<li><a href="${sanitizeText(p)}" target="_blank">${sanitizeText(p.split('/').pop())}</a></li>`).join('')}
|
||||||
</div>
|
</ul>
|
||||||
`;
|
`;
|
||||||
validationContent.appendChild(downloadDiv);
|
validationContent.appendChild(profileDiv);
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display errors
|
||||||
if (data.errors && data.errors.length > 0) {
|
if (data.errors && data.errors.length > 0) {
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
const errorContainer = document.createElement('div');
|
const errorDiv = document.createElement('div');
|
||||||
renderResultList(errorContainer, data.errors, 'Errors');
|
errorDiv.className = 'alert alert-danger';
|
||||||
validationContent.appendChild(errorContainer);
|
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) {
|
if (data.warnings && data.warnings.length > 0) {
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
const warningContainer = document.createElement('div');
|
const warningDiv = document.createElement('div');
|
||||||
renderResultList(warningContainer, data.warnings, 'Warnings', true);
|
warningDiv.className = 'alert alert-warning';
|
||||||
validationContent.appendChild(warningContainer);
|
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) {
|
if (data.results) {
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
const resultsDiv = document.createElement('div');
|
const resultsDiv = document.createElement('div');
|
||||||
@ -387,7 +338,7 @@
|
|||||||
validationContent.appendChild(resultsDiv);
|
validationContent.appendChild(resultsDiv);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.summary) {
|
// Summary
|
||||||
const summaryDiv = document.createElement('div');
|
const summaryDiv = document.createElement('div');
|
||||||
summaryDiv.className = 'alert alert-info';
|
summaryDiv.className = 'alert alert-info';
|
||||||
summaryDiv.innerHTML = `
|
summaryDiv.innerHTML = `
|
||||||
@ -397,12 +348,12 @@
|
|||||||
`;
|
`;
|
||||||
validationContent.appendChild(summaryDiv);
|
validationContent.appendChild(summaryDiv);
|
||||||
hasContent = true;
|
hasContent = true;
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasContent && data.valid) {
|
if (!hasContent && data.valid) {
|
||||||
validationContent.innerHTML = '<div class="alert alert-success">Validation passed with no errors or warnings.</div>';
|
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 => {
|
document.querySelectorAll('[data-bs-toggle="collapse"]').forEach(el => {
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', () => {
|
||||||
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
const target = document.querySelector(el.getAttribute('data-bs-target'));
|
||||||
@ -412,174 +363,12 @@
|
|||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||||||
new bootstrap.Tooltip(el);
|
new bootstrap.Tooltip(el);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidationStatus(taskId) {
|
|
||||||
fetch(`/api/check-validation-status/${taskId}`)
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 202) {
|
|
||||||
console.log('Validation still in progress...');
|
|
||||||
return null;
|
|
||||||
} else if (response.status === 200) {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
toggleSpinner(false);
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
toggleSpinner(false);
|
|
||||||
return response.json().then(errorData => {
|
|
||||||
throw new Error(errorData.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data && data.status === 'complete') {
|
|
||||||
validationResult.style.display = 'block';
|
|
||||||
displayFinalReport(data.result);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
clearInterval(pollInterval);
|
console.error('Validation error:', error);
|
||||||
toggleSpinner(false);
|
validationContent.innerHTML = `<div class="alert alert-danger">Error during validation: ${sanitizeText(error.message)}</div>`;
|
||||||
console.error('Validation failed during polling:', error);
|
|
||||||
validationResult.style.display = 'block';
|
|
||||||
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function runBlockingValidation(e) {
|
|
||||||
e.preventDefault(); // This is the crucial line to prevent default form submission
|
|
||||||
const packageName = document.getElementById('{{ form.package_name.id }}').value;
|
|
||||||
const version = document.getElementById('{{ form.version.id }}').value;
|
|
||||||
const sampleData = document.getElementById('fhir_resource').value.trim();
|
|
||||||
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
|
|
||||||
|
|
||||||
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 or XML.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
|
|
||||||
|
|
||||||
// Collect all form data including the new fields
|
|
||||||
const data = {
|
|
||||||
package_name: packageName,
|
|
||||||
version: version,
|
|
||||||
fhir_resource: sampleData,
|
|
||||||
fhir_version: document.getElementById('fhir_version').value,
|
|
||||||
do_native: document.getElementById('do_native').checked,
|
|
||||||
hint_about_must_support: document.getElementById('hint_about_must_support').checked,
|
|
||||||
assume_valid_rest_references: document.getElementById('assume_valid_rest_references').checked,
|
|
||||||
no_extensible_binding_warnings: document.getElementById('no_extensible_binding_warnings').checked,
|
|
||||||
show_times: document.getElementById('show_times').checked,
|
|
||||||
allow_example_urls: document.getElementById('allow_example_urls').checked,
|
|
||||||
check_ips_codes: document.getElementById('check_ips_codes').checked,
|
|
||||||
allow_any_extensions: document.getElementById('allow_any_extensions').checked,
|
|
||||||
tx_routing: document.getElementById('tx_routing').checked,
|
|
||||||
snomed_ct_version: document.getElementById('snomed_ct_version').value,
|
|
||||||
profiles: document.getElementById('profiles').value,
|
|
||||||
extensions: document.getElementById('extensions').value,
|
|
||||||
terminology_server: document.getElementById('terminology_server').value
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/validate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (response.status === 202) {
|
|
||||||
return response.json();
|
|
||||||
} else {
|
|
||||||
toggleSpinner(false);
|
|
||||||
return response.json().then(errorData => {
|
|
||||||
throw new Error(errorData.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'accepted' && data.task_id) {
|
|
||||||
pollInterval = setInterval(() => checkValidationStatus(data.task_id), 3000);
|
|
||||||
} else {
|
|
||||||
toggleSpinner(false);
|
|
||||||
validationResult.style.display = 'block';
|
|
||||||
validationContent.innerHTML = `<div class="alert alert-danger">Server response format unexpected.</div>`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
clearInterval(pollInterval);
|
|
||||||
toggleSpinner(false);
|
|
||||||
console.error('Validation failed:', error);
|
|
||||||
validationResult.style.display = 'block';
|
|
||||||
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
clearResultsButton.addEventListener('click', function() {
|
|
||||||
fetch('/api/clear-validation-results', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'X-CSRFToken': csrfToken
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.status === 'success') {
|
|
||||||
validationResult.style.display = 'none';
|
|
||||||
clearResultsButton.classList.add('d-none');
|
|
||||||
} else {
|
|
||||||
console.error('Failed to clear results:', data.message);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Network error while clearing results:', error);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (packageSelect) {
|
|
||||||
packageSelect.addEventListener('change', function() {
|
|
||||||
updatePackageFields(this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (validateButton) {
|
|
||||||
validateButton.addEventListener('click', function(e) {
|
|
||||||
runBlockingValidation(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (packageNameInput.value && versionInput.value) {
|
|
||||||
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
|
|
||||||
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
|
|
||||||
if (optionExists) {
|
|
||||||
packageSelect.value = currentValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fhirResourceInput) {
|
|
||||||
fhirResourceInput.addEventListener('input', function() {
|
|
||||||
try {
|
|
||||||
if (this.value.trim()) {
|
|
||||||
JSON.parse(this.value);
|
|
||||||
this.classList.remove('is-invalid');
|
|
||||||
this.classList.add('is-valid');
|
|
||||||
} else {
|
|
||||||
this.classList.remove('is-valid', 'is-invalid');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.classList.remove('is-valid');
|
|
||||||
this.classList.add('is-invalid');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import tarfile
|
|||||||
import shutil
|
import shutil
|
||||||
import io
|
import io
|
||||||
import requests
|
import requests
|
||||||
import time
|
|
||||||
import subprocess
|
|
||||||
from unittest.mock import patch, MagicMock, mock_open, call
|
from unittest.mock import patch, MagicMock, mock_open, call
|
||||||
from flask import Flask, session
|
from flask import Flask, session
|
||||||
from flask.testing import FlaskClient
|
from flask.testing import FlaskClient
|
||||||
@ -29,252 +27,9 @@ def parse_ndjson(byte_stream):
|
|||||||
lines = decoded_stream.split('\n')
|
lines = decoded_stream.split('\n')
|
||||||
return [json.loads(line) for line in lines if line.strip()]
|
return [json.loads(line) for line in lines if line.strip()]
|
||||||
|
|
||||||
class DockerComposeContainer:
|
|
||||||
"""
|
|
||||||
A class that follows the Testcontainers pattern for managing Docker Compose environments.
|
|
||||||
This implementation uses subprocess to call docker-compose directly since we're not
|
|
||||||
installing the testcontainers-python package.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, compose_file_path):
|
|
||||||
"""
|
|
||||||
Initialize with the path to the docker-compose.yml file
|
|
||||||
|
|
||||||
Args:
|
|
||||||
compose_file_path: Path to the docker-compose.yml file
|
|
||||||
"""
|
|
||||||
self.compose_file = compose_file_path
|
|
||||||
self.compose_dir = os.path.dirname(os.path.abspath(compose_file_path))
|
|
||||||
self.containers_up = False
|
|
||||||
self.service_ports = {}
|
|
||||||
self._container_ids = {}
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
"""Start containers when entering context"""
|
|
||||||
self.start()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
"""Stop containers when exiting context"""
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
def with_service_port(self, service_name, port):
|
|
||||||
"""
|
|
||||||
Map a service port (following the testcontainers builder pattern)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service_name: Name of the service in docker-compose.yml
|
|
||||||
port: Port number to expose
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self for chaining
|
|
||||||
"""
|
|
||||||
self.service_ports[service_name] = port
|
|
||||||
return self
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""Start the Docker Compose environment"""
|
|
||||||
if self.containers_up:
|
|
||||||
return self
|
|
||||||
|
|
||||||
print("Starting Docker Compose environment...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['docker-compose', '-f', self.compose_file, 'up', '-d'],
|
|
||||||
cwd=self.compose_dir,
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
error_msg = f"Failed to start Docker Compose environment: {result.stderr}"
|
|
||||||
print(error_msg)
|
|
||||||
raise RuntimeError(error_msg)
|
|
||||||
|
|
||||||
# Store container IDs for later use
|
|
||||||
self._get_container_ids()
|
|
||||||
|
|
||||||
self.containers_up = True
|
|
||||||
self._wait_for_services()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _get_container_ids(self):
|
|
||||||
"""Get the container IDs for all services"""
|
|
||||||
result = subprocess.run(
|
|
||||||
['docker-compose', '-f', self.compose_file, 'ps', '-q'],
|
|
||||||
cwd=self.compose_dir,
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
container_ids = result.stdout.strip().split('\n')
|
|
||||||
if not container_ids:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get service names for each container
|
|
||||||
for container_id in container_ids:
|
|
||||||
if not container_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
inspect_result = subprocess.run(
|
|
||||||
['docker', 'inspect', '--format', '{{index .Config.Labels "com.docker.compose.service"}}', container_id],
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if inspect_result.returncode == 0:
|
|
||||||
service_name = inspect_result.stdout.strip()
|
|
||||||
self._container_ids[service_name] = container_id
|
|
||||||
|
|
||||||
def get_container_id(self, service_name):
|
|
||||||
"""
|
|
||||||
Get the container ID for a specific service
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service_name: Name of the service in docker-compose.yml
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Container ID as string or None if not found
|
|
||||||
"""
|
|
||||||
return self._container_ids.get(service_name)
|
|
||||||
|
|
||||||
def get_service_host(self, service_name):
|
|
||||||
"""
|
|
||||||
Get the host for a specific service - for Docker Compose we just use localhost
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service_name: Name of the service in docker-compose.yml
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Host as string (usually localhost)
|
|
||||||
"""
|
|
||||||
return "localhost"
|
|
||||||
|
|
||||||
def get_service_url(self, service_name, path=""):
|
|
||||||
"""
|
|
||||||
Get the URL for a specific service
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service_name: Name of the service in docker-compose.yml
|
|
||||||
path: Optional path to append to the URL
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
URL as string
|
|
||||||
"""
|
|
||||||
port = self.service_ports.get(service_name)
|
|
||||||
if not port:
|
|
||||||
raise ValueError(f"No port mapping defined for service {service_name}")
|
|
||||||
|
|
||||||
url = f"http://{self.get_service_host(service_name)}:{port}"
|
|
||||||
if path:
|
|
||||||
# Ensure path starts with /
|
|
||||||
if not path.startswith('/'):
|
|
||||||
path = f"/{path}"
|
|
||||||
url = f"{url}{path}"
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
def get_logs(self, service_name):
|
|
||||||
"""
|
|
||||||
Get logs for a specific service
|
|
||||||
|
|
||||||
Args:
|
|
||||||
service_name: Name of the service in docker-compose.yml
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Logs as string
|
|
||||||
"""
|
|
||||||
container_id = self.get_container_id(service_name)
|
|
||||||
if not container_id:
|
|
||||||
return f"No container found for service {service_name}"
|
|
||||||
|
|
||||||
result = subprocess.run(
|
|
||||||
['docker', 'logs', container_id],
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return result.stdout
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""Stop the Docker Compose environment"""
|
|
||||||
if not self.containers_up:
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Stopping Docker Compose environment...")
|
|
||||||
result = subprocess.run(
|
|
||||||
['docker-compose', '-f', self.compose_file, 'down'],
|
|
||||||
cwd=self.compose_dir,
|
|
||||||
capture_output=True,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"Warning: Error stopping Docker Compose: {result.stderr}")
|
|
||||||
|
|
||||||
self.containers_up = False
|
|
||||||
|
|
||||||
def _wait_for_services(self):
|
|
||||||
"""Wait for all services to be ready"""
|
|
||||||
print("Waiting for services to be ready...")
|
|
||||||
|
|
||||||
# Wait for HAPI FHIR server
|
|
||||||
if 'fhir' in self.service_ports:
|
|
||||||
self._wait_for_http_service(
|
|
||||||
self.get_service_url('fhir', 'fhir/metadata'),
|
|
||||||
"HAPI FHIR server"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Wait for FHIRFLARE application
|
|
||||||
if 'fhirflare' in self.service_ports:
|
|
||||||
self._wait_for_http_service(
|
|
||||||
self.get_service_url('fhirflare'),
|
|
||||||
"FHIRFLARE application"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Give additional time for services to stabilize
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
def _wait_for_http_service(self, url, service_name, max_retries=30, retry_interval=2):
|
|
||||||
"""
|
|
||||||
Wait for an HTTP service to be ready
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: URL to check
|
|
||||||
service_name: Name of the service for logging
|
|
||||||
max_retries: Maximum number of retries
|
|
||||||
retry_interval: Interval between retries in seconds
|
|
||||||
"""
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
response = requests.get(url, timeout=5)
|
|
||||||
if response.status_code == 200:
|
|
||||||
print(f"{service_name} is ready after {attempt + 1} attempts")
|
|
||||||
return True
|
|
||||||
except requests.RequestException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
print(f"Waiting for {service_name} (attempt {attempt + 1}/{max_retries})...")
|
|
||||||
time.sleep(retry_interval)
|
|
||||||
|
|
||||||
print(f"Warning: {service_name} did not become ready in time")
|
|
||||||
return False
|
|
||||||
|
|
||||||
class TestFHIRFlareIGToolkit(unittest.TestCase):
|
class TestFHIRFlareIGToolkit(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
# Define the Docker Compose container
|
|
||||||
compose_file_path = os.path.join(os.path.dirname(__file__), 'docker-compose.yml')
|
|
||||||
cls.container = DockerComposeContainer(compose_file_path) \
|
|
||||||
.with_service_port('fhir', 8080) \
|
|
||||||
.with_service_port('fhirflare', 5000)
|
|
||||||
|
|
||||||
# Start the containers
|
|
||||||
cls.container.start()
|
|
||||||
|
|
||||||
# Configure app for testing
|
|
||||||
app.config['TESTING'] = True
|
app.config['TESTING'] = True
|
||||||
app.config['WTF_CSRF_ENABLED'] = False
|
app.config['WTF_CSRF_ENABLED'] = False
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
||||||
@ -284,7 +39,6 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
|||||||
app.config['API_KEY'] = 'test-api-key'
|
app.config['API_KEY'] = 'test-api-key'
|
||||||
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
app.config['VALIDATE_IMPOSED_PROFILES'] = True
|
||||||
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
|
||||||
app.config['HAPI_FHIR_URL'] = cls.container.get_service_url('fhir', 'fhir') # Point to containerized HAPI FHIR
|
|
||||||
|
|
||||||
cls.app_context = app.app_context()
|
cls.app_context = app.app_context()
|
||||||
cls.app_context.push()
|
cls.app_context.push()
|
||||||
@ -297,9 +51,6 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
|||||||
if os.path.exists(cls.test_packages_dir):
|
if os.path.exists(cls.test_packages_dir):
|
||||||
shutil.rmtree(cls.test_packages_dir)
|
shutil.rmtree(cls.test_packages_dir)
|
||||||
|
|
||||||
# Stop Docker Compose environment
|
|
||||||
cls.container.stop()
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if os.path.exists(self.test_packages_dir):
|
if os.path.exists(self.test_packages_dir):
|
||||||
shutil.rmtree(self.test_packages_dir)
|
shutil.rmtree(self.test_packages_dir)
|
||||||
@ -345,101 +96,209 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
|||||||
with patch('fhirpath.evaluate', side_effect=Exception("fhirpath error")):
|
with patch('fhirpath.evaluate', side_effect=Exception("fhirpath error")):
|
||||||
self.assertEqual(services.navigate_fhir_path(resource, "Patient.name[0].given"), ["John"])
|
self.assertEqual(services.navigate_fhir_path(resource, "Patient.name[0].given"), ["John"])
|
||||||
|
|
||||||
|
def test_02_render_node_as_li(self):
|
||||||
|
node = {
|
||||||
|
"element": {"path": "Patient.identifier", "id": "Patient.identifier", "sliceName": "us-ssn", "min": 0, "max": "*", "type": [{"code": "Identifier"}]},
|
||||||
|
"name": "identifier",
|
||||||
|
"children": {}
|
||||||
|
}
|
||||||
|
must_support_paths = {"Patient.identifier:us-ssn"}
|
||||||
|
with app.app_context:
|
||||||
|
html = render_template('cp_view_processed_ig.html', processed_ig=MagicMock(must_support_elements={"USCorePatientProfile": ["Patient.identifier:us-ssn"]}), profile_list=[{"name": "USCorePatientProfile"}], base_list=[])
|
||||||
|
self.assertIn("identifier:us-ssn", html)
|
||||||
|
self.assertIn("list-group-item-warning", html)
|
||||||
|
self.assertIn("Must Support (Slice: us-ssn)", html)
|
||||||
|
|
||||||
# --- Basic Page Rendering Tests ---
|
# --- Basic Page Rendering Tests ---
|
||||||
|
|
||||||
def test_03_homepage(self):
|
def test_03_homepage(self):
|
||||||
# Connect to the containerized application
|
response = self.client.get('/')
|
||||||
response = requests.get(self.container.get_service_url('fhirflare'))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn('FHIRFLARE IG Toolkit', response.text)
|
self.assertIn(b'FHIRFLARE IG Toolkit', response.data)
|
||||||
|
|
||||||
def test_04_import_ig_page(self):
|
def test_04_import_ig_page(self):
|
||||||
response = requests.get(self.container.get_service_url('fhirflare', 'import-ig'))
|
response = self.client.get('/import-ig')
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertIn('Import IG', response.text)
|
self.assertIn(b'Import IG', response.data)
|
||||||
self.assertIn('Package Name', response.text)
|
self.assertIn(b'Package Name', response.data)
|
||||||
self.assertIn('Package Version', response.text)
|
self.assertIn(b'Package Version', response.data)
|
||||||
self.assertIn('name="dependency_mode"', response.text)
|
self.assertIn(b'name="dependency_mode"', response.data)
|
||||||
|
|
||||||
# --- API Integration Tests ---
|
@patch('app.list_downloaded_packages', return_value=([], [], {}))
|
||||||
|
def test_05_view_igs_no_packages(self, mock_list_pkgs):
|
||||||
|
response = self.client.get('/view-igs')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotIn(b'<th>Package Name</th>', response.data)
|
||||||
|
self.assertIn(b'No packages downloaded yet.', response.data)
|
||||||
|
mock_list_pkgs.assert_called_once()
|
||||||
|
|
||||||
def test_30_load_ig_to_hapi_integration(self):
|
def test_06_view_igs_with_packages(self):
|
||||||
"""Test loading an IG to the containerized HAPI FHIR server"""
|
self.create_mock_tgz('hl7.fhir.us.core-6.1.0.tgz', {'package/package.json': {'name': 'hl7.fhir.us.core', 'version': '6.1.0'}})
|
||||||
|
response = self.client.get('/view-igs')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'hl7.fhir.us.core', response.data)
|
||||||
|
self.assertIn(b'6.1.0', response.data)
|
||||||
|
self.assertIn(b'<th>Package Name</th>', response.data)
|
||||||
|
|
||||||
|
@patch('app.render_template')
|
||||||
|
def test_07_push_igs_page(self, mock_render_template):
|
||||||
|
mock_render_template.return_value = "Mock Render"
|
||||||
|
response = self.client.get('/push-igs')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
mock_render_template.assert_called()
|
||||||
|
call_args, call_kwargs = mock_render_template.call_args
|
||||||
|
self.assertEqual(call_args[0], 'cp_push_igs.html')
|
||||||
|
|
||||||
|
# --- UI Form Tests ---
|
||||||
|
|
||||||
|
@patch('app.services.import_package_and_dependencies')
|
||||||
|
def test_10_import_ig_form_success(self, mock_import):
|
||||||
|
mock_import.return_value = {'requested': ('hl7.fhir.us.core', '6.1.0'), 'processed': {('hl7.fhir.us.core', '6.1.0')}, 'downloaded': {('hl7.fhir.us.core', '6.1.0'): 'path/pkg.tgz'}, 'all_dependencies': {}, 'dependencies': [], 'errors': []}
|
||||||
|
response = self.client.post('/import-ig', data={'package_name': 'hl7.fhir.us.core', 'package_version': '6.1.0', 'dependency_mode': 'recursive'}, follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Successfully downloaded hl7.fhir.us.core#6.1.0 and dependencies! Mode: recursive', response.data)
|
||||||
|
mock_import.assert_called_once_with('hl7.fhir.us.core', '6.1.0', dependency_mode='recursive')
|
||||||
|
|
||||||
|
@patch('app.services.import_package_and_dependencies')
|
||||||
|
def test_11_import_ig_form_failure_404(self, mock_import):
|
||||||
|
mock_import.return_value = {'requested': ('invalid.package', '1.0.0'), 'processed': set(), 'downloaded': {}, 'all_dependencies': {}, 'dependencies': [], 'errors': ['HTTP error fetching package: 404 Client Error: Not Found for url: ...']}
|
||||||
|
response = self.client.post('/import-ig', data={'package_name': 'invalid.package', 'package_version': '1.0.0', 'dependency_mode': 'recursive'}, follow_redirects=False)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Package not found on registry (404)', response.data)
|
||||||
|
|
||||||
|
@patch('app.services.import_package_and_dependencies')
|
||||||
|
def test_12_import_ig_form_failure_conn_error(self, mock_import):
|
||||||
|
mock_import.return_value = {'requested': ('conn.error.pkg', '1.0.0'), 'processed': set(), 'downloaded': {}, 'all_dependencies': {}, 'dependencies': [], 'errors': ['Connection error: Cannot connect to registry...']}
|
||||||
|
response = self.client.post('/import-ig', data={'package_name': 'conn.error.pkg', 'package_version': '1.0.0', 'dependency_mode': 'recursive'}, follow_redirects=False)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Could not connect to the FHIR package registry', response.data)
|
||||||
|
|
||||||
|
def test_13_import_ig_form_invalid_input(self):
|
||||||
|
response = self.client.post('/import-ig', data={'package_name': 'invalid@package', 'package_version': '1.0.0', 'dependency_mode': 'recursive'}, follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Error in Package Name: Invalid package name format.', response.data)
|
||||||
|
|
||||||
|
@patch('app.services.process_package_file')
|
||||||
|
@patch('app.services.parse_package_filename')
|
||||||
|
def test_20_process_ig_success(self, mock_parse, mock_process):
|
||||||
|
pkg_name = 'hl7.fhir.us.core'
|
||||||
|
pkg_version = '6.1.0'
|
||||||
|
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||||
|
mock_parse.return_value = (pkg_name, pkg_version)
|
||||||
|
mock_process.return_value = {
|
||||||
|
'resource_types_info': [{'name': 'Patient', 'type': 'Patient', 'is_profile': False, 'must_support': True, 'optional_usage': False}],
|
||||||
|
'must_support_elements': {'Patient': ['Patient.name', 'Patient.identifier:us-ssn']},
|
||||||
|
'examples': {'Patient': ['package/Patient-example.json']},
|
||||||
|
'complies_with_profiles': [],
|
||||||
|
'imposed_profiles': ['http://hl7.org/fhir/StructureDefinition/Patient'],
|
||||||
|
'errors': []
|
||||||
|
}
|
||||||
|
self.create_mock_tgz(filename, {'package/package.json': {'name': pkg_name, 'version': pkg_version}})
|
||||||
|
response = self.client.post('/process-igs', data={'filename': filename}, follow_redirects=False)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertTrue(response.location.endswith('/view-igs'))
|
||||||
|
with self.client.session_transaction() as sess:
|
||||||
|
self.assertIn(('success', f'Successfully processed {pkg_name}#{pkg_version}!'), sess.get('_flashes', []))
|
||||||
|
mock_parse.assert_called_once_with(filename)
|
||||||
|
mock_process.assert_called_once_with(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))
|
||||||
|
processed_ig = db.session.query(ProcessedIg).filter_by(package_name=pkg_name, version=pkg_version).first()
|
||||||
|
self.assertIsNotNone(processed_ig)
|
||||||
|
self.assertEqual(processed_ig.package_name, pkg_name)
|
||||||
|
self.assertIn('Patient.name', processed_ig.must_support_elements.get('Patient', []))
|
||||||
|
|
||||||
|
def test_21_process_ig_file_not_found(self):
|
||||||
|
response = self.client.post('/process-igs', data={'filename': 'nonexistent.tgz'}, follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Package file not found: nonexistent.tgz', response.data)
|
||||||
|
|
||||||
|
def test_22_delete_ig_success(self):
|
||||||
|
filename = 'hl7.fhir.us.core-6.1.0.tgz'
|
||||||
|
metadata_filename = 'hl7.fhir.us.core-6.1.0.metadata.json'
|
||||||
|
self.create_mock_tgz(filename, {'package/package.json': {'name': 'hl7.fhir.us.core', 'version': '6.1.0'}})
|
||||||
|
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename)
|
||||||
|
open(metadata_path, 'w').write(json.dumps({'name': 'hl7.fhir.us.core'}))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)))
|
||||||
|
self.assertTrue(os.path.exists(metadata_path))
|
||||||
|
response = self.client.post('/delete-ig', data={'filename': filename}, follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(f'Deleted: {filename}, {metadata_filename}'.encode('utf-8'), response.data)
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename)))
|
||||||
|
self.assertFalse(os.path.exists(metadata_path))
|
||||||
|
|
||||||
|
def test_23_unload_ig_success(self):
|
||||||
|
processed_ig = ProcessedIg(package_name='test.pkg', version='1.0', processed_date=datetime.now(timezone.utc), resource_types_info=[], must_support_elements={}, examples={})
|
||||||
|
db.session.add(processed_ig)
|
||||||
|
db.session.commit()
|
||||||
|
ig_id = processed_ig.id
|
||||||
|
self.assertIsNotNone(db.session.get(ProcessedIg, ig_id))
|
||||||
|
response = self.client.post('/unload-ig', data={'ig_id': str(ig_id)}, follow_redirects=True)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Unloaded processed data for test.pkg#1.0', response.data)
|
||||||
|
self.assertIsNone(db.session.get(ProcessedIg, ig_id))
|
||||||
|
|
||||||
|
# --- Phase 2 Tests ---
|
||||||
|
|
||||||
|
@patch('os.path.exists', return_value=True)
|
||||||
|
@patch('tarfile.open')
|
||||||
|
@patch('requests.put')
|
||||||
|
def test_30_load_ig_to_hapi_success(self, mock_requests_put, mock_tarfile_open, mock_os_exists):
|
||||||
pkg_name = 'hl7.fhir.us.core'
|
pkg_name = 'hl7.fhir.us.core'
|
||||||
pkg_version = '6.1.0'
|
pkg_version = '6.1.0'
|
||||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||||
self.create_mock_tgz(filename, {
|
self.create_mock_tgz(filename, {
|
||||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||||
'package/StructureDefinition-us-core-patient.json': {
|
'package/Patient-profile.json': {'resourceType': 'StructureDefinition', 'id': 'us-core-patient'}
|
||||||
'resourceType': 'StructureDefinition',
|
|
||||||
'id': 'us-core-patient',
|
|
||||||
'url': 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient',
|
|
||||||
'name': 'USCorePatientProfile',
|
|
||||||
'type': 'Patient',
|
|
||||||
'status': 'active'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
mock_tar = MagicMock()
|
||||||
# Load IG to HAPI
|
profile_member = MagicMock(spec=tarfile.TarInfo)
|
||||||
|
profile_member.name = 'package/Patient-profile.json'
|
||||||
|
profile_member.isfile.return_value = True
|
||||||
|
mock_tar.getmembers.return_value = [profile_member]
|
||||||
|
mock_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'StructureDefinition', 'id': 'us-core-patient'}).encode('utf-8'))
|
||||||
|
mock_tarfile_open.return_value.__enter__.return_value = mock_tar
|
||||||
|
mock_requests_put.return_value = MagicMock(status_code=200)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/api/load-ig-to-hapi',
|
'/api/load-ig-to-hapi',
|
||||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version}),
|
data=json.dumps({'package_name': pkg_name, 'version': pkg_version}),
|
||||||
content_type='application/json',
|
content_type='application/json'
|
||||||
headers={'X-API-Key': 'test-api-key'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = json.loads(response.data)
|
data = json.loads(response.data)
|
||||||
self.assertEqual(data['status'], 'success')
|
self.assertEqual(data['status'], 'success')
|
||||||
|
mock_requests_put.assert_called_once_with(
|
||||||
# Verify the resource was loaded by querying the HAPI FHIR server directly
|
'http://localhost:8080/fhir/StructureDefinition/us-core-patient',
|
||||||
hapi_response = requests.get(self.container.get_service_url('fhir', 'fhir/StructureDefinition/us-core-patient'))
|
json={'resourceType': 'StructureDefinition', 'id': 'us-core-patient'},
|
||||||
self.assertEqual(hapi_response.status_code, 200)
|
headers={'Content-Type': 'application/fhir+json'}
|
||||||
resource = hapi_response.json()
|
|
||||||
self.assertEqual(resource['resourceType'], 'StructureDefinition')
|
|
||||||
self.assertEqual(resource['id'], 'us-core-patient')
|
|
||||||
|
|
||||||
def test_31_validate_sample_with_hapi_integration(self):
|
|
||||||
"""Test validating a sample against the containerized HAPI FHIR server"""
|
|
||||||
# First, load the necessary StructureDefinition
|
|
||||||
pkg_name = 'hl7.fhir.us.core'
|
|
||||||
pkg_version = '6.1.0'
|
|
||||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
|
||||||
self.create_mock_tgz(filename, {
|
|
||||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
|
||||||
'package/StructureDefinition-us-core-patient.json': {
|
|
||||||
'resourceType': 'StructureDefinition',
|
|
||||||
'id': 'us-core-patient',
|
|
||||||
'url': 'http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient',
|
|
||||||
'name': 'USCorePatientProfile',
|
|
||||||
'type': 'Patient',
|
|
||||||
'status': 'active',
|
|
||||||
'snapshot': {
|
|
||||||
'element': [
|
|
||||||
{'path': 'Patient', 'min': 1, 'max': '1'},
|
|
||||||
{'path': 'Patient.name', 'min': 1, 'max': '*'},
|
|
||||||
{'path': 'Patient.identifier', 'min': 0, 'max': '*', 'mustSupport': True}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
# Load IG to HAPI
|
|
||||||
self.client.post(
|
|
||||||
'/api/load-ig-to-hapi',
|
|
||||||
data=json.dumps({'package_name': pkg_name, 'version': pkg_version}),
|
|
||||||
content_type='application/json',
|
|
||||||
headers={'X-API-Key': 'test-api-key'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate a sample that's missing a required element
|
def test_31_load_ig_to_hapi_not_found(self):
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/load-ig-to-hapi',
|
||||||
|
data=json.dumps({'package_name': 'nonexistent', 'version': '1.0'}),
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['error'], 'Package not found')
|
||||||
|
|
||||||
|
@patch('os.path.exists', return_value=True)
|
||||||
|
@patch('requests.post')
|
||||||
|
def test_32_api_validate_sample_hapi_success(self, mock_requests_post, mock_os_exists):
|
||||||
|
pkg_name = 'hl7.fhir.us.core'
|
||||||
|
pkg_version = '6.1.0'
|
||||||
sample_resource = {
|
sample_resource = {
|
||||||
'resourceType': 'Patient',
|
'resourceType': 'Patient',
|
||||||
'id': 'test-patient',
|
'id': 'valid1',
|
||||||
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']},
|
||||||
# Missing required 'name' element
|
'name': [{'given': ['John'], 'family': 'Doe'}]
|
||||||
}
|
}
|
||||||
|
mock_requests_post.return_value = MagicMock(
|
||||||
|
status_code=200,
|
||||||
|
json=lambda: {
|
||||||
|
'resourceType': 'OperationOutcome',
|
||||||
|
'issue': [{'severity': 'warning', 'diagnostics': 'Must Support element Patient.identifier missing'}]
|
||||||
|
}
|
||||||
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/api/validate-sample',
|
'/api/validate-sample',
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
@ -452,68 +311,146 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
|||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
headers={'X-API-Key': 'test-api-key'}
|
headers={'X-API-Key': 'test-api-key'}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = json.loads(response.data)
|
data = json.loads(response.data)
|
||||||
self.assertFalse(data['valid'])
|
self.assertTrue(data['valid'])
|
||||||
# Check for validation error related to missing name
|
self.assertEqual(data['warnings'], ['Must Support element Patient.identifier missing'])
|
||||||
found_name_error = any('name' in error for error in data['errors'])
|
mock_requests_post.assert_called_once_with(
|
||||||
self.assertTrue(found_name_error, f"Expected error about missing name element, got: {data['errors']}")
|
'http://localhost:8080/fhir/Patient/$validate?profile=http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient',
|
||||||
|
json=sample_resource,
|
||||||
|
headers={'Content-Type': 'application/fhir+json', 'Accept': 'application/fhir+json'},
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
|
||||||
def test_32_push_ig_to_hapi_integration(self):
|
@patch('os.path.exists', return_value=True)
|
||||||
"""Test pushing multiple resources from an IG to the containerized HAPI FHIR server"""
|
@patch('requests.post', side_effect=requests.ConnectionError("HAPI down"))
|
||||||
pkg_name = 'test.push.pkg'
|
@patch('services.navigate_fhir_path')
|
||||||
pkg_version = '1.0.0'
|
def test_33_api_validate_sample_hapi_fallback(self, mock_navigate_fhir_path, mock_requests_post, mock_os_exists):
|
||||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
pkg_name = 'hl7.fhir.us.core'
|
||||||
|
pkg_version = '6.1.0'
|
||||||
# Create a test package with multiple resources
|
sample_resource = {
|
||||||
self.create_mock_tgz(filename, {
|
|
||||||
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
|
||||||
'package/Patient-test1.json': {
|
|
||||||
'resourceType': 'Patient',
|
'resourceType': 'Patient',
|
||||||
'id': 'test1',
|
'id': 'valid1',
|
||||||
'name': [{'family': 'Test', 'given': ['Patient']}]
|
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
||||||
},
|
}
|
||||||
'package/Observation-test1.json': {
|
mock_navigate_fhir_path.return_value = None
|
||||||
'resourceType': 'Observation',
|
self.create_mock_tgz(f'{pkg_name}-{pkg_version}.tgz', {
|
||||||
'id': 'test1',
|
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||||
'status': 'final',
|
'package/StructureDefinition-us-core-patient.json': {
|
||||||
'code': {'coding': [{'system': 'http://loinc.org', 'code': '12345-6'}]}
|
'resourceType': 'StructureDefinition',
|
||||||
|
'snapshot': {'element': [{'path': 'Patient.name', 'min': 1}, {'path': 'Patient.identifier', 'mustSupport': True}]}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# Push the IG to HAPI
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/api/push-ig',
|
'/api/validate-sample',
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
'package_name': pkg_name,
|
'package_name': pkg_name,
|
||||||
'version': pkg_version,
|
'version': pkg_version,
|
||||||
'fhir_server_url': self.container.get_service_url('fhir', 'fhir'),
|
'sample_data': json.dumps(sample_resource),
|
||||||
'include_dependencies': False
|
'mode': 'single',
|
||||||
|
'include_dependencies': True
|
||||||
}),
|
}),
|
||||||
content_type='application/json',
|
content_type='application/json',
|
||||||
headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'}
|
headers={'X-API-Key': 'test-api-key'}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
streamed_data = parse_ndjson(response.data)
|
data = json.loads(response.data)
|
||||||
complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None)
|
self.assertFalse(data['valid'])
|
||||||
self.assertIsNotNone(complete_msg, "Complete message not found in streamed response")
|
self.assertIn('Required element Patient.name missing', data['errors'])
|
||||||
summary = complete_msg.get('data', {})
|
self.assertIn('HAPI validation failed', [d['issue'] for d in data['details']])
|
||||||
self.assertTrue(summary.get('success_count') >= 2, f"Expected at least 2 successful resources, got {summary.get('success_count')}")
|
|
||||||
|
|
||||||
# Verify resources were loaded by querying the HAPI FHIR server directly
|
# --- Phase 3 Tests ---
|
||||||
patient_response = requests.get(self.container.get_service_url('fhir', 'fhir/Patient/test1'))
|
|
||||||
self.assertEqual(patient_response.status_code, 200)
|
|
||||||
patient = patient_response.json()
|
|
||||||
self.assertEqual(patient['resourceType'], 'Patient')
|
|
||||||
self.assertEqual(patient['id'], 'test1')
|
|
||||||
|
|
||||||
observation_response = requests.get(self.container.get_service_url('fhir', 'fhir/Observation/test1'))
|
@patch('requests.get')
|
||||||
self.assertEqual(observation_response.status_code, 200)
|
def test_34_hapi_status_check(self, mock_requests_get):
|
||||||
observation = observation_response.json()
|
mock_requests_get.return_value = MagicMock(status_code=200, json=lambda: {'resourceType': 'CapabilityStatement'})
|
||||||
self.assertEqual(observation['resourceType'], 'Observation')
|
response = self.client.get('/fhir/metadata')
|
||||||
self.assertEqual(observation['id'], 'test1')
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['resourceType'], 'CapabilityStatement')
|
||||||
|
mock_requests_get.side_effect = requests.ConnectionError("HAPI down")
|
||||||
|
response = self.client.get('/fhir/metadata')
|
||||||
|
self.assertEqual(response.status_code, 503)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertIn('Unable to connect to HAPI FHIR server', data['error'])
|
||||||
|
|
||||||
|
def test_35_validate_sample_ui_rendering(self):
|
||||||
|
pkg_name = 'hl7.fhir.us.core'
|
||||||
|
pkg_version = '6.1.0'
|
||||||
|
sample_resource = {
|
||||||
|
'resourceType': 'Patient',
|
||||||
|
'id': 'test',
|
||||||
|
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
||||||
|
}
|
||||||
|
self.create_mock_tgz(f'{pkg_name}-{pkg_version}.tgz', {
|
||||||
|
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||||
|
'package/StructureDefinition-us-core-patient.json': {
|
||||||
|
'resourceType': 'StructureDefinition',
|
||||||
|
'snapshot': {'element': [{'path': 'Patient.name', 'min': 1}, {'path': 'Patient.identifier', 'mustSupport': True}]}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/validate-sample',
|
||||||
|
data=json.dumps({
|
||||||
|
'package_name': pkg_name,
|
||||||
|
'version': pkg_version,
|
||||||
|
'sample_data': json.dumps(sample_resource),
|
||||||
|
'mode': 'single',
|
||||||
|
'include_dependencies': True
|
||||||
|
}),
|
||||||
|
content_type='application/json',
|
||||||
|
headers={'X-API-Key': 'test-api-key'}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertFalse(data['valid'])
|
||||||
|
self.assertIn('Required element Patient.name missing', data['errors'])
|
||||||
|
self.assertIn('Must Support element Patient.identifier missing', data['warnings'])
|
||||||
|
response = self.client.get('/validate-sample')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'us-core-patient', response.data)
|
||||||
|
|
||||||
|
def test_36_must_support_consistency(self):
|
||||||
|
pkg_name = 'hl7.fhir.us.core'
|
||||||
|
pkg_version = '6.1.0'
|
||||||
|
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||||
|
self.create_mock_tgz(filename, {
|
||||||
|
'package/package.json': {'name': pkg_name, 'version': pkg_version},
|
||||||
|
'package/StructureDefinition-us-core-patient.json': {
|
||||||
|
'resourceType': 'StructureDefinition',
|
||||||
|
'snapshot': {'element': [{'path': 'Patient.name', 'min': 1}, {'path': 'Patient.identifier', 'mustSupport': True, 'sliceName': 'us-ssn'}]}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
services.process_package_file(os.path.join(app.config['FHIR_PACKAGES_DIR'], filename))
|
||||||
|
sample_resource = {
|
||||||
|
'resourceType': 'Patient',
|
||||||
|
'id': 'test',
|
||||||
|
'meta': {'profile': ['http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient']}
|
||||||
|
}
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/validate-sample',
|
||||||
|
data=json.dumps({
|
||||||
|
'package_name': pkg_name,
|
||||||
|
'version': pkg_version,
|
||||||
|
'sample_data': json.dumps(sample_resource),
|
||||||
|
'mode': 'single',
|
||||||
|
'include_dependencies': True
|
||||||
|
}),
|
||||||
|
content_type='application/json',
|
||||||
|
headers={'X-API-Key': 'test-api-key'}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertIn('Must Support element Patient.identifier missing', data['warnings'])
|
||||||
|
with self.app_context:
|
||||||
|
ig = ProcessedIg.query.filter_by(package_name=pkg_name, version=pkg_version).first()
|
||||||
|
self.assertIsNotNone(ig)
|
||||||
|
must_support_paths = ig.must_support_elements.get('Patient', [])
|
||||||
|
self.assertIn('Patient.identifier:us-ssn', must_support_paths)
|
||||||
|
response = self.client.get(f'/view-ig/{ig.id}')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b'Patient.identifier:us-ssn', response.data)
|
||||||
|
self.assertIn(b'list-group-item-warning', response.data)
|
||||||
|
|
||||||
# --- Existing API Tests ---
|
# --- Existing API Tests ---
|
||||||
|
|
||||||
@ -578,7 +515,7 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
|||||||
pkg_name = 'push.test.pkg'
|
pkg_name = 'push.test.pkg'
|
||||||
pkg_version = '1.0.0'
|
pkg_version = '1.0.0'
|
||||||
filename = f'{pkg_name}-{pkg_version}.tgz'
|
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||||
fhir_server_url = self.container.get_service_url('fhir', 'fhir')
|
fhir_server_url = 'http://fake-fhir.com/baseR4'
|
||||||
mock_get_metadata.return_value = {'imported_dependencies': []}
|
mock_get_metadata.return_value = {'imported_dependencies': []}
|
||||||
mock_tar = MagicMock()
|
mock_tar = MagicMock()
|
||||||
mock_patient = {'resourceType': 'Patient', 'id': 'pat1'}
|
mock_patient = {'resourceType': 'Patient', 'id': 'pat1'}
|
||||||
@ -627,22 +564,225 @@ class TestFHIRFlareIGToolkit(unittest.TestCase):
|
|||||||
self.assertEqual(len(summary.get('failed_details')), 0)
|
self.assertEqual(len(summary.get('failed_details')), 0)
|
||||||
mock_os_exists.assert_called_with(os.path.join(self.test_packages_dir, filename))
|
mock_os_exists.assert_called_with(os.path.join(self.test_packages_dir, filename))
|
||||||
|
|
||||||
# --- Helper method to debug container issues ---
|
@patch('os.path.exists', return_value=True)
|
||||||
|
@patch('app.services.get_package_metadata')
|
||||||
|
@patch('tarfile.open')
|
||||||
|
@patch('requests.Session')
|
||||||
|
def test_51_api_push_ig_with_failures(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||||
|
pkg_name = 'push.fail.pkg'
|
||||||
|
pkg_version = '1.0.0'
|
||||||
|
filename = f'{pkg_name}-{pkg_version}.tgz'
|
||||||
|
fhir_server_url = 'http://fail-fhir.com/baseR4'
|
||||||
|
mock_get_metadata.return_value = {'imported_dependencies': []}
|
||||||
|
mock_tar = MagicMock()
|
||||||
|
mock_ok_res = {'resourceType': 'Patient', 'id': 'ok1'}
|
||||||
|
mock_fail_res = {'resourceType': 'Observation', 'id': 'fail1'}
|
||||||
|
ok_member = MagicMock(spec=tarfile.TarInfo)
|
||||||
|
ok_member.name = 'package/Patient-ok1.json'
|
||||||
|
ok_member.isfile.return_value = True
|
||||||
|
fail_member = MagicMock(spec=tarfile.TarInfo)
|
||||||
|
fail_member.name = 'package/Observation-fail1.json'
|
||||||
|
fail_member.isfile.return_value = True
|
||||||
|
mock_tar.getmembers.return_value = [ok_member, fail_member]
|
||||||
|
def mock_extractfile(member):
|
||||||
|
if member.name == 'package/Patient-ok1.json':
|
||||||
|
return io.BytesIO(json.dumps(mock_ok_res).encode('utf-8'))
|
||||||
|
if member.name == 'package/Observation-fail1.json':
|
||||||
|
return io.BytesIO(json.dumps(mock_fail_res).encode('utf-8'))
|
||||||
|
return None
|
||||||
|
mock_tar.extractfile.side_effect = mock_extractfile
|
||||||
|
mock_tarfile_open.return_value.__enter__.return_value = mock_tar
|
||||||
|
mock_session_instance = MagicMock()
|
||||||
|
mock_ok_response = MagicMock(status_code=200)
|
||||||
|
mock_ok_response.raise_for_status.return_value = None
|
||||||
|
mock_fail_http_response = MagicMock(status_code=400)
|
||||||
|
mock_fail_http_response.json.return_value = {'resourceType': 'OperationOutcome', 'issue': [{'severity': 'error', 'diagnostics': 'Validation failed'}]}
|
||||||
|
mock_fail_exception = requests.exceptions.HTTPError(response=mock_fail_http_response)
|
||||||
|
mock_fail_http_response.raise_for_status.side_effect = mock_fail_exception
|
||||||
|
mock_session_instance.put.side_effect = [mock_ok_response, mock_fail_http_response]
|
||||||
|
mock_session.return_value = mock_session_instance
|
||||||
|
self.create_mock_tgz(filename, {'package/dummy.txt': 'content'})
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/push-ig',
|
||||||
|
data=json.dumps({
|
||||||
|
'package_name': pkg_name,
|
||||||
|
'version': pkg_version,
|
||||||
|
'fhir_server_url': fhir_server_url,
|
||||||
|
'include_dependencies': False,
|
||||||
|
'api_key': 'test-api-key'
|
||||||
|
}),
|
||||||
|
content_type='application/json',
|
||||||
|
headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
streamed_data = parse_ndjson(response.data)
|
||||||
|
complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None)
|
||||||
|
self.assertIsNotNone(complete_msg)
|
||||||
|
summary = complete_msg.get('data', {})
|
||||||
|
self.assertEqual(summary.get('status'), 'partial')
|
||||||
|
self.assertEqual(summary.get('success_count'), 1)
|
||||||
|
self.assertEqual(summary.get('failure_count'), 1)
|
||||||
|
self.assertEqual(len(summary.get('failed_details')), 1)
|
||||||
|
self.assertEqual(summary['failed_details'][0].get('resource'), 'Observation/fail1')
|
||||||
|
self.assertIn('Validation failed', summary['failed_details'][0].get('error', ''))
|
||||||
|
mock_os_exists.assert_called_with(os.path.join(self.test_packages_dir, filename))
|
||||||
|
|
||||||
def test_99_print_container_logs_on_failure(self):
|
@patch('os.path.exists', return_value=True)
|
||||||
"""Helper test that prints container logs in case of failures"""
|
@patch('app.services.get_package_metadata')
|
||||||
# This test should always pass but will print logs if other tests fail
|
@patch('tarfile.open')
|
||||||
try:
|
@patch('requests.Session')
|
||||||
if hasattr(self, 'container') and self.container.containers_up:
|
def test_52_api_push_ig_with_dependency(self, mock_session, mock_tarfile_open, mock_get_metadata, mock_os_exists):
|
||||||
for service_name in ['fhir', 'db', 'fhirflare']:
|
main_pkg_name = 'main.dep.pkg'
|
||||||
if service_name in self.container._container_ids:
|
main_pkg_ver = '1.0'
|
||||||
print(f"\n=== Logs for {service_name} ===")
|
main_filename = f'{main_pkg_name}-{main_pkg_ver}.tgz'
|
||||||
print(self.container.get_logs(service_name))
|
dep_pkg_name = 'dep.pkg'
|
||||||
except Exception as e:
|
dep_pkg_ver = '1.0'
|
||||||
print(f"Error getting container logs: {e}")
|
dep_filename = f'{dep_pkg_name}-{dep_pkg_ver}.tgz'
|
||||||
|
fhir_server_url = 'http://dep-fhir.com/baseR4'
|
||||||
|
self.create_mock_tgz(main_filename, {'package/Patient-main.json': {'resourceType': 'Patient', 'id': 'main'}})
|
||||||
|
self.create_mock_tgz(dep_filename, {'package/Observation-dep.json': {'resourceType': 'Observation', 'id': 'dep'}})
|
||||||
|
mock_get_metadata.return_value = {'imported_dependencies': [{'name': dep_pkg_name, 'version': dep_pkg_ver}]}
|
||||||
|
mock_main_tar = MagicMock()
|
||||||
|
main_member = MagicMock(spec=tarfile.TarInfo)
|
||||||
|
main_member.name = 'package/Patient-main.json'
|
||||||
|
main_member.isfile.return_value = True
|
||||||
|
mock_main_tar.getmembers.return_value = [main_member]
|
||||||
|
mock_main_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'Patient', 'id': 'main'}).encode('utf-8'))
|
||||||
|
mock_dep_tar = MagicMock()
|
||||||
|
dep_member = MagicMock(spec=tarfile.TarInfo)
|
||||||
|
dep_member.name = 'package/Observation-dep.json'
|
||||||
|
dep_member.isfile.return_value = True
|
||||||
|
mock_dep_tar.getmembers.return_value = [dep_member]
|
||||||
|
mock_dep_tar.extractfile.return_value = io.BytesIO(json.dumps({'resourceType': 'Observation', 'id': 'dep'}).encode('utf-8'))
|
||||||
|
def tar_opener(path, mode):
|
||||||
|
mock_tar_ctx = MagicMock()
|
||||||
|
if main_filename in path:
|
||||||
|
mock_tar_ctx.__enter__.return_value = mock_main_tar
|
||||||
|
elif dep_filename in path:
|
||||||
|
mock_tar_ctx.__enter__.return_value = mock_dep_tar
|
||||||
|
else:
|
||||||
|
empty_mock_tar = MagicMock()
|
||||||
|
empty_mock_tar.getmembers.return_value = []
|
||||||
|
mock_tar_ctx.__enter__.return_value = empty_mock_tar
|
||||||
|
return mock_tar_ctx
|
||||||
|
mock_tarfile_open.side_effect = tar_opener
|
||||||
|
mock_session_instance = MagicMock()
|
||||||
|
mock_put_response = MagicMock(status_code=200)
|
||||||
|
mock_put_response.raise_for_status.return_value = None
|
||||||
|
mock_session_instance.put.return_value = mock_put_response
|
||||||
|
mock_session.return_value = mock_session_instance
|
||||||
|
response = self.client.post(
|
||||||
|
'/api/push-ig',
|
||||||
|
data=json.dumps({
|
||||||
|
'package_name': main_pkg_name,
|
||||||
|
'version': main_pkg_ver,
|
||||||
|
'fhir_server_url': fhir_server_url,
|
||||||
|
'include_dependencies': True,
|
||||||
|
'api_key': 'test-api-key'
|
||||||
|
}),
|
||||||
|
content_type='application/json',
|
||||||
|
headers={'X-API-Key': 'test-api-key', 'Accept': 'application/x-ndjson'}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
streamed_data = parse_ndjson(response.data)
|
||||||
|
complete_msg = next((item for item in streamed_data if item.get('type') == 'complete'), None)
|
||||||
|
self.assertIsNotNone(complete_msg)
|
||||||
|
summary = complete_msg.get('data', {})
|
||||||
|
self.assertEqual(summary.get('status'), 'success')
|
||||||
|
self.assertEqual(summary.get('success_count'), 2)
|
||||||
|
self.assertEqual(len(summary.get('pushed_packages_summary')), 2)
|
||||||
|
mock_os_exists.assert_any_call(os.path.join(self.test_packages_dir, main_filename))
|
||||||
|
mock_os_exists.assert_any_call(os.path.join(self.test_packages_dir, dep_filename))
|
||||||
|
|
||||||
# This assertion always passes - this test is just for debug info
|
# --- Helper Route Tests ---
|
||||||
self.assertTrue(True)
|
|
||||||
|
@patch('app.ProcessedIg.query')
|
||||||
|
@patch('app.services.find_and_extract_sd')
|
||||||
|
@patch('os.path.exists')
|
||||||
|
def test_60_get_structure_definition_success(self, mock_exists, mock_find_sd, mock_query):
|
||||||
|
pkg_name = 'struct.test'
|
||||||
|
pkg_version = '1.0'
|
||||||
|
resource_type = 'Patient'
|
||||||
|
mock_exists.return_value = True
|
||||||
|
mock_sd_data = {'resourceType': 'StructureDefinition', 'snapshot': {'element': [{'id': 'Patient.name', 'min': 1}, {'id': 'Patient.birthDate', 'mustSupport': True}]}}
|
||||||
|
mock_find_sd.return_value = (mock_sd_data, 'path/to/sd.json')
|
||||||
|
mock_processed_ig = MagicMock()
|
||||||
|
mock_processed_ig.must_support_elements = {resource_type: ['Patient.birthDate']}
|
||||||
|
mock_query.filter_by.return_value.first.return_value = mock_processed_ig
|
||||||
|
response = self.client.get(f'/get-structure?package_name={pkg_name}&package_version={pkg_version}&resource_type={resource_type}')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data['must_support_paths'], ['Patient.birthDate'])
|
||||||
|
|
||||||
|
@patch('app.services.import_package_and_dependencies')
|
||||||
|
@patch('app.services.find_and_extract_sd')
|
||||||
|
@patch('os.path.exists')
|
||||||
|
def test_61_get_structure_definition_fallback(self, mock_exists, mock_find_sd, mock_import):
|
||||||
|
pkg_name = 'struct.test'
|
||||||
|
pkg_version = '1.0'
|
||||||
|
core_pkg_name, core_pkg_version = services.CANONICAL_PACKAGE
|
||||||
|
resource_type = 'Observation'
|
||||||
|
def exists_side_effect(path):
|
||||||
|
return True
|
||||||
|
mock_exists.side_effect = exists_side_effect
|
||||||
|
mock_core_sd_data = {'resourceType': 'StructureDefinition', 'snapshot': {'element': [{'id': 'Observation.status'}]}}
|
||||||
|
def find_sd_side_effect(path, identifier, profile_url=None):
|
||||||
|
if f"{pkg_name}-{pkg_version}.tgz" in path:
|
||||||
|
return (None, None)
|
||||||
|
if f"{core_pkg_name}-{core_pkg_version}.tgz" in path:
|
||||||
|
return (mock_core_sd_data, 'path/obs.json')
|
||||||
|
return (None, None)
|
||||||
|
mock_find_sd.side_effect = find_sd_side_effect
|
||||||
|
with patch('app.ProcessedIg.query') as mock_query:
|
||||||
|
mock_query.filter_by.return_value.first.return_value = None
|
||||||
|
response = self.client.get(f'/get-structure?package_name={pkg_name}&package_version={pkg_version}&resource_type={resource_type}')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertTrue(data['fallback_used'])
|
||||||
|
|
||||||
|
@patch('app.services.find_and_extract_sd', return_value=(None, None))
|
||||||
|
@patch('app.services.import_package_and_dependencies')
|
||||||
|
@patch('os.path.exists')
|
||||||
|
def test_62_get_structure_definition_not_found_anywhere(self, mock_exists, mock_import, mock_find_sd):
|
||||||
|
pkg_name = 'no.sd.pkg'
|
||||||
|
pkg_version = '1.0'
|
||||||
|
core_pkg_name, core_pkg_version = services.CANONICAL_PACKAGE
|
||||||
|
def exists_side_effect(path):
|
||||||
|
if f"{pkg_name}-{pkg_version}.tgz" in path:
|
||||||
|
return True
|
||||||
|
if f"{core_pkg_name}-{core_pkg_version}.tgz" in path:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
mock_exists.side_effect = exists_side_effect
|
||||||
|
mock_import.return_value = {'errors': ['Download failed'], 'downloaded': False}
|
||||||
|
response = self.client.get(f'/get-structure?package_name={pkg_name}&package_version={pkg_version}&resource_type=Whatever')
|
||||||
|
self.assertEqual(response.status_code, 500)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertIn('failed to download core package', data['error'])
|
||||||
|
|
||||||
|
def test_63_get_example_content_success(self):
|
||||||
|
pkg_name = 'example.test'
|
||||||
|
pkg_version = '1.0'
|
||||||
|
filename = f"{pkg_name}-{pkg_version}.tgz"
|
||||||
|
example_path = 'package/Patient-example.json'
|
||||||
|
example_content = {'resourceType': 'Patient', 'id': 'example'}
|
||||||
|
self.create_mock_tgz(filename, {example_path: example_content})
|
||||||
|
response = self.client.get(f'/get-example?package_name={pkg_name}&package_version={pkg_version}&filename={example_path}')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data, example_content)
|
||||||
|
|
||||||
|
def test_64_get_package_metadata_success(self):
|
||||||
|
pkg_name = 'metadata.test'
|
||||||
|
pkg_version = '1.0'
|
||||||
|
metadata_filename = f"{pkg_name}-{pkg_version}.metadata.json"
|
||||||
|
metadata_content = {'package_name': pkg_name, 'version': pkg_version, 'dependency_mode': 'tree-shaking'}
|
||||||
|
metadata_path = os.path.join(app.config['FHIR_PACKAGES_DIR'], metadata_filename)
|
||||||
|
open(metadata_path, 'w').write(json.dumps(metadata_content))
|
||||||
|
response = self.client.get(f'/get-package-metadata?package_name={pkg_name}&version={pkg_version}')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = json.loads(response.data)
|
||||||
|
self.assertEqual(data.get('dependency_mode'), 'tree-shaking')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@ -1,431 +0,0 @@
|
|||||||
{
|
|
||||||
"resourceType" : "Bundle",
|
|
||||||
"id" : "transaction-ex",
|
|
||||||
"type" : "transaction",
|
|
||||||
"entry" : [{
|
|
||||||
"fullUrl" : "urn:uuid:64eb2d39-8da6-4c1d-b4c7-a6d3e916cd5b",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "Patient",
|
|
||||||
"id" : "example-patient",
|
|
||||||
"meta" : {
|
|
||||||
"profile" : ["urn://example.com/ph-core/fhir/StructureDefinition/ph-core-patient"]
|
|
||||||
},
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"Patient_example-patient\"> </a>Juan Dela Cruz is a male patient born on 1 January 1980, residing in Manila, NCR, Philippines.</div>"
|
|
||||||
},
|
|
||||||
"extension" : [{
|
|
||||||
"extension" : [{
|
|
||||||
"url" : "code",
|
|
||||||
"valueCodeableConcept" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "urn:iso:std:iso:3166",
|
|
||||||
"code" : "PH",
|
|
||||||
"display" : "Philippines"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "period",
|
|
||||||
"valuePeriod" : {
|
|
||||||
"start" : "2020-01-01",
|
|
||||||
"end" : "2023-01-01"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"url" : "http://hl7.org/fhir/StructureDefinition/patient-nationality"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "http://hl7.org/fhir/StructureDefinition/patient-religion",
|
|
||||||
"valueCodeableConcept" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/v3-ReligiousAffiliation",
|
|
||||||
"code" : "1007",
|
|
||||||
"display" : "Atheism"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/indigenous-people",
|
|
||||||
"valueBoolean" : true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/indigenous-group",
|
|
||||||
"valueCodeableConcept" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/indigenous-groups",
|
|
||||||
"code" : "Ilongots",
|
|
||||||
"display" : "Ilongots"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/race",
|
|
||||||
"valueCodeableConcept" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/v3-Race",
|
|
||||||
"code" : "2036-2",
|
|
||||||
"display" : "Filipino"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"identifier" : [{
|
|
||||||
"system" : "http://philhealth.gov.ph/fhir/Identifier/philhealth-id",
|
|
||||||
"value" : "63-584789845-5"
|
|
||||||
}],
|
|
||||||
"active" : true,
|
|
||||||
"name" : [{
|
|
||||||
"family" : "Dela Cruz",
|
|
||||||
"given" : ["Juan Jane",
|
|
||||||
"Dela Fuente"]
|
|
||||||
}],
|
|
||||||
"gender" : "male",
|
|
||||||
"birthDate" : "1985-06-15",
|
|
||||||
"address" : [{
|
|
||||||
"extension" : [{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/city-municipality",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "1380200000",
|
|
||||||
"display" : "City of Las Piñas"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/city-municipality",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "1380100000",
|
|
||||||
"display" : "City of Caloocan"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/province",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "0402100000",
|
|
||||||
"display" : "Cavite"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/province",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "0403400000",
|
|
||||||
"display" : "Laguna"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/province",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "0405800000",
|
|
||||||
"display" : "Rizal"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/province",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "1704000000",
|
|
||||||
"display" : "Marinduque"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/province",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "0402100000",
|
|
||||||
"display" : "Cavite"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url" : "urn://example.com/ph-core/fhir/StructureDefinition/province",
|
|
||||||
"valueCoding" : {
|
|
||||||
"system" : "urn://example.com/ph-core/fhir/CodeSystem/PSGC",
|
|
||||||
"code" : "1705100000",
|
|
||||||
"display" : "Occidental Mindoro"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"line" : ["123 Mabini Street",
|
|
||||||
"Barangay Malinis"],
|
|
||||||
"city" : "Quezon City",
|
|
||||||
"district" : "NCR",
|
|
||||||
"postalCode" : "1100",
|
|
||||||
"country" : "PH"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "Patient"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl" : "urn:uuid:60b7132e-7cfd-44bc-83c2-de140dc8aaae",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "Encounter",
|
|
||||||
"id" : "example-encounter",
|
|
||||||
"meta" : {
|
|
||||||
"profile" : ["urn://example.com/ph-core/fhir/StructureDefinition/ph-core-encounter"]
|
|
||||||
},
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"Encounter_example-encounter\"> </a>An ambulatory encounter for Juan Dela Cruz that has been completed.</div>"
|
|
||||||
},
|
|
||||||
"status" : "finished",
|
|
||||||
"class" : {
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
|
||||||
"code" : "AMB",
|
|
||||||
"display" : "ambulatory"
|
|
||||||
},
|
|
||||||
"subject" : {
|
|
||||||
"reference" : "urn:uuid:64eb2d39-8da6-4c1d-b4c7-a6d3e916cd5b"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "Encounter"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl" : "urn:uuid:1a391d1e-a068-479a-88e3-e3d52c3a6f64",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "Condition",
|
|
||||||
"id" : "example-condition",
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"Condition_example-condition\"> </a>Juan Dela Cruz has an active diagnosis of Type 2 Diabetes Mellitus.</div>"
|
|
||||||
},
|
|
||||||
"clinicalStatus" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/condition-clinical",
|
|
||||||
"code" : "active",
|
|
||||||
"display" : "Active"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"code" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://snomed.info/sct",
|
|
||||||
"code" : "44054006",
|
|
||||||
"display" : "Diabetes mellitus type 2"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"subject" : {
|
|
||||||
"reference" : "urn:uuid:64eb2d39-8da6-4c1d-b4c7-a6d3e916cd5b"
|
|
||||||
},
|
|
||||||
"encounter" : {
|
|
||||||
"reference" : "urn:uuid:60b7132e-7cfd-44bc-83c2-de140dc8aaae"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "Condition"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl" : "urn:uuid:024dcb47-cc23-407a-839b-b4634e95abae",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "Medication",
|
|
||||||
"id" : "example-medication",
|
|
||||||
"meta" : {
|
|
||||||
"profile" : ["urn://example.com/ph-core/fhir/StructureDefinition/ph-core-medication"]
|
|
||||||
},
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"Medication_example-medication\"> </a>A medication resource has been created, but no specific details are provided.</div>"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "Medication"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl" : "urn:uuid:013f46df-f245-4a2f-beaf-9eb2c47fb1a3",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "Observation",
|
|
||||||
"id" : "blood-pressure",
|
|
||||||
"meta" : {
|
|
||||||
"profile" : ["urn://example.com/ph-core/fhir/StructureDefinition/ph-core-observation",
|
|
||||||
"http://hl7.org/fhir/StructureDefinition/vitalsigns",
|
|
||||||
"http://hl7.org/fhir/StructureDefinition/bp"]
|
|
||||||
},
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"Observation_blood-pressure\"> </a>On 17 September 2012, a blood pressure observation was recorded for Juan Dela Cruz. The systolic pressure was 107 mmHg (Normal), and the diastolic pressure was 60 mmHg (Below low normal). The measurement was taken from the right arm and performed by a practitioner.</div>"
|
|
||||||
},
|
|
||||||
"identifier" : [{
|
|
||||||
"system" : "urn:ietf:rfc:3986",
|
|
||||||
"value" : "urn:uuid:187e0c12-8dd2-67e2-99b2-bf273c878281"
|
|
||||||
}],
|
|
||||||
"basedOn" : [{
|
|
||||||
"identifier" : {
|
|
||||||
"system" : "https://acme.org/identifiers",
|
|
||||||
"value" : "1234"
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
"status" : "final",
|
|
||||||
"category" : [{
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/observation-category",
|
|
||||||
"code" : "vital-signs",
|
|
||||||
"display" : "Vital Signs"
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
"code" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://loinc.org",
|
|
||||||
"code" : "85354-9",
|
|
||||||
"display" : "Blood pressure panel with all children optional"
|
|
||||||
}],
|
|
||||||
"text" : "Blood pressure systolic & diastolic"
|
|
||||||
},
|
|
||||||
"subject" : {
|
|
||||||
"reference" : "urn:uuid:64eb2d39-8da6-4c1d-b4c7-a6d3e916cd5b"
|
|
||||||
},
|
|
||||||
"effectiveDateTime" : "2012-09-17",
|
|
||||||
"performer" : [{
|
|
||||||
"reference" : "urn:uuid:a036fd4c-c950-497b-8905-0d2c5ec6f1d4"
|
|
||||||
}],
|
|
||||||
"interpretation" : [{
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
|
|
||||||
"code" : "L",
|
|
||||||
"display" : "Low"
|
|
||||||
}],
|
|
||||||
"text" : "Below low normal"
|
|
||||||
}],
|
|
||||||
"bodySite" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://snomed.info/sct",
|
|
||||||
"code" : "85050009",
|
|
||||||
"display" : "Bone structure of humerus"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"component" : [{
|
|
||||||
"code" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://loinc.org",
|
|
||||||
"code" : "8480-6",
|
|
||||||
"display" : "Systolic blood pressure"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"valueQuantity" : {
|
|
||||||
"value" : 107,
|
|
||||||
"unit" : "mmHg",
|
|
||||||
"system" : "http://unitsofmeasure.org",
|
|
||||||
"code" : "mm[Hg]"
|
|
||||||
},
|
|
||||||
"interpretation" : [{
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
|
|
||||||
"code" : "N",
|
|
||||||
"display" : "Normal"
|
|
||||||
}],
|
|
||||||
"text" : "Normal"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"code" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://loinc.org",
|
|
||||||
"code" : "8462-4",
|
|
||||||
"display" : "Diastolic blood pressure"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"valueQuantity" : {
|
|
||||||
"value" : 60,
|
|
||||||
"unit" : "mmHg",
|
|
||||||
"system" : "http://unitsofmeasure.org",
|
|
||||||
"code" : "mm[Hg]"
|
|
||||||
},
|
|
||||||
"interpretation" : [{
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation",
|
|
||||||
"code" : "L",
|
|
||||||
"display" : "Low"
|
|
||||||
}],
|
|
||||||
"text" : "Below low normal"
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "Observation"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl" : "urn:uuid:b43c67e7-d9c4-48bb-a1b4-55769eeb9066",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "AllergyIntolerance",
|
|
||||||
"id" : "example-allergy",
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"AllergyIntolerance_example-allergy\"> </a>Juan Dela Cruz has a high criticality, active allergy to Benethamine penicillin.</div>"
|
|
||||||
},
|
|
||||||
"clinicalStatus" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical",
|
|
||||||
"code" : "active",
|
|
||||||
"display" : "Active"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"criticality" : "high",
|
|
||||||
"code" : {
|
|
||||||
"coding" : [{
|
|
||||||
"system" : "http://snomed.info/sct",
|
|
||||||
"code" : "294494002",
|
|
||||||
"display" : "Benethamine penicillin allergy"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"patient" : {
|
|
||||||
"reference" : "urn:uuid:64eb2d39-8da6-4c1d-b4c7-a6d3e916cd5b"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "AllergyIntolerance"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fullUrl" : "urn:uuid:a036fd4c-c950-497b-8905-0d2c5ec6f1d4",
|
|
||||||
"resource" : {
|
|
||||||
"resourceType" : "Practitioner",
|
|
||||||
"id" : "example-practitioner",
|
|
||||||
"meta" : {
|
|
||||||
"profile" : ["urn://example.com/ph-core/fhir/StructureDefinition/ph-core-practitioner"]
|
|
||||||
},
|
|
||||||
"text" : {
|
|
||||||
"status" : "generated",
|
|
||||||
"div" : "<div xmlns=\"http://www.w3.org/1999/xhtml\"><a name=\"Practitioner_example-practitioner\"> </a>Dr. Maria Clara Santos is a female practitioner born on May 15, 1985. She resides at 1234 Mabini Street, Manila, NCR, 1000, Philippines. She can be contacted via mobile at +63-912-345-6789 or by email at maria.santos@example.ph.</div>"
|
|
||||||
},
|
|
||||||
"name" : [{
|
|
||||||
"family" : "Santos",
|
|
||||||
"given" : ["Maria",
|
|
||||||
"Clara"]
|
|
||||||
}],
|
|
||||||
"telecom" : [{
|
|
||||||
"system" : "phone",
|
|
||||||
"value" : "+63-912-345-6789",
|
|
||||||
"use" : "mobile"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"system" : "email",
|
|
||||||
"value" : "maria.santos@example.ph",
|
|
||||||
"use" : "work"
|
|
||||||
}],
|
|
||||||
"address" : [{
|
|
||||||
"use" : "home",
|
|
||||||
"line" : ["1234 Mabini Street"],
|
|
||||||
"city" : "Manila",
|
|
||||||
"state" : "NCR",
|
|
||||||
"postalCode" : "1000",
|
|
||||||
"country" : "PH"
|
|
||||||
}],
|
|
||||||
"gender" : "female",
|
|
||||||
"birthDate" : "1985-05-15"
|
|
||||||
},
|
|
||||||
"request" : {
|
|
||||||
"method" : "POST",
|
|
||||||
"url" : "Practitioner"
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
@ -1,25 +0,0 @@
|
|||||||
2025-07-31 12:34:21,943 - services - DEBUG - Received validate-sample request
|
|
||||||
2025-07-31 12:34:21,944 - services - DEBUG - Request params: package_name=example.fhir.ph.core.r4, version=0.1.0, sample_data_length=713
|
|
||||||
2025-07-31 12:34:21,944 - services - DEBUG - Using FHIR_PACKAGES_DIR from current_app config: /app/instance/fhir_packages
|
|
||||||
2025-07-31 12:34:21,944 - services - DEBUG - Checking package file: /app/instance/fhir_packages/example.fhir.ph.core.r4-0.1.0.tgz
|
|
||||||
2025-07-31 12:34:21,944 - services - DEBUG - Validating AllergyIntolerance against example.fhir.ph.core.r4#0.1.0
|
|
||||||
2025-07-31 12:34:21,944 - services - DEBUG - Using FHIR_PACKAGES_DIR from current_app config: /app/instance/fhir_packages
|
|
||||||
2025-07-31 12:34:21,945 - services - DEBUG - Searching for SD matching 'AllergyIntolerance' with profile 'None' in example.fhir.ph.core.r4-0.1.0.tgz
|
|
||||||
2025-07-31 12:34:21,956 - services - INFO - SD matching identifier 'AllergyIntolerance' or profile 'None' not found within archive example.fhir.ph.core.r4-0.1.0.tgz
|
|
||||||
2025-07-31 12:34:21,956 - services - INFO - Validation result for AllergyIntolerance against example.fhir.ph.core.r4#0.1.0: valid=False, errors=1, warnings=0
|
|
||||||
2025-07-31 12:34:21,957 - werkzeug - INFO - 10.0.0.102 - - [31/Jul/2025 12:34:21] "POST /api/validate-sample HTTP/1.1" 200 -
|
|
||||||
2025-07-31 12:34:24,510 - werkzeug - INFO - 10.0.2.245 - - [31/Jul/2025 12:34:24] "GET / HTTP/1.1" 200 -
|
|
||||||
2025-07-31 12:34:27,378 - werkzeug - INFO - 10.0.2.245 - - [31/Jul/2025 12:34:27] "GET / HTTP/1.1" 200 -
|
|
||||||
2025-07-31 12:34:34,510 - werkzeug - INFO - 10.0.2.245 - - [31/Jul/2025 12:34:34] "GET / HTTP/1.1" 200 -
|
|
||||||
2025-07-31 12:34:36,799 - __main__ - DEBUG - Scanning packages directory: /app/instance/fhir_packages
|
|
||||||
2025-07-31 12:34:36,800 - __main__ - DEBUG - Found 8 .tgz files: ['PHCDI.r4-0.1.0.tgz', 'hl7.fhir.uv.ips-1.1.0.tgz', 'hl7.fhir.r4.core-4.0.1.tgz', 'fhir.dicom-2022.4.20221006.tgz', 'hl7.terminology.r4-5.0.0.tgz', 'example.fhir.ph.core.r4-0.1.0.tgz', 'hl7.terminology.r4-6.4.0.tgz', 'hl7.fhir.uv.extensions.r4-5.2.0.tgz']
|
|
||||||
2025-07-31 12:34:36,813 - __main__ - DEBUG - Added package: PHCDI.r4#0.1.0
|
|
||||||
2025-07-31 12:34:36,837 - __main__ - DEBUG - Added package: hl7.fhir.uv.ips#1.1.0
|
|
||||||
2025-07-31 12:34:37,378 - werkzeug - INFO - 10.0.2.245 - - [31/Jul/2025 12:34:37] "GET / HTTP/1.1" 200 -
|
|
||||||
2025-07-31 12:34:37,514 - __main__ - DEBUG - Added package: hl7.fhir.r4.core#4.0.1
|
|
||||||
2025-07-31 12:34:37,622 - __main__ - DEBUG - Added package: fhir.dicom#2022.4.20221006
|
|
||||||
2025-07-31 12:34:38,008 - __main__ - DEBUG - Added package: hl7.terminology.r4#5.0.0
|
|
||||||
2025-07-31 12:34:38,015 - __main__ - DEBUG - Added package: example.fhir.ph.core.r4#0.1.0
|
|
||||||
2025-07-31 12:34:38,413 - __main__ - DEBUG - Added package: hl7.terminology.r4#6.4.0
|
|
||||||
2025-07-31 12:34:38,524 - __main__ - DEBUG - Added package: hl7.fhir.uv.extensions.r4#5.2.0
|
|
||||||
2025-07-31 12:34:38,525 - __main__ - DEBUG - Set package choices: [('', 'None'), ('PHCDI.r4#0.1.0', 'PHCDI.r4#0.1.0'), ('example.fhir.ph.core.r4#0.1.0', 'example.fhir.ph.core.r4#0.1.0'), ('fhir.dicom#2022.4.20221006', 'fhir.dicom#2022.4.20221006'), ('hl7.fhir.r4.core#4.0.1', 'hl7.fhir.r4.core#4.0.1'), ('hl7.fhir.uv.extensions.r4#5.2.0', 'hl7.fhir.uv.extensions.r4#5.2.0'), ('hl7.fhir.uv.ips#1.1.0', 'hl7.fhir.uv.ips#1.1.0'), ('hl7.terminology.r4#5.0.0', 'hl7.terminology.r4#5.0.0'), ('hl7.terminology.r4#6.4.0', 'hl7.terminology.r4#6.4.0')]
|
|
||||||
1
website/.gitignore
vendored
1
website/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/_site/
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
source "https://rubygems.org"
|
|
||||||
|
|
||||||
gem "jekyll", "~> 3.9.3"
|
|
||||||
gem "github-pages", group: :jekyll_plugins
|
|
||||||
gem "jekyll-remote-theme"
|
|
||||||
gem "jekyll-seo-tag"
|
|
||||||
gem "webrick", "~> 1.7"
|
|
||||||
@ -1,309 +0,0 @@
|
|||||||
GEM
|
|
||||||
remote: https://rubygems.org/
|
|
||||||
specs:
|
|
||||||
activesupport (8.0.2)
|
|
||||||
base64
|
|
||||||
benchmark (>= 0.3)
|
|
||||||
bigdecimal
|
|
||||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
||||||
connection_pool (>= 2.2.5)
|
|
||||||
drb
|
|
||||||
i18n (>= 1.6, < 2)
|
|
||||||
logger (>= 1.4.2)
|
|
||||||
minitest (>= 5.1)
|
|
||||||
securerandom (>= 0.3)
|
|
||||||
tzinfo (~> 2.0, >= 2.0.5)
|
|
||||||
uri (>= 0.13.1)
|
|
||||||
addressable (2.8.7)
|
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
|
||||||
base64 (0.2.0)
|
|
||||||
benchmark (0.4.1)
|
|
||||||
bigdecimal (3.2.2)
|
|
||||||
coffee-script (2.4.1)
|
|
||||||
coffee-script-source
|
|
||||||
execjs
|
|
||||||
coffee-script-source (1.12.2)
|
|
||||||
colorator (1.1.0)
|
|
||||||
commonmarker (0.23.11)
|
|
||||||
concurrent-ruby (1.3.5)
|
|
||||||
connection_pool (2.5.3)
|
|
||||||
dnsruby (1.72.4)
|
|
||||||
base64 (~> 0.2.0)
|
|
||||||
logger (~> 1.6.5)
|
|
||||||
simpleidn (~> 0.2.1)
|
|
||||||
drb (2.2.3)
|
|
||||||
em-websocket (0.5.3)
|
|
||||||
eventmachine (>= 0.12.9)
|
|
||||||
http_parser.rb (~> 0)
|
|
||||||
ethon (0.16.0)
|
|
||||||
ffi (>= 1.15.0)
|
|
||||||
eventmachine (1.2.7)
|
|
||||||
execjs (2.10.0)
|
|
||||||
faraday (2.13.4)
|
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
|
||||||
json
|
|
||||||
logger
|
|
||||||
faraday-net_http (3.4.1)
|
|
||||||
net-http (>= 0.5.0)
|
|
||||||
ffi (1.17.2-aarch64-linux-gnu)
|
|
||||||
ffi (1.17.2-aarch64-linux-musl)
|
|
||||||
ffi (1.17.2-arm-linux-gnu)
|
|
||||||
ffi (1.17.2-arm-linux-musl)
|
|
||||||
ffi (1.17.2-arm64-darwin)
|
|
||||||
ffi (1.17.2-x86_64-darwin)
|
|
||||||
ffi (1.17.2-x86_64-linux-gnu)
|
|
||||||
ffi (1.17.2-x86_64-linux-musl)
|
|
||||||
forwardable-extended (2.6.0)
|
|
||||||
gemoji (4.1.0)
|
|
||||||
github-pages (231)
|
|
||||||
github-pages-health-check (= 1.18.2)
|
|
||||||
jekyll (= 3.9.5)
|
|
||||||
jekyll-avatar (= 0.8.0)
|
|
||||||
jekyll-coffeescript (= 1.2.2)
|
|
||||||
jekyll-commonmark-ghpages (= 0.4.0)
|
|
||||||
jekyll-default-layout (= 0.1.5)
|
|
||||||
jekyll-feed (= 0.17.0)
|
|
||||||
jekyll-gist (= 1.5.0)
|
|
||||||
jekyll-github-metadata (= 2.16.1)
|
|
||||||
jekyll-include-cache (= 0.2.1)
|
|
||||||
jekyll-mentions (= 1.6.0)
|
|
||||||
jekyll-optional-front-matter (= 0.3.2)
|
|
||||||
jekyll-paginate (= 1.1.0)
|
|
||||||
jekyll-readme-index (= 0.3.0)
|
|
||||||
jekyll-redirect-from (= 0.16.0)
|
|
||||||
jekyll-relative-links (= 0.6.1)
|
|
||||||
jekyll-remote-theme (= 0.4.3)
|
|
||||||
jekyll-sass-converter (= 1.5.2)
|
|
||||||
jekyll-seo-tag (= 2.8.0)
|
|
||||||
jekyll-sitemap (= 1.4.0)
|
|
||||||
jekyll-swiss (= 1.0.0)
|
|
||||||
jekyll-theme-architect (= 0.2.0)
|
|
||||||
jekyll-theme-cayman (= 0.2.0)
|
|
||||||
jekyll-theme-dinky (= 0.2.0)
|
|
||||||
jekyll-theme-hacker (= 0.2.0)
|
|
||||||
jekyll-theme-leap-day (= 0.2.0)
|
|
||||||
jekyll-theme-merlot (= 0.2.0)
|
|
||||||
jekyll-theme-midnight (= 0.2.0)
|
|
||||||
jekyll-theme-minimal (= 0.2.0)
|
|
||||||
jekyll-theme-modernist (= 0.2.0)
|
|
||||||
jekyll-theme-primer (= 0.6.0)
|
|
||||||
jekyll-theme-slate (= 0.2.0)
|
|
||||||
jekyll-theme-tactile (= 0.2.0)
|
|
||||||
jekyll-theme-time-machine (= 0.2.0)
|
|
||||||
jekyll-titles-from-headings (= 0.5.3)
|
|
||||||
jemoji (= 0.13.0)
|
|
||||||
kramdown (= 2.4.0)
|
|
||||||
kramdown-parser-gfm (= 1.1.0)
|
|
||||||
liquid (= 4.0.4)
|
|
||||||
mercenary (~> 0.3)
|
|
||||||
minima (= 2.5.1)
|
|
||||||
nokogiri (>= 1.13.6, < 2.0)
|
|
||||||
rouge (= 3.30.0)
|
|
||||||
terminal-table (~> 1.4)
|
|
||||||
github-pages-health-check (1.18.2)
|
|
||||||
addressable (~> 2.3)
|
|
||||||
dnsruby (~> 1.60)
|
|
||||||
octokit (>= 4, < 8)
|
|
||||||
public_suffix (>= 3.0, < 6.0)
|
|
||||||
typhoeus (~> 1.3)
|
|
||||||
html-pipeline (2.14.3)
|
|
||||||
activesupport (>= 2)
|
|
||||||
nokogiri (>= 1.4)
|
|
||||||
http_parser.rb (0.8.0)
|
|
||||||
i18n (1.14.7)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
jekyll (3.9.5)
|
|
||||||
addressable (~> 2.4)
|
|
||||||
colorator (~> 1.0)
|
|
||||||
em-websocket (~> 0.5)
|
|
||||||
i18n (>= 0.7, < 2)
|
|
||||||
jekyll-sass-converter (~> 1.0)
|
|
||||||
jekyll-watch (~> 2.0)
|
|
||||||
kramdown (>= 1.17, < 3)
|
|
||||||
liquid (~> 4.0)
|
|
||||||
mercenary (~> 0.3.3)
|
|
||||||
pathutil (~> 0.9)
|
|
||||||
rouge (>= 1.7, < 4)
|
|
||||||
safe_yaml (~> 1.0)
|
|
||||||
jekyll-avatar (0.8.0)
|
|
||||||
jekyll (>= 3.0, < 5.0)
|
|
||||||
jekyll-coffeescript (1.2.2)
|
|
||||||
coffee-script (~> 2.2)
|
|
||||||
coffee-script-source (~> 1.12)
|
|
||||||
jekyll-commonmark (1.4.0)
|
|
||||||
commonmarker (~> 0.22)
|
|
||||||
jekyll-commonmark-ghpages (0.4.0)
|
|
||||||
commonmarker (~> 0.23.7)
|
|
||||||
jekyll (~> 3.9.0)
|
|
||||||
jekyll-commonmark (~> 1.4.0)
|
|
||||||
rouge (>= 2.0, < 5.0)
|
|
||||||
jekyll-default-layout (0.1.5)
|
|
||||||
jekyll (>= 3.0, < 5.0)
|
|
||||||
jekyll-feed (0.17.0)
|
|
||||||
jekyll (>= 3.7, < 5.0)
|
|
||||||
jekyll-gist (1.5.0)
|
|
||||||
octokit (~> 4.2)
|
|
||||||
jekyll-github-metadata (2.16.1)
|
|
||||||
jekyll (>= 3.4, < 5.0)
|
|
||||||
octokit (>= 4, < 7, != 4.4.0)
|
|
||||||
jekyll-include-cache (0.2.1)
|
|
||||||
jekyll (>= 3.7, < 5.0)
|
|
||||||
jekyll-mentions (1.6.0)
|
|
||||||
html-pipeline (~> 2.3)
|
|
||||||
jekyll (>= 3.7, < 5.0)
|
|
||||||
jekyll-optional-front-matter (0.3.2)
|
|
||||||
jekyll (>= 3.0, < 5.0)
|
|
||||||
jekyll-paginate (1.1.0)
|
|
||||||
jekyll-readme-index (0.3.0)
|
|
||||||
jekyll (>= 3.0, < 5.0)
|
|
||||||
jekyll-redirect-from (0.16.0)
|
|
||||||
jekyll (>= 3.3, < 5.0)
|
|
||||||
jekyll-relative-links (0.6.1)
|
|
||||||
jekyll (>= 3.3, < 5.0)
|
|
||||||
jekyll-remote-theme (0.4.3)
|
|
||||||
addressable (~> 2.0)
|
|
||||||
jekyll (>= 3.5, < 5.0)
|
|
||||||
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
|
|
||||||
rubyzip (>= 1.3.0, < 3.0)
|
|
||||||
jekyll-sass-converter (1.5.2)
|
|
||||||
sass (~> 3.4)
|
|
||||||
jekyll-seo-tag (2.8.0)
|
|
||||||
jekyll (>= 3.8, < 5.0)
|
|
||||||
jekyll-sitemap (1.4.0)
|
|
||||||
jekyll (>= 3.7, < 5.0)
|
|
||||||
jekyll-swiss (1.0.0)
|
|
||||||
jekyll-theme-architect (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-cayman (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-dinky (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-hacker (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-leap-day (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-merlot (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-midnight (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-minimal (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-modernist (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-primer (0.6.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-github-metadata (~> 2.9)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-slate (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-tactile (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-theme-time-machine (0.2.0)
|
|
||||||
jekyll (> 3.5, < 5.0)
|
|
||||||
jekyll-seo-tag (~> 2.0)
|
|
||||||
jekyll-titles-from-headings (0.5.3)
|
|
||||||
jekyll (>= 3.3, < 5.0)
|
|
||||||
jekyll-watch (2.2.1)
|
|
||||||
listen (~> 3.0)
|
|
||||||
jemoji (0.13.0)
|
|
||||||
gemoji (>= 3, < 5)
|
|
||||||
html-pipeline (~> 2.2)
|
|
||||||
jekyll (>= 3.0, < 5.0)
|
|
||||||
json (2.13.2)
|
|
||||||
kramdown (2.4.0)
|
|
||||||
rexml
|
|
||||||
kramdown-parser-gfm (1.1.0)
|
|
||||||
kramdown (~> 2.0)
|
|
||||||
liquid (4.0.4)
|
|
||||||
listen (3.9.0)
|
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
|
||||||
logger (1.6.6)
|
|
||||||
mercenary (0.3.6)
|
|
||||||
minima (2.5.1)
|
|
||||||
jekyll (>= 3.5, < 5.0)
|
|
||||||
jekyll-feed (~> 0.9)
|
|
||||||
jekyll-seo-tag (~> 2.1)
|
|
||||||
minitest (5.25.5)
|
|
||||||
net-http (0.6.0)
|
|
||||||
uri
|
|
||||||
nokogiri (1.18.9-aarch64-linux-gnu)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-aarch64-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-arm-linux-gnu)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-arm-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-arm64-darwin)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-x86_64-darwin)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-x86_64-linux-gnu)
|
|
||||||
racc (~> 1.4)
|
|
||||||
nokogiri (1.18.9-x86_64-linux-musl)
|
|
||||||
racc (~> 1.4)
|
|
||||||
octokit (4.25.1)
|
|
||||||
faraday (>= 1, < 3)
|
|
||||||
sawyer (~> 0.9)
|
|
||||||
pathutil (0.16.2)
|
|
||||||
forwardable-extended (~> 2.6)
|
|
||||||
public_suffix (5.1.1)
|
|
||||||
racc (1.8.1)
|
|
||||||
rb-fsevent (0.11.2)
|
|
||||||
rb-inotify (0.11.1)
|
|
||||||
ffi (~> 1.0)
|
|
||||||
rexml (3.4.1)
|
|
||||||
rouge (3.30.0)
|
|
||||||
rubyzip (2.4.1)
|
|
||||||
safe_yaml (1.0.5)
|
|
||||||
sass (3.7.4)
|
|
||||||
sass-listen (~> 4.0.0)
|
|
||||||
sass-listen (4.0.0)
|
|
||||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
|
||||||
rb-inotify (~> 0.9, >= 0.9.7)
|
|
||||||
sawyer (0.9.2)
|
|
||||||
addressable (>= 2.3.5)
|
|
||||||
faraday (>= 0.17.3, < 3)
|
|
||||||
securerandom (0.4.1)
|
|
||||||
simpleidn (0.2.3)
|
|
||||||
terminal-table (1.8.0)
|
|
||||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
|
||||||
typhoeus (1.4.1)
|
|
||||||
ethon (>= 0.9.0)
|
|
||||||
tzinfo (2.0.6)
|
|
||||||
concurrent-ruby (~> 1.0)
|
|
||||||
unicode-display_width (1.8.0)
|
|
||||||
uri (1.0.3)
|
|
||||||
webrick (1.9.1)
|
|
||||||
|
|
||||||
PLATFORMS
|
|
||||||
aarch64-linux-gnu
|
|
||||||
aarch64-linux-musl
|
|
||||||
arm-linux-gnu
|
|
||||||
arm-linux-musl
|
|
||||||
arm64-darwin
|
|
||||||
x86_64-darwin
|
|
||||||
x86_64-linux-gnu
|
|
||||||
x86_64-linux-musl
|
|
||||||
|
|
||||||
DEPENDENCIES
|
|
||||||
github-pages
|
|
||||||
jekyll (~> 3.9.3)
|
|
||||||
jekyll-remote-theme
|
|
||||||
jekyll-seo-tag
|
|
||||||
webrick (~> 1.7)
|
|
||||||
|
|
||||||
BUNDLED WITH
|
|
||||||
2.7.1
|
|
||||||
@ -1,726 +0,0 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
---
|
|
||||||
|
|
||||||
# FHIRFLARE IG Toolkit
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The FHIRFLARE IG Toolkit is a Flask-based web application designed to streamline the management, processing, validation, and deployment of FHIR Implementation Guides (IGs) and test data. It offers a user-friendly interface for importing IG packages, extracting metadata, validating FHIR resources or bundles, pushing IGs to FHIR servers, converting FHIR resources to FHIR Shorthand (FSH), uploading complex test data sets with dependency management, and retrieving/splitting FHIR bundles. The toolkit includes live consoles for real-time feedback, making it an essential tool for FHIR developers and implementers.
|
|
||||||
|
|
||||||
The application can run in two modes:
|
|
||||||
|
|
||||||
* **Standalone:** Includes a Dockerized Flask frontend, SQLite database, and an embedded HAPI FHIR server for local validation and interaction.
|
|
||||||
* **Lite:** Includes only the Dockerized Flask frontend and SQLite database, excluding the local HAPI FHIR server. Requires connection to external FHIR servers for certain features.
|
|
||||||
|
|
||||||
## Installation Modes (Lite vs. Standalone)
|
|
||||||
|
|
||||||
This toolkit offers two primary installation modes to suit different needs:
|
|
||||||
|
|
||||||
* **Standalone Version:**
|
|
||||||
* Includes the full FHIRFLARE Toolkit application **and** an embedded HAPI FHIR server running locally within the Docker environment.
|
|
||||||
* Allows for local FHIR resource validation using HAPI FHIR's capabilities.
|
|
||||||
* Enables the "Use Local HAPI" option in the FHIR API Explorer and FHIR UI Operations pages, proxying requests to the internal HAPI server (`http://localhost:8080/fhir`).
|
|
||||||
* Requires Git and Maven during the initial build process (via the `.bat` script or manual steps) to prepare the HAPI FHIR server.
|
|
||||||
* Ideal for users who want a self-contained environment for development and testing or who don't have readily available external FHIR servers.
|
|
||||||
|
|
||||||
* **Lite Version:**
|
|
||||||
* Includes the FHIRFLARE Toolkit application **without** the embedded HAPI FHIR server.
|
|
||||||
* Requires users to provide URLs for external FHIR servers when using features like the FHIR API Explorer and FHIR UI Operations pages. The "Use Local HAPI" option will be disabled in the UI.
|
|
||||||
* Resource validation relies solely on local checks against downloaded StructureDefinitions, which may be less comprehensive than HAPI FHIR's validation (e.g., for terminology bindings or complex invariants).
|
|
||||||
* **Does not require Git or Maven** for setup if using the `.bat` script or running the pre-built Docker image.
|
|
||||||
* Ideal for users who primarily want to use the IG management, processing, and FSH conversion features, or who will always connect to existing external FHIR servers.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* **Import IGs:** Download FHIR IG packages and dependencies from a package registry, supporting flexible version formats (e.g., `1.2.3`, `1.1.0-preview`, `current`) and dependency pulling modes (Recursive, Patch Canonical, Tree Shaking).
|
|
||||||
* **Enhanced Package Search and Import:**
|
|
||||||
* Interactive page (`/search-and-import`) to search for FHIR IG packages from configured registries.
|
|
||||||
* Displays package details, version history, dependencies, and dependents.
|
|
||||||
* Utilizes a local database cache (`CachedPackage`) for faster subsequent searches.
|
|
||||||
* Background task to refresh the package cache from registries (`/api/refresh-cache-task`).
|
|
||||||
* Direct import from search results.
|
|
||||||
* **Manage IGs:** View, process, unload, or delete downloaded IGs, with duplicate detection and resolution.
|
|
||||||
* **Process IGs:** Extract resource types, profiles, must-support elements, examples, and profile relationships (`structuredefinition-compliesWithProfile` and `structuredefinition-imposeProfile`).
|
|
||||||
* **Validate FHIR Resources/Bundles:** Validate single FHIR resources or bundles against selected IGs, with detailed error and warning reports (alpha feature). *Note: Lite version uses local SD checks only.*
|
|
||||||
* **Push IGs:** Upload IG resources (and optionally dependencies) to a target FHIR server. Features include:
|
|
||||||
* Real-time console output.
|
|
||||||
* Authentication support (Bearer Token).
|
|
||||||
* Filtering by resource type or specific files to skip.
|
|
||||||
* Semantic comparison to skip uploading identical resources (override with **Force Upload** option).
|
|
||||||
* Correct handling of canonical resources (searching by URL/version before deciding POST/PUT).
|
|
||||||
* Dry run mode for simulation.
|
|
||||||
* Verbose logging option.
|
|
||||||
* **Upload Test Data:** Upload complex sets of test data (individual JSON/XML files or ZIP archives) to a target FHIR server. Features include:
|
|
||||||
* Robust parsing of JSON and XML (using `fhir.resources` library when available).
|
|
||||||
* Automatic dependency analysis based on resource references within the uploaded set.
|
|
||||||
* Topological sorting to ensure resources are uploaded in the correct order.
|
|
||||||
* Cycle detection in dependencies.
|
|
||||||
* Choice of individual resource uploads or a single transaction bundle.
|
|
||||||
* **Optional Pre-Upload Validation:** Validate resources against a selected profile package before uploading.
|
|
||||||
* **Optional Conditional Uploads (Individual Mode):** Check resource existence (GET) and use conditional `If-Match` headers for updates (PUT) or create resources (PUT/POST). Falls back to simple PUT if unchecked.
|
|
||||||
* Configurable error handling (stop on first error or continue).
|
|
||||||
* Authentication support (Bearer Token).
|
|
||||||
* Streaming progress log via the UI.
|
|
||||||
* Handles large numbers of files using a custom form parser.
|
|
||||||
* **Profile Relationships:** Display and validate `compliesWithProfile` and `imposeProfile` extensions in the UI (configurable).
|
|
||||||
* **FSH Converter:** Convert FHIR JSON/XML resources to FHIR Shorthand (FSH) using GoFSH, with advanced options (Package context, Output styles, Log levels, FHIR versions, Fishing Trip, Dependencies, Indentation, Meta Profile handling, Alias File, No Alias). Includes a waiting spinner.
|
|
||||||
* **Retrieve and Split Bundles:**
|
|
||||||
* Retrieve specified resource types as bundles from a FHIR server.
|
|
||||||
* Optionally fetch referenced resources, either individually or as full bundles for each referenced type.
|
|
||||||
* Split uploaded ZIP files containing bundles into individual resource JSON files.
|
|
||||||
* Download retrieved/split resources as a ZIP archive.
|
|
||||||
* Streaming progress log via the UI for retrieval operations.
|
|
||||||
* **FHIR Interaction UIs:** Explore FHIR server capabilities and interact with resources using the "FHIR API Explorer" (simple GET/POST/PUT/DELETE) and "FHIR UI Operations" (Swagger-like interface based on CapabilityStatement). *Note: Lite version requires custom server URLs.*
|
|
||||||
* **HAPI FHIR Configuration (Standalone Mode):**
|
|
||||||
* A dedicated page (`/config-hapi`) to view and edit the `application.yaml` configuration for the embedded HAPI FHIR server.
|
|
||||||
* Allows modification of HAPI FHIR properties directly from the UI.
|
|
||||||
* Option to restart the HAPI FHIR server (Tomcat) to apply changes.
|
|
||||||
* **API Support:** RESTful API endpoints for importing, pushing, retrieving metadata, validating, uploading test data, and retrieving/splitting bundles.
|
|
||||||
* **Live Console:** Real-time logs for push, validation, upload test data, FSH conversion, and bundle retrieval operations.
|
|
||||||
* **Configurable Behavior:** Control validation modes, display options via `app.config`.
|
|
||||||
* **Theming:** Supports light and dark modes.
|
|
||||||
|
|
||||||
## Technology Stack
|
|
||||||
|
|
||||||
* Python 3.12+, Flask 2.3.3, Flask-SQLAlchemy 3.0.5, Flask-WTF 1.2.1
|
|
||||||
* Jinja2, Bootstrap 5.3.3, JavaScript (ES6), Lottie-Web 5.12.2
|
|
||||||
* SQLite
|
|
||||||
* Docker, Docker Compose, Supervisor
|
|
||||||
* Node.js 18+ (for GoFSH/SUSHI), GoFSH, SUSHI
|
|
||||||
* HAPI FHIR (Standalone version only)
|
|
||||||
* Requests 2.31.0, Tarfile, Logging, Werkzeug
|
|
||||||
* fhir.resources (optional, for robust XML parsing)
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
* **Docker:** Required for containerized deployment (both versions).
|
|
||||||
* **Git & Maven:** Required **only** for building the **Standalone** version from source using the `.bat` script or manual steps. Not required for the Lite version build or for running pre-built Docker Hub images.
|
|
||||||
* **Windows:** Required if using the `.bat` scripts.
|
|
||||||
|
|
||||||
## Setup Instructions
|
|
||||||
|
|
||||||
### Running Pre-built Images (General Users)
|
|
||||||
|
|
||||||
This is the easiest way to get started without needing Git or Maven. Choose the version you need:
|
|
||||||
|
|
||||||
**Lite Version (No local HAPI FHIR):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull the latest Lite image
|
|
||||||
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
|
|
||||||
|
|
||||||
# Run the Lite version (maps port 5000 for the UI)
|
|
||||||
# You'll need to create local directories for persistent data first:
|
|
||||||
# mkdir instance logs static static/uploads instance/hapi-h2-data
|
|
||||||
docker run -d \
|
|
||||||
-p 5000:5000 \
|
|
||||||
-v ./instance:/app/instance \
|
|
||||||
-v ./static/uploads:/app/static/uploads \
|
|
||||||
-v ./instance/hapi-h2-data:/app/h2-data \
|
|
||||||
-v ./logs:/app/logs \
|
|
||||||
--name fhirflare-lite \
|
|
||||||
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-lite:latest
|
|
||||||
Standalone Version (Includes local HAPI FHIR):
|
|
||||||
```
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull the latest Standalone image
|
|
||||||
docker pull ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
|
||||||
|
|
||||||
# Run the Standalone version (maps ports 5000 and 8080)
|
|
||||||
# You'll need to create local directories for persistent data first:
|
|
||||||
# mkdir instance logs static static/uploads instance/hapi-h2-data
|
|
||||||
docker run -d \
|
|
||||||
-p 5000:5000 \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-v ./instance:/app/instance \
|
|
||||||
-v ./static/uploads:/app/static/uploads \
|
|
||||||
-v ./instance/hapi-h2-data:/app/h2-data \
|
|
||||||
-v ./logs:/app/logs \
|
|
||||||
--name fhirflare-standalone \
|
|
||||||
ghcr.io/sudo-jhare/fhirflare-ig-toolkit-standalone:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
Building from Source (Developers)
|
|
||||||
Using Windows .bat Scripts (Standalone Version Only):
|
|
||||||
|
|
||||||
First Time Setup:
|
|
||||||
|
|
||||||
Run Build and Run for first time.bat:
|
|
||||||
|
|
||||||
Code snippet
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd "<project folder>"
|
|
||||||
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
|
|
||||||
copy .\\hapi-fhir-Setup\\target\\classes\\application.yaml .\\hapi-fhir-jpaserver\\target\\classes\\application.yaml
|
|
||||||
mvn clean package -DskipTests=true -Pboot
|
|
||||||
docker-compose build --no-cache
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
This clones the HAPI FHIR server, copies configuration, builds the project, and starts the containers.
|
|
||||||
|
|
||||||
Subsequent Runs:
|
|
||||||
|
|
||||||
Run Run.bat:
|
|
||||||
|
|
||||||
Code snippet
|
|
||||||
```bash
|
|
||||||
cd "<project folder>"
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
This starts the Flask app (port 5000) and HAPI FHIR server (port 8080).
|
|
||||||
|
|
||||||
Access the Application:
|
|
||||||
|
|
||||||
* Flask UI: http://localhost:5000
|
|
||||||
* HAPI FHIR server: http://localhost:8080
|
|
||||||
* Manual Setup (Linux/MacOS/Windows):
|
|
||||||
|
|
||||||
Preparation (Standalone Version Only):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd <project folder>
|
|
||||||
git clone [https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git](https://github.com/hapifhir/hapi-fhir-jpaserver-starter.git) hapi-fhir-jpaserver
|
|
||||||
cp ./hapi-fhir-Setup/target/classes/application.yaml ./hapi-fhir-jpaserver/target/classes/application.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
Build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build HAPI FHIR (Standalone Version Only)
|
|
||||||
mvn clean package -DskipTests=true -Pboot
|
|
||||||
|
|
||||||
# Build Docker Image (Specify APP_MODE=lite in docker-compose.yml for Lite version)
|
|
||||||
docker-compose build --no-cache
|
|
||||||
```
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
Access the Application:
|
|
||||||
```
|
|
||||||
|
|
||||||
* Flask UI: http://localhost:5000
|
|
||||||
* HAPI FHIR server (Standalone only): http://localhost:8080
|
|
||||||
* Local Development (Without Docker):
|
|
||||||
|
|
||||||
Clone the Repository:
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone [https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git](https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git)
|
|
||||||
cd FHIRFLARE-IG-Toolkit
|
|
||||||
```
|
|
||||||
|
|
||||||
Install Dependencies:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv venv
|
|
||||||
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Install Node.js, GoFSH, and SUSHI (for FSH Converter):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Example for Debian/Ubuntu
|
|
||||||
curl -fsSL [https://deb.nodesource.com/setup_18.x](https://deb.nodesource.com/setup_18.x) | sudo bash -
|
|
||||||
sudo apt-get install -y nodejs
|
|
||||||
# Install globally
|
|
||||||
npm install -g gofsh fsh-sushi
|
|
||||||
Set Environment Variables:
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export FLASK_SECRET_KEY='your-secure-secret-key'
|
|
||||||
export API_KEY='your-api-key'
|
|
||||||
# Optional: Set APP_MODE to 'lite' if desired
|
|
||||||
# export APP_MODE='lite'
|
|
||||||
```
|
|
||||||
|
|
||||||
Initialize Directories:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p instance static/uploads logs
|
|
||||||
# Ensure write permissions if needed
|
|
||||||
# chmod -R 777 instance static/uploads logs
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the Application:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export FLASK_APP=app.py
|
|
||||||
flask run
|
|
||||||
```
|
|
||||||
Access at http://localhost:5000.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
Import an IG
|
|
||||||
### Search, View Details, and Import Packages
|
|
||||||
Navigate to **Search and Import Packages** (`/search-and-import`).
|
|
||||||
1. The page will load a list of available FHIR Implementation Guide packages from a local cache or by fetching from configured registries.
|
|
||||||
* A loading animation and progress messages are shown if fetching from registries.
|
|
||||||
* The timestamp of the last cache update is displayed.
|
|
||||||
2. Use the search bar to filter packages by name or author.
|
|
||||||
3. Packages are paginated for easier Browse.
|
|
||||||
4. For each package, you can:
|
|
||||||
* View its latest official and absolute versions.
|
|
||||||
* Click on the package name to navigate to a **detailed view** (`/package-details/<name>`) showing:
|
|
||||||
* Comprehensive metadata (author, FHIR version, canonical URL, description).
|
|
||||||
* A full list of available versions with publication dates.
|
|
||||||
* Declared dependencies.
|
|
||||||
* Other packages that depend on it (dependents).
|
|
||||||
* Version history (logs).
|
|
||||||
* Directly import a specific version using the "Import" button on the search page or the details page.
|
|
||||||
5. **Cache Management:**
|
|
||||||
* A "Clear & Refresh Cache" button is available to trigger a background task (`/api/refresh-cache-task`) that clears the local database and in-memory cache and fetches the latest package information from all configured registries. Progress is shown via a live log.
|
|
||||||
|
|
||||||
* Enter a package name (e.g., hl7.fhir.au.core) and version (e.g., 1.1.0-preview).
|
|
||||||
* Choose a dependency mode:
|
|
||||||
* Current Recursive: Import all dependencies listed in package.json recursively.
|
|
||||||
* Patch Canonical Versions: Import only canonical FHIR packages (e.g., hl7.fhir.r4.core).
|
|
||||||
* Tree Shaking: Import only dependencies containing resources actually used by the main package.
|
|
||||||
* Click Import to download the package and dependencies.
|
|
||||||
|
|
||||||
Manage IGs
|
|
||||||
Go to Manage FHIR Packages (/view-igs) to view downloaded and processed IGs.
|
|
||||||
|
|
||||||
Actions:
|
|
||||||
* Process: Extract metadata (resource types, profiles, must-support elements, examples).
|
|
||||||
* Unload: Remove processed IG data from the database.
|
|
||||||
* Delete: Remove package files from the filesystem.
|
|
||||||
|
|
||||||
Duplicates are highlighted for resolution.
|
|
||||||
|
|
||||||
View Processed IGs
|
|
||||||
|
|
||||||
After processing, view IG details (/view-ig/<id>), including:
|
|
||||||
|
|
||||||
* Resource types and profiles.
|
|
||||||
* Must-support elements and examples.
|
|
||||||
* Profile relationships (compliesWithProfile, imposeProfile) if enabled (DISPLAY_PROFILE_RELATIONSHIPS).
|
|
||||||
|
|
||||||
Interactive StructureDefinition viewer (Differential, Snapshot, Must Support, Key Elements, Constraints, Terminology, Search Params).
|
|
||||||
* Validate FHIR Resources/Bundles
|
|
||||||
* Navigate to Validate FHIR Sample (/validate-sample).
|
|
||||||
|
|
||||||
Select a package (e.g., hl7.fhir.au.core#1.1.0-preview).
|
|
||||||
|
|
||||||
* Choose Single Resource or Bundle mode.
|
|
||||||
* Paste or upload FHIR JSON/XML (e.g., a Patient resource).
|
|
||||||
* Submit to view validation errors/warnings. Note: Alpha feature; report issues to GitHub (remove PHI).
|
|
||||||
* Push IGs to a FHIR Server
|
|
||||||
* Go to Push IGs (/push-igs).
|
|
||||||
|
|
||||||
Select a downloaded package.
|
|
||||||
|
|
||||||
* Enter the Target FHIR Server URL.
|
|
||||||
* Configure Authentication (None, Bearer Token).
|
|
||||||
* Choose options: Include Dependencies, Force Upload (skips comparison check), Dry Run, Verbose Log.
|
|
||||||
* Optionally filter by Resource Types (comma-separated) or Skip Specific Files (paths within package, comma/newline separated).
|
|
||||||
* Click Push to FHIR Server to upload resources. Canonical resources are checked before upload. Identical resources are skipped unless Force Upload is checked.
|
|
||||||
* Monitor progress in the live console.
|
|
||||||
* Upload Test Data
|
|
||||||
* Navigate to Upload Test Data (/upload-test-data).
|
|
||||||
* Enter the Target FHIR Server URL.
|
|
||||||
* Configure Authentication (None, Bearer Token).
|
|
||||||
* Select one or more .json, .xml files, or a single .zip file containing test resources.
|
|
||||||
* Optionally check Validate Resources Before Upload? and select a Validation Profile Package.
|
|
||||||
* Choose Upload Mode:
|
|
||||||
* Individual Resources: Uploads each resource one by one in dependency order.
|
|
||||||
* Transaction Bundle: Uploads all resources in a single transaction.
|
|
||||||
* Optionally check Use Conditional Upload (Individual Mode Only)? to use If-Match headers for updates.
|
|
||||||
* Choose Error Handling:
|
|
||||||
* Stop on First Error: Halts the process if any validation or upload fails.
|
|
||||||
* Continue on Error: Reports errors but attempts to process/upload remaining resources.
|
|
||||||
* Click Upload and Process. The tool parses files, optionally validates, analyzes dependencies, topologically sorts resources, and uploads them according to selected options.
|
|
||||||
* Monitor progress in the streaming log output.
|
|
||||||
|
|
||||||
Convert FHIR to FSH
|
|
||||||
* Navigate to FSH Converter (/fsh-converter).
|
|
||||||
|
|
||||||
Optionally select a package for context (e.g., hl7.fhir.au.core#1.1.0-preview).
|
|
||||||
Choose input mode:
|
|
||||||
Upload File: Upload a FHIR JSON/XML file.
|
|
||||||
Paste Text: Paste FHIR JSON/XML content.
|
|
||||||
Configure options:
|
|
||||||
Output Style: file-per-definition, group-by-fsh-type, group-by-profile, single-file.
|
|
||||||
Log Level: error, warn, info, debug.
|
|
||||||
FHIR Version: R4, R4B, R5, or auto-detect.
|
|
||||||
Fishing Trip: Enable round-trip validation with SUSHI, generating a comparison report.
|
|
||||||
Dependencies: Specify additional packages (e.g., hl7.fhir.us.core@6.1.0, one per line).
|
|
||||||
Indent Rules: Enable context path indentation for readable FSH.
|
|
||||||
Meta Profile: Choose only-one, first, or none for meta.profile handling.
|
|
||||||
Alias File: Upload an FSH file with aliases (e.g., $MyAlias = http://example.org).
|
|
||||||
No Alias: Disable automatic alias generation.
|
|
||||||
Click Convert to FSH to generate and display FSH output, with a waiting spinner (light/dark theme) during processing.
|
|
||||||
If Fishing Trip is enabled, view the comparison report via the "Click here for SUSHI Validation" badge button.
|
|
||||||
Download the result as a .fsh file.
|
|
||||||
Retrieve and Split Bundles
|
|
||||||
Navigate to Retrieve/Split Data (/retrieve-split-data).
|
|
||||||
|
|
||||||
Retrieve Bundles from Server:
|
|
||||||
|
|
||||||
* Enter the FHIR Server URL (defaults to the proxy if empty).
|
|
||||||
* Select one or more Resource Types to retrieve (e.g., Patient, Observation).
|
|
||||||
* Optionally check Fetch Referenced Resources.
|
|
||||||
* If checked, further optionally check Fetch Full Reference Bundles to retrieve entire bundles for each referenced type (e.g., all Patients if a Patient is referenced) instead of individual resources by ID.
|
|
||||||
* Click Retrieve Bundles.
|
|
||||||
* Monitor progress in the streaming log. A ZIP file containing the retrieved bundles/resources will be prepared for download.
|
|
||||||
|
|
||||||
Split Uploaded Bundles:
|
|
||||||
|
|
||||||
* Upload a ZIP file containing FHIR bundles (JSON format).
|
|
||||||
* Click Split Bundles.
|
|
||||||
* A ZIP file containing individual resources extracted from the bundles will be prepared for download.
|
|
||||||
* Explore FHIR Operations
|
|
||||||
* Navigate to FHIR UI Operations (/fhir-ui-operations).
|
|
||||||
|
|
||||||
Toggle between local HAPI (/fhir) or a custom FHIR server.
|
|
||||||
|
|
||||||
* Click Fetch Metadata to load the server’s CapabilityStatement.
|
|
||||||
* Select a resource type (e.g., Patient, Observation) or System to view operations:
|
|
||||||
* System operations: GET /metadata, POST /, GET /_history, GET/POST /$diff, POST /$reindex, POST /$expunge, etc.
|
|
||||||
* Resource operations: GET Patient/:id, POST Observation/_search, etc.
|
|
||||||
* Use Try it out to input parameters or request bodies, then Execute to view results in JSON, XML, or narrative formats.
|
|
||||||
|
|
||||||
### Configure Embedded HAPI FHIR Server (Standalone Mode)
|
|
||||||
For users running the **Standalone version**, which includes an embedded HAPI FHIR server.
|
|
||||||
1. Navigate to **Configure HAPI FHIR** (`/config-hapi`).
|
|
||||||
2. The page displays the content of the HAPI FHIR server's `application.yaml` file.
|
|
||||||
3. You can edit the configuration directly in the text area.
|
|
||||||
* *Caution: Incorrect modifications can break the HAPI FHIR server.*
|
|
||||||
4. Click **Save Configuration** to apply your changes to the `application.yaml` file.
|
|
||||||
5. Click **Restart Tomcat** to restart the HAPI FHIR server and load the new configuration. The restart process may take a few moments.
|
|
||||||
|
|
||||||
API Usage
|
|
||||||
Import IG
|
|
||||||
Bash
|
|
||||||
|
|
||||||
curl -X POST http://localhost:5000/api/import-ig \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-d '{"package_name": "hl7.fhir.au.core", "version": "1.1.0-preview", "dependency_mode": "recursive"}'
|
|
||||||
Returns complies_with_profiles, imposed_profiles, and duplicate_packages_present info.
|
|
||||||
|
|
||||||
### Refresh Package Cache (Background Task)
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/refresh-cache-task \
|
|
||||||
-H "X-API-Key: your-api-key"
|
|
||||||
```
|
|
||||||
|
|
||||||
Push IG
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/push-ig \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: application/x-ndjson" \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-d '{
|
|
||||||
"package_name": "hl7.fhir.au.core",
|
|
||||||
"version": "1.1.0-preview",
|
|
||||||
"fhir_server_url": "http://localhost:8080/fhir",
|
|
||||||
"include_dependencies": true,
|
|
||||||
"force_upload": false,
|
|
||||||
"dry_run": false,
|
|
||||||
"verbose": false,
|
|
||||||
"auth_type": "none"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a streaming NDJSON response with progress and final summary.
|
|
||||||
|
|
||||||
Upload Test Data
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/upload-test-data \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-H "Accept: application/x-ndjson" \
|
|
||||||
-F "fhir_server_url=http://your-fhir-server/fhir" \
|
|
||||||
-F "auth_type=bearerToken" \
|
|
||||||
-F "auth_token=YOUR_TOKEN" \
|
|
||||||
-F "upload_mode=individual" \
|
|
||||||
-F "error_handling=continue" \
|
|
||||||
-F "validate_before_upload=true" \
|
|
||||||
-F "validation_package_id=hl7.fhir.r4.core#4.0.1" \
|
|
||||||
-F "use_conditional_uploads=true" \
|
|
||||||
-F "test_data_files=@/path/to/your/patient.json" \
|
|
||||||
-F "test_data_files=@/path/to/your/observations.zip"
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a streaming NDJSON response with progress and final summary. Uses multipart/form-data for file uploads.
|
|
||||||
|
|
||||||
Retrieve Bundles
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/retrieve-bundles \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-H "Accept: application/x-ndjson" \
|
|
||||||
-F "fhir_server_url=http://your-fhir-server/fhir" \
|
|
||||||
-F "resources=Patient" \
|
|
||||||
-F "resources=Observation" \
|
|
||||||
-F "validate_references=true" \
|
|
||||||
-F "fetch_reference_bundles=false"
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a streaming NDJSON response with progress. The X-Zip-Path header in the final response part will contain the path to download the ZIP archive (e.g., /tmp/retrieved_bundles_datetime.zip).
|
|
||||||
|
|
||||||
Split Bundles
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/split-bundles \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-H "Accept: application/x-ndjson" \
|
|
||||||
-F "split_bundle_zip_path=@/path/to/your/bundles.zip"
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns a streaming NDJSON response. The X-Zip-Path header in the final response part will contain the path to download the ZIP archive of split resources.
|
|
||||||
|
|
||||||
Validate Resource/Bundle
|
|
||||||
Not yet exposed via API; use the UI at /validate-sample.
|
|
||||||
|
|
||||||
Configuration Options
|
|
||||||
Located in app.py:
|
|
||||||
|
|
||||||
* VALIDATE_IMPOSED_PROFILES: (Default: True) Validates resources against imposed profiles during push.
|
|
||||||
* DISPLAY_PROFILE_RELATIONSHIPS: (Default: True) Shows compliesWithProfile and imposeProfile in the UI.
|
|
||||||
* FHIR_PACKAGES_DIR: (Default: /app/instance/fhir_packages) Stores .tgz packages and metadata.
|
|
||||||
* UPLOAD_FOLDER: (Default: /app/static/uploads) Stores GoFSH output files and FSH comparison reports.
|
|
||||||
* SECRET_KEY: Required for CSRF protection and sessions. Set via environment variable or directly.
|
|
||||||
* API_KEY: Required for API authentication. Set via environment variable or directly.
|
|
||||||
* MAX_CONTENT_LENGTH: (Default: Flask default) Max size for HTTP request body (e.g., 16 * 1024 * 1024 for 16MB). Important for large uploads.
|
|
||||||
* MAX_FORM_PARTS: (Default: Werkzeug default, often 1000) Default max number of form parts. Overridden for /api/upload-test-data by CustomFormDataParser.
|
|
||||||
|
|
||||||
### Get HAPI FHIR Configuration (Standalone Mode)
|
|
||||||
```bash
|
|
||||||
curl -X GET http://localhost:5000/api/config \
|
|
||||||
-H "X-API-Key: your-api-key"
|
|
||||||
```
|
|
||||||
Save HAPI FHIR Configuration:
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "X-API-Key: your-api-key" \
|
|
||||||
-d '{"your_yaml_key": "your_value", ...}' # Send the full YAML content as JSON
|
|
||||||
```
|
|
||||||
Restart HAPI FHIR Server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:5000/api/restart-tomcat \
|
|
||||||
-H "X-API-Key: your-api-key"
|
|
||||||
```
|
|
||||||
|
|
||||||
Testing
|
|
||||||
The project includes a test suite covering UI, API, database, file operations, and security.
|
|
||||||
|
|
||||||
Test Prerequisites:
|
|
||||||
|
|
||||||
pytest: For running tests.
|
|
||||||
pytest-mock: For mocking dependencies. Install: pip install pytest pytest-mock
|
|
||||||
Running Tests:
|
|
||||||
|
|
||||||
Bash
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd <project folder>
|
|
||||||
pytest tests/test_app.py -v
|
|
||||||
```
|
|
||||||
Test Coverage:
|
|
||||||
|
|
||||||
* UI Pages: Homepage, Import IG, Manage IGs, Push IGs, Validate Sample, View Processed IG, FSH Converter, Upload Test Data, Retrieve/Split Data.
|
|
||||||
* API Endpoints: POST /api/import-ig, POST /api/push-ig, GET /get-structure, GET /get-example, POST /api/upload-test-data, POST /api/retrieve-bundles, POST /api/split-bundles.
|
|
||||||
* Database: IG processing, unloading, viewing.
|
|
||||||
* File Operations: Package processing, deletion, FSH output, ZIP handling.
|
|
||||||
* Security: CSRF protection, flash messages, secret key.
|
|
||||||
* FSH Converter: Form submission, file/text input, GoFSH execution, Fishing Trip comparison.
|
|
||||||
* Upload Test Data: Parsing, dependency graph, sorting, upload modes, validation, conditional uploads.
|
|
||||||
|
|
||||||
Development Notes
|
|
||||||
|
|
||||||
Background
|
|
||||||
|
|
||||||
The toolkit addresses the need for a comprehensive FHIR IG management tool, with recent enhancements for resource validation, FSH conversion with advanced GoFSH features, flexible versioning, improved IG pushing, dependency-aware test data uploading, and bundle retrieval/splitting, making it a versatile platform for FHIR developers.
|
|
||||||
|
|
||||||
Technical Decisions
|
|
||||||
|
|
||||||
* Flask: Lightweight and flexible for web development.
|
|
||||||
* SQLite: Simple for development; consider PostgreSQL for production.
|
|
||||||
* Bootstrap 5.3.3: Responsive UI with custom styling.
|
|
||||||
* Lottie-Web: Renders themed animations for FSH conversion waiting spinner.
|
|
||||||
* GoFSH/SUSHI: Integrated via Node.js for advanced FSH conversion and round-trip validation.
|
|
||||||
* Docker: Ensures consistent deployment with Flask and HAPI FHIR.
|
|
||||||
* Flexible Versioning: Supports non-standard IG versions (e.g., -preview, -ballot).
|
|
||||||
* Live Console/Streaming: Real-time feedback for complex operations (Push, Upload Test Data, FSH, Retrieve Bundles).
|
|
||||||
* Validation: Alpha feature with ongoing FHIRPath improvements.
|
|
||||||
* Dependency Management: Uses topological sort for Upload Test Data feature.
|
|
||||||
* Form Parsing: Uses custom Werkzeug parser for Upload Test Data to handle large numbers of files.
|
|
||||||
|
|
||||||
Recent Updates
|
|
||||||
|
|
||||||
* Enhanced package search page with caching, detailed views (dependencies, dependents, version history), and background cache refresh.
|
|
||||||
* Upload Test Data Enhancements (April 2025):
|
|
||||||
* Added optional Pre-Upload Validation against selected IG profiles.
|
|
||||||
* Added optional Conditional Uploads (GET + POST/PUT w/ If-Match) for individual mode.
|
|
||||||
* Implemented robust XML parsing using fhir.resources library (when available).
|
|
||||||
* Fixed 413 Request Entity Too Large errors for large file counts using a custom Werkzeug FormDataParser.
|
|
||||||
* Path: templates/upload_test_data.html, app.py, services.py, forms.py.
|
|
||||||
* Push IG Enhancements (April 2025):
|
|
||||||
* Added semantic comparison to skip uploading identical resources.
|
|
||||||
* Added "Force Upload" option to bypass comparison.
|
|
||||||
* Improved handling of canonical resources (search before PUT/POST).
|
|
||||||
* Added filtering by specific files to skip during push.
|
|
||||||
* More detailed summary report in stream response.
|
|
||||||
* Path: templates/cp_push_igs.html, app.py, services.py.
|
|
||||||
* Waiting Spinner for FSH Converter (April 2025):
|
|
||||||
* Added a themed (light/dark) Lottie animation spinner during FSH execution.
|
|
||||||
* Path: templates/fsh_converter.html, static/animations/, static/js/lottie-web.min.js.
|
|
||||||
* Advanced FSH Converter (April 2025):
|
|
||||||
* Added support for GoFSH advanced options: --fshing-trip, --dependency, --indent, --meta-profile, --alias-file, --no-alias.
|
|
||||||
* Displays Fishing Trip comparison reports.
|
|
||||||
* Path: templates/fsh_converter.html, app.py, services.py, forms.py.
|
|
||||||
* (New) Retrieve and Split Data (May 2025):
|
|
||||||
* Added UI and API for retrieving bundles from a FHIR server by resource type.
|
|
||||||
* Added options to fetch referenced resources (individually or as full type bundles).
|
|
||||||
* Added functionality to split uploaded ZIP files of bundles into individual resources.
|
|
||||||
* Streaming log for retrieval and ZIP download for results.
|
|
||||||
* Paths: templates/retrieve_split_data.html, app.py, services.py, forms.py.
|
|
||||||
* Known Issues and Workarounds
|
|
||||||
* Favicon 404: Clear browser cache or verify /app/static/favicon.ico.
|
|
||||||
* CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
|
||||||
* Import Fails: Check package name/version and connectivity.
|
|
||||||
* Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
|
|
||||||
* Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
|
|
||||||
* Permissions: Ensure instance/ and static/uploads/ are writable.
|
|
||||||
* GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation.
|
|
||||||
* Upload Test Data XML Parsing: Relies on fhir.resources library for full validation; basic parsing used as fallback. Complex XML structures might not be fully analyzed for dependencies with basic parsing. Prefer JSON for reliable dependency analysis.
|
|
||||||
* 413 Request Entity Too Large: Primarily handled by CustomFormDataParser for /api/upload-test-data. Check the parser's max_form_parts limit if still occurring. MAX_CONTENT_LENGTH in app.py controls overall size. Reverse proxy limits (client_max_body_size in Nginx) might also apply.
|
|
||||||
|
|
||||||
|
|
||||||
Future Improvements
|
|
||||||
|
|
||||||
* Upload Test Data: Improve XML parsing further (direct XML->fhir.resource object if possible), add visual progress bar, add upload order preview, implement transaction bundle size splitting, add 'Clear Target Server' option (with confirmation).
|
|
||||||
* Validation: Enhance FHIRPath for complex constraints; add API endpoint.
|
|
||||||
* Sorting: Sort IG versions in /view-igs (e.g., ascending).
|
|
||||||
* Duplicate Resolution: Options to keep latest version or merge resources.
|
|
||||||
* Production Database: Support PostgreSQL.
|
|
||||||
* Error Reporting: Detailed validation error paths in the UI.
|
|
||||||
* FSH Enhancements: Add API endpoint for FSH conversion; support inline instance construction.
|
|
||||||
* FHIR Operations: Add complex parameter support (e.g., /$diff with left/right).
|
|
||||||
* Retrieve/Split Data: Add option to filter resources during retrieval (e.g., by date, specific IDs).
|
|
||||||
|
|
||||||
Completed Items
|
|
||||||
|
|
||||||
* Testing suite with basic coverage.
|
|
||||||
* API endpoints for POST /api/import-ig and POST /api/push-ig.
|
|
||||||
* Flexible versioning (-preview, -ballot).
|
|
||||||
* CSRF fixes for forms.
|
|
||||||
* Resource validation UI (alpha).
|
|
||||||
* FSH Converter with advanced GoFSH features and waiting spinner.
|
|
||||||
* Push IG enhancements (force upload, semantic comparison, canonical handling, skip files).
|
|
||||||
* Upload Test Data feature with dependency sorting, multiple upload modes, pre-upload validation, conditional uploads, robust XML parsing, and fix for large file counts.
|
|
||||||
* Retrieve and Split Data functionality with reference fetching and ZIP download.
|
|
||||||
* Far-Distant Improvements
|
|
||||||
* Cache Service: Use Redis for IG metadata caching.
|
|
||||||
* Database Optimization: Composite index on ProcessedIg.package_name and ProcessedIg.version.
|
|
||||||
|
|
||||||
|
|
||||||
Directory Structure
|
|
||||||
```bash
|
|
||||||
FHIRFLARE-IG-Toolkit/
|
|
||||||
├── app.py # Main Flask application
|
|
||||||
├── Build and Run for first time.bat # Windows script for first-time Docker setup
|
|
||||||
├── docker-compose.yml # Docker Compose configuration
|
|
||||||
├── Dockerfile # Docker configuration
|
|
||||||
├── forms.py # Form definitions
|
|
||||||
├── LICENSE.md # Apache 2.0 License
|
|
||||||
├── README.md # Project documentation
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Run.bat # Windows script for running Docker
|
|
||||||
├── services.py # Logic for IG import, processing, validation, pushing, FSH conversion, test data upload, retrieve/split
|
|
||||||
├── supervisord.conf # Supervisor configuration
|
|
||||||
├── hapi-fhir-Setup/
|
|
||||||
│ ├── README.md # HAPI FHIR setup instructions
|
|
||||||
│ └── target/
|
|
||||||
│ └── classes/
|
|
||||||
│ └── application.yaml # HAPI FHIR configuration
|
|
||||||
├── instance/
|
|
||||||
│ ├── fhir_ig.db # SQLite database
|
|
||||||
│ ├── fhir_ig.db.old # Database backup
|
|
||||||
│ └── fhir_packages/ # Stored IG packages and metadata
|
|
||||||
│ ├── ... (example packages) ...
|
|
||||||
├── logs/
|
|
||||||
│ ├── flask.log # Flask application logs
|
|
||||||
│ ├── flask_err.log # Flask error logs
|
|
||||||
│ ├── supervisord.log # Supervisor logs
|
|
||||||
│ ├── supervisord.pid # Supervisor PID file
|
|
||||||
│ ├── tomcat.log # Tomcat logs for HAPI FHIR
|
|
||||||
│ └── tomcat_err.log # Tomcat error logs
|
|
||||||
├── static/
|
|
||||||
│ ├── animations/
|
|
||||||
│ │ ├── loading-dark.json # Dark theme spinner animation
|
|
||||||
│ │ └── loading-light.json # Light theme spinner animation
|
|
||||||
│ ├── favicon.ico # Application favicon
|
|
||||||
│ ├── FHIRFLARE.png # Application logo
|
|
||||||
│ ├── js/
|
|
||||||
│ │ └── lottie-web.min.js # Lottie library for spinner
|
|
||||||
│ └── uploads/
|
|
||||||
│ ├── output.fsh # Generated FSH output (temp location)
|
|
||||||
│ └── fsh_output/ # GoFSH output directory
|
|
||||||
│ ├── ... (example GoFSH output) ...
|
|
||||||
├── templates/
|
|
||||||
│ ├── base.html # Base template
|
|
||||||
│ ├── cp_downloaded_igs.html # UI for managing IGs
|
|
||||||
│ ├── cp_push_igs.html # UI for pushing IGs
|
|
||||||
│ ├── cp_view_processed_ig.html # UI for viewing processed IGs
|
|
||||||
│ ├── fhir_ui.html # UI for FHIR API explorer
|
|
||||||
│ ├── fhir_ui_operations.html # UI for FHIR server operations
|
|
||||||
│ ├── fsh_converter.html # UI for FSH conversion
|
|
||||||
│ ├── import_ig.html # UI for importing IGs
|
|
||||||
│ ├── index.html # Homepage
|
|
||||||
│ ├── retrieve_split_data.html # UI for Retrieve and Split Data
|
|
||||||
│ ├── upload_test_data.html # UI for Uploading Test Data
|
|
||||||
│ ├── validate_sample.html # UI for validating resources/bundles
|
|
||||||
│ ├── config_hapi.html # UI for HAPI FHIR Configuration
|
|
||||||
│ └── _form_helpers.html # Form helper macros
|
|
||||||
├── tests/
|
|
||||||
│ └── test_app.py # Test suite
|
|
||||||
└── hapi-fhir-jpaserver/ # HAPI FHIR server resources (if Standalone)
|
|
||||||
```
|
|
||||||
|
|
||||||
Contributing
|
|
||||||
|
|
||||||
1. Fork the repository.
|
|
||||||
1. Create a feature branch (git checkout -b feature/your-feature).
|
|
||||||
1. Commit changes (git commit -m "Add your feature").
|
|
||||||
1. Push to your branch (git push origin feature/your-feature).
|
|
||||||
1. Open a Pull Request.
|
|
||||||
1. Ensure code follows PEP 8 and includes tests in tests/test_app.py.
|
|
||||||
|
|
||||||
Troubleshooting
|
|
||||||
|
|
||||||
* Favicon 404: Clear browser cache or verify /app/static/favicon.ico: docker exec -it <container_name> curl http://localhost:5000/static/favicon.ico
|
|
||||||
* CSRF Errors: Set FLASK_SECRET_KEY and ensure {{ form.hidden_tag() }} in forms.
|
|
||||||
* Import Fails: Check package name/version and connectivity.
|
|
||||||
* Validation Accuracy: Alpha feature; report issues to GitHub (remove PHI).
|
|
||||||
* Package Parsing: Non-standard .tgz filenames may parse incorrectly. Fallback uses name-only parsing.
|
|
||||||
* Permissions: Ensure instance/ and static/uploads/ are writable: chmod -R 777 instance static/uploads logs
|
|
||||||
* GoFSH/SUSHI Errors: Check ./logs/flask_err.log for ERROR:services:GoFSH failed. Ensure valid FHIR inputs and SUSHI installation: docker exec -it <container_name> sushi --version
|
|
||||||
* 413 Request Entity Too Large: Increase MAX_CONTENT_LENGTH and MAX_FORM_PARTS in app.py. If using a reverse proxy (e.g., Nginx), increase its client_max_body_size setting as well. Ensure the application/container is fully restarted/rebuilt.
|
|
||||||
|
|
||||||
License
|
|
||||||
|
|
||||||
Licensed under the Apache 2.0 License. See LICENSE.md for details.
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
title: FHIRFLARE IG Toolkit
|
|
||||||
description: Helm chart for deploying the fhirflare-ig-toolkit application
|
|
||||||
remote_theme: pages-themes/modernist@v0.2.0
|
|
||||||
plugins:
|
|
||||||
- jekyll-remote-theme
|
|
||||||
- jekyll-seo-tag
|
|
||||||
|
|
||||||
show_downloads: true
|
|
||||||
google_analytics:
|
|
||||||
|
|
||||||
markdown: kramdown
|
|
||||||
highlighter: rouge
|
|
||||||
|
|
||||||
|
|
||||||
url: "https://sudo-jhare.github.io/"
|
|
||||||
baseurl: "FHIRFLARE-IG-Toolkit/"
|
|
||||||
|
|
||||||
github:
|
|
||||||
repository_url: https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
layout: default
|
|
||||||
---
|
|
||||||
|
|
||||||
# FHIRFLARE IG Toolkit
|
|
||||||
|
|
||||||
{{ site.description }}
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
FHIRFLARE-IG-Toolkit is a comprehensive solution for working with FHIR Implementation Guides.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Helm chart deployment
|
|
||||||
- FHIR resources management
|
|
||||||
- Implementation Guide toolkit
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
Check out the [documentation](README.md) to get started with FHIRFLARE IG Toolkit.
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 706 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user