diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5df28185..be50695c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -36,6 +36,7 @@ deploy_to_test: 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 - curl -X POST -k https://openshift-dev.cern.ch:443/apis/build.openshift.io/v1/namespaces/test-cara/buildconfigs/cara-webservice/webhooks/${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}/generic + - curl -X POST -k https://openshift-dev.cern.ch:443/apis/build.openshift.io/v1/namespaces/test-cara/buildconfigs/cara-calculator-open/webhooks/${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}/generic - curl -X POST -k https://openshift-dev.cern.ch:443/apis/build.openshift.io/v1/namespaces/test-cara/buildconfigs/auth-service/webhooks/${OPENSHIFT_TEST_BUILD_WEBHOOK_SECRET}/generic diff --git a/README.md b/README.md index f0653b89..c0c18d46 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CARA - COVID Airborne Risk Assessment -CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions. +CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions. CARA models the concentration profile of potential infectious viruses in enclosed spaces with clear and intuitive graphs. The user can set a number of parameters, including room volume, exposure time, activity type, mask-wearing and ventilation. @@ -23,7 +23,7 @@ The objective is to facilitate targeted decision-making and investment through c While the SARS-CoV-2 virus is in circulation among the population, the notion of 'zero risk' or 'completely safe scenario' does not exist. Each event modelled is unique, and the results generated therein are only as accurate as the inputs and assumptions. -## Authors +## Authors CARA was developed by following members of CERN - European Council for Nuclear Research (visit https://home.cern/): Andre Henriques1, Marco Andreini1, Gabriella Azzopardi2, James Devine3, Philip Elson4, Nicolas Mounet2, Markus Kongstein Rognlien2,6, Nicola Tarocco5 @@ -70,7 +70,7 @@ Once you have used the scripts, the hourly temperature data for your location sh 'Feb': [0.9, 0.3, 0.0, -0.5, -0.7, -1.1, -1.2, -1.1, -0.7, 0.8, 2.5, 4.2, 5.4, 6.2, 6.3, 6.2, 6.1, 5.5, 4.5, 4.1, 3.5, 2.8, 2.5, 2.0],...` -CARA currently supports **only one geographic location for weather data per instance**. +CARA currently supports **only one geographic location for weather data per instance**. ## Running CARA locally @@ -80,7 +80,7 @@ In order to run cara locally with docker, run the following: $ docker run -it -p 8080:8080 gitlab-registry.cern.ch/cara/cara/calculator -This will start a local version of CARA, which can be visited at http://localhost:8080/. +This will start a local version of CARA, which can be visited at http://localhost:8080/. ## Development guide @@ -98,6 +98,12 @@ To run with the CERN theme: python -m cara.apps.calculator --theme=cara/apps/calculator/themes/cern ``` +To run the calculator on a different URL path: + +``` +python -m cara.apps.calculator --prefix=/mycalc +``` + ### Running the CARA Expert-App app in development mode ``` diff --git a/app-config/nginx/nginx.conf b/app-config/nginx/nginx.conf index 8104caa3..e2e45836 100644 --- a/app-config/nginx/nginx.conf +++ b/app-config/nginx/nginx.conf @@ -99,13 +99,22 @@ http { } location /calculator { - # Anything under calculator is authenticated. + return 302 /calculator-cern; + } + + location /calculator-cern { + # CERN calculator is authenticated. auth_request /auth/probe; error_page 401 = @error401; # cara-webservice is the name of the tornado server (for the calculator) # in each of docker-compose, test-cara.web.cern.ch and cara.web.cern.ch. - proxy_pass http://cara-webservice:8080/calculator; + proxy_pass http://cara-webservice:8080/calculator-cern; + } + + location /calculator-open { + # Public open calculator + proxy_pass http://cara-calculator-open:8080/calculator-open; } } } diff --git a/app-config/openshift/application.yaml b/app-config/openshift/application.yaml index 03c3c103..6d6f2c32 100644 --- a/app-config/openshift/application.yaml +++ b/app-config/openshift/application.yaml @@ -198,7 +198,7 @@ env: - name: APP_NAME value: cara-voila - image: '${PROJECT_NAME}/cara-webservice' + image: '${PROJECT_NAME}/cara-app' ports: - containerPort: 8080 protocol: TCP @@ -211,7 +211,7 @@ - cara-app from: kind: ImageStreamTag - name: 'cara-webservice:latest' + name: 'cara-app:latest' namespace: ${PROJECT_NAME} - apiVersion: v1 @@ -267,8 +267,10 @@ name: auth-service-secrets - name: APP_NAME value: cara-webservice + - name: CARA_CALCULATOR_PREFIX + value: /calculator-cern - name: CARA_THEME - value: cara/apps/calculator/themes/cern + value: cara/apps/calculator/themes/cern image: '${PROJECT_NAME}/cara-webservice' ports: - containerPort: 8080 @@ -285,6 +287,41 @@ name: 'cara-webservice:latest' namespace: ${PROJECT_NAME} - type: ConfigChange + - + apiVersion: v1 + kind: DeploymentConfig + metadata: + name: cara-calculator-open + spec: + replicas;: 1 + template: + metadata: + labels: + app: cara-calculator-open + spec: + containers: + - name: cara-calculator-open + env: + - name: APP_NAME + value: cara-webservice + - name: CARA_CALCULATOR_PREFIX + value: /calculator-open + image: '${PROJECT_NAME}/cara-webservice' + ports: + - containerPort: 8080 + protocol: TCP + triggers: + - type: ConfigChange + - type: ImageChange + imageChangeParams: + automatic: true + containerNames: + - cara-calculator-open + from: + kind: ImageStreamTag + name: 'cara-webservice:latest' + namespace: ${PROJECT_NAME} + - type: ConfigChange parameters: - name: PROJECT_NAME diff --git a/app-config/openshift/services.yaml b/app-config/openshift/services.yaml index 6e39cb6c..a8c0c4fe 100644 --- a/app-config/openshift/services.yaml +++ b/app-config/openshift/services.yaml @@ -10,6 +10,21 @@ labels: template: "cara-services" objects: + - + apiVersion: v1 + kind: Service + metadata: + labels: + app: auth-service + name: auth-service + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: auth-service - apiVersion: v1 kind: Service @@ -24,7 +39,7 @@ protocol: TCP targetPort: 8080 selector: - app: cara-app + deploymentconfig: cara-app - apiVersion: v1 kind: Service @@ -43,7 +58,7 @@ protocol: TCP targetPort: 8443 selector: - app: cara-router + deploymentconfig: cara-router - apiVersion: v1 kind: Service @@ -58,4 +73,19 @@ protocol: TCP targetPort: 8080 selector: - app: cara-webservice + deploymentconfig: cara-webservice + - + apiVersion: v1 + kind: Service + metadata: + labels: + app: cara-calculator-open + name: cara-calculator-open + spec: + ports: + - name: 8080-tcp + port: 8080 + protocol: TCP + targetPort: 8080 + selector: + deploymentconfig: cara-calculator-open diff --git a/app.sh b/app.sh index 1aa80c4a..023c0def 100755 --- a/app.sh +++ b/app.sh @@ -10,6 +10,10 @@ if [[ "$APP_NAME" == "cara-webservice" ]]; then args+=("--theme=${CARA_THEME}") fi + if [ ! -z "$CARA_CALCULATOR_PREFIX" ]; then + args+=("--prefix=${CARA_CALCULATOR_PREFIX}") + fi + echo "Starting the cara webservice with: python -m cara.apps.calculator ${args[@]}" python -m cara.apps.calculator "${args[@]}" elif [[ "$APP_NAME" == "cara-voila" ]]; then diff --git a/cara/apps/calculator/__init__.py b/cara/apps/calculator/__init__.py index 57376206..d84eeaeb 100644 --- a/cara/apps/calculator/__init__.py +++ b/cara/apps/calculator/__init__.py @@ -66,6 +66,7 @@ class BaseRequestHandler(RequestHandler): print(traceback.format_exc()) self.finish(template.render( user=self.current_user, + calculator_prefix=self.settings["calculator_prefix"], active_page='Error', contents=contents )) @@ -79,6 +80,7 @@ class Missing404Handler(BaseRequestHandler): "page.html.j2") self.finish(template.render( user=self.current_user, + calculator_prefix=self.settings["calculator_prefix"], active_page='Error', contents='Unfortunately the page you were looking for does not exist.



' )) @@ -123,7 +125,10 @@ class LandingPage(BaseRequestHandler): def get(self): template = self.settings["template_environment"].get_template( "index.html.j2") - report = template.render(user=self.current_user) + report = template.render( + user=self.current_user, + calculator_prefix=self.settings["calculator_prefix"], + ) self.finish(report) @@ -133,6 +138,7 @@ class AboutPage(BaseRequestHandler): template = template_environment.get_template("about.html.j2") report = template.render( user=self.current_user, + calculator_prefix=self.settings["calculator_prefix"], active_page="about", text_blocks=template_environment.globals['common_text'] ) @@ -146,6 +152,7 @@ class CalculatorForm(BaseRequestHandler): report = template.render( user=self.current_user, xsrf_form_html=self.xsrf_form_html(), + calculator_prefix=self.settings["calculator_prefix"], calculator_version=__version__, ) self.finish(report) @@ -160,7 +167,7 @@ class CompressedCalculatorFormInputs(BaseRequestHandler): except Exception as err: # noqa self.set_status(400) return self.finish("Invalid calculator data: it seems incomplete. Was there an error copying & pasting the URL?") - self.redirect(f'/calculator?{args}') + self.redirect(f'{self.settings["calculator_prefix"]}?{args}') class ReadmeHandler(BaseRequestHandler): @@ -168,14 +175,15 @@ class ReadmeHandler(BaseRequestHandler): template = self.settings['template_environment'].get_template("userguide.html.j2") readme = template.render( active_page="calculator/user-guide", - user=self.current_user + user=self.current_user, + calculator_prefix=self.settings["calculator_prefix"], ) self.finish(readme) def make_app( debug: bool = False, - prefix: str = '/calculator', + calculator_prefix: str = '/calculator', theme_dir: typing.Optional[Path] = None, ) -> Application: static_dir = Path(__file__).absolute().parent.parent / 'static' @@ -185,11 +193,11 @@ def make_app( (r'/_c/(.*)', CompressedCalculatorFormInputs), (r'/about', AboutPage), (r'/static/(.*)', StaticFileHandler, {'path': static_dir}), - (prefix + r'/?', CalculatorForm), - (prefix + r'/report', ConcentrationModel), - (prefix + r'/baseline-model/result', StaticModel), - (prefix + r'/user-guide', ReadmeHandler), - (prefix + r'/static/(.*)', StaticFileHandler, {'path': calculator_static_dir}), + (calculator_prefix + r'/?', CalculatorForm), + (calculator_prefix + r'/report', ConcentrationModel), + (calculator_prefix + r'/baseline-model/result', StaticModel), + (calculator_prefix + r'/user-guide', ReadmeHandler), + (calculator_prefix + r'/static/(.*)', StaticFileHandler, {'path': calculator_static_dir}), ] cara_templates = Path(__file__).parent.parent / "templates" @@ -210,9 +218,10 @@ def make_app( return Application( urls, debug=debug, + calculator_prefix=calculator_prefix, template_environment=template_environment, default_handler_class=Missing404Handler, - report_generator=ReportGenerator(loader), + report_generator=ReportGenerator(loader, calculator_prefix), xsrf_cookies=True, # COOKIE_SECRET being undefined will result in no login information being # presented to the user. diff --git a/cara/apps/calculator/__main__.py b/cara/apps/calculator/__main__.py index 0cfafe66..10298c84 100644 --- a/cara/apps/calculator/__main__.py +++ b/cara/apps/calculator/__main__.py @@ -16,6 +16,11 @@ def configure_parser(parser) -> argparse.ArgumentParser: help="A directory containing extensions for templates and static data", default=None, ) + parser.add_argument( + "--prefix", + help="Change the URL path prefix to the calculator app", + default="/calculator" + ) return parser @@ -27,7 +32,7 @@ def main(): theme_dir = Path(theme_dir).absolute() assert theme_dir.exists() assert (theme_dir / 'templates').exists() - app = make_app(debug=args.no_debug, theme_dir=theme_dir) + app = make_app(debug=args.no_debug, calculator_prefix=args.prefix, theme_dir=theme_dir) app.listen(8080) IOLoop.instance().start() diff --git a/cara/apps/calculator/report_generator.py b/cara/apps/calculator/report_generator.py index 4029e54f..ab206ec9 100644 --- a/cara/apps/calculator/report_generator.py +++ b/cara/apps/calculator/report_generator.py @@ -71,7 +71,7 @@ def calculate_report_data(model: models.ExposureModel): } -def generate_qr_code(prefix, form: FormData): +def generate_qr_code(base_url, calculator_prefix, form: FormData): form_dict = FormData.to_dict(form, strip_defaults=True) # Generate the calculator URL arguments that would be needed to re-create this @@ -80,10 +80,9 @@ def generate_qr_code(prefix, form: FormData): # Then zlib compress + base64 encode the string. To be inverted by the # /_c/ endpoint. - qr_url = prefix + "/_c/" + base64.b64encode( - zlib.compress(args.encode()) - ).decode() - url = prefix + "/calculator?" + args + compressed_args = base64.b64encode(zlib.compress(args.encode())).decode() + qr_url = f"{base_url}/_c/{compressed_args}" + url = f"{base_url}{calculator_prefix}?{args}" qr = qrcode.QRCode( version=1, @@ -281,6 +280,7 @@ def comparison_report(scenarios: typing.Dict[str, models.ExposureModel]): @dataclasses.dataclass class ReportGenerator: jinja_loader: jinja2.BaseLoader + calculator_prefix: str def build_report(self, base_url: str, form: FormData) -> str: model = form.build_model() @@ -300,7 +300,8 @@ class ReportGenerator: context.update(calculate_report_data(model)) alternative_scenarios = manufacture_alternative_scenarios(form) context['alternative_scenarios'] = comparison_report(alternative_scenarios) - context['qr_code'] = generate_qr_code(base_url, form) + context['qr_code'] = generate_qr_code(base_url, self.calculator_prefix, form) + context['calculator_prefix'] = self.calculator_prefix return context def _template_environment(self) -> jinja2.Environment: diff --git a/cara/apps/calculator/templates/base/calculator.report.html.j2 b/cara/apps/calculator/templates/base/calculator.report.html.j2 index cb57d045..043578e6 100644 --- a/cara/apps/calculator/templates/base/calculator.report.html.j2 +++ b/cara/apps/calculator/templates/base/calculator.report.html.j2 @@ -6,7 +6,7 @@ Report | CARA (COVID Airborne Risk Assessment) - + @@ -270,7 +270,7 @@


{% block disclaimer %} -

Disclaimer:

+

Disclaimer:

CARA is a risk assessment tool developed to model the concentration of viruses in enclosed spaces, in order to inform space-management decisions. diff --git a/cara/apps/calculator/templates/calculator.form.html.j2 b/cara/apps/calculator/templates/calculator.form.html.j2 index b03dc47d..bf7840ab 100644 --- a/cara/apps/calculator/templates/calculator.form.html.j2 +++ b/cara/apps/calculator/templates/calculator.form.html.j2 @@ -5,12 +5,12 @@ {% block extra_headers %} - + {% endblock extra_headers %} {% block body_scripts %} - + {% endblock body_scripts %} @@ -28,7 +28,7 @@ v{{ calculator_version }} Please sen {% if DEBUG %}

{% else %} - + {% endif %} {{ xsrf_form_html }} @@ -129,23 +129,23 @@ v{{ calculator_version }} Please sen - + HEPA filtration:    @@ -192,7 +192,7 @@ v{{ calculator_version }} Please sen
- Activity type: + Activity type:
- Exposed person(s) presence:
+ Exposed person(s) presence:
Start:    Finish:
Infected person(s) presence:
@@ -236,7 +236,7 @@ v{{ calculator_version }} Please sen ?

- +
@@ -248,7 +248,7 @@ v{{ calculator_version }} Please sen   
- + Start:    Finish:
@@ -261,7 +261,7 @@ v{{ calculator_version }} Please sen
- Duration (minutes): + Duration (minutes):
- + Start:    Finish:
@@ -295,7 +295,7 @@ v{{ calculator_version }} Please sen
- Duration (minutes): + Duration (minutes):