diff --git a/README.md b/README.md index 9318bd60..6ae24d2b 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,12 @@ To run with a specific template theme created: python -m caimira.apps.calculator --theme=caimira/apps/templates/{theme} ``` +To run the entire app in a different `APPLICATION_ROOT` path: + +``` +python -m caimira.apps.calculator --app_root=/myroot +``` + To run the calculator on a different URL path: ``` diff --git a/app-config/caimira-webservice/app.sh b/app-config/caimira-webservice/app.sh index 0d0452c8..40380a7e 100755 --- a/app-config/caimira-webservice/app.sh +++ b/app-config/caimira-webservice/app.sh @@ -6,13 +6,16 @@ if [[ "$APP_NAME" == "caimira-webservice" ]]; then args+=("--no-debug") fi - if [ ! -z "$CAIMIRA_THEME" ]; then - args+=("--theme=${CAIMIRA_THEME}") + if [ ! -z "$APPLICATION_ROOT" ]; then + args+=("--app_root=${APPLICATION_ROOT}") fi if [ ! -z "$CAIMIRA_CALCULATOR_PREFIX" ]; then args+=("--prefix=${CAIMIRA_CALCULATOR_PREFIX}") fi + if [ ! -z "$CAIMIRA_THEME" ]; then + args+=("--theme=${CAIMIRA_THEME}") + fi export "ARVE_API_KEY"="$ARVE_API_KEY" export "ARVE_CLIENT_ID"="$ARVE_CLIENT_ID" diff --git a/app-config/docker-compose.yml b/app-config/docker-compose.yml index cb9d3958..b2046c21 100644 --- a/app-config/docker-compose.yml +++ b/app-config/docker-compose.yml @@ -11,6 +11,7 @@ services: environment: - COOKIE_SECRET - APP_NAME=caimira-webservice + - APPLICATION_ROOT=/ - CAIMIRA_CALCULATOR_PREFIX=/calculator-cern - CAIMIRA_THEME=caimira/apps/templates/cern user: ${CURRENT_UID} @@ -20,6 +21,7 @@ services: environment: - COOKIE_SECRET - APP_NAME=caimira-webservice + - APPLICATION_ROOT=/ - CAIMIRA_CALCULATOR_PREFIX=/calculator-open user: ${CURRENT_UID} diff --git a/app-config/openshift/deploymentconfig.yaml b/app-config/openshift/deploymentconfig.yaml index 5e23cede..456a2d4b 100644 --- a/app-config/openshift/deploymentconfig.yaml +++ b/app-config/openshift/deploymentconfig.yaml @@ -205,6 +205,8 @@ value: '3' - name: APP_NAME value: caimira-webservice + - name: APPLICATION_ROOT + value: / - name: CAIMIRA_CALCULATOR_PREFIX value: /calculator-cern - name: CAIMIRA_THEME @@ -297,6 +299,8 @@ env: - name: APP_NAME value: caimira-webservice + - name: APPLICATION_ROOT + value: / - name: CAIMIRA_CALCULATOR_PREFIX value: /calculator-open image: '${PROJECT_NAME}/caimira-webservice' diff --git a/caimira/apps/calculator/__init__.py b/caimira/apps/calculator/__init__.py index 2f42bd0d..c36e56ec 100644 --- a/caimira/apps/calculator/__init__.py +++ b/caimira/apps/calculator/__init__.py @@ -73,7 +73,8 @@ class BaseRequestHandler(RequestHandler): print(traceback.format_exc()) self.finish(template.render( user=self.current_user, - calculator_prefix=self.settings["calculator_prefix"], + get_url = template.globals['get_url'], + get_calculator_url = template.globals["get_calculator_url"], active_page='Error', contents=contents )) @@ -87,7 +88,8 @@ class Missing404Handler(BaseRequestHandler): "page.html.j2") self.finish(template.render( user=self.current_user, - calculator_prefix=self.settings["calculator_prefix"], + get_url = template.globals['get_url'], + get_calculator_url = template.globals["get_calculator_url"], active_page='Error', contents='Unfortunately the page you were looking for does not exist.



' )) @@ -193,8 +195,9 @@ class LandingPage(BaseRequestHandler): "index.html.j2") report = template.render( user=self.current_user, - calculator_prefix=self.settings["calculator_prefix"], - text_blocks=template_environment.globals['common_text'], + get_url = template_environment.globals['get_url'], + get_calculator_url = template_environment.globals['get_calculator_url'], + text_blocks=template_environment.globals["common_text"], ) self.finish(report) @@ -205,9 +208,10 @@ class AboutPage(BaseRequestHandler): template = template_environment.get_template("about.html.j2") report = template.render( user=self.current_user, - calculator_prefix=self.settings["calculator_prefix"], + get_url = template.globals['get_url'], + get_calculator_url = template.globals["get_calculator_url"], active_page="about", - text_blocks=template_environment.globals['common_text'] + text_blocks=template_environment.globals["common_text"] ) self.finish(report) @@ -220,9 +224,10 @@ class CalculatorForm(BaseRequestHandler): report = template.render( user=self.current_user, xsrf_form_html=self.xsrf_form_html(), - calculator_prefix=self.settings["calculator_prefix"], + get_url = template.globals['get_url'], + get_calculator_url = template.globals["get_calculator_url"], calculator_version=__version__, - text_blocks=template_environment.globals['common_text'], + text_blocks=template_environment.globals["common_text"], ) self.finish(report) @@ -236,8 +241,9 @@ 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'{self.settings["calculator_prefix"]}?{args}') - + template_environment = self.settings["template_environment"] + self.redirect(f'{template_environment.globals["get_calculator_url"]()}?{args}') + class ReadmeHandler(BaseRequestHandler): def get(self): @@ -246,8 +252,9 @@ class ReadmeHandler(BaseRequestHandler): readme = template.render( active_page="calculator/user-guide", user=self.current_user, - calculator_prefix=self.settings["calculator_prefix"], - text_blocks=template_environment.globals['common_text'], + get_url = template.globals['get_url'], + get_calculator_url = template.globals["get_calculator_url"], + text_blocks=template_environment.globals["common_text"], ) self.finish(readme) @@ -337,28 +344,39 @@ class CasesData(BaseRequestHandler): # If any of the 'New_cases' is 0, it means the data is not updated. if (cases.loc[eight_days_ago:current_date]['New_cases'] == 0).any(): return self.finish('') return self.finish(str(round(cases.loc[eight_days_ago:current_date]['New_cases'].mean()))) + +def get_url(app_root: str, relative_path: str = '/'): + return app_root.rstrip('/') + relative_path.rstrip('/') + +def get_calculator_url(app_root: str, calculator_prefix: str, relative_path: str = '/'): + return app_root.rstrip('/') + calculator_prefix.rstrip('/') + relative_path.rstrip('/') def make_app( debug: bool = False, + APPLICATION_ROOT: str = '/', calculator_prefix: str = '/calculator', theme_dir: typing.Optional[Path] = None, ) -> Application: static_dir = Path(__file__).absolute().parent.parent / 'static' calculator_static_dir = Path(__file__).absolute().parent / 'static' + + get_root_url = functools.partial(get_url, APPLICATION_ROOT) + get_root_calculator_url = functools.partial(get_calculator_url, APPLICATION_ROOT, calculator_prefix) + urls: typing.Any = [ - (r'/?', LandingPage), - (r'/_c/(.*)', CompressedCalculatorFormInputs), - (r'/about', AboutPage), - (r'/static/(.*)', StaticFileHandler, {'path': static_dir}), - (calculator_prefix + r'/?', CalculatorForm), - (calculator_prefix + r'/report', ConcentrationModel), - (calculator_prefix + r'/report-json', ConcentrationModelJsonResponse), - (calculator_prefix + r'/baseline-model/result', StaticModel), - (calculator_prefix + r'/user-guide', ReadmeHandler), - (calculator_prefix + r'/api/arve/v1/(.*)/(.*)', ArveData), - (calculator_prefix + r'/cases/(.*)', CasesData), - (calculator_prefix + r'/static/(.*)', StaticFileHandler, {'path': calculator_static_dir}), + (get_root_url(r'/?'), LandingPage), + (get_root_url(r'/_c/(.*)'), CompressedCalculatorFormInputs), + (get_root_url(r'/about'), AboutPage), + (get_root_url(r'/static/(.*)'), StaticFileHandler, {'path': static_dir}), + (get_root_calculator_url(r'/?'), CalculatorForm), + (get_root_calculator_url(r'/report'), ConcentrationModel), + (get_root_calculator_url(r'/report-json'), ConcentrationModelJsonResponse), + (get_root_calculator_url(r'/baseline-model/result'), StaticModel), + (get_root_calculator_url(r'/user-guide'), ReadmeHandler), + (get_root_calculator_url(r'/api/arve/v1/(.*)/(.*)'), ArveData), + (get_root_calculator_url(r'/cases/(.*)'), CasesData), + (get_root_calculator_url(r'/static/(.*)'), StaticFileHandler, {'path': calculator_static_dir}), ] caimira_templates = Path(__file__).parent.parent / "templates" @@ -372,9 +390,11 @@ def make_app( undefined=jinja2.StrictUndefined, # fail when rendering any undefined template context variable ) - template_environment.globals['common_text'] = markdown_tools.extract_rendered_markdown_blocks( + template_environment.globals["common_text"] = markdown_tools.extract_rendered_markdown_blocks( template_environment.get_template('common_text.md.j2') ) + template_environment.globals['get_url']=get_root_url + template_environment.globals['get_calculator_url']=get_root_calculator_url if debug: tornado.log.enable_pretty_logging() @@ -382,10 +402,11 @@ def make_app( return Application( urls, debug=debug, - calculator_prefix=calculator_prefix, + # calculator_prefix=calculator_prefix, + # APPLICATION_ROOT=APPLICATION_ROOT, template_environment=template_environment, default_handler_class=Missing404Handler, - report_generator=ReportGenerator(loader, calculator_prefix), + report_generator=ReportGenerator(loader, get_root_url, get_root_calculator_url), xsrf_cookies=True, # COOKIE_SECRET being undefined will result in no login information being # presented to the user. diff --git a/caimira/apps/calculator/__main__.py b/caimira/apps/calculator/__main__.py index 7ec8b69a..d9c46bd5 100644 --- a/caimira/apps/calculator/__main__.py +++ b/caimira/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( + "--app_root", + help="Change the APPLICATION_ROOT of the app", + default="/" + ) parser.add_argument( "--prefix", help="Change the URL path prefix to the calculator app", @@ -36,7 +41,7 @@ def main(): if theme_dir is not None: theme_dir = Path(theme_dir).absolute() assert theme_dir.exists() - app = make_app(debug=args.no_debug, calculator_prefix=args.prefix, theme_dir=theme_dir) + app = make_app(debug=args.no_debug, APPLICATION_ROOT=args.app_root, calculator_prefix=args.prefix, theme_dir=theme_dir) app.listen(args.port) IOLoop.instance().start() diff --git a/caimira/apps/calculator/report_generator.py b/caimira/apps/calculator/report_generator.py index 81a64543..1dcfa00d 100644 --- a/caimira/apps/calculator/report_generator.py +++ b/caimira/apps/calculator/report_generator.py @@ -155,7 +155,7 @@ def calculate_report_data(form: FormData, model: models.ExposureModel) -> typing } -def generate_permalink(base_url, calculator_prefix, form: FormData): +def generate_permalink(base_url, get_root_url, get_root_calculator_url, form: FormData): form_dict = FormData.to_dict(form, strip_defaults=True) # Generate the calculator URL arguments that would be needed to re-create this @@ -165,8 +165,8 @@ def generate_permalink(base_url, calculator_prefix, form: FormData): # Then zlib compress + base64 encode the string. To be inverted by the # /_c/ endpoint. 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_url = f"{base_url}{get_root_url()}/_c/{compressed_args}" + url = f"{base_url}{get_root_calculator_url()}?{args}" return { 'link': url, @@ -342,7 +342,8 @@ def comparison_report( @dataclasses.dataclass class ReportGenerator: jinja_loader: jinja2.BaseLoader - calculator_prefix: str + get_root_url: typing.Any + get_root_calculator_url: typing.Any def build_report( self, @@ -377,8 +378,9 @@ class ReportGenerator: context['alternative_scenarios'] = comparison_report( form, report_data, alternative_scenarios, scenario_sample_times, executor_factory=executor_factory, ) - context['permalink'] = generate_permalink(base_url, self.calculator_prefix, form) - context['calculator_prefix'] = self.calculator_prefix + context['permalink'] = generate_permalink(base_url, self.get_root_url, self.get_root_calculator_url, form) + context['get_url'] = self.get_root_url + context['get_calculator_url'] = self.get_root_calculator_url return context @@ -387,7 +389,7 @@ class ReportGenerator: loader=self.jinja_loader, undefined=jinja2.StrictUndefined, ) - env.globals['common_text'] = markdown_tools.extract_rendered_markdown_blocks( + env.globals["common_text"] = markdown_tools.extract_rendered_markdown_blocks( env.get_template('common_text.md.j2') ) env.filters['non_zero_percentage'] = non_zero_percentage @@ -401,4 +403,4 @@ class ReportGenerator: def render(self, context: dict) -> str: template = self._template_environment().get_template("calculator.report.html.j2") - return template.render(**context, text_blocks=template.globals['common_text']) + return template.render(**context, text_blocks=template.globals["common_text"]) diff --git a/caimira/apps/templates/about.html.j2 b/caimira/apps/templates/about.html.j2 index 2ecce51d..36ea62e8 100644 --- a/caimira/apps/templates/about.html.j2 +++ b/caimira/apps/templates/about.html.j2 @@ -14,7 +14,7 @@ For information on the Airborne Transmission of SARS-CoV-2, feel free to check o CAiMIRA stands for CERN Airborne Model for Indoor Risk Assessment, previously known as CARA - COVID Airborne Risk Assessment, developed in the spring of 2020 to better understand and quantify the risk of long-range airborne spread of SARS-CoV-2 virus in workplaces. Since then, the model has evolved and now is capable of simulating the short-range component. CAiMIRA comes with different applications that allow more or less flexibility in the input parameters: diff --git a/caimira/apps/templates/base/calculator.form.html.j2 b/caimira/apps/templates/base/calculator.form.html.j2 index 06ad852a..448350b7 100644 --- a/caimira/apps/templates/base/calculator.form.html.j2 +++ b/caimira/apps/templates/base/calculator.form.html.j2 @@ -5,13 +5,13 @@ {% block extra_headers %} - + {% endblock extra_headers %} {% block body_scripts %} - + {% endblock body_scripts %} @@ -21,7 +21,7 @@ {% if DEBUG %}
{% else %} - + {% endif %} {{ xsrf_form_html }} @@ -31,7 +31,7 @@

Calculator

- +
@@ -222,7 +222,7 @@ If these conditions are not met, the air exchange might not be homogenous producing an artificially lower risk further away from the window.

- + @@ -327,14 +327,14 @@
@@ -342,7 +342,7 @@ @@ -717,13 +717,13 @@
  • If coffee breaks are included, they are spread out evenly throughout the day, in addition to any lunch break (if applicable).
  • - Refer to Calculator App user guide + Refer to Calculator App user guide for more detailed explanations on how to use this tool.

    CERN version of the CAiMIRA Calculator is available on this site to members of the CERN personnel. + A hosted CERN version of the CAiMIRA Calculator is available on this site to members of the CERN personnel.

    {% endblock caimira_at_cern %} \ No newline at end of file diff --git a/caimira/tests/apps/calculator/test_webapp.py b/caimira/tests/apps/calculator/test_webapp.py index be604f0a..6dc7d692 100644 --- a/caimira/tests/apps/calculator/test_webapp.py +++ b/caimira/tests/apps/calculator/test_webapp.py @@ -103,7 +103,7 @@ class TestOpenApp(tornado.testing.AsyncHTTPTestCase): async def test_permalink_urls(http_server_client, baseline_form): base_url = 'proto://hostname/prefix' - permalink_data = generate_permalink(base_url, "/calculator", baseline_form) + permalink_data = generate_permalink(base_url, lambda: "", lambda: "/calculator", baseline_form) expected = f'{base_url}/calculator?exposed_coffee_break_option={baseline_form.exposed_coffee_break_option}&' assert permalink_data['link'].startswith(expected) diff --git a/setup.py b/setup.py index 2d27b2e8..3d795bd6 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ REQUIREMENTS: dict = { 'test': [ 'pytest', 'pytest-mypy != v0.10.1', + 'mypy != 1.1.1', 'pytest-tornasync', 'numpy-stubs @ git+https://github.com/numpy/numpy-stubs.git', 'types-dataclasses',