diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index fe461b4..d19e21b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,20 +1,39 @@ # Dependency Review Action # -# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable +# packages will be blocked from merging. # # Source repository: https://github.com/actions/dependency-review-action # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement -name: 'Dependency Review' -on: [pull_request] +name: 'Dependency review' +on: + pull_request: + branches: [ "main" ] +# If using a dependency submission action in this workflow this permission will need to be set to: +# +# permissions: +# contents: write +# +# https://docs.github.com/en/enterprise-cloud@latest/code-security/supply-chain-security/understanding-your-software-supply-chain/using-the-dependency-submission-api permissions: contents: read + # Write permissions for pull-requests are required for using the `comment-summary-in-pr` option, comment out if you aren't using this option + pull-requests: write jobs: dependency-review: runs-on: ubuntu-latest steps: - - name: 'Checkout Repository' - uses: actions/checkout@v3 + - name: 'Checkout repository' + uses: actions/checkout@v4 - name: 'Dependency Review' - uses: actions/dependency-review-action@v2 + uses: actions/dependency-review-action@v4 + # Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options. + with: + comment-summary-in-pr: always + # fail-on-severity: moderate + # deny-licenses: GPL-1.0-or-later, LGPL-2.0-or-later + # retry-on-snapshot-warnings: true diff --git a/govuk_frontend_wtf/gov_form_base.py b/govuk_frontend_wtf/gov_form_base.py index 2941a02..f35f4cb 100644 --- a/govuk_frontend_wtf/gov_form_base.py +++ b/govuk_frontend_wtf/gov_form_base.py @@ -28,7 +28,7 @@ def map_gov_params(self, field, **kwargs): "name": field.name, "label": {"text": field.label.text}, "attributes": {}, - "hint": {"text": field.description}, + "hint": {"text": field.description} if field.description else None, } if "value" in kwargs: diff --git a/govuk_frontend_wtf/main.py b/govuk_frontend_wtf/main.py index 81e60a4..dd76f9b 100644 --- a/govuk_frontend_wtf/main.py +++ b/govuk_frontend_wtf/main.py @@ -20,21 +20,29 @@ def init_app(self, app): def wtforms_errors(form, params={}): wtforms_params = {"titleText": "There is a problem", "errorList": []} - wtforms_params["errorList"] = flatten_errors(form.errors) + id_map = {} + for field_name in form._fields.keys(): + field = getattr(form, field_name, None) + if field and hasattr(field, "id"): + id_map[field_name] = field.id + + wtforms_params["errorList"] = flatten_errors(form.errors, id_map=id_map) return merger.merge(wtforms_params, params) -def flatten_errors(errors, prefix=""): +def flatten_errors(errors, prefix="", id_map={}): """Return list of errors from form errors.""" error_list = [] if isinstance(errors, dict): for key, value in errors.items(): # Recurse to handle subforms. - error_list += flatten_errors(value, prefix=f"{prefix}{key}-") + if key in id_map: + key = id_map[key] + error_list += flatten_errors(value, prefix=f"{prefix}{key}-", id_map=id_map) elif isinstance(errors, list) and isinstance(errors[0], dict): for idx, error in enumerate(errors): - error_list += flatten_errors(error, prefix=f"{prefix}{idx}-") + error_list += flatten_errors(error, prefix=f"{prefix}{idx}-", id_map=id_map) elif isinstance(errors, list): error_list.append({"text": errors[0], "href": "#{}".format(prefix.rstrip("-"))}) else: diff --git a/tests/fixtures/wtf_widgets_data.yaml b/tests/fixtures/wtf_widgets_data.yaml index 8c01270..d378d06 100644 --- a/tests/fixtures/wtf_widgets_data.yaml +++ b/tests/fixtures/wtf_widgets_data.yaml @@ -37,6 +37,44 @@ TestStringField: -
-
\s*StringFieldHint\s*
+TestStringFieldId: + template: "{{ form.string_field_id }}" + tests: + test_empty_get: + expected_output: + - + - + -
\s*StringFieldHint\s*
+ test_output_sanitized: + request: + method: post + data: + string_field_id: + expected_output: + - + not_expected_output: + - + test_valid_post: + request: + method: post + data: + string_field_id: John Smith + expected_output: + - + - + -
\s*StringFieldHint\s*
+ test_invalid_post: + request: + method: post + data: + string_field_id: foo + expected_output: + - + - + -

\s*\s*Error:\s*\s*Example serverside error - type "John Smith" into this field to suppress it\s*

+ -
+ -
\s*StringFieldHint\s*
+ TestDateField: template: "{{ form.date_field }}" tests: @@ -585,6 +623,23 @@ TestRadioField: - - -
\s*RadioFieldHint\s*
+ test_empty_description: + request: + method: get + data: + radio_field_no_description: foo + expected_output: + -
+ -
+ -
+ - + - + - + - label class="govuk-label govuk-radios__label" for="radio_field-2">\s*Two\s* + - + - + not_expected_output: + -
TestFileField: template: "{{ form.file_field }}" @@ -729,6 +784,7 @@ TestErrorSummary: -
-

\s*There is a problem\s*

- StringField is required + - StringField is required - Date is required - IntegerField is required - Please select an option @@ -749,6 +805,7 @@ TestErrorSummary: method: post data: string_field: John Smith + string_field_id: John Smith nested_form-0-string_field: John Smith date_field: - 1 @@ -770,6 +827,7 @@ TestErrorSummary: - foo - bar radio_field: one + radio_field_no_description: one select_multiple_field: one textarea_field: foo charactercount_field: foo @@ -782,6 +840,7 @@ TestErrorSummary: method: post data: string_field: Andy + string_field_id: Andy date_field: - 1 - 1 @@ -798,6 +857,7 @@ TestErrorSummary: - foo - bar radio_field: one + radio_field_no_description: one select_multiple_field: one textarea_field: foo charactercount_field: foo diff --git a/tests/fixtures/wtf_widgets_example_form.py b/tests/fixtures/wtf_widgets_example_form.py index 0805207..68f41da 100644 --- a/tests/fixtures/wtf_widgets_example_form.py +++ b/tests/fixtures/wtf_widgets_example_form.py @@ -54,6 +54,14 @@ class ExampleForm(FlaskForm): description="StringFieldHint", ) + string_field_id = StringField( + "StringField", + id="custom-id", + widget=GovTextInput(), + validators=[InputRequired(message="StringField is required")], + description="StringFieldHint", + ) + date_field = DateField( "DateField", format="%d %m %Y", @@ -153,6 +161,13 @@ class ExampleForm(FlaskForm): description="RadioFieldHint", ) + radio_field_no_description = RadioField( + "RadioField", + widget=GovRadioInput(), + validators=[InputRequired(message="Please select an option")], + choices=[("one", "One"), ("two", "Two"), ("three", "Three")], + ) + file_field = FileField( "FileField", widget=GovFileInput(), @@ -197,3 +212,7 @@ class ExampleForm(FlaskForm): def validate_string_field(self, field): if field.data != "John Smith": raise ValidationError('Example serverside error - type "John Smith" into this field to suppress it') + + def validate_string_field_id(self, field): + if field.data != "John Smith": + raise ValidationError('Example serverside error - type "John Smith" into this field to suppress it') diff --git a/tests/requirements.in b/tests/requirements.in index ca19365..5a8e255 100644 --- a/tests/requirements.in +++ b/tests/requirements.in @@ -1,7 +1,7 @@ -deepmerge==1.1.0 +deepmerge==1.1.1 email_validator==2.0.0.post2 -flask-wtf==1.1.1 -flask==2.3.2 -govuk-frontend-jinja==2.7.0 +flask-wtf==1.1.2 +flask==2.3.3 +govuk-frontend-jinja==2.8.0 pytest-cov==4.1.0 -pyyaml==6.0 +pyyaml==6.0.1 diff --git a/tests/requirements.txt b/tests/requirements.txt index cbc8821..95f9c02 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile requirements.in @@ -9,20 +9,22 @@ blinker==1.6.2 click==8.1.4 # via flask coverage[toml]==7.2.7 - # via pytest-cov -deepmerge==1.1.0 + # via + # coverage + # pytest-cov +deepmerge==1.1.1 # via -r requirements.in dnspython==2.3.0 # via email-validator email-validator==2.0.0.post2 # via -r requirements.in -flask==2.3.2 +flask==2.3.3 # via # -r requirements.in # flask-wtf -flask-wtf==1.1.1 +flask-wtf==1.1.2 # via -r requirements.in -govuk-frontend-jinja==2.7.0 +govuk-frontend-jinja==2.8.0 # via -r requirements.in idna==3.4 # via email-validator @@ -49,9 +51,9 @@ pytest==7.4.0 # via pytest-cov pytest-cov==4.1.0 # via -r requirements.in -pyyaml==6.0 +pyyaml==6.0.1 # via -r requirements.in -werkzeug==2.3.6 +werkzeug==3.0.1 # via flask wtforms==3.0.1 # via flask-wtf