diff --git a/src/natcap/invest/__init__.py b/src/natcap/invest/__init__.py index d9fc14fd7e..22ec33e5c1 100644 --- a/src/natcap/invest/__init__.py +++ b/src/natcap/invest/__init__.py @@ -43,6 +43,11 @@ LOCALE_CODE = 'en' +def get_locale(): + """Get the current locale code.""" + return LOCALE_CODE + + def set_locale(locale_code): """Set the `gettext` attribute of natcap.invest. diff --git a/src/natcap/invest/carbon/reporter.py b/src/natcap/invest/carbon/reporter.py index 52e01df3bb..434d169fb7 100644 --- a/src/natcap/invest/carbon/reporter.py +++ b/src/natcap/invest/carbon/reporter.py @@ -8,7 +8,7 @@ import pygeoprocessing from natcap.invest import __version__ -from natcap.invest import gettext +from natcap.invest import gettext, get_locale from natcap.invest.reports import jinja_env, raster_utils, report_constants from natcap.invest.reports.raster_utils import RasterDatatype, RasterPlotConfig from natcap.invest.spec import ModelSpec @@ -131,7 +131,7 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec, raster_path=file_registry['c_storage_bas'], datatype=RasterDatatype.continuous, spec=model_spec.get_output('c_storage_bas'))] - + intermediate_raster_config_lists = [[ RasterPlotConfig( raster_path=file_registry[f'c_{pool_type}_bas'], @@ -178,7 +178,7 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec, input_raster_config_list) input_raster_caption = raster_utils.caption_raster_list( input_raster_config_list) - + outputs_img_src = raster_utils.plot_and_base64_encode_rasters( output_raster_config_list) output_raster_caption = raster_utils.caption_raster_list( @@ -214,6 +214,7 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec, with open(target_html_filepath, 'w', encoding='utf-8') as target_file: target_file.write(TEMPLATE.render( + locale=get_locale(), report_script=model_spec.reporter, invest_version=__version__, report_filepath=target_html_filepath, @@ -230,10 +231,10 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec, outputs_img_src=outputs_img_src, outputs_caption=output_raster_caption, intermediate_raster_sections=intermediate_raster_sections, - raster_group_caption=report_constants.RASTER_GROUP_CAPTION, + raster_group_caption=report_constants.raster_group_caption(), output_raster_stats_table=output_raster_stats_table, input_raster_stats_table=input_raster_stats_table, - stats_table_note=report_constants.STATS_TABLE_NOTE, + stats_table_note=report_constants.stats_table_note(), model_spec_outputs=model_spec.outputs, )) diff --git a/src/natcap/invest/cli.py b/src/natcap/invest/cli.py index 5d9caca4ee..744ecabea0 100644 --- a/src/natcap/invest/cli.py +++ b/src/natcap/invest/cli.py @@ -8,7 +8,6 @@ import json import logging import multiprocessing -import os import pprint import sys import textwrap @@ -16,10 +15,7 @@ import natcap.invest from natcap.invest import datastack -from natcap.invest import set_locale -from natcap.invest import spec from natcap.invest import ui_server -from natcap.invest import utils from natcap.invest import models from pygeoprocessing.utils import GDALUseExceptions @@ -463,8 +459,15 @@ def main(user_args=None): target_model = models.model_id_to_pyname[args.model] model_module = importlib.import_module(name=target_model) - LOGGER.info('Imported target %s from %s', - model_module.__name__, model_module) + + LOGGER.info( + f'Imported target {model_module.__name__} from {model_module}') + + generate_report = bool(model_module.MODEL_SPEC.reporter) + if generate_report: + # Reload model module to ensure text defined in model spec is + # localized before it is passed to the report. + importlib.reload(model_module) # We're deliberately not validating here because the user # can just call ``invest validate `` to validate. @@ -479,7 +482,7 @@ def main(user_args=None): generate_metadata=True, save_file_registry=True, check_outputs=False, - generate_report=bool(model_module.MODEL_SPEC.reporter)) + generate_report=generate_report) if args.subcommand == 'serve': ui_server.app.run(port=args.port) diff --git a/src/natcap/invest/coastal_vulnerability/reporter.py b/src/natcap/invest/coastal_vulnerability/reporter.py index 2c36f8622a..9e07512ebf 100644 --- a/src/natcap/invest/coastal_vulnerability/reporter.py +++ b/src/natcap/invest/coastal_vulnerability/reporter.py @@ -7,7 +7,7 @@ import pandas from natcap.invest import __version__ -from natcap.invest import gettext +from natcap.invest import gettext, get_locale import natcap.invest.spec from natcap.invest.reports import jinja_env @@ -222,9 +222,17 @@ def report(file_registry, args_dict, model_spec, target_html_filepath): na_count = exposure_geo.exposure.isna().sum() if na_count: + # There is some redundancy here to ensure the checkbox label is + # localized properly. Note that passing an f-string to gettext will not + # work; we must explicitly use .format() to populate variable values. + if na_count == 1: + null_cb_name = gettext( + '{na_count} point missing the exposure index. Show:') + else: + null_cb_name = gettext( + '{na_count} points missing the exposure index. Show:') null_checkbox = altair.binding_checkbox( - name=(f"{na_count} " - f"{gettext('point(s) missing the exposure index. Show:')}")) + name=(null_cb_name.format(na_count=na_count))) show_null = altair.param(value=False, bind=null_checkbox) null_points_chart = base_points.add_params( show_null @@ -396,6 +404,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath): with open(target_html_filepath, 'w', encoding='utf-8') as target_file: target_file.write(TEMPLATE.render( + locale=get_locale(), report_script=model_spec.reporter, invest_version=__version__, report_filepath=target_html_filepath, diff --git a/src/natcap/invest/internationalization/README.md b/src/natcap/invest/internationalization/README.md index 7b1c077402..e4bdaa7910 100644 --- a/src/natcap/invest/internationalization/README.md +++ b/src/natcap/invest/internationalization/README.md @@ -6,6 +6,9 @@ None of the translations files (.pot, .po, .mo) should be manually edited by us. ### `messages.pot` Message catalog template file. This contains all the strings ("messages") that are translated, without any translations. All the PO files are derived from this. +### `babel_config.ini` +Mappings file that tells pybabel where to look when extracting messages into the message catalog. By default, pybabel will extract messages from Python source files; we need this mappings file to ensure it also extracts messages from the Jinja templates that are used for HTML reports. + ### `locales/` Locale directory. The contents of this directory are organized in a specific structure that `gettext` expects. `locales/` contains one subdirectory for each language for which there are any translations (not including the default English). The subdirectories are named after the corresponding ISO 639-1 language code. Each language subdirectory contains a directory `LC_MESSAGES`, which then contains the message catalog files for that language. @@ -22,7 +25,7 @@ No changes are immediately needed when we add, remove, or edit strings that are When we are ready to get a new batch of translations, here is the process. These instructions assume you have defined the two-letter locale code in an environment variable `$LL`. -1. Run the following from the root invest directory, replacing `` with the language code: +1. Run the following from the root invest directory, replacing `$LL` with the language code: ``` pybabel extract \ --no-wrap \ @@ -30,6 +33,7 @@ pybabel extract \ --version $(python -m setuptools_scm) \ --msgid-bugs-address natcap-software@lists.stanford.edu \ --copyright-holder "Natural Capital Alliance" \ + --mapping src/natcap/invest/internationalization/babel_config.ini \ --output src/natcap/invest/internationalization/messages.pot \ src/ @@ -39,7 +43,7 @@ pybabel update \ --input-file src/natcap/invest/internationalization/messages.pot \ --output-file src/natcap/invest/internationalization/locales/$LL/LC_MESSAGES/messages.po ``` -This looks through the source code for strings wrapped in the `gettext(...)` function and writes them to the message catalog template. Then it updates the message catalog for the specificed language. New strings that don't yet have a translation will have an empty `msgstr` value. Previously translated messages that are no longer needed will be commented out but remain in the file. This will save translator time if they're needed again in the future. +This looks through the source code for strings wrapped in the `gettext(...)` function and writes them to the message catalog template. Then it updates the message catalog for the specified language. New strings that don't yet have a translation will have an empty `msgstr` value. Previously translated messages that are no longer needed will be commented out but remain in the file. This will save translator time if they're needed again in the future. 2. Check that the changes look correct, then commit: ``` @@ -69,7 +73,9 @@ Then follow the "Process to update translations" instructions above, starting fr * Model titles * `MODEL_SPEC` `name` and `about` text * Validation messages -* Strings that appear in the UI, such as button labels and tooltip text +* Strings that appear in HTML reports, such as section headings, figure captions, and table column headers + +Strings that appear exclusively in the Workbench UI, such as button labels and tooltip text, are also translated, but they are handled separately. See the [Workbench README](../../../../workbench/readme.md#internationalization) for details. We are not translating: diff --git a/src/natcap/invest/internationalization/babel_config.ini b/src/natcap/invest/internationalization/babel_config.ini new file mode 100644 index 0000000000..e05af5c6af --- /dev/null +++ b/src/natcap/invest/internationalization/babel_config.ini @@ -0,0 +1,9 @@ +# Extract messages from Python source files + +[python: **.py] + +# Extract messages from Jinja HTML templates + +[jinja2: **/templates/**.html] +ignore_tags = script,style +include_attrs = alt diff --git a/src/natcap/invest/ndr/reporter.py b/src/natcap/invest/ndr/reporter.py index 4b1441a275..8c76927c80 100644 --- a/src/natcap/invest/ndr/reporter.py +++ b/src/natcap/invest/ndr/reporter.py @@ -1,4 +1,3 @@ -from natcap.invest.reports import raster_utils from natcap.invest.reports import report_constants from natcap.invest.reports import sdr_ndr_report_generator from natcap.invest.reports.raster_utils import RasterDatatype, RasterPlotConfig @@ -110,7 +109,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath): raster_path=file_registry['stream'], datatype=RasterDatatype.binary_high_contrast, spec=model_spec.get_output('stream')) - stream_config.caption += report_constants.STREAM_CAPTION_APPENDIX + stream_config.caption += report_constants.stream_caption_appendix() intermediate_raster_plot_configs = [ masked_dem_config, what_drains_config, stream_config] diff --git a/src/natcap/invest/reports/__init__.py b/src/natcap/invest/reports/__init__.py index bc0eba9c95..e2b66ea79e 100644 --- a/src/natcap/invest/reports/__init__.py +++ b/src/natcap/invest/reports/__init__.py @@ -7,12 +7,18 @@ import matplotlib import pandas +from natcap.invest import gettext + jinja_env = jinja2.Environment( loader=jinja2.PackageLoader('natcap.invest.reports', 'templates'), autoescape=jinja2.select_autoescape(), - undefined=jinja2.StrictUndefined + undefined=jinja2.StrictUndefined, + extensions=['jinja2.ext.i18n'], ) +jinja_env.install_gettext_callables( + gettext=gettext, ngettext=None, newstyle=True) + MATPLOTLIB_PARAMS = { 'backend': 'agg', # 'legend.fontsize': 'small', diff --git a/src/natcap/invest/reports/report_constants.py b/src/natcap/invest/reports/report_constants.py index d8fa3e31dd..3f32d5d847 100644 --- a/src/natcap/invest/reports/report_constants.py +++ b/src/natcap/invest/reports/report_constants.py @@ -1,21 +1,35 @@ from natcap.invest import gettext -STATS_TABLE_NOTE = gettext( - '"Valid percent" indicates the percent of pixels that are not ' - 'nodata. Comparing "valid percent" values across rasters may help ' - 'you identify cases of unexpected nodata.' -) - -RASTER_GROUP_CAPTION = gettext( - 'If a plot title includes "resampled," that raster was resampled to ' - 'a lower resolution for rendering in this report. Full resolution ' - 'rasters are available in the output workspace.' -) - -STREAM_CAPTION_APPENDIX = gettext( - ' The stream network may look incomplete at this resolution, and ' - 'therefore it may be necessary to view the full-resolution raster ' - 'in GIS to assess its accuracy.' -) +# All `gettext`-wrapped strings defined in this module should be returned from +# a function, _not_ defined on the module object itself, since text defined at +# the module level is not localized unless/until the module is reloaded. +# Wrapping text strings in functions avoids the need to reload the module +# and ensures text is localized properly when it is needed. + + +def stats_table_note(): + return gettext( + '"Valid percent" indicates the percent of pixels that are not ' + 'nodata. Comparing "valid percent" values across rasters may help ' + 'you identify cases of unexpected nodata.' + ) + + +def raster_group_caption(): + """Get "pre-caption" note about raster resampling.""" + return gettext( + 'If a plot title includes "resampled," that raster was resampled to ' + 'a lower resolution for rendering in this report. Full resolution ' + 'rasters are available in the output workspace.' + ) + + +def stream_caption_appendix(): + return gettext( + ' The stream network may look incomplete at this resolution, and ' + 'therefore it may be necessary to view the full-resolution raster ' + 'in GIS to assess its accuracy.' + ) + TABLE_PAGINATION_THRESHOLD = 10 diff --git a/src/natcap/invest/reports/sdr_ndr_report_generator.py b/src/natcap/invest/reports/sdr_ndr_report_generator.py index 4057db96c3..b7b2b77390 100644 --- a/src/natcap/invest/reports/sdr_ndr_report_generator.py +++ b/src/natcap/invest/reports/sdr_ndr_report_generator.py @@ -5,7 +5,7 @@ import time from natcap.invest import __version__ -from natcap.invest import gettext +from natcap.invest import gettext, get_locale from natcap.invest.reports import ( jinja_env, report_constants, sdr_ndr_utils, raster_utils) @@ -58,8 +58,11 @@ def report(file_registry, args_dict, model_spec, target_html_filepath, intermediate_raster_plot_configs) intermediates_caption = raster_utils.caption_raster_list( intermediate_raster_plot_configs) + # Note that passing an f-string to gettext will not work; + # we must explicitly use .format() to populate variable values. intermediate_outputs_heading = gettext( - f'Stream Network Maps (flow algorithm: {args_dict["flow_dir_algorithm"]})') + 'Stream Network Maps (flow algorithm: {flow_dir_algorithm})' + ).format(flow_dir_algorithm=args_dict["flow_dir_algorithm"].upper()) (ws_vector_table, ws_vector_totals_table) = ( sdr_ndr_utils.generate_results_table_from_vector( @@ -81,6 +84,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath, with open(target_html_filepath, 'w', encoding='utf-8') as target_file: target_file.write(TEMPLATE.render( + locale=get_locale(), report_script=model_spec.reporter, invest_version=__version__, report_filepath=target_html_filepath, @@ -97,12 +101,12 @@ def report(file_registry, args_dict, model_spec, target_html_filepath, intermediate_outputs_heading=intermediate_outputs_heading, intermediate_outputs_img_src=intermediate_img_src, intermediate_outputs_caption=intermediates_caption, - raster_group_caption=report_constants.RASTER_GROUP_CAPTION, + raster_group_caption=report_constants.raster_group_caption(), ws_vector_table=ws_vector_table, ws_vector_totals_table=ws_vector_totals_table, output_raster_stats_table=output_raster_stats_table, input_raster_stats_table=input_raster_stats_table, - stats_table_note=report_constants.STATS_TABLE_NOTE, + stats_table_note=report_constants.stats_table_note(), model_spec_outputs=model_spec.outputs, )) diff --git a/src/natcap/invest/reports/templates/args-table.html b/src/natcap/invest/reports/templates/args-table.html index 95663dbfd5..a18bafd9a2 100644 --- a/src/natcap/invest/reports/templates/args-table.html +++ b/src/natcap/invest/reports/templates/args-table.html @@ -6,8 +6,8 @@ - - + + diff --git a/src/natcap/invest/reports/templates/base.html b/src/natcap/invest/reports/templates/base.html index bcafea8766..7e33955652 100644 --- a/src/natcap/invest/reports/templates/base.html +++ b/src/natcap/invest/reports/templates/base.html @@ -1,9 +1,9 @@ - + - {% block page_title %}InVEST Results: {{ model_name }}{% endblock page_title %} + {% block page_title %}{% trans %}InVEST Results: {{ model_name }}{% endtrans %}{% endblock page_title %} {% block styles %} {% include 'styles.html' %} {% endblock styles %} @@ -16,23 +16,27 @@ {% block content scoped %}

{{ self.page_title() }}

{{ model_description }}

+ {% trans %}

To learn more about the {{ model_name }} model, visit the InVEST User Guide (opens in new browser window).

+ {% endtrans %} {% endblock content %} {% block footer %}
+ {% trans %}

This report was generated by {{ report_script }}, InVEST version {{ invest_version }}.

It was saved to {{ report_filepath }} at {{ timestamp }}.

+ {% endtrans %}
{% endblock footer %} diff --git a/src/natcap/invest/reports/templates/caption.html b/src/natcap/invest/reports/templates/caption.html index 890e406ead..2201df2243 100644 --- a/src/natcap/invest/reports/templates/caption.html +++ b/src/natcap/invest/reports/templates/caption.html @@ -30,7 +30,7 @@ {% endfor %} {% if source_list %}

- Sources: {{ source_list | join('; ') }} + {% trans %}Sources:{% endtrans %} {{ source_list | join('; ') }}

{% endif %} {% endif %} diff --git a/src/natcap/invest/reports/templates/metadata.html b/src/natcap/invest/reports/templates/metadata.html index 2eebef6c6b..fee0064b9d 100644 --- a/src/natcap/invest/reports/templates/metadata.html +++ b/src/natcap/invest/reports/templates/metadata.html @@ -10,7 +10,7 @@
{{ output.path }}
{{ output.about }}
{% if output.units is defined %} -
Units: {{ output.units }}
+
{% trans %}Units:{% endtrans %} {{ output.units }}
{% endif %} {% endfor %} diff --git a/src/natcap/invest/reports/templates/models/carbon.html b/src/natcap/invest/reports/templates/models/carbon.html index 7bb745ac3d..96707de41a 100644 --- a/src/natcap/invest/reports/templates/models/carbon.html +++ b/src/natcap/invest/reports/templates/models/carbon.html @@ -16,18 +16,18 @@ {% from 'raster-plot-img.html' import raster_plot_img %} {% from 'wide-table.html' import wide_table %} -

Results

+

{% trans %}Results{% endtrans %}

{{ accordion_section( - 'Aggregate Results', + gettext('Aggregate Results'), agg_results_table | safe )}} {{ accordion_section( - 'Primary Outputs', + gettext('Primary Outputs'), content_grid([ (caption(raster_group_caption, pre_caption=True), 100), - (raster_plot_img(outputs_img_src, 'Primary Outputs'), 100), + (raster_plot_img(outputs_img_src, gettext('Primary Outputs')), 100), (caption(outputs_caption, definition_list=True), 100) ]) )}} @@ -44,7 +44,7 @@

Results

{% endfor %} {{ accordion_section( - 'Output Raster Stats', + gettext('Output Raster Stats'), content_grid([ (stats_table_note, 100), (wide_table( @@ -54,27 +54,27 @@

Results

]) )}} -

Inputs

+

{% trans %}Inputs{% endtrans %}

{% if args_dict != None %} {{ accordion_section( - 'Arguments', + gettext('Arguments'), args_table(args_dict) )}} {% endif %} {{ accordion_section( - 'LULC Maps', + gettext('LULC Maps'), content_grid([ (caption(raster_group_caption, pre_caption=True), 100), (caption(lulc_pre_caption, pre_caption=True), 100), - (raster_plot_img(inputs_img_src, 'Raster Inputs'), 100), + (raster_plot_img(inputs_img_src, gettext('LULC Maps')), 100), (caption(inputs_caption, definition_list=True), 100) ]) )}} {{ accordion_section( - 'Input Raster Stats', + gettext('Input Raster Stats'), content_grid([ (stats_table_note, 100), (wide_table( @@ -84,11 +84,11 @@

Inputs

]) )}} -

Metadata

+

{% trans %}Metadata{% endtrans %}

{{ accordion_section( - 'Output Filenames and Descriptions', + gettext('Output Filenames and Descriptions'), list_metadata(model_spec_outputs), expanded=False ) diff --git a/src/natcap/invest/reports/templates/models/coastal_vulnerability.html b/src/natcap/invest/reports/templates/models/coastal_vulnerability.html index f795d67e00..ddde6c946a 100644 --- a/src/natcap/invest/reports/templates/models/coastal_vulnerability.html +++ b/src/natcap/invest/reports/templates/models/coastal_vulnerability.html @@ -7,9 +7,9 @@ {% from 'metadata.html' import list_metadata %} {% from 'caption.html' import caption %} -

Primary Outputs

+

{% trans %}Primary Outputs{% endtrans %}

{{ accordion_section( - 'Coastal exposure and the protective role of habitats', + gettext('Coastal exposure and the protective role of habitats'), content_grid([ (content_grid([ ('
', 100), @@ -27,9 +27,9 @@

Primary Outputs

]) ) }} -

Intermediate Outputs

+

{% trans %}Intermediate Outputs{% endtrans %}

{{ accordion_section( - 'Ranked exposure variables', + gettext('Ranked exposure variables'), content_grid([ ('
', 100), (caption(rank_vars_figure_caption, rank_vars_figure_source_list), 100) @@ -37,7 +37,7 @@

Intermediate Outputs

) }} {{ accordion_section( - 'Pre-ranked variables', + gettext('Pre-ranked variables'), content_grid([ ('
', 100), (caption(facetted_histograms_caption, facetted_histograms_source_list), 100) @@ -45,23 +45,23 @@

Intermediate Outputs

) }} {{ accordion_section( - 'Wave Exposure', + gettext('Wave Exposure'), content_grid([ ('
', 65), (caption(wave_energy_map_caption, wave_energy_map_source_list), 35) ]) ) }} -

Inputs

+

{% trans %}Inputs{% endtrans %}

{{ accordion_section( - 'Arguments', + gettext('Arguments'), args_table(args_dict), )}} -

Metadata

+

{% trans %}Metadata{% endtrans %}

{{ accordion_section( - 'Output Filenames and Descriptions', + gettext('Output Filenames and Descriptions'), list_metadata(model_spec_outputs), expanded=False ) diff --git a/src/natcap/invest/reports/templates/models/sdr-ndr-report.html b/src/natcap/invest/reports/templates/models/sdr-ndr-report.html index adc574ed45..581111b2ac 100644 --- a/src/natcap/invest/reports/templates/models/sdr-ndr-report.html +++ b/src/natcap/invest/reports/templates/models/sdr-ndr-report.html @@ -16,7 +16,7 @@ {% from 'raster-plot-img.html' import raster_plot_img %} {% from 'wide-table.html' import wide_table %} -

Results

+

{% trans %}Results{% endtrans %}

{% if ws_vector_totals_table is defined and ws_vector_totals_table != None %} {% set watershed_results = content_grid([ @@ -37,15 +37,15 @@

Results

{% endif %} {{ accordion_section( - 'Results by Watershed', + gettext('Results by Watershed'), watershed_results )}} {{ accordion_section( - 'Primary Outputs', + gettext('Primary Outputs'), content_grid([ (caption(raster_group_caption, pre_caption=True), 100), - (raster_plot_img(outputs_img_src, 'Primary Outputs'), 100), + (raster_plot_img(outputs_img_src, gettext('Primary Outputs')), 100), (caption(outputs_caption, definition_list=True), 100) ]) )}} @@ -60,7 +60,7 @@

Results

)}} {{ accordion_section( - 'Output Raster Stats', + gettext('Output Raster Stats'), content_grid([ (stats_table_note, 100), (wide_table( @@ -70,24 +70,24 @@

Results

]) )}} -

Inputs

+

{% trans %}Inputs{% endtrans %}

{{ accordion_section( - 'Arguments', + gettext('Arguments'), args_table(args_dict) )}} {{ accordion_section( - 'Raster Inputs', + gettext('Raster Inputs'), content_grid([ (caption(raster_group_caption, pre_caption=True), 100), - (raster_plot_img(inputs_img_src, 'Raster Inputs'), 100), + (raster_plot_img(inputs_img_src, gettext('Raster Inputs')), 100), (caption(inputs_caption, definition_list=True), 100) ]) )}} {{ accordion_section( - 'Input Raster Stats', + gettext('Input Raster Stats'), content_grid([ (stats_table_note, 100), (wide_table( @@ -97,11 +97,11 @@

Inputs

]) )}} -

Metadata

+

{% trans %}Metadata{% endtrans %}

{{ accordion_section( - 'Output Filenames and Descriptions', + gettext('Output Filenames and Descriptions'), list_metadata(model_spec_outputs), expanded=False ) diff --git a/src/natcap/invest/reports/templates/raster-plot-img.html b/src/natcap/invest/reports/templates/raster-plot-img.html index 6f228b577b..372ab85582 100644 --- a/src/natcap/invest/reports/templates/raster-plot-img.html +++ b/src/natcap/invest/reports/templates/raster-plot-img.html @@ -1,6 +1,6 @@ {% macro raster_plot_img(img_src, img_name) -%} Raster plots: {{ img_name }} {%- endmacro %} diff --git a/src/natcap/invest/sdr/reporter.py b/src/natcap/invest/sdr/reporter.py index 5e318706b1..2e442a24ef 100644 --- a/src/natcap/invest/sdr/reporter.py +++ b/src/natcap/invest/sdr/reporter.py @@ -1,5 +1,4 @@ from natcap.invest.reports import sdr_ndr_report_generator -from natcap.invest.reports import raster_utils from natcap.invest.reports import report_constants from natcap.invest.reports.raster_utils import RasterDatatype from natcap.invest.reports.raster_utils import RasterTransform @@ -83,7 +82,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath): raster_path=file_registry['stream'], datatype=RasterDatatype.binary_high_contrast, spec=model_spec.get_output('stream')) - stream_config.caption += report_constants.STREAM_CAPTION_APPENDIX + stream_config.caption += report_constants.stream_caption_appendix() intermediate_raster_plot_configs = [ masked_dem_config, what_drains_config, stream_config] diff --git a/src/natcap/invest/spec.py b/src/natcap/invest/spec.py index 1eb3102dfe..d12abb260f 100644 --- a/src/natcap/invest/spec.py +++ b/src/natcap/invest/spec.py @@ -1,4 +1,4 @@ -import collections +import collections.abc import contextlib import copy import importlib @@ -17,7 +17,6 @@ from osgeo import ogr from osgeo import osr import geometamaker -import natcap.invest import pandas import pint import pygeoprocessing @@ -26,15 +25,15 @@ field_validator, model_validator import taskgraph +from natcap.invest import gettext, reports, utils, validation_messages +from natcap.invest import __version__ as invest_version from natcap.invest.file_registry import FileRegistry -from natcap.invest import utils -from . import gettext -from .unit_registry import u -from . import validation_messages +from natcap.invest.unit_registry import u LOGGER = logging.getLogger(__name__) + # accessing a file could take a long time if it's in a file streaming service # to prevent the UI from hanging due to slow validation, # set a timeout for these functions. @@ -2091,7 +2090,7 @@ def generate_metadata_for_outputs(self, file_registry, args_dict): formatted_args = pprint.pformat(args_dict) lineage_statement = ( f'Created by {self.model_id} execute(' - f'\n{formatted_args})\nVersion {natcap.invest.__version__}') + f'\n{formatted_args})\nVersion {invest_version}') keywords = [self.model_id, 'InVEST'] def _generate_metadata(root_key, value): @@ -2274,13 +2273,13 @@ def execute(self, args, create_logfile=False, log_level=logging.NOTSET, if generate_report: LOGGER.info('Generating report for results') - reporter_module = importlib.import_module(self.reporter) target_html_filepath = os.path.join( preprocessed_args['workspace_dir'], (f'{self.model_id}_report' f'{preprocessed_args.get("results_suffix", "")}.html')) - with natcap.invest.reports.configure_libraries(): + with reports.configure_libraries(): + reporter_module = importlib.import_module(self.reporter) reporter_module.report( registry, preprocessed_args, self, target_html_filepath) diff --git a/tests/reports/test_base_template.py b/tests/reports/test_base_template.py new file mode 100644 index 0000000000..820387501c --- /dev/null +++ b/tests/reports/test_base_template.py @@ -0,0 +1,63 @@ +import re +import unittest + +from bs4 import BeautifulSoup + +from natcap.invest.reports import jinja_env + +TEMPLATE = jinja_env.get_template('base.html') + +BSOUP_HTML_PARSER = 'html.parser' + + +def _get_render_args(): + return { + 'locale': 'en', + 'model_name': 'Test Model', + 'model_description': 'This is a test of the base template.', + 'userguide_page': 'testmodel.html', + 'report_script': 'natcap.invest.test.reporter', + 'invest_version': '987.65.0', + 'report_filepath': 'test_report.html', + 'timestamp': '1970-01-01', + } + + +class BaseTemplateTests(unittest.TestCase): + """Unit tests for base template.""" + + def test_base_template_content(self): + """Test that rendered HTML includes key details passed to template.""" + + render_args = _get_render_args() + + html = TEMPLATE.render(render_args) + soup = BeautifulSoup(html, BSOUP_HTML_PARSER) + + self.assertIn(render_args['model_name'], soup.title.string) + self.assertIn(render_args['model_name'], soup.h1.string) + + self.assertIsNotNone(soup.main.find( + string=render_args['model_description'])) + + ug_link = soup.find('a') + self.assertIn(render_args['userguide_page'], ug_link['href']) + + for arg in ['report_script', 'invest_version', + 'report_filepath', 'timestamp']: + self.assertIsNotNone( + soup.footer.find(string=re.compile(render_args[arg]))) + + def test_locale(self): + """Test that locale is set on ``lang`` attribute and in UG URL.""" + + render_args = _get_render_args() + render_args['locale'] = 'es' + + html = TEMPLATE.render(render_args) + soup = BeautifulSoup(html, BSOUP_HTML_PARSER) + + self.assertEqual(soup.html['lang'], 'es') + + ug_link = soup.find('a') + self.assertIn('/es/', ug_link['href']) diff --git a/tests/reports/test_carbon_template.py b/tests/reports/test_carbon_template.py index 68e4504be3..f31d7464a3 100644 --- a/tests/reports/test_carbon_template.py +++ b/tests/reports/test_carbon_template.py @@ -12,6 +12,7 @@ def _get_render_args(model_spec): + locale = 'en' report_filepath = 'carbon_report_test.html' invest_version = '987.65.0' timestamp = '1970-01-01' @@ -28,6 +29,7 @@ def _get_render_args(model_spec): agg_results_table = '
NameValue{% trans %}Name{% endtrans %}{% trans %}Value{% endtrans %}
' return { + 'locale': locale, 'report_script': model_spec.reporter, 'invest_version': invest_version, 'report_filepath': report_filepath, diff --git a/tests/reports/test_coastal_vulnerability.py b/tests/reports/test_coastal_vulnerability.py index 34c67acc93..f3b9616b00 100644 --- a/tests/reports/test_coastal_vulnerability.py +++ b/tests/reports/test_coastal_vulnerability.py @@ -27,6 +27,7 @@ def test_template_render(self): rank_vars_figure_json = vegalite_json wave_energy_map_json = vegalite_json html = template.render( + locale='en', report_script=__file__, invest_version='987.65.0', report_filepath='coastal_vulnerability_report_test.html', diff --git a/tests/reports/test_sdr_ndr_template.py b/tests/reports/test_sdr_ndr_template.py index 6f52e4e2b3..78293c29da 100644 --- a/tests/reports/test_sdr_ndr_template.py +++ b/tests/reports/test_sdr_ndr_template.py @@ -10,6 +10,7 @@ def _get_render_args(model_spec): + locale = 'en' report_filepath = 'sdr_ndr_report_test.html' invest_version = '987.65.0' timestamp = '1970-01-01' @@ -27,6 +28,7 @@ def _get_render_args(model_spec): raster_group_caption = 'This is another test!' return { + 'locale': locale, 'report_script': model_spec.reporter, 'invest_version': invest_version, 'report_filepath': report_filepath, @@ -91,6 +93,7 @@ def test_watershed_results_totals(self): ws_vector_totals_table = '
' html = TEMPLATE.render( + locale='en', report_script='natcap.invest.test.reporter', invest_version='987.65.0', report_filepath='sdr_ndr_report_test.html', @@ -130,6 +133,7 @@ def test_watershed_results_without_totals(self): ws_vector_totals_table = None html = TEMPLATE.render( + locale='en', report_script='natcap.invest.test.reporter', invest_version='987.65.0', report_filepath='sdr_ndr_report_test.html', diff --git a/tests/test_cli.py b/tests/test_cli.py index 1f90811123..49e88b4738 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -55,9 +55,11 @@ def test_run_coastal_blue_carbon_workspace_in_json(self): parameter_set_file.write( json.dumps(datastack_dict, indent=4, sort_keys=True)) - with unittest.mock.patch( - 'natcap.invest.coastal_blue_carbon.coastal_blue_carbon.execute', - return_value=None) as patched_model: + target = ( + 'natcap.invest.coastal_blue_carbon.coastal_blue_carbon.execute') + + with (unittest.mock.patch(target, return_value=None) as patched_model, + unittest.mock.patch('importlib.reload')): cli.main([ 'run', 'coastal_blue_carbon', # uses an exact modelname @@ -72,9 +74,11 @@ def test_run_coastal_blue_carbon(self): os.path.dirname(__file__), '..', 'data', 'invest-test-data', 'coastal_blue_carbon', 'cbc_galveston_bay.invs.json') - with unittest.mock.patch( - 'natcap.invest.coastal_blue_carbon.coastal_blue_carbon.execute', - return_value=None) as patched_model: + target = ( + 'natcap.invest.coastal_blue_carbon.coastal_blue_carbon.execute') + + with (unittest.mock.patch(target, return_value=None) as patched_model, + unittest.mock.patch('importlib.reload')): cli.main([ 'run', 'coastal_blue_carbon', # uses an exact modelname @@ -193,7 +197,9 @@ def test_model_alias(self): target = ( 'natcap.invest.coastal_blue_carbon.coastal_blue_carbon.execute') - with unittest.mock.patch(target, return_value=None) as patched_model: + + with (unittest.mock.patch(target, return_value=None) as patched_model, + unittest.mock.patch('importlib.reload')): cli.main([ 'run', 'cbc', # uses an alias