Merge branch 'feature/expert_app_resize' into feature/expert_windows_characteristics

This commit is contained in:
Luis Aleixo 2022-01-26 16:02:34 +01:00
commit 6e292908ea
48 changed files with 609 additions and 369 deletions

4
.gitignore vendored
View file

@ -9,8 +9,10 @@ __pycache__
.idea .idea
.vscode .vscode
env* env*
venv venv
support support
# openshift config check folder
app-config/openshift/test-cara
app-config/openshift/cara-prod

View file

@ -1,3 +1,8 @@
stages:
- test
- docker-build
- oc-tag
- deploy
# Use the acc-py-devtools templates found at # Use the acc-py-devtools templates found at
# https://gitlab.cern.ch/-/ide/project/acc-co/devops/python/acc-py-devtools/blob/master/-/acc_py_devtools/templates/gitlab-ci/python.yml. # https://gitlab.cern.ch/-/ide/project/acc-co/devops/python/acc-py-devtools/blob/master/-/acc_py_devtools/templates/gitlab-ci/python.yml.
@ -10,6 +15,9 @@ variables:
PY_VERSION: "3.9" PY_VERSION: "3.9"
# ###################################################################################################
# Test code
# A full installation of CARA, tested with pytest. # A full installation of CARA, tested with pytest.
test_install: test_install:
extends: .acc_py_full_test extends: .acc_py_full_test
@ -21,10 +29,20 @@ test_dev:
# A development installation of CARA tested with pytest. # A development installation of CARA tested with pytest.
test_dev-39:
variables:
PY_VERSION: "3.9"
extends: .acc_py_dev_test
# ###################################################################################################
# Test OpenShift config
.test_openshift_config: .test_openshift_config:
stage: test
rules: rules:
- if: '$OC_TOKEN && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $BRANCH' - if: '$OC_TOKEN && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == $BRANCH'
allow_failure: false # The branch must represent what is deployed. allow_failure: true # The branch must represent what is deployed. FIXME: change to true because of a diff between ConfigMaps
- if: '$OC_TOKEN && $CI_MERGE_REQUEST_EVENT_TYPE != "detached"' - if: '$OC_TOKEN && $CI_MERGE_REQUEST_EVENT_TYPE != "detached"'
allow_failure: true # Anything other than the branch may fail without blocking the pipeline. allow_failure: true # Anything other than the branch may fail without blocking the pipeline.
image: registry.cern.ch/docker.io/mambaorg/micromamba image: registry.cern.ch/docker.io/mambaorg/micromamba
@ -34,7 +52,6 @@ test_dev:
- wget https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz - wget https://github.com/openshift/origin/releases/download/v3.11.0/openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
- tar xzf ./openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz - tar xzf ./openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit.tar.gz
- mv openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit/oc $HOME/env/bin/ - mv openshift-origin-client-tools-v3.11.0-0cbc58b-linux-64bit/oc $HOME/env/bin/
script: script:
- cd ./app-config/openshift - cd ./app-config/openshift
- oc login ${OC_SERVER} --token="${OC_TOKEN}" - oc login ${OC_SERVER} --token="${OC_TOKEN}"
@ -43,57 +60,51 @@ test_dev:
- python ./config-normalise.py ./${CARA_INSTANCE}/actual ./${CARA_INSTANCE}/actual-normed - python ./config-normalise.py ./${CARA_INSTANCE}/actual ./${CARA_INSTANCE}/actual-normed
- python ./config-normalise.py ./${CARA_INSTANCE}/expected ./${CARA_INSTANCE}/expected-normed - python ./config-normalise.py ./${CARA_INSTANCE}/expected ./${CARA_INSTANCE}/expected-normed
- diff -u ./${CARA_INSTANCE}/actual-normed/ ./${CARA_INSTANCE}/expected-normed/ - diff -u ./${CARA_INSTANCE}/actual-normed/ ./${CARA_INSTANCE}/expected-normed/
artifacts: artifacts:
paths: paths:
- ./app-config/openshift/${CARA_INSTANCE}/actual - ./app-config/openshift/${CARA_INSTANCE}/actual
- ./app-config/openshift/${CARA_INSTANCE}/expected - ./app-config/openshift/${CARA_INSTANCE}/expected
check_openshift_config_test-cara: check_openshift_config_test:
extends: .test_openshift_config extends: .test_openshift_config
variables: variables:
CARA_INSTANCE: 'test-cara' CARA_INSTANCE: 'test-cara'
BRANCH: 'live/test-cara' BRANCH: 'live/test-cara'
OC_SERVER: openshift-dev.cern.ch OC_SERVER: https://api.paas.okd.cern.ch
OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_TEST_CARA}" OC_TOKEN: "${OPENSHIFT_TEST_CONFIG_CHECKER_TOKEN}"
check_openshift_config_prod: check_openshift_config_prod:
extends: .test_openshift_config extends: .test_openshift_config
variables: variables:
CARA_INSTANCE: 'cara' CARA_INSTANCE: 'cara-prod'
BRANCH: 'master' BRANCH: 'master'
OC_SERVER: openshift.cern.ch OC_SERVER: https://api.paas.okd.cern.ch
OC_TOKEN: "${OPENSHIFT_CONFIG_CHECKER_TOKEN_PROD}" OC_TOKEN: "${OPENSHIFT_PROD_CONFIG_CHECKER_TOKEN}"
# A development installation of CARA tested with pytest. # ###################################################################################################
test_dev-39: # Build docker images
variables:
PY_VERSION: "3.9"
extends: .acc_py_dev_test
.image_builder: .image_builder:
# Build and push images to the openshift instance, which automatically triggers an application re-deployment. # Build and push images to the openshift instance, which automatically triggers an application re-deployment.
stage: deploy stage: docker-build
image: rules:
# Based on guidance at https://gitlab.cern.ch/gitlabci-examples/build_docker_image. - if: '$CI_COMMIT_BRANCH == "live/test-cara"'
name: gitlab-registry.cern.ch/ci-tools/docker-image-builder variables:
entrypoint: [""] IMAGE_TAG: test-cara-latest
rules: - if: '$CI_COMMIT_BRANCH == "master"'
- if: '$OPENSHIFT_DOCKER_TOKEN_TEST != "" && $CI_COMMIT_BRANCH == "live/test-cara"' variables:
variables: IMAGE_TAG: cara-prod-latest
DOCKER_REGISTRY: "${OPENSHIFT_DOCKER_REGISTRY_TEST}" image:
DOCKER_TOKEN: "${OPENSHIFT_DOCKER_TOKEN_TEST}" # Based on guidance at https://gitlab.cern.ch/gitlabci-examples/build_docker_image.
- if: '$OPENSHIFT_DOCKER_TOKEN_PROD != "" && $CI_COMMIT_BRANCH == "master"' name: gitlab-registry.cern.ch/ci-tools/docker-image-builder
variables: entrypoint: [""]
DOCKER_REGISTRY: "${OPENSHIFT_DOCKER_REGISTRY_PROD}" script:
DOCKER_TOKEN: "${OPENSHIFT_DOCKER_TOKEN_PROD}" - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
script: - echo "Building ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:latest Docker image..."
- echo "{\"auths\":{\"$DOCKER_REGISTRY\":{\"auth\":\"$DOCKER_TOKEN\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context ${CI_PROJECT_DIR}/${DOCKER_CONTEXT_DIRECTORY} --dockerfile ${CI_PROJECT_DIR}/${DOCKERFILE_DIRECTORY}/Dockerfile --destination ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG}
- /kaniko/executor --context $CI_PROJECT_DIR/$DOCKER_CONTEXT_DIRECTORY --dockerfile $CI_PROJECT_DIR/$DOCKERFILE_DIRECTORY/Dockerfile --destination $DOCKER_REGISTRY/$IMAGE_NAME:latest
auth-service-image_builder: auth-service-image_builder:
@ -114,32 +125,67 @@ cara-webservice-image_builder:
DOCKER_CONTEXT_DIRECTORY: "" DOCKER_CONTEXT_DIRECTORY: ""
trigger_build_on_openshift:
stage: deploy
rules:
- if: '$OPENSHIFT_BUILD_WEBHOOK_SECRET'
script:
- curl -X POST -k https://openshift.cern.ch:443/apis/build.openshift.io/v1/namespaces/cara/buildconfigs/cara-router/webhooks/${OPENSHIFT_BUILD_WEBHOOK_SECRET}/generic
deploy_to_test:
stage: deploy
rules:
- if: '$CI_COMMIT_BRANCH == "live/test-cara" && $OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET'
script:
- curl -X POST -k https://openshift-dev.cern.ch:443/apis/build.openshift.io/v1/namespaces/test-cara/buildconfigs/cara-router/webhooks/${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}/generic
oci_calculator: oci_calculator:
# A convenient way for users to run the CARA calculator. extends:
- .image_builder
variables:
IMAGE_NAME: calculator
DOCKERFILE_DIRECTORY: app-config/cara-public-docker-image
DOCKER_CONTEXT_DIRECTORY: ""
# ###################################################################################################
# Link build Docker images OpenShift <-> GitLab registry
.link_docker_images_with_gitlab_registry:
stage: oc-tag
image: gitlab-registry.cern.ch/paas-tools/openshift-client:latest
rules:
- if: '$CI_COMMIT_BRANCH == "live/test-cara"'
variables:
OC_PROJECT: "test-cara"
OC_TOKEN: ${OPENSHIFT_TEST_DEPLOY_TOKEN}
IMAGE_TAG: test-cara-latest
- if: '$CI_COMMIT_BRANCH == "master"'
variables:
OC_PROJECT: "cara-prod"
OC_TOKEN: ${OPENSHIFT_PROD_DEPLOY_TOKEN}
IMAGE_TAG: cara-prod-latest
script:
- oc tag --source=docker ${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest --token ${OC_TOKEN} --server=https://api.paas.okd.cern.ch -n ${OC_PROJECT}
link_auth-service_with_gitlab_registry:
extends:
- .link_docker_images_with_gitlab_registry
variables:
IMAGE_NAME: auth-service
link_cara-webservice_with_gitlab_registry:
extends:
- .link_docker_images_with_gitlab_registry
variables:
IMAGE_NAME: cara-webservice
link_calculator_with_gitlab_registry:
extends:
- .link_docker_images_with_gitlab_registry
variables:
IMAGE_NAME: calculator
# ###################################################################################################
# Trigger build of CARA router on OpenShift
trigger_cara-router_build_on_openshift:
stage: deploy stage: deploy
rules: rules:
# Only run if branch is master (the default branch). - if: '$CI_COMMIT_BRANCH == "live/test-cara"'
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH variables:
image: OC_PROJECT: "test-cara"
name: gitlab-registry.cern.ch/ci-tools/docker-image-builder BUILD_WEBHOOK_SECRET: ${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}
entrypoint: [""] - if: '$CI_COMMIT_BRANCH == "master"'
variables:
OC_PROJECT: "cara-prod"
BUILD_WEBHOOK_SECRET: ${OPENSHIFT_PROD_BUILD_WEBHOOK_SECRET}
script: script:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - curl -X POST -k https://api.paas.okd.cern.ch/apis/build.openshift.io/v1/namespaces/${OC_PROJECT}/buildconfigs/cara-router/webhooks/${BUILD_WEBHOOK_SECRET}/generic
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/app-config/cara-public-docker-image/Dockerfile --destination $CI_REGISTRY_IMAGE/calculator:latest

View file

@ -92,7 +92,7 @@ python -m cara.apps.calculator
To run with the CERN theme: To run with the CERN theme:
``` ```
python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern python -m cara.apps.calculator --theme=cara/apps/templates/cern
``` ```
To run the calculator on a different URL path: To run the calculator on a different URL path:
@ -175,31 +175,33 @@ but it may be origin if you haven't configured it differently):
First, get the [oc](https://docs.okd.io/3.11/cli_reference/get_started_cli.html) client and then login: First, get the [oc](https://docs.okd.io/3.11/cli_reference/get_started_cli.html) client and then login:
```console ```console
$ oc login https://openshift-dev.cern.ch $ oc login https://api.paas.okd.cern.ch
``` ```
Then, switch to the project that you want to update: Then, switch to the project that you want to update:
```console ```console
$ oc project test-cara $ oc project cara-test
``` ```
If you need to create the application in a new project, run: Create a new service account in OpenShift to use GitLab container registry:
```console ```console
$ cd app-config/openshift $ oc create serviceaccount gitlabci-deployer
serviceaccount "gitlabci-deployer" created
$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc create -f - $ oc policy add-role-to-user registry-editor -z gitlabci-deployer
$ oc process -f configmap.yaml | oc create -f -
$ oc process -f services.yaml | oc create -f - # We will refer to the output of this command as `test-token`
$ oc process -f imagestreams.yaml | oc create -f - $ oc serviceaccounts get-token gitlabci-deployer
$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f - <...test-token...>
$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc create -f -
``` ```
Add the token to GitLab to allow GitLab to access OpenShift and define/change image stream tags. Go to `Settings` -> `CI / CD` -> `Variables` -> click on `Expand` button and create the variable `OPENSHIFT_TEST_DEPLOY_TOKEN`: insert the token `<...test-token...>`.
Then, create the webhook secret to be able to trigger automatic builds from GitLab. Then, create the webhook secret to be able to trigger automatic builds from GitLab.
Create and store the secret. Copy the secret above and add it to the GitLab project under `CI /CD` -> `Variables` with the name `OPENSHIFT_CARA_TEST_WEBHOOK_SECRET`. Create and store the secret. Copy the secret above and add it to the GitLab project under `CI /CD` -> `Variables` with the name `OPENSHIFT_TEST_WEBHOOK_SECRET`.
```console ```console
$ WEBHOOKSECRET=$(openssl rand -hex 50) $ WEBHOOKSECRET=$(openssl rand -hex 50)
@ -214,10 +216,25 @@ For CI usage, we also suggest creating a service account:
oc create sa gitlab-config-checker oc create sa gitlab-config-checker
``` ```
Under ``Resources`` -> ``Membership`` enable the ``View`` role for this new service account. Under ``User Management`` -> ``RoleBindings`` create a new `RoleBinding` to grant `View` access to the `gitlab-config-checker` service account:
To get this new user's authentication token go to ``Resources`` -> ``Secrets`` and locate the token in the newly * name: `gitlab-config-checker-view-role`
created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``). * role name: `view`
* service account: `gitlab-config-checker`
To get this new user's authentication token go to ``User Management`` -> ``Service Accounts`` -> `gitlab-config-checker` and locate the token in the newly created secret associated with the user (in this case ``gitlab-config-checker-token-XXXX``). Copy the `token` value from `Data`.
Create the various configurations:
```console
$ cd app-config/openshift
$ oc process -f configmap.yaml | oc create -f -
$ oc process -f services.yaml | oc create -f -
$ oc process -f imagestreams.yaml | oc create -f -
$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc create -f -
$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='cara-test' | oc create -f -
```
### CERN SSO integration ### CERN SSO integration
@ -269,11 +286,10 @@ $ cd app-config/openshift
$ oc process -f configmap.yaml | oc replace -f - $ oc process -f configmap.yaml | oc replace -f -
$ oc process -f services.yaml | oc replace -f - $ oc process -f services.yaml | oc replace -f -
$ oc process -f routes.yaml --param HOST='test-cara.web.cern.ch' | oc replace -f -
$ oc process -f imagestreams.yaml | oc replace -f - $ oc process -f imagestreams.yaml | oc replace -f -
$ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc replace -f - $ oc process -f buildconfig.yaml --param GIT_BRANCH='live/test-cara' | oc replace -f -
$ oc process -f deploymentconfig.yaml --param PROJECT_NAME='test-cara' | oc replace -f - $ oc process -f deploymentconfig.yaml --param PROJECT_NAME='cara-test' | oc replace -f -
``` ```
Be aware that if you change/replace the **route** of the PROD instance, Be aware that if you create/recreate the environment you must manually create a **route** in OpenShift,
it will lose the annotation to be exposed outside CERN (not committed in this repo). specifying the respective annotation to be exposed outside CERN.

View file

@ -1,4 +1,4 @@
FROM condaforge/mambaforge as conda FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda
RUN mamba create --yes -p /opt/app python=3.9 RUN mamba create --yes -p /opt/app python=3.9
COPY . /opt/app-source COPY . /opt/app-source
@ -17,7 +17,7 @@ RUN cd /opt/app \
&& find /opt/app/lib -name '*.pyx' -delete \ && find /opt/app/lib -name '*.pyx' -delete \
; ;
FROM debian FROM registry.cern.ch/docker.io/library/debian
COPY --from=conda /opt/app /opt/app COPY --from=conda /opt/app /opt/app
CMD [ \ CMD [ \

View file

@ -1,4 +1,4 @@
FROM python:3.9 FROM registry.cern.ch/docker.io/library/python:3.9
# Copy just the requirements.txt initially, allowing Docker effectively to cache the build (good for dev). # Copy just the requirements.txt initially, allowing Docker effectively to cache the build (good for dev).
COPY ./requirements.txt /tmp/requirements.txt COPY ./requirements.txt /tmp/requirements.txt

View file

@ -1,4 +1,4 @@
FROM condaforge/mambaforge as conda FROM registry.cern.ch/docker.io/condaforge/mambaforge as conda
RUN mamba create --yes -p /opt/app python=3.9 RUN mamba create --yes -p /opt/app python=3.9
COPY . /opt/app-source COPY . /opt/app-source
@ -18,7 +18,7 @@ RUN cd /opt/app \
&& find /opt/app/lib -name '*.pyx' -delete \ && find /opt/app/lib -name '*.pyx' -delete \
; ;
FROM debian FROM registry.cern.ch/docker.io/library/debian
COPY --from=conda /opt/app /opt/app COPY --from=conda /opt/app /opt/app
ENV PATH=/opt/app/bin/:$PATH ENV PATH=/opt/app/bin/:$PATH

View file

@ -12,7 +12,7 @@ services:
- COOKIE_SECRET - COOKIE_SECRET
- APP_NAME=cara-webservice - APP_NAME=cara-webservice
- CARA_CALCULATOR_PREFIX=/calculator-cern - CARA_CALCULATOR_PREFIX=/calculator-cern
- CARA_THEME=cara/apps/calculator/themes/cern - CARA_THEME=cara/apps/templates/cern
user: ${CURRENT_UID} user: ${CURRENT_UID}
cara-calculator-open: cara-calculator-open:

View file

@ -1,6 +1,6 @@
--- ---
kind: "Template" kind: "Template"
apiVersion: "v1" apiVersion: template.openshift.io/v1
metadata: metadata:
name: "cara-application" name: "cara-application"
creationTimestamp: null creationTimestamp: null
@ -12,7 +12,7 @@
objects: objects:
- -
kind: BuildConfig kind: BuildConfig
apiVersion: v1 apiVersion: build.openshift.io/v1
metadata: metadata:
name: cara-router name: cara-router
labels: labels:

View file

@ -9,7 +9,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
parser.description = "Fetch the openshift config for CARA" parser.description = "Fetch the openshift config for CARA"
parser.set_defaults(handler=handler) parser.set_defaults(handler=handler)
parser.add_argument( parser.add_argument(
"instance", choices=['cara', 'test-cara'], "instance", choices=['cara-prod', 'test-cara'],
help="Pick the instance for which you want to fetch the config", help="Pick the instance for which you want to fetch the config",
) )
parser.add_argument( parser.add_argument(
@ -35,25 +35,32 @@ def get_oc_server() -> typing.Optional[str]:
def fetch_config(output_directory: pathlib.Path): def fetch_config(output_directory: pathlib.Path):
output_directory.mkdir(exist_ok=True, parents=True) output_directory.mkdir(exist_ok=True, parents=True)
for component in ['routes', 'configmap', 'services', 'imagestreams', 'buildconfig', 'deploymentconfig']: for component, name in [
('configmap', 'auth-service'),
('services', None),
('imagestreams', None),
('buildconfig', None),
('deploymentconfig', None)]:
with (output_directory / f'{component}.yaml').open('wt') as fh: with (output_directory / f'{component}.yaml').open('wt') as fh:
cmd = ['oc', 'get', '--export', '-o', 'yaml', component] cmd = ['oc', 'get', '-o', 'yaml', component]
if name:
cmd += [name]
print(f'Running: {" ".join(cmd)}') print(f'Running: {" ".join(cmd)}')
subprocess.run(cmd, stdout=fh, check=True) subprocess.run(cmd, stdout=fh, check=True)
print(f'Config in: {output_directory.absolute()}') print(f'Config in: {output_directory.absolute()}')
def handler(args: argparse.ArgumentParser) -> None: def handler(args: argparse.ArgumentParser) -> None:
if args.instance == 'cara': login_server = 'https://api.paas.okd.cern.ch:443'
login_server = 'https://openshift.cern.ch:443' if args.instance == 'cara-prod':
project_name = 'cara' project_name = 'cara-prod'
elif args.instance == 'test-cara': elif args.instance == 'test-cara':
login_server = 'https://openshift-dev.cern.ch:443'
project_name = 'test-cara' project_name = 'test-cara'
actual_login_server = get_oc_server() actual_login_server = get_oc_server()
if actual_login_server != login_server: if actual_login_server != login_server:
print(f'\nPlease login to the correct openshift server with: \n\n oc login {login_server}\n', file=sys.stderr) print(f'\nPlease login to the correct OpenShift server with: \n\n oc login {login_server}\n', file=sys.stderr)
sys.exit(1) sys.exit(1)
subprocess.run(['oc', 'project', project_name], stdout=subprocess.DEVNULL, check=True) subprocess.run(['oc', 'project', project_name], stdout=subprocess.DEVNULL, check=True)

View file

@ -8,7 +8,7 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
parser.description = "Generate the config files which can be later submitted to openshift" parser.description = "Generate the config files which can be later submitted to openshift"
parser.set_defaults(handler=handler) parser.set_defaults(handler=handler)
parser.add_argument( parser.add_argument(
"instance", choices=['cara', 'test-cara'], "instance", choices=['cara-prod', 'test-cara'],
help="Pick the instance for which you want to generate the config", help="Pick the instance for which you want to generate the config",
) )
parser.add_argument( parser.add_argument(
@ -28,7 +28,6 @@ def generate_config(output_directory: pathlib.Path, project_name: str, hostname:
print(f'Running: {" ".join(cmd)}') print(f'Running: {" ".join(cmd)}')
subprocess.run(cmd, stdout=fh, check=True) subprocess.run(cmd, stdout=fh, check=True)
oc_process('routes', context={'HOST': hostname})
oc_process('configmap') oc_process('configmap')
oc_process('services') oc_process('services')
oc_process('imagestreams') oc_process('imagestreams')
@ -39,13 +38,13 @@ def generate_config(output_directory: pathlib.Path, project_name: str, hostname:
def handler(args: argparse.ArgumentParser) -> None: def handler(args: argparse.ArgumentParser) -> None:
if args.instance == 'cara': if args.instance == 'cara-prod':
project_name = 'cara' project_name = 'cara-prod'
branch = 'master' branch = 'master'
hostname = 'cara.web.cern.ch' hostname = 'cara.web.cern.ch'
elif args.instance == 'test-cara': elif args.instance == 'test-cara':
branch = 'live/test-cara'
project_name = 'test-cara' project_name = 'test-cara'
branch = 'live/test-cara'
hostname = 'test-cara.web.cern.ch' hostname = 'test-cara.web.cern.ch'
generate_config(pathlib.Path(args.output_directory), project_name, hostname, branch) generate_config(pathlib.Path(args.output_directory), project_name, hostname, branch)

View file

@ -22,8 +22,9 @@ def clean_ephemeral_config(config: dict):
config.get('metadata', []).clear() config.get('metadata', []).clear()
METADATA_TO_PRESERVE = ['labels', 'name'] METADATA_TO_PRESERVE = ['labels', 'name']
CERN_OKD4_METADATA_LABELS = ['migration.openshift.io', 'velero.io']
for item in config['items']: for item in config.get('items', {}):
item.pop('status', None) item.pop('status', None)
for key in list(item['metadata'].keys()): for key in list(item['metadata'].keys()):
@ -31,10 +32,14 @@ def clean_ephemeral_config(config: dict):
del item['metadata'][key] del item['metadata'][key]
item.get('spec', {}).pop('clusterIP', None) item.get('spec', {}).pop('clusterIP', None)
item.get('spec', {}).pop('clusterIPs', None)
item.get('spec', {}).pop('revisionHistoryLimit', None)
if item['kind'] == 'BuildConfig': if item['kind'] == 'BuildConfig':
for trigger in item.get('spec', {}).get('triggers', []): for trigger in item.get('spec', {}).get('triggers', []):
trigger.get('imageChange', {}).pop('lastTriggeredImageID', None) trigger.get('imageChange', {}).pop('lastTriggeredImageID', None)
item.get('spec', {}).pop('failedBuildsHistoryLimit', None)
item.get('spec', {}).pop('successfulBuildsHistoryLimit', None)
if item['kind'] == 'DeploymentConfig': if item['kind'] == 'DeploymentConfig':
item['spec'].get('template', {}).get('metadata', {}).pop('creationTimestamp', None) item['spec'].get('template', {}).get('metadata', {}).pop('creationTimestamp', None)
@ -46,6 +51,11 @@ def clean_ephemeral_config(config: dict):
for trigger in item['spec'].get('triggers', []): for trigger in item['spec'].get('triggers', []):
trigger.get('imageChangeParams', {}).pop('lastTriggeredImage', None) trigger.get('imageChangeParams', {}).pop('lastTriggeredImage', None)
for label in list(item['metadata'].get('labels', {}).keys()):
for prefix in CERN_OKD4_METADATA_LABELS:
if label.startswith(prefix):
item['metadata']['labels'].pop(label)
# Drop the template part of the config for now. # Drop the template part of the config for now.
# TODO: Remove this constraint to ensure our deployments reflect the fact that they are templated. # TODO: Remove this constraint to ensure our deployments reflect the fact that they are templated.
r = item['metadata'].get('labels', {}).pop('template', None) r = item['metadata'].get('labels', {}).pop('template', None)

View file

@ -1,6 +1,6 @@
--- ---
kind: "Template" kind: "Template"
apiVersion: "v1" apiVersion: template.openshift.io/v1
metadata: metadata:
name: "cara-configuration" name: "cara-configuration"
annotations: annotations:

View file

@ -1,6 +1,6 @@
--- ---
kind: "Template" kind: "Template"
apiVersion: "v1" apiVersion: template.openshift.io/v1
metadata: metadata:
name: "cara-application" name: "cara-application"
annotations: annotations:
@ -10,7 +10,7 @@
template: "cara-application" template: "cara-application"
objects: objects:
- -
apiVersion: v1 apiVersion: apps.openshift.io/v1
kind: DeploymentConfig kind: DeploymentConfig
metadata: metadata:
name: auth-service name: auth-service
@ -69,7 +69,7 @@
name: 'auth-service:latest' name: 'auth-service:latest'
namespace: ${PROJECT_NAME} namespace: ${PROJECT_NAME}
- -
apiVersion: v1 apiVersion: apps.openshift.io/v1
kind: DeploymentConfig kind: DeploymentConfig
metadata: metadata:
name: cara-app name: cara-app
@ -126,7 +126,7 @@
name: 'cara-webservice:latest' name: 'cara-webservice:latest'
namespace: ${PROJECT_NAME} namespace: ${PROJECT_NAME}
- -
apiVersion: v1 apiVersion: apps.openshift.io/v1
kind: DeploymentConfig kind: DeploymentConfig
metadata: metadata:
name: cara-router name: cara-router
@ -179,7 +179,7 @@
namespace: ${PROJECT_NAME} namespace: ${PROJECT_NAME}
- type: ConfigChange - type: ConfigChange
- -
apiVersion: v1 apiVersion: apps.openshift.io/v1
kind: DeploymentConfig kind: DeploymentConfig
metadata: metadata:
name: cara-webservice name: cara-webservice
@ -208,7 +208,7 @@
- name: CARA_CALCULATOR_PREFIX - name: CARA_CALCULATOR_PREFIX
value: /calculator-cern value: /calculator-cern
- name: CARA_THEME - name: CARA_THEME
value: cara/apps/calculator/themes/cern value: cara/apps/templates/cern
image: '${PROJECT_NAME}/cara-webservice' image: '${PROJECT_NAME}/cara-webservice'
ports: ports:
- containerPort: 8080 - containerPort: 8080
@ -263,7 +263,7 @@
namespace: ${PROJECT_NAME} namespace: ${PROJECT_NAME}
- type: ConfigChange - type: ConfigChange
- -
apiVersion: v1 apiVersion: apps.openshift.io/v1
kind: DeploymentConfig kind: DeploymentConfig
metadata: metadata:
name: cara-calculator-open name: cara-calculator-open

View file

@ -1,6 +1,6 @@
--- ---
kind: "Template" kind: "Template"
apiVersion: "v1" apiVersion: template.openshift.io/v1
metadata: metadata:
name: "cara-imagestreams" name: "cara-imagestreams"
creationTimestamp: null creationTimestamp: null
@ -12,7 +12,7 @@
objects: objects:
- -
kind: ImageStream kind: ImageStream
apiVersion: v1 apiVersion: image.openshift.io/v1
metadata: metadata:
name: auth-service name: auth-service
spec: spec:
@ -20,7 +20,7 @@
local: False local: False
- -
kind: ImageStream kind: ImageStream
apiVersion: v1 apiVersion: image.openshift.io/v1
metadata: metadata:
name: cara-router name: cara-router
spec: spec:
@ -28,7 +28,7 @@
local: False local: False
- -
kind: ImageStream kind: ImageStream
apiVersion: v1 apiVersion: image.openshift.io/v1
metadata: metadata:
name: cara-webservice name: cara-webservice
spec: spec:

View file

@ -1,6 +1,6 @@
--- ---
kind: "Template" kind: "Template"
apiVersion: "v1" apiVersion: template.openshift.io/v1
metadata: metadata:
name: "cara-route" name: "cara-route"
creationTimestamp: null creationTimestamp: null
@ -11,7 +11,7 @@
template: "cara-route" template: "cara-route"
objects: objects:
- -
apiVersion: v1 apiVersion: route.openshift.io/v1
kind: Route kind: Route
metadata: metadata:
name: cara-route name: cara-route

View file

@ -1,6 +1,6 @@
--- ---
kind: "Template" kind: "Template"
apiVersion: "v1" apiVersion: template.openshift.io/v1
metadata: metadata:
name: "cara-services" name: "cara-services"
creationTimestamp: null creationTimestamp: null

View file

@ -33,7 +33,7 @@ from .user import AuthenticatedUser, AnonymousUser
# calculator version. If the calculator needs to make breaking changes (e.g. change # calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to # form attributes) then it can also increase its MAJOR version without needing to
# increase the overall CARA version (found at ``cara.__version__``). # increase the overall CARA version (found at ``cara.__version__``).
__version__ = "3.2.0" __version__ = "3.3.0"
class BaseRequestHandler(RequestHandler): class BaseRequestHandler(RequestHandler):
@ -148,11 +148,13 @@ class StaticModel(BaseRequestHandler):
class LandingPage(BaseRequestHandler): class LandingPage(BaseRequestHandler):
def get(self): def get(self):
template_environment = self.settings["template_environment"]
template = self.settings["template_environment"].get_template( template = self.settings["template_environment"].get_template(
"index.html.j2") "index.html.j2")
report = template.render( report = template.render(
user=self.current_user, user=self.current_user,
calculator_prefix=self.settings["calculator_prefix"], calculator_prefix=self.settings["calculator_prefix"],
text_blocks=template_environment.globals['common_text']
) )
self.finish(report) self.finish(report)
@ -229,7 +231,7 @@ def make_app(
calculator_templates = Path(__file__).parent / "templates" calculator_templates = Path(__file__).parent / "templates"
templates_directories = [cara_templates, calculator_templates] templates_directories = [cara_templates, calculator_templates]
if theme_dir: if theme_dir:
templates_directories.insert(0, theme_dir / 'templates') templates_directories.insert(0, theme_dir)
loader = jinja2.FileSystemLoader([str(path) for path in templates_directories]) loader = jinja2.FileSystemLoader([str(path) for path in templates_directories])
template_environment = jinja2.Environment( template_environment = jinja2.Environment(
loader=loader, loader=loader,

View file

@ -36,7 +36,6 @@ def main():
if theme_dir is not None: if theme_dir is not None:
theme_dir = Path(theme_dir).absolute() theme_dir = Path(theme_dir).absolute()
assert theme_dir.exists() assert theme_dir.exists()
assert (theme_dir / 'templates').exists()
app = make_app(debug=args.no_debug, calculator_prefix=args.prefix, theme_dir=theme_dir) app = make_app(debug=args.no_debug, calculator_prefix=args.prefix, theme_dir=theme_dir)
app.listen(args.port) app.listen(args.port)
IOLoop.instance().start() IOLoop.instance().start()

View file

@ -303,7 +303,7 @@ class FormData:
# Initializes a ventilation instance as a window if 'natural_ventilation' is selected, or as a HEPA-filter otherwise # Initializes a ventilation instance as a window if 'natural_ventilation' is selected, or as a HEPA-filter otherwise
if self.ventilation_type == 'natural_ventilation': if self.ventilation_type == 'natural_ventilation':
if self.window_opening_regime == 'windows_open_periodically': if self.window_opening_regime == 'windows_open_periodically':
window_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration) window_interval = models.PeriodicInterval(self.windows_frequency, self.windows_duration, min(self.infected_start, self.exposed_start))
else: else:
window_interval = always_on window_interval = always_on
@ -694,7 +694,7 @@ MECHANICAL_VENTILATION_TYPES = {'mech_type_air_changes', 'mech_type_air_supply',
MASK_TYPES = {'Type I', 'FFP2'} MASK_TYPES = {'Type I', 'FFP2'}
MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'} MASK_WEARING_OPTIONS = {'mask_on', 'mask_off'}
VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation'} VENTILATION_TYPES = {'natural_ventilation', 'mechanical_ventilation', 'no_ventilation'}
VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_B117', 'SARS_CoV_2_P1', 'SARS_CoV_2_B16172'} VIRUS_TYPES = {'SARS_CoV_2', 'SARS_CoV_2_B117', 'SARS_CoV_2_B1351','SARS_CoV_2_P1', 'SARS_CoV_2_B16172', 'SARS_CoV_2_B11529'}
VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'} VOLUME_TYPES = {'room_volume_explicit', 'room_volume_from_dimensions'}
WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'} WINDOWS_OPENING_REGIMES = {'windows_open_permanently', 'windows_open_periodically', 'not-applicable'}
WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'} WINDOWS_TYPES = {'window_sliding', 'window_hinged', 'not-applicable'}

View file

@ -308,10 +308,10 @@ class ReportGenerator:
context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form) context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form)
context['calculator_prefix'] = self.calculator_prefix context['calculator_prefix'] = self.calculator_prefix
context['scale_warning'] = { context['scale_warning'] = {
'level': 'yellow-2', 'level': 'red-4',
'incidence_rate': 'lower than 25 new cases per 100 000 inhabitants', 'incidence_rate': 'higher or equal to 100 new cases per 100 000 inhabitants',
'onsite_access': 'of about 8000', 'onsite_access': 'lower than 4000',
'threshold': '' 'threshold': '5%'
} }
return context return context

View file

@ -236,6 +236,20 @@ function on_ventilation_type_change() {
}); });
} }
function on_wearing_mask_change() {
wearing_mask = $('input[type=radio][name=mask_wearing_option]')
wearing_mask.each(function (index) {
if (this.checked) {
getChildElement($(this)).show();
require_fields(this);
}
else {
getChildElement($(this)).hide();
require_fields(this);
}
})
}
/* -------UI------- */ /* -------UI------- */
function show_disclaimer() { function show_disclaimer() {
@ -558,6 +572,12 @@ $(document).ready(function () {
// Call the function now to handle forward/back button presses in the browser. // Call the function now to handle forward/back button presses in the browser.
on_ventilation_type_change(); on_ventilation_type_change();
// When the mask_wearing_option changes we want to make its respective
// children show/hide.
$("input[type=radio][name=mask_wearing_option]").change(on_wearing_mask_change);
// Call the function now to handle forward/back button presses in the browser.
on_wearing_mask_change();
// Setup the maximum number of people at page load (to handle back/forward), // Setup the maximum number of people at page load (to handle back/forward),
// and update it when total people is changed. // and update it when total people is changed.
setMaxInfectedPeople(); setMaxInfectedPeople();

View file

@ -154,16 +154,16 @@ class ExposureModelResult(View):
def update_textual_result(self, model: models.ExposureModel): def update_textual_result(self, model: models.ExposureModel):
lines = [] lines = []
P = model.infection_probability() P = np.array(model.infection_probability()).mean()
# lines.append(f'Emission rate (virus/hr): {np.round(model.concentration_model.infected.emission_rate_when_present(),0)}') # lines.append(f'Emission rate (virus/hr): {np.round(model.concentration_model.infected.emission_rate_when_present(),0)}')
lines.append(f'<b>Probability of infection: </b>{np.round(P, 0)}%') lines.append(f'<b>Probability of infection: </b>{np.round(P, 0)}%')
lines.append(f'<b>Number of exposed: </b>{model.exposed.number}') lines.append(f'<b>Number of exposed: </b>{model.exposed.number}')
new_cases = np.round(model.expected_new_cases(), 1) new_cases = np.round(np.array(model.expected_new_cases(), 1).mean())
lines.append(f'<b>Number of expected new cases: </b>{new_cases}') lines.append(f'<b>Number of expected new cases: </b>{new_cases}')
R0 = np.round(model.reproduction_number(), 1) R0 = np.round(np.array(model.reproduction_number(), 1).mean())
lines.append(f'<b>Reproduction number (R0): </b>{R0}') lines.append(f'<b>Reproduction number (R0): </b>{R0}')
self.html_output.value = '<br>\n'.join(lines) self.html_output.value = '<br>\n'.join(lines)

View file

@ -170,6 +170,10 @@ body {
display: block; display: block;
} }
.mask_icons {
height: 4em;
}
/*===== FIXED BACKGROUND IMG =====*/ /*===== FIXED BACKGROUND IMG =====*/
.fixed-background { .fixed-background {
@ -230,6 +234,10 @@ footer img {
width: 25%; width: 25%;
} }
#mobile_calculator_option {
display: none;
}
#nat_vent_image { #nat_vent_image {
height: 15em; height: 15em;
} }
@ -242,7 +250,7 @@ footer img {
height: 6em; height: 6em;
margin: 1%; margin: 1%;
} }
#mobile-app-buttons { #calculator_app_button {
display: none!important; display: none!important;
} }
.feedback { .feedback {
@ -282,6 +290,9 @@ footer img {
.nav-link { .nav-link {
padding: .5rem .5rem!important; padding: .5rem .5rem!important;
} }
#apps_dropdown {
display: none;
}
#report_version { #report_version {
font-size: .5rem; font-size: .5rem;
} }
@ -298,13 +309,6 @@ footer img {
#mobile_link { #mobile_link {
display: inline!important; display: inline!important;
} }
#desktop_logo {
display: none!important;
}
#mobile_logo {
display: block!important;
}
.feedback { .feedback {
float:right; float:right;
font-size:.75rem; font-size:.75rem;
@ -324,16 +328,16 @@ footer img {
} */ } */
/* Large (lg) devices (desktops, 992px and up) */ /* Large (lg) devices (tablets) */
@media (max-width: 992px) { @media (max-width: 64em) {
#download-pdf { .expert_app_button {
display: none; display: none;
} }
#link_reproduce_results { #desktop_logo {
display: none; display: none!important;
} }
#mobile_link { #mobile_logo {
display: inline!important; display: block!important;
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -63,13 +63,15 @@
</div><br> </div><br>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">Variant:</label></div> <div class="col-sm-3"><label class="col-form-label">Variant:</label></div>
<div class="col-sm-6"> <div class="col-sm-7">
<select id="Variant" name="virus_type" class="form-control"> <select id="Variant" name="virus_type" class="form-control">
<option value="SARS_CoV_2">SARS-CoV-2 (nominal strain)</option> <option value="SARS_CoV_2">SARS-CoV-2 (nominal strain)</option>
<option value="SARS_CoV_2_B117">SARS-CoV-2 (Alpha VOC)</option> <option value="SARS_CoV_2_B117">SARS-CoV-2 (Alpha VOC)</option>
<option value="SARS_CoV_2_B1351">SARS-CoV-2 (Beta VOC)</option>
<option value="SARS_CoV_2_P1">SARS-CoV-2 (Gamma VOC)</option> <option value="SARS_CoV_2_P1">SARS-CoV-2 (Gamma VOC)</option>
<option selected value="SARS_CoV_2_B16172">SARS-CoV-2 (Delta VOC)</option> <option value="SARS_CoV_2_B16172">SARS-CoV-2 (Delta VOC)</option>
<option selected value="SARS_CoV_2_B11529">SARS-CoV-2 (Omicron VOC)</option>
</select> </select>
</div> </div>
</div> </div>
@ -77,9 +79,11 @@
<hr width="80%"> <hr width="80%">
<b>Room data:</b> <b>Room data:</b>
<div data-tooltip="The area you wish to study (choose one of the 2 options). Use GIS Portal or measure. Also indicate if a central (radiator-type) heating system is in use."> {% block room_data %}
<div data-tooltip="The area you wish to study (choose one of the 2 options). Indicate if a central (radiator-type) heating system is in use.">
<span class="tooltip_text">?</span> <span class="tooltip_text">?</span>
</div> </div>
{% endblock room_data %}
<br> <br>
@ -188,7 +192,7 @@
<label for="mech_type_air_supply" class="col-form-label ml-2">Air supply flow rate</label> <label for="mech_type_air_supply" class="col-form-label ml-2">Air supply flow rate</label>
</div> </div>
<div> <div>
<input type="number" step="any" id="air_supply" class="non_zero form-control" name="air_supply" min="0" placeholder="(m³ / hour)" data-has-radio="#mech_type_air_supply"><br> <input type="number" step="any" id="air_supply" class="non_zero form-control" name="air_supply" min="0" placeholder="Flow rate (m³ / hour)" data-has-radio="#mech_type_air_supply"><br>
</div> </div>
</div> </div>
<div class="split"> <div class="split">
@ -197,7 +201,7 @@
<label for="mech_type_air_changes" class="col-form-label ml-2">Air changes per hour</label> <label for="mech_type_air_changes" class="col-form-label ml-2">Air changes per hour</label>
</div> </div>
<div> <div>
<input type="number" step="any" id="air_changes" class="non_zero form-control" name="air_changes" min="0" placeholder="(h⁻¹) only fresh air" data-has-radio="#mech_type_air_changes"><br> <input type="number" step="any" id="air_changes" class="non_zero form-control" name="air_changes" min="0" placeholder="Air exchange (h⁻¹)" data-has-radio="#mech_type_air_changes"><br>
</div> </div>
</div> </div>
</div> </div>
@ -205,11 +209,11 @@
<div id="DIVnatural_ventilation" class="tabbed" style="display:none"> <div id="DIVnatural_ventilation" class="tabbed" style="display:none">
<div class="split"> <div class="split">
<div><label class="col-form-label">Number of windows:</label></div> <div><label class="col-form-label">Number of windows:</label></div>
<div><input type="number" id="windows_number" class="non_zero form-control" name="windows_number" min="1"><br></div> <div><input type="number" id="windows_number" class="non_zero form-control" name="windows_number" placeholder="Number (#)" min="1"><br></div>
</div> </div>
<div class="split"> <div class="split">
<div><label class="col-form-label">Height of window: </label></div> <div><label class="col-form-label">Height of window: </label></div>
<div><input type="number" step="any" id="window_height" class="non_zero form-control" name="window_height" placeholder="meters" min="0"><br></div> <div><input type="number" step="any" id="window_height" class="non_zero form-control" name="window_height" placeholder="Height (m)" min="0"><br></div>
</div> </div>
<div class='sub_title'>Window type:</div> <div class='sub_title'>Window type:</div>
<input class="ml-2" type="radio" id="window_sliding" name="window_type" value="window_sliding" onclick="require_fields(this)" checked="checked"> <input class="ml-2" type="radio" id="window_sliding" name="window_type" value="window_sliding" onclick="require_fields(this)" checked="checked">
@ -218,11 +222,11 @@
<label for="window_hinged">Top- or Bottom-Hung</label><br> <label for="window_hinged">Top- or Bottom-Hung</label><br>
<div class="split"> <div class="split">
<div><label class="col-form-label">Width of window: </label></div> <div><label class="col-form-label">Width of window: </label></div>
<div><input type="number" step="any" id="window_width" class="non_zero disabled form-control" name="window_width" placeholder="meters" min="0" data-has-radio="#window_hinged"><br></div> <div><input type="number" step="any" id="window_width" class="non_zero disabled form-control" name="window_width" placeholder="Width (m)" min="0" data-has-radio="#window_hinged"><br></div>
</div> </div>
<div class="split"> <div class="split">
<div><label class="col-form-label">Opening distance: </label></div> <div><label class="col-form-label">Opening distance: </label></div>
<div><input type="number" step="any" id="opening_distance" class="non_zero form-control" name="opening_distance" placeholder="meters" min="0"><br></div> <div><input type="number" step="any" id="opening_distance" class="non_zero form-control" name="opening_distance" placeholder="Opening distance (m)" min="0"><br></div>
</div> </div>
<div class='sub_title'>Window open:</div> <div class='sub_title'>Window open:</div>
<div class="form-group row"> <div class="form-group row">
@ -247,7 +251,7 @@
<label for="hepa_yes" class="col-form-label ml-2">Yes</label> <label for="hepa_yes" class="col-form-label ml-2">Yes</label>
</div> </div>
<div> <div>
<input type="number" step="any" id="hepa_amount" class="non_zero disabled form-control" name="hepa_amount" placeholder="(m³ / hour)" min="0" data-has-radio="#hepa_yes"> <input type="number" step="any" id="hepa_amount" class="non_zero disabled form-control" name="hepa_amount" placeholder="Flow rate (m³ / hour)" min="0" data-has-radio="#hepa_yes">
</div> </div>
</div> </div>
<hr width="80%"> <hr width="80%">
@ -260,15 +264,30 @@
<br> <br>
<div class='sub_title'>Are masks worn when occupants are at workstations?</div> <div class='sub_title'>Are masks worn when occupants are at workstations?</div>
<input type="radio" id="mask_on" name="mask_wearing_option" value="mask_on" required> <input type="radio" id="mask_on" name="mask_wearing_option" value="mask_on" data-enables="#DIVmasks_used">
<label for="mask_on">Yes</label> <label for="mask_on">Yes</label>
<input class="ml-2" type="radio" id="mask_off" name="mask_wearing_option" value="mask_off" required checked="checked"> <input class="ml-2" type="radio" id="mask_off" name="mask_wearing_option" value="mask_off" checked="checked">
<label for="mask_off">No</label><br> <label for="mask_off">No</label><br>
Type of masks used:
<input type="radio" id="mask_type_1" name="mask_type" value="Type I" checked="checked" onclick="require_fields(this)"> <div id="DIVmasks_used" style="display:none">
<label for="mask_type_1">Type 1</label> <div class='sub_title'>Type of masks used:</div>
<input class="ml-2" type="radio" id="mask_type_ffp2" name="mask_type" value="FFP2" onclick="require_fields(this)"> <div class='split'>
<label for="mask_type_ffp2">FFP2</label><br> <div>
<input type="radio" id="mask_type_1" name="mask_type" value="Type I" checked="checked" onclick="require_fields(this)">
<label for="mask_type_1">
Surgical/Type I
<img class="mask_icons" src="/static/images/masks/t1.png">
</label>
</div>
<div>
<input type="radio" id="mask_type_ffp2" name="mask_type" value="FFP2" onclick="require_fields(this)">
<label for="mask_type_ffp2">
Respirator/FFP2
<img class="mask_icons" src="/static/images/masks/ffp2.png">
</label>
</div>
</div>
</div>
<hr width="80%"> <hr width="80%">
</div> </div>
@ -282,7 +301,7 @@
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-4"><label class="col-form-label">Total number of occupants:</label></div> <div class="col-sm-4"><label class="col-form-label">Total number of occupants:</label></div>
<div class="col-sm-6 align-self-center"><input type="number" id="total_people" class="form-control" name="total_people" min=1 required></div> <div class="col-sm-6 align-self-center"><input type="number" id="total_people" class="form-control" name="total_people" placeholder="Number (#)" min=1 required></div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -290,7 +309,7 @@
<div class="col-sm-6 align-self-center"><input type="number" id="infected_people" class="form-control" name="infected_people" min=1 value=1 required></div> <div class="col-sm-6 align-self-center"><input type="number" id="infected_people" class="form-control" name="infected_people" min=1 value=1 required></div>
</div> </div>
<span id="training_limit_error" class="red_text" hidden>Training activities limited to 1 infected<br></span> <span id="training_limit_error" class="red_text" hidden>Conference/Training activities limited to 1 infected<br></span>
<hr width="80%"> <hr width="80%">
<div class="form-group row"> <div class="form-group row">
@ -305,7 +324,7 @@
<option value="library">Library</option> <option value="library">Library</option>
<option value="lab">Laboratory</option> <option value="lab">Laboratory</option>
<option value="workshop">Workshop</option> <option value="workshop">Workshop</option>
<option value="training">Training</option> <option value="training">Conference/Training</option>
<option value="gym">Gym</option> <option value="gym">Gym</option>
</select> </select>
</div> </div>
@ -499,9 +518,11 @@
<b>Virus data:</b> <br> <b>Virus data:</b> <br>
SARS-CoV-2 covers the original "wild type" strain of the virus and three variants of concern (VOC):<br> SARS-CoV-2 covers the original "wild type" strain of the virus and three variants of concern (VOC):<br>
<ul> <ul>
<li>Alpha (also known as B.1.1.7, first identified in UK, Dec 2020),</li> <li>Alpha (also known as B.1.1.7, first identified in UK, Sept 2020),</li>
<li>Beta (also known as B.1.351, first identified in South Africa, May 2020).</li>
<li>Gamma (also known as P.1, first identified in Brazil/Japan, Jan 2021).</li> <li>Gamma (also known as P.1, first identified in Brazil/Japan, Jan 2021).</li>
<li>Delta (also known as B.1.617.2, first identified in India, Oct 2020).</li> <li>Delta (also known as B.1.617.2, first identified in India, Oct 2020).</li>
<li>Omicron (also known as B.1.1.529, first identified in South Africa, November 2021).</li>
</ul> </ul>
Modify the default as necessary, according to local area prevalence e.g. for <a href="https://www.covid19.admin.ch/fr/epidemiologic/virus-variants?detGeo=GE">Geneva</a> Modify the default as necessary, according to local area prevalence e.g. for <a href="https://www.covid19.admin.ch/fr/epidemiologic/virus-variants?detGeo=GE">Geneva</a>
or <a href="https://www.santepubliquefrance.fr/dossiers/coronavirus-covid-19/covid-19-cartographie-des-variants-en-france-donnees-par-region-et-par-departement">Ain (France)</a>.<br> or <a href="https://www.santepubliquefrance.fr/dossiers/coronavirus-covid-19/covid-19-cartographie-des-variants-en-france-donnees-par-region-et-par-departement">Ain (France)</a>.<br>
@ -528,8 +549,8 @@
<li>Library = all seated, no talking, just breathing,</li> <li>Library = all seated, no talking, just breathing,</li>
<li>Laboratory = light physical activity, talking 50% of the time,</li> <li>Laboratory = light physical activity, talking 50% of the time,</li>
<li>Workshop = moderate physical activity, talking 50% of the time,</li> <li>Workshop = moderate physical activity, talking 50% of the time,</li>
<li>Training = trainer standing and talking, rest seated and talking quietly. <li>Conference/Training = speaker/trainer standing and talking, rest seated and talking quietly.
Trainer assumed infected (worst case scenario),</li> Speaker/Trainer assumed infected (worst case scenario),</li>
<li>Gym = heavy exercise, no talking, just breathing.</li> <li>Gym = heavy exercise, no talking, just breathing.</li>
</ul> </ul>
<b>Activity breaks:</b><br> <b>Activity breaks:</b><br>

View file

@ -21,7 +21,7 @@
<div id="report-header-div" class="d-flex flex-row" style="margin: 1%"> <div id="report-header-div" class="d-flex flex-row" style="margin: 1%">
<img id="report_logo" src="/static/images/cara_logo.200x200.png" class="d-inline-block align-middle mr-3"> <img id="report_logo" src="/static/images/cara_logo.200x200.png" class="d-inline-block align-middle mr-3">
<div style="margin-right: -105px" class='align-self-center mr-auto'> <div style="margin-right: -105px" class='align-self-center mr-auto'>
<h2 class="header_text mb-0">CARA - CALCULATOR REPORT</h1> <h2 class="header_text mb-0">REPORT</h1>
<p class="mb-0" id="report_version"> Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}</p> <p class="mb-0" id="report_version"> Created {{ creation_date }} using CARA calculator version v{{ form.calculator_version }}</p>
</div> </div>
<button type="button" class="btn btn-outline-dark align-self-center" id="download-pdf" style="margin-right: -100pt" onclick="print()">Print Report</button> <button type="button" class="btn btn-outline-dark align-self-center" id="download-pdf" style="margin-right: -100pt" onclick="print()">Print Report</button>
@ -202,10 +202,14 @@
SARS-CoV-2 (nominal strain) SARS-CoV-2 (nominal strain)
{% elif form.virus_type == "SARS_CoV_2_B117" %} {% elif form.virus_type == "SARS_CoV_2_B117" %}
<a href="https://www.ecdc.europa.eu/en/publications-data/covid-19-risk-assessment-spread-new-sars-cov-2-variants-eueea">SARS-CoV-2 (Alpha VOC) </a> <a href="https://www.ecdc.europa.eu/en/publications-data/covid-19-risk-assessment-spread-new-sars-cov-2-variants-eueea">SARS-CoV-2 (Alpha VOC) </a>
{% elif form.virus_type == "SARS_CoV_2_B1351" %}
<a href="https://www.eurosurveillance.org/content/10.2807/1560-7917.ES.2021.26.24.2100509">SARS-CoV-2 (Beta VOC) </a>
{% elif form.virus_type == "SARS_CoV_2_P1" %} {% elif form.virus_type == "SARS_CoV_2_P1" %}
<a href="https://doi.org/10.1126/science.abh2644">SARS-CoV-2 (Gamma VOC)</a> <a href="https://doi.org/10.1126/science.abh2644">SARS-CoV-2 (Gamma VOC)</a>
{% elif form.virus_type == "SARS_CoV_2_B16172" %} {% elif form.virus_type == "SARS_CoV_2_B16172" %}
<a href="https://www.bmj.com/content/373/bmj.n1513">SARS-CoV-2 (Delta VOC)</a> <a href="https://www.bmj.com/content/373/bmj.n1513">SARS-CoV-2 (Delta VOC)</a>
{% elif form.virus_type == "SARS_CoV_2_B11529" %}
<a href="https://gitlab.cern.ch/cara/cara/-/issues/226">SARS-CoV-2 (Omicron VOC)</a>
{% endif %} {% endif %}
</p></li> </p></li>
<li><p class="data_text">Room Volume: {{ model.concentration_model.room.volume }} m³</p></li> <li><p class="data_text">Room Volume: {{ model.concentration_model.room.volume }} m³</p></li>
@ -298,7 +302,7 @@
{% elif form.activity_type == "workshop" %} {% elif form.activity_type == "workshop" %}
Workshop = assembly workshop environment, all persons doing moderate physical activity, talking 50% of the time. Workshop = assembly workshop environment, all persons doing moderate physical activity, talking 50% of the time.
{% elif form.activity_type == "training" %} {% elif form.activity_type == "training" %}
Training one person (the trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the trainer is the infected person, for the worst case scenario. Conference/Training one person (the speaker/trainer) standing, talking, all others seated, talking quietly (whispering). It is assumed the speaker/trainer is the infected person, for the worst case scenario.
{% elif form.activity_type == "lab" %} {% elif form.activity_type == "lab" %}
Laboratory = Lab or technical environment, all persons doing light physical activity, talking 50% of the time. Laboratory = Lab or technical environment, all persons doing light physical activity, talking 50% of the time.
{% elif form.activity_type == "gym" %} {% elif form.activity_type == "gym" %}

View file

@ -0,0 +1,59 @@
{% extends "layout.html.j2" %}
{% set active_page="home/" %}
{% block main %}
{# <div style="height: 5em; display: block;"></div> #}
<header class= "bg-light">
<div class="container container--padding">
<img src="/static/images/cara_full_text.png" class="logo d-block m-auto" id="desktop_logo">
<img src="/static/images/cara_full_logo.png" class="logo d-none m-auto" id="mobile_logo">
</div>
</header>
<div class="container container--padding">
<div class="d-flex mb-3 justify-content-center" id="calculator_app_button">
<div><a href="{{ calculator_prefix }}" role="button" class="btn btn-outline-primary"><div class="d-flex d-row"><i class="icon-calculator"></i><span class="pl-1">Calculator</div></a></div>
</div>
<div class="split">
<div class="col-lg-8 col-md-7 pl-0">
<h2 class="paragraph-title">Introduction</h2><br>
<div>
<p>
CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions.
It does this by simulating the long-range airborne spread SARS-CoV-2 virus in a finite volume, assuming homogenous mixing, and it estimates the risk of COVID-19 airborne transmission therein.
Please see the <a href="/about">About</a> page for more details on the methodology, assumptions and limitations of CARA.
</p>
<p>
The full CARA source code can be accessed freely under an Apache 2.0 open source license from our <a href="https://gitlab.cern.ch/cara/cara">code repository</a>.
It includes detailed instructions on how to run your own version of this tool.
</p>
<br>
</div>
</div>
<div class="align-self-center">
<img src="static/images/CARA_1_Vs3_Colour.jpg" class="cara_home_image">
</div>
</div>
<br>
<div id="apps_section" class="d-none">
<div class="d-flex flex-row" >
<h2 class="paragraph-title pr-4 align-self-center">Apps:</h2>
<br>
<div class="pr-3"><a href="{{ calculator_prefix }}" role="button" class="btn btn-lg btn-outline-primary"><div class="d-flex d-row"><i class="icon-calculator"></i><span class="pl-2">Calculator</div></a></div>
<br>
<div class="expert_app_button"><a href="https://cara.web.cern.ch/expert-app" role="button" class="btn btn-lg btn-outline-secondary"><div class="d-flex d-row"><i class="icon-expert"></i><span class="pl-2">Expert (beta)</div></a></div>
</div>
<hr width="95%">
</div>
{% block cara_at_cern %}
{% endblock cara_at_cern %}
<br>
<h3 class="paragraph-title">Acknowledgements</h3><br>
{{ text_blocks['Acknowledgements'] }}
<span style="height: 3vh; display: block;"></span>
</div>
</div>
{% endblock main %}

View file

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>
{% block title %}
CARA | COVID Airborne Risk Assessment
{% endblock title %}
</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="/static/css/style.css">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,600,700&display=swap" rel="stylesheet">
{% block extra_headers %}
{% endblock extra_headers %}
</head>
<body>
<nav class="navbar navbar-dark navbar-expand-lg">
<div class="container">
<a href="/" class="navbar-brand"><img src="/static/images/cara_logo_white_text.png" alt="Logo" title="Logo"></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-link"><a href="/" class="{{ "header-navbar nav-link active" if "home/" == active_page else "header-navbar nav-link" }}">HOME</a></li>
<div id="apps_dropdown">
<li class="nav-item dropdown p-2">
<a class="nav-link dropdown-toggle {{ "header-navbar nav-link active" if "calculator/" in active_page else "header-navbar nav-link" }}" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
APPS
</a>
<ul class="dropdown-menu dropwown-navbar-colors" style="min-width: 14rem;" aria-labelledby="navbarDropdown">
<li><a href="{{ calculator_prefix }}" class="{{ "header-navbar nav-link active" if "calculator/" == active_page else "header-navbar nav-link" }}">CARA CALCULATOR</a></li>
<li><a href="{{ calculator_prefix }}/user-guide" style="margin-left: 4rem" class="{{ "header-navbar nav-link active" if "user-guide" in active_page else "header-navbar nav-link" }}">USER GUIDE</a></li>
<li><a href="/expert-app" class="{{ "header-navbar nav-link active" if "/expert-app" == active_page else "header-navbar nav-link" }}">EXPERT APP (BETA)</a></li>
</ul>
</li>
</div>
<div id="mobile_calculator_option">
<li class="nav-link"><a href="{{ calculator_prefix }}" class="{{ "header-navbar nav-link active" if "calculator/" == active_page else "header-navbar nav-link" }}">CARA CALCULATOR</a></li>
<li class="nav-link"><a href="{{ calculator_prefix }}/user-guide" class="{{ "header-navbar nav-link active" if "user-guide" in active_page else "header-navbar nav-link" }}">USER GUIDE</a></li>
</div>
{% block covid_information%}
{% endblock covid_information%}
<li class="nav-link"><a href="/about" class="{{ "header-navbar nav-link active" if "about" == active_page else "header-navbar nav-link" }}">ABOUT</a></li>
{% if user.is_authenticated() %}
<li class="nav-item dropdown p-2">
<a class="nav-link active dropdown-toggle d-inline-block" href="https://cern.ch/users-portal" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Signed in as: {{ user.username }}
</a>
<ul class="dropdown-menu dropwown-navbar-colors" style="min-width: 14rem;" aria-labelledby="navbarDropdown">
<li><a href="/auth/logout" class="nav-link ml-2">Sign out</a></li>
</ul>
{# Sent to Piwik for statistics #}
<script>
var AuthUserDomain = "{{ user.domain() }}";
</script>
{% endif %}
</li>
</ul>
</div>
</div>
</nav>
<main role="main">
{% block main %}
{% endblock main %}
</main>
<footer>
<div class="container">
<div class="row text-light text-center py-4 justify-content-center">
<div class="col-sm-10 col-md-8 col-lg-6">
<img src="/static/images/cara_logo_white_text.png" alt="Logo">
<p><span style="font-size:10px;"><em>CERN strives to deploy its know-how and technologies to help solve
the challenges arising in the local and global fight against COVID-19. As a particle physics
research organisation, CERN is not in a position to advise on medical research, health or health
policy issues. Any initiative is conducted on a best effort and as-is basis, without liability or
warranty.</em></span></p>
<p style="font-size:10px;">
CARA is <a href="https://gitlab.cern.ch/cara/cara/-/blob/master/LICENSE" class="ext">Apache 2.0 licensed</a> open-source
software developed at CERN.
You can find the source code at <a href="https://gitlab.cern.ch/cara/cara">https://gitlab.cern.ch/cara/cara</a>,
where we welcome contributions, feature requests and issue reports.
</p>
</div>
</div>
</div>
</footer>
<script src="/static/js/js_packaged_for_theme.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
<script src="/static/js/jquery.colorbox-min.js"></script>
<script src="/static/js/ScrollMagic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/js/bootstrap.min.js" integrity="sha512-8qmis31OQi6hIRgvkht0s6mCOittjMa9GMqtK9hes5iEQBQE/Ca6yGE5FsW36vyipGoWQswBj/QBm2JR086Rkw==" crossorigin="anonymous"></script>
<script src="/static/js/usage-tracking.js"></script>
<!-- Popper JS -->
<script src="/static/js/popper.min.js"></script>
<!-- Font Awesome -->
<script src="/static/js/all.min.js"></script>
{% block body_scripts %}
{% endblock body_scripts %}
</body>
</html>

View file

@ -70,8 +70,10 @@ The choices are:</p>
<ul> <ul>
<li><code>SARS-CoV-2 (nominal strain)</code>, covering typical strains and variants which are not of concern from an epidemiologic point of view of the virus;</li> <li><code>SARS-CoV-2 (nominal strain)</code>, covering typical strains and variants which are not of concern from an epidemiologic point of view of the virus;</li>
<li><code>SARS-CoV-2 (Alpha VOC)</code>, first identified in the UK at the end of 2020 which is found to be approximately 1.5x more transmissible compared to the non-VOCs; </li> <li><code>SARS-CoV-2 (Alpha VOC)</code>, first identified in the UK at the end of 2020 which is found to be approximately 1.5x more transmissible compared to the non-VOCs; </li>
<li><code>SARS-CoV-2 (Beta VOC)</code>, first identified in South Africa in May 2020 which is found to be approximately 1.25x more transmissible compared to the non-VOCs; </li>
<li><code>SARS-CoV-2 (Gamma VOC)</code>, first identified in Brazil in January 2021 which is found to be approximately 2.2x more transmissible compared to the non-VOCs.</li> <li><code>SARS-CoV-2 (Gamma VOC)</code>, first identified in Brazil in January 2021 which is found to be approximately 2.2x more transmissible compared to the non-VOCs.</li>
<li><code>SARS-CoV-2 (Delta VOC)</code>, first identified in India towards the end of 2020 which is found to be approximately 60% more transmissible compared to the ALPHA VOC.</li> <li><code>SARS-CoV-2 (Delta VOC)</code>, first identified in India towards the end of 2020 which is found to be approximately 60% more transmissible compared to the ALPHA VOC.</li>
<li><code>SARS-CoV-2 (Omicron VOC)</code>, first identified in South Africa in November 2021 which is found to be at least 2.53x more transmissible compared to the DELTA VOC.</li>
</ul> </ul>
<p>The user can modify the selected variant from the default, according to the prevalence of the different variants in the local area. Access to this information can be found here:</p> <p>The user can modify the selected variant from the default, according to the prevalence of the different variants in the local area. Access to this information can be found here:</p>
<ul> <ul>
@ -86,7 +88,9 @@ However, this value may be revised in the future as more studies of the Gamma VO
<h3>Room Data</h3> <h3>Room Data</h3>
<br> <br>
<p>Please enter either the room volume (in m³) or both the floor area (m²) and the room height (m). <p>Please enter either the room volume (in m³) or both the floor area (m²) and the room height (m).
This information is available via GIS Portal (<a href="https://gis.cern.ch/gisportal/">https://gis.cern.ch/gisportal/</a>).</p> {% block room_volume_guide %}
</p>
{% endblock room_volume_guide %}
<br> <br>
<h4>Room heating system</h4> <h4>Room heating system</h4>
<br> <br>
@ -157,9 +161,9 @@ Within the number of people occupying the space, you should specify how many are
<li><strong>Control Room (night shift)</strong> = All persons seated, all talking 10% of the time. Everyone (exposed and infected occupants) is treated the same in this model.</li> <li><strong>Control Room (night shift)</strong> = All persons seated, all talking 10% of the time. Everyone (exposed and infected occupants) is treated the same in this model.</li>
<li><strong>Lab</strong> = Based on a typical lab or technical working area, all persons are doing light activity and talking 50% of the time. Everyone (exposed and infected occupants) is treated the same in this model.</li> <li><strong>Lab</strong> = Based on a typical lab or technical working area, all persons are doing light activity and talking 50% of the time. Everyone (exposed and infected occupants) is treated the same in this model.</li>
<li><strong>Workshop</strong> = Based on a mechanical assembly workshop or equipment installation scenario, all persons are doing moderate activity and talking 50% of the time. This activity is equally applicable to bicycling, or walking on a gradient, in the LHC tunnels. Everyone (exposed and infected occupants) is treated the same in this model.</li> <li><strong>Workshop</strong> = Based on a mechanical assembly workshop or equipment installation scenario, all persons are doing moderate activity and talking 50% of the time. This activity is equally applicable to bicycling, or walking on a gradient, in the LHC tunnels. Everyone (exposed and infected occupants) is treated the same in this model.</li>
<li><strong>Training</strong> = Based on a typical training course scenario. <li><strong>Conference/Training</strong> = Based on a typical conference/training course scenario.
One individual (the trainer) is standing and talking, with all other individuals seated and talking quietly (whispering). One individual (the speaker/trainer) is standing and talking, with all other individuals seated and talking quietly (whispering).
In this case it is assumed that the infected person is the trainer, because this is the worst case in terms of viral shedding.</li> In this case it is assumed that the infected person is the speaker/trainer, because this is the worst case in terms of viral shedding.</li>
<li><strong>Gym</strong> = All persons are doing heavy exercise and breathing (not talking). Everyone (exposed and infected occupants) is treated the same in this model.</li> <li><strong>Gym</strong> = All persons are doing heavy exercise and breathing (not talking). Everyone (exposed and infected occupants) is treated the same in this model.</li>
</ul> </ul>
<br><h3>Timings</h3> <br><h3>Timings</h3>
@ -210,7 +214,7 @@ If not, then you can input separate breaks. This is particularly different when
<p>The model allows for a simulation with either a continuous wearing of face masks throughout the duration of the event, or have the removed at all times - i.e. all occupants (infected and exposed alike) wear or not masks for the duration of the simulation. <p>The model allows for a simulation with either a continuous wearing of face masks throughout the duration of the event, or have the removed at all times - i.e. all occupants (infected and exposed alike) wear or not masks for the duration of the simulation.
Please bear in mind the user inputs shall be aligned with the current applicable public health &amp; safety instructions. Please bear in mind the user inputs shall be aligned with the current applicable public health &amp; safety instructions.
Please check what are the applicable rules, before deciding which assumptions are used for the simulation.</p> Please check what are the applicable rules, before deciding which assumptions are used for the simulation.</p>
<p>If you have selected the Training activity type, this equates to the trainer and all participants either wearing masks throughout the training (Yes), or removing them when seated/standing at their socially distanced positions within the training room (No). <p>If you have selected the Conference/Training activity type, this equates to the speakr/trainer and all participants either wearing masks throughout the conference/training (Yes), or removing them when seated/standing at their socially distanced positions within the conference/training room (No).
Please confirm what are the applicable rules, before deciding which assumptions are used for the simulation</p> Please confirm what are the applicable rules, before deciding which assumptions are used for the simulation</p>
<p>For the time being only the Type 1 surgical and FFP2 masks can be selected.</p> <p>For the time being only the Type 1 surgical and FFP2 masks can be selected.</p>
<br> <br>

View file

@ -0,0 +1,2 @@
{# The main calculator form, this template is intended to be implemented by themes #}
{% extends "base/calculator.form.html.j2" %}

View file

@ -0,0 +1,7 @@
{% extends "base/calculator.form.html.j2" %}
{% block room_data %}
<div data-tooltip="The area you wish to study (choose one of the 2 options). Use GIS Portal or measure. Also indicate if a central (radiator-type) heating system is in use.">
<span class="tooltip_text">?</span>
</div>
{% endblock room_data %}

View file

@ -0,0 +1,11 @@
{% extends "base/index.html.j2" %}
{% block cara_at_cern %}
<h2 class="paragraph-title">CARA @ CERN</h2><br>
<div>
<p>
CARA has been developed by CERN with the intention of allowing members of personnel with roles related to supervision, health & safety or space management to simulate the concerned workplaces on CERN sites.
A hosted <a href="{{ calculator_prefix }}">CERN version of the CARA Covid Calculator</a> is available on this site to members of the CERN personnel.
</p>
</div>
{% endblock cara_at_cern %}

View file

@ -0,0 +1,5 @@
{% extends "base/layout.html.j2" %}
{% block covid_information %}
<li class="nav-link"><a href="https://hse.cern/covid-19-information" class="header-navbar nav-link">COVID INFORMATION</a></li>
{% endblock covid_information %}

View file

@ -0,0 +1,5 @@
{% extends "base/userguide.html.j2" %}
{% block room_volume_guide %}
This information is available via GIS Portal (<a href="https://gis.cern.ch/gisportal/">https://gis.cern.ch/gisportal/</a>).</p>
{% endblock room_volume_guide %}

View file

@ -1,69 +1,2 @@
{% extends "layout.html.j2" %} {# The main index, this template is intended to be implemented by themes #}
{% set active_page="home/" %} {% extends "base/index.html.j2" %}
{% block main %}
{# <div style="height: 5em; display: block;"></div> #}
<header class= "bg-light">
<div class="container container--padding">
<img src="/static/images/cara_full_text.png" class="logo d-block m-auto" id="desktop_logo">
<img src="/static/images/cara_full_logo.png" class="logo d-none m-auto" id="mobile_logo">
</div>
</header>
<div class="container container--padding">
<div class="d-flex flex-row mb-3 justify-content-around" id="mobile-app-buttons">
<div><a href="{{ calculator_prefix }}" role="button" class="btn btn-outline-primary"><div class="d-flex d-row"><i class="icon-calculator"></i><span class="pl-1">Calculator</div></a></div>
<div><a href="https://cara.web.cern.ch/expert-app" role="button" class="btn btn-outline-secondary"><div class="d-flex d-row"><i class="icon-expert"></i><span class="pl-1">Expert (beta)</div></a></div>
</div>
<div class="split">
<div class="col-sm-8 pl-0">
<h2 class="paragraph-title">Introduction</h2><br>
<div>
<p>
CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions.
It does this by simulating the long-range airborne spread SARS-CoV-2 virus in a finite volume, assuming homogenous mixing, and it estimates the risk of COVID-19 airborne transmission therein.
Please see the <a href="/about">About</a> page for more details on the methodology, assumptions and limitations of CARA.
</p>
<p>
The full CARA source code can be accessed freely under an Apache 2.0 open source license from our <a href="https://gitlab.cern.ch/cara/cara">code repository</a>.
It includes detailed instructions on how to run your own version of this tool.
</p>
<br>
</div>
</div>
<div class="align-self-center">
<img src="static/images/CARA_1_Vs3_Colour.jpg" class="cara_home_image">
</div>
</div>
<br>
<div id="apps_section" class="d-none">
<div class="d-flex flex-row" >
<h2 class="paragraph-title pr-4 align-self-center">Apps:</h2>
<br>
<div class="pr-3"><a href="{{ calculator_prefix }}" role="button" class="btn btn-lg btn-outline-primary"><div class="d-flex d-row"><i class="icon-calculator"></i><span class="pl-2">Calculator</div></a></div>
<br>
<div><a href="https://cara.web.cern.ch/expert-app" role="button" class="btn btn-lg btn-outline-secondary"><div class="d-flex d-row"><i class="icon-expert"></i><span class="pl-2">Expert (beta)</div></a></div>
</div>
<hr width="95%">
</div>
<h2 class="paragraph-title">CARA @ CERN</h2><br>
<div>
<p>
CARA has been developed by CERN with the intention of allowing members of personnel with roles related to supervision, health & safety or space management to simulate the concerned workplaces on CERN sites.
A hosted <a href="{{ calculator_prefix }}">CERN version of the CARA Covid Calculator</a> is available on this site to members of the CERN personnel.
</p>
</div>
<br>
<h3 class="paragraph-title">Acknowledgements</h3><br>
<p>
We wish to thank CERNs HSE Unit, Beams Department, Experimental Physics Department, Information Technology Department, Industry, Procurement and Knowledge Transfer Department and International Relations Sector for their support to the study.
Thanks to Doris Forkel-Wirth, Benoit Delille, Walid Fadel, Olga Beltramello, Letizia Di Giulio, Evelyne Dho, Wayne Salter, Benoit Salvant and colleagues from the COVID working group for providing expert advice and extensively testing the model.
Finally, we wish to thank Fabienne Landua and the design service for preparing the illustrations and Alessandro Raimondo, Ana Padua and Manuela Cirilli from the Knowledge Transfer Group for their continuous support.
Our compliments towards the work and research performed by world leading scientists in this domain: Dr. Julian Tang, Prof. Manuel Gameiro, Dr. Linsey Marr, Prof. Jose Jimenez, Prof. Lidia Morawska, Prof. Yuguo Li et al. - their scientific contribution was indispensable for this project.
<span style="height: 3vh; display: block;"></span>
</p>
</div>
</div>
{% endblock main %}

View file

@ -1,113 +1,2 @@
<!DOCTYPE html> {# The main layout, this template is intended to be implemented by themes #}
<html lang="en"> {% extends "base/layout.html.j2" %}
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>
{% block title %}
CARA | COVID Airborne Risk Assessment
{% endblock title %}
</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2" crossorigin="anonymous">
<link rel="stylesheet" href="/static/css/style.css">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,500,600,700&display=swap" rel="stylesheet">
{% block extra_headers %}
{% endblock extra_headers %}
</head>
<body>
<nav class="navbar navbar-dark navbar-expand-lg">
<div class="container">
<a href="/" class="navbar-brand"><img src="/static/images/cara_logo_white_text.png" alt="Logo" title="Logo"></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarResponsive">
<ul class="navbar-nav ml-auto">
<li class="nav-link"><a href="/" class="{{ "header-navbar nav-link active" if "home/" == active_page else "header-navbar nav-link" }}">HOME</a></li>
<li class="nav-item dropdown p-2">
<a class="nav-link dropdown-toggle {{ "header-navbar nav-link active" if "calculator/" in active_page else "header-navbar nav-link" }}" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
APPS
</a>
<ul class="dropdown-menu dropwown-navbar-colors" style="min-width: 14rem;" aria-labelledby="navbarDropdown">
<li><a href="{{ calculator_prefix }}" class="{{ "header-navbar nav-link active" if "calculator/" == active_page else "header-navbar nav-link" }}">CARA CALCULATOR</a></li>
<li><a href="{{ calculator_prefix }}/user-guide" style="margin-left: 4rem" class="{{ "header-navbar nav-link active" if "user-guide" in active_page else "header-navbar nav-link" }}">USER GUIDE</a></li>
<li><a href="/expert-app" class="{{ "header-navbar nav-link active" if "/expert-app" == active_page else "header-navbar nav-link" }}">EXPERT APP (BETA)</a></li>
</ul>
</li>
<li class="nav-link"><a href="https://hse.cern/covid-19-information" class="header-navbar nav-link">COVID INFORMATION</a></li>
<li class="nav-link"><a href="/about" class="{{ "header-navbar nav-link active" if "about" == active_page else "header-navbar nav-link" }}">ABOUT</a></li>
{% if user.is_authenticated() %}
<li class="nav-item dropdown p-2">
<a class="nav-link active dropdown-toggle d-inline-block" href="https://cern.ch/users-portal" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Signed in as: {{ user.username }}
</a>
<ul class="dropdown-menu dropwown-navbar-colors" style="min-width: 14rem;" aria-labelledby="navbarDropdown">
<li><a href="/auth/logout" class="nav-link ml-2">Sign out</a></li>
</ul>
{# Sent to Piwik for statistics #}
<script>
var AuthUserDomain = "{{ user.domain() }}";
</script>
{% endif %}
</li>
</ul>
</div>
</div>
</nav>
<main role="main">
{% block main %}
{% endblock main %}
</main>
<footer>
<div class="container">
<div class="row text-light text-center py-4 justify-content-center">
<div class="col-sm-10 col-md-8 col-lg-6">
<img src="/static/images/cara_logo_white_text.png" alt="Logo">
<p><span style="font-size:10px;"><em>CERN strives to deploy its know-how and technologies to help solve
the challenges arising in the local and global fight against COVID-19. As a particle physics
research organisation, CERN is not in a position to advise on medical research, health or health
policy issues. Any initiative is conducted on a best effort and as-is basis, without liability or
warranty.</em></span></p>
<p style="font-size:10px;">
CARA is <a href="https://gitlab.cern.ch/cara/cara/-/blob/master/LICENSE" class="ext">Apache 2.0 licensed</a> open-source
software developed at CERN.
You can find the source code at <a href="https://gitlab.cern.ch/cara/cara">https://gitlab.cern.ch/cara/cara</a>,
where we welcome contributions, feature requests and issue reports.
</p>
</div>
</div>
</div>
</footer>
<script src="/static/js/js_packaged_for_theme.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
<script src="/static/js/jquery.colorbox-min.js"></script>
<script src="/static/js/ScrollMagic.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/js/bootstrap.min.js" integrity="sha512-8qmis31OQi6hIRgvkht0s6mCOittjMa9GMqtK9hes5iEQBQE/Ca6yGE5FsW36vyipGoWQswBj/QBm2JR086Rkw==" crossorigin="anonymous"></script>
<script src="/static/js/usage-tracking.js"></script>
<!-- Popper JS -->
<script src="/static/js/popper.min.js"></script>
<!-- Font Awesome -->
<script src="/static/js/all.min.js"></script>
{% block body_scripts %}
{% endblock body_scripts %}
</body>
</html>

View file

@ -0,0 +1,2 @@
{# The main calculator report, this template is intended to be implemented by themes #}
{% extends "base/userguide.html.j2" %}

View file

@ -123,11 +123,14 @@ class PeriodicInterval(Interval):
#: occurring, a value of 0 signifies that the event never happens. #: occurring, a value of 0 signifies that the event never happens.
duration: float duration: float
#: Time at which the first person (infected or exposed) arrives at the enclosed space.
start: float = 0.0
def boundaries(self) -> BoundarySequence_t: def boundaries(self) -> BoundarySequence_t:
if self.period == 0 or self.duration == 0: if self.period == 0 or self.duration == 0:
return tuple() return tuple()
result = [] result = []
for i in np.arange(0, 24, self.period / 60): for i in np.arange(self.start, 24, self.period / 60):
# NOTE: It is important that the time type is float, not np.float, in # NOTE: It is important that the time type is float, not np.float, in
# order to allow hashability (for caching). # order to allow hashability (for caching).
result.append((float(i), float(i+self.duration/60))) result.append((float(i), float(i+self.duration/60)))
@ -450,11 +453,8 @@ class SARSCoV2(Virus):
piecewise constant model (for more details see A. Henriques et al, piecewise constant model (for more details see A. Henriques et al,
CERN-OPEN-2021-004, DOI: 10.17181/CERN.1GDQ.5Y75) CERN-OPEN-2021-004, DOI: 10.17181/CERN.1GDQ.5Y75)
""" """
halflife = np.empty_like(humidity) return np.piecewise(humidity, [humidity <= 0.4, humidity > 0.4], [3.8, 1.1])
halflife[humidity <= 0.4] = 3.8
halflife[humidity > 0.4] = 1.1
return halflife
Virus.types = { Virus.types = {
'SARS_CoV_2': SARSCoV2( 'SARS_CoV_2': SARSCoV2(
@ -478,6 +478,10 @@ Virus.types = {
viral_load_in_sputum=1e9, viral_load_in_sputum=1e9,
infectious_dose=30/1.6, infectious_dose=30/1.6,
), ),
'SARS_CoV_2_B11529': SARSCoV2(
viral_load_in_sputum=1e9,
infectious_dose=50/4.9841,
),
} }

View file

@ -40,6 +40,8 @@ symptomatic_vl_frequencies = LogCustomKernel(
# From CERN-OPEN-2021-04 and refererences therein # From CERN-OPEN-2021-04 and refererences therein
# NB the infectious dose is inversely proportional to infectiousness of the strain
# i.e. an infectious dose of 80 is 25% more infectious than a dose of 100, (1/(80/100))
virus_distributions = { virus_distributions = {
'SARS_CoV_2': mc.SARSCoV2( 'SARS_CoV_2': mc.SARSCoV2(
viral_load_in_sputum=symptomatic_vl_frequencies, viral_load_in_sputum=symptomatic_vl_frequencies,
@ -49,6 +51,10 @@ virus_distributions = {
viral_load_in_sputum=symptomatic_vl_frequencies, viral_load_in_sputum=symptomatic_vl_frequencies,
infectious_dose=60, infectious_dose=60,
), ),
'SARS_CoV_2_B1351': mc.SARSCoV2(
viral_load_in_sputum=symptomatic_vl_frequencies,
infectious_dose=80,
),
'SARS_CoV_2_P1': mc.SARSCoV2( 'SARS_CoV_2_P1': mc.SARSCoV2(
viral_load_in_sputum=symptomatic_vl_frequencies, viral_load_in_sputum=symptomatic_vl_frequencies,
infectious_dose=100/2.25, infectious_dose=100/2.25,
@ -56,7 +62,11 @@ virus_distributions = {
'SARS_CoV_2_B16172': mc.SARSCoV2( 'SARS_CoV_2_B16172': mc.SARSCoV2(
viral_load_in_sputum=symptomatic_vl_frequencies, viral_load_in_sputum=symptomatic_vl_frequencies,
infectious_dose=60/1.6, infectious_dose=60/1.6,
), ),
'SARS_CoV_2_B11529': mc.SARSCoV2(
viral_load_in_sputum=symptomatic_vl_frequencies,
infectious_dose=(1/4.9841)*100,
),
} }

View file

@ -12,4 +12,4 @@ pip3 install -e .
echo "############################################" echo "############################################"
echo "CARA is now running at http://localhost:8080" echo "CARA is now running at http://localhost:8080"
echo "############################################" echo "############################################"
python3 -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern python3 -m cara.apps.calculator --theme=cara/apps/templates/cern

View file

@ -6,4 +6,4 @@ pip install -e .
echo "############################################" echo "############################################"
echo "CARA is now running at http://localhost:8080" echo "CARA is now running at http://localhost:8080"
echo "############################################" echo "############################################"
python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern python -m cara.apps.calculator --theme=cara/apps/templates/cern

View file

@ -48,7 +48,7 @@ def test_ventilation_slidingwindow(baseline_form: model_generator.FormData):
assert isinstance(baseline_window, models.SlidingWindow) assert isinstance(baseline_window, models.SlidingWindow)
window = models.SlidingWindow( window = models.SlidingWindow(
active=models.PeriodicInterval(period=120, duration=10), active=models.PeriodicInterval(period=120, duration=10, start=minutes_since_midnight(9 * 60)),
inside_temp=models.PiecewiseConstant((0, 24), (293,)), inside_temp=models.PiecewiseConstant((0, 24), (293,)),
outside_temp=baseline_window.outside_temp, outside_temp=baseline_window.outside_temp,
window_height=1.6, opening_length=0.6, window_height=1.6, opening_length=0.6,
@ -80,7 +80,7 @@ def test_ventilation_hingedwindow(baseline_form: model_generator.FormData):
assert isinstance(baseline_window, models.HingedWindow) assert isinstance(baseline_window, models.HingedWindow)
window = models.HingedWindow( window = models.HingedWindow(
active=models.PeriodicInterval(period=120, duration=10), active=models.PeriodicInterval(period=120, duration=10, start=minutes_since_midnight(9 * 60)),
inside_temp=models.PiecewiseConstant((0, 24), (293,)), inside_temp=models.PiecewiseConstant((0, 24), (293,)),
outside_temp=baseline_window.outside_temp, outside_temp=baseline_window.outside_temp,
window_height=1.6, window_width=1., opening_length=0.6, window_height=1.6, window_width=1., opening_length=0.6,
@ -141,7 +141,7 @@ def test_ventilation_window_hepa(baseline_form: model_generator.FormData):
# Now build the equivalent ventilation instance directly, and compare. # Now build the equivalent ventilation instance directly, and compare.
window = models.SlidingWindow( window = models.SlidingWindow(
active=models.PeriodicInterval(period=120, duration=10), active=models.PeriodicInterval(period=120, duration=10, start=minutes_since_midnight(9 * 60)),
inside_temp=models.PiecewiseConstant((0, 24), (293,)), inside_temp=models.PiecewiseConstant((0, 24), (293,)),
outside_temp=baseline_window.outside_temp, outside_temp=baseline_window.outside_temp,
window_height=1.6, opening_length=0.6, window_height=1.6, opening_length=0.6,

View file

@ -72,14 +72,13 @@ class TestBasicApp(tornado.testing.AsyncHTTPTestCase):
class TestCernApp(tornado.testing.AsyncHTTPTestCase): class TestCernApp(tornado.testing.AsyncHTTPTestCase):
def get_app(self): def get_app(self):
cern_theme = Path(cara.apps.calculator.__file__).parent / 'themes' / 'cern' cern_theme = Path(cara.apps.calculator.__file__).parent.parent / 'themes' / 'cern'
return cara.apps.calculator.make_app(theme_dir=cern_theme) return cara.apps.calculator.make_app(theme_dir=cern_theme)
@tornado.testing.gen_test(timeout=_TIMEOUT) @tornado.testing.gen_test(timeout=_TIMEOUT)
def test_report(self): def test_report(self):
response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result')) response = yield self.http_client.fetch(self.get_url('/calculator/baseline-model/result'))
self.assertEqual(response.code, 200) self.assertEqual(response.code, 200)
assert 'CERN HSE' in response.body.decode()
assert 'expected number of new cases is' in response.body.decode() assert 'expected number of new cases is' in response.body.decode()

View file

@ -0,0 +1,18 @@
# locust
A simple open source load testing tool that allows to define user behavior.
In order to set it up for the first time, we followed the documentation at https://locust.io/. In particular, we:
* Defined a class for the users that will be simulating.
* Defined a ``wait_time`` variable that will make the simulated users wait between the specified seconds after each task executed.
* Decorated our method with ``@Task`` that creates a micro-thread that calls this method.
* Defined the ``self.client`` attribute that makes it possible to make HTTP calls that will be logged by Locust.
To use, uncomment the desired method on ``lucust.py``` file, open the terminal on this folder and run the following command:
``locust -f locust.py --host https://cara.web.cern.ch``
Then, open up a browser and point it to http://localhost:8089.
By default we pointed out the test to our own web server.
``Start swarming`` will trigger the simulation.

View file

@ -0,0 +1,42 @@
from locust import HttpUser, task, between
from gevent.pool import Group
import time
'''
Method no. 1 - Simulation with each single user
running x requests in parallel.
Specify the desired number of parallel requests in
"num_of_parallel_requests" variable (35 by default).
This method was used in simulations with one single
user perfoming 35 requests in parallel.
'''
# num_of_parallel_requests = 35
# class User(HttpUser):
# wait_time = between(0.05, 0.1)
# @task(1)
# def test_api(self):
# group = Group()
# for i in range(0, num_of_parallel_requests):
# group.spawn(lambda:self.client.get("/calculator-open/baseline-model/result"))
# group.join()
# while(1):
# time.sleep(1)
'''
Method no. 2 - Simulation with different users
running x requests concurrently.
With this method, each user is intended to
perform one single request.
This method was used in simulations with different
number of users requesting once at the same time.
'''
# class User(HttpUser):
# @task(1)
# def test_api(self):
# self.client.get("/calculator-open/baseline-model/result")
# while(1):
# time.sleep(1)