diff --git a/README.md b/README.md index 7a9f1243..1ef2ec9d 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,11 @@ From the `server/` directory run the following 3. `python -m http.server 1337` 4. visit `http://localhost:1337/htmlcov/` and dig into modules to see which individual line coverage + +## Selenium tests + +There are a few tests that use a live browser to test interaction. These do not run by default. You'll have to run them via `python manage.py test --selenium tests/selenium/`. + +These tests should work both in postgres and sqlite. But when using postgres, if anything goes wrong, the DB may not reset correctly and you may have to reset it (see dropdb test db command above). + +You can run regular tests or these tests, never both at the same time. Do not attempt to run regular tests with --selenium or selenium tests without it, or else it will error and you will need to reset the test DB. diff --git a/server/cpho/fixtures/dimension_lookups.yaml b/server/cpho/fixtures/dimension_lookups.yaml index 31ee55ad..dcc1ce91 100644 --- a/server/cpho/fixtures/dimension_lookups.yaml +++ b/server/cpho/fixtures/dimension_lookups.yaml @@ -104,7 +104,7 @@ fields: dimension_type: 1 name_en: Male - name_fr: Male + name_fr: Mâle value: m excel_code: "MALES" order: 0.0 @@ -113,7 +113,7 @@ fields: dimension_type: 1 name_en: Female - name_fr: Female + name_fr: Femelle value: f excel_code: "FEMALES" order: 1.0 diff --git a/server/cpho/jinja2/base.jinja2 b/server/cpho/jinja2/base.jinja2 index d47ee430..b6e9a4ed 100644 --- a/server/cpho/jinja2/base.jinja2 +++ b/server/cpho/jinja2/base.jinja2 @@ -76,14 +76,16 @@ data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" - aria-label="Toggle navigation"> + aria-label="{{ tm('toggle_navigation') }}"> {% endblock %} -
+
{% if messages %} {% for message in messages %} diff --git a/server/cpho/jinja2/benchmarking/manage_benchmarking_data.jinja2 b/server/cpho/jinja2/benchmarking/manage_benchmarking_data.jinja2 index 4ad6871b..cffb5295 100644 --- a/server/cpho/jinja2/benchmarking/manage_benchmarking_data.jinja2 +++ b/server/cpho/jinja2/benchmarking/manage_benchmarking_data.jinja2 @@ -7,68 +7,79 @@ {{ bc.item(tm("benchmarking") , None, True) }} {% endblock %} -{% macro benchmarking_form_field_with_errors(field) %} - {{ field }} +{% macro benchmarking_form_field_with_errors(form,field,row_label_id) %} + {{ field.as_widget(attrs={"aria-labelledby": row_label_id}) }} {% if field.errors %} -
+
{% for error in field.errors %}{{ error|escape }}{% endfor %}
{% endif %} {% endmacro %} {% macro benchmarking_form(form) %} + {% set row_label_id = form.prefix ~ "-row-label" %} - {{ benchmarking_form_field_with_errors(form.oecd_country) }} - {{ benchmarking_form_field_with_errors(form.value) }} - {{ form.unit }} - {{ benchmarking_form_field_with_errors(form.year) }} + + {{ benchmarking_form_field_with_errors(form,form.oecd_country, row_label_id) }} + + + + + {{ benchmarking_form_field_with_errors(form,form.value, row_label_id) }} + {{ form.unit.as_widget(attrs={"aria-labelledby": row_label_id}) }} + {{ benchmarking_form_field_with_errors(form,form.year, row_label_id) }} {# {{ benchmarking_form_field_with_errors(form.standard_deviation) }} #} - {{ benchmarking_form_field_with_errors(form.comparison_to_oecd_avg) }} - {{ form.labels }} - {{ form.methodology_differences }} + {{ benchmarking_form_field_with_errors(form, form.comparison_to_oecd_avg, row_label_id) }} + {{ form.labels.as_widget(attrs={"aria-labelledby": row_label_id}) }} + {{ form.methodology_differences.as_widget(attrs={"aria-labelledby": row_label_id}) }} - {{ form.is_deleted }} + {{ form.is_deleted.as_widget(attrs={"aria-labelledby": row_label_id}) }} {{ form.id }} {% endmacro %} {% block content %} -
{{ tm("benchmarking_metadata") }} {{ tm("for") }} : {{ indicator.name }}
+
{{ tm("benchmarking_metadata") }} {{ tm("for") }} : {{ indicator.bilingual_name }}
Export Benchmarking + href="{{ url('export_benchmarking', args=[indicator.id]) }}">{{ tm("export_benchmarking") }} - - {{ benchmarking_form(benchmarking_formset.empty_form) }} -
+
+ + {{ benchmarking_form(benchmarking_formset.empty_form) }} +
+
{{ benchmarking_formset.management_form }} {{ csrf_input }} - - - - - - - - {# #} - - - - - - - - {% for form in benchmarking_formset %}{{ benchmarking_form(form) }}{% endfor %} - -
{{ tm("oecd_country") }}{{ tm("value") }}{{ tm("unit") }}{{ tm("year") }}{{ tdt("Standard Deviation") }}{{ tm("comparison_to_oecd_average") }}{{ tm("labels") }}{{ tm("methodology_differences") }}{{ tm("delete") }}
+
+ + + + + + + + + {# #} + + + + + + + + {% for form in benchmarking_formset %}{{ benchmarking_form(form) }}{% endfor %} + +
{{ tm("oecd_country") }}{{ tm("value") }}{{ tm("unit") }}{{ tm("year") }}{{ tdt("Standard Deviation") }}{{ tm("comparison_to_oecd_average") }}{{ tm("labels") }}{{ tm("methodology_differences") }}{{ tm("delete") }}
+
{% if benchmarking_formset.non_form_errors() %}
{{ benchmarking_formset.non_form_errors() }}
{% endif %} @@ -82,31 +93,7 @@ {% endif %}
- + {% endblock %} diff --git a/server/cpho/jinja2/breadcrumb_macros.jinja2 b/server/cpho/jinja2/breadcrumb_macros.jinja2 index e7fa2285..fbe4664b 100644 --- a/server/cpho/jinja2/breadcrumb_macros.jinja2 +++ b/server/cpho/jinja2/breadcrumb_macros.jinja2 @@ -15,7 +15,7 @@ {% endmacro %} {% macro indicator(indicator, active) %} - {{ item(indicator.name , url('view_indicator', args=[indicator.id]) , active) }} + {{ item(indicator.bilingual_name , url('view_indicator', args=[indicator.id]) , active) }} {% endmacro %} {% macro indicator_period(indicator, period, active) %} diff --git a/server/cpho/jinja2/changelog/changelog_macros.jinja2 b/server/cpho/jinja2/changelog/changelog_macros.jinja2 index 5ff25d4e..c4adfb56 100644 --- a/server/cpho/jinja2/changelog/changelog_macros.jinja2 +++ b/server/cpho/jinja2/changelog/changelog_macros.jinja2 @@ -1,72 +1,75 @@ {% from 'generic_macros.jinja2' import date_display %} {% macro changelog_table(edit_entries, show_model_type=True, show_user=True) %} - - - - - {% if show_user %}{% endif %} - - {% if show_model_type %}{% endif %} - - - - - - - - {% for edit_entry in edit_entries %} - {% if not edit_entry.diffs %} - - - {% if show_user %} - - {% endif %} - - {% if show_model_type %}{% endif %} - - - - {% endif %} - {% for diff in edit_entry.diffs %} - - - {% if show_user %} +
+ +
{{ tm("date") }}{{ tm("author") }}{{ tm("action") }}{{ tm("object_type") }}{{ tm("name") }}{{ tm("field") }}{{ tm("previous_change") }}{{ tm("this_change") }}
{{ date_display(edit_entry.right_version.timestamp) }} - {% if edit_entry.author %} - {{ edit_entry.author.pretty_name }} - {% else %} - {{ tm("n/a") }} - {% endif %} - {{ tm("saved") }}{{ edit_entry.eternal._meta.verbose_name }}{{ edit_entry.live_name }}{{ tm("no_change_detected_compared_to_previous_version") }}
{{ date_display(edit_entry.right_version.timestamp) }}
+ + + + {% if show_user %}{% endif %} + + {% if show_model_type %}{% endif %} + + + + + + + + {% for edit_entry in edit_entries %} + {% if not edit_entry.diffs %} + + + {% if show_user %} + + {% endif %} + + {% if show_model_type %}{% endif %} + + + + {% endif %} + {% for diff in edit_entry.diffs %} + + + {% if show_user %} + + {% endif %} + + {% if show_model_type %}{% endif %} + - {% endif %} - - {% if show_model_type %}{% endif %} - - - {###############autoescape content is escaped ######################} - {% autoescape false %} - - {% endautoescape %} - {% autoescape false %} - - {% endautoescape %} - {% block extra_columns %}{% endblock %} - + {###############autoescape content is escaped ######################} + {% autoescape false %} + + {% endautoescape %} + {% autoescape false %} + + {% endautoescape %} + {% block extra_columns %}{% endblock %} + + {% endfor %} {% endfor %} - {% endfor %} - -
{{ tm("date") }}{{ tm("author") }}{{ tm("action") }}{{ tm("object_type") }}{{ tm("name") }}{{ tm("field") }}{{ tm("previous_change") }}{{ tm("this_change") }}
{{ date_display(edit_entry.right_version.timestamp) }} + {% if edit_entry.author %} + {{ edit_entry.author.pretty_name }} + {% else %} + {{ tm("n/a") }} + {% endif %} + {{ tm("saved") }}{{ edit_entry.eternal._meta.verbose_name }}{{ edit_entry.live_name }}{{ tm("no_change_detected_compared_to_previous_version") }}
{{ date_display(edit_entry.right_version.timestamp) }} + {% if edit_entry.author %} + {{ edit_entry.author.pretty_name }} + {% else %} + {{ tm("n/a") }} + {% endif %} + {{ diff.action }}{{ edit_entry.eternal._meta.verbose_name }}{{ edit_entry.live_name }} - {% if edit_entry.author %} - {{ edit_entry.author.pretty_name }} - {% else %} - {{ tm("n/a") }} - {% endif %} + {% if diff.field %}{{ diff.field.verbose_name }}{% endif %} {{ diff.action }}{{ edit_entry.eternal._meta.verbose_name }}{{ edit_entry.live_name }} - {% if diff.field %}{{ diff.field.verbose_name }}{% endif %} - {{ diff.get_before_diff() }}{{ diff.get_after_diff() }}
{{ diff.get_before_diff() }}{{ diff.get_after_diff() }}
+ + +
{% endmacro %} diff --git a/server/cpho/jinja2/cpho_macros.jinja2 b/server/cpho/jinja2/cpho_macros.jinja2 index 4e6f91b4..a8d0cc22 100644 --- a/server/cpho/jinja2/cpho_macros.jinja2 +++ b/server/cpho/jinja2/cpho_macros.jinja2 @@ -2,34 +2,36 @@ {% macro indicator_data_table_w_submissions(data) %} {# assumes dimension_type and dimension_value are prefetched #} - - - - - - - - - - - - {% for datum in data %} +
+
{{ tm("stratifier_value") }}{{ tm("indicator_value") }}{{ tm("last_modified") }}{{ tm("hso_submission") }}{{ tm("program_submission") }}
+ - - - - - + + + + + - {% endfor %} - -
- {% if datum.dimension_type.is_literal %} - {{ datum.literal_dimension_val }} - {% else %} - {{ datum.dimension_value.name }} - {% endif %} - {{ datum.value }}{{ date_display(datum.last_version_date) }}{{ submission_status_label(datum.submission_status(submission_type="hso") ) }}{{ submission_status_label(datum.submission_status(submission_type="program") ) }}{{ tm("stratifier_value") }}{{ tm("indicator_value") }}{{ tm("last_modified") }}{{ tm("hso_submission") }}{{ tm("program_submission") }}
+ + + {% for datum in data %} + + + {% if datum.dimension_type.is_literal %} + {{ datum.literal_dimension_val }} + {% else %} + {{ datum.dimension_value.name }} + {% endif %} + + {{ datum.value }} + {{ date_display(datum.last_version_date) }} + {{ submission_status_label(datum.submission_status(submission_type="hso") ) }} + {{ submission_status_label(datum.submission_status(submission_type="program") ) }} + + {% endfor %} + + + {% endmacro %} {% macro submission_status_badge(submission_status) %} @@ -57,3 +59,17 @@ {% endmacro %} + + +{# rm whitespace too #} +{% macro form_field_error_id(field) -%} + {{ field.id_for_label }}_errormessage +{%- endmacro %} + +{% macro form_field_error(field) %} + {% if field.errors %} + + {% endif %} +{% endmacro %} diff --git a/server/cpho/jinja2/generic_macros.jinja2 b/server/cpho/jinja2/generic_macros.jinja2 index 5234034c..856b1361 100644 --- a/server/cpho/jinja2/generic_macros.jinja2 +++ b/server/cpho/jinja2/generic_macros.jinja2 @@ -10,3 +10,18 @@
{{ text }}
{% endmacro %} + + +{% macro required_asterisk() %} + +{% endmacro %} + + +{% macro required_explainer_alert() %} +
+ + {{ tm("required_explainer") }} + {{ required_asterisk() }} + +
+{% endmacro %} diff --git a/server/cpho/jinja2/indicator_data/manage_indicator_data.jinja2 b/server/cpho/jinja2/indicator_data/manage_indicator_data.jinja2 index 75809595..d1692390 100644 --- a/server/cpho/jinja2/indicator_data/manage_indicator_data.jinja2 +++ b/server/cpho/jinja2/indicator_data/manage_indicator_data.jinja2 @@ -9,9 +9,10 @@ {{ bc.item(tm("edit") , None, True) }} {% endblock %} -{% macro check_display_error(err, field) %} - {% if err %} -
+{% macro check_display_error(form,field) %} + {% if field.errors %} +
{% for error in field.errors %}{{ error|escape }}{% endfor %}
{% endif %} @@ -27,7 +28,7 @@ {{ form.value }} {{ hidden_a11y_label(form.value, row_name) }} - {{ check_display_error(form.errors.value, form.value) }} + {{ check_display_error(form, form.value) }} {{ form.value_unit }} @@ -36,12 +37,12 @@ {{ form.value_lower_bound }} {{ hidden_a11y_label(form.value_lower_bound, row_name) }} - {{ check_display_error(form.errors.value_lower_bound, form.value_lower_bound) }} + {{ check_display_error(form, form.value_lower_bound) }} {{ form.value_upper_bound }} {{ hidden_a11y_label(form.value_upper_bound, row_name) }} - {{ check_display_error(form.errors.value_upper_bound, form.value_upper_bound) }} + {{ check_display_error(form, form.value_upper_bound) }} {{ form.data_quality }} @@ -50,12 +51,12 @@ {{ form.single_year_timeframe }} {{ hidden_a11y_label(form.single_year_timeframe, row_name) }} - {{ check_display_error(form.errors.single_year_timeframe, form.single_year_timeframe) }} + {{ check_display_error(form, form.single_year_timeframe) }} {{ form.multi_year_timeframe }} {{ hidden_a11y_label(form.multi_year_timeframe, row_name) }} - {{ check_display_error(form.errors.multi_year_timeframe, form.multi_year_timeframe) }} + {{ check_display_error(form, form.multi_year_timeframe) }} {{ form.value_displayed }} @@ -78,15 +79,17 @@ {% endmacro %} {% block content %} -

{{ tm("indicator_data") }} : {{ indicator.name }} ({{ period }})

+

{{ tm("indicator_data") }} : {{ indicator.bilingual_name }} ({{ period }})

Cancel + href="{{ url('view_indicator_for_period', args=[indicator.id, period.id]) }}">{{ tm("cancel") }} +
- - {# This is a form template to be used by 'add new form' feature #} - {# Parent must be a table, otherwise browser will strip the invalid tr/td tags #} - {{ age_group_form(age_group_formset.empty_form) }} -
+ + {# This is a form template to be used by 'add new form' feature #} + {# Parent must be a table, otherwise browser will strip the invalid tr/td tags #} + {{ age_group_form(age_group_formset.empty_form) }} +
+
{% if respects_rule('can_edit_indicator_data', {"indicator": indicator, "period": period}) %} {% set mode = "edit" %} {% else %} @@ -130,7 +133,7 @@ {{ form.value }} {{ hidden_a11y_label(form.value, row_name) }} - {{ check_display_error(form.errors.value, form.value) }} + {{ check_display_error(form, form.value) }} {{ form.value_unit }} @@ -139,12 +142,12 @@ {{ form.value_lower_bound }} {{ hidden_a11y_label(form.value_lower_bound, row_name) }} - {{ check_display_error(form.errors.value_lower_bound, form.value_lower_bound) }} + {{ check_display_error(form, form.value_lower_bound) }} {{ form.value_upper_bound }} {{ hidden_a11y_label(form.value_upper_bound, row_name) }} - {{ check_display_error(form.errors.value_upper_bound, form.value_upper_bound) }} + {{ check_display_error(form, form.value_upper_bound) }} {{ form.data_quality }} @@ -153,12 +156,12 @@ {{ form.single_year_timeframe }} {{ hidden_a11y_label(form.single_year_timeframe, row_name) }} - {{ check_display_error(form.errors.single_year_timeframe, form.single_year_timeframe) }} + {{ check_display_error(form, form.single_year_timeframe) }} {{ form.multi_year_timeframe }} {{ hidden_a11y_label(form.multi_year_timeframe, row_name) }} - {{ check_display_error(form.errors.multi_year_timeframe, form.multi_year_timeframe) }} + {{ check_display_error(form, form.multi_year_timeframe) }} {{ form.value_displayed }} @@ -270,6 +273,10 @@ //newForm.outerHTML = `${newFormHtml}`; totalFormsInput.setAttribute('value', `${numForms}`) + + // a11y: focus on first input of newly added form + newForm.querySelector('select,input').focus(); + } {% endif %} diff --git a/server/cpho/jinja2/indicators/_indicator_form.jinja2 b/server/cpho/jinja2/indicators/_indicator_form.jinja2 index bc610393..3dfac184 100644 --- a/server/cpho/jinja2/indicators/_indicator_form.jinja2 +++ b/server/cpho/jinja2/indicators/_indicator_form.jinja2 @@ -1,14 +1,27 @@ {{ form.media }} -{% from 'cpho_macros.jinja2' import hidden_a11y_label %} +{% from 'cpho_macros.jinja2' import hidden_a11y_label, form_field_error_id, form_field_error %} +{% from 'generic_macros.jinja2' import required_asterisk, required_explainer_alert %} {% macro form_field(field, rich=false) %} {% set hso_field_display = field.name in form.hso_only_field_names and respects_rule('is_admin_or_hso') %} {% set non_hso_field_display = field.name not in form.hso_only_field_names %} + + {% set attrs = {} %} + {% if field.errors %} + {% set attrs = {'aria-describedby': form_field_error_id(field)} %} + {% endif %} + {% if (hso_field_display or non_hso_field_display) %} +
+ {% if field.errors %}
{{ form_field_error(field) }}
{% endif %}
-
{{ field.label_tag(attrs={'class': 'col col-form-label', 'aria-label': field.label }) }}
+ + {% if field.name in form.hso_only_field_names %} {{ tm("hso_only") }} {% endif %} @@ -16,9 +29,9 @@
{% if rich %} - {{ field|safe }} + {{ field.as_widget(attrs=attrs) |safe }} {% else %} - {{ field }} + {{ field.as_widget(attrs=attrs) }} {% endif %}
@@ -27,20 +40,20 @@ {% endmacro %} {% macro fieldset_field(field, helptext=None) %} -
-
+
+
-
- {{ field.label }} -
+
{{ field.label }}
{% if helptext %}
{{ helptext }}
{% endif %} -
-
{{ field }}
+ +
{{ field }}
{% endmacro %} -
+ +{{ required_explainer_alert() }} + {{ csrf_input }}
@@ -58,8 +71,11 @@ {{ form_field(form.sub_indicator_measurement) }} {{ form_field(form.sub_indicator_measurement_fr) }} - {{ fieldset_field(form.relevant_dimensions, helptext=tm("not_published_just_a_convenience_factor_to_filter_in_app_choices") ) }} - {{ fieldset_field(form.relevant_period_types, helptext=tm("not_published_just_a_convenience_factor_to_filter_in_app_choices") ) }} + {# TODO: re-add helptext #} + {# {{ fieldset_field(form.relevant_dimensions, helptext=tm("not_published_just_a_convenience_factor_to_filter_in_app_choices") ) }} #} + {{ fieldset_field(form.relevant_dimensions,) }} + {# {{ fieldset_field(form.relevant_period_types, helptext=tm("not_published_just_a_convenience_factor_to_filter_in_app_choices") ) }} #} + {{ fieldset_field(form.relevant_period_types,) }}
@@ -242,59 +258,61 @@

{{ tm("quintile_metadata_description") }}

- - - - - - - - - - - - - - - - - {% set row_name = tm('quintile_values') %} - - - - - - - - - - - -
{{ tm("quintile") }}{{ vb_name('cpho.Indicator', 'g1') }}{{ vb_name('cpho.Indicator', 'g2_lower') }}{{ vb_name('cpho.Indicator', 'g2_upper') }}{{ vb_name('cpho.Indicator', 'g3_lower') }}{{ vb_name('cpho.Indicator', 'g3_upper') }}{{ vb_name('cpho.Indicator', 'g4_lower') }}{{ vb_name('cpho.Indicator', 'g4_upper') }}{{ vb_name('cpho.Indicator', 'g5') }}
{{ row_name }} -
{{ form.g1 }}
- {{ hidden_a11y_label(form.g1, row_name) }} -
-
{{ form.g2_lower }}
- {{ hidden_a11y_label(form.g2_lower, row_name) }} -
-
{{ form.g2_upper }}
- {{ hidden_a11y_label(form.g2_upper, row_name) }} -
-
{{ form.g3_lower }}
- {{ hidden_a11y_label(form.g3_lower, row_name ,) }} -
-
{{ form.g3_upper }}
- {{ hidden_a11y_label(form.g3_upper, row_name) }} -
-
{{ form.g4_lower }}
- {{ hidden_a11y_label(form.g4_lower, row_name) }} -
-
{{ form.g4_upper }}
- {{ hidden_a11y_label(form.g4_upper, row_name) }} -
-
{{ form.g5 }}
- {{ hidden_a11y_label(form.g5, row_name) }} -
+
+ + + + + + + + + + + + + + + + + {% set row_name = tm('quintile_values') %} + + + + + + + + + + + +
{{ tm("quintile") }}{{ vb_name('cpho.Indicator', 'g1') }}{{ vb_name('cpho.Indicator', 'g2_lower') }}{{ vb_name('cpho.Indicator', 'g2_upper') }}{{ vb_name('cpho.Indicator', 'g3_lower') }}{{ vb_name('cpho.Indicator', 'g3_upper') }}{{ vb_name('cpho.Indicator', 'g4_lower') }}{{ vb_name('cpho.Indicator', 'g4_upper') }}{{ vb_name('cpho.Indicator', 'g5') }}
{{ row_name }} +
{{ form.g1 }}
+ {{ hidden_a11y_label(form.g1, row_name) }} +
+
{{ form.g2_lower }}
+ {{ hidden_a11y_label(form.g2_lower, row_name) }} +
+
{{ form.g2_upper }}
+ {{ hidden_a11y_label(form.g2_upper, row_name) }} +
+
{{ form.g3_lower }}
+ {{ hidden_a11y_label(form.g3_lower, row_name ,) }} +
+
{{ form.g3_upper }}
+ {{ hidden_a11y_label(form.g3_upper, row_name) }} +
+
{{ form.g4_lower }}
+ {{ hidden_a11y_label(form.g4_lower, row_name) }} +
+
{{ form.g4_upper }}
+ {{ hidden_a11y_label(form.g4_upper, row_name) }} +
+
{{ form.g5 }}
+ {{ hidden_a11y_label(form.g5, row_name) }} +
+
diff --git a/server/cpho/jinja2/indicators/edit_indicator.jinja2 b/server/cpho/jinja2/indicators/edit_indicator.jinja2 index f41ed765..da4a9483 100644 --- a/server/cpho/jinja2/indicators/edit_indicator.jinja2 +++ b/server/cpho/jinja2/indicators/edit_indicator.jinja2 @@ -9,7 +9,7 @@ {% block content %} -
{{ tm("edit_indicator") }} : {{ object.name }}
+
{{ tm("edit_indicator") }} : {{ object.bilingual_name }}
{% endif %} -
+
@@ -38,13 +38,13 @@ {% for indicator in object_list %} {% if respects_rule('can_access_indicator', indicator) %} - + {% endif %} diff --git a/server/cpho/jinja2/indicators/upload/upload_help_modal.jinja2 b/server/cpho/jinja2/indicators/upload/upload_help_modal.jinja2 index bd97a0e3..fd867902 100644 --- a/server/cpho/jinja2/indicators/upload/upload_help_modal.jinja2 +++ b/server/cpho/jinja2/indicators/upload/upload_help_modal.jinja2 @@ -10,7 +10,7 @@ + aria-label="{{ tm('close') }}">
{{ indicator.name }}{{ indicator.bilingual_name }} {{ indicator.get_category_display() }} {{ indicator.get_topic_display() }} {{ tm("view_indicator") }} + aria-label="{{ tm("view_indicator") }}, {{ indicator.bilingual_name }}">{{ tm("view_indicator") }}
diff --git a/server/cpho/jinja2/indicators/view_indicator_for_period.jinja2 b/server/cpho/jinja2/indicators/view_indicator_for_period.jinja2 index 1a17f616..fee73396 100644 --- a/server/cpho/jinja2/indicators/view_indicator_for_period.jinja2 +++ b/server/cpho/jinja2/indicators/view_indicator_for_period.jinja2 @@ -10,7 +10,7 @@ {% endblock %} {% block content %} -
{{ tm("indicator_data") }} : {{ object.name }} {{ period }}
+
{{ tm("indicator_data") }} : {{ object.bilingual_name }} {{ period }}
{% if respects_rule('can_edit_indicator_data', {"indicator": indicator, "period": period}) %} diff --git a/server/cpho/jinja2/modals/submission_modal.jinja2 b/server/cpho/jinja2/modals/submission_modal.jinja2 index 8130000d..85ed2e6c 100644 --- a/server/cpho/jinja2/modals/submission_modal.jinja2 +++ b/server/cpho/jinja2/modals/submission_modal.jinja2 @@ -11,7 +11,7 @@ + aria-label="{{ tm('close') }}">
- - - - - - - - {{ field_tr(tm("name") , ind.name) }} - {{ field_tr(tm("name_french") , ind.name_fr) }} +
+
{{ tm("field") }}{{ tm("value") }}
+ + + + + + + + {{ field_tr(tm("name") , ind.name) }} + {{ field_tr(tm("name_french") , ind.name_fr) }} - {{ field_tr(tm("category") , ind.get_category_display() ) }} - {{ field_tr(tm("topic") , ind.get_topic_display() ) }} + {{ field_tr(tm("category") , ind.get_category_display() ) }} + {{ field_tr(tm("topic") , ind.get_topic_display() ) }} - {{ field_tr(tm("detailed_indicator") , ind.detailed_indicator) }} - {{ field_tr(tm("detailed_indicator_french") , ind.detailed_indicator_fr) }} + {{ field_tr(tm("detailed_indicator") , ind.detailed_indicator) }} + {{ field_tr(tm("detailed_indicator_french") , ind.detailed_indicator_fr) }} - {{ field_tr(tm("sub_indicator_measurement") , ind.sub_indicator_measurement) }} - {{ field_tr(tm("sub_indicator_measurement_french") , ind.sub_indicator_measurement_fr) }} - - - - + {{ field_tr(tm("sub_indicator_measurement") , ind.sub_indicator_measurement) }} + {{ field_tr(tm("sub_indicator_measurement_french") , ind.sub_indicator_measurement_fr) }} + + + + - {{ header_tr(tm("general_metadata") ) }} + {{ header_tr(tm("general_metadata") ) }} - {{ field_tr(tm("measure_text") , ind.measure_text) }} - {{ field_tr(tm("measure_text_french") , ind.measure_text_fr) }} + {{ field_tr(tm("measure_text") , ind.measure_text) }} + {{ field_tr(tm("measure_text_french") , ind.measure_text_fr) }} - {{ field_tr(tm("impact_text") , ind.impact_text) }} - {{ field_tr(tm("impact_text_french") , ind.impact_text_fr) }} + {{ field_tr(tm("impact_text") , ind.impact_text) }} + {{ field_tr(tm("impact_text_french") , ind.impact_text_fr) }} - {{ field_tr(tm("title_overall") , ind.title_overall) }} - {{ field_tr(tm("title_overall_french") , ind.title_overall_fr) }} + {{ field_tr(tm("title_overall") , ind.title_overall) }} + {{ field_tr(tm("title_overall_french") , ind.title_overall_fr) }} - {{ field_tr(tm("table_title_overall") , ind.table_title_overall) }} - {{ field_tr(tm("table_title_overall_french") , ind.table_title_overall_fr) }} + {{ field_tr(tm("table_title_overall") , ind.table_title_overall) }} + {{ field_tr(tm("table_title_overall_french") , ind.table_title_overall_fr) }} - {{ field_tr(tm("sdg_goal") , ind.sdg_goal) }} - {{ field_tr(tm("sdg_goal_french") , ind.sdg_goal_fr) }} + {{ field_tr(tm("sdg_goal") , ind.sdg_goal) }} + {{ field_tr(tm("sdg_goal_french") , ind.sdg_goal_fr) }} - {{ field_tr(tm("general_footnotes") , ind.general_footnotes) }} - {{ field_tr(tm("general_footnotes_french") , ind.general_footnotes_fr) }} + {{ field_tr(tm("general_footnotes") , ind.general_footnotes) }} + {{ field_tr(tm("general_footnotes_french") , ind.general_footnotes_fr) }} - {{ field_tr(tm("main_source_english") , ind.main_source_english) }} - {{ field_tr(tm("main_source_french") , ind.main_source_fr) }} + {{ field_tr(tm("main_source_english") , ind.main_source_english) }} + {{ field_tr(tm("main_source_french") , ind.main_source_fr) }} - {{ field_tr(tm("other_relevant_sources_english") , ind.other_relevant_sources_english) }} - {{ field_tr(tm("other_relevant_sources_french") , ind.other_relevant_sources_fr) }} + {{ field_tr(tm("other_relevant_sources_english") , ind.other_relevant_sources_english) }} + {{ field_tr(tm("other_relevant_sources_french") , ind.other_relevant_sources_fr) }} - {{ field_tr(tm("recommendations_for_hso") , ind.recommendations_for_hso) }} - {{ field_tr(tm("recommendations_for_hso_french") , ind.recommendations_for_hso_fr) }} + {{ field_tr(tm("recommendations_for_hso") , ind.recommendations_for_hso) }} + {{ field_tr(tm("recommendations_for_hso_french") , ind.recommendations_for_hso_fr) }} - {{ header_tr(tm("sex_metadata") ) }} - {{ field_tr(tm("title_sex") , ind.title_sex) }} - {{ field_tr(tm("title_sex_french") , ind.title_sex_fr) }} + {{ header_tr(tm("sex_metadata") ) }} + {{ field_tr(tm("title_sex") , ind.title_sex) }} + {{ field_tr(tm("title_sex_french") , ind.title_sex_fr) }} - {{ field_tr(tm("table_title_sex") , ind.table_title_sex) }} - {{ field_tr(tm("table_title_sex_french") , ind.table_title_sex_fr) }} + {{ field_tr(tm("table_title_sex") , ind.table_title_sex) }} + {{ field_tr(tm("table_title_sex_french") , ind.table_title_sex_fr) }} - {{ header_tr(tm("grade_metadata") ) }} - {{ field_tr(tm("title_grade") , ind.title_grade) }} - {{ field_tr(tm("title_grade_french") , ind.title_grade_fr) }} + {{ header_tr(tm("grade_metadata") ) }} + {{ field_tr(tm("title_grade") , ind.title_grade) }} + {{ field_tr(tm("title_grade_french") , ind.title_grade_fr) }} - {{ field_tr(tm("table_title_grade") , ind.table_title_grade) }} - {{ field_tr(tm("table_title_grade_french") , ind.table_title_grade_fr) }} + {{ field_tr(tm("table_title_grade") , ind.table_title_grade) }} + {{ field_tr(tm("table_title_grade_french") , ind.table_title_grade_fr) }} - {{ header_tr(tm("hospital_setting_metadata") ) }} - {{ field_tr(tm("title_hospital_setting") , ind.title_hospital_setting) }} - {{ field_tr(tm("title_hospital_setting_french") , ind.title_hospital_setting_fr) }} + {{ header_tr(tm("hospital_setting_metadata") ) }} + {{ field_tr(tm("title_hospital_setting") , ind.title_hospital_setting) }} + {{ field_tr(tm("title_hospital_setting_french") , ind.title_hospital_setting_fr) }} - {{ field_tr(tm("table_title_hospital_setting") , ind.table_title_hospital_setting) }} - {{ field_tr(tm("table_title_hospital_setting_french") , ind.table_title_hospital_setting_fr) }} + {{ field_tr(tm("table_title_hospital_setting") , ind.table_title_hospital_setting) }} + {{ field_tr(tm("table_title_hospital_setting_french") , ind.table_title_hospital_setting_fr) }} - {{ header_tr(tm("age_metadata") ) }} - {{ field_tr(tm("title_age") , ind.title_age) }} - {{ field_tr(tm("title_age_french") , ind.title_age_fr) }} + {{ header_tr(tm("age_metadata") ) }} + {{ field_tr(tm("title_age") , ind.title_age) }} + {{ field_tr(tm("title_age_french") , ind.title_age_fr) }} - {{ field_tr(tm("table_title_age") , ind.table_title_age) }} - {{ field_tr(tm("table_title_age_french") , ind.table_title_age_fr) }} + {{ field_tr(tm("table_title_age") , ind.table_title_age) }} + {{ field_tr(tm("table_title_age_french") , ind.table_title_age_fr) }} - {{ header_tr(tm("province_metadata") ) }} - {{ field_tr(tm("title_province_territory") , ind.title_province_territory) }} - {{ field_tr(tm("title_province_territory_french") , ind.title_province_territory_fr) }} + {{ header_tr(tm("province_metadata") ) }} + {{ field_tr(tm("title_province_territory") , ind.title_province_territory) }} + {{ field_tr(tm("title_province_territory_french") , ind.title_province_territory_fr) }} - {{ field_tr(tm("table_title_province_territory") , ind.table_title_province_territory) }} - {{ field_tr(tm("table_title_province_territory_french") , ind.table_title_province_territory_fr) }} + {{ field_tr(tm("table_title_province_territory") , ind.table_title_province_territory) }} + {{ field_tr(tm("table_title_province_territory_french") , ind.table_title_province_territory_fr) }} - {{ field_tr(tm("pt_dynamic_text") , ind.pt_dynamic_text) }} - {{ field_tr(tm("pt_dynamic_text_french") , ind.pt_dynamic_text_fr) }} + {{ field_tr(tm("pt_dynamic_text") , ind.pt_dynamic_text) }} + {{ field_tr(tm("pt_dynamic_text_french") , ind.pt_dynamic_text_fr) }} - {{ header_tr(tm("living_arrangement_metadata") ) }} - {{ field_tr(tm("title_living_arrangement") , ind.title_living_arrangement) }} - {{ field_tr(tm("title_living_arrangement_french") , ind.title_living_arrangement_fr) }} + {{ header_tr(tm("living_arrangement_metadata") ) }} + {{ field_tr(tm("title_living_arrangement") , ind.title_living_arrangement) }} + {{ field_tr(tm("title_living_arrangement_french") , ind.title_living_arrangement_fr) }} - {{ field_tr(tm("table_title_living_arrangement") , ind.table_title_living_arrangement) }} - {{ field_tr(tm("table_title_living_arrangement_french") , ind.table_title_living_arrangement_fr) }} + {{ field_tr(tm("table_title_living_arrangement") , ind.table_title_living_arrangement) }} + {{ field_tr(tm("table_title_living_arrangement_french") , ind.table_title_living_arrangement_fr) }} - {{ header_tr(tm("education_household_metadata") ) }} - {{ field_tr(tm("title_education_household") , ind.title_education_household) }} - {{ field_tr(tm("title_education_household_french") , ind.title_education_household_fr) }} + {{ header_tr(tm("education_household_metadata") ) }} + {{ field_tr(tm("title_education_household") , ind.title_education_household) }} + {{ field_tr(tm("title_education_household_french") , ind.title_education_household_fr) }} - {{ field_tr(tm("table_title_education_household") , ind.table_title_education_household) }} - {{ field_tr(tm("table_title_education_household_french") , ind.table_title_education_household_fr) }} + {{ field_tr(tm("table_title_education_household") , ind.table_title_education_household) }} + {{ field_tr(tm("table_title_education_household_french") , ind.table_title_education_household_fr) }} - {{ header_tr(tm("income_quintiles_metadata") ) }} - {{ field_tr(tm("title_income_quintiles") , ind.title_income_quintiles) }} - {{ field_tr(tm("title_income_quintiles_french") , ind.title_income_quintiles_fr) }} + {{ header_tr(tm("income_quintiles_metadata") ) }} + {{ field_tr(tm("title_income_quintiles") , ind.title_income_quintiles) }} + {{ field_tr(tm("title_income_quintiles_french") , ind.title_income_quintiles_fr) }} - {{ field_tr(tm("table_title_income_quintiles") , ind.table_title_income_quintiles) }} - {{ field_tr(tm("table_title_income_quintiles_french") , ind.table_title_income_quintiles_fr) }} + {{ field_tr(tm("table_title_income_quintiles") , ind.table_title_income_quintiles) }} + {{ field_tr(tm("table_title_income_quintiles_french") , ind.table_title_income_quintiles_fr) }} - {{ header_tr(tm("trend_analysis_metadata") ) }} - {{ field_tr(tm("title_trend") , ind.title_trend) }} - {{ field_tr(tm("title_trend_french") , ind.title_trend_fr) }} + {{ header_tr(tm("trend_analysis_metadata") ) }} + {{ field_tr(tm("title_trend") , ind.title_trend) }} + {{ field_tr(tm("title_trend_french") , ind.title_trend_fr) }} - {{ field_tr(tm("table_title_trend") , ind.table_title_trend) }} - {{ field_tr(tm("table_title_trend_french") , ind.table_title_trend_fr) }} + {{ field_tr(tm("table_title_trend") , ind.table_title_trend) }} + {{ field_tr(tm("table_title_trend_french") , ind.table_title_trend_fr) }} - {{ field_tr(tm("visual_description_trend") , ind.visual_description_trend) }} - {{ field_tr(tm("visual_description_trend_french") , ind.visual_description_trend_fr) }} + {{ field_tr(tm("visual_description_trend") , ind.visual_description_trend) }} + {{ field_tr(tm("visual_description_trend_french") , ind.visual_description_trend_fr) }} - {{ field_tr(tm("x_axis_trend") , ind.x_axis_trend) }} - {{ field_tr(tm("x_axis_trend_french") , ind.x_axis_trend_fr) }} + {{ field_tr(tm("x_axis_trend") , ind.x_axis_trend) }} + {{ field_tr(tm("x_axis_trend_french") , ind.x_axis_trend_fr) }} - {{ field_tr(tm("y_axis_trend") , ind.y_axis_trend) }} - {{ field_tr(tm("y_axis_trend_french") , ind.y_axis_trend_fr) }} + {{ field_tr(tm("y_axis_trend") , ind.y_axis_trend) }} + {{ field_tr(tm("y_axis_trend_french") , ind.y_axis_trend_fr) }} - {{ field_tr(tm("y_axis_trend_min") , ind.y_axis_trend_min) }} - {{ field_tr(tm("y_axis_trend_max") , ind.y_axis_trend_max) }} + {{ field_tr(tm("y_axis_trend_min") , ind.y_axis_trend_min) }} + {{ field_tr(tm("y_axis_trend_max") , ind.y_axis_trend_max) }} - {{ field_tr(tm("trend_footnotes") , ind.trend_footnotes) }} - {{ field_tr(tm("trend_footnotes_french") , ind.trend_footnotes_fr) }} + {{ field_tr(tm("trend_footnotes") , ind.trend_footnotes) }} + {{ field_tr(tm("trend_footnotes_french") , ind.trend_footnotes_fr) }} - {{ header_tr(tm("benchmarking_metadata") ) }} - {{ field_tr(tm("title_benchmark") , ind.title_benchmark) }} - {{ field_tr(tm("title_benchmark_french") , ind.title_benchmark_fr) }} + {{ header_tr(tm("benchmarking_metadata") ) }} + {{ field_tr(tm("title_benchmark") , ind.title_benchmark) }} + {{ field_tr(tm("title_benchmark_french") , ind.title_benchmark_fr) }} - {{ field_tr(tm("table_title_benchmark") , ind.table_title_benchmark) }} - {{ field_tr(tm("table_title_benchmark_french") , ind.table_title_benchmark_fr) }} + {{ field_tr(tm("table_title_benchmark") , ind.table_title_benchmark) }} + {{ field_tr(tm("table_title_benchmark_french") , ind.table_title_benchmark_fr) }} - {{ field_tr(tm("x_axis_benchmark") , ind.x_axis_benchmark) }} - {{ field_tr(tm("x_axis_benchmark_french") , ind.x_axis_benchmark_fr) }} + {{ field_tr(tm("x_axis_benchmark") , ind.x_axis_benchmark) }} + {{ field_tr(tm("x_axis_benchmark_french") , ind.x_axis_benchmark_fr) }} - {{ field_tr(tm("benchmarking_dynamic_text") , ind.benchmarking_dynamic_text) }} - {{ field_tr(tm("benchmarking_dynamic_text_french") , ind.benchmarking_dynamic_text_fr) }} + {{ field_tr(tm("benchmarking_dynamic_text") , ind.benchmarking_dynamic_text) }} + {{ field_tr(tm("benchmarking_dynamic_text_french") , ind.benchmarking_dynamic_text_fr) }} - {{ field_tr(tm("benchmarking_footnotes") , ind.benchmarking_footnotes) }} - {{ field_tr(tm("benchmarking_footnotes_french") , ind.benchmarking_footnotes_fr) }} + {{ field_tr(tm("benchmarking_footnotes") , ind.benchmarking_footnotes) }} + {{ field_tr(tm("benchmarking_footnotes_french") , ind.benchmarking_footnotes_fr) }} - {{ field_tr(tm("benchmarking_sources_english") , ind.benchmarking_sources_english) }} - {{ field_tr(tm("benchmarking_sources_french") , ind.benchmarking_sources_fr) }} + {{ field_tr(tm("benchmarking_sources_english") , ind.benchmarking_sources_english) }} + {{ field_tr(tm("benchmarking_sources_french") , ind.benchmarking_sources_fr) }} - -
{{ tm("field") }}{{ tm("value") }}
{{ tm("relevant_dimensions") }} -
    - {% for item in ind.relevant_dimensions.all() %}
  • {{ item.name }}
  • {% endfor %} -
-
{{ tm("relevant_dimensions") }} +
    + {% for item in ind.relevant_dimensions.all() %} + {# #} +
  • {{ item.name }}
  • + {# #} + {% endfor %} +
+
+ + +
{{ tm("quintile_values") }}

{{ tm("quintile_metadata_description") }}

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ tm("quintile") }}G1G2 {{ tm("lower") }}G2 {{ tm("upper") }}G3 {{ tm("lower") }}G3 {{ tm("upper") }}G4 {{ tm("lower") }}G4 {{ tm("upper") }}G5
{{ tm("quintile_values") }}{{ ind.g1 }}{{ ind.g2_lower }}{{ ind.g2_upper }}{{ ind.g3_lower }}{{ ind.g3_upper }}{{ ind.g4_lower }}{{ ind.g4_upper }}{{ ind.g5 }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ tm("quintile") }}G1G2 {{ tm("lower") }}G2 {{ tm("upper") }}G3 {{ tm("lower") }}G3 {{ tm("upper") }}G4 {{ tm("lower") }}G4 {{ tm("upper") }}G5
{{ tm("quintile_values") }}{{ ind.g1 }}{{ ind.g2_lower }}{{ ind.g2_upper }}{{ ind.g3_lower }}{{ ind.g3_upper }}{{ ind.g4_lower }}{{ ind.g4_upper }}{{ ind.g5 }}
+
@@ -292,32 +301,35 @@
- - - - - - - - - - - - - - {% for item in metadata["benchmarking"].order_by('labels', 'value') %} +
+ +
{{ tm("oecd_country") }}{{ tm("value") }}{{ tm("unit") }}{{ tm("year") }}{{ tm("comparison_to_oecd_average") }}{{ tm("labels") }}{{ tm("methodology_differences") }}
+ - - - - - - - + + + + + + + - {% endfor %} - -
{{ item.oecd_country }}{{ item.value }}{{ item.get_unit_display() }}{{ item.year }}{{ item.get_comparison_to_oecd_avg_display() }}{{ item.get_labels_display() }}{{ tm(item.get_methodology_differences_display() ) }}{{ tm("oecd_country") }}{{ tm("value") }}{{ tm("unit") }}{{ tm("year") }}{{ tm("comparison_to_oecd_average") }}{{ tm("labels") }}{{ tm("methodology_differences") }}
+ + + {% for item in metadata["benchmarking"].order_by('labels', 'value') %} + + {{ item.oecd_country }} + {{ item.value }} + {{ item.get_unit_display() }} + {{ item.year }} + {{ item.get_comparison_to_oecd_avg_display() }} + {{ item.get_labels_display() }} + {{ tm(item.get_methodology_differences_display() ) }} + + {% endfor %} + + +
@@ -344,36 +356,38 @@
- - - - - - - - - - - - - - - - {% for item in metadata["trend"] %} +
+
{{ tm("year_or_range") }}{{ tm("data_point") }}{{ tm("line_of_best_fit_point") }}{{ tm("trend_segment") }}{{ tm("trend") }}{{ tm("data_quality") }}{{ tm("unit") }}{{ tm("data_lower_ci") }}{{ tm("data_upper_ci") }}
+ - - - - - - - - - + + + + + + + + + - {% endfor %} - -
{{ item.year }}{{ item.data_point }}{{ item.line_of_best_fit_point }}{{ item.trend_segment }}{{ item.get_trend_display() }}{{ item.get_data_quality_display() }}{{ item.get_unit_display() }}{{ item.data_point_lower_ci }}{{ item.data_point_upper_ci }}{{ tm("year_or_range") }}{{ tm("data_point") }}{{ tm("line_of_best_fit_point") }}{{ tm("trend_segment") }}{{ tm("trend") }}{{ tm("data_quality") }}{{ tm("unit") }}{{ tm("data_lower_ci") }}{{ tm("data_upper_ci") }}
+ + + {% for item in metadata["trend"] %} + + {{ item.year }} + {{ item.data_point }} + {{ item.line_of_best_fit_point }} + {{ item.trend_segment }} + {{ item.get_trend_display() }} + {{ item.get_data_quality_display() }} + {{ item.get_unit_display() }} + {{ item.data_point_lower_ci }} + {{ item.data_point_upper_ci }} + + {% endfor %} + + +
diff --git a/server/cpho/jinja2/trend_analysis/manage_trend_analysis_data.jinja2 b/server/cpho/jinja2/trend_analysis/manage_trend_analysis_data.jinja2 index d39c899b..eb03eddf 100644 --- a/server/cpho/jinja2/trend_analysis/manage_trend_analysis_data.jinja2 +++ b/server/cpho/jinja2/trend_analysis/manage_trend_analysis_data.jinja2 @@ -7,29 +7,36 @@ {{ bc.item(tm("trend_analysis") , None, True) }} {% endblock %} -{% macro trend_analysis_form_field_with_errors(field) %} - {{ field }} +{% macro trend_analysis_form_field_with_errors(field,row_label_id) %} + {{ field.as_widget(attrs={"aria-labelledby": row_label_id}) }} {% if field.errors %} -
+
{% for error in field.errors %}{{ error|escape }}{% endfor %}
{% endif %} {% endmacro %} {% macro trend_analysis_form(form) %} + {% set row_label_id = form.prefix ~ "-row-label" %} - {{ trend_analysis_form_field_with_errors(form.year) }} - {{ trend_analysis_form_field_with_errors(form.data_point) }} - {{ trend_analysis_form_field_with_errors(form.line_of_best_fit_point) }} - {{ trend_analysis_form_field_with_errors(form.trend_segment) }} - {{ form.trend }} - {{ form.data_quality }} - {{ form.unit }} - {{ trend_analysis_form_field_with_errors(form.data_point_lower_ci) }} - {{ trend_analysis_form_field_with_errors(form.data_point_upper_ci) }} - {{ form.is_deleted }} + {{ trend_analysis_form_field_with_errors(form.year, row_label_id) }} + + + + + + {{ trend_analysis_form_field_with_errors(form.data_point, row_label_id) }} + {{ trend_analysis_form_field_with_errors(form.line_of_best_fit_point, row_label_id) }} + {{ trend_analysis_form_field_with_errors(form.trend_segment, row_label_id) }} + {{ form.trend.as_widget(attrs={"aria-labelledby": row_label_id}) }} + {{ form.data_quality.as_widget(attrs={"aria-labelledby": row_label_id}) }} + {{ form.unit.as_widget(attrs={"aria-labelledby": row_label_id}) }} + {{ trend_analysis_form_field_with_errors(form.data_point_lower_ci, row_label_id) }} + {{ trend_analysis_form_field_with_errors(form.data_point_upper_ci, row_label_id) }} + + {{ form.is_deleted.as_widget(attrs={"aria-labelledby": row_label_id}) }} {{ form.id }} @@ -84,29 +91,6 @@ class="btn btn-primary float-end my-3" value="{{ tm('save') }}"> - + {% endblock %} diff --git a/server/cpho/jinja2/user_management/indicator_directory_home.jinja2 b/server/cpho/jinja2/user_management/indicator_directory_home.jinja2 index aa5fac6a..ab26e17e 100644 --- a/server/cpho/jinja2/user_management/indicator_directory_home.jinja2 +++ b/server/cpho/jinja2/user_management/indicator_directory_home.jinja2 @@ -26,26 +26,29 @@

{{ tm("users") }}

- - - - - - - {% for user in indicator_directory.users.all() %} - - - - - {% endfor %} - -
{{ tm("user") }}{{ tm("last_login") }}
{{ user }} - {% if user.last_login %} - {{ date_display(user.last_login) }} - {% else %} - {{ tm("never") }} - {% endif %} -
+
+ + + + + + + + {% for user in indicator_directory.users.all() %} + + + + + {% endfor %} + +
{{ tm("user") }}{{ tm("last_login") }}
{{ user }} + {% if user.last_login %} + {{ date_display(user.last_login) }} + {% else %} + {{ tm("never") }} + {% endif %} +
+
diff --git a/server/cpho/jinja2/user_management/user_management_page.jinja2 b/server/cpho/jinja2/user_management/user_management_page.jinja2 index c3fc0b11..acb59d16 100644 --- a/server/cpho/jinja2/user_management/user_management_page.jinja2 +++ b/server/cpho/jinja2/user_management/user_management_page.jinja2 @@ -3,39 +3,42 @@ {% block content %}

{{ tm("user_management") }}

- - - - - - - - - - - {% for user in all_users %} +
+ +
{{ tdt("Username") }}{{ tdt("Special roles") }}{{ tdt("Last login") }}{{ tdt("Modify") }}
+ - - - - + + + + - {% endfor %} - -
{{ user }} - {% if user.is_admin %}{{ tdt("Admin") }}{% endif %} - {% if user.is_hso %}{{ tdt("HSO") }}{% endif %} - - {% if user.last_login %}{{ date_display(user.last_login) }}{% endif %} - - {{ tdt("Modify") }} - {{ tdt("Username") }}{{ tdt("Special roles") }}{{ tdt("Last login") }}{{ tdt("Modify") }}
+ + + {% for user in all_users %} + + {{ user }} + + {% if user.is_admin %}{{ tdt("Admin") }}{% endif %} + {% if user.is_hso %}{{ tdt("HSO") }}{% endif %} + + + {% if user.last_login %}{{ date_display(user.last_login) }}{% endif %} + + + {{ tm("edit") }} + + + {% endfor %} + + + -

{{ tdt("Indicator directories") }}

+

{{ tm("indicator_directories") }}

    {% for directory in indicator_directories %}
  • diff --git a/server/cpho/models/indicators.py b/server/cpho/models/indicators.py index e7a19afc..d55af52a 100644 --- a/server/cpho/models/indicators.py +++ b/server/cpho/models/indicators.py @@ -369,12 +369,14 @@ class Meta: config_name="notes", null=True, blank=True ) + @property + def bilingual_name(self): + if get_lang_code() == "fr" and self.name_fr: + return self.name_fr + "" + return self.name + "" + def __str__(self): - return " ".join( - [ - str(self.name), - ] - ) + return self.bilingual_name def get_relevant_periods(self): # filter Period. to years that have data or have the same type as the indicator's relevant_period_types diff --git a/server/cpho/rules.py b/server/cpho/rules.py index d7e46f3e..a00d5f23 100644 --- a/server/cpho/rules.py +++ b/server/cpho/rules.py @@ -73,7 +73,8 @@ def can_use_indicator_upload(user): @auto_rule def can_manage_users(user): - return is_admin_or_hso(user) + # return is_admin_or_hso(user) + return is_admin(user) @auto_rule diff --git a/server/cpho/scripts/countries.csv b/server/cpho/scripts/countries.csv index 605f9a73..5947fe94 100644 --- a/server/cpho/scripts/countries.csv +++ b/server/cpho/scripts/countries.csv @@ -1,43 +1,43 @@ id,name_en,name_fr -0000,Australia, -0001,Austria, -0002,Belgium, -0003,Canada, -0004,Chile, -0005,Colombia, -0006,Costa Rica, -0007,Czech Republic, -0008,Denmark, -0009,Estonia, -0010,Finland, -0011,France, -0012,Germany, -0013,Greece, -0014,Hungary, -0015,Iceland, -0016,Ireland, -0017,Israel, -0018,Italy, -0019,Japan, -0020,Korea, -0021,Latvia, -0022,Lithuania, -0023,Luxembourg, -0024,Netherlands, -0025,New Zealand, -0026,Norway, -0027,Poland, -0028,Portugal, -0029,Slovak Republic, -0030,Slovenia, -0031,Spain, -0032,Sweden, -0033,Switzerland, -0034,Türkiye, -0035,United Kingdom, -0036,United States, -0038,Mexico, -0040,Brazil, -0041,Russia, -0046,China, -0047,OECD Average, +0000,Australia,Australie +0001,Austria,Autriche +0002,Belgium,Belgique +0003,Canada,Canada +0004,Chile,Chili +0005,Colombia,Colombie +0006,Costa Rica,Costa Rica +0007,Czech Republic,République tchèque +0008,Denmark,Danemark +0009,Estonia,Estonie +0010,Finland,Finlande +0011,France,France +0012,Germany,Allemagne +0013,Greece,Grèce +0014,Hungary,Hongrie +0015,Iceland,Islande +0016,Ireland,Irlande +0017,Israel,Israël +0018,Italy,Italie +0019,Japan,Japon +0020,Korea,Korea +0021,Latvia,Lettonie +0022,Lithuania,Lituanie +0023,Luxembourg,Luxembourg +0024,Netherlands,Pays-Bas +0025,New Zealand,Nouvelle-Zélande +0026,Norway,Norvège +0027,Poland,Pologne +0028,Portugal,Portugal +0029,Slovak Republic,République slovaque +0030,Slovenia,Slovénie +0031,Spain,Espagne +0032,Sweden,Suède +0033,Switzerland, Suisse +0034,Türkiye, Turquie +0035,United Kingdom,Royaume-Uni +0036,United States,États-Unis +0038,Mexico,Mexique +0040,Brazil,Bresil +0041,Russia,Russie +0046,China,Chine +0047,OECD Average,Moyenne OCDE diff --git a/server/cpho/translations.py b/server/cpho/translations.py index 6c8a6d78..2edccaeb 100644 --- a/server/cpho/translations.py +++ b/server/cpho/translations.py @@ -1,4 +1,29 @@ translation_entries = { + "skip_to_main": { + "en": "Skip to main content", + "fr": "Passer au contenu principal", + }, + "required_explainer": { + "en": "Required fields are indicated with a red asterisk", + "fr": "Les champs obligatoires sont indiqués par une étoile rouge", + }, + "toggle_navigation": { + "en": "Toggle navigation", + "fr": "Basculer la navigation", + }, + "close": { + "en": "Close", + "fr": "Fermer", + }, + "breadcrumb_trail": {"en": "Breadcrumb trail", "fr": "Fil d'Ariane"}, + "please_select_an_option_below": { + "en": "Please select an option below", + "fr": "Veuillez sélectionner une option ci-dessous", + }, + "symbol_of_the_government_of_canada": { + "en": "Symbol of the Government of Canada", + "fr": "Symbole du gouvernement du Canada", + }, "site_title": { "en": "CPHO Phase 2", "fr": "DSPC Phase 2", @@ -40,6 +65,14 @@ "translated_using": "Edit", "fr_auto": "Modifier", }, + "create_new_user": { + "en": "Create new user", + "fr": "Créer un nouvel utilisateur", + }, + "indicator_directories": { + "en": "Indicator directories", + "fr": "Répertoires d'indicateurs", + }, "delete": { "en": "Delete", "fr": None, @@ -1471,6 +1504,14 @@ "en": "Hospital Setting Metadata", "fr": "Métadonnées de cadre hospitalier", }, + "export_benchmarking": { + "en": "Export Benchmarking", + "fr": "Exporter l'analyse comparative", + }, + "editor_for": { + "en": "Editor for", + "fr": "Éditeur pour", + }, "filter": { "en": "Filter", "fr": "Trier", diff --git a/server/cpho/util.py b/server/cpho/util.py index 2c949b4d..d7a096d1 100644 --- a/server/cpho/util.py +++ b/server/cpho/util.py @@ -6,6 +6,7 @@ from django.utils.translation import get_language import pytz +from data_fetcher import cache_within_request from cpho.constants import ADMIN_GROUP_NAME, HSO_GROUP_NAME from cpho.text import tdt, tm @@ -37,12 +38,12 @@ def __get__(self, cls, owner): class GroupFetcher: @classproperty - @lru_cache + @cache_within_request def admin_group(cls): return Group.objects.get_or_create(name=ADMIN_GROUP_NAME)[0] @classproperty - @lru_cache + @cache_within_request def hso_group(cls): return Group.objects.get_or_create(name=HSO_GROUP_NAME)[0] diff --git a/server/cpho/views/indicator_data.py b/server/cpho/views/indicator_data.py index ee895d1a..c990dc63 100644 --- a/server/cpho/views/indicator_data.py +++ b/server/cpho/views/indicator_data.py @@ -14,6 +14,8 @@ from phac_aspc.rules import test_rule +from server.form_util import DescribedByErrorMixin, StandardFormMixin + from cpho.constants import SUBMISSION_STATUSES from cpho.models import ( DimensionType, @@ -51,7 +53,11 @@ def get_form_kwargs(self, index): } -class IndicatorDatumForm(ModelForm): +class IndicatorDatumForm( + ModelForm, + StandardFormMixin, + DescribedByErrorMixin, +): class Meta: model = IndicatorDatum fields = [ @@ -428,8 +434,6 @@ def post(self, *args, **kwargs): ) else: # get will just render the forms and their errors - print(self.predefined_values_formset.errors) - print(self.age_group_formset.errors) messages.error(self.request, tm("error_saving_form")) return self.get(*args, **kwargs) diff --git a/server/cpho/views/indicator_metadata.py b/server/cpho/views/indicator_metadata.py index 4c2dc7cb..bb5410d1 100644 --- a/server/cpho/views/indicator_metadata.py +++ b/server/cpho/views/indicator_metadata.py @@ -17,6 +17,8 @@ from phac_aspc.rules import test_rule +from server.form_util import DescribedByErrorMixin, StandardFormMixin + from cpho.constants import SUBMISSION_STATUSES from cpho.models import ( Benchmarking, @@ -39,7 +41,12 @@ ) -class BenchmarkingForm(RequiredIfNotDeletedMixin, ModelForm): +class BenchmarkingForm( + RequiredIfNotDeletedMixin, + ModelForm, + DescribedByErrorMixin, + StandardFormMixin, +): class Meta: model = Benchmarking fields = [ @@ -298,7 +305,12 @@ def get_context_data(self, **kwargs): return context -class TrendAnalysisForm(RequiredIfNotDeletedMixin, ModelForm): +class TrendAnalysisForm( + RequiredIfNotDeletedMixin, + ModelForm, + StandardFormMixin, + DescribedByErrorMixin, +): class Meta: model = TrendAnalysis fields = [ diff --git a/server/cpho/views/indicators.py b/server/cpho/views/indicators.py index d94ca596..2290cc14 100644 --- a/server/cpho/views/indicators.py +++ b/server/cpho/views/indicators.py @@ -43,6 +43,18 @@ def label_from_instance(self, obj): return obj.name_en +class CustomCkEditorWidget(CKEditorWidget): + def __init__(self, *args, label=None, **kwargs): + super().__init__(*args, **kwargs) + + if not label: + raise Exception("Ckeditor requires an explicit label") + + title = tm("editor_for") + " : " + label + + self.config["title"] = title + + class IndicatorForm(ModelForm): class Meta: model = Indicator @@ -98,8 +110,10 @@ def ckEditorField(required=False, french=False, label=None): return forms.CharField( required=required, - widget=CKEditorWidget( - config_name="notes", attrs={"class": class_string} + widget=CustomCkEditorWidget( + config_name="notes", + label=label, + attrs={"class": class_string}, ), label=label if label else None, ) diff --git a/server/pytest.ini b/server/pytest.ini index 3daaa9de..2ba2e11d 100644 --- a/server/pytest.ini +++ b/server/pytest.ini @@ -1,4 +1,6 @@ [pytest] DJANGO_SETTINGS_MODULE= server.settings -addopts = -p no:warnings -v -s +addopts = -p no:warnings -v -s -m "not selenium" asyncio_mode = auto +markers = + selenium: end-to-end browser tests diff --git a/server/requirements.txt b/server/requirements.txt index 048e101b..0311310b 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -24,7 +24,7 @@ pytz==2022.2.1 requests==2.32.0 whitenoise==6.4.0 rules==3.3 -django-ckeditor==6.7.1 +django-ckeditor==6.7.3 PyYAML==6.0.1 # necessary to apply fixtures in prod diff --git a/server/requirements_selenium.txt b/server/requirements_selenium.txt new file mode 100644 index 00000000..81a73ea0 --- /dev/null +++ b/server/requirements_selenium.txt @@ -0,0 +1 @@ +selenium==4.39.0 diff --git a/server/server/form_util.py b/server/server/form_util.py index 04e39581..3ad86065 100644 --- a/server/server/form_util.py +++ b/server/server/form_util.py @@ -102,6 +102,33 @@ def translate_choices(choices): choices[i] = (value, _("no")) +class DescribedByErrorMixin(forms.Form): + """ + adds aria-describedby attributes for error messages + assumes error messages follow the field naming convention + """ + + def error_id_for_field(self, bound_field): + id_for_label = bound_field.id_for_label + error_id = f"{id_for_label}_errormessage" + return error_id + + def full_clean(self): + r = super().full_clean() + for bound_field in self.visible_fields(): + if self.errors.get(bound_field.name): + error_id = self.error_id_for_field(bound_field) + widget = bound_field.field.widget + current_aria_describedby = widget.attrs.get( + "aria-describedby", "" + ) + widget.attrs["aria-describedby"] = ( + current_aria_describedby + " " + error_id + ) + + return r + + class StandardFormMixin( FormControlMixin, NumberInputMixin, diff --git a/server/static/cpho.css b/server/static/cpho.css index 15bd7464..6eede980 100644 --- a/server/static/cpho.css +++ b/server/static/cpho.css @@ -160,10 +160,22 @@ span.new-ind-tag { width: 5% } -/* Accessibility */ -.btn:focus { - color: white; - background-color: #0535d2; + + +.form-control, +.form-select, +.phac_aspc_form_autocomplete.phac_aspc_form_autocomplete { + border-color: #8594A3; +} + + +.btn:focus, +.form-select:focus, +.form-control:focus, +.phac_aspc_form_autocomplete_focus_ring.active, +a:focus, +input[type="checkbox"]:focus, +input[type="radio"]:focus { outline-offset: 0.125rem; border-color: #0535d2; box-shadow: 0 0 0 0.125rem white; diff --git a/server/static/js/manage_benchmarking_scripts.js b/server/static/js/manage_benchmarking_scripts.js new file mode 100644 index 00000000..57717c44 --- /dev/null +++ b/server/static/js/manage_benchmarking_scripts.js @@ -0,0 +1,63 @@ +let addButton = document.querySelector("#add-form"); +if (addButton) { + addButton.addEventListener("click", addForm); +} + +function addForm(e) { + let forms = document.querySelectorAll( + ".benchmarking-form-list > .benchmarking-form" + ); + let newFormIndex = forms.length; + let numForms = forms.length + 1; + let formTemplateNode = document.querySelector( + ".benchmarking-empty-form-container" + ); + let container = document.querySelector(".benchmarking-form-list"); + let totalFormsInput = document.querySelector("#id_benchmarking-TOTAL_FORMS"); //management form inputs + + e.preventDefault(); + let formRegex = RegExp(`benchmarking-__prefix__-`, "g"); + console.log(formRegex); + let newFormHtml = formTemplateNode.innerHTML.replace( + formRegex, + `benchmarking-${newFormIndex}-` + ); + + let newForm = document.createElement("tr"); + newForm.classList.add("benchmarking-form"); + newForm.innerHTML = newFormHtml; + container.appendChild(newForm); + totalFormsInput.setAttribute("value", `${numForms}`); + + // focus on first input of newly added form + newForm.querySelector("select,input").focus(); + updateInvisibleRowLabels(); +} + + +let rowWord = "Entry"; +if (window.location.href.includes("fr-ca")) { + rowWord = "Entrée"; +} +function updateInvisibleRowLabels() { + document.querySelectorAll(".benchmarking-form").forEach(function (row) { + const countrySelector = row.querySelector( + 'select[name$="-oecd_country"]' + ) + const countryText = countrySelector.options[countrySelector.selectedIndex].text; + const rowLabelSpan = row.querySelector('span[id$="-row-label"] .country-name'); + if (countryText.replaceAll("-", "")) { + rowLabelSpan.textContent = countryText; + } else { + rowIndex = Array.from(row.parentNode.children).indexOf(row) + 1; + rowLabelSpan.textContent = `${rowWord} ${rowIndex}`; + } + }); +} + + +updateInvisibleRowLabels(); + +document.addEventListener("change", function (e) { + updateInvisibleRowLabels(); +}); diff --git a/server/static/js/manage_trend_analysis_script.js b/server/static/js/manage_trend_analysis_script.js new file mode 100644 index 00000000..3c3a55a1 --- /dev/null +++ b/server/static/js/manage_trend_analysis_script.js @@ -0,0 +1,63 @@ +let addButton = document.querySelector("#add-form"); +addButton.addEventListener("click", addForm); + +function addForm(e) { + let forms = document.querySelectorAll( + ".trend-analysis-form-list > .trend-analysis-form" + ); + let newFormIndex = forms.length; + let numForms = forms.length + 1; + let formTemplateNode = document.querySelector( + ".trend-analysis-empty-form-container" + ); + let container = document.querySelector(".trend-analysis-form-list"); + let totalFormsInput = document.querySelector( + "#id_trend_analysis-TOTAL_FORMS" + ); //management form inputs + + e.preventDefault(); + let formRegex = RegExp(`trend_analysis-__prefix__-`, "g"); + console.log(formRegex); + let newFormHtml = formTemplateNode.innerHTML.replace( + formRegex, + `trend_analysis-${newFormIndex}-` + ); + + let newForm = document.createElement("tr"); + newForm.classList.add("trend-analysis-form"); + newForm.innerHTML = newFormHtml; + container.appendChild(newForm); + totalFormsInput.setAttribute("value", `${numForms}`); + + // a11y: focus on first input of newly added form + newForm.querySelector("select,input").focus(); + updateInvisibleRowLabels(); +} + + +let rowWord = "Entry"; +if (window.location.href.includes("fr-ca")) { + rowWord = "Entrée"; +} +function updateInvisibleRowLabels() { + document.querySelectorAll(".trend-analysis-form").forEach(function (row) { + const yearInput = row.querySelector( + 'input[name$="-year"]' + ) + const yearText = yearInput.value; + const rowLabelSpan = row.querySelector('span[id$="-row-label"] .year-key'); + if (yearText.trim()) { + rowLabelSpan.textContent = yearText; + } else { + rowIndex = Array.from(row.parentNode.children).indexOf(row) + 1; + rowLabelSpan.textContent = `${rowWord} ${rowIndex}`; + } + }); +} + + +updateInvisibleRowLabels(); + +document.addEventListener("change", function (e) { + updateInvisibleRowLabels(); +}); diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 8ac11005..e44f0bac 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import Group from django.core.management import call_command from django.db import transaction from django.test.client import Client @@ -6,9 +7,9 @@ from jinja2 import Template as Jinja2Template from phac_aspc.django.settings.utils import configure_settings_for_tests +from cpho import constants from cpho.management.commands.seed_countries import seed_countries from cpho.models import User -from cpho.util import GroupFetcher # Modify django settings to skip axes authentication backend configure_settings_for_tests() @@ -24,26 +25,40 @@ def enable_db_access_for_all_tests(db): @pytest.fixture(scope="session") -def globally_scoped_fixture_helper(django_db_setup, django_db_blocker): - with django_db_blocker.unblock(): - # Wrap in try + atomic block to do non crashing rollback - # This means we don't have to re-create a test DB each time - try: - with transaction.atomic(): - yield - raise Exception - except Exception: - pass +def globally_scoped_fixture_helper( + django_db_setup, django_db_blocker, request +): + + # If ANY collected test is marked selenium, don't do the global atomic trick. + if ( + request.config.getoption("-m", default="") + and "selenium" in request.config.getoption("-m") + and "not selenium" not in request.config.getoption("-m") + ): + print("Activating selenium transaction mode") + with django_db_blocker.unblock(): + yield + return + + else: + with django_db_blocker.unblock(): + # Wrap in try + atomic block to do non crashing rollback + # This means we don't have to re-create a test DB each time + try: + with transaction.atomic(): + yield + raise Exception + except Exception: + pass @pytest.fixture(scope="session", autouse=True) -def seed_core_data(globally_scoped_fixture_helper): +def seed_core_data_fixture(globally_scoped_fixture_helper): + print("Seeding core data...") call_command("loaddata", "cpho/fixtures/periods.yaml") call_command("loaddata", "cpho/fixtures/dimension_lookups.yaml") seed_countries(mode="reset") - # ensure groups created (caching groups break accross tests if not pre-created) - GroupFetcher.hso_group - GroupFetcher.admin_group + Group.objects.get_or_create(name=constants.ADMIN_GROUP_NAME) @pytest.fixture @@ -64,7 +79,8 @@ def hso_user(): username="hso", password="hso", ) - hso.groups.add(GroupFetcher.hso_group) + group, _ = Group.objects.get_or_create(name=constants.HSO_GROUP_NAME) + hso.groups.add(group) return hso diff --git a/server/tests/pytest_test_runner.py b/server/tests/pytest_test_runner.py index f5ba686f..2cd0ad04 100644 --- a/server/tests/pytest_test_runner.py +++ b/server/tests/pytest_test_runner.py @@ -12,18 +12,26 @@ def add_arguments(cls, parser): help="remaps to -k test-selection argument in pytest", ) + parser.add_argument( + "--selenium", + action="store_true", + help="run selenium tests", + ) + def __init__( self, verbosity=1, failfast=False, keepdb=True, select=None, + selenium=False, **kwargs, ): self.verbosity = verbosity self.failfast = failfast self.keepdb = keepdb self.select = select + self.selenium = selenium def run_tests(self, test_labels): """Run pytest and return the exitcode. @@ -45,6 +53,8 @@ def run_tests(self, test_labels): argv.append("--exitfirst") if self.keepdb: argv.append("--reuse-db") + if self.selenium: + argv.append("-m selenium") argv.extend(test_labels) return pytest.main(argv) diff --git a/server/tests/selenium/__init__.py b/server/tests/selenium/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/server/tests/selenium/conftest.py b/server/tests/selenium/conftest.py new file mode 100644 index 00000000..0508b494 --- /dev/null +++ b/server/tests/selenium/conftest.py @@ -0,0 +1,56 @@ +from django.conf import settings +from django.contrib.auth import ( + BACKEND_SESSION_KEY, + HASH_SESSION_KEY, + SESSION_KEY, +) +from django.contrib.sessions.backends.db import SessionStore + +import pytest + + +# override the global autouse fixture just for selenium tests in this package +# see tests/conftest.py's enable_db_access_for_all_tests +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(transactional_db): + pass + + +@pytest.fixture(scope="session") +def driver(): + from selenium import webdriver + + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + d = webdriver.Chrome(options=options) + yield d + d.quit() + + +@pytest.fixture +def force_login(driver, live_server): + """ + Returns a function: force_login(user) that logs the browser in by setting session cookie. + """ + + def _force_login(user): + # 1) create a session in Django + session = SessionStore() + session[SESSION_KEY] = str(user.pk) + session[BACKEND_SESSION_KEY] = settings.AUTHENTICATION_BACKENDS[0] + session[HASH_SESSION_KEY] = user.get_session_auth_hash() + session.save() + + # 2) visit domain once so Selenium is allowed to set cookies + driver.get(live_server.url + "/404-does-not-exist") + + # 3) set the session cookie in the browser + driver.add_cookie( + { + "name": settings.SESSION_COOKIE_NAME, + "value": session.session_key, + "path": "/", + } + ) + + return _force_login diff --git a/server/tests/selenium/selenium_util.py b/server/tests/selenium/selenium_util.py new file mode 100644 index 00000000..992ccf89 --- /dev/null +++ b/server/tests/selenium/selenium_util.py @@ -0,0 +1,17 @@ +def get_element(driver, selector): + from selenium.webdriver.common.by import By + + return driver.find_element(By.CSS_SELECTOR, selector) + + +def get_elements(driver, selector): + from selenium.webdriver.common.by import By + + return driver.find_elements(By.CSS_SELECTOR, selector) + + +def click_button(driver, selector): + # just using element.click() can fail if the element is not in view + element = get_element(driver, selector) + driver.execute_script("arguments[0].scrollIntoView();", element) + driver.execute_script("arguments[0].click();", element) diff --git a/server/tests/selenium/test_dynamic_formsets.py b/server/tests/selenium/test_dynamic_formsets.py new file mode 100644 index 00000000..89e3b9ab --- /dev/null +++ b/server/tests/selenium/test_dynamic_formsets.py @@ -0,0 +1,104 @@ +# this is a bunch of meta tests + +from django.urls import reverse + +import pytest + +from cpho.model_factories import IndicatorFactory +from cpho.models import DimensionType, Indicator, Period + +from .selenium_util import click_button, get_element, get_elements + +# tag the whole module +pytestmark = [ + pytest.mark.selenium, + pytest.mark.django_db(transaction=True), +] + + +def test_dynamic_agegroup_formset(live_server, driver, hso_user, force_login): + + period = Period.objects.first() + ind = IndicatorFactory() + age_group_dim = DimensionType.objects.get(code="age") + + record0_25 = ind.data.create( + period=period, + dimension_type=age_group_dim, + literal_dimension_val="0-25", + value=5, + ) + + url = reverse( + "manage_indicator_data", args=[ind.id, period.id, age_group_dim.pk] + ) + + force_login(hso_user) + + driver.get(live_server.url + url) + + assert "CPHO Phase 2" in driver.title + + original_form_count = len( + get_elements(driver, "form table tr.age-group-form") + ) + assert original_form_count == 2 # one existing + one empty form + + click_button(driver, "#add-form") + + new_form_count = len(get_elements(driver, "form table tr.age-group-form")) + assert new_form_count == original_form_count + 1 + + +def test_dynamic_trend_formset(live_server, driver, hso_user, force_login): + + period = Period.objects.first() + ind = IndicatorFactory() + + url = reverse("manage_trend_analysis_data", args=[ind.id]) + + force_login(hso_user) + + driver.get(live_server.url + url) + + assert "CPHO Phase 2" in driver.title + + original_form_count = len( + get_elements(driver, "form table tr.trend-analysis-form") + ) + assert original_form_count == 1 # just an empty form + + click_button(driver, "#add-form") + + new_form_count = len( + get_elements(driver, "form table tr.trend-analysis-form") + ) + assert new_form_count == original_form_count + 1 + + +def test_dynamic_benchmarking_formset( + live_server, driver, hso_user, force_login +): + + period = Period.objects.first() + ind = IndicatorFactory() + + url = reverse("manage_benchmarking_data", args=[ind.id]) + + force_login(hso_user) + + driver.get(live_server.url + url) + + assert "CPHO Phase 2" in driver.title + + original_form_count = len( + get_elements(driver, "form table tr.benchmarking-form") + ) + assert original_form_count == 1 # just an empty form + + click_button(driver, "#add-form") + + new_form_count = len( + get_elements(driver, "form table tr.benchmarking-form") + ) + assert new_form_count == original_form_count + 1 diff --git a/server/tests/selenium/test_indicator_form_interaction.py b/server/tests/selenium/test_indicator_form_interaction.py new file mode 100644 index 00000000..44f23cf1 --- /dev/null +++ b/server/tests/selenium/test_indicator_form_interaction.py @@ -0,0 +1,49 @@ +# this is a bunch of meta tests + +from django.urls import reverse + +import pytest + +from cpho.model_factories import IndicatorFactory +from cpho.models import DimensionType, Indicator, Period + +from .selenium_util import click_button, get_element, get_elements + +# tag the whole module +pytestmark = [ + pytest.mark.selenium, + pytest.mark.django_db(transaction=True), +] + + +def test_indicator_form_interaction( + live_server, driver, hso_user, force_login +): + ind = IndicatorFactory() + url = reverse("edit_indicator", args=[ind.id]) + + force_login(hso_user) + + driver.get(live_server.url + url) + + assert "CPHO Phase 2" in driver.title + + english_name_input = get_element(driver, "#id_name") + french_name_input = get_element(driver, "#id_name_fr") + fr_visibility_toggle = get_element(driver, "#show_french") + # check toggle is not checked + assert not fr_visibility_toggle.get_property("checked") + + # check enlish visible + assert english_name_input.is_displayed() + + # check french hidden + assert not french_name_input.is_displayed() + + click_button(driver, "#show_french") + # check toggle is checked + assert fr_visibility_toggle.get_property("checked") + # check french visible + assert french_name_input.is_displayed() + # check english still visible + assert english_name_input.is_displayed() diff --git a/server/tests/selenium/test_selenium_configuration.py b/server/tests/selenium/test_selenium_configuration.py new file mode 100644 index 00000000..08f78871 --- /dev/null +++ b/server/tests/selenium/test_selenium_configuration.py @@ -0,0 +1,34 @@ +# this is a bunch of meta tests + +from django.urls import reverse + +import pytest + +from cpho.models import User + +# tag the whole module +pytestmark = [ + pytest.mark.selenium, + pytest.mark.django_db(transaction=True), +] + + +# first, check that tests are working with independent DBs +def test_selenium_isolated_db_one(live_server, driver): + u = User.objects.create(username="testuser") + + +def test_selenium_isolated_db_two(live_server, driver): + # without isolation, this would fail uniqueness constraint + u = User.objects.create(username="testuser") + + +def test_login_works(live_server, driver, hso_user, force_login): + force_login(hso_user) + + driver.get(live_server.url + reverse("list_indicators")) + # result = driver.get(live_server.url + reverse("list_users")) + # div = driver.find_element(By.TAG_NAME, "div") + + assert "CPHO Phase 2" in driver.title + assert "login" not in driver.title.lower()