diff --git a/.github/workflows/build-and-push-image.yml b/.github/workflows/build-and-push-image.yml
index a6515f6f7c..71a94d524e 100644
--- a/.github/workflows/build-and-push-image.yml
+++ b/.github/workflows/build-and-push-image.yml
@@ -41,7 +41,7 @@ jobs:
steps.check-prod-image.outputs.ops-build-needed }}
steps:
- name: Configure AWS Dev Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GithubDeployECSService
aws-region: eu-west-2
@@ -57,7 +57,7 @@ jobs:
fi
- name: Configure AWS Production credentials
if: env.PUSH_IMAGE_TO_PRODUCTION == 'true'
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::820242920762:role/GithubDeployECSService
aws-region: eu-west-2
@@ -102,25 +102,25 @@ jobs:
aws-role: ${{ fromJSON(needs.define-matrix.outputs.aws-roles) }}
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ env.git_ref }}
- name: Write build SHA
run: git rev-parse HEAD > public/sha
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: ${{ matrix.aws-role }}
aws-region: eu-west-2
- name: Login to ECR
id: login-ecr
- uses: aws-actions/amazon-ecr-login@v2
+ uses: aws-actions/amazon-ecr-login@376925c9d111252e87ae59691e5a442dd100ef6a # v2.1.3
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v4
+ uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
# yamllint disable rule:line-length
- name: Build and push webapp image
if: needs.check-image-presence.outputs.webapp-build-needed == 'true'
- uses: docker/build-push-action@v7
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
push: true
@@ -132,7 +132,7 @@ jobs:
}}/mavis/webapp:buildcache,mode=max,image-manifest=true,oci-mediatypes=true
- name: Build and push ops image
if: needs.check-image-presence.outputs.ops-build-needed == 'true'
- uses: docker/build-push-action@v7
+ uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: .
file: ops.Dockerfile
diff --git a/.github/workflows/call-end-to-end-tests.yml b/.github/workflows/call-end-to-end-tests.yml
index 34e9b12ef6..114d0e36e7 100644
--- a/.github/workflows/call-end-to-end-tests.yml
+++ b/.github/workflows/call-end-to-end-tests.yml
@@ -3,7 +3,10 @@ name: Call end-to-end tests
on:
workflow_call:
inputs:
- cross_service_tests:
+ fhir_api_tests:
+ required: true
+ type: boolean
+ reporting_tests:
required: true
type: boolean
endpoint:
@@ -31,7 +34,8 @@ jobs:
# yamllint disable-line rule:line-length
uses: NHSDigital/manage-vaccinations-in-schools-testing/.github/workflows/end-to-end-tests.yaml@main
with:
- cross_service_tests: ${{ inputs.cross_service_tests }}
+ fhir_api_tests: ${{ inputs.fhir_api_tests }}
+ reporting_tests: ${{ inputs.reporting_tests }}
github_ref: ${{ inputs.github_ref }}
endpoint: ${{ inputs.endpoint }}
secrets:
diff --git a/.github/workflows/continuous-deployment.yml b/.github/workflows/continuous-deployment.yml
index 16c3a32dc2..01578c22cc 100644
--- a/.github/workflows/continuous-deployment.yml
+++ b/.github/workflows/continuous-deployment.yml
@@ -28,7 +28,8 @@ jobs:
uses: ./.github/workflows/call-end-to-end-tests.yml
secrets: inherit
with:
- cross_service_tests: true
+ fhir_api_tests: true
+ reporting_tests: true
endpoint: https://qa.mavistesting.com
github_ref: main
slack-notification:
diff --git a/.github/workflows/create_dockerized_db.yml b/.github/workflows/create_dockerized_db.yml
index ae5cb9b552..cb12738052 100644
--- a/.github/workflows/create_dockerized_db.yml
+++ b/.github/workflows/create_dockerized_db.yml
@@ -29,11 +29,11 @@ jobs:
RAILS_MASTER_KEY: intentionally-insecure-dev-key00
SKIP_TEST_DATABASE: true
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.github_ref || github.ref_name == 'next' && 'next' || github.ref_name }}
repository: nhsuk/manage-vaccinations-in-schools
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .tool-versions
cache: yarn
@@ -56,7 +56,7 @@ jobs:
sleep 2
done
'
- - uses: ruby/setup-ruby@v1
+ - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
- name: Populate database for testing
@@ -65,13 +65,13 @@ jobs:
bin/rails feature_flags:enable_for_development
bin/mavis gias import --input-file=spec/fixtures/dfe-schools.zip
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
- name: Login to ECR
id: login-ecr
- uses: aws-actions/amazon-ecr-login@v2
+ uses: aws-actions/amazon-ecr-login@376925c9d111252e87ae59691e5a442dd100ef6a # v2.1.3
# yamllint disable rule:line-length
- name: get github ref short
id: github-ref
diff --git a/.github/workflows/data-replication-pipeline.yml b/.github/workflows/data-replication-pipeline.yml
index 9c8fc4ca54..75604ca7df 100644
--- a/.github/workflows/data-replication-pipeline.yml
+++ b/.github/workflows/data-replication-pipeline.yml
@@ -70,7 +70,7 @@ jobs:
git-sha: ${{ steps.get-git-sha.outputs.git-sha }}
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ env.git_ref_to_deploy }}
- name: Get git sha
@@ -93,11 +93,11 @@ jobs:
id-token: write
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ env.git_ref_to_deploy }}
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: ${{ env.aws_role }}
aws-region: eu-west-2
@@ -125,7 +125,7 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Populate web task definition
id: create-task-definition
- uses: aws-actions/amazon-ecs-render-task-definition@v1
+ uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
task-definition-family:
mavis-data-replication-task-definition-${{ inputs.environment }}-template
@@ -140,7 +140,7 @@ jobs:
mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp
}}/data-replication-task-definition.json
- name: Upload artifact for data-replication task definition
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.environment }}-data-replication-task-definition
path: ${{ runner.temp }}/data-replication-task-definition.json
@@ -189,12 +189,12 @@ jobs:
id-token: write
steps:
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: ${{ env.aws_role }}
aws-region: eu-west-2
- name: Download data-replication task definition artifact
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-data-replication-task-definition
@@ -205,7 +205,7 @@ jobs:
jq --arg f "$family_name" '.family = $f' "$file_path" > tmpfile && mv tmpfile "$file_path"
- name: Deploy data-replication service
id: ecs-deploy
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2
+ uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2.6.1
with:
task-definition: ${{ runner.temp }}/data-replication-task-definition.json
cluster: mavis-${{ inputs.environment }}-data-replication
diff --git a/.github/workflows/deploy-documentation.yml b/.github/workflows/deploy-documentation.yml
index e557a03232..9c227712ad 100644
--- a/.github/workflows/deploy-documentation.yml
+++ b/.github/workflows/deploy-documentation.yml
@@ -18,8 +18,8 @@ jobs:
cancel-in-progress: true
steps:
- - uses: actions/checkout@v6
- - uses: ruby/setup-ruby@v1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
@@ -27,7 +27,7 @@ jobs:
run: bundle exec rake rdoc:generate
- name: Deploy to GitHub Pages
- uses: JamesIves/github-pages-deploy-action@v4
+ uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0
with:
branch: gh-pages
folder: docs/rdoc
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 76f3444b8a..9716d5d539 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -80,7 +80,7 @@ jobs:
fi
fi
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.git_ref_to_deploy || github.sha }}
- name: Get git sha
@@ -109,12 +109,12 @@ jobs:
repository_name: ${{ matrix.service == 'ops' && 'mavis/ops' || 'mavis/webapp' }}
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
id: checkout-code
with:
ref: ${{ needs.validate-and-resolve-sha.outputs.git-sha }}
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
@@ -131,7 +131,7 @@ jobs:
echo "digest=$digest" >> "$GITHUB_OUTPUT"
- name: Populate task definition
id: create-task-definition
- uses: aws-actions/amazon-ecs-render-task-definition@v1
+ uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
task-definition-family:
mavis-${{ matrix.service }}-task-definition-${{ inputs.environment }}-template
@@ -147,7 +147,7 @@ jobs:
mv ${{ steps.create-task-definition.outputs.task-definition }} ${{ runner.temp }}/${{
matrix.service }}-task-definition.json
- name: Upload artifact for ${{ matrix.service }} task definition
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.environment }}-${{ matrix.service }}-task-definition
path: ${{ runner.temp }}/${{ matrix.service }}-task-definition.json
@@ -196,14 +196,14 @@ jobs:
id-token: write
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Download ops task definition artifact
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-ops-task-definition
@@ -332,12 +332,12 @@ jobs:
fromJSON(format('["{0}"]', inputs.server_types)) }}
steps:
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Download ${{ matrix.service }} task definition artifact
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-${{ matrix.service }}-task-definition
@@ -348,7 +348,7 @@ jobs:
jq --arg f "$family_name" '.family = $f' "$file_path" > tmpfile && mv tmpfile "$file_path"
- name: Deploy ${{ matrix.service }} service
id: ecs-deploy
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2
+ uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2.6.1
with:
task-definition: ${{ runner.temp }}/${{ matrix.service }}-task-definition.json
cluster: ${{ env.cluster_name }}
@@ -376,14 +376,14 @@ jobs:
id-token: write
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::${{ env.aws_account_id }}:role/GithubDeployECSService
aws-region: eu-west-2
- name: Download ops task definition artifact
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ runner.temp }}
name: ${{ inputs.environment }}-ops-task-definition
diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml
index 14f0e1b615..6765a9dddc 100644
--- a/.github/workflows/draft-new-release.yml
+++ b/.github/workflows/draft-new-release.yml
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Generate release notes
diff --git a/.github/workflows/end-to-end-tests-aws.yml b/.github/workflows/end-to-end-tests-aws.yml
index 2f3ce8aae4..28c9740c4d 100644
--- a/.github/workflows/end-to-end-tests-aws.yml
+++ b/.github/workflows/end-to-end-tests-aws.yml
@@ -26,11 +26,11 @@ jobs:
application-image-git-ref: ${{ steps.check-image.outputs.GIT_REF_SHA }}
steps:
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
repository: nhsuk/manage-vaccinations-in-schools
@@ -59,18 +59,18 @@ jobs:
id-token: write
steps:
- name: Checkout code
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.check-development-image-presence.outputs.application-image-git-ref }}
repository: nhsuk/manage-vaccinations-in-schools
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
- name: Login to ECR
id: login-ecr
- uses: aws-actions/amazon-ecr-login@v2
+ uses: aws-actions/amazon-ecr-login@376925c9d111252e87ae59691e5a442dd100ef6a # v2.1.3
- name: Build and push mavis/development docker image
# yamllint disable rule:line-length
run: |
@@ -91,11 +91,11 @@ jobs:
db_git_ref_sha: ${{ steps.check-image.outputs.GIT_REF_SHA }}
steps:
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
repository: nhsuk/manage-vaccinations-in-schools
@@ -145,13 +145,13 @@ jobs:
run_task_arn: ${{ steps.run-task.outputs.run-task-arn }}
steps:
- name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
- name: Render task definition web
id: render-task-definition-web
- uses: aws-actions/amazon-ecs-render-task-definition@v1
+ uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
task-definition-family: assurance-testing-mavis-development-task-definition-template
container-name: mavis-development-web
@@ -161,7 +161,7 @@ jobs:
needs.check-development-image-presence.outputs.application-image-git-ref }}
- name: Render task definition database
id: render-task-definition-database
- uses: aws-actions/amazon-ecs-render-task-definition@v1
+ uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
task-definition: ${{ steps.render-task-definition-web.outputs.task-definition }}
container-name: mavis-development-db
@@ -171,7 +171,7 @@ jobs:
needs.check-database-image-presence.outputs.db_git_ref_sha }}
- name: Render task definition sidekiq
id: render-task-definition-sidekiq
- uses: aws-actions/amazon-ecs-render-task-definition@v1
+ uses: aws-actions/amazon-ecs-render-task-definition@77954e213ba1f9f9cb016b86a1d4f6fcdea0d57e # v1.8.4
with:
task-definition: ${{ steps.render-task-definition-database.outputs.task-definition }}
container-name: mavis-development-sidekiq
@@ -197,7 +197,7 @@ jobs:
echo "run-task-subnets=$subnet_id" >> "$GITHUB_OUTPUT"
echo "run-task-security-groups=$security_group_id" >> "$GITHUB_OUTPUT"
- name: Deploy task definition
- uses: aws-actions/amazon-ecs-deploy-task-definition@v2
+ uses: aws-actions/amazon-ecs-deploy-task-definition@fc8fc60f3a60ffd500fcb13b209c59d221ac8c8c # v2.6.1
id: run-task
with:
task-definition: "assurance-testing-mavis-development-task-definition.json"
@@ -218,7 +218,7 @@ jobs:
container_ip: ${{ steps.compile-outputs.outputs.container_ip }}
steps:
- name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
@@ -349,7 +349,8 @@ jobs:
contents: write
id-token: write
with:
- cross_service_tests: false
+ fhir_api_tests: false
+ reporting_tests: true
github_ref: ${{ needs.find-correct-test-branch.outputs.test_branch }}
endpoint: http://${{ needs.wait-for-task-stability.outputs.container_ip }}:4000
stop-docker-environment:
@@ -360,7 +361,7 @@ jobs:
id-token: write
steps:
- name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v6
+ uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
with:
role-to-assume: arn:aws:iam::393416225559:role/GitHubAssuranceTestRole
aws-region: eu-west-2
diff --git a/.github/workflows/end-to-end-tests-local.yml b/.github/workflows/end-to-end-tests-local.yml
index 361e109776..b6793a5f6c 100644
--- a/.github/workflows/end-to-end-tests-local.yml
+++ b/.github/workflows/end-to-end-tests-local.yml
@@ -22,16 +22,16 @@ jobs:
RAILS_ENV: end_to_end
steps:
- name: Checkout base branch
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.base_ref }}
- name: Setup Node.js on base branch
- uses: actions/setup-node@v6
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .tool-versions
cache: yarn
- name: Setup Ruby on base branch
- uses: ruby/setup-ruby@v1
+ uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
- name: Install JS dependencies on base branch
@@ -39,16 +39,16 @@ jobs:
- name: Setup database
run: bin/rails db:setup
- name: Checkout head branch
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.head_ref }}
- name: Setup Node.js on head branch
- uses: actions/setup-node@v6
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .tool-versions
cache: yarn
- name: Setup Ruby on head branch
- uses: ruby/setup-ruby@v1
+ uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
- name: Install JS dependencies on head branch
@@ -81,7 +81,7 @@ jobs:
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
- name: Clone testing repository
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: NHSDigital/manage-vaccinations-in-schools-testing
ref: ${{ steps.check-branch.outputs.test_branch }}
@@ -89,7 +89,7 @@ jobs:
- name: Setup testing repository environment file
run: mv ${{ env.MAVIS_TEST_REPO }}/.env.generic ${{ env.MAVIS_TEST_REPO }}/.env
- name: Setup uv
- uses: astral-sh/setup-uv@v7
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Setup Playwright
working-directory: ${{ env.MAVIS_TEST_REPO }}
run: |
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a510d77148..149a4985c7 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -12,16 +12,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: ruby/setup-ruby@v1
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
cache: yarn
node-version-file: .tool-versions
- run: yarn install --immutable --immutable-cache --check-cache
- - uses: jdx/mise-action@v4
+ - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
with:
install_args: actionlint hk pkl shellcheck yamllint
- run: hk check --all
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2688593af0..7e8a2a8a19 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -25,12 +25,12 @@ jobs:
DATABASE_USER: postgres
DATABASE_PASSWORD: postgres
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .tool-versions
cache: yarn
- - uses: ruby/setup-ruby@v1
+ - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
- name: Precompile assets
@@ -62,12 +62,12 @@ jobs:
RAILS_ENV: development
DATABASE_URL: postgres://postgres:postgres@localhost:5432/manage_vaccinations_development
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .tool-versions
cache: yarn
- - uses: ruby/setup-ruby@v1
+ - uses: ruby/setup-ruby@60ecfba8750476ff216b59eee3b88218bb5111cc # v1.303.0
with:
bundler-cache: true
- name: Check seeds run
@@ -77,8 +77,8 @@ jobs:
name: Jest
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: .tool-versions
cache: yarn
diff --git a/.github/workflows/update-version-numbers-slack.yml b/.github/workflows/update-version-numbers-slack.yml
index 120e67ab17..40d33e6705 100644
--- a/.github/workflows/update-version-numbers-slack.yml
+++ b/.github/workflows/update-version-numbers-slack.yml
@@ -17,7 +17,7 @@ jobs:
main_tag: ${{ steps.get_tags.outputs.main_tag }}
steps:
- name: Checkout code and fetch tags
- uses: actions/checkout@v6
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Get latest tags for branches
diff --git a/.gitignore b/.gitignore
index e63374ceb6..d5339587fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -90,3 +90,7 @@ dump.rdb
# Data that gets downloaded by CLI tools
db/data/dfe-schools.zip
db/data/nhs-gp-practices.csv
+
+# Python
+__pycache__/
+*.pyc
diff --git a/.ruby-version b/.ruby-version
index 9c63baa1a2..8b52f98145 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-ruby-4.0.2
+ruby-4.0.3
diff --git a/.tool-versions b/.tool-versions
index 453c61cede..81e3b2884f 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -4,7 +4,8 @@ hk 1.37.0
nodejs 22.15.0
pkl 0.31.0
postgres 17.2
+python 3.14.4
redis 8.2.1
-ruby 4.0.2
+ruby 4.0.3
shellcheck 0.11.0
yamllint 1.38.0
diff --git a/.yamllint.yaml b/.yamllint.yaml
index 8fe11f057d..72d7e94cfe 100644
--- a/.yamllint.yaml
+++ b/.yamllint.yaml
@@ -10,6 +10,8 @@ rules:
min-spaces-from-content: 1
document-start: disable
line-length:
- max: 100
+ # Ideally, this value would be smaller, but it makes dealing with long lines
+ # including commit SHAs, etc. very difficult.
+ max: 120
truthy:
ignore: .github/workflows/*
diff --git a/Dockerfile b/Dockerfile
index 886495fb40..546b4d5241 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@
# check=error=true
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
-ARG RUBY_VERSION=4.0.2
+ARG RUBY_VERSION=4.0.3
ARG BUNDLE_WITHOUT="development:test"
ARG RAILS_ENV="production"
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
diff --git a/Gemfile b/Gemfile
index f34c946b56..85d6ad7559 100644
--- a/Gemfile
+++ b/Gemfile
@@ -118,9 +118,7 @@ group :test do
gem "capybara"
gem "capybara_accessible_selectors",
github: "citizensadvice/capybara_accessible_selectors"
- gem "capybara-screenshot"
gem "climate_control"
- gem "cuprite"
gem "database_cleaner-active_record"
gem "its"
gem "rack_session_access"
diff --git a/Gemfile.lock b/Gemfile.lock
index e59652107d..5db0fb4bdb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/citizensadvice/capybara_accessible_selectors.git
- revision: e204122d3949530828ba8ea52e33409d86f72679
+ revision: d518ee5271f60e18a555434160ae02b74e534046
specs:
capybara_accessible_selectors (0.15.0)
capybara (~> 3.36)
@@ -129,7 +129,7 @@ GEM
asciidoctor (>= 1.5.7, < 3.x)
rexml
ast (2.4.3)
- async (2.38.0)
+ async (2.39.0)
console (~> 1.29)
fiber-annotation
io-event (~> 1.11)
@@ -139,17 +139,17 @@ GEM
actioncable-next
async (~> 2.9)
async-websocket
- async-container (0.34.3)
+ async-container (0.34.5)
async (~> 2.22)
- async-http (0.94.2)
+ async-http (0.95.0)
async (>= 2.10.2)
async-pool (~> 0.11)
io-endpoint (~> 0.14)
io-stream (~> 0.6)
metrics (~> 0.12)
- protocol-http (~> 0.58)
- protocol-http1 (~> 0.36)
- protocol-http2 (~> 0.22)
+ protocol-http (~> 0.62)
+ protocol-http1 (~> 0.39)
+ protocol-http2 (~> 0.26)
protocol-url (~> 0.2)
traces (~> 0.10)
async-http-cache (0.4.6)
@@ -162,11 +162,11 @@ GEM
async-service (~> 0.12)
async-pool (0.11.2)
async (>= 2.0)
- async-service (0.21.0)
+ async-service (0.22.0)
async
async-container (~> 0.34)
string-format (~> 0.2)
- async-utilization (0.3.1)
+ async-utilization (0.3.2)
console (~> 1.0)
async-websocket (0.30.0)
async-http (~> 0.76)
@@ -220,7 +220,7 @@ GEM
i18n
bcrypt (3.1.22)
benchmark (0.5.0)
- bigdecimal (4.1.1)
+ bigdecimal (4.1.2)
bindata (2.5.1)
bindex (0.8.1)
bootsnap (1.23.0)
@@ -237,9 +237,6 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
- capybara-screenshot (1.0.27)
- capybara (>= 1.0, < 4)
- launchy
caxlsx (4.4.2)
htmlentities (~> 4.3, >= 4.3.4)
marcel (~> 1.0)
@@ -247,8 +244,6 @@ GEM
rubyzip (>= 2.4, < 4)
cgi (0.5.1)
charlock_holmes (0.7.9)
- childprocess (5.1.0)
- logger (~> 1.5)
climate_control (1.2.0)
coderay (1.1.3)
concurrent-ruby (1.3.6)
@@ -271,9 +266,6 @@ GEM
cssbundling-rails (1.4.3)
railties (>= 6.0.0)
csv (3.3.5)
- cuprite (0.17)
- capybara (~> 3.0)
- ferrum (~> 0.17.0)
data_migrate (11.3.1)
activerecord (>= 6.1)
railties (>= 6.1)
@@ -302,7 +294,7 @@ GEM
dry-cli (1.4.1)
email_validator (2.2.4)
activemodel
- erb (6.0.2)
+ erb (6.0.3)
erubi (1.13.1)
et-orbi (1.4.0)
tzinfo
@@ -314,9 +306,9 @@ GEM
factory_bot_rails (6.5.1)
factory_bot (~> 6.5)
railties (>= 6.1.0)
- faker (3.6.1)
+ faker (3.7.1)
i18n (>= 1.8.11, < 2)
- falcon (0.55.2)
+ falcon (0.55.3)
async
async-container (~> 0.20)
async-http (~> 0.75)
@@ -346,12 +338,6 @@ GEM
faraday (>= 1, < 3)
faraday-net_http (3.4.2)
net-http (~> 0.5)
- ferrum (0.17.1)
- addressable (~> 2.5)
- base64 (~> 0.2)
- concurrent-ruby (~> 1.1)
- webrick (~> 1.7)
- websocket-driver (~> 0.7)
ffi (1.17.3-arm64-darwin)
ffi (1.17.3-x86_64-linux-gnu)
fhir_models (5.0.0)
@@ -414,7 +400,7 @@ GEM
activesupport
io-console (0.8.2)
io-endpoint (0.17.2)
- io-event (1.14.4)
+ io-event (1.15.1)
io-stream (0.11.1)
irb (1.17.0)
pp (>= 0.6.0)
@@ -427,7 +413,7 @@ GEM
jmespath (1.6.2)
jsbundling-rails (1.3.1)
railties (>= 6.0.0)
- json (2.19.3)
+ json (2.19.4)
json-jwt (1.17.0)
activesupport (>= 4.2)
aes_key_wrap
@@ -446,10 +432,6 @@ GEM
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
language_server-protocol (3.17.0.5)
- launchy (3.1.1)
- addressable (~> 2.8)
- childprocess (~> 5.0)
- logger (~> 1.6)
lint_roller (1.1.0)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -493,7 +475,7 @@ GEM
mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0924)
mini_mime (1.1.5)
- minitest (6.0.3)
+ minitest (6.0.5)
drb (~> 2.0)
prism (~> 1.5)
msgpack (1.8.0)
@@ -552,8 +534,8 @@ GEM
ostruct (0.6.3)
pagy (9.4.0)
paint (2.3.0)
- parallel (1.27.0)
- parallel_tests (5.6.0)
+ parallel (1.28.0)
+ parallel_tests (5.7.0)
parallel
parser (3.3.10.2)
ast (~> 2.4.1)
@@ -571,13 +553,13 @@ GEM
activesupport (>= 7.0.0)
rack
protocol-hpack (1.5.1)
- protocol-http (0.60.0)
- protocol-http1 (0.37.0)
- protocol-http (~> 0.58)
- protocol-http2 (0.24.0)
+ protocol-http (0.62.2)
+ protocol-http1 (0.39.0)
+ protocol-http (~> 0.62)
+ protocol-http2 (0.26.0)
protocol-hpack (~> 1.4)
- protocol-http (~> 0.47)
- protocol-rack (0.21.1)
+ protocol-http (~> 0.62)
+ protocol-rack (0.22.1)
io-stream (>= 0.10)
protocol-http (~> 0.58)
rack (>= 1.0)
@@ -642,7 +624,7 @@ GEM
rails-html-sanitizer (1.7.0)
loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
- rails_semantic_logger (4.19.0)
+ rails_semantic_logger (4.20.0)
rack
railties (>= 5.1)
semantic_logger (~> 4.16)
@@ -656,7 +638,7 @@ GEM
tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
- rake (13.3.1)
+ rake (13.4.2)
rb-fsevent (0.11.2)
rb-inotify (0.11.1)
ffi (~> 1.0)
@@ -771,7 +753,7 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
- semantic_logger (4.17.0)
+ semantic_logger (4.18.0)
concurrent-ruby (~> 1.0)
sentry-rails (6.5.0)
railties (>= 5.2.0)
@@ -911,7 +893,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
xrb (0.11.2)
- yard (0.9.38)
+ yard (0.9.42)
yard-activesupport-concern (0.0.1)
yard (>= 0.8)
yard-solargraph (0.1.0)
@@ -945,7 +927,6 @@ DEPENDENCIES
bootsnap
brakeman
capybara
- capybara-screenshot
capybara_accessible_selectors!
caxlsx
charlock_holmes
@@ -953,7 +934,6 @@ DEPENDENCIES
config
cssbundling-rails
csv
- cuprite
data_migrate
database_cleaner-active_record
debug
@@ -1038,7 +1018,7 @@ DEPENDENCIES
with_advisory_lock
RUBY VERSION
- ruby 4.0.2
+ ruby 4.0.3
BUNDLED WITH
4.0.8
diff --git a/app/assets/stylesheets/components/_filters.scss b/app/assets/stylesheets/components/_filters.scss
index 055a2ed2a9..4f95db37de 100644
--- a/app/assets/stylesheets/components/_filters.scss
+++ b/app/assets/stylesheets/components/_filters.scss
@@ -14,6 +14,8 @@
padding-left: nhsuk-spacing(3);
background-color: nhsuk-colour("grey-1");
+ @include nhsuk-font-size(22);
+
// stylelint-disable-next-line max-nesting-depth
@include nhsuk-media-query($from: tablet) {
margin-left: #{nhsuk-spacing(-4) - 1px};
diff --git a/app/assets/stylesheets/components/_session-banner.scss b/app/assets/stylesheets/components/_session-banner.scss
index 677c2fe5ac..6d40b7aa78 100644
--- a/app/assets/stylesheets/components/_session-banner.scss
+++ b/app/assets/stylesheets/components/_session-banner.scss
@@ -12,6 +12,7 @@
}
.nhsuk-notification-banner {
+ margin-bottom: nhsuk-spacing(4);
border-color: nhsuk-colour("white");
}
@@ -31,7 +32,18 @@
color: $nhsuk-reverse-text-colour;
}
+ .app-secondary-navigation {
+ margin: 0;
+ border-bottom: 0 none;
+ background: nhsuk-shade($nhsuk-brand-colour, 20%);
+ box-shadow: 0 -1px 0 0 $nhsuk-secondary-border-colour;
+ }
+
.app-secondary-navigation__list {
- box-shadow: 0 -1px 0 0 $nhsuk-reverse-border-colour;
+ box-shadow: none;
+
+ @include nhsuk-media-query($until: tablet) {
+ margin-left: #{$nhsuk-gutter-half * -1};
+ }
}
}
diff --git a/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_index.scss b/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_index.scss
index fcc9da392b..5668984153 100644
--- a/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_index.scss
+++ b/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_index.scss
@@ -1,3 +1,2 @@
@forward "details";
@forward "summary-list";
-@forward "tables";
diff --git a/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_tables.scss b/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_tables.scss
deleted file mode 100644
index 823ca1f5b4..0000000000
--- a/app/assets/stylesheets/vendor/nhsuk-frontend/overrides/_tables.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-// Don’t show a grey background when hovering over table rows
-.nhsuk-table__row:hover {
- background: none;
-}
diff --git a/app/components/app_gillick_assessment_component.rb b/app/components/app_gillick_assessment_component.rb
index 2adf5c16ce..3c3412f9b4 100644
--- a/app/components/app_gillick_assessment_component.rb
+++ b/app/components/app_gillick_assessment_component.rb
@@ -19,6 +19,7 @@ def gillick_assessment
.gillick_assessments
.order(created_at: :desc)
.for_session(session)
+ .where(date: Date.current)
.for_programme(programme)
.first
end
diff --git a/app/components/app_import_format_details_component.rb b/app/components/app_import_format_details_component.rb
index 33fea34d33..098eb9f066 100644
--- a/app/components/app_import_format_details_component.rb
+++ b/app/components/app_import_format_details_component.rb
@@ -288,7 +288,13 @@ def programme
end
def vaccine_and_batch
- vaccines = team.vaccines.pluck(:upload_name).map { tag.i(it) }
+ vaccines =
+ team
+ .vaccines
+ .where.not(upload_name: [nil, ""])
+ .order(:upload_name)
+ .pluck(:upload_name)
+ .map { tag.i(it) }
[
{
diff --git a/app/components/app_location_card_component.rb b/app/components/app_location_card_component.rb
index fc661fba17..f203e0d05a 100644
--- a/app/components/app_location_card_component.rb
+++ b/app/components/app_location_card_component.rb
@@ -10,7 +10,7 @@ def initialize(location, patient_count:, next_session_date:, heading_level: 4)
def call
render AppCardComponent.new(link_to:, compact: true) do |card|
- card.with_heading(level: @heading_level) { heading }
+ card.with_heading(level: @heading_level, size: "s") { heading }
govuk_summary_list(rows:)
end
end
diff --git a/app/components/app_patient_search_form_component.rb b/app/components/app_patient_search_form_component.rb
index ea8442042e..033f00bced 100644
--- a/app/components/app_patient_search_form_component.rb
+++ b/app/components/app_patient_search_form_component.rb
@@ -2,11 +2,7 @@
class AppPatientSearchFormComponent < ViewComponent::Base
# Remove these statuses once implemented.
- HIDDEN_PROGRAMME_STATUSES = %w[
- needs_consent_request_failed
- needs_consent_request_not_scheduled
- needs_consent_request_scheduled
- ].freeze
+ HIDDEN_PROGRAMME_STATUSES = %w[needs_consent_request_failed].freeze
def initialize(
form,
diff --git a/app/components/app_patient_search_result_card_component.rb b/app/components/app_patient_search_result_card_component.rb
index 512ae1ec9c..7b8892d005 100644
--- a/app/components/app_patient_search_result_card_component.rb
+++ b/app/components/app_patient_search_result_card_component.rb
@@ -38,7 +38,7 @@ def initialize(
def call
render AppCardComponent.new(link_to:, compact: true) do |card|
- card.with_heading(level: @heading_level) do
+ card.with_heading(level: @heading_level, size: "s") do
patient.full_name_with_known_as
end
govuk_summary_list(rows:)
diff --git a/app/components/app_patient_session_consent_component.html.erb b/app/components/app_patient_session_consent_component.html.erb
index e59be42717..4d9a5f48a7 100644
--- a/app/components/app_patient_session_consent_component.html.erb
+++ b/app/components/app_patient_session_consent_component.html.erb
@@ -21,6 +21,10 @@
<%= patient.full_name %> is ready for the vaccinator.
<% elsif consent_status_value == :no_contact_details %>
We cannot send consent requests because we have no parent contact details.
+ <% elsif consent_status_value == :request_not_scheduled %>
+ No consent request is scheduled to be sent because the child has not been added to a scheduled session.
+ <% elsif consent_status_value == :request_scheduled %>
+ A consent request will be sent on <%= session.send_consent_requests_at.to_fs(:long) %>.
<% end %>
diff --git a/app/components/app_patient_session_consent_component.rb b/app/components/app_patient_session_consent_component.rb
index 9ecb56506c..5b43a0d8e2 100644
--- a/app/components/app_patient_session_consent_component.rb
+++ b/app/components/app_patient_session_consent_component.rb
@@ -63,7 +63,9 @@ def consent_status_generator
patient:,
consents:,
vaccination_records:,
- parents:
+ parents:,
+ sessions: [session],
+ consent_notifications:
)
end
@@ -76,19 +78,14 @@ def triage_status_generator
consents:,
triages:,
vaccination_records:,
- parents:
+ parents:,
+ sessions: [session],
+ consent_notifications:
)
end
def latest_consent_request
- @latest_consent_request ||=
- patient
- .consent_notifications
- .request
- .has_all_programmes_of([programme])
- .for_academic_year(academic_year)
- .order(sent_at: :desc)
- .first
+ @latest_consent_request ||= consent_notifications.first
end
def consents
@@ -116,8 +113,14 @@ def vaccination_records
patient.vaccination_records.for_programme(programme).order_by_performed_at
end
+ SEND_CONSENT_REQUEST_STATUSES = %i[
+ no_response
+ request_scheduled
+ request_not_scheduled
+ ].freeze
+
def can_send_consent_request?
- consent_status_value == :no_response &&
+ consent_status_value.in?(SEND_CONSENT_REQUEST_STATUSES) &&
patient.send_notifications?(team: @session.team) &&
session.can_receive_consent? && patient.parents.any?(&:contactable?)
end
@@ -134,4 +137,13 @@ def who_refused
def show_health_answers?
grouped_consents.any?(&:response_given?)
end
+
+ def consent_notifications
+ patient
+ .consent_notifications
+ .request
+ .has_all_programmes_of([programme])
+ .for_academic_year(academic_year)
+ .order(sent_at: :desc)
+ end
end
diff --git a/app/components/app_patient_session_programme_component.rb b/app/components/app_patient_session_programme_component.rb
new file mode 100644
index 0000000000..fdaa6750c1
--- /dev/null
+++ b/app/components/app_patient_session_programme_component.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+class AppPatientSessionProgrammeComponent < ViewComponent::Base
+ erb_template <<-ERB
+ <%= render AppCardComponent.new(feature: true) do |card| %>
+ <% card.with_heading(level: 4, colour:) { heading } %>
+ <% if details.present? %>
+
<%= details %>
+ <% end %>
+ <% if programme_status.vaccinated? || programme_status.cannot_vaccinate? %>
+ <%= render AppPatientVaccinationTableComponent.new(
+ patient,
+ programme:,
+ academic_year:,
+ show_caption: true,
+ show_details: false
+ ) %>
+ <% end %>
+ <%= render AppActionLinkComponent.new(
+ text: action_link_text,
+ href: patient_programme_path(patient, programme.type)
+ ) %>
+ <% end %>
+ ERB
+
+ def initialize(patient:, session:, programme:)
+ @patient = patient
+ @session = session
+ @programme = programme
+ end
+
+ private
+
+ attr_reader :patient, :session, :programme
+
+ delegate :academic_year, to: :session
+ delegate :triage_summary, to: :helpers
+
+ def heading
+ "#{resolver[:prefix]}: #{resolver[:text]}"
+ end
+
+ def colour
+ resolver[:colour]
+ end
+
+ def details
+ if triage_driven_cannot_vaccinate? && latest_triage &&
+ (summary = triage_summary(latest_triage)).present?
+ summary
+ elsif programme_status.due?
+ criteria_label =
+ I18n.t(
+ programme_status.vaccine_criteria.to_param,
+ scope: :vaccine_criteria
+ )
+ if criteria_label.present?
+ "#{patient.given_name} is ready to vaccinate (#{criteria_label.downcase})."
+ else
+ "#{patient.given_name} is ready to vaccinate."
+ end
+ elsif programme_status.vaccinated?
+ record =
+ patient
+ .vaccination_records
+ .for_programme(programme)
+ .order_by_performed_at
+ .first
+ nurse = [
+ record&.performed_by_given_name,
+ record&.performed_by_family_name
+ ].compact_blank.join(" ")
+ if nurse.present?
+ "#{patient.given_name} was vaccinated by #{nurse} on #{record&.performed_at&.to_fs(:long)}."
+ else
+ "#{patient.given_name} was vaccinated on #{record&.performed_at&.to_fs(:long)}."
+ end
+ elsif programme_status.needs_triage?
+ if latest_triage&.invite_to_clinic? &&
+ (summary = triage_summary(latest_triage)).present?
+ summary
+ else
+ "You need to decide if it’s safe to vaccinate #{patient.given_name}."
+ end
+ else
+ resolver[:details_text]
+ end
+ end
+
+ def triage_driven_cannot_vaccinate?
+ programme_status.cannot_vaccinate_do_not_vaccinate? ||
+ programme_status.cannot_vaccinate_delay_vaccination?
+ end
+
+ def latest_triage
+ @latest_triage ||=
+ TriageFinder.call(
+ patient.triages.includes(:performed_by),
+ programme_type: programme.type,
+ academic_year:
+ )
+ end
+
+ def programme_status
+ @programme_status ||= patient.programme_status(programme, academic_year:)
+ end
+
+ def action_link_text
+ "View child’s #{programme.name} record"
+ end
+
+ def resolver
+ @resolver ||=
+ PatientProgrammeStatusResolver.call(
+ patient,
+ programme_type: programme.type,
+ academic_year:
+ )
+ end
+end
diff --git a/app/components/app_patient_session_search_result_card_component.rb b/app/components/app_patient_session_search_result_card_component.rb
index cdb2fb590b..53166b830a 100644
--- a/app/components/app_patient_session_search_result_card_component.rb
+++ b/app/components/app_patient_session_search_result_card_component.rb
@@ -36,7 +36,7 @@ def initialize(
def call
render AppCardComponent.new(link_to: card_link, compact: true) do |card|
- card.with_heading(level: @heading_level) { heading }
+ card.with_heading(level: @heading_level, size: "s") { heading }
safe_join([summary_list, registration_buttons].compact)
end
end
diff --git a/app/components/app_patient_session_triage_component.rb b/app/components/app_patient_session_triage_component.rb
index 4075ed14a5..2737a94c58 100644
--- a/app/components/app_patient_session_triage_component.rb
+++ b/app/components/app_patient_session_triage_component.rb
@@ -14,6 +14,13 @@ def initialize(
@current_user = current_user
@triage_form = triage_form || default_triage_form
@parents = patient.parents
+ @patient_locations =
+ patient.patient_locations.includes(
+ location: [
+ :location_programme_year_groups,
+ { team_locations: { sessions: :session_programme_year_groups } }
+ ]
+ )
end
def render?
@@ -28,7 +35,8 @@ def render?
:programme,
:current_user,
:triage_form,
- :parents
+ :parents,
+ :patient_locations
delegate :academic_year, :team, to: :session
@@ -77,7 +85,9 @@ def triage_status_generator
consents:,
triages:,
vaccination_records:,
- parents:
+ parents:,
+ sessions: [session],
+ consent_notifications:
)
end
@@ -89,7 +99,9 @@ def consent_status_generator
patient:,
consents:,
vaccination_records:,
- parents:
+ parents:,
+ sessions: [session],
+ consent_notifications:
)
end
@@ -118,4 +130,12 @@ def latest_triage
def default_triage_form
TriageForm.new(patient:, session:, programme:, current_user:)
end
+
+ def consent_notifications
+ patient
+ .consent_notifications
+ .request
+ .has_all_programmes_of([programme])
+ .for_academic_year(academic_year)
+ end
end
diff --git a/app/components/app_patient_session_vaccination_component.rb b/app/components/app_patient_session_vaccination_component.rb
deleted file mode 100644
index 1d606e038b..0000000000
--- a/app/components/app_patient_session_vaccination_component.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-class AppPatientSessionVaccinationComponent < ViewComponent::Base
- erb_template <<-ERB
-
Programme status
-
- <%= render AppCardComponent.new(feature: true) do |card| %>
- <% card.with_heading(level: 4, colour:) { heading } %>
- <%= render AppPatientVaccinationTableComponent.new(
- patient,
- programme:,
- academic_year:,
- show_caption: true
- ) %>
- <% end %>
- ERB
-
- def initialize(patient:, session:, programme:)
- @patient = patient
- @session = session
- @programme = programme
- end
-
- def render?
- patient
- .vaccination_records
- .for_programme(programme)
- .any? { it.show_in_academic_year?(academic_year) }
- end
-
- private
-
- attr_reader :patient, :session, :programme
-
- delegate :academic_year, :team, to: :session
-
- def colour = resolved_status.fetch(:colour)
-
- def heading =
- "#{resolved_status.fetch(:prefix)}: #{resolved_status.fetch(:text)}"
-
- def resolved_status
- @resolved_status ||=
- PatientProgrammeStatusResolver.call(
- patient,
- programme_type: programme.type,
- academic_year:,
- context_location_id: session.location_id
- )
- end
-end
diff --git a/app/components/app_patient_vaccination_table_component.html.erb b/app/components/app_patient_vaccination_table_component.html.erb
index e70734569f..e8af226e8f 100644
--- a/app/components/app_patient_vaccination_table_component.html.erb
+++ b/app/components/app_patient_vaccination_table_component.html.erb
@@ -1,13 +1,13 @@
<% if vaccination_records.present? %>
<%= govuk_table(html_attributes: { class: "nhsuk-table-responsive" }) do |table| %>
- <% table.with_caption(text: "Vaccination records", size: "s") if show_caption %>
+ <% table.with_caption(text: "Vaccination outcomes", size: "s") if show_caption %>
<% table.with_head do |head| %>
<% head.with_row do |row| %>
<% row.with_cell(text: "Date") %>
- <% row.with_cell(text: "Location") %>
+ <% row.with_cell(text: "Location") if show_details %>
<% row.with_cell(text: "Programme") if show_programme %>
- <% row.with_cell(text: "Source") %>
+ <% row.with_cell(text: "Source") if show_details %>
<% row.with_cell(text: "Outcome") %>
<% end %>
<% end %>
@@ -17,19 +17,21 @@
<% body.with_row do |row| %>
<% row.with_cell do %>
Date
- <%= link_to vaccination_record.performed_at.to_date.to_fs(:long),
+ <%= link_to vaccination_record.performed_at.to_fs(:long),
vaccination_record_path(vaccination_record) %>
<% end %>
- <% row.with_cell do %>
-
Location
- <%= helpers.vaccination_record_location(vaccination_record) %>
+ <% if show_details %>
+ <% row.with_cell do %>
+
Location
+ <%= helpers.vaccination_record_location(vaccination_record) %>
- <% if (location = vaccination_record.location) && location.has_address? %>
-
-
- <%= helpers.format_address_single_line(location) %>
-
+ <% if (location = vaccination_record.location) && location.has_address? %>
+
+
+ <%= helpers.format_address_single_line(location) %>
+
+ <% end %>
<% end %>
<% end %>
@@ -40,9 +42,11 @@
<% end %>
<% end %>
- <% row.with_cell do %>
-
Source
- <%= vaccination_record_source(vaccination_record) %>
+ <% if show_details %>
+ <% row.with_cell do %>
+
Source
+ <%= vaccination_record_source(vaccination_record) %>
+ <% end %>
<% end %>
<% row.with_cell do %>
diff --git a/app/components/app_patient_vaccination_table_component.rb b/app/components/app_patient_vaccination_table_component.rb
index 6ccc4b52a4..e11ffd6547 100644
--- a/app/components/app_patient_vaccination_table_component.rb
+++ b/app/components/app_patient_vaccination_table_component.rb
@@ -1,18 +1,25 @@
# frozen_string_literal: true
class AppPatientVaccinationTableComponent < ViewComponent::Base
- def initialize(patient, academic_year:, programme: nil, show_caption: false)
+ def initialize(
+ patient,
+ academic_year:,
+ programme: nil,
+ show_caption: false,
+ show_details: true
+ )
@patient = patient
@academic_year = academic_year
@programme = programme
@show_caption = show_caption
+ @show_details = show_details
end
private
delegate :govuk_table, :vaccination_record_source, to: :helpers
- attr_reader :patient, :academic_year, :programme, :show_caption
+ attr_reader :patient, :academic_year, :programme, :show_caption, :show_details
def show_programme = programme.nil?
diff --git a/app/components/app_session_actions_component.rb b/app/components/app_session_actions_component.rb
index 810854c258..56a72c8f70 100644
--- a/app/components/app_session_actions_component.rb
+++ b/app/components/app_session_actions_component.rb
@@ -6,8 +6,12 @@ class AppSessionActionsComponent < ViewComponent::Base
<% card.with_heading(level: 3) { "Action required" } %>
<% if rows.any? %>
<%= govuk_summary_list(rows:) %>
- <% else %>
-
No action required
+ <% end %>
+ <% if policy(session).invite_to_clinic? %>
+ <%= render AppActionLinkComponent.new(
+ href: edit_session_invite_to_clinic_path(@session),
+ text: "Send clinic invitations",
+ ) %>
<% end %>
<% end %>
ERB
diff --git a/app/components/app_session_buttons_component.rb b/app/components/app_session_buttons_component.rb
deleted file mode 100644
index 4f6ff40914..0000000000
--- a/app/components/app_session_buttons_component.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-class AppSessionButtonsComponent < ViewComponent::Base
- erb_template <<-ERB
-
- <% if policy(session).edit? %>
- <%= govuk_button_link_to "Edit session", edit_session_path(session), secondary: true %>
-
- <%= govuk_button_link_to "Download offline spreadsheet",
- session_path(session, format: :xlsx),
- secondary: true %>
-
- <% if policy(session).invite_to_clinic? %>
- <%= link_to "Send clinic invitations", edit_session_invite_to_clinic_path(@session) %>
- <% end %>
- <% end %>
-
- ERB
-
- def initialize(session)
- @session = session
- end
-
- private
-
- attr_reader :session
-
- delegate :policy, :govuk_button_link_to, to: :helpers
-end
diff --git a/app/components/app_session_card_component.rb b/app/components/app_session_card_component.rb
index 18dde741b5..765de07ddd 100644
--- a/app/components/app_session_card_component.rb
+++ b/app/components/app_session_card_component.rb
@@ -23,7 +23,7 @@ def initialize(
def call
render AppCardComponent.new(link_to:, compact: true) do |card|
- card.with_heading(level: @heading_level) { heading }
+ card.with_heading(level: @heading_level, size: "s") { heading }
safe_join([summary_list, button_group].compact)
end
end
diff --git a/app/components/app_session_details_component.rb b/app/components/app_session_details_component.rb
index 5ecce170f2..bb064a704e 100644
--- a/app/components/app_session_details_component.rb
+++ b/app/components/app_session_details_component.rb
@@ -3,7 +3,7 @@
class AppSessionDetailsComponent < ViewComponent::Base
erb_template <<-ERB
<%= render AppCardComponent.new do |card| %>
- <% card.with_heading(level: 3) { "Session details" } %>
+ <% card.with_heading(level: 3, actions:) { "Session details" } %>
<%= render AppSessionSummaryComponent.new(
session,
patient_count: session.patients.count,
@@ -13,6 +13,11 @@ class AppSessionDetailsComponent < ViewComponent::Base
show_status: true,
show_consent_style: true
) %>
+ <% if helpers.policy(session).edit? %>
+ <%= govuk_button_link_to "Download offline spreadsheet",
+ session_path(session, format: :xlsx),
+ secondary: true %>
+ <% end %>
<% end %>
ERB
@@ -25,4 +30,10 @@ def initialize(session)
attr_reader :session
delegate :govuk_button_link_to, to: :helpers
+
+ def actions
+ return [] unless helpers.policy(session).edit?
+
+ [{ text: "Edit session", href: helpers.edit_session_path(session) }]
+ end
end
diff --git a/app/components/app_session_overview_component.rb b/app/components/app_session_overview_component.rb
index 89732e3d37..4ca1353406 100644
--- a/app/components/app_session_overview_component.rb
+++ b/app/components/app_session_overview_component.rb
@@ -4,21 +4,11 @@ class AppSessionOverviewComponent < ViewComponent::Base
erb_template <<-ERB
<%= render AppSessionStatsComponent.new(session) %>
-
- <%= render AppSessionVaccinationsComponent.new(session) %>
-
+ <%= render AppSessionVaccinationsComponent.new(session) %>
-
- <%= render AppSessionActionsComponent.new(session) %>
-
+ <%= render AppSessionActionsComponent.new(session) %>
-
- <%= render AppSessionDetailsComponent.new(session) %>
-
-
-
- <%= render AppSessionButtonsComponent.new(session) %>
-
+ <%= render AppSessionDetailsComponent.new(session) %>
ERB
def initialize(session)
diff --git a/app/controllers/api/testing/reporting_refresh_controller.rb b/app/controllers/api/testing/reporting_refresh_controller.rb
index 0946e59ff8..62e38a4782 100644
--- a/app/controllers/api/testing/reporting_refresh_controller.rb
+++ b/app/controllers/api/testing/reporting_refresh_controller.rb
@@ -2,7 +2,12 @@
class API::Testing::ReportingRefreshController < API::Testing::BaseController
def create
- ReportingAPI::RefreshJob.perform_later
- render status: :accepted
+ if params[:wait].present?
+ ReportingAPI::RefreshJob.perform_now
+ render status: :ok
+ else
+ ReportingAPI::RefreshJob.perform_later
+ render status: :accepted
+ end
end
end
diff --git a/app/controllers/class_imports_controller.rb b/app/controllers/class_imports_controller.rb
index 8b4a48bf94..ff10481a78 100644
--- a/app/controllers/class_imports_controller.rb
+++ b/app/controllers/class_imports_controller.rb
@@ -28,7 +28,6 @@ def create
**class_import_params
)
- @class_import.load_data!
if @class_import.invalid?
render :new, status: :unprocessable_content and return
end
diff --git a/app/controllers/cohort_imports_controller.rb b/app/controllers/cohort_imports_controller.rb
index 7d1a08239c..0b46990875 100644
--- a/app/controllers/cohort_imports_controller.rb
+++ b/app/controllers/cohort_imports_controller.rb
@@ -26,7 +26,6 @@ def create
**cohort_import_params
)
- @cohort_import.load_data!
if @cohort_import.invalid?
render :new, status: :unprocessable_content and return
end
diff --git a/app/controllers/draft_consents_controller.rb b/app/controllers/draft_consents_controller.rb
index 10a125fbf2..5b0448b672 100644
--- a/app/controllers/draft_consents_controller.rb
+++ b/app/controllers/draft_consents_controller.rb
@@ -12,9 +12,9 @@ class DraftConsentsController < ApplicationController
include WizardControllerConcern
- before_action :set_triage_form, if: :includes_triage_step?
- before_action :set_parent_options, if: -> { current_step == :who }
before_action :set_back_link_path
+ before_action :set_new_or_existing_contact_options, if: :is_who_step?
+ before_action :set_triage_form, if: :includes_triage_step?
def show
authorize Consent, :edit?
@@ -213,6 +213,58 @@ def set_steps
self.steps = @draft_consent.wizard_steps
end
+ def set_back_link_path
+ @back_link_path =
+ if @draft_consent.editing?
+ wizard_path("confirm")
+ elsif current_step == @draft_consent.wizard_steps.first
+ session_patient_programme_path(@session, @patient, @programme)
+ else
+ previous_wizard_path
+ end
+ end
+
+ def is_who_step? = current_step == :who
+
+ NewOrExistingContactOption = Struct.new(:value, :label, :hint)
+
+ def set_new_or_existing_contact_options
+ @new_or_existing_contact_options = []
+
+ if @patient.can_self_consent_after_gillick_assessment?(
+ location: @session.location,
+ programme_type: @programme.type
+ )
+ @new_or_existing_contact_options << NewOrExistingContactOption.new(
+ value: "patient",
+ label: "Child (Gillick competent)"
+ )
+ end
+
+ parent_relationships =
+ (
+ @patient.parent_relationships.includes(:parent) +
+ @patient
+ .consents
+ .where(programme_type: @programme.type)
+ .filter_map(&:parent_relationship)
+ ).compact.uniq.sort_by(&:label)
+
+ @new_or_existing_contact_options +=
+ parent_relationships.map do |parent_relationship|
+ NewOrExistingContactOption.new(
+ value: parent_relationship.parent.id,
+ label: parent_relationship.label_with_parent,
+ hint: parent_relationship.parent.contact_label
+ )
+ end
+
+ @new_or_existing_contact_options << NewOrExistingContactOption.new(
+ value: "new",
+ label: "Add a new parental contact"
+ )
+ end
+
def includes_triage_step?
current_step.in?(%i[triage confirm]) && steps.include?("triage")
end
@@ -235,28 +287,6 @@ def set_triage_form
end
end
- def set_parent_options
- @parent_options =
- (
- @patient.parent_relationships.includes(:parent) +
- @patient
- .consents
- .select { it.programme_type == @programme.type }
- .filter_map(&:parent_relationship)
- ).compact.uniq.sort_by(&:label)
- end
-
- def set_back_link_path
- @back_link_path =
- if @draft_consent.editing?
- wizard_path("confirm")
- elsif current_step == @draft_consent.wizard_steps.first
- session_patient_programme_path(@session, @patient, @programme)
- else
- previous_wizard_path
- end
- end
-
# Returns:
# {
# question_0: %i[notes response],
diff --git a/app/controllers/immunisation_imports_controller.rb b/app/controllers/immunisation_imports_controller.rb
index a07b384266..fbc086ccd8 100644
--- a/app/controllers/immunisation_imports_controller.rb
+++ b/app/controllers/immunisation_imports_controller.rb
@@ -22,7 +22,6 @@ def create
**immunisation_import_params
)
- @immunisation_import.load_data!
if @immunisation_import.invalid?
render :new, status: :unprocessable_content and return
end
diff --git a/app/forms/triage_form.rb b/app/forms/triage_form.rb
index 3cc672f958..9f4be40846 100644
--- a/app/forms/triage_form.rb
+++ b/app/forms/triage_form.rb
@@ -130,7 +130,9 @@ def consent_status_generator
patient:,
consents: patient.consents,
vaccination_records: [],
- parents: []
+ parents: [],
+ sessions: [],
+ consent_notifications: []
)
end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
index 2abf45709d..b6edeea650 100644
--- a/app/helpers/sessions_helper.rb
+++ b/app/helpers/sessions_helper.rb
@@ -39,9 +39,9 @@ def session_dates(session)
end
if dates.length == 2
- "#{min_date_str} – #{max_date_str}"
+ "#{min_date_str} to #{max_date_str}"
else
- "#{min_date_str} – #{max_date_str} (#{dates.length} dates)"
+ "#{min_date_str} to #{max_date_str} (#{dates.length} dates)"
end
end
end
@@ -58,18 +58,24 @@ def session_status(session)
def session_title(session)
programmes = session.programmes.map(&:name).to_sentence
- dates = ("on #{session_dates(session)}" if session.dates.present?)
items =
if session.generic_clinic?
- [programmes, "community clinic", dates].compact
+ [programmes, "community clinic"].compact
else
- [programmes, "session at", session.location.name, dates].compact
+ [programmes, "session at", session.location.name].compact
end
items.join(" ")
end
+ def session_caption(session)
+ dates = session_dates(session) if session.dates.present?
+ year_groups = format_year_groups(session.year_groups)
+
+ [dates, year_groups].compact.join(" – ")
+ end
+
def session_consent_style(session)
session.outbreak ? "Outbreak request" : "Standard request"
end
diff --git a/app/helpers/triages_helper.rb b/app/helpers/triages_helper.rb
index 94c6a3c903..5354dbaa52 100644
--- a/app/helpers/triages_helper.rb
+++ b/app/helpers/triages_helper.rb
@@ -61,16 +61,22 @@ def triage_summary(triage)
triage.programme.has_multiple_vaccine_methods?
vaccination_method =
Vaccine.human_enum_name(:method_prefix, triage.vaccine_method)
- "is safe to vaccinate using the #{vaccination_method} vaccine only."
+ " is safe to vaccinate using the #{vaccination_method} vaccine only."
else
- "is safe to vaccinate."
+ " is safe to vaccinate."
end
elsif triage.do_not_vaccinate?
- "should not be vaccinated."
+ " should not be vaccinated."
elsif triage.delay_vaccination?
- "’s vaccination should be delayed."
+ if triage.delay_vaccination_until.present?
+ "’s vaccination should be delayed until #{triage.delay_vaccination_until.to_fs(:long)}."
+ else
+ "’s vaccination should be delayed."
+ end
+ elsif triage.invite_to_clinic?
+ "’s vaccination should take place at a clinic."
end
- "#{prefix}#{triage.patient.full_name} #{suffix}" if suffix
+ "#{prefix}#{triage.patient.given_name}#{suffix}" if suffix
end
end
diff --git a/app/jobs/process_import_job.rb b/app/jobs/process_import_job.rb
index ba0503f3b8..05bb95761e 100644
--- a/app/jobs/process_import_job.rb
+++ b/app/jobs/process_import_job.rb
@@ -6,11 +6,14 @@ class ProcessImportJob < ApplicationJob
queue_as :imports
def perform(import)
+ return if import.processed?
+
SemanticLogger.tagged(import_id: import.id) do
Sentry.set_tags(import_id: import.id)
import.parse_rows!
+ return if import.invalid?
return if import.rows_are_invalid?
import.process!
diff --git a/app/jobs/process_patient_changeset_job.rb b/app/jobs/process_patient_changeset_job.rb
index b708e42fcd..0176af93b1 100644
--- a/app/jobs/process_patient_changeset_job.rb
+++ b/app/jobs/process_patient_changeset_job.rb
@@ -19,7 +19,7 @@ def perform(patient_changeset_id)
if patient_changeset.import.changesets.pending.none?
import = patient_changeset.import
- if Flipper.enabled?(:import_search_pds)
+ if Flipper.enabled?(:pds) && Flipper.enabled?(:pds_search_during_import)
import.validate_pds_match_rate!
return if import.low_pds_match_rate?
end
diff --git a/app/lib/careplus/client.rb b/app/lib/careplus/client.rb
new file mode 100644
index 0000000000..456fcdab46
--- /dev/null
+++ b/app/lib/careplus/client.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require "cgi"
+require "net/http"
+require "uri"
+
+module Careplus
+ class Client
+ TARGET_NAMESPACE_BASE = "https://careplus.syhapp.thirdparty.nhs.uk"
+
+ def initialize(username:, password:, namespace:, payload:)
+ @username = username
+ @password = password
+ @namespace = namespace
+ @payload = payload
+ end
+
+ def send_csv
+ uri =
+ URI.parse("#{Settings.careplus.base_url}/#{namespace}/soap.SchImms.cls")
+ soap_body = build_soap_envelope
+ post_soap_request(uri, soap_body)
+ end
+
+ def self.send_csv(...) = new(...).send_csv
+
+ private_class_method :new
+
+ private
+
+ attr_reader :username, :password, :namespace, :payload
+
+ def build_soap_envelope
+ escaped_payload = CGI.escapeHTML(payload)
+ target_namespace = "#{TARGET_NAMESPACE_BASE}/#{namespace}/webservices"
+
+ <<~XML
+
+
+
+
+ #{username}
+ #{password}
+ #{escaped_payload}
+
+
+
+ XML
+ end
+
+ def post_soap_request(uri, body)
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = uri.scheme == "https"
+
+ request = Net::HTTP::Post.new(uri.request_uri)
+ request["Content-Type"] = "text/xml; charset=utf-8"
+ request.body = body
+
+ http.request(request)
+ end
+ end
+end
diff --git a/app/lib/csv_parser.rb b/app/lib/csv_parser.rb
index bf259485d4..0e6d2e99d9 100644
--- a/app/lib/csv_parser.rb
+++ b/app/lib/csv_parser.rb
@@ -41,7 +41,8 @@ def to_date
parsed_values =
DATE_FORMATS.lazy.filter_map do |format|
- Date.strptime(value, format)
+ date = Date.strptime(value, format)
+ date.year >= 1000 ? date : nil
rescue ArgumentError, TypeError
nil
end
diff --git a/app/lib/mavis_cli.rb b/app/lib/mavis_cli.rb
index a48466ce06..1542ba0294 100644
--- a/app/lib/mavis_cli.rb
+++ b/app/lib/mavis_cli.rb
@@ -58,6 +58,7 @@ def self.terminal_lines
require_relative "mavis_cli/pds/get"
require_relative "mavis_cli/pds/search"
require_relative "mavis_cli/reports/export_automated_careplus"
+require_relative "mavis_cli/reports/send_to_careplus"
require_relative "mavis_cli/schools/add_programme_year_group"
require_relative "mavis_cli/schools/add_to_team"
require_relative "mavis_cli/schools/create"
diff --git a/app/lib/mavis_cli/reports/export_automated_careplus.rb b/app/lib/mavis_cli/reports/export_automated_careplus.rb
index 91f6ba5a31..616d63ce05 100644
--- a/app/lib/mavis_cli/reports/export_automated_careplus.rb
+++ b/app/lib/mavis_cli/reports/export_automated_careplus.rb
@@ -126,8 +126,8 @@ def call(
# prevent this tool from creating database entries at all
ActiveRecord::Base.transaction do
- careplus_export =
- CareplusExport.create!(
+ careplus_report =
+ CareplusReport.create!(
team:,
academic_year: academic_year_value,
date_from: parsed_start_date,
@@ -141,10 +141,10 @@ def call(
)
now_iso = now.iso8601(6)
- CareplusExportVaccinationRecord.insert_all!(
+ CareplusReportVaccinationRecord.insert_all!(
records.map do |record|
{
- careplus_export_id: careplus_export.id,
+ careplus_report_id: careplus_report.id,
vaccination_record_id: record.id,
change_type: 0,
created_at: now_iso,
diff --git a/app/lib/mavis_cli/reports/send_to_careplus.rb b/app/lib/mavis_cli/reports/send_to_careplus.rb
new file mode 100644
index 0000000000..1c33eaf426
--- /dev/null
+++ b/app/lib/mavis_cli/reports/send_to_careplus.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative "../../careplus/client"
+
+module MavisCLI
+ module Reports
+ class SendToCareplus < Dry::CLI::Command
+ desc "Send a CarePlus CSV file to the CarePlus endpoint"
+
+ example [
+ "--input=tmp/automated_export.csv",
+ "--input=tmp/automated_export.csv --ods_code=ABC123"
+ ]
+
+ FALLBACK_NAMESPACE = "MOCK"
+ FALLBACK_USERNAME = "mavis_user"
+ FALLBACK_PASSWORD = "mavis_password"
+
+ option :input, required: true, desc: "Path to the CSV file to send"
+ option :ods_code,
+ desc: "ODS code of the organisation (to use team credentials)"
+ option :workgroup,
+ desc:
+ "Team workgroup (required if the organisation has multiple teams)"
+
+ def call(input:, ods_code: nil, workgroup: nil, **)
+ MavisCLI.load_rails
+
+ unless File.exist?(input)
+ warn "File not found: '#{input}'"
+ return
+ end
+
+ username, password, namespace =
+ resolve_credentials(ods_code:, workgroup:)
+ return if username.nil?
+
+ csv_payload = File.read(input)
+
+ response =
+ Careplus::Client.send_csv(
+ username:,
+ password:,
+ namespace:,
+ payload: csv_payload
+ )
+
+ if response.is_a?(Net::HTTPSuccess)
+ puts "Success (HTTP #{response.code})"
+ puts response.body
+ else
+ warn "Request failed with HTTP #{response.code}: #{response.message}"
+ warn response.body
+ end
+ end
+
+ private
+
+ def resolve_credentials(ods_code:, workgroup:)
+ if ods_code.nil?
+ return FALLBACK_USERNAME, FALLBACK_PASSWORD, FALLBACK_NAMESPACE
+ end
+
+ organisation = Organisation.find_by(ods_code:)
+ if organisation.nil?
+ warn "Could not find organisation with ODS code '#{ods_code}'"
+ return nil, nil
+ end
+
+ teams = organisation.teams
+ teams = teams.where(workgroup:) if workgroup
+
+ if teams.empty?
+ warn(
+ if workgroup
+ "Could not find team '#{workgroup}' for organisation '#{ods_code}'"
+ else
+ "Organisation '#{ods_code}' has no teams."
+ end
+ )
+ return nil, nil, nil
+ end
+
+ if workgroup.nil? && teams.many?
+ warn "Organisation '#{ods_code}' has multiple teams. Specify --workgroup."
+ return nil, nil, nil
+ end
+
+ team = teams.sole
+
+ unless team.careplus_username.present? &&
+ team.careplus_password.present?
+ warn "Team '#{team.name}' does not have CarePlus credentials configured."
+ return nil, nil, nil
+ end
+
+ [
+ team.careplus_username,
+ team.careplus_password,
+ team.careplus_namespace
+ ]
+ end
+ end
+ end
+
+ register "reports" do |prefix|
+ prefix.register "send-to-careplus", Reports::SendToCareplus
+ end
+end
diff --git a/app/lib/mavis_cli/teams/list.rb b/app/lib/mavis_cli/teams/list.rb
index a46c5f3692..4a31e994d3 100644
--- a/app/lib/mavis_cli/teams/list.rb
+++ b/app/lib/mavis_cli/teams/list.rb
@@ -26,15 +26,24 @@ def call(ods_code: nil)
rows =
teams.find_each.map do |team|
- team.slice(:id, :name, :workgroup).merge(
+ team.slice(:id, :name, :workgroup, :type).merge(
ods_code: team.organisation.ods_code,
- programmes: team.programmes.map(&:name).join(", ")
+ programmes: team.programmes.map(&:name).join(", "),
+ nr_cut_off_date: team.national_reporting_cut_off_date
)
end
puts TableTennis.new(
rows,
- columns: %i[id name ods_code workgroup programmes],
+ columns: %i[
+ id
+ name
+ type
+ ods_code
+ workgroup
+ programmes
+ nr_cut_off_date
+ ],
zebra: true
)
end
diff --git a/app/lib/nhs/immunisations_api.rb b/app/lib/nhs/immunisations_api.rb
index 078b5342c9..a2b807ce94 100644
--- a/app/lib/nhs/immunisations_api.rb
+++ b/app/lib/nhs/immunisations_api.rb
@@ -412,6 +412,11 @@ def check_bundle_link_params(bundle, request_params)
uri = URI(link)
bundle_params = URI.decode_www_form(uri.query).to_h
+ # The Imms API used to have a bug where they referred to `immunization.target` instead of `-immunization.target`.
+ # They have since fixed this, but they now include both versions in the `bundle.link` field. We must exclude the
+ # deprecated version from the comparison.
+ bundle_params.delete("immunization.target")
+
# We don't care about the order of the target values
bundle_params["-immunization.target"] = bundle_params[
"-immunization.target"
diff --git a/app/lib/nhs/pds.rb b/app/lib/nhs/pds.rb
index 71a85a0459..cc497f53e5 100644
--- a/app/lib/nhs/pds.rb
+++ b/app/lib/nhs/pds.rb
@@ -34,7 +34,8 @@ class InvalidSearchData < StandardError
class << self
def get_patient(nhs_number)
- return unless Settings.pds.enabled
+ return unless Flipper.enabled?(:pds)
+
NHS::API.connection.get(
"personal-demographics/FHIR/R4/Patient/#{nhs_number}"
)
@@ -55,7 +56,8 @@ def get_patient(nhs_number)
end
def search_patients(attributes)
- return unless Settings.pds.enabled
+ return unless Flipper.enabled?(:pds)
+
if (missing_attrs = (attributes.keys.map(&:to_s) - SEARCH_FIELDS)).any?
raise "Unrecognised attributes: #{missing_attrs.join(", ")}"
end
diff --git a/app/lib/notifier/patient.rb b/app/lib/notifier/patient.rb
index 9eee4c152d..29d1d51930 100644
--- a/app/lib/notifier/patient.rb
+++ b/app/lib/notifier/patient.rb
@@ -3,6 +3,8 @@
class Notifier::Patient
extend ActiveSupport::Concern
+ CONSENT_REMINDER_TYPES = %i[initial_reminder subsequent_reminder].freeze
+
def initialize(patient)
@patient = patient
end
@@ -235,6 +237,8 @@ def send_consent_notification(
SMSDeliveryJob.perform_later(sms_template, **params)
end
+ PatientStatusUpdaterJob.perform_async(patient.id)
+
consent_notification
end
@@ -246,7 +250,13 @@ def generate_consent_templates(
type:
)
is_school = location.gias_school?
- base_template = :"consent_#{is_school ? "school" : "clinic"}_#{type}"
+
+ base_template =
+ if is_school && CONSENT_REMINDER_TYPES.include?(type)
+ :consent_school_reminder
+ else
+ :"consent_#{is_school ? "school" : "clinic"}_#{type}"
+ end
# We can only handle a single programme group or variant in the template.
group = ProgrammeGrouper.call(programmes).keys.sole
diff --git a/app/lib/patient_status_updater.rb b/app/lib/patient_status_updater.rb
index 2ea8d83efb..b9102b0871 100644
--- a/app/lib/patient_status_updater.rb
+++ b/app/lib/patient_status_updater.rb
@@ -42,16 +42,24 @@ def update_programme_statuses!
merge_patient_scope(Patient::ProgrammeStatus)
.where(academic_year: academic_years)
- .includes(
- :attendance_record,
- :consents,
- :patient,
- :patient_locations,
- :triages,
- :vaccination_records,
- :parents
- )
- .find_in_batches do |batch|
+ .in_batches do |relation|
+ batch =
+ relation.includes(
+ :attendance_record,
+ :consents,
+ :patient,
+ :patient_locations,
+ :triages,
+ :vaccination_records,
+ :parents,
+ :consent_notifications,
+ patient_locations: {
+ location: [
+ { team_locations: { sessions: :session_programme_year_groups } }
+ ]
+ }
+ ).to_a
+
batch.each(&:assign)
Patient::ProgrammeStatus.import!(
@@ -84,8 +92,15 @@ def update_registration_statuses!
merge_patient_scope(Patient::RegistrationStatus)
.joins(session: :team_location)
.where(team_location: { academic_year: academic_years })
- .includes(:attendance_records, :patient, :session, :vaccination_records)
- .find_in_batches do |batch|
+ .in_batches do |relation|
+ batch =
+ relation.includes(
+ :attendance_records,
+ :patient,
+ :session,
+ :vaccination_records
+ ).to_a
+
batch.each(&:assign_status)
Patient::RegistrationStatus.import!(
@@ -98,23 +113,6 @@ def update_registration_statuses!
end
end
- def patient_statuses_to_import
- @patient_statuses_to_import ||=
- (patient_scope || Patient.all)
- .pluck(:id, :birth_academic_year)
- .flat_map do |patient_id, birth_academic_year|
- academic_years.flat_map do |academic_year|
- year_group = birth_academic_year.to_year_group(academic_year:)
-
- programme_types_per_year_group
- .fetch(year_group, [])
- .map do |programme_type|
- [patient_id, programme_type, academic_year]
- end
- end
- end
- end
-
def programme_statuses_to_import
@programme_statuses_to_import ||=
(patient_scope || Patient.all)
@@ -153,19 +151,6 @@ def patient_location_statuses_to_import
end
end
- def programme_types_per_year_group
- @programme_types_per_year_group ||=
- Location::ProgrammeYearGroup
- .joins(:location_year_group)
- .where(location_year_group: { academic_year: academic_years })
- .distinct
- .pluck(:programme_type, :"location_year_group.value")
- .each_with_object({}) do |(programme_type, year_group), hash|
- hash[year_group] ||= []
- hash[year_group] << programme_type
- end
- end
-
def programme_types_per_session_id_and_year_group
@programme_types_per_session_id_and_year_group ||=
Session::ProgrammeYearGroup
@@ -180,4 +165,23 @@ def programme_types_per_session_id_and_year_group
hash[session_id][year_group] << programme_type
end
end
+
+ # We preload this association separately because including it in the nested
+ # `patient_locations` preload (see includes above) caused the updater process
+ # to be killed, even with very small batches. The likely cause is memory pressure
+ # from eager loading a deeply nested association graph.
+ #
+ # Preloading it here for the distinct `Location` records in each batch keeps
+ # `StatusGenerator::Programme` query-free without incurring the cost of the
+ # larger nested preload.
+ def preload_location_programme_year_groups(batch)
+ locations = batch.flat_map(&:patient_locations).map(&:location).uniq
+
+ ActiveRecord::Associations::Preloader.new(
+ records: locations,
+ associations: {
+ location_programme_year_groups: :location_year_group
+ }
+ ).call
+ end
end
diff --git a/app/lib/status_generator/consent.rb b/app/lib/status_generator/consent.rb
index 81c1880313..26ce6bbeb3 100644
--- a/app/lib/status_generator/consent.rb
+++ b/app/lib/status_generator/consent.rb
@@ -7,7 +7,9 @@ def initialize(
patient:,
consents:,
vaccination_records:,
- parents:
+ parents:,
+ sessions:,
+ consent_notifications:
)
@programme_type = programme_type
@academic_year = academic_year
@@ -15,6 +17,8 @@ def initialize(
@consents = consents
@vaccination_records = vaccination_records
@parents = parents
+ @sessions = sessions
+ @consent_notifications = consent_notifications
end
def programme
@@ -32,6 +36,10 @@ def status
:conflicts
elsif status_should_be_no_contact_details?
:no_contact_details
+ elsif status_should_be_request_scheduled?
+ :request_scheduled
+ elsif status_should_be_request_not_scheduled?
+ :request_not_scheduled
elsif status_should_be_no_response?
:no_response
else
@@ -62,7 +70,9 @@ def disease_types
:patient,
:consents,
:vaccination_records,
- :parents
+ :parents,
+ :sessions,
+ :consent_notifications
def vaccinated?
return @vaccinated if defined?(@vaccinated)
@@ -131,6 +141,20 @@ def status_should_be_no_contact_details?
parents.none?(&:contactable?)
end
+ def status_should_be_request_scheduled?
+ return false if vaccinated?
+
+ parents_contactable? && consent_notifications.empty? &&
+ sessions.any? { consent_request_scheduled_in_future?(it) }
+ end
+
+ def status_should_be_request_not_scheduled?
+ return false if vaccinated?
+
+ parents_contactable? && consent_notifications.empty? &&
+ (sessions.empty? || sessions.any? { consent_request_not_scheduled?(it) })
+ end
+
def agreed_vaccine_methods
@agreed_vaccine_methods ||=
consents_for_status.map(&:vaccine_methods).inject(&:intersection)
@@ -162,4 +186,21 @@ def latest_consents
@latest_consents ||=
ConsentGrouper.call(consents, programme_type:, academic_year:)
end
+
+ def parents_contactable? = parents.any?(&:contactable?)
+
+ def consent_request_scheduled_in_future?(session)
+ send_at = session.send_consent_requests_at
+
+ # Not using future? because it doesn't work with Timecop
+ send_at.present? && send_at > Time.current
+ end
+
+ # Treat consent requests as not scheduled when send_consent_requests_at is
+ # missing or has already passed, such as for sessions activated on the
+ # same day they are created.
+ def consent_request_not_scheduled?(session)
+ send_at = session.send_consent_requests_at
+ send_at.nil? || send_at < Time.current
+ end
end
diff --git a/app/lib/status_generator/programme.rb b/app/lib/status_generator/programme.rb
index cff78a6ee8..6aa1fffb1c 100644
--- a/app/lib/status_generator/programme.rb
+++ b/app/lib/status_generator/programme.rb
@@ -17,7 +17,8 @@ def initialize(
triages:,
attendance_record:,
vaccination_records:,
- parents:
+ parents:,
+ consent_notifications:
)
@programme_type = programme_type
@academic_year = academic_year
@@ -28,6 +29,8 @@ def initialize(
@attendance_record = attendance_record
@vaccination_records = vaccination_records
@parents = parents
+ @consent_notifications =
+ find_matching_consent_notifications(consent_notifications)
@vaccination_criteria =
VaccinationCriteria.new(
@@ -172,7 +175,8 @@ def consent_vaccine_methods
:triages,
:attendance_record,
:vaccination_criteria,
- :parents
+ :parents,
+ :consent_notifications
delegate :vaccinated?,
:vaccinated_vaccination_record,
@@ -237,11 +241,11 @@ def should_be_needs_consent_request_failed?
end
def should_be_needs_consent_request_scheduled?
- false # TODO: Implement this status.
+ is_eligible? && consent_status == :request_scheduled
end
def should_be_needs_consent_request_not_scheduled?
- false # TODO: Implement this status.
+ is_eligible? && consent_status == :request_not_scheduled
end
def should_be_needs_consent_no_contact_details?
@@ -288,6 +292,13 @@ def default_programme_year_groups
Programme.find(programme_type).default_year_groups
end
+ def find_matching_consent_notifications(notifications)
+ notifications.select do |notification|
+ notification.programme_types.include?(programme_type) &&
+ notification.session&.team_location&.academic_year == academic_year
+ end
+ end
+
def consent_generator
@consent_generator ||=
StatusGenerator::Consent.new(
@@ -296,7 +307,9 @@ def consent_generator
patient:,
consents:,
vaccination_records:,
- parents:
+ parents:,
+ sessions:,
+ consent_notifications:
)
end
@@ -309,7 +322,34 @@ def triage_generator
consents:,
triages:,
vaccination_records:,
- parents:
+ parents:,
+ sessions:,
+ consent_notifications:
)
end
+
+ def sessions
+ @sessions ||=
+ patient_locations
+ .reject { it.location.generic_clinic? }
+ .flat_map { sessions_for(it) }
+ .uniq
+ end
+
+ def sessions_for(patient_location)
+ patient_location
+ .location
+ .team_locations
+ .select { it.academic_year == academic_year }
+ .flat_map(&:sessions)
+ .select { it.programme_types.include?(programme_type) }
+ .select { session_in_patient_location_date_range?(it, patient_location) }
+ .reject(&:completed?)
+ end
+
+ def session_in_patient_location_date_range?(session, patient_location)
+ return true if session.dates.empty?
+
+ session.dates.any? { |date| date.in?(patient_location.date_range) }
+ end
end
diff --git a/app/lib/status_generator/triage.rb b/app/lib/status_generator/triage.rb
index 0dcfa09561..82c5136904 100644
--- a/app/lib/status_generator/triage.rb
+++ b/app/lib/status_generator/triage.rb
@@ -8,7 +8,9 @@ def initialize(
consents:,
triages:,
vaccination_records:,
- parents:
+ parents:,
+ sessions:,
+ consent_notifications:
)
@programme_type = programme_type
@academic_year = academic_year
@@ -17,6 +19,8 @@ def initialize(
@triages = triages
@vaccination_records = vaccination_records
@parents = parents
+ @sessions = sessions
+ @consent_notifications = consent_notifications
end
def programme
@@ -80,7 +84,9 @@ def vaccination_history_requires_triage?
:consents,
:triages,
:vaccination_records,
- :parents
+ :parents,
+ :sessions,
+ :consent_notifications
def vaccinated?
return @vaccinated if defined?(@vaccinated)
@@ -133,7 +139,9 @@ def consent_generator
patient:,
consents:,
vaccination_records:,
- parents:
+ parents:,
+ sessions:,
+ consent_notifications:
)
end
diff --git a/app/lib/update_patients_from_pds.rb b/app/lib/update_patients_from_pds.rb
index 7f7e519816..9312c67568 100644
--- a/app/lib/update_patients_from_pds.rb
+++ b/app/lib/update_patients_from_pds.rb
@@ -27,6 +27,6 @@ def self.call(...) = new(...).call
attr_reader :patients, :queue
def enqueue?
- @enqueue ||= Settings.pds.enqueue_bulk_updates
+ Flipper.enabled?(:pds) && Flipper.enabled?(:pds_enqueue_bulk_updates)
end
end
diff --git a/app/models/access_log_entry.rb b/app/models/access_log_entry.rb
index 1e19b38315..0cc5c66d76 100644
--- a/app/models/access_log_entry.rb
+++ b/app/models/access_log_entry.rb
@@ -20,7 +20,6 @@
#
# Foreign Keys
#
-# fk_rails_... (patient_id => patients.id)
# fk_rails_... (user_id => users.id)
#
class AccessLogEntry < ApplicationRecord
diff --git a/app/models/careplus_export.rb b/app/models/careplus_report.rb
similarity index 76%
rename from app/models/careplus_export.rb
rename to app/models/careplus_report.rb
index 91ce3b3b20..c016e8324e 100644
--- a/app/models/careplus_export.rb
+++ b/app/models/careplus_report.rb
@@ -2,7 +2,7 @@
# == Schema Information
#
-# Table name: careplus_exports
+# Table name: careplus_reports
#
# id :bigint not null, primary key
# academic_year :integer not null
@@ -21,24 +21,24 @@
#
# Indexes
#
-# index_careplus_exports_on_programme_types (programme_types) USING gin
-# index_careplus_exports_on_status_and_scheduled_at (status,scheduled_at)
-# index_careplus_exports_on_team_id (team_id)
-# index_careplus_exports_on_team_id_and_academic_year (team_id,academic_year)
+# index_careplus_reports_on_programme_types (programme_types) USING gin
+# index_careplus_reports_on_status_and_scheduled_at (status,scheduled_at)
+# index_careplus_reports_on_team_id (team_id)
+# index_careplus_reports_on_team_id_and_academic_year (team_id,academic_year)
#
# Foreign Keys
#
# fk_rails_... (team_id => teams.id)
#
-class CareplusExport < ApplicationRecord
+class CareplusReport < ApplicationRecord
include HasManyProgrammes
audited associated_with: :team
belongs_to :team
- has_many :careplus_export_vaccination_records, dependent: :destroy
- has_many :vaccination_records, through: :careplus_export_vaccination_records
+ has_many :careplus_report_vaccination_records, dependent: :destroy
+ has_many :vaccination_records, through: :careplus_report_vaccination_records
enum :status, { pending: 0, sending: 1, sent: 2, failed: 3 }, validate: true
diff --git a/app/models/careplus_export_vaccination_record.rb b/app/models/careplus_report_vaccination_record.rb
similarity index 52%
rename from app/models/careplus_export_vaccination_record.rb
rename to app/models/careplus_report_vaccination_record.rb
index 5280e4f85f..c2c405e00b 100644
--- a/app/models/careplus_export_vaccination_record.rb
+++ b/app/models/careplus_report_vaccination_record.rb
@@ -2,28 +2,28 @@
# == Schema Information
#
-# Table name: careplus_export_vaccination_records
+# Table name: careplus_report_vaccination_records
#
# change_type :integer not null
# created_at :datetime not null
# updated_at :datetime not null
-# careplus_export_id :bigint not null, primary key
+# careplus_report_id :bigint not null, primary key
# vaccination_record_id :bigint not null, primary key
#
# Indexes
#
-# idx_on_careplus_export_id_8ce4ed1ff0 (careplus_export_id)
-# idx_on_vaccination_record_id_d4c93aefb7 (vaccination_record_id)
+# idx_on_careplus_report_id_98876049c7 (careplus_report_id)
+# idx_on_vaccination_record_id_e7f05454ab (vaccination_record_id)
#
# Foreign Keys
#
-# fk_rails_... (careplus_export_id => careplus_exports.id) ON DELETE => cascade
+# fk_rails_... (careplus_report_id => careplus_reports.id) ON DELETE => cascade
# fk_rails_... (vaccination_record_id => vaccination_records.id)
#
-class CareplusExportVaccinationRecord < ApplicationRecord
- self.primary_key = %i[careplus_export_id vaccination_record_id]
+class CareplusReportVaccinationRecord < ApplicationRecord
+ self.primary_key = %i[careplus_report_id vaccination_record_id]
- belongs_to :careplus_export
+ belongs_to :careplus_report
belongs_to :vaccination_record
enum :change_type, { created: 0, updated: 1 }, validate: true
diff --git a/app/models/concerns/csv_importable.rb b/app/models/concerns/csv_importable.rb
index 0b5c3ee620..7fb7a6a63d 100644
--- a/app/models/concerns/csv_importable.rb
+++ b/app/models/concerns/csv_importable.rb
@@ -6,7 +6,7 @@ module CSVImportable
MAX_CSV_ROWS = 20_000
included do
- attr_accessor :csv_is_malformed, :data, :rows
+ attr_accessor :rows
encrypts :csv_data
@@ -83,17 +83,37 @@ module CSVImportable
before_save :ensure_processed_with_count_statistics
end
- def csv=(file)
- self.csv_data = remove_bom_if_present(file&.read)
- self.csv_filename = file&.original_filename
- end
+ # Assign an uploaded CSV file to this import.
+ #
+ # Reads the uploaded file into {Import::CSVData}, stores the original filename,
+ # and updates {#rows_count} based on the parsed CSV data.
+ #
+ # If the file contains a UTF byte-order mark (BOM) (common when exporting from
+ # Excel), the encoding is detected and handled before reading.
+ #
+ # Raises an error if called on a persisted record, as changing the CSV file for
+ # an existing import is not allowed.
+ #
+ # @param source [ActionDispatch::Http::UploadedFile] the uploaded CSV file
+ # @raise [RuntimeError] if called on a persisted record
+ # @raise [ArgumentError] if `source` is not an uploaded file
+ def csv=(source)
+ if persisted?
+ raise "Cannot change the CSV file for an existing import. " \
+ "Create a new import instead."
+ end
- # CSV files exported from Excel may have a BOM.
- # https://en.wikipedia.org/wiki/Byte_order_mark
- # e.g. if you create a new class import from scratch in Excel on Mac v16,
- # save the file as CSV, and upload it.
- def remove_bom_if_present(data)
- StringIO.new(data).tap(&:set_encoding_by_bom).read
+ if source.is_a?(ActionDispatch::Http::UploadedFile)
+ # CSV files exported from Excel may have a BOM.
+ # https://en.wikipedia.org/wiki/Byte_order_mark
+ # e.g. if you create a new class import from scratch in Excel on Mac v16,
+ # save the file as CSV, and upload it.
+ self.csv_data = source.to_io.tap(&:set_encoding_by_bom).read
+ self.csv_filename = source&.original_filename
+ self.rows_count = csv_data_object&.count
+ else
+ raise ArgumentError, "Expected an uploaded file, got #{source}"
+ end
end
# Needed so that validations match the form field name.
@@ -101,27 +121,18 @@ def csv
csv_data
end
- def csv_removed?
- csv_removed_at != nil
+ def csv_data_object
+ @csv_data_object ||= Import::CSVData.new(csv_data)
end
- def load_data!
- return if invalid?
-
- self.data ||= CSVParser.call(csv_data)
- self.rows_count = data.count
- rescue CSV::MalformedCSVError
- self.csv_is_malformed = true
+ def csv_removed?
+ csv_removed_at != nil
end
def parse_rows!
- load_data! if data.nil?
return if invalid?
- self.rows =
- remove_trailing_blank_rows(data)
- .then { |rows| has_instruction_row? ? rows.drop(1) : rows }
- .map { |row_data| parse_row(row_data) }
+ self.rows = csv_data_object.records.map { |row_data| parse_row(row_data) }
if invalid?
self.serialized_errors = errors.to_hash
@@ -130,46 +141,10 @@ def parse_rows!
end
end
- def remove_trailing_blank_rows(table)
- found_values = false
-
- # map(&:itself) because CSV::Table doesn't have a reverse method
- rows_in_reverse_order = table.map(&:itself).reverse
-
- filtered_rows =
- rows_in_reverse_order.select do |row|
- if found_values
- true
- elsif row.fields.all?(&:blank?)
- false
- else
- found_values = true
- true
- end
- end
-
- filtered_rows.reverse
- end
-
- def has_instruction_row?
- data&.first&.[](0)&.to_s&.match?(/\A(Required|Optional)([,.:]|$)/)
- end
-
def processed?
processed_at != nil
end
- def process!
- return if processed?
-
- parse_rows! if rows.nil?
- return if invalid?
-
- process_import!
-
- TeamCachedCounts.new(team).reset_import_issues!
- end
-
def remove!
return if csv_removed?
update!(csv_data: nil, csv_removed_at: Time.zone.now)
@@ -186,13 +161,11 @@ def load_serialized_errors!(limit: nil)
end
def csv_is_valid
- return unless csv_is_malformed
-
- errors.add(:csv, :invalid)
+ errors.add(:csv, :invalid) unless csv_data_object.well_formed?
end
def csv_is_not_too_large
- return unless data
+ return unless csv_data
if rows_count > MAX_CSV_ROWS
errors.add(:csv, :too_many_rows, count: MAX_CSV_ROWS)
@@ -200,10 +173,11 @@ def csv_is_not_too_large
end
def csv_has_records
- return unless data
+ return unless csv_data
csv_has_no_records =
- data.empty? || (data.count == 1 && has_instruction_row?)
+ csv_data_object.empty? ||
+ (csv_data_object.count == 1 && csv_data_object.has_instruction_row?)
errors.add(:csv, :empty) if csv_has_no_records
end
@@ -214,7 +188,7 @@ def rows_are_valid
check_rows_are_unique
- row_offset = has_instruction_row? ? 3 : 2
+ row_offset = csv_data_object.has_instruction_row? ? 3 : 2
rows.each.with_index do |row, index|
next if row.errors.empty?
diff --git a/app/models/concerns/patient_import_concern.rb b/app/models/concerns/patient_import_concern.rb
index 5a75ea4aa1..1b39cad8ab 100644
--- a/app/models/concerns/patient_import_concern.rb
+++ b/app/models/concerns/patient_import_concern.rb
@@ -149,7 +149,8 @@ def school_move_does_not_move_patient?(school_move:, patient:)
end
def patient_archived_and_not_in_another_team?(patient:, team:)
- patient.archived?(team:) && patient.teams.where.not(id: team.id).empty?
+ patient.archived?(team_id: team.id) &&
+ patient.teams.where.not(id: team.id).empty?
end
def reset_counts(import)
diff --git a/app/models/draft_vaccination_record.rb b/app/models/draft_vaccination_record.rb
index 6cfd257d57..522f8be547 100644
--- a/app/models/draft_vaccination_record.rb
+++ b/app/models/draft_vaccination_record.rb
@@ -352,7 +352,9 @@ def vaccine_method_matches_consent_and_triage?
patient:,
consents: patient.consents,
vaccination_records: [],
- parents: []
+ parents: [],
+ sessions: [],
+ consent_notifications: []
)
triage_generator =
@@ -363,7 +365,9 @@ def vaccine_method_matches_consent_and_triage?
consents: patient.consents,
triages: patient.triages,
vaccination_records: [],
- parents: []
+ parents: [],
+ sessions: [],
+ consent_notifications: []
)
approved_vaccine_methods =
diff --git a/app/models/immunisation_import.rb b/app/models/immunisation_import.rb
index d4fac6250e..51601a3e86 100644
--- a/app/models/immunisation_import.rb
+++ b/app/models/immunisation_import.rb
@@ -64,10 +64,38 @@ def records_count
end
end
+ def process!
+ raise "'rows' are empty. Call parse_rows! before processing." if rows.nil?
+
+ counts = count_columns.index_with(0)
+
+ @vaccination_records_batch = Set.new
+ @patients_batch = Set.new
+ @patient_locations_batch = Set.new
+ @archive_reasons_batch = Set.new
+
+ ActiveRecord::Base.transaction do
+ rows.each do |row|
+ count_column_to_increment = process_row(row)
+ counts[count_column_to_increment] += 1
+ bulk_import(rows: 100)
+ end
+
+ bulk_import(rows: :all)
+
+ postprocess_rows!
+
+ update_columns(processed_at: Time.zone.now, status: :processed, **counts)
+ end
+
+ post_commit!
+ end
+
private
+ # TODO: This is called by the `rows_are_valid` validation. Move it to it's own validation.
def check_rows_are_unique
- row_offset = has_instruction_row? ? 3 : 2
+ row_offset = csv_data_object.has_instruction_row? ? 3 : 2
rows
.map(&:full_row_deduplication_attributes)
@@ -124,32 +152,6 @@ def process_row(row)
count_column_to_increment
end
- def process_import!
- counts = count_columns.index_with(0)
-
- @vaccination_records_batch = Set.new
- @patients_batch = Set.new
- @patient_locations_batch = Set.new
- @archive_reasons_batch = Set.new
-
- ActiveRecord::Base.transaction do
- rows.each do |row|
- count_column_to_increment = process_row(row)
- counts[count_column_to_increment] += 1
- bulk_import(rows: 100)
- end
-
- bulk_import(rows: :all)
-
- postprocess_rows!
-
- update_columns(processed_at: Time.zone.now, status: :processed, **counts)
- end
-
- post_commit!
- UpdatePatientsFromPDS.call(patients, queue: :imports)
- end
-
def bulk_import(rows: 100)
return if rows != :all && @vaccination_records_batch.size < rows
@@ -209,18 +211,22 @@ def postprocess_rows!
.find_each do |vaccination_record|
NextDoseTriageFactory.call(vaccination_record:)
end
+ end
+ def post_commit!
PatientTeamUpdater.call(patient_scope: patients)
PatientStatusUpdater.call(patient_scope: Patient.where(id: patients.ids))
+ vaccination_records.sync_all_to_nhs_immunisations_api
+
vaccination_records
.includes(:patient, :team, :subteam)
.find_each do |vaccination_record|
AlreadyHadNotificationSender.call(vaccination_record:)
end
- end
- def post_commit!
- vaccination_records.sync_all_to_nhs_immunisations_api
+ UpdatePatientsFromPDS.call(patients, queue: :imports)
+
+ TeamCachedCounts.new(team).reset_import_issues!
end
end
diff --git a/app/models/immunisation_import_row.rb b/app/models/immunisation_import_row.rb
index 315b55b6df..b31d1a2a02 100644
--- a/app/models/immunisation_import_row.rb
+++ b/app/models/immunisation_import_row.rb
@@ -930,6 +930,11 @@ def validate_patient_date_of_birth
patient_date_of_birth.header,
"Enter a date of birth in the past."
)
+ elsif patient_date_of_birth.to_date < Date.new(2000, 1, 1)
+ errors.add(
+ patient_date_of_birth.header,
+ "is too old to still be in school"
+ )
end
end
diff --git a/app/models/import/csv_data.rb b/app/models/import/csv_data.rb
new file mode 100644
index 0000000000..21df92721a
--- /dev/null
+++ b/app/models/import/csv_data.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class Import::CSVData
+ attr_accessor :data, :malformed
+
+ # Use with serialization in the Import model
+ def initialize(data)
+ @data = data
+ end
+
+ def well_formed?
+ csv_table
+ !malformed
+ end
+
+ def empty? = csv_table.blank?
+
+ def csv_table
+ @csv_table ||=
+ begin
+ CSVParser.call(data) if data.present?
+ rescue CSV::MalformedCSVError
+ @malformed = true
+ nil
+ end
+ end
+
+ def count = csv_table&.count || 0
+
+ def records(&block)
+ remove_trailing_blank_rows
+ .then { |rows| has_instruction_row? ? rows.drop(1) : rows }
+ .each(&block)
+ end
+
+ def has_instruction_row?
+ csv_table&.first&.[](0)&.to_s&.match?(/\A(Required|Optional)([,.:]|$)/)
+ end
+
+ private
+
+ def remove_trailing_blank_rows
+ found_values = false
+
+ # map(&:itself) because CSV::Table doesn't have a reverse method
+ rows_in_reverse_order = csv_table.map(&:itself).reverse
+
+ filtered_rows =
+ rows_in_reverse_order.select do |row|
+ if found_values
+ true
+ elsif row.fields.all?(&:blank?)
+ false
+ else
+ found_values = true
+ true
+ end
+ end
+
+ filtered_rows.reverse
+ end
+end
diff --git a/app/models/important_notice.rb b/app/models/important_notice.rb
index 039b3a6693..cf453bd585 100644
--- a/app/models/important_notice.rb
+++ b/app/models/important_notice.rb
@@ -90,6 +90,7 @@ def message
end
def can_dismiss?
- type.in?(%w[deceased restricted gillick_no_notify team_changed])
+ type.in?(%w[deceased restricted gillick_no_notify team_changed]) ||
+ patient.archived?(team_id:)
end
end
diff --git a/app/models/notify_log_entry.rb b/app/models/notify_log_entry.rb
index eb1bd91fd2..58577c668a 100644
--- a/app/models/notify_log_entry.rb
+++ b/app/models/notify_log_entry.rb
@@ -31,7 +31,7 @@
#
# fk_rails_... (consent_form_id => consent_forms.id)
# fk_rails_... (parent_id => parents.id) ON DELETE => nullify
-# fk_rails_... (patient_id => patients.id)
+# fk_rails_... (patient_id => patients.id) ON DELETE => cascade
# fk_rails_... (sent_by_user_id => users.id)
#
class NotifyLogEntry < ApplicationRecord
@@ -77,7 +77,13 @@ class NotifyLogEntry < ApplicationRecord
"38727494-9a81-42b3-9c1f-5c31e55333e7" => :vaccination_administered_menacwy,
"3abe7ca8-a889-484b-ab9f-07523302eb6a" => :vaccination_administered_td_ipv,
"7238ee27-5840-40e5-b9b9-3130ba4cd4fa" => :vaccination_administered_flu,
- "0b1095db-fb38-4105-9f01-a364fa8bbb1c" => :vaccination_administered_mmr
+ "0b1095db-fb38-4105-9f01-a364fa8bbb1c" => :vaccination_administered_mmr,
+ "ea03aada-0912-4373-91e1-80082071a7aa" =>
+ :consent_school_subsequent_reminder_doubles,
+ "c942ce27-590e-4387-9aa8-5b9b4f2796d1" =>
+ :consent_school_subsequent_reminder_flu,
+ "5f70d21d-00b6-41e6-bdc9-e64455972b43" =>
+ :consent_school_subsequent_reminder_hpv
}.freeze
self.inheritance_column = nil
diff --git a/app/models/onboarding.rb b/app/models/onboarding.rb
index b9f6c2346f..89404cc292 100644
--- a/app/models/onboarding.rb
+++ b/app/models/onboarding.rb
@@ -24,8 +24,11 @@ class Onboarding
TEAM_ATTRIBUTES = {
point_of_care: %i[
+ careplus_namespace
+ careplus_password
careplus_staff_code
careplus_staff_type
+ careplus_username
careplus_venue_code
days_before_consent_reminders
days_before_consent_requests
diff --git a/app/models/patient.rb b/app/models/patient.rb
index 396918584e..2c6b86c282 100644
--- a/app/models/patient.rb
+++ b/app/models/patient.rb
@@ -511,11 +511,11 @@ def sessions
.distinct
end
- def archived?(team:)
+ def archived?(team_id:)
if archive_reasons.loaded?
- archive_reasons.any? { it.team_id == team.id }
+ archive_reasons.any? { it.team_id == team_id }
else
- archive_reasons.exists?(team:)
+ archive_reasons.exists?(team_id:)
end
end
@@ -547,6 +547,14 @@ def show_year_group?(team:)
end
end
+ def can_self_consent_after_gillick_assessment?(location:, programme_type:)
+ gillick_assessments
+ .where(location:, programme_type:, date: Date.current)
+ .order(created_at: :desc)
+ &.first
+ &.gillick_competent? || false
+ end
+
def programme_status(programme, academic_year:)
# TODO: Update this method to accept the `programme_type` so that we can
# then determine the right programme variant from the `disease_types` on
diff --git a/app/models/patient/programme_status.rb b/app/models/patient/programme_status.rb
index 9009503317..c3ea99ac3e 100644
--- a/app/models/patient/programme_status.rb
+++ b/app/models/patient/programme_status.rb
@@ -38,7 +38,18 @@ class Patient::ProgrammeStatus < ApplicationRecord
belongs_to :location, optional: true
has_many :patient_locations,
- -> { includes(location: :location_programme_year_groups) },
+ -> do
+ includes(
+ location: [
+ :location_programme_year_groups,
+ {
+ team_locations: {
+ sessions: :session_programme_year_groups
+ }
+ }
+ ]
+ )
+ end,
through: :patient
has_many :consents,
@@ -65,6 +76,10 @@ class Patient::ProgrammeStatus < ApplicationRecord
has_many :parents, through: :patient
+ has_many :consent_notifications,
+ -> { request.includes(session: :team_location) },
+ through: :patient
+
GROUPS = %w[
not_eligible
needs_consent
@@ -122,6 +137,8 @@ class Patient::ProgrammeStatus < ApplicationRecord
default: :not_eligible,
validate: true
+ # If you add more consent_status enums here, check
+ # whether ReportingAPI::Total also needs updating.
enum :consent_status,
{
no_response: 0,
@@ -130,7 +147,9 @@ class Patient::ProgrammeStatus < ApplicationRecord
conflicts: 3,
not_required: 4,
follow_up_requested: 5,
- no_contact_details: 6
+ no_contact_details: 6,
+ request_scheduled: 7,
+ request_not_scheduled: 8
},
default: :no_response,
prefix: :consent,
@@ -154,6 +173,10 @@ def has_refusal? = status.in?(HAS_REFUSAL_STATUSES.keys)
def cannot_vaccinate? = status.in?(CANNOT_VACCINATE_STATUSES.keys)
+ def needs_triage? = status.in?(NEEDS_TRIAGE_STATUSES.keys)
+
+ def due? = status.in?(DUE_STATUSES.keys)
+
def vaccinated? = status.in?(VACCINATED_STATUSES.keys)
def group = GROUPS.find { status.starts_with?(it) }
@@ -205,7 +228,8 @@ def generator
triages:,
attendance_record:,
vaccination_records:,
- parents:
+ parents:,
+ consent_notifications:
)
end
end
diff --git a/app/models/patient_changeset.rb b/app/models/patient_changeset.rb
index 66ee909a29..2b4575bc27 100644
--- a/app/models/patient_changeset.rb
+++ b/app/models/patient_changeset.rb
@@ -277,7 +277,7 @@ def school_move
if patient.new_record? || patient.school != school ||
patient.not_in_team?(team:, academic_year:) ||
- patient.archived?(team:) || patient.school_moves.any?
+ patient.archived?(team_id: team.id) || patient.school_moves.any?
school_move =
patient.school_moves.includes(:teams).first ||
SchoolMove.new(patient:)
diff --git a/app/models/patient_import.rb b/app/models/patient_import.rb
index 949c4ff0a6..fb31e1885c 100644
--- a/app/models/patient_import.rb
+++ b/app/models/patient_import.rb
@@ -34,13 +34,15 @@ def records_count
changesets.from_file.count
end
- def process_import!
+ def process!
+ raise "'rows' are empty. Call parse_rows! before processing." if rows.nil?
+
changesets =
rows.each_with_index.map do |row, row_number|
PatientChangeset.from_import_row(row:, import: self, row_number:)
end
- if Flipper.enabled?(:import_search_pds)
+ if Flipper.enabled?(:pds) && Flipper.enabled?(:pds_search_during_import)
process_no_postcode_changesets(self.changesets.without_postcode)
if self.changesets.with_postcode.any?
enqueue_pds_cascading_searches(self.changesets.with_postcode)
@@ -54,6 +56,8 @@ def process_import!
return if changesets_are_invalid?
enqueue_review_jobs(self.changesets)
+
+ TeamCachedCounts.new(team).reset_import_issues!
end
def validate_pds_match_rate!
@@ -158,7 +162,7 @@ def process_no_postcode_changesets(changesets)
def enqueue_review_jobs(changesets)
review_changesets =
- if Flipper.enabled?(:import_search_pds)
+ if Flipper.enabled?(:pds) && Flipper.enabled?(:pds_search_during_import)
changesets.with_postcode
else
changesets
@@ -179,6 +183,8 @@ def enqueue_pds_cascading_searches(changesets)
end
end
+ # TODO: This is called by the `rows_are_valid` validation. Move it to it's own validation.
+ # TODO: Currently entested, unlike the equivalent in ImmunisationImport. Add tests.
def check_rows_are_unique
rows
.map(&:nhs_number_value)
diff --git a/app/models/patient_merge_log_entry.rb b/app/models/patient_merge_log_entry.rb
index db1cda8771..fa0b6f2e3b 100644
--- a/app/models/patient_merge_log_entry.rb
+++ b/app/models/patient_merge_log_entry.rb
@@ -21,7 +21,7 @@
#
# Foreign Keys
#
-# fk_rails_... (patient_id => patients.id)
+# fk_rails_... (patient_id => patients.id) ON DELETE => cascade
# fk_rails_... (user_id => users.id)
#
class PatientMergeLogEntry < ApplicationRecord
diff --git a/app/models/patient_programme_vaccinations_search.rb b/app/models/patient_programme_vaccinations_search.rb
index c2c3d03474..7e40989cf8 100644
--- a/app/models/patient_programme_vaccinations_search.rb
+++ b/app/models/patient_programme_vaccinations_search.rb
@@ -19,7 +19,7 @@
#
# Foreign Keys
#
-# fk_rails_... (patient_id => patients.id)
+# fk_rails_... (patient_id => patients.id) ON DELETE => cascade
#
class PatientProgrammeVaccinationsSearch < ApplicationRecord
include BelongsToProgramme
diff --git a/app/models/pds_search_result.rb b/app/models/pds_search_result.rb
index 4751ebd4e4..d7805eb93b 100644
--- a/app/models/pds_search_result.rb
+++ b/app/models/pds_search_result.rb
@@ -21,7 +21,7 @@
#
# Foreign Keys
#
-# fk_rails_... (patient_id => patients.id)
+# fk_rails_... (patient_id => patients.id) ON DELETE => cascade
#
class PDSSearchResult < ApplicationRecord
belongs_to :patient
diff --git a/app/models/reporting_api/total.rb b/app/models/reporting_api/total.rb
index b5f68a9411..4a699f3980 100644
--- a/app/models/reporting_api/total.rb
+++ b/app/models/reporting_api/total.rb
@@ -42,6 +42,27 @@ class ReportingAPI::Total < ApplicationRecord
CONSENT_REFUSED = 2
CONSENT_CONFLICTS = 3
CONSENT_NOT_REQUIRED = 4
+ NO_CONTACT_DETAILS = 6
+ REQUEST_SCHEDULED = 7
+ REQUEST_NOT_SCHEDULED = 8
+
+ CONSENT_GIVEN_STATUSES = [CONSENT_GIVEN, CONSENT_NOT_REQUIRED].freeze
+
+ NO_CONSENT_STATUSES = [
+ CONSENT_NO_RESPONSE,
+ CONSENT_REFUSED,
+ CONSENT_CONFLICTS,
+ NO_CONTACT_DETAILS,
+ REQUEST_SCHEDULED,
+ REQUEST_NOT_SCHEDULED
+ ].freeze
+
+ CONSENT_NO_RESPONSE_STATUSES = [
+ CONSENT_NO_RESPONSE,
+ NO_CONTACT_DETAILS,
+ REQUEST_SCHEDULED,
+ REQUEST_NOT_SCHEDULED
+ ].freeze
scope :not_archived, -> { where(is_archived: false) }
scope :vaccinated,
@@ -70,19 +91,17 @@ def self.vaccinated_count
end
def self.consent_given_count
- where(consent_status: [CONSENT_GIVEN, CONSENT_NOT_REQUIRED]).distinct.count(
- :patient_id
- )
+ where(consent_status: CONSENT_GIVEN_STATUSES).distinct.count(:patient_id)
end
def self.no_consent_count
- where(
- consent_status: [CONSENT_NO_RESPONSE, CONSENT_REFUSED, CONSENT_CONFLICTS]
- ).distinct.count(:patient_id)
+ where(consent_status: NO_CONSENT_STATUSES).distinct.count(:patient_id)
end
def self.consent_no_response_count
- where(consent_status: CONSENT_NO_RESPONSE).distinct.count(:patient_id)
+ where(consent_status: CONSENT_NO_RESPONSE_STATUSES).distinct.count(
+ :patient_id
+ )
end
def self.consent_refused_count
@@ -96,17 +115,19 @@ def self.consent_conflicts_count
def self.with_aggregate_metrics
vaccinated_condition =
"status IN (#{VACCINATED_STATUSES.join(",")}) OR has_already_vaccinated_consent = true"
- no_consent_condition =
- "consent_status IN (#{CONSENT_NO_RESPONSE}, #{CONSENT_REFUSED}, #{CONSENT_CONFLICTS})"
consent_given_condition =
- "consent_status IN (#{CONSENT_GIVEN}, #{CONSENT_NOT_REQUIRED})"
+ "consent_status IN (#{CONSENT_GIVEN_STATUSES.join(",")})"
+ no_consent_condition =
+ "consent_status IN (#{NO_CONSENT_STATUSES.join(",")})"
+ consent_no_response_condition =
+ "consent_status IN (#{CONSENT_NO_RESPONSE_STATUSES.join(",")})"
select(
"COUNT(DISTINCT patient_id) AS cohort",
"COUNT(DISTINCT patient_id) FILTER (WHERE #{vaccinated_condition}) AS vaccinated",
"COUNT(DISTINCT patient_id) FILTER (WHERE NOT (#{vaccinated_condition})) AS not_vaccinated",
"COUNT(DISTINCT patient_id) FILTER (WHERE #{consent_given_condition}) AS consent_given",
"COUNT(DISTINCT patient_id) FILTER (WHERE #{no_consent_condition}) AS no_consent",
- "COUNT(DISTINCT patient_id) FILTER (WHERE consent_status = #{CONSENT_NO_RESPONSE}) AS consent_no_response",
+ "COUNT(DISTINCT patient_id) FILTER (WHERE #{consent_no_response_condition}) AS consent_no_response",
"COUNT(DISTINCT patient_id) FILTER (WHERE consent_status = #{CONSENT_REFUSED}) AS consent_refused",
"COUNT(DISTINCT patient_id) FILTER (WHERE consent_status = #{CONSENT_CONFLICTS}) AS consent_conflicts"
)
diff --git a/app/models/school_move_log_entry.rb b/app/models/school_move_log_entry.rb
index 6a44d9cf53..9ead2783eb 100644
--- a/app/models/school_move_log_entry.rb
+++ b/app/models/school_move_log_entry.rb
@@ -21,7 +21,7 @@
#
# Foreign Keys
#
-# fk_rails_... (patient_id => patients.id)
+# fk_rails_... (patient_id => patients.id) ON DELETE => cascade
# fk_rails_... (school_id => locations.id)
# fk_rails_... (team_id => teams.id)
# fk_rails_... (user_id => users.id)
diff --git a/app/models/team.rb b/app/models/team.rb
index 40ba86e39e..8b7adec608 100644
--- a/app/models/team.rb
+++ b/app/models/team.rb
@@ -5,8 +5,11 @@
# Table name: teams
#
# id :bigint not null, primary key
+# careplus_namespace :string
+# careplus_password :string
# careplus_staff_code :string
# careplus_staff_type :string
+# careplus_username :string
# careplus_venue_code :string
# days_before_consent_reminders :integer default(7), not null
# days_before_consent_requests :integer default(21), not null
@@ -80,6 +83,25 @@ class Team < ApplicationRecord
normalizes :email, with: EmailAddressNormaliser.new
normalizes :phone, with: PhoneNumberNormaliser.new
+ encrypts :careplus_username, :careplus_password
+
+ scope :careplus_enabled,
+ -> do
+ where
+ .not(careplus_staff_code: [nil, ""])
+ .where.not(careplus_staff_type: [nil, ""])
+ .where.not(careplus_venue_code: [nil, ""])
+ end
+ scope :has_careplus_credentials,
+ -> do
+ where
+ .not(careplus_namespace: [nil, ""])
+ .where.not(careplus_username: nil)
+ .where.not(careplus_password: nil)
+ end
+ scope :eligible_for_automated_careplus_reports,
+ -> { careplus_enabled.has_careplus_credentials }
+
enum :type,
{ point_of_care: 0, national_reporting: 1, support: 2 },
validate: true,
@@ -149,4 +171,9 @@ def careplus_enabled?
careplus_staff_code.present? && careplus_staff_type.present? &&
careplus_venue_code.present?
end
+
+ def eligible_for_automated_careplus_reports?
+ careplus_enabled? && careplus_username.present? &&
+ careplus_password.present? && careplus_namespace.present?
+ end
end
diff --git a/app/views/consent_forms/search.html.erb b/app/views/consent_forms/search.html.erb
index bacd0f96d2..f0826cca22 100644
--- a/app/views/consent_forms/search.html.erb
+++ b/app/views/consent_forms/search.html.erb
@@ -39,7 +39,10 @@
<%= link_to "View full consent response", consent_form_path(@consent_form) %>
- <%= govuk_button_link_to "Create new record", patient_consent_form_path(@consent_form), secondary: true unless @nhs_number_taken %>
+ <%= render AppActionLinkComponent.new(
+ href: patient_consent_form_path(@consent_form),
+ text: "Create new record",
+ ) unless @nhs_number_taken %>
<% end %>
diff --git a/app/views/draft_consents/who.html.erb b/app/views/draft_consents/who.html.erb
index cf812fd7a6..5a6f9765d0 100644
--- a/app/views/draft_consents/who.html.erb
+++ b/app/views/draft_consents/who.html.erb
@@ -2,43 +2,19 @@
<%= govuk_back_link(href: @back_link_path) %>
<% end %>
-<% page_title = "Who are you trying to get consent from?" %>
-
-<%= h1 page_title: do %>
-