From 4b83f839eb8c8e782098851741b1dc09616f5ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rueda=20L=C3=B3pez?= Date: Mon, 29 Jul 2024 17:01:00 +0200 Subject: [PATCH 01/22] Pagination and search on conflicts page (#13154) * Included conflicts pagination and search * Added test * Lint * Completed test * Fixed view and lint * Improved test * Lint * Deleted partials --- .../verification_conflicts/filterable.rb | 31 ++++++++++ .../decidim/admin/conflicts_controller.rb | 18 ++++-- .../decidim/admin/conflicts/index.html.erb | 35 +++++++++-- decidim-admin/config/locales/en.yml | 2 + .../system/admin_checks_conflicts_spec.rb | 58 +++++++++++++++++++ 5 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 decidim-admin/app/controllers/concerns/decidim/admin/verification_conflicts/filterable.rb create mode 100644 decidim-admin/spec/system/admin_checks_conflicts_spec.rb diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/verification_conflicts/filterable.rb b/decidim-admin/app/controllers/concerns/decidim/admin/verification_conflicts/filterable.rb new file mode 100644 index 0000000000000..cb2937b676a8a --- /dev/null +++ b/decidim-admin/app/controllers/concerns/decidim/admin/verification_conflicts/filterable.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Admin + module VerificationConflicts + module Filterable + extend ActiveSupport::Concern + + included do + include Decidim::Admin::Filterable + + private + + def base_query + collection + end + + def search_field_predicate + :current_user_name_or_current_user_nickname_or_current_user_email_cont + end + + def filters + [] + end + end + end + end + end +end diff --git a/decidim-admin/app/controllers/decidim/admin/conflicts_controller.rb b/decidim-admin/app/controllers/decidim/admin/conflicts_controller.rb index a3c7881737bfc..5533813993cfa 100644 --- a/decidim-admin/app/controllers/decidim/admin/conflicts_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/conflicts_controller.rb @@ -3,18 +3,16 @@ module Decidim module Admin class ConflictsController < Decidim::Admin::ApplicationController + include Decidim::Admin::VerificationConflicts::Filterable + layout "decidim/admin/users" - helper_method :context_breadcrumb_items + helper_method :context_breadcrumb_items, :conflicts add_breadcrumb_item_from_menu :impersonate_menu def index enforce_permission_to :index, :impersonatable_user - - @conflicts = Decidim::Verifications::Conflict.joins(:current_user).where( - decidim_users: { decidim_organization_id: current_organization.id } - ) end def edit @@ -66,6 +64,16 @@ def impersonations_breadcrumb_item url: decidim_admin.impersonatable_users_path } end + + def collection + @collection ||= Decidim::Verifications::Conflict.joins(:current_user).where( + decidim_users: { decidim_organization_id: current_organization.id } + ) + end + + def conflicts + @conflicts ||= filtered_collection.order(created_at: :desc) + end end end end diff --git a/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb b/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb index ac81afc7dfcd1..16117766de80e 100644 --- a/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb +++ b/decidim-admin/app/views/decidim/admin/conflicts/index.html.erb @@ -1,10 +1,32 @@ <% add_decidim_page_title(t("title", scope: "decidim.admin.conflicts")) %> -
-
-

- <%= t("title", scope: "decidim.admin.conflicts") %> -

+
+

+ <%= t("title", scope: "decidim.admin.conflicts") %> +

+
+ +
+ +
+ +
@@ -17,7 +39,7 @@ - <% @conflicts.each do |conflict| %> + <% conflicts.each do |conflict| %> @@ -30,3 +52,4 @@
<%= conflict.current_user.name %> <%= conflict.managed_user.name %>
+<%= decidim_paginate conflicts %> diff --git a/decidim-admin/config/locales/en.yml b/decidim-admin/config/locales/en.yml index b0cf812736638..8644aca3caaef 100644 --- a/decidim-admin/config/locales/en.yml +++ b/decidim-admin/config/locales/en.yml @@ -351,6 +351,8 @@ en: conflicts: attempts: Attempts 'false': 'No' + index: + text: Search by current user email, name or nickname. managed_user_name: Managed User solved: Solved title: Verification conflicts diff --git a/decidim-admin/spec/system/admin_checks_conflicts_spec.rb b/decidim-admin/spec/system/admin_checks_conflicts_spec.rb new file mode 100644 index 0000000000000..461bbf1e62a5c --- /dev/null +++ b/decidim-admin/spec/system/admin_checks_conflicts_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin checks conflicts" do + let(:organization) { create(:organization) } + let(:resource_controller) { Decidim::Admin::ConflictsController } + + let!(:user) { create(:user, :admin, :confirmed, organization:) } + let!(:first_conflictive_user) { create(:user, :admin, :confirmed, organization:) } + let!(:second_conflictive_user) { create(:user, :admin, :confirmed, organization:) } + + let!(:first_user_conflicts) { create_list(:conflict, 15, current_user: first_conflictive_user) } + let!(:second_user_conflicts) { create_list(:conflict, 15, current_user: second_conflictive_user) } + + before do + switch_to_host(organization.host) + login_as user, scope: :user + visit decidim_admin.root_path + click_on "Participants" + click_on "Verification conflicts" + end + + include_context "with filterable context" + + context "when listing conflicts" do + before { visit current_path } + + it_behaves_like "paginating a collection" do + let!(:collection) { create_list(:conflict, 50, current_user: first_conflictive_user) } + end + end + + context "when searching by current user name, nickname or email" do + before { visit current_path } + + it "can be searched by name" do + search_by_text(first_conflictive_user.name) + + expect(page).to have_content(first_conflictive_user.name) + expect(page).to have_no_content(second_conflictive_user.name) + end + + it "can be searched by nickname" do + search_by_text(first_conflictive_user.nickname) + + expect(page).to have_content(first_conflictive_user.name) + expect(page).to have_no_content(second_conflictive_user.name) + end + + it "can be searched by email" do + search_by_text(first_conflictive_user.email) + + expect(page).to have_content(first_conflictive_user.name) + expect(page).to have_no_content(second_conflictive_user.name) + end + end +end From b44e500971ace7ca3efdada37c3a85e34d55ea89 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Mon, 29 Jul 2024 18:48:20 +0300 Subject: [PATCH 02/22] Set correct method for `secret_key_base` (#13229) --- decidim-core/app/forms/decidim/omniauth_registration_form.rb | 2 +- decidim-core/lib/decidim/attribute_encryptor.rb | 2 +- decidim-core/lib/decidim/newsletter_encryptor.rb | 2 +- decidim-core/lib/decidim/paddable.rb | 2 +- decidim-core/spec/lib/paddable_spec.rb | 2 +- ...20190315203056_add_session_token_to_decidim_forms_answers.rb | 2 +- decidim-initiatives/app/forms/decidim/initiatives/vote_form.rb | 2 +- .../app/services/decidim/initiatives/data_encryptor.rb | 2 +- decidim-system/app/cells/decidim/system/system_checks_cell.rb | 2 +- .../app/forms/decidim/system/base_organization_form.rb | 2 +- .../app/forms/decidim/verifications/sms/mobile_phone_form.rb | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/decidim-core/app/forms/decidim/omniauth_registration_form.rb b/decidim-core/app/forms/decidim/omniauth_registration_form.rb index 4ea5e5dc36ba5..be3c0da669079 100644 --- a/decidim-core/app/forms/decidim/omniauth_registration_form.rb +++ b/decidim-core/app/forms/decidim/omniauth_registration_form.rb @@ -21,7 +21,7 @@ class OmniauthRegistrationForm < Form validates :uid, presence: true def self.create_signature(provider, uid) - Digest::MD5.hexdigest("#{provider}-#{uid}-#{Rails.application.secrets.secret_key_base}") + Digest::MD5.hexdigest("#{provider}-#{uid}-#{Rails.application.secret_key_base}") end def normalized_nickname diff --git a/decidim-core/lib/decidim/attribute_encryptor.rb b/decidim-core/lib/decidim/attribute_encryptor.rb index 58a167dae3aa0..80564a5c068f6 100644 --- a/decidim-core/lib/decidim/attribute_encryptor.rb +++ b/decidim-core/lib/decidim/attribute_encryptor.rb @@ -21,7 +21,7 @@ def self.decrypt(string_encrypted) def self.cryptor @cryptor ||= begin key = ActiveSupport::KeyGenerator.new("attribute").generate_key( - Rails.application.secrets.secret_key_base, ActiveSupport::MessageEncryptor.key_len + Rails.application.secret_key_base, ActiveSupport::MessageEncryptor.key_len ) ActiveSupport::MessageEncryptor.new(key) end diff --git a/decidim-core/lib/decidim/newsletter_encryptor.rb b/decidim-core/lib/decidim/newsletter_encryptor.rb index 5908481c332e5..679502d265f35 100644 --- a/decidim-core/lib/decidim/newsletter_encryptor.rb +++ b/decidim-core/lib/decidim/newsletter_encryptor.rb @@ -14,7 +14,7 @@ def self.sent_at_decrypted(string_encrypted) def self.crypt_data key = ActiveSupport::KeyGenerator.new("sent_at").generate_key( - Rails.application.secrets.secret_key_base, ActiveSupport::MessageEncryptor.key_len + Rails.application.secret_key_base, ActiveSupport::MessageEncryptor.key_len ) ActiveSupport::MessageEncryptor.new(key) end diff --git a/decidim-core/lib/decidim/paddable.rb b/decidim-core/lib/decidim/paddable.rb index 21ef3aaf2357c..19b69ceb77ff7 100644 --- a/decidim-core/lib/decidim/paddable.rb +++ b/decidim-core/lib/decidim/paddable.rb @@ -66,7 +66,7 @@ def token return tokenizer.hex_digest(id) end - Digest::MD5.hexdigest("#{id}-#{Rails.application.secrets.secret_key_base}") + Digest::MD5.hexdigest("#{id}-#{Rails.application.secret_key_base}") end def build_pad_url(id) diff --git a/decidim-core/spec/lib/paddable_spec.rb b/decidim-core/spec/lib/paddable_spec.rb index 09acfccd9ca11..86225ec6de1d1 100644 --- a/decidim-core/spec/lib/paddable_spec.rb +++ b/decidim-core/spec/lib/paddable_spec.rb @@ -111,7 +111,7 @@ module Decidim end def unsecure_hash(id) - Digest::MD5.hexdigest("#{id}-#{Rails.application.secrets.secret_key_base}") + Digest::MD5.hexdigest("#{id}-#{Rails.application.secret_key_base}") end def secure_hash(id) diff --git a/decidim-forms/db/migrate/20190315203056_add_session_token_to_decidim_forms_answers.rb b/decidim-forms/db/migrate/20190315203056_add_session_token_to_decidim_forms_answers.rb index 2e025e9423d91..ad8603540e520 100644 --- a/decidim-forms/db/migrate/20190315203056_add_session_token_to_decidim_forms_answers.rb +++ b/decidim-forms/db/migrate/20190315203056_add_session_token_to_decidim_forms_answers.rb @@ -10,7 +10,7 @@ def change add_index :decidim_forms_answers, :session_token Answer.find_each do |answer| - answer.session_token = Digest::MD5.hexdigest("#{answer.decidim_user_id}-#{Rails.application.secrets.secret_key_base}") + answer.session_token = Digest::MD5.hexdigest("#{answer.decidim_user_id}-#{Rails.application.secret_key_base}") answer.save! end end diff --git a/decidim-initiatives/app/forms/decidim/initiatives/vote_form.rb b/decidim-initiatives/app/forms/decidim/initiatives/vote_form.rb index 25f0754b65ec0..c9449d6a19305 100644 --- a/decidim-initiatives/app/forms/decidim/initiatives/vote_form.rb +++ b/decidim-initiatives/app/forms/decidim/initiatives/vote_form.rb @@ -48,7 +48,7 @@ def hash_id [ initiative.id, document_number || signer.id, - Rails.application.secrets.secret_key_base + Rails.application.secret_key_base ].compact.join("-") ) end diff --git a/decidim-initiatives/app/services/decidim/initiatives/data_encryptor.rb b/decidim-initiatives/app/services/decidim/initiatives/data_encryptor.rb index 997916e206ac8..ab05aafa92724 100644 --- a/decidim-initiatives/app/services/decidim/initiatives/data_encryptor.rb +++ b/decidim-initiatives/app/services/decidim/initiatives/data_encryptor.rb @@ -9,7 +9,7 @@ class DataEncryptor def initialize(args = {}) @secret = args.fetch(:secret) || "default" @key = ActiveSupport::KeyGenerator.new(secret).generate_key( - Rails.application.secrets.secret_key_base, ActiveSupport::MessageEncryptor.key_len + Rails.application.secret_key_base, ActiveSupport::MessageEncryptor.key_len ) @encryptor = ActiveSupport::MessageEncryptor.new(@key) end diff --git a/decidim-system/app/cells/decidim/system/system_checks_cell.rb b/decidim-system/app/cells/decidim/system/system_checks_cell.rb index 658c891248d16..24970a40a9e9c 100644 --- a/decidim-system/app/cells/decidim/system/system_checks_cell.rb +++ b/decidim-system/app/cells/decidim/system/system_checks_cell.rb @@ -23,7 +23,7 @@ def checks end def correct_secret_key_base? - Rails.application.secrets.secret_key_base&.length == 128 + Rails.application.secret_key_base&.length == 128 end def generated_secret_key diff --git a/decidim-system/app/forms/decidim/system/base_organization_form.rb b/decidim-system/app/forms/decidim/system/base_organization_form.rb index f68167899a99c..74c813efcf40f 100644 --- a/decidim-system/app/forms/decidim/system/base_organization_form.rb +++ b/decidim-system/app/forms/decidim/system/base_organization_form.rb @@ -114,7 +114,7 @@ def encrypted_omniauth_settings # We need a valid secret key base for encrypting the SMTP password with it # It is also necessary for other things in Rails (like Cookies encryption) def validate_secret_key_base_for_encryption - return if Rails.application.secrets.secret_key_base&.length == 128 + return if Rails.application.secret_key_base&.length == 128 errors.add(:password, I18n.t("activemodel.errors.models.organization.attributes.password.secret_key")) end diff --git a/decidim-verifications/app/forms/decidim/verifications/sms/mobile_phone_form.rb b/decidim-verifications/app/forms/decidim/verifications/sms/mobile_phone_form.rb index 0bd950ddff831..36054d7b75840 100644 --- a/decidim-verifications/app/forms/decidim/verifications/sms/mobile_phone_form.rb +++ b/decidim-verifications/app/forms/decidim/verifications/sms/mobile_phone_form.rb @@ -18,7 +18,7 @@ def handler_name # A mobile phone can only be verified once but it should be private. def unique_id Digest::MD5.hexdigest( - "#{mobile_phone_number}-#{Rails.application.secrets.secret_key_base}" + "#{mobile_phone_number}-#{Rails.application.secret_key_base}" ) end From 13f6c94f3afb08d5bc65fa2996083e30314c0166 Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 30 Jul 2024 09:06:31 +0300 Subject: [PATCH 03/22] Add overwrite parameter to upload-artifact (#13238) * Add overwrite parameter to upload-artifact * Empty commit to run the CI checks * Empty commit to run the CI checks --- .github/workflows/test_app.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index e9334e98c7664..bd5fe26548bfe 100644 --- a/.github/workflows/test_app.yml +++ b/.github/workflows/test_app.yml @@ -122,3 +122,4 @@ jobs: name: screenshots path: ./spec/decidim_dummy_app/tmp/screenshots if-no-files-found: ignore + overwrite: true From f73187761e6090d30dc0a0a9d0b64534fafc423b Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 30 Jul 2024 10:24:57 +0300 Subject: [PATCH 04/22] Add exception handling when the versioned model does not exists (#13236) --- .../migrate/20240722215500_change_object_changes_on_versions.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/decidim-core/db/migrate/20240722215500_change_object_changes_on_versions.rb b/decidim-core/db/migrate/20240722215500_change_object_changes_on_versions.rb index 8ab4262cc9a9d..a0a926cfa21d1 100644 --- a/decidim-core/db/migrate/20240722215500_change_object_changes_on_versions.rb +++ b/decidim-core/db/migrate/20240722215500_change_object_changes_on_versions.rb @@ -25,6 +25,8 @@ def up end version.update_columns(old_object_changes: nil, object_changes:) # rubocop:disable Rails/SkipsModelValidations + rescue NameError + Rails.logger.info "Skipping History of #{version.item_type} with id #{version.item_id}" end PaperTrail::Version.reset_column_information From 6a7a7d3d2da23a6b1be61bb024359edf96fa848f Mon Sep 17 00:00:00 2001 From: decidim-bot Date: Tue, 30 Jul 2024 09:59:18 +0200 Subject: [PATCH 05/22] New Crowdin updates (#13203) * New translations en.yml (Romanian) * New translations en.yml (French) * New translations en.yml (Spanish) * New translations en.yml (Arabic) * New translations en.yml (Bulgarian) * New translations en.yml (Catalan) * New translations en.yml (Czech) * New translations en.yml (German) * New translations en.yml (Greek) * New translations en.yml (Basque) * New translations en.yml (Finnish) * New translations en.yml (Hungarian) * New translations en.yml (Italian) * New translations en.yml (Japanese) * New translations en.yml (Lithuanian) * New translations en.yml (Dutch) * New translations en.yml (Norwegian) * New translations en.yml (Polish) * New translations en.yml (Portuguese) * New translations en.yml (Slovak) * New translations en.yml (Swedish) * New translations en.yml (Turkish) * New translations en.yml (Chinese Simplified) * New translations en.yml (Chinese Traditional) * New translations en.yml (Galician) * New translations en.yml (Icelandic) * New translations en.yml (Portuguese, Brazilian) * New translations en.yml (Indonesian) * New translations en.yml (Spanish, Mexico) * New translations en.yml (Spanish, Mexico) * New translations en.yml (Latvian) * New translations en.yml (French, Canada) * New translations en.yml (French, Canada) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Ukrainian with many plural form) * New translations en.yml (Russian with many plural form) * New translations en.yml (Finnish (plain)) * New translations en.yml (Spanish) * New translations en.yml (Catalan) * New translations en.yml (German) * New translations en.yml (German) * New translations en.yml (Polish) * New translations en.yml (Romanian) * New translations en.yml (French) * New translations en.yml (Spanish) * New translations en.yml (Arabic) * New translations en.yml (Bulgarian) * New translations en.yml (Catalan) * New translations en.yml (Czech) * New translations en.yml (German) * New translations en.yml (Greek) * New translations en.yml (Basque) * New translations en.yml (Finnish) * New translations en.yml (Hungarian) * New translations en.yml (Italian) * New translations en.yml (Japanese) * New translations en.yml (Lithuanian) * New translations en.yml (Dutch) * New translations en.yml (Norwegian) * New translations en.yml (Polish) * New translations en.yml (Portuguese) * New translations en.yml (Slovak) * New translations en.yml (Swedish) * New translations en.yml (Turkish) * New translations en.yml (Chinese Simplified) * New translations en.yml (Chinese Traditional) * New translations en.yml (Galician) * New translations en.yml (Icelandic) * New translations en.yml (Portuguese, Brazilian) * New translations en.yml (Indonesian) * New translations en.yml (Spanish, Mexico) * New translations en.yml (Spanish, Mexico) * New translations en.yml (Latvian) * New translations en.yml (French, Canada) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Ukrainian with many plural form) * New translations en.yml (Russian with many plural form) * New translations en.yml (Finnish (plain)) * New translations en.yml (Spanish) * New translations en.yml (Spanish) * New translations en.yml (Catalan) * New translations en.yml (Catalan) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (Finnish) * New translations en.yml (French) * New translations en.yml (French) * New translations en.yml (German) * New translations en.yml (German) * New translations en.yml (German) * New translations en.yml (German) * New translations en.yml (German) * New translations en.yml (French) * New translations en.yml (French) * New translations en.yml (Spanish, Mexico) * New translations en.yml (French, Canada) * New translations en.yml (French, Canada) * New translations en.yml (French, Canada) * New translations en.yml (Spanish, Paraguay) * New translations en.yml (Finnish (plain)) * New translations en.yml (Finnish (plain)) * New translations en.yml (Finnish (plain)) * New translations en.yml (Spanish) * New translations en.yml (Catalan) * New translations en.yml (German) --- decidim-admin/config/locales/ca.yml | 2 + decidim-admin/config/locales/de.yml | 2 + decidim-admin/config/locales/es.yml | 2 + decidim-admin/config/locales/fr-CA.yml | 2 + decidim-admin/config/locales/fr.yml | 2 + decidim-assemblies/config/locales/de.yml | 2 +- decidim-assemblies/config/locales/pl.yml | 6 ++ decidim-conferences/config/locales/de.yml | 2 +- decidim-core/config/locales/ca.yml | 2 + decidim-core/config/locales/de.yml | 2 + decidim-core/config/locales/es-MX.yml | 2 + decidim-core/config/locales/es-PY.yml | 2 + decidim-core/config/locales/es.yml | 2 + decidim-core/config/locales/fi-plain.yml | 2 + decidim-core/config/locales/fi.yml | 2 + decidim-core/config/locales/fr-CA.yml | 2 + decidim-core/config/locales/fr.yml | 2 + decidim-initiatives/config/locales/de.yml | 3 + decidim-initiatives/config/locales/es-MX.yml | 3 + decidim-initiatives/config/locales/es-PY.yml | 3 + .../config/locales/fi-plain.yml | 3 + decidim-initiatives/config/locales/fi.yml | 3 + decidim-initiatives/config/locales/fr-CA.yml | 3 + .../config/locales/de.yml | 2 +- decidim-proposals/config/locales/ar.yml | 2 - decidim-proposals/config/locales/bg.yml | 4 -- decidim-proposals/config/locales/ca.yml | 33 ++++++++-- decidim-proposals/config/locales/cs.yml | 4 -- decidim-proposals/config/locales/de.yml | 63 +++++++++++++------ decidim-proposals/config/locales/el.yml | 4 -- decidim-proposals/config/locales/es-MX.yml | 33 ++++++++-- decidim-proposals/config/locales/es-PY.yml | 33 ++++++++-- decidim-proposals/config/locales/es.yml | 33 ++++++++-- decidim-proposals/config/locales/eu.yml | 4 -- decidim-proposals/config/locales/fi-plain.yml | 33 ++++++++-- decidim-proposals/config/locales/fi.yml | 33 ++++++++-- decidim-proposals/config/locales/fr-CA.yml | 8 +-- decidim-proposals/config/locales/fr.yml | 8 +-- decidim-proposals/config/locales/gl.yml | 2 - decidim-proposals/config/locales/hu.yml | 4 -- decidim-proposals/config/locales/id-ID.yml | 2 - decidim-proposals/config/locales/is-IS.yml | 2 - decidim-proposals/config/locales/it.yml | 2 - decidim-proposals/config/locales/ja.yml | 10 +-- decidim-proposals/config/locales/lt.yml | 4 -- decidim-proposals/config/locales/lv.yml | 2 - decidim-proposals/config/locales/nl.yml | 2 - decidim-proposals/config/locales/no.yml | 2 - decidim-proposals/config/locales/pl.yml | 4 -- decidim-proposals/config/locales/pt-BR.yml | 2 - decidim-proposals/config/locales/pt.yml | 2 - decidim-proposals/config/locales/ro-RO.yml | 2 - decidim-proposals/config/locales/ru.yml | 2 - decidim-proposals/config/locales/sk.yml | 2 - decidim-proposals/config/locales/sv.yml | 2 - decidim-proposals/config/locales/tr-TR.yml | 2 - decidim-proposals/config/locales/uk.yml | 2 - decidim-proposals/config/locales/zh-CN.yml | 2 - decidim-proposals/config/locales/zh-TW.yml | 4 -- 59 files changed, 287 insertions(+), 124 deletions(-) diff --git a/decidim-admin/config/locales/ca.yml b/decidim-admin/config/locales/ca.yml index ac6734c438dce..cb9e40ca4c9ff 100644 --- a/decidim-admin/config/locales/ca.yml +++ b/decidim-admin/config/locales/ca.yml @@ -350,6 +350,8 @@ ca: conflicts: attempts: Intents 'false': 'No' + index: + text: Cercar per correu electrònic, nom o àlies. managed_user_name: Usuari gestionat solved: Resolt title: Conflictes de verificació diff --git a/decidim-admin/config/locales/de.yml b/decidim-admin/config/locales/de.yml index 3c4fb6e9db92c..128483a14792c 100644 --- a/decidim-admin/config/locales/de.yml +++ b/decidim-admin/config/locales/de.yml @@ -350,6 +350,8 @@ de: conflicts: attempts: Versuche 'false': 'Nein' + index: + text: Suche nach bestehender E-Mail, Name oder Spitzname des Teilnehmenden. managed_user_name: Verwalteter Benutzer solved: Gelöst title: Überprüfungskonflikte diff --git a/decidim-admin/config/locales/es.yml b/decidim-admin/config/locales/es.yml index e91b9f6461af7..62d77115d4355 100644 --- a/decidim-admin/config/locales/es.yml +++ b/decidim-admin/config/locales/es.yml @@ -350,6 +350,8 @@ es: conflicts: attempts: Intentos 'false': 'No' + index: + text: Buscar por correo electrónico, nombre o alias. managed_user_name: Usuario gestionado solved: Resuelto title: Conflictos de verificación diff --git a/decidim-admin/config/locales/fr-CA.yml b/decidim-admin/config/locales/fr-CA.yml index de182311cf48c..50a052f64cee1 100644 --- a/decidim-admin/config/locales/fr-CA.yml +++ b/decidim-admin/config/locales/fr-CA.yml @@ -350,6 +350,8 @@ fr-CA: conflicts: attempts: Tentatives 'false': 'Non' + index: + text: Recherche par adresse e-mail, nom ou pseudonyme. managed_user_name: Utilisateur représenté solved: Résolu title: Conflits de vérification diff --git a/decidim-admin/config/locales/fr.yml b/decidim-admin/config/locales/fr.yml index 3db6d76ea9799..ee8d6e2a725a5 100644 --- a/decidim-admin/config/locales/fr.yml +++ b/decidim-admin/config/locales/fr.yml @@ -350,6 +350,8 @@ fr: conflicts: attempts: Tentatives 'false': 'Non' + index: + text: Recherche par adresse e-mail, nom ou pseudonyme. managed_user_name: Utilisateur représenté solved: Résolu title: Conflits de vérification diff --git a/decidim-assemblies/config/locales/de.yml b/decidim-assemblies/config/locales/de.yml index 5c625d3dddc76..2f45e6937c19f 100644 --- a/decidim-assemblies/config/locales/de.yml +++ b/decidim-assemblies/config/locales/de.yml @@ -247,7 +247,7 @@ de: admin: Administrator collaborator: Mitarbeiter moderator: Moderator - valuator: Schätzer + valuator: Bewertende titles: assemblies: Gremien assemblies_types: Versammlungstypen diff --git a/decidim-assemblies/config/locales/pl.yml b/decidim-assemblies/config/locales/pl.yml index 5a77c998cbf0a..2df994f071720 100644 --- a/decidim-assemblies/config/locales/pl.yml +++ b/decidim-assemblies/config/locales/pl.yml @@ -453,6 +453,12 @@ pl: take_part: Dołącz index: promoted_assemblies: Wyróżnione zespoły + metadata: + children_item: + one: "%{count} zespół" + few: "%{count} zespoły" + many: "%{count} zespołów" + other: "%{count} zespołów" order_by_assemblies: assemblies: one: "%{count} zespół" diff --git a/decidim-conferences/config/locales/de.yml b/decidim-conferences/config/locales/de.yml index aaa6a444f6e12..792a40e4f53e4 100644 --- a/decidim-conferences/config/locales/de.yml +++ b/decidim-conferences/config/locales/de.yml @@ -234,7 +234,7 @@ de: admin: Administrator collaborator: Mitarbeiter moderator: Moderator - valuator: Schätzer + valuator: Bewertende media_link: fields: date: Datum diff --git a/decidim-core/config/locales/ca.yml b/decidim-core/config/locales/ca.yml index 0968c115bfead..d6fefa177467d 100644 --- a/decidim-core/config/locales/ca.yml +++ b/decidim-core/config/locales/ca.yml @@ -1261,6 +1261,7 @@ ca: administrators: Administradores allow_public_contact: Permetre que qualsevol participant m'enviï missatges directes, encara que no la segueixi. allow_push_notifications: Mostrar notificacions emergents per a saber que passa mentre no estàs a la plataforma. Pots deshabilitar-les en qualsevol moment. + assigned_to_proposal: Vull rebre un correu electrònic quan algú m'assigni una proposta a avaluar direct_messages: Rebre missatges directes de qualsevol email_on_moderations: Vull rebre un correu electrònic cada cop que es reporti algo o a algú per moderació. everything_followed: Tot el que segueixo @@ -1279,6 +1280,7 @@ ca: push_notifications_reminder: Per rebre notificacions de la plataforma, primer les has de permetre a la configuració del teu navegador. receive_notifications_about: Vull rebre notificacions update_notifications_settings: Guardar canvis + valuators: Avaluadores update: error: S'ha produït un error en actualitzar la configuració de les notificacions. success: La configuració de les notificacions s'ha actualitzat correctament. diff --git a/decidim-core/config/locales/de.yml b/decidim-core/config/locales/de.yml index 99d42ab9bdab4..d2289abd42f5d 100644 --- a/decidim-core/config/locales/de.yml +++ b/decidim-core/config/locales/de.yml @@ -1266,6 +1266,7 @@ de: administrators: Administratoren allow_public_contact: Allen erlauben, mir Direktnachrichten zu senden, selbst wenn ich ihnen nicht folge. allow_push_notifications: Erhalten Sie Push-Benachrichtigungen, um Mitteilungen zu erhalten, wenn Sie nicht auf der Plattform sind. Sie können diese jederzeit ausschalten. + assigned_to_proposal: Ich möchte eine E-Mail erhalten, wenn mir jemand einen Vorschlag zur Bewertung zuweist direct_messages: Direktnachrichten von allen erhalten email_on_moderations: Ich möchte jedes Mal, wenn etwas oder jemand zur Moderation gemeldet wird, eine E-Mail erhalten. everything_followed: Alles was ich folge @@ -1284,6 +1285,7 @@ de: push_notifications_reminder: Um Benachrichtigungen von der Plattform zu erhalten, müssen Sie diese zuerst in den Browser-Einstellungen zulassen. receive_notifications_about: Ich möchte Benachrichtigungen erhalten über update_notifications_settings: Änderungen speichern + valuators: Bewertende update: error: Beim Aktualisieren der Benachrichtigungseinstellungen ist ein Fehler aufgetreten. success: Ihre Benachrichtigungseinstellungen wurden erfolgreich aktualisiert. diff --git a/decidim-core/config/locales/es-MX.yml b/decidim-core/config/locales/es-MX.yml index 2ef6ce629dae8..6df40f25cbdf9 100644 --- a/decidim-core/config/locales/es-MX.yml +++ b/decidim-core/config/locales/es-MX.yml @@ -1264,6 +1264,7 @@ es-MX: administrators: Administradoras allow_public_contact: Permitir que cualquier participante me envíe un mensaje directo, incluso si no la sigo. allow_push_notifications: Mostrar notificaciones emergentes para saber qué pasa cuando no estás en la plataforma. Puedes desactivarlas en cualquier momento. + assigned_to_proposal: Quiero recibir un correo electrónico cuando alguien me asigne una propuesta para evaluar direct_messages: Recibir mensajes directos de cualquiera email_on_moderations: Quiero recibir un correo electrónico cada vez que se reporta algo o a alguien para moderación. everything_followed: Todo lo que sigo @@ -1282,6 +1283,7 @@ es-MX: push_notifications_reminder: Para obtener notificaciones de la plataforma, primero tienes que permitirlas en la configuración de tu navegador. receive_notifications_about: Quiero recibir notificaciones sobre update_notifications_settings: Guardar cambios + valuators: Evaluadoras update: error: Ha habido un error al actualizar la configuración de las notificaciones. success: La configuración de las notificaciones se han actualizado correctamente. diff --git a/decidim-core/config/locales/es-PY.yml b/decidim-core/config/locales/es-PY.yml index f35c67e8aa4b8..a789977a59883 100644 --- a/decidim-core/config/locales/es-PY.yml +++ b/decidim-core/config/locales/es-PY.yml @@ -1264,6 +1264,7 @@ es-PY: administrators: Administradoras allow_public_contact: Permitir que cualquier participante me envíe un mensaje directo, incluso si no la sigo. allow_push_notifications: Mostrar notificaciones emergentes para saber qué pasa cuando no estás en la plataforma. Puedes desactivarlas en cualquier momento. + assigned_to_proposal: Quiero recibir un correo electrónico cuando alguien me asigne una propuesta para evaluar direct_messages: Recibir mensajes directos de cualquiera email_on_moderations: Quiero recibir un correo electrónico cada vez que se reporta algo o a alguien para moderación. everything_followed: Todo lo que sigo @@ -1282,6 +1283,7 @@ es-PY: push_notifications_reminder: Para obtener notificaciones de la plataforma, primero tienes que permitirlas en la configuración de tu navegador. receive_notifications_about: Quiero recibir notificaciones sobre update_notifications_settings: Guardar cambios + valuators: Evaluadoras update: error: Ha habido un error al actualizar la configuración de las notificaciones. success: La configuración de las notificaciones se han actualizado correctamente. diff --git a/decidim-core/config/locales/es.yml b/decidim-core/config/locales/es.yml index 7b2c816085bc4..df0b7dd1c6a4a 100644 --- a/decidim-core/config/locales/es.yml +++ b/decidim-core/config/locales/es.yml @@ -1261,6 +1261,7 @@ es: administrators: Administradoras allow_public_contact: Permitir que cualquier participante me envíe un mensaje directo, incluso si no la sigo. allow_push_notifications: Recibe notificaciones emergentes para estar al día de lo que pasa en la plataforma. Puedes desactivarlas en cualquier momento. + assigned_to_proposal: Quiero recibir un correo electrónico cuando alguien me asigne una propuesta para evaluar direct_messages: Recibir mensajes directos de cualquiera email_on_moderations: Quiero recibir un correo electrónico cada vez que se reporta algo o a alguien para moderación. everything_followed: Todo lo que sigo @@ -1279,6 +1280,7 @@ es: push_notifications_reminder: Para recibir notificaciones de la plataforma, primero tienes que permitirlas en la configuración de tu navegador. receive_notifications_about: Quiero recibir notificaciones update_notifications_settings: Guardar cambios + valuators: Evaluadoras update: error: Se ha producido un error al actualizar la configuración de las notificaciones. success: La configuración de las notificaciones se ha actualizado correctamente. diff --git a/decidim-core/config/locales/fi-plain.yml b/decidim-core/config/locales/fi-plain.yml index 730ec3c457d02..fd448e406a359 100644 --- a/decidim-core/config/locales/fi-plain.yml +++ b/decidim-core/config/locales/fi-plain.yml @@ -1261,6 +1261,7 @@ fi-pl: administrators: Hallintakäyttäjät allow_public_contact: Salli kenen tahansa lähettää minulle yksityisviestejä, vaikka en seuraisi heitä. allow_push_notifications: Vastaanota push-ilmoituksia, mikäli haluat tietää, mitä alustalla tapahtuu, kun et ole paikalla. Voit poistaa ne käytöstä milloin tahansa. + assigned_to_proposal: Haluan saada sähköpostia, kun joku antaa minulle ehdotuksen arvioitavaksi direct_messages: Vastaanota yksityisviestejä kaikilta email_on_moderations: Haluan saada sähköpostia aina, kun jotain tai joku käyttäjä ilmoitetaan moderoitavaksi. everything_followed: Kaikki mitä seuraan @@ -1279,6 +1280,7 @@ fi-pl: push_notifications_reminder: Mikäli haluat vastaanottaa push-ilmoituksia alustalta, sinun on sallittava ne ensin selaimesi asetuksista tälle sivustolle. receive_notifications_about: Haluan saada ilmoituksia update_notifications_settings: Tallenna muutokset + valuators: Arvioijat update: error: Ilmoitusasetustesi päivityksessä tapahtui virhe. success: Ilmoitusasetuksesi päivitettiin onnistuneesti. diff --git a/decidim-core/config/locales/fi.yml b/decidim-core/config/locales/fi.yml index 3826c451901b5..37f7131536b35 100644 --- a/decidim-core/config/locales/fi.yml +++ b/decidim-core/config/locales/fi.yml @@ -1261,6 +1261,7 @@ fi: administrators: Hallintakäyttäjät allow_public_contact: Salli kenen tahansa lähettää minulle yksityisviestejä, vaikka en seuraisi heitä. allow_push_notifications: Vastaanota push-ilmoituksia, mikäli haluat tietää, mitä alustalla tapahtuu, kun et ole paikalla. Voit poistaa ne käytöstä milloin tahansa. + assigned_to_proposal: Haluan saada sähköpostia, kun joku antaa minulle ehdotuksen arvioitavaksi direct_messages: Vastaanota yksityisviestejä kaikilta email_on_moderations: Haluan saada sähköpostia aina, kun jotain tai joku käyttäjä ilmoitetaan moderoitavaksi. everything_followed: Kaikki mitä seuraan @@ -1279,6 +1280,7 @@ fi: push_notifications_reminder: Mikäli haluat vastaanottaa push-ilmoituksia alustalta, sinun on sallittava ne ensin selaimesi asetuksista tälle sivustolle. receive_notifications_about: Haluan saada ilmoituksia update_notifications_settings: Tallenna muutokset + valuators: Arvioijat update: error: Ilmoitusasetustesi päivitys epäonnistui. success: Ilmoitusasetuksesi päivitys onnistui. diff --git a/decidim-core/config/locales/fr-CA.yml b/decidim-core/config/locales/fr-CA.yml index 31ac7c0157425..4a7f91e539c37 100644 --- a/decidim-core/config/locales/fr-CA.yml +++ b/decidim-core/config/locales/fr-CA.yml @@ -1259,6 +1259,7 @@ fr-CA: administrators: Administrateurs allow_public_contact: "Allow anyone to send me a direct message, even if I do not follow them.\nAutoriser tout le monde à m'envoyer un message direct même si je ne les suis pas." allow_push_notifications: Recevez des notifications push pour savoir ce qui se passe lorsque vous n'êtes pas sur la plateforme. Vous pouvez les désactiver à tout moment. + assigned_to_proposal: Je veux recevoir un email quand quelqu'un m'assigne une proposition à évaluer direct_messages: Recevoir des messages directs de n'importe qui email_on_moderations: Je veux recevoir un email chaque fois que quelqu'un ou qu'une ressource a été signalé à des fins de modérations. everything_followed: Tout ce que je suis @@ -1277,6 +1278,7 @@ fr-CA: push_notifications_reminder: Pour recevoir des notifications de la plateforme, vous devez d'abord les autoriser dans les paramètres de votre navigateur. receive_notifications_about: Je veux recevoir des notifications sur update_notifications_settings: Enregistrer les modifications + valuators: Évaluateurs update: error: Une erreur s'est produite lors de la mise à jour des paramètres de vos notifications. success: Vos paramètres de notifications ont été mis à jour avec succès. diff --git a/decidim-core/config/locales/fr.yml b/decidim-core/config/locales/fr.yml index 6c4b475395758..d28cc712834a8 100644 --- a/decidim-core/config/locales/fr.yml +++ b/decidim-core/config/locales/fr.yml @@ -1259,6 +1259,7 @@ fr: administrators: Administrateurs allow_public_contact: "Allow anyone to send me a direct message, even if I do not follow them.\nAutoriser tout le monde à m'envoyer un message direct même si je ne les suis pas." allow_push_notifications: Recevez des notifications push pour savoir ce qui se passe lorsque vous n'êtes pas sur la plateforme. Vous pouvez les désactiver à tout moment. + assigned_to_proposal: Je veux recevoir un email quand quelqu'un m'assigne une proposition à évaluer direct_messages: Recevoir des messages directs de n'importe qui email_on_moderations: Je veux recevoir un email chaque fois que quelqu'un ou qu'une ressource a été signalé à des fins de modérations. everything_followed: Tout ce que je suis @@ -1277,6 +1278,7 @@ fr: push_notifications_reminder: Pour recevoir des notifications de la plateforme, vous devez d'abord les autoriser dans les paramètres de votre navigateur. receive_notifications_about: Je veux recevoir des notifications sur update_notifications_settings: Enregistrer les modifications + valuators: Évaluateurs update: error: Une erreur s'est produite lors de la mise à jour des paramètres de vos notifications. success: Vos paramètres de notifications ont été mis à jour avec succès. diff --git a/decidim-initiatives/config/locales/de.yml b/decidim-initiatives/config/locales/de.yml index f75cc8d7c0a7a..612472264629f 100644 --- a/decidim-initiatives/config/locales/de.yml +++ b/decidim-initiatives/config/locales/de.yml @@ -91,6 +91,7 @@ de: accepted: Ausreichend Unterschriften created: Erstellt discarded: Verworfen + open: Offen rejected: Zu wenige Unterschriften validating: Technische Validierung type_id_eq: @@ -328,6 +329,7 @@ de: accepted: Ausreichend Unterschriften created: Erstellt discarded: Verworfen + open: Offen rejected: Ungenügend Unterschriften validating: Technische Validierung application_helper: @@ -576,6 +578,7 @@ de: accepted: Ausreichend Unterschriften created: Erstellt discarded: Verworfen + open: Offen rejected: Ungenügend Unterschriften validating: Technische Validierung states: diff --git a/decidim-initiatives/config/locales/es-MX.yml b/decidim-initiatives/config/locales/es-MX.yml index e8d55c6e93c3a..90d035bd7b7db 100644 --- a/decidim-initiatives/config/locales/es-MX.yml +++ b/decidim-initiatives/config/locales/es-MX.yml @@ -91,6 +91,7 @@ es-MX: accepted: Con las firmas necesarias created: Creada discarded: Descartada + open: Abierta rejected: No ha conseguido las firmas necesarias validating: Validación técnica type_id_eq: @@ -328,6 +329,7 @@ es-MX: accepted: Ha conseguido las firmas created: Creado discarded: Descartado + open: Abierta rejected: No ha conseguido las firmas necesarias validating: Validación técnica application_helper: @@ -576,6 +578,7 @@ es-MX: accepted: Con las firmas necesarias created: Creada discarded: Descartada + open: Abiertas rejected: No ha conseguido las firmas validating: Validación técnica states: diff --git a/decidim-initiatives/config/locales/es-PY.yml b/decidim-initiatives/config/locales/es-PY.yml index 2b19a4bd5e3b7..69fbaadac8eca 100644 --- a/decidim-initiatives/config/locales/es-PY.yml +++ b/decidim-initiatives/config/locales/es-PY.yml @@ -91,6 +91,7 @@ es-PY: accepted: Con las firmas necesarias created: Creada discarded: Descartada + open: Abierta rejected: No ha conseguido las firmas necesarias validating: Validación técnica type_id_eq: @@ -328,6 +329,7 @@ es-PY: accepted: Ha conseguido las firmas created: Creado discarded: Descartado + open: Abierta rejected: No ha conseguido las firmas necesarias validating: Validación técnica application_helper: @@ -576,6 +578,7 @@ es-PY: accepted: Con las firmas necesarias created: Creada discarded: Descartada + open: Abiertas rejected: No ha conseguido las firmas validating: Validación técnica states: diff --git a/decidim-initiatives/config/locales/fi-plain.yml b/decidim-initiatives/config/locales/fi-plain.yml index 6815fd511b788..b39636ddd5dba 100644 --- a/decidim-initiatives/config/locales/fi-plain.yml +++ b/decidim-initiatives/config/locales/fi-plain.yml @@ -91,6 +91,7 @@ fi-pl: accepted: Tarpeeksi allekirjoituksia created: Luotu discarded: Hylätyt + open: Avoimet rejected: Liian vähän allekirjoituksia validating: Teknisessä validoinnissa type_id_eq: @@ -328,6 +329,7 @@ fi-pl: accepted: Tarpeeksi allekirjoituksia created: Luotu discarded: Hylätty + open: Avoimet rejected: Liian vähän allekirjoituksia validating: Teknisessä validoinnissa application_helper: @@ -576,6 +578,7 @@ fi-pl: accepted: Tarpeeksi allekirjoituksia created: Luotu discarded: Hylätty + open: Avoimet rejected: Liian vähän allekirjoituksia validating: Teknisessä tarkastuksessa states: diff --git a/decidim-initiatives/config/locales/fi.yml b/decidim-initiatives/config/locales/fi.yml index 177004a2f58ce..f4d0b05d99f20 100644 --- a/decidim-initiatives/config/locales/fi.yml +++ b/decidim-initiatives/config/locales/fi.yml @@ -91,6 +91,7 @@ fi: accepted: Tarpeeksi allekirjoituksia created: Luotu discarded: Hylätyt + open: Avoimet rejected: Liian vähän allekirjoituksia validating: Teknisessä validoinnissa type_id_eq: @@ -328,6 +329,7 @@ fi: accepted: Tarpeeksi allekirjoituksia created: Luotu discarded: Hylätty + open: Avoimet rejected: Liian vähän allekirjoituksia validating: Teknisessä tarkastuksessa application_helper: @@ -576,6 +578,7 @@ fi: accepted: Tarpeeksi allekirjoituksia created: Luotu discarded: Hylätty + open: Avoimet rejected: Liian vähän allekirjoituksia validating: Teknisessä tarkastuksessa states: diff --git a/decidim-initiatives/config/locales/fr-CA.yml b/decidim-initiatives/config/locales/fr-CA.yml index c90a79cc9dc14..53545299252a8 100644 --- a/decidim-initiatives/config/locales/fr-CA.yml +++ b/decidim-initiatives/config/locales/fr-CA.yml @@ -91,6 +91,7 @@ fr-CA: accepted: Assez de signatures created: Créée discarded: Rejetée + open: Ouverte rejected: Pas assez de signatures validating: Validation technique type_id_eq: @@ -323,6 +324,7 @@ fr-CA: accepted: Assez de signatures created: Créée discarded: Retirée + open: Ouverte rejected: Pas assez de signatures validating: Validation technique application_helper: @@ -571,6 +573,7 @@ fr-CA: accepted: Assez de signatures created: Créé le discarded: Abandonné + open: Ouverte rejected: Pas assez de signatures validating: Validation technique states: diff --git a/decidim-participatory_processes/config/locales/de.yml b/decidim-participatory_processes/config/locales/de.yml index 6512060b05d14..aea745b54a1d3 100644 --- a/decidim-participatory_processes/config/locales/de.yml +++ b/decidim-participatory_processes/config/locales/de.yml @@ -163,7 +163,7 @@ de: admin: Administrator collaborator: Mitarbeiter moderator: Moderator - valuator: Schätzer + valuator: Bewertende user: fields: invitation_accepted_at: Einladung akzeptiert am diff --git a/decidim-proposals/config/locales/ar.yml b/decidim-proposals/config/locales/ar.yml index 7b544194c69e2..ce56c8eb2b48e 100644 --- a/decidim-proposals/config/locales/ar.yml +++ b/decidim-proposals/config/locales/ar.yml @@ -158,7 +158,6 @@ ar: participatory_texts_enabled: تم تمكين النصوص التشاركية participatory_texts_enabled_readonly: لا يمكن التفاعل مع هذا الإعداد إذا كانت هناك اقتراحات موجودة. الرجاء إنشاء "مكون مقترحات" جديد إذا كنت ترغب في تمكين هذه الميزة أو تجاهل كافة المقترحات المستوردة في قائمة "النصوص التشاركية" إذا كنت ترغب في تعطيلها. proposal_answering_enabled: تم تمكين الرد على الاقتراح - proposal_edit_before_minutes: يمكن تحرير المقترحات من قبل المؤلفين قبل مرور عدة دقائق proposal_edit_time: تحرير المقترح proposal_edit_time_choices: infinite: السماح بتعديل المقترحات لفترة زمنية غير محدودة @@ -406,7 +405,6 @@ ar: form: note: ملحوظة submit: خضع - leave_your_note: اترك ملاحظتك title: ملاحظات خاصة proposals: edit: diff --git a/decidim-proposals/config/locales/bg.yml b/decidim-proposals/config/locales/bg.yml index 0ad5cc52ef239..51586b4be2f5e 100644 --- a/decidim-proposals/config/locales/bg.yml +++ b/decidim-proposals/config/locales/bg.yml @@ -179,7 +179,6 @@ bg: participatory_texts_enabled: Текстовете от участници са разрешени participatory_texts_enabled_readonly: Тази настройка не е активна, ако има съществуващи предложения. Моля, създайте нов компонент „Предложения“, ако желаете да активирате тази функция или изчистете всички внесени предложения в менюто „Текст на участници“, ако искате да я деактивирате. proposal_answering_enabled: Отговора на предложение е разрешен - proposal_edit_before_minutes: Предложенията могат да бъдат редактирани от авторите преди да минат толкова минути proposal_edit_time: Редактиране на предложение proposal_edit_time_choices: infinite: Разрешаване на редактиране на предложения за безкраен период от време @@ -235,10 +234,8 @@ bg: proposals: admin: proposal_note_created: - email_intro: Някой е направил бележка в предложението %{resource_title}. Вижте я в администраторския панел. email_outro: Получихте известие, защото можете да оценяте предложението. email_subject: Някой е направил бележка в предложението %{resource_title}. - notification_title: Някой е направил бележка в предложението %{resource_title}. Вижте я в администраторския панел. collaborative_draft_access_accepted: email_intro: '%{requester_name} получи достъп като сътрудник в съвместната чернова %{resource_title}.' email_outro: Получихте известие, защото сте сътрудник в %{resource_title}. @@ -473,7 +470,6 @@ bg: form: note: Бележка submit: Подаване - leave_your_note: Оставете Вашата бележка title: Лични бележки proposal_states: create: diff --git a/decidim-proposals/config/locales/ca.yml b/decidim-proposals/config/locales/ca.yml index 1534f82942858..b8648a31ee9e3 100644 --- a/decidim-proposals/config/locales/ca.yml +++ b/decidim-proposals/config/locales/ca.yml @@ -170,6 +170,11 @@ ca: random: Aleatòriament recent: Recents with_more_authors: Amb més autores + edit_time: Les autores poden editar les seves propostes abans que passi aquest temps + edit_time_units: + days: Dies + hours: Hores + minutes: Minuts geocoding_enabled: Geocodificació habilitada minimum_votes_per_user: Suports mínims per usuari new_proposal_body_template: Plantilla pel text de nova proposta @@ -179,11 +184,14 @@ ca: participatory_texts_enabled: Texts participatius habilitats participatory_texts_enabled_readonly: No es pot interactuar amb aquesta configuració si hi ha propostes existents. Si us plau, crea un nou 'component de propostes' si vols habilitar aquesta característica o descarta totes les propostes importades al menú "textos participatius" si vols deshabilitar-lo. proposal_answering_enabled: Resposta a propostes habilitada - proposal_edit_before_minutes: Les propostes poden ser editades per les autores abans que passin aquests minuts proposal_edit_time: Edició de propostes proposal_edit_time_choices: infinite: Permet l'edició de propostes durant un període infinit limited: Permet l'edició de propostes durant una finestra temporal específica + proposal_edit_time_unit_options: + days: Dies + hours: Hores + minutes: Minuts proposal_length: Longitud màxima del cos de la proposta proposal_limit: Límit de propostes per participant proposal_wizard_step_1_help_text: Text d'ajuda pel pas "Crear" de l'assistent de propostes @@ -234,11 +242,26 @@ ca: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Has estat assignada com a avaluadora de la proposta "%{resource_title}". Això vol dir que es confia en tu per donar la teva opinió i per donar una resposta adequada en els pròxims dies. Pots veure la proposta al taulell d'administració. + email_outro: Reps aquesta notificació perquè pots avaluar la proposta. + email_subject: Has estat assignada com a avaluadora de la proposta "%{resource_title}". + notification_title: Has estat assignada com a avaluadora de la proposta "%{resource_title}". Pots veure la proposta al taulell d'administració. proposal_note_created: - email_intro: Algú ha deixat una nota a la proposta "%{resource_title}". Revisa-la ara a través del taulell d'administració. + email_intro: '%{author_name} ha creat una nota privada a %{resource_title}. La pots veure al taulell d''administració.' email_outro: Reps aquesta notificació perquè pots avaluar la proposta. email_subject: Algú ha deixat una nota a la proposta %{resource_title}. - notification_title: Algú ha deixat una nota a la proposta %{resource_title}. Revisa-la ara a través del taulell d'administració. + notification_title: %{author_name} %{author_nickname} ha creat una nota privada a %{resource_title}. La pots veure al taulell d'administració. + proposal_note_mentioned: + email_intro: '"%{author_name}" "%{author_nickname}" t''ha esmentat a una nota privada a "%{resource_title}". La pots veure al taulell d''administració.' + email_outro: Has rebut aquesta notificació perquè t'han esmentat a una nota privada. + email_subject: Algú ha deixat una nota a la proposta %{resource_title}. + notification_title: %{author_name} %{author_nickname} te ha esmentat a una nota privada a %{resource_title}. La pots veure al taulell d'administració. + proposal_note_replied: + email_intro: '%{author_name} ha respost la teva nota privada a %{resource_title}. Ho pots veure al taulell d''administració.' + email_outro: Has rebut aquesta notificació perquè ets l'autora de la nota privada. + email_subject: "%{author_name} ha respost a la teva nota privada a %{resource_title}." + notification_title: %{author_name} %{author_nickname} ha respost a la teva nota privada a %{resource_title}. Ho pots veure al taulell d'administració. collaborative_draft_access_accepted: email_intro: 'S''ha acceptat %{requester_name} per accedir com a contribuidora de l''esborrany col·laboratiu %{resource_title}.' email_outro: Has rebut aquesta notificació perquè vas contribuir a la proposta "%{resource_title}". @@ -473,7 +496,9 @@ ca: form: note: Nota submit: Enviar - leave_your_note: Deixa la teva nota + reply: + error: S'ha produït un error en crear aquesta resposta a la nota privada. + success: La resposta a la nota privada s'ha creat amb èxit. title: Notes privades proposal_states: create: diff --git a/decidim-proposals/config/locales/cs.yml b/decidim-proposals/config/locales/cs.yml index 2d3510a702e8f..365c9e9fd13cf 100644 --- a/decidim-proposals/config/locales/cs.yml +++ b/decidim-proposals/config/locales/cs.yml @@ -176,7 +176,6 @@ cs: participatory_texts_enabled: Povolené účastnické texty participatory_texts_enabled_readonly: Pokud existují stávající návrhy, nelze toto nastavení měnit. Pokud chcete tuto vlastnost povolit nebo vyhodit všechny importované návrhy v menu "participativní texty" prosím vytvořte novou složku "Návrhy", pokud ji chcete zakázat. proposal_answering_enabled: Odpovídání návrhu je povoleno - proposal_edit_before_minutes: Návrhy mohou být editovány autory před tím, než projde hodně minut proposal_edit_time: Úprava návrhů proposal_edit_time_choices: infinite: Povolit úpravy návrhů na nekonečnou dobu @@ -226,10 +225,8 @@ cs: proposals: admin: proposal_note_created: - email_intro: Někdo zanechal poznámku k návrhu "%{resource_title}". Podívejte se na admin panel. email_outro: Obdrželi jste toto oznámení, protože můžete návrh ohodnotit. email_subject: Někdo zanechal poznámku na návrh %{resource_title}. - notification_title: Někdo zanechal poznámku k návrhu %{resource_title}. Podívejte se na admin panel. collaborative_draft_access_accepted: email_intro: '%{requester_name} byl přijat k přístupu jako přispěvatel konceptu spolupráce %{resource_title}.' email_outro: Obdrželi jste toto oznámení, protože jste spolupracovníkem %{resource_title}. @@ -457,7 +454,6 @@ cs: form: note: Poznámka submit: Předložit - leave_your_note: Nechte svou poznámku title: Soukromé poznámky proposal_states: create: diff --git a/decidim-proposals/config/locales/de.yml b/decidim-proposals/config/locales/de.yml index 355d651efd8cb..a8141c194ee45 100644 --- a/decidim-proposals/config/locales/de.yml +++ b/decidim-proposals/config/locales/de.yml @@ -52,7 +52,7 @@ de: keep_authors: Behalten Sie die ursprünglichen Autoren valuation_assignment: admin_log: - valuator_role_id: Name des Schätzers + valuator_role_id: Name des Bewertenden errors: models: participatory_text: @@ -128,7 +128,7 @@ de: validating: Technische Validierung withdrawn: Zurückgezogen valuator_role_ids_has: - label: Zugewiesen zu dem Schätzer + label: Zur Bewertung zugewiesen with_any_state: label: Beantwortet values: @@ -170,6 +170,11 @@ de: random: Zufällig recent: Neueste with_more_authors: Mit mehr Autoren + edit_time: Vorschläge können vor Ablauf dieser Zeit von den Autoren bearbeitet werden + edit_time_units: + days: Tage + hours: Stunden + minutes: Minuten geocoding_enabled: Geocoding aktiviert minimum_votes_per_user: Minimal erforderliche Anzahl Stimmen eines Abstimmenden new_proposal_body_template: Textvorlage für neuen Vorschlag @@ -179,11 +184,14 @@ de: participatory_texts_enabled: Partizipative Texte aktiviert participatory_texts_enabled_readonly: Die Interaktion mit dieser Einstellung ist nicht möglich, wenn Vorschläge bestehen. Bitte erstellen Sie eine neue „Vorschlagskomponente“, wenn Sie diese Funktion aktivieren möchten, oder verwerfen Sie alle importierten Vorschläge im Menü „Beteiligungstexte“, wenn Sie diese Funktion deaktivieren möchten. proposal_answering_enabled: Vorschlagsantworten aktiviert - proposal_edit_before_minutes: Vorschläge können von Autoren innerhalb dieser Zeit bearbeitet werden proposal_edit_time: Vorschläge bearbeiten proposal_edit_time_choices: infinite: Vorschläge können beliebig lang bearbeitet werden limited: Vorschläge können nach einem spezifischen Zeitfenster nicht mehr bearbeitet werden + proposal_edit_time_unit_options: + days: Tage + hours: Stunden + minutes: Minuten proposal_length: Maximale Länge des Haupttextes proposal_limit: Vorschlagslimit pro Benutzer proposal_wizard_step_1_help_text: Hilfetext "Erstellen"-Schritt im Vorschlagsassistenten @@ -234,11 +242,26 @@ de: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Ihnen wurde der Vorschlag "%{resource_title}" zur Bewertung zugewiesen. Das bedeutet, dass von Ihnen in den nächsten Tagen eine Rückmeldung und Einschätzung erwartet wird. Jetzt im Admin-Panel anschauen. + email_outro: Sie haben diese Benachrichtigung erhalten, weil Sie diesen Vorschlag bewerten können. + email_subject: Der Vorschlag %{resource_title} wurde Ihnen zur Bewertung zugewiesen. + notification_title: Ihnen wurde der Vorschlag %{resource_title} zur Bewertung zugewiesen. Jetzt Admin-Panel anschauen. proposal_note_created: - email_intro: Jemand hat eine Notiz zum Vorschlag "%{resource_title}" hinterlassen. Schauen Sie sie im Admin-Panel an. + email_intro: '%{author_name} hat eine private Notiz unter %{resource_title} erstellt. Jetzt im Admin-Panel anzeigen.' email_outro: Sie haben diese Benachrichtigung erhalten, weil Sie diesen Vorschlag bewerten können. email_subject: Jemand hat eine Notiz für Vorschlag %{resource_title} erstellt. - notification_title: Jemand hat eine Notiz für den Vorschlag %{resource_title}erstellt. Sie können sie über das Admin-Panel anzeigen. + notification_title: %{author_name} hat eine private Notiz unter %{resource_title} erstellt. Jetzt im Admin-Panel anzeigen. + proposal_note_mentioned: + email_intro: Sie wurden in einer privaten Notiz in "%{resource_title}" von "%{author_name}" "%{author_nickname}" erwähnt. Jetzt im Admin-Panel anzeigen. + email_outro: Sie haben diese Benachrichtigung erhalten, weil Sie in einer privaten Notiz erwähnt wurden. + email_subject: Jemand hat Sie in einer Notiz zum Vorschlag "%{resource_title}" erwähnt. + notification_title: Sie wurden in einer privaten Notiz unter "%{resource_title}" von %{author_name} %{author_nickname} erwähnt. Jetzt im Admin-Panel anzeigen. + proposal_note_replied: + email_intro: '%{author_name} hat deine private Notiz unter %{resource_title} beantwortet. Jetzt im Admin-Panel anzeigen.' + email_outro: Sie haben diese Benachrichtigung erhalten, weil Sie der Autor der Noitz sind. + email_subject: "%{author_name} hat auf Ihre private Notiz unter %{resource_title} geantwortet." + notification_title: %{author_name} hat Ihre private Notiz unter %{resource_title} beantworter. Jetzt im Admin-Panel anzeigen. collaborative_draft_access_accepted: email_intro: '%{requester_name} wurde zur Mitwirkung am kollaborativen Entwurf %{resource_title} akzeptiert.' email_outro: Sie haben diese Benachrichtigung erhalten, weil Sie bei %{resource_title} mitwirken. @@ -473,7 +496,9 @@ de: form: note: Anmerkung submit: Absenden - leave_your_note: Anmerkung hinterlassen + reply: + error: Beim Erstellen der Antwort zu dieser Vorschlagsnotiz ist ein Fehler aufgetreten. + success: Antwort auf die Vorschlagsnotiz erfolgreich erstellt. title: Private Notizen proposal_states: create: @@ -512,7 +537,7 @@ de: select_a_meeting: Besprechung auswählen index: actions: Aktionen - assign_to_valuator: Zuweisen zu dem Schätzer + assign_to_valuator: Zuweisen zur Bewertung assign_to_valuator_button: Zuweisen cancel: Abbrechen change_category: Kategorie ändern @@ -528,7 +553,7 @@ de: split_button: Aufteilen statuses: Status title: Vorschläge - unassign_from_valuator: Zuweisung an den Schätzer zurückziehen + unassign_from_valuator: Zuweisung zur Bewertung zurückziehen unassign_from_valuator_button: Zuweisung aufheben update: Aktualisieren update_scope_button: Umfang aktualisieren @@ -540,7 +565,7 @@ de: select_a_proposal: Bitte wählen Sie einen Vorschlag. show: amendments_count: Anzahl der Ergänzungen - assigned_valuators: Zugewiesene Schätzer + assigned_valuators: Zugewiesene Bewertende body: Haupttext comments_count: Anzahl der Kommentare documents: Unterlagen @@ -554,8 +579,8 @@ de: ranking: "%{ranking} von %{total}" related_meetings: Ähnliche Besprechungen remove_assignment: Zuweisung entfernen - remove_assignment_confirmation: Sind Sie sicher, diesen Schätzer von diesem Vorschlag zurückzuziehen? - valuators: Schätzer + remove_assignment_confirmation: Sind Sie sicher, dass Sie den BewerterIn dieses Vorschlags entfernen möchten? + valuators: Bewertende votes_count: Anzahl Stimmen update_category: invalid: 'Diese Vorschläge gehörten bereits zur Kategorie %{subject_name}: %{proposals}.' @@ -587,11 +612,11 @@ de: success: Die Vorschläge wurden erfolgreich in neue aufgeteilt. valuation_assignments: create: - invalid: Bei der Zuweisung der Vorschläge an einen Schätzer ist ein Fehler aufgetreten. - success: Die Vorschläge wurden erfolgreich dem Schätzer zugewiesen. + invalid: Bei der Zuweisung der Vorschläge zur Bewertung ist ein Fehler aufgetreten. + success: Die Vorschläge wurden erfolgreich zur Bewertung zugewiesen. delete: - invalid: Beim Zurückziehen der Zuweisung der Vorschläge an einen Schätzer ist ein Fehler aufgetreten. - success: Die Zuweisung der Vorschläge wurden erfolgreich vom Schätzer zurückgezogen. + invalid: Beim Zurückziehen der Zuweisung der Vorschläge zur Bewertung ist ein Fehler aufgetreten. + success: Die Zuweisung zur Bewertung der Vorschläge wurde erfolgreich aufgehoben. admin_log: proposal: answer: "%{user_name} beantwortete den %{resource_name} Vorschlag auf den %{space_name} Feldern" @@ -601,8 +626,8 @@ de: proposal_note: create: "%{user_name} eine private Notiz zu dem %{resource_name} Vorschlag auf dem %{space_name} Feld hinterlassen" valuation_assignment: - create: "%{user_name} hat den Vorschlag %{resource_name} einem Schätzer zugewiesen" - delete: "%{user_name} hat die Zuweisung vom Vorschlag %{proposal_title} von einem Schätzer zurückgezogen" + create: "%{user_name} hat den Vorschlag %{resource_name} zur Bewertung zugewiesen" + delete: "%{user_name} hat die Zuweisung zur Bewertung des Vorschlags %{proposal_title} zurückgezogen" answers: accepted: Angenommen evaluating: In Bewertung @@ -748,8 +773,8 @@ de: scope: Umfang state: Status title: Titel - valuator: Schätzer - valuators: Schätzer + valuator: BewerterIn + valuators: Bewertende votes: Stimmen proposal_state: css_class: CSS-Klasse diff --git a/decidim-proposals/config/locales/el.yml b/decidim-proposals/config/locales/el.yml index 5423fb6386076..c875d647c2ce7 100644 --- a/decidim-proposals/config/locales/el.yml +++ b/decidim-proposals/config/locales/el.yml @@ -152,7 +152,6 @@ el: participatory_texts_enabled: Τα κείμενα συμμετοχής ενεργοποιήθηκαν participatory_texts_enabled_readonly: Δεν είναι δυνατή η αλληλεπίδραση με αυτήν τη ρύθμιση αν διατίθενται υπάρχουσες προτάσεις. Δημιουργήστε ένα νέο «Στοιχείο προτάσεων» αν θέλετε να ενεργοποιήσετε αυτήν τη δυνατότητα ή απορρίψτε όλες τις προτάσεις που έχουν εισαχθεί από το μενού «Κείμενα συμμετοχής» αν θέλετε να την απενεργοποιήσετε. proposal_answering_enabled: Η απάντηση στην πρόταση ενεργοποιήθηκε - proposal_edit_before_minutes: Οι προτάσεις μπορούν να υποβληθούν σε επεξεργασία από συντάκτες προτού περάσουν αρκετά λεπτά proposal_edit_time: Επεξεργασία πρότασης proposal_edit_time_choices: infinite: Επιτρέψτε την επεξεργασία προτάσεων για άπειρο χρονικό διάστημα @@ -200,10 +199,8 @@ el: proposals: admin: proposal_note_created: - email_intro: Κάποιος άφησε μια σημείωση στην πρόταση "%{resource_title}". Ελέγξτε την στο του πίνακα διαχείρισης. email_outro: Λάβατε αυτήν την ειδοποίηση επειδή μπορείτε να εκτιμήσετε την πρόταση. email_subject: Κάποιος άφησε μια σημείωση στην πρόταση %{resource_title}. - notification_title: Κάποιος άφησε μια σημείωση στην πρόταση %{resource_title}. Ελέγξτε την στο πίνακα διαχείρισης. collaborative_draft_access_accepted: email_intro: '%{requester_name} έχει γίνει δεκτή η πρόσβαση ως συνεισφέρων του %{resource_title} συνεργατικού σχεδίου.' email_outro: Έχετε λάβει αυτήν την ειδοποίηση επειδή είστε συνεργάτης του %{resource_title}. @@ -404,7 +401,6 @@ el: form: note: Σημείωση submit: Υποβολή - leave_your_note: Αφήστε τη σημείωσή σας title: Ιδιωτικές σημειώσεις proposals: answer: diff --git a/decidim-proposals/config/locales/es-MX.yml b/decidim-proposals/config/locales/es-MX.yml index c0f06ac0a0f9b..74b769a0b333a 100644 --- a/decidim-proposals/config/locales/es-MX.yml +++ b/decidim-proposals/config/locales/es-MX.yml @@ -170,6 +170,11 @@ es-MX: random: Aleatorio recent: Recientes with_more_authors: Con más autoras + edit_time: Las autoras pueden editar las propuestas antes de que pase este tiempo + edit_time_units: + days: Días + hours: Horas + minutes: Minutos geocoding_enabled: Geocodificación habilitada minimum_votes_per_user: Votos mínimos por usuario new_proposal_body_template: Plantilla para el texto de nueva propuesta @@ -179,11 +184,14 @@ es-MX: participatory_texts_enabled: Textos participativos habilitados participatory_texts_enabled_readonly: No se puede interactuar con esta configuración si hay propuestas existentes. Por favor, crea un nuevo componente de propuesta si quieres activar esta característica o descartar todas las propuestas importadas en el menú "Textos Participativos" si quieres desactivarla. proposal_answering_enabled: Respuesta a propuestas habilitadas - proposal_edit_before_minutes: Las propuestas pueden ser editadas por los autores antes de que pasen estos minutos proposal_edit_time: Edición de propuestas proposal_edit_time_choices: infinite: Permitir editar propuestas por una cantidad infinita de tiempo limited: Permitir la edición de propuestas dentro de un plazo específico + proposal_edit_time_unit_options: + days: Días + hours: Horas + minutes: Minutos proposal_length: Longitud máxima del cuerpo de la propuesta proposal_limit: Límite de propuestas por usuario proposal_wizard_step_1_help_text: Texto de ayuda para el paso "Crear" del asistente de propuesta @@ -234,11 +242,26 @@ es-MX: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Se te ha asignado como evaluadora de la propuesta "%{resource_title}". Esto significa que se ha confiado en ti para dar tu opinión y dar una respuesta adecuada en los próximos días. Puedes ver la propuesta en el panel de administración. + email_outro: Recibes esta notificación porque puedes evaluar la propuesta. + email_subject: Has sido asignada como evaluadora de la propuesta "%{resource_title}". + notification_title: Se te ha asignado como evaluadora de la propuesta "%{resource_title}". Puedes verla en el panel de administración. proposal_note_created: - email_intro: Alguien ha dejado una nota en la propuesta "%{resource_title}". Revísala ahora a través del panel de administración. + email_intro: '%{author_name} ha creado una nota privada en %{resource_title}. Puedes verlo en el panel de administración.' email_outro: Recibes esta notificación porque puedes evaluar la propuesta. email_subject: Alguien dejó una nota en la propuesta %{resource_title}. - notification_title: Alguien ha dejado una nota en la propuesta %{resource_title}. Revísala ahora a través del panel de administración. + notification_title: %{author_name} %{author_nickname} ha creado una nota privada en %{resource_title}. Puedes verla en el panel de administración. + proposal_note_mentioned: + email_intro: '"%{author_name}" "%{author_nickname}" te ha mencionado en una nota privada en "%{resource_title}". Puedes verla en el panel de administración.' + email_outro: Has recibido esta notificación porque se te ha mencionado en una nota privada. + email_subject: Alguien dejó una nota en la propuesta %{resource_title}. + notification_title: %{author_name} %{author_nickname} te ha mencionado en una nota privada en %{resource_title}. Puedes verla en el panel de administración. + proposal_note_replied: + email_intro: '%{author_name} ha respondido a tu nota privada en %{resource_title}. Puedes verlo en el panel de administración.' + email_outro: Has recibido esta notificación porque eres el autor de la nota privada. + email_subject: "%{author_name} ha respondido a tu nota privada en %{resource_title}." + notification_title: %{author_name} %{author_nickname} ha respondido a tu nota privada en %{resource_title}. Puedes verlo en el panel de administración. collaborative_draft_access_accepted: email_intro: '%{requester_name} ha sido aceptada para acceder como contribuidora del borrador colaborativo %{resource_title}.' email_outro: Has recibido esta notificación porque eres una contribuidora de %{resource_title}. @@ -473,7 +496,9 @@ es-MX: form: note: Nota submit: Enviar - leave_your_note: Deja tu nota + reply: + error: Se ha producido un error al crear esta respuesta en la nota privada. + success: La respuesta a la nota privada se ha creado con éxito. title: Notas privadas proposal_states: create: diff --git a/decidim-proposals/config/locales/es-PY.yml b/decidim-proposals/config/locales/es-PY.yml index 0d7b52140d4f8..260098a805e1c 100644 --- a/decidim-proposals/config/locales/es-PY.yml +++ b/decidim-proposals/config/locales/es-PY.yml @@ -170,6 +170,11 @@ es-PY: random: Aleatorio recent: Recientes with_more_authors: Con más autoras + edit_time: Las autoras pueden editar las propuestas antes de que pase este tiempo + edit_time_units: + days: Días + hours: Horas + minutes: Minutos geocoding_enabled: Geocodificación habilitada minimum_votes_per_user: Votos mínimos por usuario new_proposal_body_template: Plantilla para el texto de nueva propuesta @@ -179,11 +184,14 @@ es-PY: participatory_texts_enabled: Textos participativos habilitados participatory_texts_enabled_readonly: No se puede interactuar con esta configuración si hay propuestas existentes. Por favor, crea un nuevo componente de propuesta si quieres activar esta característica o descartar todas las propuestas importadas en el menú "Textos Participativos" si quieres desactivarla. proposal_answering_enabled: Respuesta a propuestas habilitadas - proposal_edit_before_minutes: Las propuestas pueden ser editadas por los autores antes de que pasen estos minutos proposal_edit_time: Edición de propuestas proposal_edit_time_choices: infinite: Permitir editar propuestas por una cantidad infinita de tiempo limited: Permitir la edición de propuestas dentro de un plazo específico + proposal_edit_time_unit_options: + days: Días + hours: Horas + minutes: Minutos proposal_length: Longitud máxima del cuerpo de la propuesta proposal_limit: Límite de propuestas por usuario proposal_wizard_step_1_help_text: Asistente de propuesta "Crear" paso texto de ayuda @@ -234,11 +242,26 @@ es-PY: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Se te ha asignado como evaluadora de la propuesta "%{resource_title}". Esto significa que se ha confiado en ti para dar tu opinión y dar una respuesta adecuada en los próximos días. Puedes ver la propuesta en el panel de administración. + email_outro: Recibes esta notificación porque puedes evaluar la propuesta. + email_subject: Has sido asignada como evaluadora de la propuesta "%{resource_title}". + notification_title: Se te ha asignado como evaluadora de la propuesta "%{resource_title}". Puedes verla en el panel de administración. proposal_note_created: - email_intro: Alguien ha dejado una nota en la propuesta "%{resource_title}". Revísala ahora a través del panel de administración. + email_intro: '%{author_name} ha creado una nota privada en %{resource_title}. Puedes verlo en el panel de administración.' email_outro: Recibes esta notificación porque puedes evaluar la propuesta. email_subject: Alguien dejó una nota en la propuesta %{resource_title}. - notification_title: Alguien ha dejado una nota en la propuesta %{resource_title}. Revísala ahora a través del panel de administración. + notification_title: %{author_name} %{author_nickname} ha creado una nota privada en %{resource_title}. Puedes verla en el panel de administración. + proposal_note_mentioned: + email_intro: '"%{author_name}" "%{author_nickname}" te ha mencionado en una nota privada en "%{resource_title}". Puedes verla en el panel de administración.' + email_outro: Has recibido esta notificación porque se te ha mencionado en una nota privada. + email_subject: Alguien dejó una nota en la propuesta %{resource_title}. + notification_title: %{author_name} %{author_nickname} te ha mencionado en una nota privada en %{resource_title}. Puedes verla en el panel de administración. + proposal_note_replied: + email_intro: '%{author_name} ha respondido a tu nota privada en %{resource_title}. Puedes verlo en el panel de administración.' + email_outro: Has recibido esta notificación porque eres el autor de la nota privada. + email_subject: "%{author_name} ha respondido a tu nota privada en %{resource_title}." + notification_title: %{author_name} %{author_nickname} ha respondido a tu nota privada en %{resource_title}. Puedes verlo en el panel de administración. collaborative_draft_access_accepted: email_intro: '%{requester_name} ha sido aceptada para acceder como contribuidora del borrador colaborativo %{resource_title}.' email_outro: Has recibido esta notificación porque eres una contribuidora de %{resource_title}. @@ -473,7 +496,9 @@ es-PY: form: note: Nota submit: Enviar - leave_your_note: Deja tu nota + reply: + error: Se ha producido un error al crear esta respuesta en la nota privada. + success: La respuesta a la nota privada se ha creado con éxito. title: Notas privadas proposal_states: create: diff --git a/decidim-proposals/config/locales/es.yml b/decidim-proposals/config/locales/es.yml index 7639928fbf743..a2aa690cd43f4 100644 --- a/decidim-proposals/config/locales/es.yml +++ b/decidim-proposals/config/locales/es.yml @@ -170,6 +170,11 @@ es: random: Aleatorio recent: Recientes with_more_authors: Con más autoras + edit_time: Las autoras pueden editar las propuestas antes de que pase este tiempo + edit_time_units: + days: Días + hours: Horas + minutes: Minutos geocoding_enabled: Geocodificación habilitada minimum_votes_per_user: Votos mínimos por usuario new_proposal_body_template: Plantilla para el texto de nueva propuesta @@ -179,11 +184,14 @@ es: participatory_texts_enabled: Textos participativos habilitados participatory_texts_enabled_readonly: No se puede interactuar con esta configuración si hay propuestas existentes. Por favor, crea un nuevo componente de propuesta si quieres activar esta característica o descartar todas las propuestas importadas en el menú "Textos Participativos" si quieres desactivarla. proposal_answering_enabled: Respuesta a propuestas habilitada - proposal_edit_before_minutes: Las propuestas pueden ser editadas por las autoras antes de que pasen estos minutos proposal_edit_time: Edición de propuestas proposal_edit_time_choices: infinite: Permitir editar propuestas por una cantidad infinita de tiempo limited: Permitir la edición de propuestas dentro de un plazo específico + proposal_edit_time_unit_options: + days: Días + hours: Horas + minutes: Minutos proposal_length: Longitud máxima del cuerpo de la propuesta proposal_limit: Límite de propuestas por participante proposal_wizard_step_1_help_text: Texto de ayuda para el paso "Crear" del asistente de propuestas @@ -234,11 +242,26 @@ es: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Se te ha asignado como evaluadora de la propuesta "%{resource_title}". Esto significa que se ha confiado en ti para dar tu opinión y dar una respuesta adecuada en los próximos días. Puedes ver la propuesta en el panel de administración. + email_outro: Recibes esta notificación porque puedes evaluar la propuesta. + email_subject: Has sido asignada como evaluadora de la propuesta "%{resource_title}". + notification_title: Se te ha asignado como evaluadora de la propuesta "%{resource_title}". Puedes verla en el panel de administración. proposal_note_created: - email_intro: Alguien ha dejado una nota en la propuesta "%{resource_title}". Revísala ahora a través del panel de administración. + email_intro: '%{author_name} ha creado una nota privada en %{resource_title}. Puedes verlo en el panel de administración.' email_outro: Recibes esta notificación porque puedes evaluar la propuesta. email_subject: Alguien dejó una nota en la propuesta %{resource_title}. - notification_title: Alguien ha dejado una nota en la propuesta %{resource_title}. Revísala ahora a través del panel de administración. + notification_title: %{author_name} %{author_nickname} ha creado una nota privada en %{resource_title}. Puedes verla en el panel de administración. + proposal_note_mentioned: + email_intro: '"%{author_name}" "%{author_nickname}" te ha mencionado en una nota privada en "%{resource_title}". Puedes verla en el panel de administración.' + email_outro: Has recibido esta notificación porque se te ha mencionado en una nota privada. + email_subject: Alguien dejó una nota en la propuesta %{resource_title}. + notification_title: %{author_name} %{author_nickname} te ha mencionado en una nota privada en %{resource_title}. Puedes verla en el panel de administración. + proposal_note_replied: + email_intro: '%{author_name} ha respondido a tu nota privada en %{resource_title}. Puedes verlo en el panel de administración.' + email_outro: Has recibido esta notificación porque eres el autor de la nota privada. + email_subject: "%{author_name} ha respondido a tu nota privada en %{resource_title}." + notification_title: %{author_name} %{author_nickname} ha respondido a tu nota privada en %{resource_title}. Puedes verlo en el panel de administración. collaborative_draft_access_accepted: email_intro: '%{requester_name} ha sido aceptada para acceder como contribuidora del borrador colaborativo %{resource_title}.' email_outro: Has recibido esta notificación porque eres una contribuidora de %{resource_title}. @@ -473,7 +496,9 @@ es: form: note: Nota submit: Enviar - leave_your_note: Deja tu nota + reply: + error: Se ha producido un error al crear esta respuesta en la nota privada. + success: La respuesta a la nota privada se ha creado con éxito. title: Notas privadas proposal_states: create: diff --git a/decidim-proposals/config/locales/eu.yml b/decidim-proposals/config/locales/eu.yml index 22a8cc9072434..e2d819124ee5d 100644 --- a/decidim-proposals/config/locales/eu.yml +++ b/decidim-proposals/config/locales/eu.yml @@ -176,7 +176,6 @@ eu: participatory_texts_enabled: Testu parte-hartzaileak gaituta participatory_texts_enabled_readonly: Proposamenak badaude, ezin da ezarpen honekin elkarreragin. Mesedez, sor ezazu proposamen-osagai berria, ezaugarri hau aktibatu nahi baduzu edo baztertu `Testu parte hartzaileak` menuan inportatutako proposamen guztiak, desaktibatu nahi baduzu. proposal_answering_enabled: Proposamena erantzutea gaituta dago - proposal_edit_before_minutes: Proposamenak egileek editatu ahal izango dituzte minutu asko igaro aurretik proposal_edit_time: Proposamenen edizioa proposal_edit_time_choices: infinite: Utzi proposamenak editatzen epe mugagabe batean @@ -232,10 +231,8 @@ eu: proposals: admin: proposal_note_created: - email_intro: 'Norbaitek ohar bat utzi du "%{resource_title}" proposamenean. Berrikusimorain hemen: the admin panel.' email_outro: Jakinarazpen hau jaso duzu proposamena ebaluatu ahal duzulako. email_subject: Norbaitek ohar bat utzi du %{resource_title} proposamenean. - notification_title: Norbaitek ohar bat utzi du %{resource_title} proposamenean. Berrikusi orain honen bidez panel de administración. collaborative_draft_access_accepted: email_intro: '%{requester_name} onartu da %{resource_title} zirriborro koloboratiboan laguntzaile gisa sartzeko.' email_outro: Jakinarazpen hau jaso duzu %{resource_title}-ko kolaboratzailea zarelako. @@ -471,7 +468,6 @@ eu: form: note: Oharra submit: Bidali - leave_your_note: Utzi zure oharra title: Ohar pribatuak proposal_states: create: diff --git a/decidim-proposals/config/locales/fi-plain.yml b/decidim-proposals/config/locales/fi-plain.yml index 25e3ef6f2ccc3..6da134609434d 100644 --- a/decidim-proposals/config/locales/fi-plain.yml +++ b/decidim-proposals/config/locales/fi-plain.yml @@ -170,6 +170,11 @@ fi-pl: random: Satunnainen recent: Viimeisimmät with_more_authors: Eniten tekijöitä + edit_time: Ehdotuksia voi muokata tämän aikamääreen sisällä + edit_time_units: + days: Päivää + hours: Tuntia + minutes: Minuuttia geocoding_enabled: Geokoodaus käytössä minimum_votes_per_user: Vähimmäisäänimäärä käyttäjää kohden new_proposal_body_template: Uuden ehdotuksen leipätekstin mallipohja @@ -179,11 +184,14 @@ fi-pl: participatory_texts_enabled: Ehdotusaineistot ovat käytössä participatory_texts_enabled_readonly: Tätä asetusta ei voi muuttaa, mikäli ehdotuksia on jo olemassa. Luo uusi "Ehdotukset-komponentti", mikäli haluat ottaa tämän ominaisuuden käyttöön tai hylkää kaikki tuodut ehdotukset "Ehdotusaineistot" -toiminnosta, mikäli haluat ottaa sen pois käytöstä. proposal_answering_enabled: Ehdotukseen vastaaminen käytössä - proposal_edit_before_minutes: Tekijät voivat muokata ehdotuksia tämän ajan sisällä (minuuttia) proposal_edit_time: Ehdotusten muokkaus proposal_edit_time_choices: infinite: Ehdotusten muokkaus on sallittu ilman aikarajaa limited: Ehdotusten muokkaus on sallittu määritetyn aikarajan sisällä niiden julkaisusta + proposal_edit_time_unit_options: + days: Päivää + hours: Tuntia + minutes: Minuuttia proposal_length: Ehdotuksen runkotekstin merkkien enimmäismäärä proposal_limit: Ehdotusten enimmäismäärä käyttäjää kohden proposal_wizard_step_1_help_text: Ehdotuksen luonnin "Luo" -vaiheen ohjeteksti @@ -234,11 +242,26 @@ fi-pl: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Sinulle on annettu arvioitavaksi ehdotus "%{resource_title}". Tämä tarkoittaa, että sinut on valtuutettu antamaan palautetta ja vastaamaan ehdotukseen tulevien päivien aikana. Tarkasta ehdotus hallintapaneelin kautta. + email_outro: Tämä ilmoitus on lähetetty sinulle, koska voit arvioida ehdotuksen. + email_subject: Sinulle on annettu arvioitavaksi ehdotus %{resource_title}. + notification_title: Sinulle on annettu arvioitavaksi ehdotus "%{resource_title}". Tarkasta ehdotus hallintapaneelin kautta. proposal_note_created: - email_intro: Joku on jättänyt muistiinpanon ehdotukseen "%{resource_title}". Tutustu siihen hallintapaneelin kautta. + email_intro: '%{author_name} on luonut yksityisen muistiinpanon kohteeseen %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta.' email_outro: Saat tämän ilmoituksen, koska voit arvioida ehdotuksen. email_subject: Joku on jättänyt huomion ehdotukseen %{resource_title}. - notification_title: Joku on jättänyt muistiinpanon ehdotukseen %{resource_title}. Tutustu siihen hallintapaneelin kautta. + notification_title: %{author_name} on luonut yksityisen muistiinpanon kohteeseen %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta. + proposal_note_mentioned: + email_intro: Sinut on mainittu yksityisessä muistiinpanossa "%{resource_title}" henkilön "%{author_name}" "%{author_nickname}" toimesta. Tarkista muistiinpano hallintapaneelin kautta. + email_outro: Tämä ilmoitus on lähetetty sinulle, koska sinut on mainittu yksityisessä muistiinpanossa. + email_subject: Sinut on mainittu ehdotuksen %{resource_title} muistiinpanossa. + notification_title: Sinut on mainittu yksityisessä muistiinpanossa %{resource_title} henkilön %{author_name} %{author_nickname} toimesta. Tarkista se hallintapaneelin kautta. + proposal_note_replied: + email_intro: '%{author_name} on vastannut yksityiseen muistiinpanoon kohteessa %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta.' + email_outro: Sait tämän ilmoituksen, koska olet muistiinpanon laatija. + email_subject: "%{author_name} on vastannut yksityiseen muistiinpanoon kohteessa %{resource_title}." + notification_title: %{author_name}%{author_nickname} on vastannut yksityiseen muistiinpanoosi kohteessa %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta. collaborative_draft_access_accepted: email_intro: '%{requester_name} on hyväksytty osallistujaksi yhteistyöluonnokseen %{resource_title}.' email_outro: Tämä ilmoitus on lähetetty sinulle, koska olet osallistujana kohteessa %{resource_title}. @@ -472,7 +495,9 @@ fi-pl: form: note: Huomautus submit: Lähetä - leave_your_note: Jätä huomautuksesi + reply: + error: Muistiinpanon vastauksen luonti ehdotukselle epäonnistui. + success: Muistiinpanon vastauksen luonti ehdotukselle onnistui. title: Omat muistiinpanot proposal_states: create: diff --git a/decidim-proposals/config/locales/fi.yml b/decidim-proposals/config/locales/fi.yml index f689417e56e0b..98d39038bd0ea 100644 --- a/decidim-proposals/config/locales/fi.yml +++ b/decidim-proposals/config/locales/fi.yml @@ -170,6 +170,11 @@ fi: random: Satunnainen recent: Viimeisimmät with_more_authors: Eniten tekijöitä + edit_time: Ehdotuksia voi muokata tämän aikamääreen sisällä + edit_time_units: + days: Päivää + hours: Tuntia + minutes: Minuuttia geocoding_enabled: Geokoodaus käytössä minimum_votes_per_user: Vähimmäisäänimäärä käyttäjää kohden new_proposal_body_template: Uuden ehdotuksen leipätekstin mallipohja @@ -179,11 +184,14 @@ fi: participatory_texts_enabled: Ehdotusaineistot ovat käytössä participatory_texts_enabled_readonly: Tätä asetusta ei voi muuttaa, mikäli ehdotuksia on jo olemassa. Luo uusi "Ehdotukset-komponentti", mikäli haluat ottaa tämän ominaisuuden käyttöön tai hylkää kaikki tuodut ehdotukset "Ehdotusaineistot" -toiminnosta, mikäli haluat ottaa sen pois käytöstä. proposal_answering_enabled: Ehdotukseen vastaaminen käytössä - proposal_edit_before_minutes: Tekijät voivat muokata ehdotuksia tämän ajan sisällä (minuuttia) proposal_edit_time: Ehdotusten muokkaus proposal_edit_time_choices: infinite: Ehdotusten muokkaus on sallittu ilman aikarajaa limited: Ehdotusten muokkaus on sallittu määritetyn aikarajan sisällä niiden julkaisusta + proposal_edit_time_unit_options: + days: Päivää + hours: Tuntia + minutes: Minuuttia proposal_length: Ehdotuksen runkotekstin merkkien enimmäismäärä proposal_limit: Ehdotusten enimmäismäärä käyttäjää kohden proposal_wizard_step_1_help_text: Ehdotuksen luonnin "Luo" -vaiheen ohjeteksti @@ -234,11 +242,26 @@ fi: events: proposals: admin: + proposal_assigned_to_valuator: + email_intro: Sinulle on annettu arvioitavaksi ehdotus "%{resource_title}". Tämä tarkoittaa, että sinut on valtuutettu antamaan palautetta ja vastaamaan ehdotukseen tulevien päivien aikana. Tarkasta ehdotus hallintapaneelin kautta. + email_outro: Tämä ilmoitus on lähetetty sinulle, koska voit arvioida ehdotuksen. + email_subject: Sinulle on annettu arvioitavaksi ehdotus %{resource_title}. + notification_title: Sinulle on annettu arvioitavaksi ehdotus "%{resource_title}". Tarkasta ehdotus hallintapaneelin kautta. proposal_note_created: - email_intro: Joku on jättänyt muistiinpanon ehdotukseen "%{resource_title}". Tutustu siihen hallintapaneelin kautta. + email_intro: '%{author_name} on luonut yksityisen muistiinpanon kohteeseen %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta.' email_outro: Tämä ilmoitus on lähetetty sinulle, koska voit arvioida ehdotuksen. email_subject: Joku on jättänyt huomion ehdotukseen %{resource_title}. - notification_title: Joku on jättänyt muistiinpanon ehdotukseen %{resource_title}. Tutustu siihen hallintapaneelin kautta. + notification_title: %{author_name} on luonut yksityisen muistiinpanon kohteeseen %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta. + proposal_note_mentioned: + email_intro: Sinut on mainittu yksityisessä muistiinpanossa "%{resource_title}" henkilön "%{author_name}" "%{author_nickname}" toimesta. Tarkista muistiinpano hallintapaneelin kautta. + email_outro: Tämä ilmoitus on lähetetty sinulle, koska sinut on mainittu yksityisessä muistiinpanossa. + email_subject: Sinut on mainittu ehdotuksen %{resource_title} muistiinpanossa. + notification_title: Sinut on mainittu yksityisessä muistiinpanossa %{resource_title} henkilön %{author_name} %{author_nickname} toimesta. Tarkista se hallintapaneelin kautta. + proposal_note_replied: + email_intro: '%{author_name} on vastannut yksityiseen muistiinpanoon kohteessa %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta.' + email_outro: Sait tämän ilmoituksen, koska olet muistiinpanon laatija. + email_subject: "%{author_name} on vastannut yksityiseen muistiinpanoon kohteessa %{resource_title}." + notification_title: %{author_name}%{author_nickname} on vastannut yksityiseen muistiinpanoosi kohteessa %{resource_title}. Tarkasta muistiinpano hallintapaneelin kautta. collaborative_draft_access_accepted: email_intro: '%{requester_name} on hyväksytty osallistujaksi yhteistyöluonnokseen %{resource_title}.' email_outro: Tämä ilmoitus on lähetetty sinulle, koska olet osallistujana kohteessa %{resource_title}. @@ -472,7 +495,9 @@ fi: form: note: Huomautus submit: Lähetä - leave_your_note: Jätä huomautuksesi + reply: + error: Muistiinpanon vastauksen luonti ehdotukselle epäonnistui. + success: Muistiinpanon vastauksen luonti ehdotukselle onnistui. title: Omat muistiinpanot proposal_states: create: diff --git a/decidim-proposals/config/locales/fr-CA.yml b/decidim-proposals/config/locales/fr-CA.yml index dc7989f567516..45e2ba3caec25 100644 --- a/decidim-proposals/config/locales/fr-CA.yml +++ b/decidim-proposals/config/locales/fr-CA.yml @@ -179,7 +179,6 @@ fr-CA: participatory_texts_enabled: Textes participatifs activés participatory_texts_enabled_readonly: Impossible d'interagir avec ce paramètre s'il y a des propositions existantes. Veuillez créer une nouvelle fonctionnalité `Propositions` si vous voulez activer cette fonctionnalité ou supprimer toutes les propositions importées dans 'Textes participatifs` si vous voulez la désactiver. proposal_answering_enabled: Autoriser la réponse officielle aux propositions - proposal_edit_before_minutes: Délai (en minutes) après lequel les auteurs ne peuvent plus modifier leurs propositions proposal_edit_time: Durée d'édition des propositions proposal_edit_time_choices: infinite: Autoriser l'édition des propositions pour une durée infinie @@ -234,11 +233,13 @@ fr-CA: events: proposals: admin: + proposal_assigned_to_valuator: + email_outro: Vous avez reçu cette notification car vous pouvez évaluer la proposition. + email_subject: Vous avez été assigné en tant qu'évaluateur à la proposition %{resource_title}. + notification_title: Vous avez été assigné en tant qu'évaluateur à la proposition %{resource_title}. Jetez-y un œil sur le panneau d'administration. proposal_note_created: - email_intro: Quelqu'un a laissé une note sur la proposition "%{resource_title}". Consultez-la sur le panneau d'administration. email_outro: Vous avez reçu cette notification car vous pouvez évaluer la proposition. email_subject: Quelqu'un a laissé une note sur la proposition %{resource_title}. - notification_title: Quelqu'un a laissé une note sur la proposition %{resource_title}. Consultez-la sur le panneau d'administration. collaborative_draft_access_accepted: email_intro: '%{requester_name} a été accepté en tant que contributeur du brouillon collaboratif %{resource_title}.' email_outro: Vous avez reçu cette notification car vous êtes collaborateur de %{resource_title}. Si vous souhaitez vous désabonner des notifications, connectez-vous à la plateforme, puis rendez-vous dans l'onglet “Mon compte” > “Paramètres des notifications”. @@ -473,7 +474,6 @@ fr-CA: form: note: Remarque submit: Soumettre - leave_your_note: Laisser une remarque title: Notes privées proposal_states: create: diff --git a/decidim-proposals/config/locales/fr.yml b/decidim-proposals/config/locales/fr.yml index 9ece416ec9bf2..d94ee7e43aac9 100644 --- a/decidim-proposals/config/locales/fr.yml +++ b/decidim-proposals/config/locales/fr.yml @@ -179,7 +179,6 @@ fr: participatory_texts_enabled: Textes participatifs activés participatory_texts_enabled_readonly: Impossible d'interagir avec ce paramètre s'il y a des propositions existantes. Veuillez créer une nouvelle fonctionnalité `Propositions` si vous voulez activer cette fonctionnalité ou supprimer toutes les propositions importées dans 'Textes participatifs` si vous voulez la désactiver. proposal_answering_enabled: Autoriser la réponse officielle aux propositions - proposal_edit_before_minutes: Délai (en minutes) après lequel les auteurs ne peuvent plus modifier leurs propositions proposal_edit_time: Durée d'édition des propositions proposal_edit_time_choices: infinite: Autoriser l'édition des propositions pour une durée infinie @@ -234,11 +233,13 @@ fr: events: proposals: admin: + proposal_assigned_to_valuator: + email_outro: Vous avez reçu cette notification car vous pouvez évaluer la proposition. + email_subject: Vous avez été assigné en tant qu'évaluateur à la proposition %{resource_title}. + notification_title: Vous avez été assigné en tant qu'évaluateur à la proposition %{resource_title}. Jetez-y un œil sur le panneau d'administration. proposal_note_created: - email_intro: Quelqu'un a laissé une note sur la proposition "%{resource_title}". Consultez-la sur le panneau d'administration. email_outro: Vous avez reçu cette notification car vous pouvez évaluer la proposition. Si vous souhaitez vous désabonner des notifications, connectez-vous à la plateforme, puis rendez-vous dans l'onglet “Mon compte” > “Paramètres des notifications”. email_subject: Quelqu'un a laissé une note sur la proposition %{resource_title}. - notification_title: Quelqu'un a laissé une note sur la proposition %{resource_title}. Consultez-la sur le panneau d'administration. collaborative_draft_access_accepted: email_intro: '%{requester_name} a été accepté en tant que contributeur du brouillon collaboratif %{resource_title}.' email_outro: Vous avez reçu cette notification car vous êtes collaborateur de %{resource_title}. Si vous souhaitez vous désabonner des notifications, connectez-vous à la plateforme, puis rendez-vous dans l'onglet “Mon compte” > “Paramètres des notifications”. @@ -473,7 +474,6 @@ fr: form: note: Remarque submit: Soumettre - leave_your_note: Laisser une remarque title: Notes privées proposal_states: create: diff --git a/decidim-proposals/config/locales/gl.yml b/decidim-proposals/config/locales/gl.yml index a949fcfdb59a9..505c6a2ebaac4 100644 --- a/decidim-proposals/config/locales/gl.yml +++ b/decidim-proposals/config/locales/gl.yml @@ -131,7 +131,6 @@ gl: official_proposals_enabled: Propostas oficiais habilitadas participatory_texts_enabled: Permitir textos participativos proposal_answering_enabled: Contestando a proposta habilitada - proposal_edit_before_minutes: As propostas poden ser editadas por autores antes de que pase moitos minutos proposal_length: Lonxitude máxima do corpo da proposta proposal_limit: Límite de proposta por usuario proposal_wizard_step_1_help_text: Asistente de propostas "Crear" texto de axuda paso a paso @@ -315,7 +314,6 @@ gl: form: note: Nota submit: Enviar - leave_your_note: Deixe a súa nota title: Notas privadas proposals: edit: diff --git a/decidim-proposals/config/locales/hu.yml b/decidim-proposals/config/locales/hu.yml index 9f99873576b62..31ed0be41031f 100644 --- a/decidim-proposals/config/locales/hu.yml +++ b/decidim-proposals/config/locales/hu.yml @@ -132,7 +132,6 @@ hu: participatory_texts_enabled: Engedélyezett részvételi szövegek participatory_texts_enabled_readonly: Meglévő javaslatok esetén nem lehet alkalmazni ezt a beállítást. Legyen szíves, egy új 'Javaslatok komponens' -t hozzon létre, hogy ezt a funkciót használja, vagy vesse el az importált javaslatokat a "Részvételi Szövegek" menüben, ha ki akarja kapcsolni. proposal_answering_enabled: Javaslat válasz engedélyezve - proposal_edit_before_minutes: A javaslatokat a szerzők az ülés lezárásáig szerkeszthetik proposal_edit_time: Javaslat szerkesztése proposal_edit_time_choices: infinite: A javaslatok szerkesztésének ne legyen időbeli korlátja @@ -176,10 +175,8 @@ hu: proposals: admin: proposal_note_created: - email_intro: Valaki egy jegyzetet írt a(z) "%{resource_title}" javaslathoz. Ellenőrizze az adminisztrációs panelen. email_outro: Azért kapta ezt az értesítést, mert értékelheti a javaslatot. email_subject: Valaki egy jegyzetet írt a(z) "%{resource_title}" javaslathoz. - notification_title: Valaki egy jegyzetet írt a(z) %{resource_title} javaslathoz. Ellenőrizze az adminisztrációs panelen. collaborative_draft_access_accepted: email_subject: "%{requester_name} jóváhagyva, mint közreműködő ebben: %{resource_title}." notification_title: '%{requester_name} %{requester_nickname} jóváhagyva, mint a közreműködő ebben a közös vázlatban: %{resource_title}.' @@ -362,7 +359,6 @@ hu: form: note: Jegyzet submit: Küldés - leave_your_note: Jegyzet elhagyása title: Privát jegyzetek proposal_states: create: diff --git a/decidim-proposals/config/locales/id-ID.yml b/decidim-proposals/config/locales/id-ID.yml index c337d3e51425a..d62ef3226b130 100644 --- a/decidim-proposals/config/locales/id-ID.yml +++ b/decidim-proposals/config/locales/id-ID.yml @@ -76,7 +76,6 @@ id: official_proposals_enabled: Proposal resmi diaktifkan participatory_texts_enabled: Teks partisipatif diaktifkan proposal_answering_enabled: Pengangkatan proposal diaktifkan - proposal_edit_before_minutes: Proposal dapat diedit oleh penulis sebelum ini banyak menit berlalu proposal_length: Panjang badan proposal maksimum proposal_limit: Batas proposal per pengguna proposal_wizard_step_1_help_text: Panduan proposal "Buat" teks bantuan langkah @@ -233,7 +232,6 @@ id: form: note: Catatan submit: Menyerahkan - leave_your_note: Tinggalkan catatanmu title: Catatan pribadi proposals: edit: diff --git a/decidim-proposals/config/locales/is-IS.yml b/decidim-proposals/config/locales/is-IS.yml index 0486e39d7ad06..983aa16eb0297 100644 --- a/decidim-proposals/config/locales/is-IS.yml +++ b/decidim-proposals/config/locales/is-IS.yml @@ -31,7 +31,6 @@ is-IS: new_proposal_help_text: Ný tillaga hjálpartexta official_proposals_enabled: Opinberar tillögur virkar proposal_answering_enabled: Tillaga svarað virkt - proposal_edit_before_minutes: Tillögur má breyta af höfundum áður en þessi mörg mínútur fara fram proposal_length: Hámark umsóknar lengd líkamans proposal_wizard_step_1_help_text: Tillaga töframaður "Búa til" skref hjálpartexta threshold_per_proposal: Gildi fyrir hverja tillögu @@ -87,7 +86,6 @@ is-IS: form: note: Athugaðu submit: Senda inn - leave_your_note: Skildu athugasemdina þína title: Einkaskilaboð proposals: form: diff --git a/decidim-proposals/config/locales/it.yml b/decidim-proposals/config/locales/it.yml index bbae6bef5fc01..303b006b1cad2 100644 --- a/decidim-proposals/config/locales/it.yml +++ b/decidim-proposals/config/locales/it.yml @@ -136,7 +136,6 @@ it: participatory_texts_enabled: Testi di partecipazione abilitati participatory_texts_enabled_readonly: Non è possibile interagire con questa impostazione se ci sono proposte esistenti. Per favore, crea un nuovo componente `Proposte` se si desidera abilitare questa funzione o elimina tutte le proposte importate nel menu `Testi partecipativi` se si desidera disabilitarla. proposal_answering_enabled: Risposta alla proposta abilitata - proposal_edit_before_minutes: Minuti dall'inserimento dopo i quali le proposte non potranno piùessere modificate dagli autori proposal_edit_time: Modifica proposta proposal_edit_time_choices: infinite: Consenti la modifica delle proposte per un tempo infinito @@ -328,7 +327,6 @@ it: form: note: Nota submit: Invia - leave_your_note: Lascia il tuo messaggio title: Note private proposals: edit: diff --git a/decidim-proposals/config/locales/ja.yml b/decidim-proposals/config/locales/ja.yml index b0c3088857f4f..4e4c0a8779a55 100644 --- a/decidim-proposals/config/locales/ja.yml +++ b/decidim-proposals/config/locales/ja.yml @@ -175,7 +175,6 @@ ja: participatory_texts_enabled: 参加型テキストを有効にする participatory_texts_enabled_readonly: 既存の提案がある場合は、この設定は操作できません。 この機能を有効にする場合は、新しい `提案 コンポーネント` を作成し、もし無効にしたい場合はインポートされたすべての提案を `参加型 テキスト` メニューから破棄してください。 proposal_answering_enabled: 提案への回答を有効にする - proposal_edit_before_minutes: 作成者が提案を作成してから編集が可能な時間(分) proposal_edit_time: 提案の編集 proposal_edit_time_choices: infinite: いつでも提案を編集することを許可する @@ -231,10 +230,14 @@ ja: proposals: admin: proposal_note_created: - email_intro: 誰かが提案 "%{resource_title}" にメモを残しました。 管理者パネルで確認してください。 + email_intro: '%{author_name} は %{resource_title} でプライベートノートを作成しました。 管理パネル で確認してください。' email_outro: 提案を評価できるため、この通知を受け取りました。 email_subject: 誰かが提案 %{resource_title} にメモを残しました。 - notification_title: 誰かが提案 %{resource_title} にメモを残しました。詳細は管理者パネルで確認してください。 + notification_title: %{author_name} %{author_nickname}%{resource_title}にプライベートノートを作成しました。 管理者パネル で確認してください。 + proposal_note_mentioned: + email_intro: '"%{resource_title}" 内の "%{author_name}" "%{author_nickname}" によるプライベートノートで言及されました。管理パネルで確認してください。' + email_outro: プライベートノートで言及されているため、この通知を受信しました。 + email_subject: 誰かが提案 %{resource_title} のメモであなたをメンションしました。 collaborative_draft_access_accepted: email_intro: '%{requester_name} は、 %{resource_title} の共同草案のコントリビューターとして承認されました。' email_outro: %{resource_title} のコラボレーターであるため、この通知を受け取りました。 @@ -467,7 +470,6 @@ ja: form: note: メモ submit: 送信 - leave_your_note: メモを残してください title: プライベートノート proposal_states: create: diff --git a/decidim-proposals/config/locales/lt.yml b/decidim-proposals/config/locales/lt.yml index 9a71bb3dd73dc..0561484ec5d1d 100644 --- a/decidim-proposals/config/locales/lt.yml +++ b/decidim-proposals/config/locales/lt.yml @@ -158,7 +158,6 @@ lt: participatory_texts_enabled: Dalyvaujamieji tekstai aktyvuoti participatory_texts_enabled_readonly: Jei yra pasiūlymų, negalima sąveika su šia nuostata. Sukurkite naują „Pasiūlymų komponentą“, jei norite aktyvinti šią funkciją arba, jei norite išjungti šią funkciją, visus importuotus pasiūlymus pašalinkite iš laukelio „Bendri tekstai“ meniu. proposal_answering_enabled: Atsakymas į pasiūlymą aktyvintas - proposal_edit_before_minutes: Kol nepraeis nurodytas skaičius minučių, autoriai gali keisti pasiūlymus proposal_edit_time: Pasiūlymų koregavimas proposal_edit_time_choices: infinite: Lesti redaguoti pasiūlymus neribotą laiką @@ -206,10 +205,8 @@ lt: proposals: admin: proposal_note_created: - email_intro: Kažkas paliko pastabą dėl pasiūlymo „%{resource_title}“. Pasitikrinkite tai administratoriaus srityje. email_outro: Šį pranešimą gavote dėl to, kad galite vertinti pasiūlymą. email_subject: Kažkas paliko pastabą dėl pasiūlymo „%{resource_title}“. - notification_title: Kažkas paliko pastabą dėl pasiūlymo %{resource_title}. Peržiūrėkite ją administratoriaus srityje. collaborative_draft_access_accepted: email_intro: '%{requester_name} buvo priimtas kaip %{resource_title} bendradarbiavimo juodraščio bendradarbis.' email_outro: Šį pranešimą gavote dėl to, kad esate %{resource_title} bendraautorius. @@ -416,7 +413,6 @@ lt: form: note: Pastaba submit: Pateikti - leave_your_note: Palikite pastabą title: Privačios pastabos proposals: answer: diff --git a/decidim-proposals/config/locales/lv.yml b/decidim-proposals/config/locales/lv.yml index 96c2864870979..540d570a307b0 100644 --- a/decidim-proposals/config/locales/lv.yml +++ b/decidim-proposals/config/locales/lv.yml @@ -101,7 +101,6 @@ lv: participatory_texts_enabled: Līdzdalības teksti ir iespējoti participatory_texts_enabled_readonly: Nevar mijiedarboties ar šo iestatījumu, ja jau ir priekšlikumi. Lūdzu, izveidojiet priekšlikumu komponentu, ja vēlaties iespējot šo funkciju, vai atmetiet visus importētos priekšlikumus izvēlnē „Līdzdalības teksti”, ja vēlaties to atspējot. proposal_answering_enabled: Atbilde uz priekšlikumiem ir iespējota - proposal_edit_before_minutes: Autori var rediģēt priekšlikumus, pirms ir pagājušas tik daudz minūtes proposal_length: Maksimālais priekšlikuma pamatteksta garums proposal_limit: Priekšlikumu skaita limits vienam dalībniekam proposal_wizard_step_1_help_text: Priekšlikumu vedņa „Izveidot” soļa palīdzības teksts @@ -277,7 +276,6 @@ lv: form: note: Piezīme submit: Iesniegt - leave_your_note: Atstājiet savu piezīmi title: Privātas piezīmes proposals: edit: diff --git a/decidim-proposals/config/locales/nl.yml b/decidim-proposals/config/locales/nl.yml index 39c38798acccf..3b229d1eebc34 100644 --- a/decidim-proposals/config/locales/nl.yml +++ b/decidim-proposals/config/locales/nl.yml @@ -124,7 +124,6 @@ nl: participatory_texts_enabled: Participatieve teksten ingeschakeld participatory_texts_enabled_readonly: Instelling niet aanpasbaar als er bestaande voorstellen zijn. Maak een nieuwe `Voorstellen component` aan als u deze functie wilt inschakelen of wijs alle geïmporteerde voorstellen in het `Participatieve Teksten` menu af, als u de instelling wilt uitschakelen. proposal_answering_enabled: Reacties op voorstellen ingeschakeld - proposal_edit_before_minutes: Voorstellen kunnen door auteurs worden bewerkt voordat de tijd (uitgedrukt in minuten) verstreken is proposal_edit_time: Voorstel bewerken proposal_edit_time_choices: infinite: Voorstellen onbeperkt in tijd laten bewerken @@ -337,7 +336,6 @@ nl: form: note: Opmerking submit: Bevestigen - leave_your_note: Laat je opmerking achter title: Privé-opmerkingen proposals: edit: diff --git a/decidim-proposals/config/locales/no.yml b/decidim-proposals/config/locales/no.yml index 0865c4170b2e4..e92d67a9dfe4f 100644 --- a/decidim-proposals/config/locales/no.yml +++ b/decidim-proposals/config/locales/no.yml @@ -136,7 +136,6 @@ participatory_texts_enabled: Deltakende tekster aktivert participatory_texts_enabled_readonly: Kan ikke samhandle med denne innstillingen hvis det finnes eksisterende forslag. Vennligst, opprett et nytt "Forslags komponent" hvis du vil aktivere denne funksjonen eller forkaste alle importerte forslag i `Deltakende tekster` -menyen hvis du vil deaktivere den. proposal_answering_enabled: Forslags besvaring aktivert - proposal_edit_before_minutes: Forslag kan redigeres av forfattere før så mange minutter går proposal_edit_time: Forslagsredigering proposal_edit_time_choices: infinite: Tillat å redigere forslag til en uendelig tidsperiode @@ -337,7 +336,6 @@ form: note: Merk submit: Send inn - leave_your_note: Legg igjen notatene dine title: Private notater proposals: edit: diff --git a/decidim-proposals/config/locales/pl.yml b/decidim-proposals/config/locales/pl.yml index 4591ba36bd731..8e17b356ed216 100644 --- a/decidim-proposals/config/locales/pl.yml +++ b/decidim-proposals/config/locales/pl.yml @@ -182,7 +182,6 @@ pl: participatory_texts_enabled: Włącz kolektywną legislację participatory_texts_enabled_readonly: Nie można korzystać z tego ustawienia, jeśli są już propozycje. Utwórz nowy komponent propozycji, jeśli chcesz włączyć tę funkcję, lub odrzuć wszystkie zaimportowane propozycje w menu „Kolektywna Legislacja”, jeśli chcesz ją wyłączyć. proposal_answering_enabled: Włączono odpowiadanie na propozycję - proposal_edit_before_minutes: Propozycje mogą być edytowane przez autorów przed upływem tylu minut proposal_edit_time: Edycja propozycji proposal_edit_time_choices: infinite: Zezwalaj na edycję propozycji przez nieograniczony czas @@ -238,10 +237,8 @@ pl: proposals: admin: proposal_note_created: - email_intro: Ktoś zostawił notatkę do propozycji „%{resource_title}”. Sprawdź ją w panelu administratora. email_outro: Otrzymujesz to powiadomienie, ponieważ możesz weryfikować propozycję. email_subject: Ktoś zostawił notatkę do propozycji %{resource_title}. - notification_title: Ktoś zostawił notatkę do propozycji %{resource_title}. Sprawdź ją w panelu administratora. collaborative_draft_access_accepted: email_intro: '%{requester_name} otrzymał(a) dostęp jako współtwórca wspólnego szkicu %{resource_title}.' email_outro: Otrzymujesz to powiadomienie, ponieważ współpracujesz przy dokumencie %{resource_title}. @@ -475,7 +472,6 @@ pl: form: note: Notatka submit: Zatwierdź - leave_your_note: Zostaw swoją notatkę title: Prywatne notatki proposal_states: create: diff --git a/decidim-proposals/config/locales/pt-BR.yml b/decidim-proposals/config/locales/pt-BR.yml index 3dd0e1f116759..57363c26c8cda 100644 --- a/decidim-proposals/config/locales/pt-BR.yml +++ b/decidim-proposals/config/locales/pt-BR.yml @@ -140,7 +140,6 @@ pt-BR: participatory_texts_enabled: Textos participativos habilitados participatory_texts_enabled_readonly: Não é possível interagir com esta configuração se houver propostas existentes. Por Favor, crie um novo componente `Propostas` se você quiser habilitar esta funcionalidade ou descartar todas as propostas importadas no menu `Textos Participatórios` se você quiser desativá-la. proposal_answering_enabled: Resposta de proposta ativada - proposal_edit_before_minutes: As propostas podem ser editadas pelos autores antes que muitos minutos passem. proposal_edit_time: Editando proposta proposal_edit_time_choices: infinite: Permitir a edição de propostas por um período infinito de tempo @@ -381,7 +380,6 @@ pt-BR: form: note: Nota submit: Enviar - leave_your_note: Deixe sua nota title: Notas privadas proposals: edit: diff --git a/decidim-proposals/config/locales/pt.yml b/decidim-proposals/config/locales/pt.yml index 36ef3f4f77c63..7a89107bb51e2 100644 --- a/decidim-proposals/config/locales/pt.yml +++ b/decidim-proposals/config/locales/pt.yml @@ -141,7 +141,6 @@ pt: participatory_texts_enabled: Textos participativos ativados participatory_texts_enabled_readonly: Não é possível interagir com esta configuração caso existam propostas. Crie um novo "Componente de propostas" caso pretenda ativar esta característica ou elimine todas as propostas importadas no menu "Textos Participativos" caso pretenda desativá-la. proposal_answering_enabled: Respostas à proposta ativadas - proposal_edit_before_minutes: As propostas podem ser editadas pelos autores antes que estes minutos passem proposal_edit_time: Edição de proposta proposal_edit_time_choices: infinite: Permitir editar propostas por um limite de tempo indeterminado @@ -340,7 +339,6 @@ pt: form: note: Nota submit: Submeter - leave_your_note: Deixe a sua nota title: Notas privadas proposals: edit: diff --git a/decidim-proposals/config/locales/ro-RO.yml b/decidim-proposals/config/locales/ro-RO.yml index cb644d9a4ef83..a5c232252b104 100644 --- a/decidim-proposals/config/locales/ro-RO.yml +++ b/decidim-proposals/config/locales/ro-RO.yml @@ -144,7 +144,6 @@ ro: participatory_texts_enabled: Modulul texte participative a fost activat participatory_texts_enabled_readonly: Nu se poate interacționa cu această setare dacă există deja propuneri. Te rugăm, creează o nouă componenta 'Propuneri' dacă dorești să activezi această funcționalitate. Dacă vrei să o dezactivezi mergi în meniul `Texte participative`și renunță la toate propunerile importate. proposal_answering_enabled: Modulul de răspuns pentru propuneri a fost activat - proposal_edit_before_minutes: Propunerile pot fi editate de către autori până la expirarea termenului definit mai jos în minute proposal_edit_time: Durata editării propunerii proposal_edit_time_choices: infinite: Permite editarea propunerilor pentru o perioadă infinită de timp @@ -356,7 +355,6 @@ ro: form: note: Notă submit: Trimite - leave_your_note: Lasă-ți nota title: Note private proposals: edit: diff --git a/decidim-proposals/config/locales/ru.yml b/decidim-proposals/config/locales/ru.yml index c71d795a6204c..5ee1458778bc1 100644 --- a/decidim-proposals/config/locales/ru.yml +++ b/decidim-proposals/config/locales/ru.yml @@ -65,7 +65,6 @@ ru: new_proposal_help_text: Подсказки по созданию нового предложения official_proposals_enabled: Включена возможность выдвигать служебные предложения proposal_answering_enabled: Включена возможность отвечать на предложения - proposal_edit_before_minutes: Предложения могут быть отредактированы авторами до того, как пройдет столько минут proposal_length: Предельная длина основного текста предложения proposal_limit: Предельное количество предложений от одного участника proposal_wizard_step_1_help_text: Справка мастера предложений о шаге "Создать" @@ -131,7 +130,6 @@ ru: form: note: Примечание submit: Отправить - leave_your_note: Оставьте свое примечание title: Частные примечания proposals: form: diff --git a/decidim-proposals/config/locales/sk.yml b/decidim-proposals/config/locales/sk.yml index c9fdf4d93516c..ae46a642f6e9a 100644 --- a/decidim-proposals/config/locales/sk.yml +++ b/decidim-proposals/config/locales/sk.yml @@ -107,7 +107,6 @@ sk: participatory_texts_enabled: Povolené participatívne texty participatory_texts_enabled_readonly: Ak existujú návrhy, nemožno toto nastavenie meniť. Prosíme vytvorte novú "Súčasť návrhov", ak ho chcete povoliť, alebo zahoďte všetky importované návrhy v "Participatívnych textoch" v menu, ak ho chcete zakázať. proposal_answering_enabled: Odpovedanie na návrh je povolené - proposal_edit_before_minutes: Návrhy môžu byť editované autormi až po uplynutí toľkoto minút proposal_length: Maximálna dĺžka návrhu proposal_limit: Limit návrhu na užívateľa proposal_wizard_step_1_help_text: Pomocný text Sprievodcu "Vytvoriť" návrh @@ -281,7 +280,6 @@ sk: form: note: Poznámka submit: Predložiť - leave_your_note: Nechajte svoju poznámku title: Súkromné ​​poznámky proposals: edit: diff --git a/decidim-proposals/config/locales/sv.yml b/decidim-proposals/config/locales/sv.yml index a146c46c63edc..94320491aeb1c 100644 --- a/decidim-proposals/config/locales/sv.yml +++ b/decidim-proposals/config/locales/sv.yml @@ -151,7 +151,6 @@ sv: participatory_texts_enabled: Deltagartexter är aktiverade participatory_texts_enabled_readonly: Det går inte att ändra denna inställning om det finns befintliga förslag. Skapa en ny `Förslagskomponent` om du vill aktivera funktionen, eller kasta alla importerade förslag under menyn `Deltagartexter` om du vill avaktivera den. proposal_answering_enabled: Aktiverat svar på förslag - proposal_edit_before_minutes: Förslag kan redigeras av författare inom så här många minuter proposal_edit_time: Redigering av förslag proposal_edit_time_choices: infinite: Tillåt redigering av förslag utan tidsbegränsning @@ -369,7 +368,6 @@ sv: form: note: Annteckning submit: Skicka in - leave_your_note: Lämna anteckningen title: Privata anteckningar proposals: edit: diff --git a/decidim-proposals/config/locales/tr-TR.yml b/decidim-proposals/config/locales/tr-TR.yml index a1b92bf224b28..ee55395ecfbf7 100644 --- a/decidim-proposals/config/locales/tr-TR.yml +++ b/decidim-proposals/config/locales/tr-TR.yml @@ -137,7 +137,6 @@ tr: participatory_texts_enabled: Katılımcı metinler etkinleştirildi participatory_texts_enabled_readonly: Mevcut teklifler varsa bu ayarla etkileşim kurulamaz. Bu özelliği etkinleştirmek istiyorsanız lütfen yeni bir "Teklifler bileşeni" oluşturun veya devre dışı bırakmak istiyorsanız "Katılımcı Metinler" menüsünde içe aktarılan tüm teklifleri iptal edin. proposal_answering_enabled: Teklif yanıtlama etkin - proposal_edit_before_minutes: Teklifler, bu birkaç dakika geçmeden yazarlar tarafından düzenlenebilir proposal_length: Maksimum teklif gövdesi uzunluğu proposal_limit: Katılımcı başına teklif sınırı proposal_wizard_step_1_help_text: Teklif sihirbazı "Oluştur" adımı yardım metni @@ -339,7 +338,6 @@ tr: form: note: Not submit: Gönder - leave_your_note: Notunu bırak title: Özel notlar proposal_states: create: diff --git a/decidim-proposals/config/locales/uk.yml b/decidim-proposals/config/locales/uk.yml index ae428dda000fb..4495d5f6af1e1 100644 --- a/decidim-proposals/config/locales/uk.yml +++ b/decidim-proposals/config/locales/uk.yml @@ -65,7 +65,6 @@ uk: new_proposal_help_text: Підказки зі внесення нової пропозиції official_proposals_enabled: Службові пропозиції увімкнено proposal_answering_enabled: Увімкнено відповіді на пропозиції - proposal_edit_before_minutes: Пропозиції можуть бути відредаговані авторами до того, як пройде стільки хвилин proposal_length: Гранична довжина основного тексту пропозиції proposal_limit: Гранична кількість пропозицій від одного учасника proposal_wizard_step_1_help_text: Довідка майстра пропозицій щодо кроку "Створити" @@ -131,7 +130,6 @@ uk: form: note: Примітка submit: Надіслати - leave_your_note: Залиште свою нотатку title: Приватні примітки proposals: form: diff --git a/decidim-proposals/config/locales/zh-CN.yml b/decidim-proposals/config/locales/zh-CN.yml index a220de61e3ac5..b75475461f3c5 100644 --- a/decidim-proposals/config/locales/zh-CN.yml +++ b/decidim-proposals/config/locales/zh-CN.yml @@ -116,7 +116,6 @@ zh-CN: participatory_texts_enabled: 参与性案文已启用 participatory_texts_enabled_readonly: 如果有现存的提议,无法与此设置进行交互。 请, 如果您想要启用此功能,或在"参与文本"菜单中放弃所有导入的提议,则创建一个新的 "建议组件"。 proposal_answering_enabled: 建议答案已启用 - proposal_edit_before_minutes: 建议可以由作者在这么多分钟过去之前编辑 proposal_length: 最大建议体长度 proposal_limit: 每个参与者的建议限制 proposal_wizard_step_1_help_text: 建议向导“创建”步骤帮助文本 @@ -294,7 +293,6 @@ zh-CN: form: note: 说明 submit: 提交 - leave_your_note: 留下您的便笺 title: 私人笔记 proposals: edit: diff --git a/decidim-proposals/config/locales/zh-TW.yml b/decidim-proposals/config/locales/zh-TW.yml index e807611447835..90d3d7c229741 100644 --- a/decidim-proposals/config/locales/zh-TW.yml +++ b/decidim-proposals/config/locales/zh-TW.yml @@ -143,7 +143,6 @@ zh-TW: participatory_texts_enabled: 參與式文字已啟用 participatory_texts_enabled_readonly: 如果已經存在提案,則無法與此設置互動。如果您想要啟用此功能,請創建一個新的“提案組件”,或者如果要禁用此功能,請在“參與式文字”選單中放棄所有已匯入的提案。 proposal_answering_enabled: 提案回答已啟用 - proposal_edit_before_minutes: 在幾分鐘後,作者可以編輯提案 proposal_edit_time: 編輯提案 proposal_edit_time_choices: infinite: 允許無限時間編輯提案 @@ -191,10 +190,8 @@ zh-TW: proposals: admin: proposal_note_created: - email_intro: 有人在提案「%{resource_title}」上留了一則留言。在管理員面板檢查它。 email_outro: 您收到此通知,因為您可以對提案進行評估。 email_subject: 有人在提案 %{resource_title} 上留下了一則註記。 - notification_title: 有人在提案「%{resource_title}」上留了一則留言。請至管理面板查看。 collaborative_draft_access_accepted: email_intro: '%{requester_name} 已經被接受成為 %{resource_title} 協作草稿的貢獻者。' email_outro: 您收到此通知,是因為您是 %{resource_title} 的協作者。 @@ -395,7 +392,6 @@ zh-TW: form: note: 附註 submit: 提交 - leave_your_note: 留下您的註記 title: 私有注解 proposals: answer: From 7b39b5a414bc0d82d224b93c2bc2eb6fb53528ab Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 30 Jul 2024 16:10:48 +0300 Subject: [PATCH 06/22] Add spring as dependency (#13235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add spring as dependency * Fix issues * Remove spring from ./lib/decidim/gem_manager.rb * Update RELEASE_NOTES.md Co-authored-by: Andrés Pereira de Lucena * Apply suggestions from code review Co-authored-by: Andrés Pereira de Lucena --------- Co-authored-by: Andrés Pereira de Lucena --- Gemfile | 2 -- Gemfile.lock | 4 ++-- RELEASE_NOTES.md | 15 ++++++++++++++- decidim-dev/decidim-dev.gemspec | 2 ++ decidim-generators/Gemfile | 2 -- decidim-generators/Gemfile.lock | 4 ++-- .../lib/decidim/generators/app_generator.rb | 5 ----- .../generators/component_templates/Gemfile.erb | 2 -- lib/decidim/gem_manager.rb | 2 -- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/Gemfile b/Gemfile index bb3a2a7ab0deb..722594c1ac409 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,5 @@ end group :development do gem "letter_opener_web", "~> 2.0" gem "listen", "~> 3.1" - gem "spring", "~> 4.0" - gem "spring-watcher-listen", "~> 2.0" gem "web-console", "~> 4.2" end diff --git a/Gemfile.lock b/Gemfile.lock index d6c6710bc65a8..7b1ea44818882 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,6 +143,8 @@ PATH selenium-webdriver (~> 4.9) simplecov (~> 0.22.0) simplecov-cobertura (~> 2.1.0) + spring (~> 4.0) + spring-watcher-listen (~> 2.0) w3c_rspec_validators (~> 0.3.0) webmock (~> 3.18) wisper-rspec (~> 1.0) @@ -810,8 +812,6 @@ DEPENDENCIES listen (~> 3.1) parallel_tests (~> 4.2) puma (>= 6.3.1) - spring (~> 4.0) - spring-watcher-listen (~> 2.0) web-console (~> 4.2) RUBY VERSION diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b51cab7fb8ed7..184a233fadd08 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -17,6 +17,7 @@ gem "decidim-dev", github: "decidim/decidim" ```console sudo apt install p7zip # or the alternative installation process for your operating system. See "2.1. 7zip dependency introduction" +bundle remove spring spring-watcher-listen bundle update decidim bin/rails decidim:upgrade bin/rails db:migrate @@ -42,7 +43,19 @@ You can read more about this change on PR [#13185](https://github.com/decidim/de These are one time actions that need to be done after the code is updated in the production database. -### 3.1. [[TITLE OF THE ACTION]] +### 3.1. Remove spring and spring-watcher-listen from your Gemfile + +To simplify the upgrade process, we have decided to add `spring` and `spring-watcher-listener` as hard dependencies of `decidim-dev`. + +Before upgrading to this version, make sure you run in your console: + +```bash +bundle remove spring spring-watcher-listen +``` + +You can read more about this change on PR [#13235](https://github.com/decidim/decidim/pull/13235). + +### 3.2. [[TITLE OF THE ACTION]] You can read more about this change on PR [#XXXX](https://github.com/decidim/decidim/pull/XXXX). diff --git a/decidim-dev/decidim-dev.gemspec b/decidim-dev/decidim-dev.gemspec index abc210bf9f72a..2d4f9e2753fd0 100644 --- a/decidim-dev/decidim-dev.gemspec +++ b/decidim-dev/decidim-dev.gemspec @@ -68,6 +68,8 @@ Gem::Specification.new do |s| s.add_dependency "selenium-webdriver", "~> 4.9" s.add_dependency "simplecov", "~> 0.22.0" s.add_dependency "simplecov-cobertura", "~> 2.1.0" + s.add_dependency "spring", "~> 4.0" + s.add_dependency "spring-watcher-listen", "~> 2.0" s.add_dependency "w3c_rspec_validators", "~> 0.3.0" s.add_dependency "webmock", "~> 3.18" s.add_dependency "wisper-rspec", "~> 1.0" diff --git a/decidim-generators/Gemfile b/decidim-generators/Gemfile index d3cb2cfc3c49b..e6310f98ec10c 100644 --- a/decidim-generators/Gemfile +++ b/decidim-generators/Gemfile @@ -29,7 +29,5 @@ end group :development do gem "letter_opener_web", "~> 2.0" gem "listen", "~> 3.1" - gem "spring", "~> 4.0" - gem "spring-watcher-listen", "~> 2.0" gem "web-console", "~> 4.2" end diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 9e26d731d3cdb..3d6f66c0c0c9e 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -143,6 +143,8 @@ PATH selenium-webdriver (~> 4.9) simplecov (~> 0.22.0) simplecov-cobertura (~> 2.1.0) + spring (~> 4.0) + spring-watcher-listen (~> 2.0) w3c_rspec_validators (~> 0.3.0) webmock (~> 3.18) wisper-rspec (~> 1.0) @@ -813,8 +815,6 @@ DEPENDENCIES net-pop (~> 0.1.1) net-smtp (~> 0.3.1) puma (>= 6.3.1) - spring (~> 4.0) - spring-watcher-listen (~> 2.0) web-console (~> 4.2) wicked_pdf (~> 2.1) diff --git a/decidim-generators/lib/decidim/generators/app_generator.rb b/decidim-generators/lib/decidim/generators/app_generator.rb index 8961f88cf29f8..44ab1f1d45f50 100644 --- a/decidim-generators/lib/decidim/generators/app_generator.rb +++ b/decidim-generators/lib/decidim/generators/app_generator.rb @@ -357,11 +357,6 @@ def dev_performance_config end end CONFIG - - if ENV.fetch("RAILS_BOOST_PERFORMANCE", false).to_s == "true" - gsub_file "Gemfile", /gem "spring".*/, "# gem \"spring\"" - gsub_file "Gemfile", /gem "spring-watcher-listen".*/, "# gem \"spring-watcher-listen\"" - end end def authorization_handler diff --git a/decidim-generators/lib/decidim/generators/component_templates/Gemfile.erb b/decidim-generators/lib/decidim/generators/component_templates/Gemfile.erb index fa5a466daf9f0..ebffebf0695b2 100644 --- a/decidim-generators/lib/decidim/generators/component_templates/Gemfile.erb +++ b/decidim-generators/lib/decidim/generators/component_templates/Gemfile.erb @@ -21,7 +21,5 @@ group :development do gem "faker", "~> 3.2" gem "letter_opener_web", "~> 2.0" gem "listen", "~> 3.1" - gem "spring", "~> 4.0" - gem "spring-watcher-listen", "~> 2.0" gem "web-console", "~> 4.2" end diff --git a/lib/decidim/gem_manager.rb b/lib/decidim/gem_manager.rb index 30ad364b33f45..4283d28b1d076 100644 --- a/lib/decidim/gem_manager.rb +++ b/lib/decidim/gem_manager.rb @@ -223,8 +223,6 @@ def patch_component_gemfile_generator gem "faker", "#{Gem.loaded_specs["decidim-dev"].dependencies.select { |a| a.name == "faker" }.first.requirement}" gem "letter_opener_web", "#{fetch_gemfile_version("letter_opener_web")}" gem "listen", "#{fetch_gemfile_version("listen")}" - gem "spring", "#{fetch_gemfile_version("spring")}" - gem "spring-watcher-listen", "#{fetch_gemfile_version("spring-watcher-listen")}" gem "web-console", "#{fetch_gemfile_version("web-console")}" end ) From f3b367cfe29822ad1fda691e6dd3bce2dc84b18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 30 Jul 2024 17:37:45 +0200 Subject: [PATCH 07/22] Remove pagination configuration initializer (#13210) * Remove pagination configuration initializer * Remove useless helper * Remove useless concern * Change default pagination values * Fix specs --- decidim-admin/README.md | 13 ----------- .../concerns/decidim/admin/filterable.rb | 2 +- .../concerns/decidim/admin/paginable.rb | 20 ----------------- .../decidim/admin/application_controller.rb | 1 - .../admin/paginable/per_page_helper.rb | 22 ------------------- decidim-admin/lib/decidim/admin.rb | 15 ------------- .../manage_paginated_collection_examples.rb | 6 ++--- .../admin_manages_global_moderations_spec.rb | 2 +- .../participatory_space_private_user_spec.rb | 6 ++--- ...e_admin_manages_global_moderations_spec.rb | 2 +- ...derator_manages_global_moderations_spec.rb | 2 +- .../manage_assembly_members_examples.rb | 6 ++--- ...rator_manages_assembly_moderations_spec.rb | 2 +- .../spec/system/explore_posts_spec.rb | 6 ++--- .../system/admin_manages_comments_spec.rb | 2 +- .../shared/manage_media_links_examples.rb | 6 ++--- ...tor_manages_conference_moderations_spec.rb | 2 +- .../controllers/concerns/decidim/paginable.rb | 2 +- .../paginated_resource_examples.rb | 10 ++++----- .../spec/system/last_activity_spec.rb | 2 +- decidim-core/spec/system/search_spec.rb | 6 ++--- .../admin/admin_manages_comments_spec.rb | 2 +- ...erator_manages_process_moderations_spec.rb | 2 +- .../admin/proposal_states_controller.rb | 2 +- .../admin/admin_manages_proposals_spec.rb | 2 +- 25 files changed, 35 insertions(+), 108 deletions(-) delete mode 100644 decidim-admin/app/controllers/concerns/decidim/admin/paginable.rb delete mode 100644 decidim-admin/app/helpers/decidim/admin/paginable/per_page_helper.rb diff --git a/decidim-admin/README.md b/decidim-admin/README.md index 30ab9341197cf..aee1d4636f2c6 100644 --- a/decidim-admin/README.md +++ b/decidim-admin/README.md @@ -42,19 +42,6 @@ There are some pages that exist by default and cannot be deleted since there are links to them inside the Decidim framework, see `Decidim::StaticPage` for the default list. -### Pager Configuration - -The number of results shown per page and per page range can be configured in the app `decidim.rb` initializer as follows: - -```ruby -Decidim::Admin.configure do |config| - config.per_page_range = [15, 50, 100] -end -``` - -* `Decidim::Admin.per_page_range.first` sets the `default_per_page` value for `Decidim::Admin` (in Kaminari) -* `Decidim::Admin.per_page_range.last` sets the `max_per_page` value for `Decidim::Admin` (in Kaminari) - ## Contributing See [Decidim](https://github.com/decidim/decidim). diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb b/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb index e04c659b8573d..d3412478782e7 100644 --- a/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb +++ b/decidim-admin/app/controllers/concerns/decidim/admin/filterable.rb @@ -9,7 +9,7 @@ module Filterable extend ActiveSupport::Concern included do - include Decidim::Admin::Paginable + include Decidim::Paginable include Decidim::TranslatableAttributes helper Decidim::Admin::FilterableHelper diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/paginable.rb b/decidim-admin/app/controllers/concerns/decidim/admin/paginable.rb deleted file mode 100644 index ea1e082755622..0000000000000 --- a/decidim-admin/app/controllers/concerns/decidim/admin/paginable.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require "active_support/concern" - -module Decidim - module Admin - module Paginable - # Common logic to paginate admin resources - extend ActiveSupport::Concern - - included do - include Decidim::Paginable - - def per_page - params[:per_page].present? ? params[:per_page].to_i : Decidim::Admin.per_page_range.first - end - end - end - end -end diff --git a/decidim-admin/app/controllers/decidim/admin/application_controller.rb b/decidim-admin/app/controllers/decidim/admin/application_controller.rb index 49e4e064f3f9c..7c610b5f9d79c 100644 --- a/decidim-admin/app/controllers/decidim/admin/application_controller.rb +++ b/decidim-admin/app/controllers/decidim/admin/application_controller.rb @@ -27,7 +27,6 @@ class ApplicationController < ::DecidimController helper Decidim::Admin::IconWithTooltipHelper helper Decidim::Admin::MenuHelper helper Decidim::Admin::ScopesHelper - helper Decidim::Admin::Paginable::PerPageHelper helper Decidim::DecidimFormHelper helper Decidim::ReplaceButtonsHelper helper Decidim::ScopesHelper diff --git a/decidim-admin/app/helpers/decidim/admin/paginable/per_page_helper.rb b/decidim-admin/app/helpers/decidim/admin/paginable/per_page_helper.rb deleted file mode 100644 index bb9715bde28cf..0000000000000 --- a/decidim-admin/app/helpers/decidim/admin/paginable/per_page_helper.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Decidim - module Admin - module Paginable - # This module includes helpers the :per_page cell's option - module PerPageHelper - def per_page_options - OpenStruct.new( - per_page:, - per_page_range: Decidim::Admin.per_page_range - ) - end - - # Renders the pagination dropdown menu in the admin panel. - def admin_filters_pagination - cell("decidim/admin/results_per_page", per_page_options) - end - end - end - end -end diff --git a/decidim-admin/lib/decidim/admin.rb b/decidim-admin/lib/decidim/admin.rb index 60edff72e7145..a7778ccba621b 100644 --- a/decidim-admin/lib/decidim/admin.rb +++ b/decidim-admin/lib/decidim/admin.rb @@ -17,21 +17,6 @@ module Admin include ActiveSupport::Configurable - # Public Setting that configures Kaminari configuration options - # https://github.com/kaminari/kaminari#general-configuration-options - - # Range of number of results per_page. Defaults to [15, 50, 100]. - # per_page_range.first sets the default number per page - # per_page_range.last sets the default max_per_page - config_accessor :per_page_range do - [15, 50, 100] - end - - Kaminari.configure do |config| - config.default_per_page = Decidim::Admin.per_page_range.first - config.max_per_page = Decidim::Admin.per_page_range.last - end - # Public: Stores an instance of ViewHooks def self.view_hooks @view_hooks ||= ViewHooks.new diff --git a/decidim-admin/lib/decidim/admin/test/manage_paginated_collection_examples.rb b/decidim-admin/lib/decidim/admin/test/manage_paginated_collection_examples.rb index c7ea21248be81..6912eff355be7 100644 --- a/decidim-admin/lib/decidim/admin/test/manage_paginated_collection_examples.rb +++ b/decidim-admin/lib/decidim/admin/test/manage_paginated_collection_examples.rb @@ -12,13 +12,13 @@ end describe "Number of results per page" do - it "lists 15 resources per page by default" do - expect(page).to have_css(".table-list tbody tr", count: 15) + it "lists 25 resources per page by default" do + expect(page).to have_css(".table-list tbody tr", count: 25) end it "changes the number of results per page" do within "[data-pagination]" do - page.find("details", text: "15").click + page.find("details", text: "25").click click_on "50" end diff --git a/decidim-admin/spec/system/admin_manages_global_moderations_spec.rb b/decidim-admin/spec/system/admin_manages_global_moderations_spec.rb index 70cb195495800..5d2beb672e60a 100644 --- a/decidim-admin/spec/system/admin_manages_global_moderations_spec.rb +++ b/decidim-admin/spec/system/admin_manages_global_moderations_spec.rb @@ -92,7 +92,7 @@ end it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } let(:moderations_link_text) { "Global moderations" } let(:moderations_link_in_admin_menu) { false } end diff --git a/decidim-admin/spec/system/participatory_space_private_user_spec.rb b/decidim-admin/spec/system/participatory_space_private_user_spec.rb index c3407c96840fb..9ff2a75bb9044 100644 --- a/decidim-admin/spec/system/participatory_space_private_user_spec.rb +++ b/decidim-admin/spec/system/participatory_space_private_user_spec.rb @@ -8,11 +8,9 @@ let!(:user) { create(:user, :admin, :confirmed, organization:) } let(:assembly) { create(:assembly, organization:, private_space: true) } + let!(:private_users) { create_list(:assembly_private_user, 26, privatable_to: assembly, user: create(:user, organization: assembly.organization)) } + before do - 21.times do |_i| - user = create(:user, organization:) - create(:assembly_private_user, user:, privatable_to: assembly) - end switch_to_host(organization.host) login_as user, scope: :user visit decidim_admin_assemblies.edit_assembly_path(assembly) diff --git a/decidim-admin/spec/system/space_admin_manages_global_moderations_spec.rb b/decidim-admin/spec/system/space_admin_manages_global_moderations_spec.rb index 6336aaf105de9..ff91bb1ef37f7 100644 --- a/decidim-admin/spec/system/space_admin_manages_global_moderations_spec.rb +++ b/decidim-admin/spec/system/space_admin_manages_global_moderations_spec.rb @@ -75,7 +75,7 @@ end it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } let(:moderations_link_text) { "Global moderations" } let(:moderations_link_in_admin_menu) { false } end diff --git a/decidim-admin/spec/system/space_moderator_manages_global_moderations_spec.rb b/decidim-admin/spec/system/space_moderator_manages_global_moderations_spec.rb index 8854e9d60510a..15583647b28de 100644 --- a/decidim-admin/spec/system/space_moderator_manages_global_moderations_spec.rb +++ b/decidim-admin/spec/system/space_moderator_manages_global_moderations_spec.rb @@ -51,7 +51,7 @@ end it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } let(:moderations_link_text) { "Global moderations" } let(:moderations_link_in_admin_menu) { false } end diff --git a/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb b/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb index 2688fa4e79a6b..337d494252b32 100644 --- a/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb +++ b/decidim-assemblies/spec/shared/manage_assembly_members_examples.rb @@ -151,7 +151,7 @@ end context "when paginating" do - let!(:collection_size) { 20 } + let!(:collection_size) { 30 } let!(:collection) { create_list(:assembly_member, collection_size, assembly:) } let!(:resource_selector) { "#assembly_members tbody tr" } @@ -159,8 +159,8 @@ visit current_path end - it "lists 15 members per page by default" do - expect(page).to have_css(resource_selector, count: 15) + it "lists 25 members per page by default" do + expect(page).to have_css(resource_selector, count: 25) expect(page).to have_css("[data-pages] [data-page]", count: 2) click_on "Next" expect(page).to have_css("[data-pages] [data-page][aria-current='page']", text: "2") diff --git a/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb b/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb index 83335630cb36a..a6e4c89791a8f 100644 --- a/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb +++ b/decidim-assemblies/spec/system/admin/assembly_moderator_manages_assembly_moderations_spec.rb @@ -19,6 +19,6 @@ it_behaves_like "manage moderations" it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } end end diff --git a/decidim-blogs/spec/system/explore_posts_spec.rb b/decidim-blogs/spec/system/explore_posts_spec.rb index 44f3f2ea1e276..e847238e3b5df 100644 --- a/decidim-blogs/spec/system/explore_posts_spec.rb +++ b/decidim-blogs/spec/system/explore_posts_spec.rb @@ -48,15 +48,15 @@ end context "when paginating" do - let(:collection_size) { 15 } + let(:collection_size) { 25 } let!(:collection) { create_list(:post, collection_size, component:) } before do visit_component end - it "lists 10 resources per page by default" do - expect(page).to have_css("#blogs > a", count: 10) + it "lists 25 resources per page by default" do + expect(page).to have_css("#blogs > a", count: 25) expect(page).to have_css("[data-pages] [data-page]", count: 2) end end diff --git a/decidim-comments/spec/system/admin_manages_comments_spec.rb b/decidim-comments/spec/system/admin_manages_comments_spec.rb index 716797f3f8b74..7064550a41a15 100644 --- a/decidim-comments/spec/system/admin_manages_comments_spec.rb +++ b/decidim-comments/spec/system/admin_manages_comments_spec.rb @@ -20,6 +20,6 @@ it_behaves_like "manage moderations" it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } end end diff --git a/decidim-conferences/spec/shared/manage_media_links_examples.rb b/decidim-conferences/spec/shared/manage_media_links_examples.rb index 85f849b369afa..002402605ad4f 100644 --- a/decidim-conferences/spec/shared/manage_media_links_examples.rb +++ b/decidim-conferences/spec/shared/manage_media_links_examples.rb @@ -94,7 +94,7 @@ end context "when paginating" do - let!(:collection_size) { 15 } + let!(:collection_size) { 30 } let!(:collection) { create_list(:media_link, collection_size, conference:) } let!(:resource_selector) { "#media_links tbody tr" } @@ -102,8 +102,8 @@ visit current_path end - it "lists 10 media links per page by default" do - expect(page).to have_css(resource_selector, count: 10) + it "lists 25 media links per page by default" do + expect(page).to have_css(resource_selector, count: 25) expect(page).to have_css("[data-pages] [data-page]", count: 2) click_on "Next" expect(page).to have_css("[data-pages] [data-page][aria-current='page']", text: "2") diff --git a/decidim-conferences/spec/system/admin/conference_moderator_manages_conference_moderations_spec.rb b/decidim-conferences/spec/system/admin/conference_moderator_manages_conference_moderations_spec.rb index 0df5fd7ed2d69..76c0352ea166d 100644 --- a/decidim-conferences/spec/system/admin/conference_moderator_manages_conference_moderations_spec.rb +++ b/decidim-conferences/spec/system/admin/conference_moderator_manages_conference_moderations_spec.rb @@ -19,6 +19,6 @@ it_behaves_like "manage moderations" it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } end end diff --git a/decidim-core/app/controllers/concerns/decidim/paginable.rb b/decidim-core/app/controllers/concerns/decidim/paginable.rb index 3d0795e5008b8..72cc699a0beb1 100644 --- a/decidim-core/app/controllers/concerns/decidim/paginable.rb +++ b/decidim-core/app/controllers/concerns/decidim/paginable.rb @@ -7,7 +7,7 @@ module Decidim module Paginable extend ActiveSupport::Concern - OPTIONS = [10, 20, 50, 100].freeze + OPTIONS = [25, 50, 100].freeze included do helper_method :per_page, :page_offset diff --git a/decidim-core/lib/decidim/core/test/shared_examples/paginated_resource_examples.rb b/decidim-core/lib/decidim/core/test/shared_examples/paginated_resource_examples.rb index 200c72ae0aa54..88abe67291092 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/paginated_resource_examples.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/paginated_resource_examples.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true shared_examples "a paginated resource" do - let(:collection_size) { 30 } + let(:collection_size) { 50 } before do visit_component end - it "lists 10 resources per page by default" do - expect(page).to have_css(resource_selector, count: 10) - expect(page).to have_css("[data-pages] [data-page]", count: 3) + it "lists 25 resources per page by default" do + expect(page).to have_css(resource_selector, count: 25) + expect(page).to have_css("[data-pages] [data-page]", count: 2) end it "results per page can be changed from the selector" do expect(page).to have_css("[data-pagination]") within "[data-pagination]" do - page.find("summary", text: "10").click + page.find("summary", text: "25").click click_on "50" end diff --git a/decidim-core/spec/system/last_activity_spec.rb b/decidim-core/spec/system/last_activity_spec.rb index 98927847a8f4c..f8c442331795c 100644 --- a/decidim-core/spec/system/last_activity_spec.rb +++ b/decidim-core/spec/system/last_activity_spec.rb @@ -112,7 +112,7 @@ end context "when there are recently update old activities" do - let(:commentables) { create_list(:dummy_resource, 20, component:) } + let(:commentables) { create_list(:dummy_resource, 50, component:) } let(:comments) { commentables.map { |commentable| create(:comment, commentable:) } } let!(:action_logs) do comments.map do |comment| diff --git a/decidim-core/spec/system/search_spec.rb b/decidim-core/spec/system/search_spec.rb index 5b8618d161261..fefe6c9e19f43 100644 --- a/decidim-core/spec/system/search_spec.rb +++ b/decidim-core/spec/system/search_spec.rb @@ -70,16 +70,16 @@ context "when there is a malformed URL" do let(:participatory_space) { create(:participatory_process, :published, :with_steps, organization:) } let!(:proposal_component) { create(:proposal_component, participatory_space:) } - let!(:proposals) { create_list(:proposal, 11, component: proposal_component) } + let!(:proposals) { create_list(:proposal, 50, component: proposal_component) } before do proposals.each { |s| s.update(published_at: Time.current) } end it "displays the results page" do - visit %{/search?filter[with_resource_type]=Decidim::Proposals::Proposal&page=2&per_page=10'"()%26%25} + visit %{/search?filter[with_resource_type]=Decidim::Proposals::Proposal&page=2&per_page=25'"()%26%25} - expect(page).to have_content("22 Results for the search") + expect(page).to have_content("100 Results for the search") expect(page).to have_content("Results per page") end end diff --git a/decidim-initiatives/spec/system/admin/admin_manages_comments_spec.rb b/decidim-initiatives/spec/system/admin/admin_manages_comments_spec.rb index 1f3dad5291d36..7f8765b41691d 100644 --- a/decidim-initiatives/spec/system/admin/admin_manages_comments_spec.rb +++ b/decidim-initiatives/spec/system/admin/admin_manages_comments_spec.rb @@ -22,7 +22,7 @@ end it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:comment, 17, commentable:) } + let!(:reportables) { create_list(:comment, 27, commentable:) } let!(:moderations) do reportables.first(reportables.length - 1).map do |reportable| moderation = create(:moderation, reportable:, participatory_space: commentable, report_count: 1) diff --git a/decidim-participatory_processes/spec/system/admin/process_moderator_manages_process_moderations_spec.rb b/decidim-participatory_processes/spec/system/admin/process_moderator_manages_process_moderations_spec.rb index 5399927b91aa1..23fde80f9507d 100644 --- a/decidim-participatory_processes/spec/system/admin/process_moderator_manages_process_moderations_spec.rb +++ b/decidim-participatory_processes/spec/system/admin/process_moderator_manages_process_moderations_spec.rb @@ -27,6 +27,6 @@ it_behaves_like "manage moderations" it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:dummy_resource, 17, component: current_component) } + let!(:reportables) { create_list(:dummy_resource, 27, component: current_component) } end end diff --git a/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_states_controller.rb b/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_states_controller.rb index 92540ebdf1e84..a9c1b729ed2b6 100644 --- a/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_states_controller.rb +++ b/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_states_controller.rb @@ -4,7 +4,7 @@ module Decidim module Proposals module Admin class ProposalStatesController < Admin::ApplicationController - include Decidim::Admin::Paginable + include Decidim::Paginable helper_method :proposal_states, :proposal_state def index diff --git a/decidim-proposals/spec/system/admin/admin_manages_proposals_spec.rb b/decidim-proposals/spec/system/admin/admin_manages_proposals_spec.rb index b1c130999faac..74d30732837ce 100644 --- a/decidim-proposals/spec/system/admin/admin_manages_proposals_spec.rb +++ b/decidim-proposals/spec/system/admin/admin_manages_proposals_spec.rb @@ -27,6 +27,6 @@ it_behaves_like "publish answers" it_behaves_like "sorted moderations" do - let!(:reportables) { create_list(:proposal, 17, component: current_component) } + let!(:reportables) { create_list(:proposal, 27, component: current_component) } end end From ebeb34cf3863a621d8e168ecd12d0074b3311ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Tue, 30 Jul 2024 20:47:27 +0200 Subject: [PATCH 08/22] Fix proposals' preview spacing (#13265) --- .../app/views/decidim/proposals/proposals/preview.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decidim-proposals/app/views/decidim/proposals/proposals/preview.html.erb b/decidim-proposals/app/views/decidim/proposals/proposals/preview.html.erb index 7a81a7084e10e..5ebddba88966c 100644 --- a/decidim-proposals/app/views/decidim/proposals/proposals/preview.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/proposals/preview.html.erb @@ -7,7 +7,7 @@ <%= render partial: "wizard_header", locals: { callout_help_text_class: "warning" } %> -
+

<%= present(@proposal).title(links: true, html_escape: true) %>

<% unless component_settings.participatory_texts_enabled? %> From c02ee1b73ad114a229269b8118e04fa033e2de9d Mon Sep 17 00:00:00 2001 From: Alexandru Emil Lupu Date: Tue, 6 Aug 2024 15:27:53 +0300 Subject: [PATCH 09/22] Parametrize Chrome Driver (#13277) --- .github/actions/spelling/expect.txt | 1 + .github/workflows/test_app.yml | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 5178fcfe0647b..eb54f8bd08061 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -24,6 +24,7 @@ AGPL Ajuntament Ajuntamentde alabs +amd amendables AMR andreslucena diff --git a/.github/workflows/test_app.yml b/.github/workflows/test_app.yml index bd5fe26548bfe..e2c30e6d77cf8 100644 --- a/.github/workflows/test_app.yml +++ b/.github/workflows/test_app.yml @@ -38,6 +38,11 @@ on: required: false default: true type: boolean + chrome_version: + description: 'Chrome & Chromedriver version' + required: false + default: "126.0.6478.182" + type: string jobs: build_app: @@ -78,7 +83,14 @@ jobs: - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ inputs.ruby_version }} + - run: | + wget --no-verbose -O /tmp/chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${{inputs.chrome_version}}-1_amd64.deb + sudo dpkg -i /tmp/chrome.deb + rm /tmp/chrome.deb + name: Install Chrome version ${{inputs.chrome_version}} - uses: nanasess/setup-chromedriver@v2 + with: + chromedriver-version: ${{inputs.chrome_version}} - uses: actions/cache@v4 id: app-cache with: From ed1e0d7ff98c07eb651cf9712e369677377b2b55 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:35:04 +0200 Subject: [PATCH 10/22] Create taxonomies section in the general settings (#13112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add taxonomies menu, crud * add drug and drop * change select options * add search * add tests * system test * fix validation * fix locale * spring * erb linter * factory * factory * change permissions, model, add tests * add exceptions * change views * views changes * refactor reordering * fix migration * handle pagination with drag & drop reordering * refactor sortable. Make suborders work * add subcontroller * handle remote submission * prevent editing sub-taxonomies not belonging to parent * start styling drawer * stylize drawer * css fixes * validate number of parents creating taxonomies * add specs: commands, forms * add system specs * fix system spec for reorders * change removable method * change view: add 3rd level * fix sortable * fix lint, fix update taxonomy spec * fix locale * fix test for controller * change styles * fix codeclimate * fix check speelling * change select options, replace element to item * change table styles * fix tests * cops * rename to items * correct counters * fix migration * fix specs * add meaninful confirmation message * fix factories * fix opendrawer * remove helper. fix drawer bottom * fix weight creation. Add seeds * use attributes and injection in taxonomy factories * fix specs * improve search * fix spec * fix commands * remove taxonomy_filters * use references * remove duplicated index --------- Co-authored-by: Ivan Vergés --- .github/actions/spelling/expect.txt | 3 + .../commands/decidim/admin/create_taxonomy.rb | 23 ++ .../decidim/admin/destroy_taxonomy.rb | 18 ++ .../decidim/admin/reorder_taxonomies.rb | 76 ++++++ .../commands/decidim/admin/update_taxonomy.rb | 20 ++ .../decidim/admin/taxonomies/filterable.rb | 27 ++ .../decidim/admin/taxonomies_controller.rb | 113 ++++++++ .../admin/taxonomy_items_controller.rb | 91 +++++++ .../app/forms/decidim/admin/taxonomy_form.rb | 20 ++ .../forms/decidim/admin/taxonomy_item_form.rb | 54 ++++ .../app/packs/src/decidim/admin/sortable.js | 44 ++-- .../decidim/admin/_taxonomies.scss | 74 ++++++ .../decidim/admin/application.scss | 1 + .../permissions/decidim/admin/permissions.rb | 14 + .../admin/taxonomies/_filters.html.erb | 19 ++ .../decidim/admin/taxonomies/_form.html.erb | 5 + .../decidim/admin/taxonomies/_row.html.erb | 33 +++ .../admin/taxonomies/_row_children.html.erb | 8 + .../decidim/admin/taxonomies/_table.html.erb | 70 +++++ .../taxonomies/_taxonomy_actions.html.erb | 10 + .../decidim/admin/taxonomies/edit.html.erb | 87 +++++++ .../decidim/admin/taxonomies/index.html.erb | 28 ++ .../decidim/admin/taxonomies/new.html.erb | 16 ++ .../admin/taxonomy_items/_form.html.erb | 8 + .../admin/taxonomy_items/edit.html.erb | 12 + .../decidim/admin/taxonomy_items/new.html.erb | 12 + decidim-admin/config/locales/en.yml | 50 ++++ decidim-admin/config/routes.rb | 5 + decidim-admin/lib/decidim/admin/menu.rb | 8 +- .../decidim/admin/create_taxonomy_spec.rb | 85 ++++++ .../decidim/admin/destroy_taxonomy_spec.rb | 40 +++ .../decidim/admin/reorder_taxonomies_spec.rb | 57 ++++ .../decidim/admin/update_taxonomy_spec.rb | 65 +++++ .../controllers/taxonomies_controller_spec.rb | 123 +++++++++ .../forms/decidim/admin/taxonomy_form_spec.rb | 46 ++++ .../decidim/admin/taxonomy_item_form_spec.rb | 75 ++++++ .../system/admin_manages_taxonomies_spec.rb | 243 ++++++++++++++++++ .../lib/decidim/assemblies/seeds.rb | 5 + .../app/helpers/decidim/modal_helper.rb | 23 ++ .../app/models/decidim/organization.rb | 2 + .../app/models/decidim/taxonomization.rb | 23 ++ decidim-core/app/models/decidim/taxonomy.rb | 85 ++++++ decidim-core/app/packs/src/decidim/index.js | 2 + .../app/packs/stylesheets/decidim/_modal.scss | 38 +++ .../decidim/admin_log/taxonomy_presenter.rb | 52 ++++ .../log/value_types/taxonomy_presenter.rb | 29 +++ .../presenters/decidim/taxonomy_presenter.rb | 14 + decidim-core/config/locales/en.yml | 11 + ...0240704115429_create_decidim_taxonomies.rb | 23 ++ decidim-core/lib/decidim/core/engine.rb | 1 + decidim-core/lib/decidim/core/seeds.rb | 30 +++ .../lib/decidim/core/test/factories.rb | 32 +++ decidim-core/lib/decidim/seeds.rb | 8 + .../spec/models/decidim/taxonomy_spec.rb | 165 ++++++++++++ .../value_types/taxonomy_presenter_spec.rb | 33 +++ .../decidim/participatory_processes/seeds.rb | 5 + 56 files changed, 2247 insertions(+), 17 deletions(-) create mode 100644 decidim-admin/app/commands/decidim/admin/create_taxonomy.rb create mode 100644 decidim-admin/app/commands/decidim/admin/destroy_taxonomy.rb create mode 100644 decidim-admin/app/commands/decidim/admin/reorder_taxonomies.rb create mode 100644 decidim-admin/app/commands/decidim/admin/update_taxonomy.rb create mode 100644 decidim-admin/app/controllers/concerns/decidim/admin/taxonomies/filterable.rb create mode 100644 decidim-admin/app/controllers/decidim/admin/taxonomies_controller.rb create mode 100644 decidim-admin/app/controllers/decidim/admin/taxonomy_items_controller.rb create mode 100644 decidim-admin/app/forms/decidim/admin/taxonomy_form.rb create mode 100644 decidim-admin/app/forms/decidim/admin/taxonomy_item_form.rb create mode 100644 decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/_filters.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/_form.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/_row.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/_row_children.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/_table.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/_taxonomy_actions.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/edit.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/index.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomies/new.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomy_items/_form.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomy_items/edit.html.erb create mode 100644 decidim-admin/app/views/decidim/admin/taxonomy_items/new.html.erb create mode 100644 decidim-admin/spec/commands/decidim/admin/create_taxonomy_spec.rb create mode 100644 decidim-admin/spec/commands/decidim/admin/destroy_taxonomy_spec.rb create mode 100644 decidim-admin/spec/commands/decidim/admin/reorder_taxonomies_spec.rb create mode 100644 decidim-admin/spec/commands/decidim/admin/update_taxonomy_spec.rb create mode 100644 decidim-admin/spec/controllers/taxonomies_controller_spec.rb create mode 100644 decidim-admin/spec/forms/decidim/admin/taxonomy_form_spec.rb create mode 100644 decidim-admin/spec/forms/decidim/admin/taxonomy_item_form_spec.rb create mode 100644 decidim-admin/spec/system/admin_manages_taxonomies_spec.rb create mode 100644 decidim-core/app/models/decidim/taxonomization.rb create mode 100644 decidim-core/app/models/decidim/taxonomy.rb create mode 100644 decidim-core/app/presenters/decidim/admin_log/taxonomy_presenter.rb create mode 100644 decidim-core/app/presenters/decidim/log/value_types/taxonomy_presenter.rb create mode 100644 decidim-core/app/presenters/decidim/taxonomy_presenter.rb create mode 100644 decidim-core/db/migrate/20240704115429_create_decidim_taxonomies.rb create mode 100644 decidim-core/spec/models/decidim/taxonomy_spec.rb create mode 100644 decidim-core/spec/presenters/decidim/log/value_types/taxonomy_presenter_spec.rb diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index eb54f8bd08061..8bba11c2eae07 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -867,6 +867,9 @@ tableize tagsinput tailwindcss tarekraafat +taxonomizable +taxonomization +taxonomizations technopolitical templatable templateable diff --git a/decidim-admin/app/commands/decidim/admin/create_taxonomy.rb b/decidim-admin/app/commands/decidim/admin/create_taxonomy.rb new file mode 100644 index 0000000000000..3eeb41db36f98 --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/create_taxonomy.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Decidim + module Admin + # A command with all the business logic to create a taxonomy. + # This command is called from the controller. + class CreateTaxonomy < Decidim::Commands::CreateResource + fetch_form_attributes :name, :organization, :parent_id + + protected + + def resource_class = Decidim::Taxonomy + + def extra_params + { + extra: { + parent_name: form.try(:parent).try(:name) + } + } + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/destroy_taxonomy.rb b/decidim-admin/app/commands/decidim/admin/destroy_taxonomy.rb new file mode 100644 index 0000000000000..6280d9fb798df --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/destroy_taxonomy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Decidim + module Admin + # A command with all the business logic to destroy a taxonomy. + class DestroyTaxonomy < Decidim::Commands::DestroyResource + private + + def extra_params + { + extra: { + parent_name: resource.parent.try(:name) + } + } + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/reorder_taxonomies.rb b/decidim-admin/app/commands/decidim/admin/reorder_taxonomies.rb new file mode 100644 index 0000000000000..9a4d0fdd5ce6b --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/reorder_taxonomies.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Decidim + module Admin + # A command that reorders a collection of taxonomies + # the ones that might be missing. + class ReorderTaxonomies < Decidim::Command + # Public: Initializes the command. + # + # organization - the Organization where the content blocks reside + # order - an Array holding the order of IDs of published content blocks. + def initialize(organization, order, offset = 0) + @organization = organization + @order = order + @offset = offset + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid. + # - :invalid if the data was not valid and we could not proceed. + # + # Returns nothing. + def call + return broadcast(:invalid) if order.blank? + return broadcast(:invalid) if collection.empty? + + reorder_steps + broadcast(:ok) + end + + private + + attr_reader :organization, :offset + + def reorder_steps + transaction do + reset_weights + collection.reload + set_new_weights + end + end + + def reset_weights + # rubocop:disable Rails/SkipsModelValidations + collection.where.not(weight: nil).where(id: order).update_all(weight: nil) + # rubocop:enable Rails/SkipsModelValidations + end + + def set_new_weights + data = order.each_with_index.inject({}) do |hash, (id, index)| + hash.update(id => index + 1 + offset) + end + + data.each do |id, weight| + item = collection.find_by(id:) + item.update!(weight:) if item.present? + end + end + + def order + return nil unless @order.is_a?(Array) && @order.present? + + @order + end + + def collection + @collection ||= Decidim::Taxonomy.where(organization:, parent_id: first_item.parent_id) + end + + def first_item + @first_item ||= Decidim::Taxonomy.where(organization:).find(order.first) + end + end + end +end diff --git a/decidim-admin/app/commands/decidim/admin/update_taxonomy.rb b/decidim-admin/app/commands/decidim/admin/update_taxonomy.rb new file mode 100644 index 0000000000000..a127d527f8e1c --- /dev/null +++ b/decidim-admin/app/commands/decidim/admin/update_taxonomy.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Decidim + module Admin + # A command to update a taxonomy. + class UpdateTaxonomy < Decidim::Commands::UpdateResource + fetch_form_attributes :name, :parent_id + + protected + + def extra_params + { + extra: { + parent_name: resource.parent.try(:name) + } + } + end + end + end +end diff --git a/decidim-admin/app/controllers/concerns/decidim/admin/taxonomies/filterable.rb b/decidim-admin/app/controllers/concerns/decidim/admin/taxonomies/filterable.rb new file mode 100644 index 0000000000000..90f293777723b --- /dev/null +++ b/decidim-admin/app/controllers/concerns/decidim/admin/taxonomies/filterable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Admin + module Taxonomies + module Filterable + extend ActiveSupport::Concern + + included do + include Decidim::Admin::Filterable + + private + + def base_query + collection + end + + def search_field_predicate + :name_or_children_name_cont + end + end + end + end + end +end diff --git a/decidim-admin/app/controllers/decidim/admin/taxonomies_controller.rb b/decidim-admin/app/controllers/decidim/admin/taxonomies_controller.rb new file mode 100644 index 0000000000000..2be03d943c65c --- /dev/null +++ b/decidim-admin/app/controllers/decidim/admin/taxonomies_controller.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +module Decidim + module Admin + class TaxonomiesController < Decidim::Admin::ApplicationController + include Decidim::Admin::Taxonomies::Filterable + + layout "decidim/admin/settings" + + add_breadcrumb_item_from_menu :admin_settings_menu + + helper_method :collection, :parent_options, :taxonomy + + before_action only: :edit do + redirect_to edit_taxonomy_path(taxonomy.parent) unless taxonomy && taxonomy.root? + end + + def index + @taxonomies = filtered_collection + end + + def new + enforce_permission_to :create, :taxonomy + + @form = form(Decidim::Admin::TaxonomyForm).instance + end + + def create + enforce_permission_to :create, :taxonomy + + @form = form(Decidim::Admin::TaxonomyForm).from_params(params) + CreateTaxonomy.call(@form) do + on(:ok) do + flash[:notice] = I18n.t("create.success", scope: "decidim.admin.taxonomies") + redirect_to taxonomies_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("create.invalid", scope: "decidim.admin.taxonomies") + render action: "new" + end + end + end + + def edit + enforce_permission_to(:update, :taxonomy, taxonomy:) + @form = form(Decidim::Admin::TaxonomyForm).from_model(taxonomy) + @taxonomies = filtered_collection + end + + def update + enforce_permission_to(:update, :taxonomy, taxonomy:) + @form = form(Decidim::Admin::TaxonomyForm).from_params(params) + + UpdateTaxonomy.call(@form, taxonomy) do + on(:ok) do + flash[:notice] = I18n.t("update.success", scope: "decidim.admin.taxonomies") + redirect_to taxonomies_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("update.invalid", scope: "decidim.admin.taxonomies") + render action: "edit" + end + end + end + + def destroy + enforce_permission_to(:destroy, :taxonomy, taxonomy:) + + DestroyTaxonomy.call(taxonomy, current_user) do + on(:ok) do + flash[:notice] = I18n.t("destroy.success", scope: "decidim.admin.taxonomies") + redirect_to taxonomies_path + end + + on(:invalid) do + flash.now[:alert] = I18n.t("destroy.invalid", scope: "decidim.admin.taxonomies") + redirect_to taxonomies_path + end + end + end + + def reorder + enforce_permission_to :update, :taxonomy + + ReorderTaxonomies.call(current_organization, params[:ids_order], page_offset) do + on(:ok) do + head :ok + end + + on(:invalid) do + head :bad_request + end + end + end + + private + + def collection + @collection ||= taxonomy ? taxonomy.children : root_taxonomies + end + + def root_taxonomies + @root_taxonomies ||= current_organization.taxonomies.where(parent_id: nil) + end + + def taxonomy + @taxonomy ||= current_organization.taxonomies.find_by(id: params[:id]) + end + end + end +end diff --git a/decidim-admin/app/controllers/decidim/admin/taxonomy_items_controller.rb b/decidim-admin/app/controllers/decidim/admin/taxonomy_items_controller.rb new file mode 100644 index 0000000000000..5a76cf2149956 --- /dev/null +++ b/decidim-admin/app/controllers/decidim/admin/taxonomy_items_controller.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Decidim + module Admin + class TaxonomyItemsController < Decidim::Admin::ApplicationController + layout false + + helper_method :taxonomy, :taxonomy_item, :parent_options, :selected_parent_id + before_action do + if taxonomy_item && taxonomy_item.parent_ids.exclude?(taxonomy.id) + flash[:alert] = I18n.t("update.invalid", scope: "decidim.admin.taxonomies") + render plain: I18n.t("update.invalid", scope: "decidim.admin.taxonomies"), status: :unprocessable_entity + end + end + + def new + enforce_permission_to :create, :taxonomy_item + @form = form(Decidim::Admin::TaxonomyItemForm).instance + end + + def create + enforce_permission_to :create, :taxonomy_item + @form = form(Decidim::Admin::TaxonomyItemForm).from_params(params) + CreateTaxonomy.call(@form) do + on(:ok) do + flash[:notice] = I18n.t("create.success", scope: "decidim.admin.taxonomies") + redirect_to edit_taxonomy_path(taxonomy) + end + + on(:invalid) do + flash.now[:alert] = I18n.t("create.invalid", scope: "decidim.admin.taxonomies") + render action: "new" + end + end + end + + def edit + enforce_permission_to :update, :taxonomy_item, taxonomy: taxonomy_item + @form = form(Decidim::Admin::TaxonomyItemForm).from_model(taxonomy_item) + end + + def update + enforce_permission_to :update, :taxonomy_item, taxonomy: taxonomy_item + @form = form(Decidim::Admin::TaxonomyItemForm).from_params(params) + UpdateTaxonomy.call(@form, taxonomy_item) do + on(:ok) do + flash[:notice] = I18n.t("update.success", scope: "decidim.admin.taxonomies") + redirect_to edit_taxonomy_path(taxonomy) + end + + on(:invalid) do + flash.now[:alert] = I18n.t("update.invalid", scope: "decidim.admin.taxonomies") + render action: "edit" + end + end + end + + private + + def taxonomy + @taxonomy ||= Decidim::Taxonomy.find_by(organization: current_organization, id: params[:taxonomy_id]) + end + + def taxonomy_item + @taxonomy_item ||= Decidim::Taxonomy.find_by(organization: current_organization, id: params[:id]) + end + + def selected_parent_id + @selected_parent_id ||= taxonomy_item&.parent_id || taxonomy.id + end + + def parent_options + @parent_options ||= begin + options = [[I18n.t("new.none", scope: "decidim.admin.taxonomy_items"), taxonomy.id]] + taxonomy.children.each do |child| + next if child.id == taxonomy_item&.id + + options << [translated_attribute(child.name).to_s, child.id] + # add children to the list with indentation + child.children.each do |grandchild| + next if grandchild.id == taxonomy_item&.id + + options << ["   #{translated_attribute(grandchild.name)}".html_safe, grandchild.id] + end + end + options + end + end + end + end +end diff --git a/decidim-admin/app/forms/decidim/admin/taxonomy_form.rb b/decidim-admin/app/forms/decidim/admin/taxonomy_form.rb new file mode 100644 index 0000000000000..a092cd3428a19 --- /dev/null +++ b/decidim-admin/app/forms/decidim/admin/taxonomy_form.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Decidim + module Admin + # A form object to be used when creating or updating a taxonomy. + class TaxonomyForm < Decidim::Form + include Decidim::TranslatableAttributes + + mimic :taxonomy + + translatable_attribute :name, String + + validates :name, translatable_presence: true + + alias organization current_organization + + def parent_id = nil + end + end +end diff --git a/decidim-admin/app/forms/decidim/admin/taxonomy_item_form.rb b/decidim-admin/app/forms/decidim/admin/taxonomy_item_form.rb new file mode 100644 index 0000000000000..9b33372748bd6 --- /dev/null +++ b/decidim-admin/app/forms/decidim/admin/taxonomy_item_form.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Decidim + module Admin + # A form object to be used when creating or updating a taxonomy. + class TaxonomyItemForm < Decidim::Form + include Decidim::TranslatableAttributes + + mimic :taxonomy + + # we do not use "name" here to avoid collisions when using foundation tabs for multilingual fields tabs + # as this is used in a modal and the name identifier is used for the root taxonomy + translatable_attribute :item_name, String + attribute :parent_id, Integer + + validates :item_name, translatable_presence: true + validate :validate_parent_id_within_same_root_taxonomy + + alias name item_name + + def map_model(model) + self.item_name = model.name + end + + def self.from_params(params, additional_params = {}) + additional_params[:taxonomy] = {} + if params[:taxonomy] + params[:taxonomy].each do |key, value| + additional_params[:taxonomy][key[8..]] = value if key.start_with?("item_name_") + end + end + super + end + + def validate_parent_id_within_same_root_taxonomy + if parent + current_root_taxonomy = if parent.root? + parent + else + parent.root_taxonomy + end + + errors.add(:parent_id, :invalid) unless parent.root_taxonomy.id == current_root_taxonomy.id + else + errors.add(:parent_id, :invalid) + end + end + + def parent + @parent ||= Decidim::Taxonomy.find_by(id: parent_id) + end + end + end +end diff --git a/decidim-admin/app/packs/src/decidim/admin/sortable.js b/decidim-admin/app/packs/src/decidim/admin/sortable.js index 9b66bc447ba37..fb67eb654b2a5 100644 --- a/decidim-admin/app/packs/src/decidim/admin/sortable.js +++ b/decidim-admin/app/packs/src/decidim/admin/sortable.js @@ -1,19 +1,31 @@ -import createSortList from "src/decidim/admin/sort_list.component" +import sortable from "html5sortable/dist/html5sortable.es"; -// Once in DOM -$(() => { - const selector = ".js-sortable" - const $sortable = $(selector) +/* + Initializes any element with the class js-sortable as a sortable list + User html5Sortable, with options available as data-draggable-options (see https://github.com/lukasoppermann/html5sortable) - $sortable.each((index, elem) => { - const item = (elem.id) - ? `#${elem.id}` - : selector + Event are dispatched on the element with the class js-sortable, so you can simply do: - createSortList(item, { - handle: "li", - forcePlaceholderSize: true, - placeholderClass: "sort-placeholder" - }) - }) -}) + document.querySelector('.js-sortable').addEventListener('sortupdate', (event) => { + console.log('The new order is:', event.target.children); + }); +*/ +window.addEventListener("DOMContentLoaded", () => { + const draggables = document.querySelectorAll(".js-sortable"); + + if (draggables) { + draggables.forEach((draggable) => { + let options = { + "forcePlaceholderSize": true + }; + ["items", "acceptFrom", "handle", "placeholderClass", "placeholder", "hoverClass"].forEach((option) => { + let dataOption = `draggable${option.charAt(0).toUpperCase() + option.slice(1)}`; + if (draggable.dataset[dataOption]) { + options[option] = draggable.dataset[dataOption]; + } + }); + // console.log("initialize sortable with options", options); + sortable(draggable, options); + }); + } +}); diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss new file mode 100644 index 0000000000000..ece16340ab739 --- /dev/null +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/_taxonomies.scss @@ -0,0 +1,74 @@ +.spinner-container { + &::before { + @apply absolute -mt-2 -ml-4 top-1/2 left-1/2; + } +} + +.draggable-taxonomy { + &.change-page { + @apply bg-gray-3 italic; + } + + .dragger { + @apply text-2xl cursor-ns-resize; + } + + td { + @apply align-top; + + &.js-drag-handle .dragger { + @apply mt-1; + } + } +} + +ul.taxonomy { + @apply flex; + + li { + @apply flex-none text-center w-20 mt-2 mb-2; + + &:first-child { + @apply flex-initial text-left flex-grow; + } + + &:nth-child(2), + &:last-child { + @apply w-36; + } + } + + .taxonomy-list__actions { + span { + @apply ml-1; + } + + svg { + @apply inline-block fill-secondary w-5 h-5; + } + + .action-space { + @apply w-5 h-5; + } + } +} + +[data-dialog]#item-form { + @apply bottom-0 sm:top-[5.5rem] top-[8.55rem]; + + [data-dialog-container] > :last-child { + grid-column: span 1 / span 1; + } +} + +#item-form-content { + @apply p-6 rounded-r-none py-0; + + h1 { + @apply mt-6 mx-0; + } + + .row { + @apply px-0; + } +} diff --git a/decidim-admin/app/packs/stylesheets/decidim/admin/application.scss b/decidim-admin/app/packs/stylesheets/decidim/admin/application.scss index 3a6dbd77ac3b4..ff8d63b8cb319 100755 --- a/decidim-admin/app/packs/stylesheets/decidim/admin/application.scss +++ b/decidim-admin/app/packs/stylesheets/decidim/admin/application.scss @@ -24,6 +24,7 @@ @import "stylesheets/decidim/admin/_logs.scss"; @import "stylesheets/decidim/admin/_filters.scss"; @import "stylesheets/decidim/admin/_table-list.scss"; +@import "stylesheets/decidim/admin/_taxonomies.scss"; @import "stylesheets/decidim/admin/_dropdown.scss"; @import "stylesheets/decidim/admin/_item_show.scss"; @import "stylesheets/decidim/admin/_item_edit.scss"; diff --git a/decidim-admin/app/permissions/decidim/admin/permissions.rb b/decidim-admin/app/permissions/decidim/admin/permissions.rb index 2ef73b4a793a0..296e97322c035 100644 --- a/decidim-admin/app/permissions/decidim/admin/permissions.rb +++ b/decidim-admin/app/permissions/decidim/admin/permissions.rb @@ -59,6 +59,12 @@ def permissions allow! if permission_action.subject == :help_sections allow! if permission_action.subject == :share_token allow! if permission_action.subject == :reminder + + if permission_action.subject == :taxonomy + permission_action.action == :destroy ? allow_destroy_taxonomy? : allow! + end + + allow! if permission_action.subject == :taxonomy_item end permission_action @@ -253,6 +259,14 @@ def admin_terms_accepted? def available_authorization_handlers? user.organization.available_authorization_handlers.any? end + + def allow_destroy_taxonomy? + return unless permission_action.action == :destroy + + taxonomy = context.fetch(:taxonomy, nil) + + toggle_allow(taxonomy&.removable?) + end end end end diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/_filters.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/_filters.html.erb new file mode 100644 index 0000000000000..d635006f292d4 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/_filters.html.erb @@ -0,0 +1,19 @@ +
+ +
diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/_form.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/_form.html.erb new file mode 100644 index 0000000000000..d715fd194cb11 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/_form.html.erb @@ -0,0 +1,5 @@ +
+
+ <%= form.translated :text_field, :name, autofocus: true, class: "js-hashtags", hashtaggable: true, aria: { label: :name } %> +
+
diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/_row.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/_row.html.erb new file mode 100644 index 0000000000000..3b4facc02e743 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/_row.html.erb @@ -0,0 +1,33 @@ + + <%== icon("draggable", class: "dragger") %> + +
    +
  • <%= link_to translated_attribute(taxonomy.name), taxonomy.root? ? edit_taxonomy_path(taxonomy) : edit_taxonomy_item_path(taxonomy_id: taxonomy.root_taxonomy.id, id: taxonomy.id), class: "js-drawer-editor" %>
  • +
  • <%= taxonomy.root? ? taxonomy.children_count : taxonomy.taxonomizations_count %>
  • +
  • <%= render "taxonomy_actions", taxonomy: %>
  • +
+ <% if with_children && taxonomy.children.any? %> +
+ <% taxonomy.children.each do |child| %> + <%= render partial: "decidim/admin/taxonomies/row_children", locals: { child:, class_name: "", grandchild: false } %> + + <% if child.children.any? %> +
+ <% child.children.each do |grandchild| %> + <%= render partial: "decidim/admin/taxonomies/row_children", locals: { child: grandchild, class_name: "ml-6", grandchild: true } %> + <% end %> +
+ <% end %> + <% end %> +
+ <% end %> + + diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/_row_children.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/_row_children.html.erb new file mode 100644 index 0000000000000..27645aaebf29b --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/_row_children.html.erb @@ -0,0 +1,8 @@ +
    +
  • + <%== icon("draggable", class: "dragger with-children") %> + <%= link_to translated_attribute(child.name), edit_taxonomy_item_path(taxonomy_id: child.root_taxonomy.id, id: child.id), class: "js-drawer-editor" %> +
  • +
  • <%= child.taxonomizations_count %>
  • +
  • <%= render "taxonomy_actions", taxonomy: child %>
  • +
diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/_table.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/_table.html.erb new file mode 100644 index 0000000000000..0598010cee6c1 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/_table.html.erb @@ -0,0 +1,70 @@ +
+ + + + + + + + + <% if path = path_to_prev_page(collection) %> + + <% end %> + + <% collection.each do |taxonomy| %> + <%= render "row", taxonomy:, with_children: !taxonomy.root? %> + <% end %> + <% if path = path_to_next_page(collection) %> + + <% end %> + +
Move +
    +
  • <%= t("decidim.admin.taxonomies.name") %>
  • +
  • <%= taxonomy.blank? ? t("decidim.admin.taxonomies.amount") : t("decidim.admin.taxonomies.count") %>
  • +
  • <%= t("decidim.admin.taxonomies.actions") %>
  • +
+
<%= t(".to_prev_page") %>
<%= t(".to_next_page") %>
+
+ +<%= decidim_paginate collection %> + + diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/_taxonomy_actions.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/_taxonomy_actions.html.erb new file mode 100644 index 0000000000000..fb5047608c536 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/_taxonomy_actions.html.erb @@ -0,0 +1,10 @@ +<% if allowed_to? :update, :taxonomy, taxonomy: taxonomy %> + <%= icon_link_to "pencil-line", taxonomy.root? ? edit_taxonomy_path(taxonomy) : edit_taxonomy_item_path(taxonomy_id: taxonomy.root_taxonomy.id, id: taxonomy.id), t("actions.edit", scope: "decidim.admin"), class: "action-icon--edit js-drawer-editor" %> +<% else %> + +<% end %> +<% if allowed_to? :destroy, :taxonomy, taxonomy: taxonomy %> + <%= icon_link_to "delete-bin-line", taxonomy, t("actions.destroy", scope: "decidim.admin"), class: "action-icon--remove", method: :delete, data: { confirm: t(".confirm_destroy", name: strip_tags(translated_attribute(taxonomy.name))) } %> +<% else %> + +<% end %> diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/edit.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/edit.html.erb new file mode 100644 index 0000000000000..3116411d36a0c --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/edit.html.erb @@ -0,0 +1,87 @@ +<% add_decidim_page_title(t(".title", taxonomy_name: translated_attribute(taxonomy.name))) %> + +
+

+ <%= t(".title", taxonomy_name: translated_attribute(taxonomy.name)) %> + <%= link_to :back, class: "button button__transparent-secondary button__sm" do %> + <%= t(".back") %> + <% end %> +

+
+ +
+ <%= decidim_form_for(@form, html: { class: "form-defaults form edit_taxonomy_" }) do |f| %> + <%= render partial: "form", object: f %> +
+
+ <%= f.submit t(".update"), class: "button button__sm button__secondary" %> +
+
+ <% end %> +
+ +
+

+ <%= t(".subtitle", taxonomy_name: translated_attribute(taxonomy.name)) %> + <%= link_to t(".new_item"), new_taxonomy_item_path(taxonomy), id: "new-item", class: "js-drawer-editor button button__sm button__secondary new" %> +

+ + <% if collection.any? %> + <%= render "filters" %> + + <% if @taxonomies.any? %> +

<%= t(".description") %>

+ <%= render "table", collection: @taxonomies %> + <% else %> +

<%= t("no_items_found", scope: "decidim.admin.taxonomies.index") %>

+ <% end %> + <% else %> +

<%= t(".no_items") %>

+ <% end %> +
+ +<%= decidim_drawer id: "item-form" do %> +
+<% end %> + + diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/index.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/index.html.erb new file mode 100644 index 0000000000000..2a789b9b04421 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/index.html.erb @@ -0,0 +1,28 @@ +<% add_decidim_page_title(t("taxonomies", scope: "decidim.admin.titles")) %> + +
+
+
+

+ <%= t "decidim.admin.titles.taxonomies" %> + <% if allowed_to? :create, :taxonomy %> + <%= link_to t(".new_taxonomy"), new_taxonomy_path, class: "button button__sm button__secondary new" %> + <% end %> +

+

+ <%= t(".description") %> +

+
+
+ <% if collection.any? %> + <%= render "filters" %> + + <% if @taxonomies.any? %> + <%= render "table", collection: @taxonomies if @taxonomies %> + <% else %> +

<%= t(".no_items_found") %>

+ <% end %> + <% else %> +

<%= t("decidim.admin.taxonomies.no_taxonomies") %>

+ <% end %> +
diff --git a/decidim-admin/app/views/decidim/admin/taxonomies/new.html.erb b/decidim-admin/app/views/decidim/admin/taxonomies/new.html.erb new file mode 100644 index 0000000000000..8ced13697109f --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomies/new.html.erb @@ -0,0 +1,16 @@ +<% add_decidim_page_title(t(".title")) %> +
+

+ <%= t ".title" %> +

+
+
+ <%= decidim_form_for(@form, url: taxonomies_path, html: { class: "form-defaults form new_taxonomy_" }) do |f| %> + <%= render partial: "form", object: f %> +
+
+ <%= f.submit t(".create"), class: "button button__sm button__secondary" %> +
+
+ <% end %> +
diff --git a/decidim-admin/app/views/decidim/admin/taxonomy_items/_form.html.erb b/decidim-admin/app/views/decidim/admin/taxonomy_items/_form.html.erb new file mode 100644 index 0000000000000..9a20b8b1fed84 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomy_items/_form.html.erb @@ -0,0 +1,8 @@ +<%= display_flash_messages %> + +
+ <%= form.translated :text_field, :item_name, autofocus: true, class: "js-hashtags", hashtaggable: true, aria: { label: :name } %> +
+
+ <%= form.select :parent_id, options_for_select(parent_options, selected: selected_parent_id), {}, class: "form-control" %> +
diff --git a/decidim-admin/app/views/decidim/admin/taxonomy_items/edit.html.erb b/decidim-admin/app/views/decidim/admin/taxonomy_items/edit.html.erb new file mode 100644 index 0000000000000..56332917d60e4 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomy_items/edit.html.erb @@ -0,0 +1,12 @@ +
+
+ <%= decidim_form_for(@form, url: taxonomy_item_path(taxonomy_id: taxonomy.id, id: taxonomy_item.id ), remote: true, html: { id: "taxonomy-item-form", class: "form-defaults form new_taxonomy_" }) do |f| %> +

+ <%= t ".title", taxonomy: translated_attribute(taxonomy.name) %> + <%= f.submit t(".update"), class: "button button__sm button__secondary" %> +

+ + <%= render partial: "form", object: f %> + <% end %> +
+
diff --git a/decidim-admin/app/views/decidim/admin/taxonomy_items/new.html.erb b/decidim-admin/app/views/decidim/admin/taxonomy_items/new.html.erb new file mode 100644 index 0000000000000..b2a4c43a91f36 --- /dev/null +++ b/decidim-admin/app/views/decidim/admin/taxonomy_items/new.html.erb @@ -0,0 +1,12 @@ +
+
+ <%= decidim_form_for(@form, url: taxonomy_items_path(taxonomy), remote: true, html: { id: "taxonomy-item-form", class: "form-defaults form new_taxonomy_" }) do |f| %> +

+ <%= t ".title", taxonomy: translated_attribute(taxonomy.name) %> + <%= f.submit t(".create"), class: "button button__sm button__secondary" %> +

+ + <%= render partial: "form", object: f %> + <% end %> +
+
diff --git a/decidim-admin/config/locales/en.yml b/decidim-admin/config/locales/en.yml index 8644aca3caaef..bd10c90d56fcd 100644 --- a/decidim-admin/config/locales/en.yml +++ b/decidim-admin/config/locales/en.yml @@ -470,6 +470,9 @@ en: pending: Pending rejected: Rejected verified: Verified + taxonomies: + taxonomy_id_eq: + label: Taxonomy forms: file_help: import: @@ -592,6 +595,7 @@ en: settings: Settings static_page_topics: Topics static_pages: Pages + taxonomies: Taxonomies user_groups: Groups users: Participants metrics: @@ -1019,6 +1023,51 @@ en: update: error: There was a problem updating this page. success: Page updated successfully. + taxonomies: + actions: Actions + amount: Amount + count: Count + create: + invalid: There was a problem creating this taxonomy. + success: Taxonomy created successfully. + destroy: + invalid: There was a problem destroying this taxonomy. + success: Taxonomy successfully destroyed. + edit: + back: Back + description: The items in this taxonomy will be used to classify or filter resources, such as participatory spaces or components. Use drag and drop to reorder the list. + new_item: New item + no_items: There are currently no items in this taxonomy. Create a list of items here to classify or filter resources, such as participatory spaces or components. Items can be nested up to three levels. + subtitle: Items in "%{taxonomy_name}" + title: Edit taxonomy "%{taxonomy_name}" + update: Update + filters: + search_placeholder: Search + index: + description: A taxonomy allows admins categorizing and organizing content. For example, add a taxonomy to classify processes by geographical scope. + new_taxonomy: New taxonomy + no_items_found: No taxonomies found matching the search criteria. + name: Name + new: + create: Create taxonomy + title: New taxonomy + no_taxonomies: There are currently no taxonomies. Create a list of taxonomies here and defined items in each of them to classify or filter resources. + table: + to_next_page: Drag over for next page + to_prev_page: Drag over for previous page + taxonomy_actions: + confirm_destroy: Are you sure you want to delete %{name}? This will also delete all the children items. + update: + invalid: There was a problem updating this taxonomy. + success: Taxonomy updated successfully. + taxonomy_items: + edit: + title: Edit item in %{taxonomy} + update: Update item + new: + create: Create item + none: None + title: New item in %{taxonomy} titles: admin_log: Admin log area_types: Area types @@ -1039,6 +1088,7 @@ en: scope_types: Scope types scopes: Scopes statistics: Activity + taxonomies: Taxonomies user_groups: Groups users: Administrators user_group: diff --git a/decidim-admin/config/routes.rb b/decidim-admin/config/routes.rb index 03311899fe4a8..75dc6156ca15c 100644 --- a/decidim-admin/config/routes.rb +++ b/decidim-admin/config/routes.rb @@ -122,6 +122,11 @@ resources :conflicts, only: [:index, :edit, :update], controller: "conflicts" + resources :taxonomies, except: [:show] do + patch :reorder, on: :collection + resources :items, only: [:new, :create, :edit, :update], controller: "taxonomy_items" + end + root to: "dashboard#show" end end diff --git a/decidim-admin/lib/decidim/admin/menu.rb b/decidim-admin/lib/decidim/admin/menu.rb index 1abacacc56e32..c7942b4e358b7 100644 --- a/decidim-admin/lib/decidim/admin/menu.rb +++ b/decidim-admin/lib/decidim/admin/menu.rb @@ -163,11 +163,17 @@ def self.register_admin_settings_menu! icon_name: "home-gear-line", if: allowed_to?(:update, :organization, organization: current_organization) + menu.add_item :taxonomies, + I18n.t("menu.taxonomies", scope: "decidim.admin"), + decidim_admin.taxonomies_path, + icon_name: "price-tag-3-line", + position: 1.3 + menu.add_item :scopes, I18n.t("menu.scopes", scope: "decidim.admin"), decidim_admin.scopes_path, icon_name: "price-tag-3-line", - position: 1.3, + position: 1.4, if: allowed_to?(:read, :scope), active: [%w( decidim/admin/scopes diff --git a/decidim-admin/spec/commands/decidim/admin/create_taxonomy_spec.rb b/decidim-admin/spec/commands/decidim/admin/create_taxonomy_spec.rb new file mode 100644 index 0000000000000..b55c029d62c51 --- /dev/null +++ b/decidim-admin/spec/commands/decidim/admin/create_taxonomy_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe CreateTaxonomy do + subject { described_class.new(form) } + + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:name) { attributes_for(:taxonomy)[:name] } + let(:form) do + double( + invalid?: invalid, + name:, + organization:, + current_user: user, + parent_id: + ) + end + let(:invalid) { false } + let(:parent_id) { nil } + + context "when the form is not valid" do + let(:invalid) { true } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when the form is valid" do + it "creates the taxonomy" do + expect { subject.call }.to change(Decidim::Taxonomy, :count).by(1) + end + + it "broadcasts ok" do + expect { subject.call }.to broadcast(:ok) + end + + it "sets the name" do + subject.call + expect(Decidim::Taxonomy.last.name).to eq(name) + end + + it "sets the organization" do + subject.call + expect(Decidim::Taxonomy.last.organization).to eq(organization) + end + + context "when parent_id is provided" do + let!(:parent) { create(:taxonomy, organization:) } + let!(:parent_id) { parent.id } + + it "sets the parent" do + expect do + subject.call + end.to change(Decidim::Taxonomy, :count).by(1) + + created_taxonomy = Decidim::Taxonomy.find_by(parent_id: parent.id) + + expect(created_taxonomy.parent).to eq(parent) + expect(created_taxonomy.id).not_to eq(parent.id) + expect(created_taxonomy.name).to eq(name) + end + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:create!) + .with( + Decidim::Taxonomy, + form.current_user, + hash_including(:name, :organization, :parent_id), + hash_including(extra: hash_including(:parent_name)) + ) + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + end + end + end +end diff --git a/decidim-admin/spec/commands/decidim/admin/destroy_taxonomy_spec.rb b/decidim-admin/spec/commands/decidim/admin/destroy_taxonomy_spec.rb new file mode 100644 index 0000000000000..33f3ee96bdef3 --- /dev/null +++ b/decidim-admin/spec/commands/decidim/admin/destroy_taxonomy_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe DestroyTaxonomy do + subject { described_class.new(taxonomy, user) } + + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:taxonomy) { create(:taxonomy, organization:) } + + it "destroys the taxonomy" do + subject.call + expect { taxonomy.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "broadcasts ok" do + expect do + subject.call + end.to broadcast(:ok) + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:perform_action!) + .with( + :delete, + taxonomy, + user, + extra: hash_including(:parent_name) + ) + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + end + end +end diff --git a/decidim-admin/spec/commands/decidim/admin/reorder_taxonomies_spec.rb b/decidim-admin/spec/commands/decidim/admin/reorder_taxonomies_spec.rb new file mode 100644 index 0000000000000..b8318f57f1ddc --- /dev/null +++ b/decidim-admin/spec/commands/decidim/admin/reorder_taxonomies_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe ReorderTaxonomies do + subject { described_class.new(*args) } + + let(:args) { [organization, order] } + let(:organization) { create(:organization) } + + let!(:taxonomy1) { create(:taxonomy, weight: 1, organization:) } + let!(:taxonomy2) { create(:taxonomy, weight: 2, organization:) } + let!(:taxonomy3) { create(:taxonomy, weight: 3, organization:) } + let!(:taxonomy4) { create(:taxonomy, weight: 40, organization:) } + let!(:external_taxonomy) { create(:taxonomy, weight: 11) } + + let(:order) { [taxonomy3.id, taxonomy1.id, taxonomy2.id] } + + context "when the order is nil" do + let(:order) { nil } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when the order is empty" do + let(:order) { [] } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when the order is valid" do + it "is valid" do + expect { subject.call }.to broadcast(:ok) + end + + it "reorders the blocks" do + subject.call + taxonomy1.reload + taxonomy2.reload + taxonomy3.reload + taxonomy4.reload + external_taxonomy.reload + + expect(taxonomy3.weight).to eq 1 + expect(taxonomy1.weight).to eq 2 + expect(taxonomy2.weight).to eq 3 + expect(taxonomy4.weight).to eq 40 + expect(external_taxonomy.weight).to eq 11 + end + end + end +end diff --git a/decidim-admin/spec/commands/decidim/admin/update_taxonomy_spec.rb b/decidim-admin/spec/commands/decidim/admin/update_taxonomy_spec.rb new file mode 100644 index 0000000000000..ccbef52422a36 --- /dev/null +++ b/decidim-admin/spec/commands/decidim/admin/update_taxonomy_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe UpdateTaxonomy do + subject { described_class.new(form, taxonomy) } + + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:taxonomy) { create(:taxonomy, organization:) } + let(:parent) { create(:taxonomy, organization:) } + let(:form) do + double( + invalid?: invalid, + current_user: user, + name:, + parent_id: parent.id + ) + end + + let(:name) { Decidim::Faker::Localized.literal("New name") } + let(:parent_id) { parent.id } + let(:invalid) { false } + + context "when the form is not valid" do + let(:invalid) { true } + + it "is not valid" do + expect { subject.call }.to broadcast(:invalid) + end + end + + context "when the form is valid" do + before do + subject.call + taxonomy.reload + end + + it "updates the name of the taxonomy" do + expect(translated(taxonomy.name)).to eq("New name") + end + + it "updates the parent_id of the taxonomy" do + expect(taxonomy.parent_id).to eq(parent.id) + end + + it "traces the action", versioning: true do + expect(Decidim.traceability) + .to receive(:update!) + .with( + taxonomy, + form.current_user, + hash_including(:name, :parent_id), + hash_including(extra: hash_including(:parent_name)) + ) + .and_call_original + + expect { subject.call }.to change(Decidim::ActionLog, :count) + action_log = Decidim::ActionLog.last + expect(action_log.version).to be_present + end + end + end +end diff --git a/decidim-admin/spec/controllers/taxonomies_controller_spec.rb b/decidim-admin/spec/controllers/taxonomies_controller_spec.rb new file mode 100644 index 0000000000000..5016d0ad53dd1 --- /dev/null +++ b/decidim-admin/spec/controllers/taxonomies_controller_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Admin + describe TaxonomiesController do + routes { Decidim::Admin::Engine.routes } + + let(:organization) { create(:organization) } + let(:current_user) { create(:user, :admin, :confirmed, organization:) } + let(:taxonomy) { create(:taxonomy, organization:) } + + before do + request.env["decidim.current_organization"] = organization + sign_in current_user, scope: :user + end + + describe "GET index" do + let!(:taxonomy1) { create(:taxonomy, name: { en: "Category1" }, organization:) } + let!(:taxonomy2) { create(:taxonomy, name: { en: "Category2" }, organization:) } + + it "assigns @taxonomies" do + get :index, params: { q: { name_or_children_name_cont: "Category1" } } + expect(assigns(:taxonomies)).to include(taxonomy1) + expect(assigns(:taxonomies)).not_to include(taxonomy2) + end + + it "renders the index template" do + get :index, params: { q: { name_or_children_name_cont: "Category1" } } + expect(response).to render_template("index") + end + end + + describe "GET new" do + it "assigns a new form instance" do + get :new + form = assigns(:form) + expect(form).to be_a(Decidim::Admin::TaxonomyForm) + expect(form.id).to be_nil + end + + it "renders the new template" do + get :new + expect(response).to render_template("new") + end + end + + describe "POST create" do + let(:valid_params) { { taxonomy: { name: { en: "New Taxonomy" }, weight: 1 } } } + let(:invalid_params) { { taxonomy: { name: { en: "" }, weight: nil } } } + + it "creates a new taxonomy with valid params" do + expect do + post :create, params: valid_params + end.to change(Decidim::Taxonomy, :count).by(1) + end + + it "does not create a new taxonomy with invalid params" do + expect do + post :create, params: invalid_params + end.not_to change(Decidim::Taxonomy, :count) + end + end + + describe "GET edit" do + let!(:sub_taxonomy1) { create(:taxonomy, parent: taxonomy, name: { en: "Sub 1" }, organization:) } + let!(:sub_taxonomy2) { create(:taxonomy, parent: taxonomy, name: { en: "Sub 2" }, organization:) } + + it "assigns the requested taxonomy to @form" do + get :edit, params: { id: taxonomy.id } + expect(assigns(:form).attributes).to include("id" => taxonomy.id) + end + + it "assigns @taxonomies" do + get :index, params: { id: taxonomy.id, q: { name_or_children_name_cont: "Sub 1" } } + expect(assigns(:taxonomies)).to include(sub_taxonomy1) + expect(assigns(:taxonomies)).not_to include(sub_taxonomy2) + end + + it "renders the edit template" do + get :edit, params: { id: taxonomy.id } + expect(response).to render_template("edit") + end + + context "when editing a non-root taxonomy" do + let(:child_taxonomy) { create(:taxonomy, parent: taxonomy, organization:) } + + it "redirects to the root" do + get :edit, params: { id: child_taxonomy.id } + expect(response).to redirect_to(edit_taxonomy_path(taxonomy)) + end + end + end + + describe "PATCH update" do + let(:valid_params) { { id: taxonomy.id, taxonomy: { name: { en: "Updated Taxonomy" }, weight: 1 } } } + let(:invalid_params) { { id: taxonomy.id, taxonomy: { name: { en: "" }, weight: nil } } } + + it "updates the taxonomy with valid params" do + patch :update, params: valid_params + taxonomy.reload + + expect(taxonomy.name["en"]).to eq("Updated Taxonomy") + end + + it "does not update the taxonomy with invalid params" do + patch :update, params: invalid_params + taxonomy.reload + + expect(taxonomy.name["en"]).not_to eq("") + end + end + + describe "DELETE destroy" do + it "deletes the taxonomy" do + delete :destroy, params: { id: taxonomy.id } + expect(Decidim::Taxonomy).not_to exist(taxonomy.id) + end + end + end + end +end diff --git a/decidim-admin/spec/forms/decidim/admin/taxonomy_form_spec.rb b/decidim-admin/spec/forms/decidim/admin/taxonomy_form_spec.rb new file mode 100644 index 0000000000000..b4e9aa505d2f4 --- /dev/null +++ b/decidim-admin/spec/forms/decidim/admin/taxonomy_form_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe TaxonomyForm do + subject { described_class.from_params(attributes).with_context(context) } + + let(:organization) { create(:organization) } + let(:name) { attributes_for(:taxonomy)[:name] } + let(:attributes) { { name: } } + let(:context) do + { + current_organization: organization + } + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when name is not present" do + let(:name) { { "en" => "" } } + + it { is_expected.to be_invalid } + end + + describe "#name" do + it "returns the name" do + expect(subject.name).to eq(name) + end + end + + describe "#organization" do + it "returns the current organization" do + expect(subject.organization).to eq(organization) + end + end + + describe "#parent_id" do + it "returns nil" do + expect(subject.parent_id).to be_nil + end + end + end +end diff --git a/decidim-admin/spec/forms/decidim/admin/taxonomy_item_form_spec.rb b/decidim-admin/spec/forms/decidim/admin/taxonomy_item_form_spec.rb new file mode 100644 index 0000000000000..4960ae8faf4b2 --- /dev/null +++ b/decidim-admin/spec/forms/decidim/admin/taxonomy_item_form_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim::Admin + describe TaxonomyItemForm do + subject { described_class.from_params(attributes).with_context(context) } + + let(:organization) { create(:organization) } + let(:item_name) { attributes_for(:taxonomy)[:name] } + let(:root_taxonomy) { create(:taxonomy, organization:) } + let(:parent) { create(:taxonomy, organization:, parent: root_taxonomy) } + let(:parent_id) { parent.id } + let(:attributes) do + { + "item_name" => item_name, + "parent_id" => parent_id + } + end + let(:context) do + { + current_organization: organization + } + end + + context "when everything is OK" do + it { is_expected.to be_valid } + end + + context "when item_name is not present" do + let(:item_name) { { "en" => "" } } + + it { is_expected.to be_invalid } + end + + context "when parent_id is not present" do + let(:parent_id) { nil } + + it { is_expected.to be_invalid } + end + + describe "#item_name" do + it "returns the item_name" do + expect(subject.item_name).to eq(item_name) + end + end + + describe "#parent_id" do + it "returns the parent_id" do + expect(subject.parent_id).to eq(parent_id) + end + end + + describe ".from_params" do + let(:params) do + { + "taxonomy" => { "item_name_en" => "Test item" }, + "parent_id" => parent.id + } + end + + it "creates a form with the correct attributes" do + form = described_class.from_params(params).with_context(context) + expect(form.item_name).to eq({ "en" => "Test item" }) + expect(form.parent_id).to eq(parent.id) + end + end + + describe "#name" do + it "returns the item_name" do + expect(subject.name).to eq(subject.item_name) + end + end + end +end diff --git a/decidim-admin/spec/system/admin_manages_taxonomies_spec.rb b/decidim-admin/spec/system/admin_manages_taxonomies_spec.rb new file mode 100644 index 0000000000000..c4b7be31a5ecd --- /dev/null +++ b/decidim-admin/spec/system/admin_manages_taxonomies_spec.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Admin manages taxonomies" do + let(:organization) { create(:organization) } + let(:user) { create(:user, :admin, :confirmed, organization:) } + let(:attributes) { attributes_for(:taxonomy) } + + before do + switch_to_host(organization.host) + login_as(user, scope: :user) + visit decidim_admin.taxonomies_path + end + + it "displays the taxonomies" do + expect(page).to have_content("Taxonomies") + end + + context "when creating a new taxonomy" do + before do + click_on "New taxonomy" + fill_in_i18n( + :taxonomy_name, + "#taxonomy-name-tabs", + **attributes[:name].except("machine_translations") + ) + click_on "Create taxonomy" + end + + it "displays a success message" do + expect(page).to have_content("Taxonomy created successfully.") + end + + it "creates a new taxonomy" do + expect(page).to have_content(translated(attributes[:name])) + end + end + + context "when creating a new taxonomy with invalid data" do + before do + click_on "New taxonomy" + fill_in_i18n( + :taxonomy_name, + "#taxonomy-name-tabs", + en: "" + ) + click_on "Create taxonomy" + end + + it "displays an error message" do + expect(page).to have_content("cannot be blank") + end + end + + context "when creating a new taxonomy item" do + let!(:taxonomy) { create(:taxonomy, organization:) } + + before do + visit decidim_admin.taxonomies_path + click_edit_taxonomy + click_on "New item" + fill_in_i18n( + :taxonomy_item_name, + "#taxonomy-item_name-tabs", + **attributes[:name].except("machine_translations") + ) + click_on "Create item" + end + + it "creates a new taxonomy item" do + expect(page).to have_content(translated(attributes[:name])) + end + end + + context "when creating a new taxonomy item with invalid data" do + let!(:taxonomy) { create(:taxonomy, organization:) } + + before do + visit decidim_admin.taxonomies_path + click_edit_taxonomy + click_on "New item" + fill_in_i18n( + :taxonomy_item_name, + "#taxonomy-item_name-tabs", + en: "" + ) + click_on "Create item" + end + + it "displays an error message" do + expect(page).to have_content("cannot be blank") + end + end + + context "when creating a new taxonomy item for a parent taxonomy" do + let!(:taxonomy) { root_taxonomy } + let!(:root_taxonomy) { create(:taxonomy, organization:) } + let!(:parent_taxonomy) { create(:taxonomy, organization:, parent: root_taxonomy) } + + before do + visit decidim_admin.taxonomies_path + click_edit_taxonomy + click_on "New item" + fill_in_i18n( + :taxonomy_item_name, + "#taxonomy-item_name-tabs", + **attributes[:name].except("machine_translations") + ) + select translated(parent_taxonomy.name), from: "taxonomy_parent_id" + click_on "Create item" + end + + it "creates a new taxonomy item" do + within(".js-sortable tr", text: translated(parent_taxonomy.name)) do + expect(page).to have_content(translated(attributes[:name])) + end + end + end + + context "when editing a taxonomy" do + let!(:taxonomy) { create(:taxonomy, organization:) } + + before do + visit decidim_admin.taxonomies_path + click_edit_taxonomy + fill_in_i18n( + :taxonomy_name, + "#taxonomy-name-tabs", + **attributes[:name].except("machine_translations") + ) + click_on "Update" + end + + it "displays a success message" do + expect(page).to have_content("Taxonomy updated successfully.") + end + + it "updates the taxonomy" do + expect(page).to have_content(translated(translated(attributes[:name]))) + end + end + + context "when deleting a taxonomy" do + let!(:taxonomy) { create(:taxonomy, organization:) } + + before do + visit decidim_admin.taxonomies_path + click_delete_taxonomy + end + + it "displays a success message" do + expect(page).to have_content("Taxonomy successfully destroyed.") + end + + it "deletes the taxonomy" do + expect(page).to have_no_content(taxonomy.name) + end + end + + context "when reordering root taxonomies" do + let!(:taxonomy1) { create(:taxonomy, :with_children, children_count: 1, organization:) } + let!(:taxonomy2) { create(:taxonomy, :with_children, children_count: 2, organization:) } + let!(:taxonomy3) { create(:taxonomy, :with_children, children_count: 3, organization:) } + + before do + visit decidim_admin.taxonomies_path + end + + it "reorders the taxonomies", :js do + expect(page).to have_css(".js-sortable") + + within ".js-sortable" do + expect(page).to have_content(translated(taxonomy1.name)) + expect(page).to have_content(translated(taxonomy2.name)) + expect(page).to have_content(translated(taxonomy3.name)) + end + + page.execute_script(<<~JS) + var first = document.querySelector('.js-sortable tr:first-child'); + var last = document.querySelector('.js-sortable tr:last-child'); + last.parentNode.insertBefore(first, last.nextSibling); + var event = new Event('sortupdate', {bubbles: true}); + document.querySelector('.js-sortable').dispatchEvent(event); + JS + + within ".js-sortable tr:first-child td:nth-child(2)" do + expect(page).to have_content(translated(taxonomy2.name)) + end + within ".js-sortable tr:nth-child(2) td:nth-child(2)" do + expect(page).to have_content(translated(taxonomy3.name)) + end + within ".js-sortable tr:last-child td:nth-child(2)" do + expect(page).to have_content(translated(taxonomy1.name)) + end + + # Refresh the page to ensure the order is persisted + visit current_path + + within ".js-sortable tr:first-child td:nth-child(2)" do + expect(page).to have_content(translated(taxonomy2.name)) + end + within ".js-sortable tr:nth-child(2) td:nth-child(2)" do + expect(page).to have_content(translated(taxonomy3.name)) + end + within ".js-sortable tr:last-child td:nth-child(2)" do + expect(page).to have_content(translated(taxonomy1.name)) + end + end + end + + context "when multiple pages" do + let!(:taxonomies) { create_list(:taxonomy, 51, organization:) } + + before do + visit decidim_admin.taxonomies_path(page: 2) + end + + it "displays the pagination" do + expect(page).to have_content(translated(taxonomies[25].name)) + expect(page).to have_content("Drag over for previous page") + expect(page).to have_link("Prev") + + all(".js-sortable tr").last.drag_to(all(".js-sortable tr").first) + + expect(page).to have_content("Drag over for next page") + expect(page).to have_content(translated(taxonomies[25].name)) + expect(page).to have_no_content(translated(taxonomies[24].name)) + end + end + + def click_delete_taxonomy + within "tr", text: translated(taxonomy.name) do + accept_confirm { click_on "Delete" } + end + end + + def click_edit_taxonomy + within "tr", text: translated(taxonomy.name) do + click_on "Edit" + end + end +end diff --git a/decidim-assemblies/lib/decidim/assemblies/seeds.rb b/decidim-assemblies/lib/decidim/assemblies/seeds.rb index f23f716052a39..75a81a9fb9c79 100644 --- a/decidim-assemblies/lib/decidim/assemblies/seeds.rb +++ b/decidim-assemblies/lib/decidim/assemblies/seeds.rb @@ -8,6 +8,11 @@ class Seeds < Decidim::Seeds def call create_content_block! + taxonomy = create_taxonomy!(name: "Assembly Types", parent: nil) + 2.times do + create_taxonomy!(name: ::Faker::Lorem.word, parent: taxonomy) + end + 2.times do |_n| assembly = create_assembly! diff --git a/decidim-core/app/helpers/decidim/modal_helper.rb b/decidim-core/app/helpers/decidim/modal_helper.rb index bc44f2f0346da..9f30f439f4c09 100644 --- a/decidim-core/app/helpers/decidim/modal_helper.rb +++ b/decidim-core/app/helpers/decidim/modal_helper.rb @@ -34,5 +34,28 @@ def decidim_modal(opts = {}, &) end end end + + def decidim_drawer(opts = {}, &) + opts[:closable] = true unless opts.has_key?(:closable) + + button = if opts[:closable] == false + "" + else + content_tag( + :button, + "×".html_safe, + type: :button, + data: { dialog_close: opts[:id] || "", dialog_closable: "" }, + "aria-label": t("close_modal", scope: "decidim.shared.confirm_modal") + ) + end + + content = opts[:remote].nil? ? button + capture(&).html_safe : button + icon("loader-3-line") + content_tag(:div, id: opts[:id], data: { dialog: opts[:id] || "", drawer: true }.merge(opts[:data] || {})) do + content_tag(:div, id: "#{opts[:id]}-content", class: opts[:class]) do + content + end + end + end end end diff --git a/decidim-core/app/models/decidim/organization.rb b/decidim-core/app/models/decidim/organization.rb index 9215737ddf4ef..5be5af8810402 100644 --- a/decidim-core/app/models/decidim/organization.rb +++ b/decidim-core/app/models/decidim/organization.rb @@ -35,6 +35,8 @@ class Organization < ApplicationRecord has_many :templates, foreign_key: "decidim_organization_id", class_name: "Decidim::Templates::Template", dependent: :destroy if defined? Decidim::Templates + has_many :taxonomies, foreign_key: "decidim_organization_id", class_name: "Decidim::Taxonomy", inverse_of: :organization, dependent: :destroy + # Users registration mode. Whether users can register or access the system. Does not affect users that access through Omniauth integrations. # enabled: Users registration and sign in are enabled (default value). # existing: Users cannot be registered in the system. Only existing users can sign in. diff --git a/decidim-core/app/models/decidim/taxonomization.rb b/decidim-core/app/models/decidim/taxonomization.rb new file mode 100644 index 0000000000000..db502a5b32bb5 --- /dev/null +++ b/decidim-core/app/models/decidim/taxonomization.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Decidim + # Represents the link between a taxonomy and any taxonomizable entity. + class Taxonomization < ApplicationRecord + belongs_to :taxonomy, + class_name: "Decidim::Taxonomy", + counter_cache: :taxonomizations_count, + inverse_of: :taxonomizations + + belongs_to :taxonomizable, polymorphic: true + + validate :prevent_root_taxonomization + + private + + def prevent_root_taxonomization + return unless taxonomy.root? + + errors.add(:taxonomy, :invalid) + end + end +end diff --git a/decidim-core/app/models/decidim/taxonomy.rb b/decidim-core/app/models/decidim/taxonomy.rb new file mode 100644 index 0000000000000..5619334c1ae6f --- /dev/null +++ b/decidim-core/app/models/decidim/taxonomy.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module Decidim + # Represents a hierarchical classification used to organize various entities within the system. + # Taxonomies are primarily used to categorize and manage different aspects of participatory spaces, + # such as proposals, assemblies, and other components, within an organization. + class Taxonomy < ApplicationRecord + include Decidim::TranslatableResource + include Decidim::FilterableResource + include Decidim::Traceable + + before_create :set_default_weight + + translatable_fields :name + + belongs_to :organization, + foreign_key: "decidim_organization_id", + class_name: "Decidim::Organization", + inverse_of: :taxonomies + + belongs_to :parent, + class_name: "Decidim::Taxonomy", + counter_cache: :children_count, + optional: true + + has_many :children, + foreign_key: "parent_id", + class_name: "Decidim::Taxonomy", + dependent: :destroy + + has_many :taxonomizations, class_name: "Decidim::Taxonomization", dependent: :destroy + + validates :name, presence: true + validates :weight, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true + validate :validate_max_children_levels + + default_scope { order(:weight) } + + ransacker_i18n :name + + scope :search_by_name, lambda { |name| + where("name ->> ? ILIKE ?", I18n.locale.to_s, "%#{name}%") + } + + def self.log_presenter_class_for(_log) + Decidim::AdminLog::TaxonomyPresenter + end + + def self.ransackable_scopes(_auth_object = nil) + [:search_by_name] + end + + def translated_name + Decidim::TaxonomyPresenter.new(self).translated_name + end + + def root_taxonomy + @root_taxonomy ||= root? ? self : parent.root_taxonomy + end + + def parent_ids + @parent_ids ||= parent_id ? parent.parent_ids + [parent_id] : [] + end + + def root? = parent_id.nil? + + def removable? + true + end + + private + + def set_default_weight + return if weight.present? + + self.weight = Taxonomy.where(parent_id:).count + end + + def validate_max_children_levels + return unless parent_id + + errors.add(:base, :invalid) if parent_ids.size > 3 + end + end +end diff --git a/decidim-core/app/packs/src/decidim/index.js b/decidim-core/app/packs/src/decidim/index.js index 2077d6cb82dc5..ed6677024f3b1 100644 --- a/decidim-core/app/packs/src/decidim/index.js +++ b/decidim-core/app/packs/src/decidim/index.js @@ -196,6 +196,8 @@ const initializer = (element = document) => { element.querySelectorAll("[data-toggle]").forEach((elem) => createToggle(elem)) element.querySelectorAll(".new_report").forEach((elem) => changeReportFormBehavior(elem)) + + document.dispatchEvent(new CustomEvent("decidim:loaded", { detail: { element } })); } // If no jQuery is used the Tribute feature used in comments to autocomplete diff --git a/decidim-core/app/packs/stylesheets/decidim/_modal.scss b/decidim-core/app/packs/stylesheets/decidim/_modal.scss index 508c8f825facd..d5f937761663a 100644 --- a/decidim-core/app/packs/stylesheets/decidim/_modal.scss +++ b/decidim-core/app/packs/stylesheets/decidim/_modal.scss @@ -42,4 +42,42 @@ @apply w-full md:w-auto; } } + + &[data-drawer] { + & > * { + @apply h-full max-h-full w-0 inset-0 translate-x-0 translate-y-0 right-0 left-auto max-w-[90%] lg:max-w-[900px] transition-none transition-[width] duration-500 overflow-visible; + } + + &[aria-hidden="false"] { + > * { + @apply w-[60%]; + } + } + + [data-dialog-container] { + @apply grid-cols-none; + + &.layout-content { + @apply bg-white; + + > .spinner-container { + @apply sticky h-screen; + } + } + } + + [data-dialog-closable] { + @apply absolute top-0 right-auto -left-10 p-4 py-2 bg-white rounded-bl; + } + + .form-defaults { + h1 { + @apply text-2xl font-semibold text-black my-10 mx-4 border-b border-gray border-b-[1px] pb-8; + + > .button { + @apply text-sm float-right; + } + } + } + } } diff --git a/decidim-core/app/presenters/decidim/admin_log/taxonomy_presenter.rb b/decidim-core/app/presenters/decidim/admin_log/taxonomy_presenter.rb new file mode 100644 index 0000000000000..04f19f710da53 --- /dev/null +++ b/decidim-core/app/presenters/decidim/admin_log/taxonomy_presenter.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Decidim + module AdminLog + # This class holds the logic to present a `Decidim::Taxonomy` + # for the `AdminLog` log. + # + # Usage should be automatic and you should not need to call this class + # directly, but here is an example: + # + # action_log = Decidim::ActionLog.last + # view_helpers # => this comes from the views + # TaxonomyPresenter.new(action_log, view_helpers).present + class TaxonomyPresenter < Decidim::Log::BasePresenter + private + + def diff_fields_mapping + { + name: :i18n, + parent_id: :taxonomy + } + end + + def action_string + case action + when "create", "delete", "update" + if parent_name.present? + "decidim.admin_log.taxonomy.#{action}_with_parent" + else + "decidim.admin_log.taxonomy.#{action}" + end + else + super + end + end + + def i18n_labels_taxonomy + "activemodel.attributes.taxonomy" + end + + def i18n_params + super.merge( + parent_taxonomy: h.translated_attribute(parent_name) + ) + end + + def parent_name + action_log.extra.dig("extra", "parent_name") + end + end + end +end diff --git a/decidim-core/app/presenters/decidim/log/value_types/taxonomy_presenter.rb b/decidim-core/app/presenters/decidim/log/value_types/taxonomy_presenter.rb new file mode 100644 index 0000000000000..fa928aafb881f --- /dev/null +++ b/decidim-core/app/presenters/decidim/log/value_types/taxonomy_presenter.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Decidim + module Log + module ValueTypes + # This class presents the given value as a Decidim::Taxonomy. Check + # the `DefaultPresenter` for more info on how value + # presenters work. + class TaxonomyPresenter < DefaultPresenter + # Public: Presents the value as a Decidim::Taxonomy. If the taxonomy can + # be found, it shows its title. Otherwise it shows its ID. + # + # Returns an HTML-safe String. + def present + return unless value + return h.translated_attribute(taxonomy.name) if taxonomy + + I18n.t("not_found", id: value, scope: "decidim.log.value_types.taxonomy_presenter") + end + + private + + def taxonomy + @taxonomy ||= Decidim::Taxonomy.find_by(id: value) + end + end + end + end +end diff --git a/decidim-core/app/presenters/decidim/taxonomy_presenter.rb b/decidim-core/app/presenters/decidim/taxonomy_presenter.rb new file mode 100644 index 0000000000000..27b636cbe10eb --- /dev/null +++ b/decidim-core/app/presenters/decidim/taxonomy_presenter.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Decidim + # + # Decorator for taxonomies. + # + class TaxonomyPresenter < SimpleDelegator + include Decidim::TranslationsHelper + + def translated_name + @translated_name ||= translated_attribute name + end + end +end diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index 7fda4b9b0ed11..f506ca68ae265 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -20,6 +20,8 @@ en: body: Body report: details: Additional comments + taxonomy: + name: Taxonomy name user: about: About email: Your email @@ -261,6 +263,13 @@ en: create: "%{user_name} created the %{resource_name} static page" delete: "%{user_name} deleted the %{resource_name} static page" update: "%{user_name} updated the %{resource_name} static page" + taxonomy: + create: "%{user_name} created the %{resource_name} taxonomy" + create_with_parent: "%{user_name} created the %{resource_name} taxonomy inside the %{parent_taxonomy} taxonomy" + delete: "%{user_name} deleted the %{resource_name} taxonomy" + delete_with_parent: "%{user_name} deleted the %{resource_name} taxonomy inside the %{parent_taxonomy} taxonomy" + update: "%{user_name} updated the %{resource_name} taxonomy" + update_with_parent: "%{user_name} updated the %{resource_name} taxonomy inside the %{parent_taxonomy} taxonomy" user: block: "%{user_name} blocked user %{resource_name}" invite: "%{user_name} invited the participant %{resource_name} with role: %{role}" @@ -1091,6 +1100,8 @@ en: not_found: 'The scope was not found on the database (ID: %{id})' scope_type_presenter: not_found: 'The scope type was not found on the database (ID: %{id})' + taxonomy_presenter: + not_found: 'The taxonomy was not found on the database (ID: %{id})' managed_users: expired_session: The current administration session of a participant has expired. map: diff --git a/decidim-core/db/migrate/20240704115429_create_decidim_taxonomies.rb b/decidim-core/db/migrate/20240704115429_create_decidim_taxonomies.rb new file mode 100644 index 0000000000000..cf7ace36750dc --- /dev/null +++ b/decidim-core/db/migrate/20240704115429_create_decidim_taxonomies.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class CreateDecidimTaxonomies < ActiveRecord::Migration[7.0] + def change + create_table :decidim_taxonomies do |t| + t.jsonb :name, null: false, default: {} + t.references :decidim_organization, null: false, index: true + t.references :parent, index: true + t.integer :weight + t.integer :children_count, null: false, default: 0 + t.integer :taxonomizations_count, null: false, default: 0 + t.timestamps + end + + create_table :decidim_taxonomizations do |t| + t.references :taxonomy, null: false, index: true + t.references :taxonomizable, null: false, polymorphic: true, index: { name: "index_taxonomizations_on_taxonomizable" } + t.timestamps + end + + add_index :decidim_taxonomizations, [:taxonomy_id, :taxonomizable_id, :taxonomizable_type], name: "index_taxonomizations_on_id_tid_and_ttype", unique: true + end +end diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index fe310334112c0..fdb1f714fd8c4 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -173,6 +173,7 @@ class Engine < ::Rails::Engine Decidim.icons.register(name: "dislike", icon: "dislike-line", description: "Dislike", category: "action", engine: :core) Decidim.icons.register(name: "drag-move-2-line", icon: "drag-move-2-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "drag-move-2-fill", icon: "drag-move-2-fill", category: "system", description: "", engine: :core) + Decidim.icons.register(name: "draggable", icon: "draggable", category: "system", description: "", engine: :core) Decidim.icons.register(name: "login-circle-line", icon: "login-circle-line", category: "system", description: "", engine: :core) Decidim.icons.register(name: "list-check", icon: "list-check", category: "system", description: "", engine: :core) Decidim.icons.register(name: "add-fill", icon: "add-fill", category: "system", description: "", engine: :core) diff --git a/decidim-core/lib/decidim/core/seeds.rb b/decidim-core/lib/decidim/core/seeds.rb index 8234648923d04..37f7f267d1bcc 100644 --- a/decidim-core/lib/decidim/core/seeds.rb +++ b/decidim-core/lib/decidim/core/seeds.rb @@ -18,6 +18,36 @@ def call organization = create_organization! + if organization.taxonomies.none? + taxonomy = create_taxonomy!(name: "Scopes", parent: nil) + 3.times do + sub_taxonomy = create_taxonomy!(name: ::Faker::Address.state, parent: taxonomy) + + 5.times do + create_taxonomy!(name: ::Faker::Address.city, parent: sub_taxonomy) + end + end + + taxonomy = create_taxonomy!(name: "Areas", parent: nil) + sub_taxonomy = create_taxonomy!(name: "Territorial", parent: taxonomy) + 3.times do + create_taxonomy!(name: ::Faker::Lorem.word, parent: sub_taxonomy) + end + sub_taxonomy = create_taxonomy!(name: "Sectorial", parent: taxonomy) + 5.times do + create_taxonomy!(name: ::Faker::Lorem.word, parent: sub_taxonomy) + end + + taxonomy = create_taxonomy!(name: "Categories", parent: nil) + 3.times do + sub_taxonomy = create_taxonomy!(name: ::Faker::Lorem.sentence(word_count: 5), parent: taxonomy) + + 5.times do + create_taxonomy!(name: ::Faker::Lorem.sentence(word_count: 5), parent: sub_taxonomy) + end + end + end + if organization.top_scopes.none? province = create_scope_type!(name: "province", plural: "provinces") municipality = create_scope_type!(name: "municipality", plural: "municipalities") diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index 7115d3fcd83f5..79ec6ed81582b 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -628,6 +628,38 @@ def generate_localized_title(field = nil, skip_injection: false) organization end + factory :taxonomy, class: "Decidim::Taxonomy" do + transient do + skip_injection { false } + end + + name { generate_localized_title(:taxonomy_name, skip_injection:) } + organization + parent { nil } + weight { nil } + + trait :with_parent do + association :parent, factory: :taxonomy + end + + trait :with_children do + transient do + children_count { 3 } + end + + after(:create) do |taxonomy, evaluator| + create_list(:taxonomy, evaluator.children_count, parent: taxonomy, organization: taxonomy.organization) + taxonomy.reload + taxonomy.update(weight: taxonomy.children.count) + end + end + end + + factory :taxonomization, class: "Decidim::Taxonomization" do + taxonomy { association(:taxonomy, :with_parent) } + taxonomizable { association(:dummy_resource) } + end + factory :coauthorship, class: "Decidim::Coauthorship" do transient do skip_injection { false } diff --git a/decidim-core/lib/decidim/seeds.rb b/decidim-core/lib/decidim/seeds.rb index 1682e9e716aa1..82ea5eab8fa0d 100644 --- a/decidim-core/lib/decidim/seeds.rb +++ b/decidim-core/lib/decidim/seeds.rb @@ -97,6 +97,14 @@ def create_blob!(seeds_file:, filename:, content_type:) ) end + def create_taxonomy!(name:, parent:) + Decidim::Taxonomy.create!( + name: Decidim::Faker::Localized.literal(name), + organization:, + parent: + ) + end + def create_category!(participatory_space:) Decidim::Category.create!( name: Decidim::Faker::Localized.sentence(word_count: 5), diff --git a/decidim-core/spec/models/decidim/taxonomy_spec.rb b/decidim-core/spec/models/decidim/taxonomy_spec.rb new file mode 100644 index 0000000000000..388fe12703493 --- /dev/null +++ b/decidim-core/spec/models/decidim/taxonomy_spec.rb @@ -0,0 +1,165 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe Taxonomy do + subject(:taxonomy) { build(:taxonomy, name: taxonomy_name, parent: root_taxonomy, organization:) } + + let(:organization) { create(:organization) } + let(:root_taxonomy) { create(:taxonomy, organization:) } + let(:taxonomy_name) { attributes_for(:taxonomy)[:name] } + + context "when everything is ok" do + it { is_expected.to be_valid } + it { is_expected.not_to be_root } + + it "returns the root taxonomy" do + expect(taxonomy.root_taxonomy).to eq(root_taxonomy) + end + + it "returns the parent ids" do + expect(taxonomy.parent_ids).to eq([root_taxonomy.id]) + end + end + + context "when root taxonomy" do + subject(:taxonomy) { root_taxonomy } + + it { is_expected.to be_root } + end + + context "when a child taxonomy" do + subject(:child) { create(:taxonomy, parent: taxonomy, organization:) } + + it { is_expected.not_to be_root } + + it "returns the root taxonomy" do + expect(child.root_taxonomy).to eq(root_taxonomy) + end + + it "returns the parent ids" do + expect(child.parent_ids).to eq([root_taxonomy.id, taxonomy.id]) + end + end + + context "when name is missing" do + let(:taxonomy_name) { nil } + + it { is_expected.to be_invalid } + end + + context "when organization is missing" do + before { taxonomy.organization = nil } + + it { is_expected.to be_invalid } + end + + describe "#weight" do + let!(:taxonomy1) { create(:taxonomy, organization:) } + let!(:taxonomy2) { create(:taxonomy, organization:) } + + it "sets a default weight" do + expect(taxonomy1.weight).to eq(0) + expect(taxonomy2.weight).to eq(1) + end + + context "when different parents" do + let!(:taxonomy1_child1) { create(:taxonomy, parent: taxonomy1, organization:) } + let!(:taxonomy1_child2) { create(:taxonomy, parent: taxonomy1, organization:) } + let!(:taxonomy2_child1) { create(:taxonomy, parent: taxonomy2, organization:) } + let!(:taxonomy2_child2) { create(:taxonomy, parent: taxonomy2, organization:) } + + it "sets a default weight for children" do + expect(taxonomy1_child1.weight).to eq(0) + expect(taxonomy1_child2.weight).to eq(1) + expect(taxonomy2_child1.weight).to eq(0) + expect(taxonomy2_child2.weight).to eq(1) + end + end + + context "when weight is set" do + subject(:taxonomy) { create(:taxonomy, weight: 5, organization:) } + + it "sets the specified weight" do + expect(taxonomy.weight).to eq(5) + end + end + end + + context "when managing associations" do + context "with children" do + let!(:child_taxonomy) { create(:taxonomy, parent: taxonomy, organization:) } + + it "can belong to a parent taxonomy" do + expect(taxonomy.parent).to eq(root_taxonomy) + end + + it "can have many children taxonomies" do + expect(taxonomy.children).to include(child_taxonomy) + expect(taxonomy.children.count).to eq(1) + end + + it "can be deleted with children" do + expect(root_taxonomy.children_count).to eq(1) + expect { taxonomy.destroy }.to change(Decidim::Taxonomy, :count).by(-2) + expect(Decidim::Taxonomy.find_by(id: taxonomy.id)).to be_nil + expect(root_taxonomy.children_count).to eq(0) + end + + context "when more than 3 levels of children" do + subject(:child_of_child_taxonomy) { build(:taxonomy, parent: grandchild_taxonomy, organization:) } + + let(:grandchild_taxonomy) { create(:taxonomy, parent: child_taxonomy, organization:) } + + it { is_expected.to be_invalid } + end + end + + context "with taxonomizations" do + let!(:taxonomization) { create(:taxonomization, taxonomy:) } + + it "can be deleted if it has taxonomizations" do + expect { taxonomy.destroy }.to change(Decidim::Taxonomy, :count).by(-1) + end + end + + context "when adding taxonomizations" do + let(:taxonomization) { build(:taxonomization, taxonomy:) } + + it "can be associated with a taxonomization" do + expect(taxonomy.taxonomizations_count).to eq(0) + taxonomy.taxonomizations << taxonomization + taxonomy.save + expect(taxonomy.taxonomizations).to include(taxonomization) + expect(taxonomy.taxonomizations_count).to eq(1) + end + end + + context "when adding taxonomizations to the root taxonomy" do + let(:taxonomization) { build(:taxonomization, taxonomy: root_taxonomy) } + + it "cannot be associated with a taxonomization" do + root_taxonomy.taxonomizations << taxonomization + expect(root_taxonomy).to be_invalid + expect(taxonomization).to be_invalid + end + end + end + + context "when using ransackable scopes" do + let(:taxonomy_attributes1) { attributes_for(:taxonomy) } + let(:taxonomy_attributes2) { attributes_for(:taxonomy) } + let(:taxonomy_name1) { taxonomy_attributes1[:name] } + let(:taxonomy_name2) { taxonomy_attributes2[:name] } + let!(:taxonomy1) { create(:taxonomy, name: taxonomy_name1, organization:) } + let!(:taxonomy2) { create(:taxonomy, name: taxonomy_name2, organization:) } + + it "returns taxonomies matching the name" do + result = described_class.search_by_name(translated(taxonomy_attributes1[:name])) + expect(result).to include(taxonomy1) + expect(result).not_to include(taxonomy2) + end + end + end +end diff --git a/decidim-core/spec/presenters/decidim/log/value_types/taxonomy_presenter_spec.rb b/decidim-core/spec/presenters/decidim/log/value_types/taxonomy_presenter_spec.rb new file mode 100644 index 0000000000000..63a97eda8a60e --- /dev/null +++ b/decidim-core/spec/presenters/decidim/log/value_types/taxonomy_presenter_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Log::ValueTypes::TaxonomyPresenter, type: :helper do + subject { described_class.new(value, helper) } + + let(:value) { taxonomy.id } + let!(:taxonomy) { create(:taxonomy) } + + before do + helper.extend(Decidim::ApplicationHelper) + helper.extend(Decidim::TranslationsHelper) + end + + describe "#present" do + context "when the taxonomy is found" do + let(:title) { taxonomy.name["en"] } + + it "shows the taxonomy title" do + expect(subject.present).to eq title + end + end + + context "when the taxonomy is not found" do + let(:value) { taxonomy.id + 1 } + + it "shows a string explaining the problem" do + expect(subject.present).to eq "The taxonomy was not found on the database (ID: #{value})" + end + end + end +end diff --git a/decidim-participatory_processes/lib/decidim/participatory_processes/seeds.rb b/decidim-participatory_processes/lib/decidim/participatory_processes/seeds.rb index e4fb84c5cd49d..232de01fc8601 100644 --- a/decidim-participatory_processes/lib/decidim/participatory_processes/seeds.rb +++ b/decidim-participatory_processes/lib/decidim/participatory_processes/seeds.rb @@ -18,6 +18,11 @@ def call Decidim::ContentBlocksCreator.new(process_group).create_default! end + taxonomy = create_taxonomy!(name: "Process Types", parent: nil) + 2.times do + create_taxonomy!(name: ::Faker::Lorem.word, parent: taxonomy) + end + process_types = [] 2.times do process_types << create_process_type! From 92998ada9660165d0faa545a3707c0deeeb3a538 Mon Sep 17 00:00:00 2001 From: Anna Topalidi <60363870+antopalidi@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:45:07 +0200 Subject: [PATCH 11/22] Provide bulk action answers to proposals through answers templates (#13057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add bulk action for uptading proposals * fix context with organization * fix redirect * add controller spec * add job spec * add spec * fix spec * change generate token * fix lint * changes after the review * refactor controller and job * remove unused key * fix specs * fix spec * interpolate answers * check for translated fields * check for machine translations * apply corrections * update spec * cops * fix template detection --------- Co-authored-by: Ivan Vergés --- .../proposals/admin/needs_interpolations.rb | 40 ++++++ .../admin/proposal_answers_controller.rb | 52 ++++++- .../proposals/admin/proposal_answer_form.rb | 1 + .../admin/proposal_bulk_actions_helper.rb | 25 ++++ .../decidim/proposals/application_helper.rb | 4 + .../proposals/admin/proposal_answer_job.rb | 20 +++ .../decidim/proposals/proposal_state.rb | 2 +- .../src/decidim/proposals/admin/proposals.js | 13 ++ .../admin/proposal_answers/_form.html.erb | 2 +- .../admin/proposals/_bulk-actions.html.erb | 3 + .../admin/proposals/_proposal-tr.html.erb | 4 +- .../_apply_answer_template.html.erb | 22 +++ .../proposals/bulk_actions/_dropdown.html.erb | 7 + decidim-proposals/config/locales/en.yml | 4 + .../lib/decidim/proposals/admin_engine.rb | 1 + .../admin/proposal_answers_controller_spec.rb | 119 ++++++++++++++++ .../admin/proposal_answer_job_spec.rb | 62 ++++++++ .../decidim/proposals/proposal_state_spec.rb | 41 +++++- .../admin_manages_proposal_states_spec.rb | 8 +- .../proposal_answer_templates_controller.rb | 17 +-- ...es_proposal_bulk_answers_templates_spec.rb | 133 ++++++++++++++++++ 21 files changed, 551 insertions(+), 29 deletions(-) create mode 100644 decidim-proposals/app/controllers/concerns/decidim/proposals/admin/needs_interpolations.rb create mode 100644 decidim-proposals/app/jobs/decidim/proposals/admin/proposal_answer_job.rb create mode 100644 decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_apply_answer_template.html.erb create mode 100644 decidim-proposals/spec/controllers/decidim/proposals/admin/proposal_answers_controller_spec.rb create mode 100644 decidim-proposals/spec/jobs/decidim/proposals/admin/proposal_answer_job_spec.rb create mode 100644 decidim-templates/spec/system/admin/admin_manages_proposal_bulk_answers_templates_spec.rb diff --git a/decidim-proposals/app/controllers/concerns/decidim/proposals/admin/needs_interpolations.rb b/decidim-proposals/app/controllers/concerns/decidim/proposals/admin/needs_interpolations.rb new file mode 100644 index 0000000000000..5adcb0231701b --- /dev/null +++ b/decidim-proposals/app/controllers/concerns/decidim/proposals/admin/needs_interpolations.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Proposals + module Admin + module NeedsInterpolations + extend ActiveSupport::Concern + + included do + def populate_interpolations(text, proposal) + return populate_string_interpolations(text, proposal) if text.is_a?(String) + + populate_hash_interpolations(text, proposal) + end + + def populate_hash_interpolations(hash, proposal) + return hash unless hash.is_a?(Hash) + + hash.transform_values do |value| + populate_interpolations(value, proposal) + end + end + + def populate_string_interpolations(value, proposal) + value = value.gsub("%{organization}", translated_attribute(proposal.organization.name)) + value = value.gsub("%{name}", author_name(proposal)) + value.gsub("%{admin}", current_user.name) + end + + def author_name(proposal) + name = proposal.creator_author.try(:title) || proposal.creator_author.try(:name) + translated_attribute(name) + end + end + end + end + end +end diff --git a/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb b/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb index eb69d6a749ad2..fe00dd686c0e1 100644 --- a/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb +++ b/decidim-proposals/app/controllers/decidim/proposals/admin/proposal_answers_controller.rb @@ -5,6 +5,9 @@ module Proposals module Admin # This controller allows admins to answer proposals in a participatory process. class ProposalAnswersController < Admin::ApplicationController + include ActionView::Helpers::SanitizeHelper + include Decidim::Proposals::Admin::NeedsInterpolations + helper_method :proposal helper Proposals::ApplicationHelper @@ -14,13 +17,13 @@ class ProposalAnswersController < Admin::ApplicationController def edit enforce_permission_to(:create, :proposal_answer, proposal:) - @form = form(Admin::ProposalAnswerForm).from_model(proposal) + @form = form(ProposalAnswerForm).from_model(proposal) end def update enforce_permission_to(:create, :proposal_answer, proposal:) @notes_form = form(ProposalNoteForm).instance - @answer_form = form(Admin::ProposalAnswerForm).from_params(params) + @answer_form = form(ProposalAnswerForm).from_params(params) Admin::AnswerProposal.call(@answer_form, proposal) do on(:ok) do @@ -35,6 +38,33 @@ def update end end + def update_multiple_answers + valid_proposals = [] + failed_proposals = [] + proposals.each do |proposal| + proposal_answer_form = answer_form(proposal) + if allowed_to?(:create, :proposal_answer, proposal:) && proposal_answer_form.valid? && !proposal.emendation? + valid_proposals << proposal.id + ProposalAnswerJob.perform_later(proposal, proposal_answer_form.attributes, { current_organization:, current_component:, current_user: }) + else + failed_proposals << proposal.id + end + end + + if failed_proposals.any? + flash[:alert] = + t("proposals.answer.bulk_answer_error", scope: "decidim.proposals.admin", template: strip_tags(translated_attribute(template&.name)), + proposals: failed_proposals.join(", ")) + end + if valid_proposals.any? + flash[:notice] = + I18n.t("proposals.answer.bulk_answer_success", scope: "decidim.proposals.admin", template: strip_tags(translated_attribute(template&.name)), + count: valid_proposals.count) + end + + redirect_to EngineRouter.admin_proxy(current_component).root_path + end + private def skip_manage_component_permission @@ -44,6 +74,24 @@ def skip_manage_component_permission def proposal @proposal ||= Proposal.where(component: current_component).find(params[:id]) end + + def proposals + @proposals ||= Proposal.where(component: current_component).where(id: params[:proposal_ids]) + end + + def template + return unless Decidim.module_installed?(:templates) + + @template ||= Decidim::Templates::Template.find_by(id: params[:template][:template_id]) + end + + def answer_form(proposal) + form(ProposalAnswerForm).from_params(answer: populate_interpolations(template&.description, proposal), internal_state: proposal_state&.token) + end + + def proposal_state + @proposal_state ||= Decidim::Proposals::ProposalState.find_by(id: template&.field_values&.dig("proposal_state_id")) + end end end end diff --git a/decidim-proposals/app/forms/decidim/proposals/admin/proposal_answer_form.rb b/decidim-proposals/app/forms/decidim/proposals/admin/proposal_answer_form.rb index b322dd7ef23c0..5e0bfe15a8926 100644 --- a/decidim-proposals/app/forms/decidim/proposals/admin/proposal_answer_form.rb +++ b/decidim-proposals/app/forms/decidim/proposals/admin/proposal_answer_form.rb @@ -6,6 +6,7 @@ module Admin # A form object to be used when admin users want to answer a proposal. class ProposalAnswerForm < Decidim::Form include TranslatableAttributes + mimic :proposal_answer translatable_attribute :answer, String diff --git a/decidim-proposals/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb b/decidim-proposals/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb index ab78c964b032c..c10e7783aa8ed 100644 --- a/decidim-proposals/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb +++ b/decidim-proposals/app/helpers/decidim/proposals/admin/proposal_bulk_actions_helper.rb @@ -8,6 +8,31 @@ def proposal_find(id) Decidim::Proposals::Proposal.find(id) end + # Public: Generates a select field with the templates of the given component. + # + # component - A component instance. + # prompt - An i18n string to show as prompt + # + # Returns a String. + def bulk_templates_select(component, prompt, id: nil) + options_for_select = find_templates_for_select(component) + select(:template, :template_id, options_for_select, prompt:, id:) + end + + def find_templates_for_select(component) + return [] unless Decidim.module_installed? :templates + return @templates_for_select if @templates_for_select + + templates = Decidim::Templates::Template.where( + target: :proposal_answer, + templatable: component + ).order(:templatable_id) + + @templates_for_select = templates.map do |template| + [translated_attribute(template.name), template.id] + end + end + # find the valuators for the current space. def find_valuators_for_select(participatory_space, current_user) valuator_roles = participatory_space.user_roles(:valuator) diff --git a/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb b/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb index c9cfacf252e8e..9d8d9f6ea1e4b 100644 --- a/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb +++ b/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb @@ -248,6 +248,10 @@ def component_name i18n_key = controller_name == "collaborative_drafts" ? "decidim.proposals.collaborative_drafts.name" : "decidim.components.proposals.name" (defined?(current_component) && translated_attribute(current_component&.name).presence) || t(i18n_key) end + + def templates_available? + Decidim.module_installed?(:templates) && defined?(Decidim::Templates::Template) && Decidim::Templates::Template.exists?(templatable: current_component) + end end end end diff --git a/decidim-proposals/app/jobs/decidim/proposals/admin/proposal_answer_job.rb b/decidim-proposals/app/jobs/decidim/proposals/admin/proposal_answer_job.rb new file mode 100644 index 0000000000000..918a5d0ad982a --- /dev/null +++ b/decidim-proposals/app/jobs/decidim/proposals/admin/proposal_answer_job.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + module Admin + class ProposalAnswerJob < ApplicationJob + queue_as :default + + def perform(proposal, attributes, context) + answer_form = ProposalAnswerForm.from_params(attributes).with_context(**context) + + Admin::AnswerProposal.call(answer_form, proposal) do + on(:ok) { Rails.logger.info "Proposal #{proposal.id} answered successfully." } + on(:invalid) { Rails.logger.error "Proposal ID #{proposal.id} could not be updated. Errors: #{answer_form.errors.full_messages}" } + end + end + end + end + end +end diff --git a/decidim-proposals/app/models/decidim/proposals/proposal_state.rb b/decidim-proposals/app/models/decidim/proposals/proposal_state.rb index 81b0b790f2617..a91bcd84808bc 100644 --- a/decidim-proposals/app/models/decidim/proposals/proposal_state.rb +++ b/decidim-proposals/app/models/decidim/proposals/proposal_state.rb @@ -38,7 +38,7 @@ def self.colors protected def generate_token - self.token = ensure_unique_token(translated_attribute(title).parameterize(separator: "_")) + self.token = ensure_unique_token(token.presence || translated_attribute(title).parameterize(separator: "_")) end def ensure_unique_token(token) diff --git a/decidim-proposals/app/packs/src/decidim/proposals/admin/proposals.js b/decidim-proposals/app/packs/src/decidim/proposals/admin/proposals.js index 76d839953ead7..202a9a1d23685 100644 --- a/decidim-proposals/app/packs/src/decidim/proposals/admin/proposals.js +++ b/decidim-proposals/app/packs/src/decidim/proposals/admin/proposals.js @@ -13,9 +13,15 @@ $(() => { return $(".table-list [data-published-state=false] .js-check-all-proposal:checked").length } + const selectedProposalsAllowsAnswerCount = function() { + return $(".table-list [data-allow-answer=true] .js-check-all-proposal:checked").length + } + const selectedProposalsCountUpdate = function() { const selectedProposals = selectedProposalsCount(); const selectedProposalsNotPublishedAnswer = selectedProposalsNotPublishedAnswerCount(); + const allowAnswerProposals = selectedProposalsAllowsAnswerCount(); + if (selectedProposals === 0) { $("#js-selected-proposals-count").text("") $("#js-assign-proposals-to-valuator-actions").addClass("hide"); @@ -36,6 +42,13 @@ $(() => { } else { $('button[data-action="publish-answers"]').parent().hide(); } + + if (allowAnswerProposals > 0) { + $('button[data-action="apply-answer-template"]').parent().show(); + $("#js-form-apply-answer-template-number").text(allowAnswerProposals); + } else { + $('button[data-action="apply-answer-template"]').parent().hide(); + } } const showBulkActionsButton = function() { diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb index c940a84618e4a..4eac51cc3893f 100644 --- a/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb @@ -5,7 +5,7 @@

<%= t ".title", title: present(proposal).title %>

- <% if defined?(Decidim::Templates) %> + <% if templates_available? %> <%= render "decidim/templates/admin/proposal_answer_templates/template_chooser", form: f %> <% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb index 431f7fcba2917..9a9f29e9dc0fe 100644 --- a/decidim-proposals/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposals/_bulk-actions.html.erb @@ -35,3 +35,6 @@ <%= render partial: "decidim/proposals/admin/proposals/bulk_actions/merge" %> <%= render partial: "decidim/proposals/admin/proposals/bulk_actions/split" %> <%= render partial: "decidim/proposals/admin/proposals/bulk_actions/publish_answers" %> +<% if templates_available? %> + <%= render partial: "decidim/proposals/admin/proposals/bulk_actions/apply_answer_template", locals: { templates: find_templates_for_select(current_component) } %> +<% end %> diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb index b575744ae6274..ab77ab38fc54c 100644 --- a/decidim-proposals/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposals/_proposal-tr.html.erb @@ -1,4 +1,6 @@ -> +> <%= check_box_tag "proposal_ids_s[]", proposal.id, false, class: "js-check-all-proposal js-proposal-list-check js-proposal-id-#{proposal.id}" %>
diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_apply_answer_template.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_apply_answer_template.html.erb new file mode 100644 index 0000000000000..c2aec6951e1b6 --- /dev/null +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_apply_answer_template.html.erb @@ -0,0 +1,22 @@ +
+ <%= form_tag(update_multiple_answers_proposals_path, method: :post, id: "js-form-apply-answer-template", class: "form form-defaults flex items-center gap-x-2") do %> +
+ <% proposals.each do |proposal| %> + <%= check_box_tag "proposal_ids[]", proposal.id, false, class: "js-check-all-proposal js-proposal-id-#{proposal.id}" %> + <% end %> +
+ <%= hidden_field_tag :template_id %> + + <%= bulk_templates_select( + current_component, + templates.present? ? + t("apply_answer_template", scope: "decidim.proposals.admin.proposals.index") : + t("no_templates_available", scope: "decidim.proposals.admin.proposals.index"), + id: "template_id_select" + ) %> + + <%= submit_tag(t("decidim.proposals.admin.proposals.index.update"), id: "js-submit-apply-answer-template", class: "button button__sm button__secondary small button--simple float-left") %> + + + <% end %> +
diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb index 6b014272cb311..d326fbabf9fb6 100644 --- a/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposals/bulk_actions/_dropdown.html.erb @@ -49,6 +49,13 @@ <% end %> + <% if templates_available? %> +
  • + +
  • + <% end %>
  • <% end %> + <% if(extra_actions) %> + <% extra_actions.each do |action| %> +
  • + <%= link_to(*action) %> +
  • + <% end %> + <% end %>
    diff --git a/decidim-comments/app/cells/decidim/comments/comment_cell.rb b/decidim-comments/app/cells/decidim/comments/comment_cell.rb index 5e29f003fc6c1..709265af55e1b 100644 --- a/decidim-comments/app/cells/decidim/comments/comment_cell.rb +++ b/decidim-comments/app/cells/decidim/comments/comment_cell.rb @@ -67,6 +67,7 @@ def cache_hash hash.push(model.down_votes_count) hash.push(model.cache_key_with_version) hash.push(model.author.cache_key_with_version) + hash.push(extra_actions.to_s) @hash = hash.join(Decidim.cache_key_separator) end @@ -86,6 +87,27 @@ def order options[:order] || "older" end + def extra_actions + return @extra_actions if defined?(@extra_actions) && @extra_actions.present? + + @extra_actions = model.extra_actions_for(current_user) + return unless @extra_actions + + @extra_actions.map! do |action| + [ + "#{icon(action[:icon]) if action[:icon].present?}#{action[:label]}", + action[:url], + { + class: "dropdown__item" + } + ].tap do |link| + link[2][:method] = action[:method] if action[:method].present? + link[2][:remote] = action[:remote] if action[:remote].present? + link[2][:data] = action[:data] if action[:data].present? + end + end + end + def reply_id "comment#{model.id}-reply" end diff --git a/decidim-comments/app/models/decidim/comments/comment.rb b/decidim-comments/app/models/decidim/comments/comment.rb index 2b6887863bca8..2d2564d9cf93a 100644 --- a/decidim-comments/app/models/decidim/comments/comment.rb +++ b/decidim-comments/app/models/decidim/comments/comment.rb @@ -207,6 +207,10 @@ def edited? Decidim::ActionLog.where(resource: self).exists?(["extra @> ?", Arel.sql("{\"edit\":true}")]) end + def extra_actions_for(current_user) + root_commentable.try(:actions_for_comment, self, current_user) + end + private def body_length diff --git a/decidim-comments/lib/decidim/comments/commentable.rb b/decidim-comments/lib/decidim/comments/commentable.rb index 889ba3de7d91d..5a7c7f4fffb40 100644 --- a/decidim-comments/lib/decidim/comments/commentable.rb +++ b/decidim-comments/lib/decidim/comments/commentable.rb @@ -67,6 +67,17 @@ def update_comments_count update_columns(comments_count:, updated_at: Time.current) end # rubocop:enable Rails/SkipsModelValidations + + # Public: Returns an array with extra actions available for a comment and a user. + # Returns an array of hashes with the following keys: + # - label: The label to be displayed in the UI. + # - url: The action to be performed when the user clicks the label. + # - method: The HTTP method to be used when performing the action (optional). + # - icon: The icon to be displayed next to the label (optional). + # - data: Any "data-*" attributes to be included in the link (optional). + def actions_for_comment(_comment, _current_user) + [] + end end end end diff --git a/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb b/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb index 980e979ff690c..7dc5618526538 100644 --- a/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb +++ b/decidim-comments/spec/cells/decidim/comments/comment_cell_spec.rb @@ -227,5 +227,28 @@ module Decidim::Comments end end end + + describe "#extra_actions" do + let(:current_user) { create(:user, :confirmed, organization: component.organization) } + let(:actions) do + [{ + label: "Poke comment", + url: "/poke" + }] + end + + before do + allow(commentable).to receive(:actions_for_comment).with(comment, current_user).and_return(actions) + end + + it "renders the extra actions" do + expect(subject).to have_link("Poke comment", href: "/poke") + end + + it "generates a cache hash with the action data" do + hash = my_cell.send(:cache_hash) + expect(hash).to include(actions.to_s) + end + end end end diff --git a/decidim-comments/spec/models/comment_spec.rb b/decidim-comments/spec/models/comment_spec.rb index 89010614b2051..50215c08af338 100644 --- a/decidim-comments/spec/models/comment_spec.rb +++ b/decidim-comments/spec/models/comment_spec.rb @@ -322,6 +322,24 @@ module Comments end end end + + describe "#extra_actions_for" do + it "returns blank" do + expect(comment.extra_actions_for(author)).to eq([]) + end + + context "when the root commentable provides actions" do + let(:actions) { "Some actions" } + + before do + allow(commentable).to receive(:actions_for_comment).with(comment, author).and_return(actions) + end + + it "returns the actions" do + expect(comment.extra_actions_for(author)).to eq(actions) + end + end + end end end end diff --git a/decidim-core/app/jobs/decidim/event_publisher_job.rb b/decidim-core/app/jobs/decidim/event_publisher_job.rb index 58598d04ba639..4241edf5a0bea 100644 --- a/decidim-core/app/jobs/decidim/event_publisher_job.rb +++ b/decidim-core/app/jobs/decidim/event_publisher_job.rb @@ -4,21 +4,26 @@ module Decidim class EventPublisherJob < ApplicationJob queue_as :events - attr_reader :resource + attr_reader :resource, :data def perform(event_name, data) @resource = data[:resource] + @data = data return unless data[:force_send] || notifiable? - EmailNotificationGeneratorJob.perform_later( - event_name, - data[:event_class], - data[:resource], - data[:followers], - data[:affected_users], - data[:extra] - ) + if event_type.include?(:email) + EmailNotificationGeneratorJob.perform_later( + event_name, + data[:event_class], + data[:resource], + data[:followers], + data[:affected_users], + data[:extra] + ) + end + + return unless event_type.include?(:notification) NotificationGeneratorJob.perform_later( event_name, @@ -32,6 +37,10 @@ def perform(event_name, data) private + def event_type + (data[:event_class].presence && data[:event_class].safe_constantize&.types) || [] + end + # Whether this event should be notified or not. Useful when you want the # event to decide based on the params. # diff --git a/decidim-core/lib/decidim/core/engine.rb b/decidim-core/lib/decidim/core/engine.rb index fdb1f714fd8c4..124772d77ccc8 100644 --- a/decidim-core/lib/decidim/core/engine.rb +++ b/decidim-core/lib/decidim/core/engine.rb @@ -178,6 +178,7 @@ class Engine < ::Rails::Engine Decidim.icons.register(name: "list-check", icon: "list-check", category: "system", description: "", engine: :core) Decidim.icons.register(name: "add-fill", icon: "add-fill", category: "system", description: "", engine: :core) Decidim.icons.register(name: "clipboard-line", icon: "clipboard-line", category: "system", description: "", engine: :initiatives) + Decidim.icons.register(name: "user-forbid-line", icon: "user-forbid-line", category: "system", description: "", engine: :core) # Refactor later: Some of the icons here are duplicated, and it would be a greater refactor to remove the duplicates Decidim.icons.register(name: "Decidim::Amendment", icon: "git-branch-line", category: "activity", description: "Amendment", engine: :core) diff --git a/decidim-core/lib/decidim/core/test/factories.rb b/decidim-core/lib/decidim/core/test/factories.rb index 79ec6ed81582b..aa452381b7664 100644 --- a/decidim-core/lib/decidim/core/test/factories.rb +++ b/decidim-core/lib/decidim/core/test/factories.rb @@ -769,6 +769,11 @@ def generate_localized_title(field = nil, skip_injection: false) some_extra_data: "1" } end + + trait :proposal_coauthor_invite do + event_name { "decidim.events.proposals.coauthor_invited" } + event_class { "Decidim::Proposals::CoauthorInvitedEvent" } + end end factory :conversation, class: "Decidim::Messaging::Conversation" do diff --git a/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb b/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb index 02c77417f4d0f..10bcc8fed8949 100644 --- a/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb +++ b/decidim-core/lib/decidim/core/test/shared_examples/simple_event.rb @@ -105,25 +105,6 @@ end end - describe "resource_path" do - it "is generated correctly" do - expect(subject.resource_path).to be_kind_of(String) - end - end - - describe "resource_url" do - it "is generated correctly" do - expect(subject.resource_url).to be_kind_of(String) - expect(subject.resource_url).to start_with("http") - end - end - - describe "resource_title" do - it "responds to the method" do - expect(subject).to respond_to(:resource_title) - end - end - unless skip_space_checks describe "participatory_space_url" do it "is generated correctly" do @@ -197,3 +178,36 @@ end end end + +shared_examples_for "a notification event only" do + describe "types" do + subject { described_class } + + it "supports notifications" do + expect(subject.types).to include :notification + end + + it "does not support emails" do + expect(subject.types).not_to include :email + end + end + + describe "resource_path" do + it "is generated correctly" do + expect(subject.resource_path).to be_kind_of(String) + end + end + + describe "resource_url" do + it "is generated correctly" do + expect(subject.resource_url).to be_kind_of(String) + expect(subject.resource_url).to start_with("http") + end + end + + describe "resource_title" do + it "responds to the method" do + expect(subject).to respond_to(:resource_title) + end + end +end diff --git a/decidim-core/spec/jobs/decidim/event_publisher_job_spec.rb b/decidim-core/spec/jobs/decidim/event_publisher_job_spec.rb index c65d97c6f022b..dac34cc406891 100644 --- a/decidim-core/spec/jobs/decidim/event_publisher_job_spec.rb +++ b/decidim-core/spec/jobs/decidim/event_publisher_job_spec.rb @@ -19,9 +19,11 @@ let(:event_name) { "some_event" } let(:data) do { - resource: + resource:, + event_class: } end + let(:event_class) { "Decidim::Dev::DummyResourceEvent" } context "when the resource is publicable" do let(:resource) { build(:dummy_resource) } @@ -173,5 +175,47 @@ end end end + + context "when the event is not present" do + let(:resource) { build(:dummy_resource, :published) } + let(:event_class) { nil } + + it "does not enqueue the jobs" do + expect(Decidim::EmailNotificationGeneratorJob).not_to receive(:perform_later) + expect(Decidim::NotificationGeneratorJob).not_to receive(:perform_later) + + subject + end + end + + context "when the event class does not send emails" do + let(:resource) { build(:dummy_resource, :published) } + + before do + allow(Decidim::Dev::DummyResourceEvent).to receive(:types).and_return([:notification]) + end + + it "does not enqueue the email job" do + expect(Decidim::EmailNotificationGeneratorJob).not_to receive(:perform_later) + expect(Decidim::NotificationGeneratorJob).to receive(:perform_later) + + subject + end + end + + context "when the event class does not send notifications" do + let(:resource) { build(:dummy_resource, :published) } + + before do + allow(Decidim::Dev::DummyResourceEvent).to receive(:types).and_return([:email]) + end + + it "does not enqueue the notification job" do + expect(Decidim::EmailNotificationGeneratorJob).to receive(:perform_later) + expect(Decidim::NotificationGeneratorJob).not_to receive(:perform_later) + + subject + end + end end end diff --git a/decidim-proposals/app/commands/decidim/proposals/accept_coauthorship.rb b/decidim-proposals/app/commands/decidim/proposals/accept_coauthorship.rb new file mode 100644 index 0000000000000..314c4b0af16cc --- /dev/null +++ b/decidim-proposals/app/commands/decidim/proposals/accept_coauthorship.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when a user accepts an invitation to be a coauthor of a proposal. + class AcceptCoauthorship < Decidim::Command + # Public: Initializes the command. + # + # proposal - The proposal to add a coauthor to. + # coauthor - The user to invite as coauthor. + def initialize(proposal, coauthor) + @proposal = proposal + @coauthor = coauthor + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid, together with the proposal. + # - :invalid if the coauthor is not valid. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @coauthor + return broadcast(:invalid) if @proposal.authors.include?(@coauthor) + + begin + transaction do + @proposal.add_coauthor(@coauthor) + @proposal.coauthor_invitations_for(@coauthor).destroy_all + end + + generate_notifications + rescue ActiveRecord::RecordInvalid + return broadcast(:invalid) + end + + broadcast(:ok) + end + + private + + def generate_notifications + # notify the author that the co-author has accepted the invitation + Decidim::EventsManager.publish( + event: "decidim.events.proposals.coauthor_accepted_invite", + event_class: Decidim::Proposals::CoauthorAcceptedInviteEvent, + resource: @proposal, + affected_users: @proposal.authors.reject { |author| author == @coauthor }, + extra: { coauthor_id: @coauthor.id } + ) + + # notify the co-author of the new co-authorship + Decidim::EventsManager.publish( + event: "decidim.events.proposals.accepted_coauthorship", + event_class: Decidim::Proposals::AcceptedCoauthorshipEvent, + resource: @proposal, + affected_users: [@coauthor] + ) + end + end + end +end diff --git a/decidim-proposals/app/commands/decidim/proposals/cancel_coauthorship.rb b/decidim-proposals/app/commands/decidim/proposals/cancel_coauthorship.rb new file mode 100644 index 0000000000000..7f78f72c6e9d8 --- /dev/null +++ b/decidim-proposals/app/commands/decidim/proposals/cancel_coauthorship.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when a user cancels an invitation to be a coauthor. + class CancelCoauthorship < Decidim::Command + # Public: Initializes the command. + # + # proposal - The proposal to add a coauthor to. + # coauthor - The user to invite as coauthor. + def initialize(proposal, coauthor) + @proposal = proposal + @coauthor = coauthor + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid, together with the proposal. + # - :invalid if the coauthor is not valid. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @coauthor + return broadcast(:invalid) if @proposal.authors.include?(@coauthor) + + @proposal.coauthor_invitations_for(@coauthor).destroy_all + + broadcast(:ok) + end + end + end +end diff --git a/decidim-proposals/app/commands/decidim/proposals/invite_coauthor.rb b/decidim-proposals/app/commands/decidim/proposals/invite_coauthor.rb new file mode 100644 index 0000000000000..15ad20be26247 --- /dev/null +++ b/decidim-proposals/app/commands/decidim/proposals/invite_coauthor.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when a user invites a coauthor to a proposal + class InviteCoauthor < Decidim::Command + # Public: Initializes the command. + # + # proposal - The proposal to add a coauthor to. + # coauthor - The user to invite as coauthor. + def initialize(proposal, coauthor) + @proposal = proposal + @coauthor = coauthor + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid, together with the proposal. + # - :invalid if the coauthor is not valid. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @coauthor + return broadcast(:invalid) if @proposal.authors.include?(@coauthor) + + transaction do + generate_notifications + end + + broadcast(:ok) + end + + private + + def generate_notifications + Decidim::EventsManager.publish( + event: "decidim.events.proposals.coauthor_invited", + event_class: Decidim::Proposals::CoauthorInvitedEvent, + resource: @proposal, + affected_users: [@coauthor] + ) + end + end + end +end diff --git a/decidim-proposals/app/commands/decidim/proposals/reject_coauthorship.rb b/decidim-proposals/app/commands/decidim/proposals/reject_coauthorship.rb new file mode 100644 index 0000000000000..e53a98c43c9d5 --- /dev/null +++ b/decidim-proposals/app/commands/decidim/proposals/reject_coauthorship.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when a user rejects and invitation to be a proposal co-author + class RejectCoauthorship < Decidim::Command + # Public: Initializes the command. + # + # proposal - The proposal to add a coauthor to. + # coauthor - The user to invite as coauthor. + def initialize(proposal, coauthor) + @proposal = proposal + @coauthor = coauthor + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid, together with the proposal. + # - :invalid if the coauthor is not valid. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @coauthor + return broadcast(:invalid) if @proposal.authors.include?(@coauthor) + + @proposal.coauthor_invitations_for(@coauthor).destroy_all + generate_notifications + + broadcast(:ok) + end + + private + + def generate_notifications + # notify the author that the co-author has rejected the invitation + Decidim::EventsManager.publish( + event: "decidim.events.proposals.coauthor_rejected_invite", + event_class: Decidim::Proposals::CoauthorRejectedInviteEvent, + resource: @proposal, + affected_users: @proposal.authors, + extra: { coauthor_id: @coauthor.id } + ) + + # notify the co-author of his own decision + Decidim::EventsManager.publish( + event: "decidim.events.proposals.rejected_coauthorship", + event_class: Decidim::Proposals::RejectedCoauthorshipEvent, + resource: @proposal, + affected_users: [@coauthor] + ) + end + end + end +end diff --git a/decidim-proposals/app/controllers/decidim/proposals/invite_coauthors_controller.rb b/decidim-proposals/app/controllers/decidim/proposals/invite_coauthors_controller.rb new file mode 100644 index 0000000000000..6d0c022d40871 --- /dev/null +++ b/decidim-proposals/app/controllers/decidim/proposals/invite_coauthors_controller.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class InviteCoauthorsController < Decidim::Proposals::ApplicationController + include Decidim::ControllerHelpers + + helper_method :proposal + + before_action :authenticate_user! + + # author invites coauthor + def create + enforce_permission_to :invite, :proposal_coauthor_invites, { proposal:, coauthor: } + + InviteCoauthor.call(proposal, coauthor) do + on(:ok) do + flash[:notice] = I18n.t("create.success", scope: "decidim.proposals.invite_coauthors", author_name: coauthor.name) + end + + on(:invalid) do + flash[:alert] = I18n.t("create.error", scope: "decidim.proposals.invite_coauthors") + end + end + + redirect_to Decidim::ResourceLocatorPresenter.new(proposal).path + end + + # author cancels invitation + def cancel + enforce_permission_to :cancel, :proposal_coauthor_invites, { proposal:, coauthor: } + + CancelCoauthorship.call(proposal, coauthor) do + on(:ok) do + flash[:notice] = I18n.t("cancel.success", scope: "decidim.proposals.invite_coauthors", author_name: coauthor.name) + end + + on(:invalid) do + flash[:alert] = I18n.t("cancel.error", scope: "decidim.proposals.invite_coauthors") + end + end + + redirect_to Decidim::ResourceLocatorPresenter.new(proposal).path + end + + # coauthor accepts invitation + def update + enforce_permission_to :accept, :proposal_coauthor_invites, { proposal:, coauthor: } + + AcceptCoauthorship.call(proposal, current_user) do + on(:ok) do + render json: { message: I18n.t("update.success", scope: "decidim.proposals.invite_coauthors") } + end + + on(:invalid) do + render json: { message: I18n.t("update.error", scope: "decidim.proposals.invite_coauthors") }, status: :unprocessable_entity + end + end + end + + # coauthor declines invitation + def destroy + enforce_permission_to :decline, :proposal_coauthor_invites, { proposal:, coauthor: } + + RejectCoauthorship.call(proposal, current_user) do + on(:ok) do + render json: { message: I18n.t("destroy.success", scope: "decidim.proposals.invite_coauthors") } + end + + on(:invalid) do + render json: { message: I18n.t("destroy.error", scope: "decidim.proposals.invite_coauthors") }, status: :unprocessable_entity + end + end + end + + private + + def coauthor + @coauthor ||= Decidim::User.find(params[:id]) + end + + def proposal + @proposal ||= Proposal.where(component: current_component).find(params[:proposal_id]) + end + end + end +end diff --git a/decidim-proposals/app/events/decidim/proposals/accepted_coauthorship_event.rb b/decidim-proposals/app/events/decidim/proposals/accepted_coauthorship_event.rb new file mode 100644 index 0000000000000..8aca087660a50 --- /dev/null +++ b/decidim-proposals/app/events/decidim/proposals/accepted_coauthorship_event.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class AcceptedCoauthorshipEvent < CoauthorAcceptedInviteEvent + end + end +end diff --git a/decidim-proposals/app/events/decidim/proposals/coauthor_accepted_invite_event.rb b/decidim-proposals/app/events/decidim/proposals/coauthor_accepted_invite_event.rb new file mode 100644 index 0000000000000..f0d1dce18e7aa --- /dev/null +++ b/decidim-proposals/app/events/decidim/proposals/coauthor_accepted_invite_event.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class CoauthorAcceptedInviteEvent < Decidim::Events::BaseEvent + include Decidim::Events::NotificationEvent + include Decidim::Core::Engine.routes.url_helpers + + def notification_title + I18n.t("notification_title", **i18n_options).html_safe + end + + delegate :name, to: :author, prefix: true + delegate :name, to: :coauthor, prefix: true, allow_nil: true + + def author_path + profile_path(author.nickname) + end + + def coauthor_path + profile_path(coauthor.nickname) if coauthor + end + + def author + resource.creator_author + end + + def coauthor + @coauthor ||= Decidim::User.find_by(id: extra["coauthor_id"]) + end + + def i18n_scope + event_name + end + + def i18n_options + { + scope: i18n_scope, + coauthor_name:, + coauthor_path:, + author_name:, + author_path:, + resource_path:, + resource_title: + } + end + end + end +end diff --git a/decidim-proposals/app/events/decidim/proposals/coauthor_invited_event.rb b/decidim-proposals/app/events/decidim/proposals/coauthor_invited_event.rb new file mode 100644 index 0000000000000..010046e0c0177 --- /dev/null +++ b/decidim-proposals/app/events/decidim/proposals/coauthor_invited_event.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class CoauthorInvitedEvent < Decidim::Events::SimpleEvent + include Decidim::Events::CoauthorEvent + include Decidim::Core::Engine.routes.url_helpers + + def action_cell + "decidim/notification_actions/buttons" unless user_is_coauthor? + end + + def action_data + [ + { + i18n_label: "decidim.events.proposals.coauthor_invited.actions.accept", + url: invite_path, + icon: "check-line", + method: "patch" + }, + { + i18n_label: "decidim.events.proposals.coauthor_invited.actions.decline", + url: invite_path, + icon: "close-circle-line", + method: "delete" + } + ] + end + + def resource_url + notifications_url(host: component.organization.host) + end + + private + + def invite_path + @invite_path ||= EngineRouter.main_proxy(component).proposal_invite_coauthor_path(proposal_id: resource, id: user.id) + end + + def user_is_coauthor? + resource.authors.include?(user) + end + end + end +end diff --git a/decidim-proposals/app/events/decidim/proposals/coauthor_rejected_invite_event.rb b/decidim-proposals/app/events/decidim/proposals/coauthor_rejected_invite_event.rb new file mode 100644 index 0000000000000..887bfe2a8f29b --- /dev/null +++ b/decidim-proposals/app/events/decidim/proposals/coauthor_rejected_invite_event.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class CoauthorRejectedInviteEvent < CoauthorAcceptedInviteEvent + end + end +end diff --git a/decidim-proposals/app/events/decidim/proposals/rejected_coauthorship_event.rb b/decidim-proposals/app/events/decidim/proposals/rejected_coauthorship_event.rb new file mode 100644 index 0000000000000..864dbe3f24b13 --- /dev/null +++ b/decidim-proposals/app/events/decidim/proposals/rejected_coauthorship_event.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class RejectedCoauthorshipEvent < AcceptedCoauthorshipEvent + end + end +end diff --git a/decidim-proposals/app/models/decidim/proposals/proposal.rb b/decidim-proposals/app/models/decidim/proposals/proposal.rb index 80bdd76715ea1..eeb3b4846d0de 100644 --- a/decidim-proposals/app/models/decidim/proposals/proposal.rb +++ b/decidim-proposals/app/models/decidim/proposals/proposal.rb @@ -500,6 +500,47 @@ def process_amendment_state_change! end end + def user_has_actions?(user) + return false if authors.include?(user) + return false if user&.blocked? + return false if user&.deleted? + return false unless user&.confirmed? + + true + end + + def actions_for_comment(comment, current_user) + return if comment.commentable != self + return unless authors.include?(current_user) + return unless user_has_actions?(comment.author) + + if coauthor_invitations_for(comment.author).any? + [ + { + label: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation"), + url: EngineRouter.main_proxy(component).cancel_proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id), + icon: "user-forbid-line", + method: :delete, + data: { confirm: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation_confirm") } + } + ] + else + [ + { + label: I18n.t("decidim.proposals.actions.mark_as_coauthor"), + url: EngineRouter.main_proxy(component).proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id), + icon: "user-add-line", + method: :post, + data: { confirm: I18n.t("decidim.proposals.actions.mark_as_coauthor_confirm") } + } + ] + end + end + + def coauthor_invitations_for(user) + Decidim::Notification.where(event_class: "Decidim::Proposals::CoauthorInvitedEvent", resource: self, user:) + end + private def copied_from_other_component? diff --git a/decidim-proposals/app/permissions/decidim/proposals/permissions.rb b/decidim-proposals/app/permissions/decidim/proposals/permissions.rb index 7d32a04a96c28..703de80f927dd 100644 --- a/decidim-proposals/app/permissions/decidim/proposals/permissions.rb +++ b/decidim-proposals/app/permissions/decidim/proposals/permissions.rb @@ -15,6 +15,8 @@ def permissions apply_proposal_permissions(permission_action) when :collaborative_draft apply_collaborative_draft_permissions(permission_action) + when :proposal_coauthor_invites + apply_proposal_coauthor_invites(permission_action) else permission_action end @@ -158,6 +160,46 @@ def can_react_to_request_access_collaborative_draft? toggle_allow(collaborative_draft.created_by?(user)) end + + def apply_proposal_coauthor_invites(permission_action) + return toggle_allow(false) unless coauthor + return toggle_allow(false) unless proposal + + case permission_action.action + when :invite + toggle_allow(valid_coauthor? && !notification_already_sent?) + when :cancel + toggle_allow(valid_coauthor? && notification_already_sent?) + when :accept, :decline + toggle_allow(can_be_coauthor?) + end + end + + def coauthor + context.fetch(:coauthor, nil) + end + + def notification_already_sent? + @notification_already_sent ||= proposal.coauthor_invitations_for(coauthor).any? + end + + def coauthor_in_comments? + @coauthor_in_comments ||= proposal.comments.where(author: coauthor).any? + end + + def valid_coauthor? + return false unless proposal.authors.include?(user) + return false unless proposal.user_has_actions?(coauthor) + + coauthor_in_comments? + end + + def can_be_coauthor? + return false unless user == coauthor + return false unless proposal.user_has_actions?(coauthor) + + notification_already_sent? + end end end end diff --git a/decidim-proposals/config/locales/en.yml b/decidim-proposals/config/locales/en.yml index c4ff92af32a65..f42516e28ad79 100644 --- a/decidim-proposals/config/locales/en.yml +++ b/decidim-proposals/config/locales/en.yml @@ -242,6 +242,8 @@ en: votes_hidden: Votes hidden (if votes are enabled, checking this will hide the number of votes) events: proposals: + accepted_coauthorship: + notification_title: You have been added as a co-author of the proposal %{resource_title}. admin: proposal_assigned_to_valuator: email_intro: You have been assigned as a valuator to the proposal "%{resource_title}". This means you have been trusted to give them feedback and a proper response in the next coming days. Check it out at the admin panel. @@ -263,6 +265,18 @@ en: email_outro: You have received this notification because you are the author of the note. email_subject: "%{author_name} has replied your private note in %{resource_title}." notification_title: %{author_name} %{author_nickname} has replied your private note in %{resource_title}. Check it out at the admin panel. + coauthor_accepted_invite: + notification_title: %{coauthor_name} has accepted your invitation to become a co-author of the proposal %{resource_title}. + coauthor_invited: + actions: + accept: Accept + decline: Decline + email_intro: 'You have been invited to be a co-author of the proposal "%{resource_title}". You can accept or decline the invitation in this page:' + email_outro: You have received this notification because the author of the proposal wants to recognize your contributions by becoming a co-author. + email_subject: You have been invited to be a co-author of the proposal "%{resource_title}" + notification_title: %{author_name} would like to invite you as a co-author of the proposal %{resource_title}. + coauthor_rejected_invite: + notification_title: %{coauthor_name} has declined your invitation to become a co-author of the proposal %{resource_title}. collaborative_draft_access_accepted: email_intro: '%{requester_name} has been accepted to access as a contributor of the %{resource_title} collaborative draft.' email_outro: You have received this notification because you are a collaborator of %{resource_title}. @@ -340,6 +354,8 @@ en: email_outro: You have received this notification because you are the author of the proposal. email_subject: The %{resource_title} proposal scope has been updated notification_title: The %{resource_title} proposal scope has been updated by an admin. + rejected_coauthorship: + notification_title: You have declined the invitation from %{author_name} to become a co-author of the proposal %{resource_title}. voting_enabled: email_intro: 'You can vote proposals in %{participatory_space_title}! Start participating in this page:' email_outro: You have received this notification because you are following %{participatory_space_title}. You can stop receiving notifications following the previous link. @@ -404,11 +420,15 @@ en: proposals: actions: answer_proposal: Answer proposal + cancel_coauthor_invitation: Cancel co-author invitation + cancel_coauthor_invitation_confirm: Are you sure you want to cancel the co-author invitation? delete_proposal_state_confirm: Are you sure you want to delete this state? destroy: Delete state edit_proposal: Edit proposal edit_proposal_state: Edit state import: Import proposals from another component + mark_as_coauthor: Mark as co-author + mark_as_coauthor_confirm: Are you sure you want to mark this user as a co-author? The receiver will receive a notification to accept or decline the invitation. new: New proposal new_proposal_state: New status participatory_texts: Participatory texts @@ -762,6 +782,19 @@ en: destroy_draft: error: There was a problem deleting the collaborative draft. success: Proposal draft was successfully deleted. + invite_coauthors: + cancel: + error: There was a problem canceling the co-author invitation. + success: Co-author invitation successfully canceled. + create: + error: There was a problem inviting the co-author. + success: "%{author_name} successfully invited as a co-author." + destroy: + error: There was a problem declining the invitation. + success: The invitation has been declined. + update: + error: There was a problem accepting the invitation. + success: The invitation has been accepted. last_activity: new_proposal: 'New proposal:' proposal_updated: 'Proposal updated:' diff --git a/decidim-proposals/lib/decidim/proposals/engine.rb b/decidim-proposals/lib/decidim/proposals/engine.rb index fc949c39cf342..f9eb49ad2b096 100644 --- a/decidim-proposals/lib/decidim/proposals/engine.rb +++ b/decidim-proposals/lib/decidim/proposals/engine.rb @@ -22,6 +22,11 @@ class Engine < ::Rails::Engine end resource :proposal_vote, only: [:create, :destroy] resources :versions, only: [:show] + resources :invite_coauthors, only: [:index, :create, :update, :destroy] do + collection do + delete :cancel + end + end end resources :collaborative_drafts, except: [:destroy] do member do diff --git a/decidim-proposals/spec/commands/decidim/proposals/accept_coauthorship_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/accept_coauthorship_spec.rb new file mode 100644 index 0000000000000..074b966b19a03 --- /dev/null +++ b/decidim-proposals/spec/commands/decidim/proposals/accept_coauthorship_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe AcceptCoauthorship do + let!(:proposal) { create(:proposal) } + + let(:coauthor) { create(:user, organization: proposal.organization) } + let!(:notification) do + create(:notification, :proposal_coauthor_invite, resource: proposal, user: coauthor) + end + let!(:another_notification) do + create(:notification, :proposal_coauthor_invite, resource: proposal) + end + + let(:command) { described_class.new(proposal, coauthor) } + + describe "when the coauthor is valid" do + it "adds the coauthor to the proposal" do + expect do + command.call + end.to change { proposal.coauthorships.count }.by(1) + end + + it "broadcasts :ok" do + expect { command.call }.to broadcast(:ok) + end + + it "removes the notification" do + expect { command.call }.to change(Decidim::Notification, :count).by(-1) + expect(Decidim::Notification.all.to_a).to eq([another_notification]) + end + + it_behaves_like "fires an ActiveSupport::Notification event", "decidim.events.proposals.accepted_coauthorship" + it_behaves_like "fires an ActiveSupport::Notification event", "decidim.events.proposals.coauthor_accepted_invite" + + it "notifies the coauthor and existing authors about the new coauthorship" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.proposals.coauthor_accepted_invite", + event_class: Decidim::Proposals::CoauthorAcceptedInviteEvent, + resource: proposal, + affected_users: proposal.authors.reject { |author| author == coauthor }, + extra: { coauthor_id: coauthor.id } + ) + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.proposals.accepted_coauthorship", + event_class: Decidim::Proposals::AcceptedCoauthorshipEvent, + resource: proposal, + affected_users: [coauthor] + ) + + command.call + end + end + + describe "when the coauthor is not in the same organization" do + let(:coauthor) { create(:user) } + + it "does not add the coauthor to the proposal" do + expect do + command.call + end.not_to(change { proposal.coauthorships.count }) + end + + it "broadcasts :invalid" do + expect { command.call }.to broadcast(:invalid) + end + end + + describe "when the coauthor is already an author" do + let!(:coauthor) { create(:user, organization: proposal.organization) } + + before do + proposal.add_coauthor(coauthor) + end + + it "does not add the coauthor to the proposal" do + expect do + command.call + end.not_to(change { proposal.coauthorships.count }) + end + + it "broadcasts :invalid" do + expect { command.call }.to broadcast(:invalid) + end + end + + describe "when the coauthor is nil" do + let(:coauthor) { nil } + let(:notification) { create(:notification, :proposal_coauthor_invite) } + + it "does not add the coauthor to the proposal" do + expect do + command.call + end.not_to(change { proposal.coauthorships.count }) + end + + it "broadcasts :invalid" do + expect { command.call }.to broadcast(:invalid) + end + end + end + end +end diff --git a/decidim-proposals/spec/commands/decidim/proposals/cancel_coauthorship_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/cancel_coauthorship_spec.rb new file mode 100644 index 0000000000000..1dedc29d84d4d --- /dev/null +++ b/decidim-proposals/spec/commands/decidim/proposals/cancel_coauthorship_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe CancelCoauthorship do + let!(:proposal) { create(:proposal) } + + let(:coauthor) { create(:user, organization: proposal.organization) } + let!(:notification) do + create(:notification, :proposal_coauthor_invite, user: coauthor, resource: proposal) + end + let!(:another_notification) do + create(:notification, :proposal_coauthor_invite, resource: proposal) + end + + let(:command) { described_class.new(proposal, coauthor) } + + describe "when the coauthor is valid" do + it "broadcasts :ok" do + expect { command.call }.to broadcast(:ok) + end + + it "removes the coauthor from the proposal" do + expect { command.call }.to change(Decidim::Notification, :count).by(-1) + expect(Decidim::Notification.all.to_a).to eq([another_notification]) + end + end + + describe "when the coauthor is not valid" do + let(:coauthor) { nil } + let(:notification) { create(:notification, :proposal_coauthor_invite, resource: proposal) } + + it "broadcasts :invalid" do + expect { command.call }.to broadcast(:invalid) + end + end + + describe "when the coauthor is already an author" do + let!(:coauthor) { create(:user, organization: proposal.organization) } + + before do + proposal.add_coauthor(coauthor) + end + + it "does not remove the coauthor from the proposal" do + expect do + command.call + end.not_to(change(Decidim::Notification, :count)) + end + end + end + end +end diff --git a/decidim-proposals/spec/commands/decidim/proposals/invite_coauthor_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/invite_coauthor_spec.rb new file mode 100644 index 0000000000000..92ff6a8ab3075 --- /dev/null +++ b/decidim-proposals/spec/commands/decidim/proposals/invite_coauthor_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe InviteCoauthor do + let!(:proposal) { create(:proposal) } + + let(:coauthor) { create(:user, organization: proposal.organization) } + let(:command) { described_class.new(proposal, coauthor) } + + describe "when the coauthor is valid" do + it "broadcasts :ok" do + expect { command.call }.to broadcast(:ok) + end + + it "generates a notification" do + perform_enqueued_jobs do + expect { command.call }.to change(Decidim::Notification, :count).by(1) + end + end + + it_behaves_like "fires an ActiveSupport::Notification event", "decidim.events.proposals.coauthor_invited" + + it "notifies the coauthor about the invitation" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.proposals.coauthor_invited", + event_class: Decidim::Proposals::CoauthorInvitedEvent, + resource: proposal, + affected_users: [coauthor] + ) + + command.call + end + end + + describe "when the coauthor is already an author" do + before do + proposal.add_coauthor(coauthor) + end + + it "does not generate a notification" do + expect { command.call }.not_to change(Decidim::Notification, :count) + end + end + + describe "when the coauthor is nil" do + let(:coauthor) { nil } + + it "broadcasts :invalid" do + expect { command.call }.to broadcast(:invalid) + end + end + end + end +end diff --git a/decidim-proposals/spec/commands/decidim/proposals/reject_coauthorship_spec.rb b/decidim-proposals/spec/commands/decidim/proposals/reject_coauthorship_spec.rb new file mode 100644 index 0000000000000..eb9c578f47880 --- /dev/null +++ b/decidim-proposals/spec/commands/decidim/proposals/reject_coauthorship_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe RejectCoauthorship do + let!(:proposal) { create(:proposal) } + + let(:coauthor) { create(:user, organization: proposal.organization) } + let(:command) { described_class.new(proposal, coauthor) } + + let!(:notification) do + create(:notification, :proposal_coauthor_invite, user: coauthor, resource: proposal) + end + + let!(:another_notification) do + create(:notification, :proposal_coauthor_invite, resource: proposal) + end + + describe "when the coauthor is valid" do + it "broadcasts :ok" do + expect { command.call }.to broadcast(:ok) + end + + it "removes the notification" do + expect { command.call }.to change(Decidim::Notification, :count).by(-1) + expect(Decidim::Notification.all.to_a).to eq([another_notification]) + end + + it_behaves_like "fires an ActiveSupport::Notification event", "decidim.events.proposals.rejected_coauthorship" + it_behaves_like "fires an ActiveSupport::Notification event", "decidim.events.proposals.coauthor_rejected_invite" + + it "notifies the coauthor and existing authors about the new coauthorship" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.proposals.coauthor_rejected_invite", + event_class: Decidim::Proposals::CoauthorRejectedInviteEvent, + resource: proposal, + affected_users: proposal.authors, + extra: { coauthor_id: coauthor.id } + ) + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.proposals.rejected_coauthorship", + event_class: Decidim::Proposals::RejectedCoauthorshipEvent, + resource: proposal, + affected_users: [coauthor] + ) + + command.call + end + end + + describe "when the coauthor is nil" do + let(:coauthor) { nil } + let(:notification) { create(:notification, :proposal_coauthor_invite, resource: proposal) } + + it "broadcasts :invalid" do + expect { command.call }.to broadcast(:invalid) + end + end + + describe "when the coauthor is already an author" do + let!(:coauthor) { create(:user, organization: proposal.organization) } + + before do + proposal.add_coauthor(coauthor) + end + + it "does not remove the coauthor from the proposal" do + expect do + command.call + end.not_to(change(Decidim::Notification, :count)) + end + end + end + end +end diff --git a/decidim-proposals/spec/events/decidim/proposals/accepted_coauthorship_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/accepted_coauthorship_event_spec.rb new file mode 100644 index 0000000000000..db31a24df2268 --- /dev/null +++ b/decidim-proposals/spec/events/decidim/proposals/accepted_coauthorship_event_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe AcceptedCoauthorshipEvent do + let(:resource) { create(:proposal) } + let(:participatory_process) { create(:participatory_process, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:resource_title) { decidim_sanitize_translated(resource.title) } + let(:event_name) { "decidim.events.proposals.accepted_coauthorship" } + + let(:notification_title) do + "You have been added as a co-author of the proposal #{resource_title}." + end + + include_context "when a simple event" + + it_behaves_like "a simple event notification" + it_behaves_like "a notification event only" + end + end +end diff --git a/decidim-proposals/spec/events/decidim/proposals/coauthor_accepted_invite_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/coauthor_accepted_invite_event_spec.rb new file mode 100644 index 0000000000000..3546358fd201a --- /dev/null +++ b/decidim-proposals/spec/events/decidim/proposals/coauthor_accepted_invite_event_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe CoauthorAcceptedInviteEvent do + include_context "when a simple event" + + let(:resource) { create(:proposal) } + let(:extra) do + { + "coauthor_id" => coauthor.id + } + end + let(:notification_title) do + "#{coauthor.name} has accepted your invitation to become a co-author of the proposal #{resource_title}." + end + let(:participatory_process) { create(:participatory_process, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:resource_title) { decidim_sanitize_translated(resource.title) } + let(:event_name) { "decidim.events.proposals.coauthor_accepted_invite" } + let(:coauthor) { create(:user, organization: resource.organization) } + + it_behaves_like "a simple event notification" + it_behaves_like "a notification event only" + end + end +end diff --git a/decidim-proposals/spec/events/decidim/proposals/coauthor_invited_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/coauthor_invited_event_spec.rb new file mode 100644 index 0000000000000..37d3d6e4dcf77 --- /dev/null +++ b/decidim-proposals/spec/events/decidim/proposals/coauthor_invited_event_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe CoauthorInvitedEvent do + let(:resource) { create(:proposal) } + let(:participatory_process) { create(:participatory_process, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:resource_title) { decidim_sanitize_translated(resource.title) } + let(:event_name) { "decidim.events.proposals.coauthor_invited" } + + include_context "when a simple event" + + it_behaves_like "a simple event" + + describe "email_subject" do + context "when resource title contains apostrophes" do + let(:resource) { create(:proposal) } + + it "is generated correctly" do + expect(subject.email_subject).to eq("You have been invited to be a co-author of the proposal \"#{resource_title}\"") + end + end + + it "is generated correctly" do + expect(subject.email_subject).to eq("You have been invited to be a co-author of the proposal \"#{resource_title}\"") + end + end + + describe "email_intro" do + it "is generated correctly" do + expect(subject.email_intro) + .to eq("You have been invited to be a co-author of the proposal \"#{resource_title}\". You can accept or decline the invitation in this page:") + end + end + + describe "email_outro" do + it "is generated correctly" do + expect(subject.email_outro) + .to eq("You have received this notification because the author of the proposal wants to recognize your contributions by becoming a co-author.") + end + end + + describe "notification_title" do + it "is generated correctly" do + expect(subject.notification_title) + .to eq("#{author.name} would like to invite you as a co-author of the proposal #{resource_title}.") + end + end + end + end +end diff --git a/decidim-proposals/spec/events/decidim/proposals/coauthor_rejected_invite_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/coauthor_rejected_invite_event_spec.rb new file mode 100644 index 0000000000000..f8cd640d94557 --- /dev/null +++ b/decidim-proposals/spec/events/decidim/proposals/coauthor_rejected_invite_event_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe CoauthorRejectedInviteEvent do + include_context "when a simple event" + + let(:resource) { create(:proposal) } + let(:extra) do + { + "coauthor_id" => coauthor.id + } + end + let(:notification_title) do + "#{coauthor.name} has declined your invitation to become a co-author of the proposal #{resource_title}." + end + let(:participatory_process) { create(:participatory_process, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:resource_title) { decidim_sanitize_translated(resource.title) } + let(:event_name) { "decidim.events.proposals.coauthor_rejected_invite" } + let(:coauthor) { create(:user, organization: resource.organization) } + + it_behaves_like "a simple event notification" + it_behaves_like "a notification event only" + end + end +end diff --git a/decidim-proposals/spec/events/decidim/proposals/rejected_coauthorship_event_spec.rb b/decidim-proposals/spec/events/decidim/proposals/rejected_coauthorship_event_spec.rb new file mode 100644 index 0000000000000..3f19e41908618 --- /dev/null +++ b/decidim-proposals/spec/events/decidim/proposals/rejected_coauthorship_event_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Proposals + describe RejectedCoauthorshipEvent do + let(:resource) { create(:proposal) } + let(:participatory_process) { create(:participatory_process, organization:) } + let(:proposal_component) { create(:proposal_component, participatory_space: participatory_process) } + let(:resource_title) { decidim_sanitize_translated(resource.title) } + let(:event_name) { "decidim.events.proposals.rejected_coauthorship" } + + let(:notification_title) do + "You have declined the invitation from #{author.name} to become a co-author of the proposal #{resource_title}." + end + + include_context "when a simple event" + it_behaves_like "a simple event notification" + it_behaves_like "a notification event only" + end + end +end diff --git a/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb b/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb index 2fef38f16a1f7..269b6b06f4c91 100644 --- a/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb +++ b/decidim-proposals/spec/models/decidim/proposals/proposal_spec.rb @@ -269,6 +269,91 @@ module Proposals expect(results).to eq([assigned_proposal]) end end + + describe "#coauthor_invitations_for" do + let!(:notification) do + create(:notification, :proposal_coauthor_invite, resource: proposal, user: coauthor) + end + let(:coauthor) { create(:user, organization:) } + + it "returns the coauthor invitations for the given user" do + expect(proposal.coauthor_invitations_for(coauthor)).to eq([notification]) + end + + context "when the user has not been invited" do + it "returns empty" do + expect(proposal.coauthor_invitations_for(create(:user))).to be_empty + end + end + + context "when the user the notification is for another event" do + let!(:notification) do + create(:notification, event_class: "Decidim::Proposals::AnotherEvent", resource: proposal, user: coauthor) + end + + it "returns empty" do + expect(proposal.coauthor_invitations_for(coauthor)).to be_empty + end + end + end + + describe "#actions_for_comment" do + let(:proposal) { create(:proposal, component:) } + let(:author) { create(:user, :confirmed, organization: component.organization) } + let(:comment) { create(:comment, author:, commentable: proposal) } + + it "returns actions to invite the comment author as a co-author" do + expect(proposal.actions_for_comment(comment, proposal.creator_author)).to eq([ + { + label: "Mark as co-author", + url: EngineRouter.main_proxy(component).proposal_invite_coauthors_path(proposal_id: proposal.id, id: comment.author.id), + icon: "user-add-line", + method: :post, + data: { + confirm: "Are you sure you want to mark this user as a co-author? The receiver will receive a notification to accept or decline the invitation." + } + } + ]) + end + + context "when the requester user is not the author of the proposal" do + it "returns nil" do + expect(proposal.actions_for_comment(comment, create(:user))).to be_nil + end + end + + context "when no requester" do + it "returns nil" do + expect(proposal.actions_for_comment(comment, nil)).to be_nil + end + end + + context "when the author has already been invited" do + let!(:notification) do + create(:notification, :proposal_coauthor_invite, resource: proposal, user: comment.author) + end + + it "returns actions to remove the co-author invitation" do + expect(proposal.actions_for_comment(comment, proposal.creator_author)).to eq([ + { + label: "Cancel co-author invitation", + url: EngineRouter.main_proxy(component).cancel_proposal_invite_coauthors_path(proposal_id: proposal.id, id: comment.author.id), + icon: "user-forbid-line", + method: :delete, + data: { + confirm: "Are you sure you want to cancel the co-author invitation?" + } + } + ]) + end + + context "when the requester user is not the author of the proposal" do + it "returns nil" do + expect(proposal.actions_for_comment(comment, create(:user))).to be_nil + end + end + end + end end end end diff --git a/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb b/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb index eafe6abddc948..6149b42923f24 100644 --- a/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb +++ b/decidim-proposals/spec/permissions/decidim/proposals/permissions_spec.rb @@ -11,9 +11,11 @@ current_component: proposal_component, current_settings:, proposal:, - component_settings: + component_settings:, + coauthor: } end + let(:coauthor) { nil } let(:proposal_component) { create(:proposal_component) } let(:proposal) { create(:proposal, component: proposal_component) } let(:component_settings) do @@ -230,4 +232,162 @@ it { is_expected.to be true } end end + + context "when inviting coauthor" do + let(:action) do + { scope: :public, action: :invite, subject: :proposal_coauthor_invites } + end + let(:coauthor) { create(:user, :confirmed, organization: user.organization) } + let!(:comment) { create(:comment, commentable: proposal, author: coauthor) } + + shared_examples "coauthor is invitable" do + it { is_expected.to be true } + + context "when already an author" do + before do + proposal.add_coauthor(coauthor) + end + + it { is_expected.to be false } + end + + context "when the current user is not the author" do + let(:user) { create(:user, :confirmed, organization: proposal.organization) } + + it { is_expected.to be false } + end + + context "when no comments" do + let!(:comment) { nil } + + it { is_expected.to be false } + end + + context "when coauthor not in comments" do + let!(:comment) { create(:comment, commentable: proposal) } + + it { is_expected.to be false } + end + + context "when coauthor is not confirmed" do + let(:coauthor) { create(:user, organization: user.organization) } + + it { is_expected.to be false } + end + + context "when coauthor is blocked" do + let(:coauthor) { create(:user, :blocked, organization: user.organization) } + + it { is_expected.to be false } + end + + context "when coauthor is deleted" do + let(:coauthor) { create(:user, :deleted, organization: user.organization) } + + it { is_expected.to be false } + end + end + + it_behaves_like "coauthor is invitable" + + context "when a notification for the coauthor already exists" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, user: coauthor) } + + it { is_expected.to be true } + end + + context "when notification exists for the same proposal" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, user: coauthor, resource: proposal) } + + it { is_expected.to be false } + end + + context "when notification is for another user" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, resource: proposal) } + + it { is_expected.to be true } + end + + context "when canceling invitation" do + let(:action) do + { scope: :public, action: :cancel, subject: :proposal_coauthor_invites } + end + let!(:notification) { create(:notification, :proposal_coauthor_invite, resource: proposal, user: coauthor) } + + it_behaves_like "coauthor is invitable" + end + end + + context "when coauthor is invited" do + let(:action) do + { scope: :public, action: :accept, subject: :proposal_coauthor_invites } + end + let(:user) { create(:user, :confirmed, organization: proposal.organization) } + let(:coauthor) { user } + let!(:notification) { create(:notification, :proposal_coauthor_invite, resource: proposal, user: coauthor) } + + shared_examples "can accept coauthor" do + it { is_expected.to be true } + + context "when already an author" do + before do + proposal.add_coauthor(coauthor) + end + + it { is_expected.to be false } + end + + context "when the user is not the coauthor" do + let(:coauthor) { create(:user, :confirmed, organization: proposal.organization) } + + it { is_expected.to be false } + end + + context "when no invitation" do + let!(:notification) { nil } + + it { is_expected.to be false } + end + + context "when notification is not for the same proposal" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, user: coauthor) } + + it { is_expected.to be false } + end + + context "when notification is for another coauthor" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, resource: proposal) } + + it { is_expected.to be false } + end + + context "when coauthor is blocked" do + let(:coauthor) { create(:user, :blocked, organization: proposal.organization) } + + it { is_expected.to be false } + end + + context "when coauthor is deleted" do + let(:coauthor) { create(:user, :deleted, organization: proposal.organization) } + + it { is_expected.to be false } + end + + context "when coauthor is not confirmed" do + let(:coauthor) { create(:user, organization: proposal.organization) } + + it { is_expected.to be false } + end + end + + it_behaves_like "can accept coauthor" + + context "when declining invitation" do + let(:action) do + { scope: :public, action: :decline, subject: :proposal_coauthor_invites } + end + + it_behaves_like "can accept coauthor" + end + end end diff --git a/decidim-proposals/spec/system/proposal_comment_actions_spec.rb b/decidim-proposals/spec/system/proposal_comment_actions_spec.rb new file mode 100644 index 0000000000000..6bce6b53c0d39 --- /dev/null +++ b/decidim-proposals/spec/system/proposal_comment_actions_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "Interact with commenters" do + include_context "with a component" + let(:manifest_name) { "proposals" } + let(:proposal) { create(:proposal, users: [user], component:) } + let(:commenter) { create(:user, :confirmed, organization:) } + let!(:comment) { create(:comment, commentable: proposal, author: commenter) } + let!(:author_comment) { create(:comment, commentable: proposal, author: user) } + + def visit_proposal + visit resource_locator(proposal).path + end + + context "when author" do + before do + login_as user, scope: :user + visit_proposal + end + + context "when the user is the commenter" do + it "has no actions" do + within "#comment_#{author_comment.id}" do + click_on "…" + expect(page).to have_no_content("Mark as co-author") + end + end + end + + context "when the user is not the commenter" do + let(:notification) { Decidim::Notification.last } + + it "author can invite" do + expect(page).to have_content("2 comments") + within "#comment_#{comment.id}" do + click_on "…" + perform_enqueued_jobs do + accept_confirm { click_on("Mark as co-author") } + end + end + + expect(page).to have_content("successfully invited as a co-author") + + within "#comment_#{comment.id}" do + click_on "…" + expect(page).to have_link("Cancel co-author invitation") + expect(page).to have_no_content("Mark as co-author") + end + + expect(notification.event_class).to eq("Decidim::Proposals::CoauthorInvitedEvent") + expect(notification.resource).to eq(proposal) + expect(notification.user).to eq(commenter) + # sends email to commenter + expect(last_email.to).to include(commenter.email) + end + end + + context "when a notification already exists" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, user: commenter, resource: proposal) } + + it "author can cancel invitation" do + within "#comment_#{comment.id}" do + click_on "…" + perform_enqueued_jobs do + accept_confirm { click_on("Cancel co-author invitation") } + end + end + + expect(page).to have_content("Co-author invitation successfully canceled") + + within "#comment_#{comment.id}" do + click_on "…" + expect(page).to have_link("Mark as co-author") + expect(page).to have_no_content("Cancel co-author invitation") + end + + expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + context "when commenter" do + let!(:notification) { create(:notification, :proposal_coauthor_invite, user: commenter, resource: proposal) } + let(:author_notification) { Decidim::Notification.find_by(user:) } + + before do + login_as commenter, scope: :user + visit decidim.notifications_path + end + + it "coauthor can accept invitation" do + expect(page).to have_content("#{user.name} would like to invite you as a co-author of the proposal") + + perform_enqueued_jobs do + click_on "Accept" + + expect(page).to have_content("The invitation has been accepted") + expect(proposal.reload.authors).to include(commenter) + expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) + + visit decidim.notifications_path + expect(page).to have_no_content("would like to invite you as a co-author of the proposal") + expect(page).to have_content("You have been added as a co-author of the proposal") + expect(last_email).to be_nil + expect(author_notification.event_class).to eq("Decidim::Proposals::CoauthorAcceptedInviteEvent") + expect(author_notification.extra["coauthor_id"]).to eq(commenter.id) + end + end + + it "coauthor can decline invitation" do + perform_enqueued_jobs do + click_on "Decline" + + expect(page).to have_content("The invitation has been declined") + expect(proposal.reload.authors).not_to include(commenter) + expect { notification.reload }.to raise_error(ActiveRecord::RecordNotFound) + + visit decidim.notifications_path + expect(page).to have_no_content("would like to invite you as a co-author of the proposal") + expect(page).to have_content("You have declined the invitation from") + expect(last_email).to be_nil + expect(author_notification.event_class).to eq("Decidim::Proposals::CoauthorRejectedInviteEvent") + expect(author_notification.extra["coauthor_id"]).to eq(commenter.id) + end + end + end +end diff --git a/decidim-templates/lib/decidim/templates/engine.rb b/decidim-templates/lib/decidim/templates/engine.rb index 52b1f988cc51a..0ea1c044f6632 100644 --- a/decidim-templates/lib/decidim/templates/engine.rb +++ b/decidim-templates/lib/decidim/templates/engine.rb @@ -15,10 +15,6 @@ class Engine < ::Rails::Engine # root to: "templates#index" end - initializer "decidim_templates.register_icons" do - Decidim.icons.register(name: "user-forbid-line", icon: "user-forbid-line", category: "system", description: "", engine: :templates) - end - initializer "decidim_templates.webpacker.assets_path" do Decidim.register_assets_path File.expand_path("app/packs", root) end diff --git a/docs/modules/develop/pages/classes/models.adoc b/docs/modules/develop/pages/classes/models.adoc index dd248acf9e1da..e631e356f2814 100644 --- a/docs/modules/develop/pages/classes/models.adoc +++ b/docs/modules/develop/pages/classes/models.adoc @@ -80,9 +80,9 @@ Most commonly used concerns are: === Module specific concerns -- `Decidim::Comments::Commentable` -- `Decidim::Comments::CommentableWithComponent` -- `Decidim::Comments::HasAvailabilityAttributes` +- `xref:develop:commentable.adoc[Decidim::Comments::Commentable]` +- `xref:develop:commentable.adoc[Decidim::Comments::CommentableWithComponent]` +- `xref:develop:commentable.adoc[Decidim::Comments::HasAvailabilityAttributes]` - `Decidim::Forms::HasQuestionnaire` - `Decidim::Initiatives::HasArea` - `Decidim::Initiatives::InitiativeSlug` diff --git a/docs/modules/develop/pages/commentable.adoc b/docs/modules/develop/pages/commentable.adoc new file mode 100644 index 0000000000000..f8dd55453df32 --- /dev/null +++ b/docs/modules/develop/pages/commentable.adoc @@ -0,0 +1,127 @@ += Commentable + +== Things can be commentable + +`Commentable` is a feature to allow participants to comment on resources. This feature is used in many places in Decidim, like proposals, meetings, debates, etc. + +When commenting a resource, the comments counter is increased and a notification to all the followers of the participant, the participatory space and/or the resource is sent. + +Participants can comment with their own identity or with the identify of the `user_groups` they belong to. + +== Data model + +The `decidim_comments` table stores all the comments given to resources that are commentable. This is, one commentable has many comments, and each comment belongs to one commentable. Also, a comment can be a commentable itself, so comments can be nested. +For performance, a commentable has a counter cache of comments. + +[source,ascii] +---- ++----------------------+ +| Decidim::Commentable | +| ((Proposal,...)) | +-------------+ ++----------------------+ 0..N +-------------------+ +--+Decidim::User| +|-has_many comments |-------+Decidim::Comment | | +-------------+ +|#counter cache column | +-------------------+ | +|-comments_counter | |-author: may be a |<--+ ++----------------------+ | user or a | | + | user_group| | +------------------+ + +-------------------+ +--+Decidim::UserGroup| + +------------------+ +---- + +Thus, each commentable must have the comments counter cache column. +This is an example migration to add the comments counter cache column to a resource: + +[source,ruby] +---- +class AddCommentableCounterCacheToMeetings < ActiveRecord::Migration[5.2] + def change + add_column :decidim_meetings_meetings, :comments_count, :integer, null: false, default: 0 + Decidim::Meetings::Meeting.reset_column_information + Decidim::Meetings::Meeting.find_each(&:update_comments_count) + end +end +---- + +== Public view + +=== The "Comment" cell + +The `Comment` cell is used to render a comment in the public view. Usually, it is used in the `Comments` cell to render a list of comments. + +[source,ruby] +---- +cell("decidim/comments", resource) +---- + +This will render all the comments for the given resource. + +=== Comments actions + +Each comment has a set of actions that can be performed by the user. By default, these actions are available: + +- A user can edit and delete their own comments. +- A user can report a comment. +- A user can copy the link to a comment. + +These actions are available through a dropdown menu in the comment. + +=== Resource-specific extra actions + +Each resource can define extra actions that can be performed on comments. These actions must be returned by the resource object through the `actions_for_comment` method. If the resource does not define this method, or returns empty, no extra actions will be available. + +This method will receive two parameters, the comment itself and the current user that wants to interact with the comment. + +It must return an array of hashes, where each hash has the following keys: + +- `label`: The text for the link for the action. +- `url`: The URL where the action will be performed. +- `icon`: The icon to be displayed for the action (optional). +- `method`: The HTTP method for the action, usually `:post` or `:delete` (optional as it defaults to `get`). +- `data`: A hash with the data attributes for the link (optional). + +All these extra actions will be displayed in the dropdown menu of the comment after the default ones. + +For example, this is how the `Proposal` model defines extra actions for comments: + +[source,ruby] +---- +def user_has_actions?(user) + return false if authors.include?(user) + return false if user&.blocked? + return false if user&.deleted? + return false unless user&.confirmed? + + true +end + +def actions_for_comment(comment, current_user) + return if comment.commentable != self + return unless authors.include?(current_user) + return unless user_has_actions?(comment.author) + + if coauthor_invitations_for(comment.author).any? + [ + { + label: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation"), + url: EngineRouter.main_proxy(component).cancel_proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id), + icon: "user-forbid-line", + method: :delete, + data: { confirm: I18n.t("decidim.proposals.actions.cancel_coauthor_invitation_confirm") } + } + ] + else + [ + { + label: I18n.t("decidim.proposals.actions.mark_as_coauthor"), + url: EngineRouter.main_proxy(component).proposal_invite_coauthors_path(proposal_id: id, id: comment.author.id), + icon: "user-add-line", + method: :post, + data: { confirm: I18n.t("decidim.proposals.actions.mark_as_coauthor_confirm") } + } + ] + end +end +---- + +This will render a new menu item with the text "Mark as coauthor" or "Cancel coauthor invitation" depending on the state of the comment author that will allow to add the comment's author as a co-author of the proposal. + From eceb01013b93d75fd2e52b99013b8372365d8e96 Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Fri, 9 Aug 2024 16:14:58 +0300 Subject: [PATCH 13/22] Generalize the term "Geocoding enabled" (#13285) * Generalize the term "Geocoding enabled" * Update the terminology also in docs and specs --- decidim-budgets/config/locales/en.yml | 2 +- .../spec/lib/decidim/budgets/admin/component_spec.rb | 2 +- decidim-core/spec/types/component_input_filter_spec.rb | 4 ++-- decidim-proposals/config/locales/en.yml | 2 +- decidim-proposals/spec/system/edit_proposal_spec.rb | 2 +- docs/modules/services/pages/maps.adoc | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/decidim-budgets/config/locales/en.yml b/decidim-budgets/config/locales/en.yml index ee8f2ac008192..4ad939e9f1a19 100644 --- a/decidim-budgets/config/locales/en.yml +++ b/decidim-budgets/config/locales/en.yml @@ -329,7 +329,7 @@ en: errors: budget_voting_rule_only_one: Only one voting rule must be enabled. budget_voting_rule_required: One voting rule is required. - geocoding_enabled: Geocoding enabled + geocoding_enabled: Maps enabled landing_page_content: Budgets landing page more_information_modal: More information modal projects_per_page: Projects per page diff --git a/decidim-budgets/spec/lib/decidim/budgets/admin/component_spec.rb b/decidim-budgets/spec/lib/decidim/budgets/admin/component_spec.rb index 2a15450d41692..3fb6b8bad91ed 100644 --- a/decidim-budgets/spec/lib/decidim/budgets/admin/component_spec.rb +++ b/decidim-budgets/spec/lib/decidim/budgets/admin/component_spec.rb @@ -49,7 +49,7 @@ def new_settings(name, data) Decidim::Component.build_settings(manifest, name, data, organization) end - describe "with geocoding enabled" do + describe "with maps enabled" do let(:geocoding_enabled) { true } # One budget rule must me activated let(:percent_enabled) { true } diff --git a/decidim-core/spec/types/component_input_filter_spec.rb b/decidim-core/spec/types/component_input_filter_spec.rb index 6745f466e6890..ab2f2a11ec98e 100644 --- a/decidim-core/spec/types/component_input_filter_spec.rb +++ b/decidim-core/spec/types/component_input_filter_spec.rb @@ -61,11 +61,11 @@ module Core end end - context "when searching components with geocoding enabled" do + context "when searching components with maps enabled" do let!(:model_with_geocoding_enabled) { create(:proposal_component, :published, :with_geocoding_enabled, participatory_space: model) } let(:query) { "{ components(filter: { withGeolocationEnabled: true} ) { id } }" } - it "returns the component with geocoding enabled" do + it "returns the component with maps enabled" do ids = response["components"].map { |component| component["id"].to_i } expect(ids).to include(*model_with_geocoding_enabled.id) end diff --git a/decidim-proposals/config/locales/en.yml b/decidim-proposals/config/locales/en.yml index f42516e28ad79..efad62021cfcb 100644 --- a/decidim-proposals/config/locales/en.yml +++ b/decidim-proposals/config/locales/en.yml @@ -176,7 +176,7 @@ en: days: Days hours: Hours minutes: Minutes - geocoding_enabled: Geocoding enabled + geocoding_enabled: Maps enabled minimum_votes_per_user: Minimum votes per user new_proposal_body_template: New proposal body template new_proposal_body_template_help: You can define prefilled text that the new Proposals will have diff --git a/decidim-proposals/spec/system/edit_proposal_spec.rb b/decidim-proposals/spec/system/edit_proposal_spec.rb index a24aba7156ca2..23cecd7352d8d 100644 --- a/decidim-proposals/spec/system/edit_proposal_spec.rb +++ b/decidim-proposals/spec/system/edit_proposal_spec.rb @@ -186,7 +186,7 @@ end end - context "with geocoding enabled" do + context "with maps enabled" do let(:component) { create(:proposal_component, :with_geocoding_enabled, participatory_space: participatory_process) } let(:address) { "6 Villa des Nymphéas 75020 Paris" } let(:new_address) { "6 rue Sorbier 75020 Paris" } diff --git a/docs/modules/services/pages/maps.adoc b/docs/modules/services/pages/maps.adoc index 27350b3f532a5..64bacc24ca66a 100644 --- a/docs/modules/services/pages/maps.adoc +++ b/docs/modules/services/pages/maps.adoc @@ -241,8 +241,8 @@ As of April 2017, only proposals and meetings have maps and geocoding. === Proposals -In order to enable geocoding for proposals you will need to edit the component configuration and turn on "Geocoding enabled" configuration. -This works for that specific component, so you can have geocoding enabled for proposals in a participatory process, and disabled for another proposals component in the same participatory process. +In order to enable maps for proposals you will need to edit the component configuration and turn on "Maps enabled" configuration. +This works for that specific component, so you can have maps enabled for proposals in a participatory process, and disabled for another proposals component in the same participatory process. === Meetings From a651f1658400a3e3b5e78ee887605900fb6b0acd Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Fri, 9 Aug 2024 16:39:17 +0300 Subject: [PATCH 14/22] Remove unnecessary expected spelling variant (#13287) --- .github/actions/spelling/expect.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 8bba11c2eae07..2e3c7fae82733 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -869,7 +869,6 @@ tailwindcss tarekraafat taxonomizable taxonomization -taxonomizations technopolitical templatable templateable From 100795c1287d096f2cc1613fe5a939cf130ae48f Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Fri, 9 Aug 2024 16:40:32 +0300 Subject: [PATCH 15/22] Disable search engine choise window for ChromeDriver (#13282) --- decidim-dev/lib/decidim/dev/test/rspec_support/capybara.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/decidim-dev/lib/decidim/dev/test/rspec_support/capybara.rb b/decidim-dev/lib/decidim/dev/test/rspec_support/capybara.rb index a3b4e4dffe127..1d7e1bf3455de 100644 --- a/decidim-dev/lib/decidim/dev/test/rspec_support/capybara.rb +++ b/decidim-dev/lib/decidim/dev/test/rspec_support/capybara.rb @@ -58,6 +58,7 @@ def protocol options = Selenium::WebDriver::Chrome::Options.new options.args << "--explicitly-allowed-ports=#{Capybara.server_port}" options.args << "--headless=new" + options.args << "--disable-search-engine-choice-screen" # Prevents closing the window normally # Do not limit browser resources options.args << "--disable-dev-shm-usage" options.args << "--no-sandbox" From 6145811bd533293863a3ca20ef12e16984e300e2 Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Fri, 9 Aug 2024 17:06:55 +0300 Subject: [PATCH 16/22] Make confirm page unload compatible with the current unload event standards and newer Chromium (#13283) * Refactor the "beforeunload" event listeners to custom confirm dialogs * Make the confirm unload message translatable * Change `beforeunload` -> `pagehide` event where applicable * Fix spelling mistakes * Generalize handling the confirm dialog * Revert the change in configuration.js and instead use i18n `getMessages` * Remove the event listener if action is allowed (link or form) * Remove unnecessary commented code --- .../headers/browser_feature_permissions.rb | 50 ++++++ .../decidim/application_controller.rb | 1 + decidim-core/app/packs/src/decidim/confirm.js | 138 +++++++++------- .../app/packs/src/decidim/form_remote.js | 2 +- .../app/packs/src/decidim/impersonation.js | 2 +- decidim-core/app/packs/src/decidim/index.js | 5 +- .../packs/src/decidim/session_timeouter.js | 2 +- .../app/packs/src/decidim/utilities/dom.js | 148 ++++++++++++++++++ .../decidim/_js_configuration.html.erb | 1 + decidim-core/config/locales/en.yml | 1 + .../rspec_support/confirmation_helpers.rb | 8 +- .../app/packs/src/decidim/forms/forms.js | 40 ++--- 12 files changed, 313 insertions(+), 85 deletions(-) create mode 100644 decidim-core/app/controllers/concerns/decidim/headers/browser_feature_permissions.rb create mode 100644 decidim-core/app/packs/src/decidim/utilities/dom.js diff --git a/decidim-core/app/controllers/concerns/decidim/headers/browser_feature_permissions.rb b/decidim-core/app/controllers/concerns/decidim/headers/browser_feature_permissions.rb new file mode 100644 index 0000000000000..0c94e3b66f7dc --- /dev/null +++ b/decidim-core/app/controllers/concerns/decidim/headers/browser_feature_permissions.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "active_support/concern" + +module Decidim + module Headers + # This module controls the "Permissions-Policy" header to define the + # specific sets of browser features that the website is able to use. + module BrowserFeaturePermissions + extend ActiveSupport::Concern + + included do + after_action :define_permissions_policy + end + + private + + def define_permissions_policy + return if response.media_type != "text/html" + return if response.headers["Permissions-Policy"].present? + + # Allow the "unload" and "onbeforeunload" events to be used at the + # current domain to prevent the user unintentionally changing the page + # when they have something important to do on the page, such as an + # unsaved form. + # + # This header is required because Chrome is phasing this event out due + # to some performance issues with the back/forward cache feature of the + # browser. However, currently there are no alternative events that would + # allow preventing accidental page reloads, tab closing or window + # closing. + # + # For further information, see: + # https://developer.chrome.com/docs/web-platform/deprecating-unload + # https://github.com/fergald/docs/blob/master/explainers/permissions-policy-unload.md + # + # Note that even Google suggests using the "beforeunload" for this + # particular use case: + # https://developer.chrome.com/docs/web-platform/page-lifecycle-api#events + # + # beforeunload + # Important: the beforeunload event should only be used to alert the + # user of unsaved changes. Once those changes are saved, the event + # should be removed. It should never be added unconditionally to the + # page, as doing so can hurt performance in some cases. + response.headers["Permissions-Policy"] = "unload=(self)" + end + end + end +end diff --git a/decidim-core/app/controllers/decidim/application_controller.rb b/decidim-core/app/controllers/decidim/application_controller.rb index 40bd3c53423eb..8b8cc44b564a2 100644 --- a/decidim-core/app/controllers/decidim/application_controller.rb +++ b/decidim-core/app/controllers/decidim/application_controller.rb @@ -16,6 +16,7 @@ class ApplicationController < ::DecidimController include NeedsTosAccepted include Headers::HttpCachingDisabler include Headers::ContentSecurityPolicy + include Headers::BrowserFeaturePermissions include ActionAuthorization include ForceAuthentication include SafeRedirect diff --git a/decidim-core/app/packs/src/decidim/confirm.js b/decidim-core/app/packs/src/decidim/confirm.js index c69446ca23ee6..f1ddfd3ca8ded 100644 --- a/decidim-core/app/packs/src/decidim/confirm.js +++ b/decidim-core/app/packs/src/decidim/confirm.js @@ -5,12 +5,14 @@ * it to gain control over the confirm events BEFORE rails-ujs is loaded. */ -import Rails from "@rails/ujs" +const { Rails } = window; class ConfirmDialog { constructor(sourceElement) { this.$modal = $("#confirm-modal"); - this.$source = sourceElement; + if (sourceElement) { + this.$source = $(sourceElement); + } this.$content = $("[data-confirm-modal-content]", this.$modal); this.$buttonConfirm = $("[data-confirm-ok]", this.$modal); this.$buttonCancel = $("[data-confirm-cancel]", this.$modal); @@ -29,22 +31,37 @@ class ConfirmDialog { this.$buttonConfirm.on("click", (ev) => { ev.preventDefault(); - window.Decidim.currentDialogs["confirm-modal"].close() - resolve(true); - this.$source.focus(); + this.close(() => resolve(true)); }); this.$buttonCancel.on("click", (ev) => { ev.preventDefault(); - window.Decidim.currentDialogs["confirm-modal"].close() - resolve(false); - this.$source.focus(); + this.close(() => resolve(false)); }); }); } + + close(afterClose) { + window.Decidim.currentDialogs["confirm-modal"].close() + afterClose(); + if (this.$source) { + this.$source.focus(); + } + } } +const runConfirm = (message, sourceElement = null) => new Promise((resolve) => { + const dialog = new ConfirmDialog(sourceElement); + dialog.confirm(message).then((answer) => { + let completed = true; + if (sourceElement) { + completed = Rails.fire(sourceElement, "confirm:complete", [answer]); + } + resolve(answer && completed); + }); +}); + // Override the default confirm dialog by Rails // See: // https://github.com/rails/rails/blob/fba1064153d8e2f4654df7762a7d3664b93e9fc8/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee @@ -64,37 +81,35 @@ const allowAction = (ev, element) => { return false; } - const dialog = new ConfirmDialog( - $(element) - ); - dialog.confirm(message).then((answer) => { - const completed = Rails.fire(element, "confirm:complete", [answer]); - if (answer && completed) { - // Allow the event to propagate normally and re-dispatch it without - // the confirm data attribute which the Rails internal method is - // checking. - $(element).data("confirm", null); - $(element).removeAttr("data-confirm"); - - // The submit button click events will not do anything if they are - // dispatched as is. In these cases, just submit the underlying form. - if (ev.type === "click" && - ( - $(element).is('button[type="submit"]') || - $(element).is('input[type="submit"]') - ) - ) { - $(element).parents("form").submit(); - } else { - let origEv = ev.originalEvent || ev; - let newEv = origEv; - if (typeof Event === "function") { - // Clone the event because otherwise some click events may not - // work properly when re-dispatched. - newEv = new origEv.constructor(origEv.type, origEv); - } - ev.target.dispatchEvent(newEv); + runConfirm(message, element).then((answer) => { + if (!answer) { + return; + } + + // Allow the event to propagate normally and re-dispatch it without + // the confirm data attribute which the Rails internal method is + // checking. + $(element).data("confirm", null); + $(element).removeAttr("data-confirm"); + + // The submit button click events will not do anything if they are + // dispatched as is. In these cases, just submit the underlying form. + if (ev.type === "click" && + ( + $(element).is('button[type="submit"]') || + $(element).is('input[type="submit"]') + ) + ) { + $(element).parents("form").submit(); + } else { + let origEv = ev.originalEvent || ev; + let newEv = origEv; + if (typeof Event === "function") { + // Clone the event because otherwise some click events may not + // work properly when re-dispatched. + newEv = new origEv.constructor(origEv.type, origEv); } + ev.target.dispatchEvent(newEv); } }); @@ -129,26 +144,31 @@ const handleDocumentEvent = (ev, matchSelectors) => { }); }; -document.addEventListener("click", (ev) => { - return handleDocumentEvent(ev, [ - Rails.linkClickSelector, - Rails.buttonClickSelector, - Rails.formInputClickSelector - ]); -}); -document.addEventListener("change", (ev) => { - return handleDocumentEvent(ev, [Rails.inputChangeSelector]); -}); -document.addEventListener("submit", (ev) => { - return handleDocumentEvent(ev, [Rails.formSubmitSelector]); -}); +// Note that this needs to be run **before** Rails.start() +export const initializeConfirm = () => { + document.addEventListener("click", (ev) => { + return handleDocumentEvent(ev, [ + Rails.linkClickSelector, + Rails.buttonClickSelector, + Rails.formInputClickSelector + ]); + }); + document.addEventListener("change", (ev) => { + return handleDocumentEvent(ev, [Rails.inputChangeSelector]); + }); + document.addEventListener("submit", (ev) => { + return handleDocumentEvent(ev, [Rails.formSubmitSelector]); + }); -// This is needed for the confirm dialog to work with Foundation Abide. -// Abide registers its own submit click listeners since Foundation 5.6.x -// which will be handled before the document listeners above. This would -// break the custom confirm functionality when used with Foundation Abide. -document.addEventListener("DOMContentLoaded", function() { - $(Rails.formInputClickSelector).on("click.confirm", (ev) => { - handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector)); + // This is needed for the confirm dialog to work with Foundation Abide. + // Abide registers its own submit click listeners since Foundation 5.6.x + // which will be handled before the document listeners above. This would + // break the custom confirm functionality when used with Foundation Abide. + document.addEventListener("DOMContentLoaded", function() { + $(Rails.formInputClickSelector).on("click.confirm", (ev) => { + handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector)); + }); }); -}); +}; + +export default runConfirm; diff --git a/decidim-core/app/packs/src/decidim/form_remote.js b/decidim-core/app/packs/src/decidim/form_remote.js index 40c5be91e958d..36fa646abf164 100644 --- a/decidim-core/app/packs/src/decidim/form_remote.js +++ b/decidim-core/app/packs/src/decidim/form_remote.js @@ -1,4 +1,4 @@ -import Rails from "@rails/ujs"; +const { Rails } = window; // Make the remote form submit buttons disabled when the form is being // submitted to avoid multiple submits. diff --git a/decidim-core/app/packs/src/decidim/impersonation.js b/decidim-core/app/packs/src/decidim/impersonation.js index d3de232db77df..d9ddd1ee503c7 100644 --- a/decidim-core/app/packs/src/decidim/impersonation.js +++ b/decidim-core/app/packs/src/decidim/impersonation.js @@ -15,7 +15,7 @@ $(() => { }, 1000); // Prevent reload when page is already unloading, otherwise it may cause infinite reloads. - window.addEventListener("beforeunload", () => { + window.addEventListener("pagehide", () => { clearInterval(exitInterval); return; }); diff --git a/decidim-core/app/packs/src/decidim/index.js b/decidim-core/app/packs/src/decidim/index.js index ed6677024f3b1..6f3fd45dc32a0 100644 --- a/decidim-core/app/packs/src/decidim/index.js +++ b/decidim-core/app/packs/src/decidim/index.js @@ -41,7 +41,6 @@ import "src/decidim/vizzs" import "src/decidim/responsive_horizontal_tabs" import "src/decidim/security/selfxss_warning" import "src/decidim/session_timeouter" -import "src/decidim/confirm" import "src/decidim/results_listing" import "src/decidim/impersonation" import "src/decidim/gallery" @@ -53,6 +52,7 @@ import "src/decidim/sticky_header" import "src/decidim/attachments" // local deps that require initialization +import ConfirmDialog, { initializeConfirm } from "src/decidim/confirm" import formDatePicker from "src/decidim/datepicker/form_datepicker" import Configuration from "src/decidim/configuration" import ExternalLink from "src/decidim/external_link" @@ -89,6 +89,7 @@ window.Decidim = window.Decidim || { addInputEmoji, EmojiButton, Dialogs, + ConfirmDialog, announceForScreenReader }; @@ -121,6 +122,8 @@ window.initFoundation = (element) => { }); }; +// Confirm initialization needs to happen before Rails.start() +initializeConfirm(); Rails.start() /** diff --git a/decidim-core/app/packs/src/decidim/session_timeouter.js b/decidim-core/app/packs/src/decidim/session_timeouter.js index 051d2f6dc2238..e4a6bc0f5c9c2 100644 --- a/decidim-core/app/packs/src/decidim/session_timeouter.js +++ b/decidim-core/app/packs/src/decidim/session_timeouter.js @@ -127,7 +127,7 @@ $(() => { setTimer(timeoutInSeconds); }); - window.addEventListener("beforeunload", () => { + window.addEventListener("pagehide", () => { clearInterval(exitInterval); return; }); diff --git a/decidim-core/app/packs/src/decidim/utilities/dom.js b/decidim-core/app/packs/src/decidim/utilities/dom.js new file mode 100644 index 0000000000000..c272c19e17bc9 --- /dev/null +++ b/decidim-core/app/packs/src/decidim/utilities/dom.js @@ -0,0 +1,148 @@ +import confirmAction from "src/decidim/confirm" +import { getMessages } from "src/decidim/i18n" + +const { Rails } = window; + +const createUnloadPreventer = () => { + const preventUnloadConditions = []; + + const confirmMessage = getMessages("confirmUnload") || "Are you sure you want to leave this page?"; + + const canUnload = (event) => !preventUnloadConditions.some((condition) => condition(event)); + + // TLDR: + // The beforeunload event does not work during tests due to the deprecation of + // the unload event and ChromeDriver automatically accepting these dialogs. + // --- + // + // Even when there are custom listeners on links and forms, the beforeunload + // event is to ensure that the user does not accidentally reload the page or + // close the browser or the tab. Note that this does not work during the tests + // with ChromeDriver due to the deprecation of the unload event and + // ChromeDriver automatically accepting these dialogs. For the time being, + // this should work when a real user interacts with the browser along with the + // "Permissions-Policy" header set by the backend. For more information about + // the header, see Decidim::Headers::BrowserFeaturePermissions). + const unloadListener = (event) => { + if (canUnload(event)) { + return; + } + + // According to: + // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event + // + // > [...] best practice is to trigger the dialog by invoking + // > preventDefault() on the event object, while also setting returnValue to + // > support legacy cases. + event.preventDefault(); + event.returnValue = true; + }; + + // The beforeunload event listener has to be registered AFTER a user + // interaction which is why it is wrapped around the next click event that + // happens after the first unload listener was registered. Otherwise it might + // not work due to the deprecation of the unload APIs in Chromium based + // browsers and possibly in the web standards in the future. + // + // According to: + // https://developer.chrome.com/docs/web-platform/page-lifecycle-api#the_beforeunload_event + // + // > Never add a beforeunload listener unconditionally or use it as an + // > end-of-session signal. Only add it when a user has unsaved work, and + // > remove it as soon as that work has been saved. + const registerBeforeUnload = () => { + window.removeEventListener("click", registerBeforeUnload); + window.addEventListener("beforeunload", unloadListener); + }; + + const disableBeforeUnload = () => { + window.removeEventListener("click", registerBeforeUnload); + window.removeEventListener("beforeunload", unloadListener); + }; + + const linkClickListener = (ev) => { + const link = ev.target?.closest("a"); + if (!link) { + return; + } + + if (canUnload(ev)) { + disableBeforeUnload(); + document.removeEventListener("click", linkClickListener); + return; + } + + window.exitUrl = link.href; + + ev.preventDefault(); + ev.stopPropagation(); + + confirmAction(confirmMessage, link).then((answer) => { + if (!answer) { + return; + } + + disableBeforeUnload(); + document.removeEventListener("click", linkClickListener); + link.click(); + }); + }; + + const formSubmitListener = (ev) => { + const source = ev.target?.closest("form"); + if (!source) { + return; + } + + if (canUnload(ev)) { + disableBeforeUnload(); + document.removeEventListener("submit", formSubmitListener); + return; + } + + const button = source.closest(Rails.formSubmitSelector); + if (!button) { + return; + } + + ev.preventDefault(); + ev.stopImmediatePropagation(); + ev.stopPropagation(); + + confirmAction(confirmMessage, button).then((answer) => { + if (!answer) { + return; + } + + disableBeforeUnload(); + document.removeEventListener("submit", formSubmitListener); + source.submit(); + }); + }; + + const registerPreventUnloadListeners = () => { + window.addEventListener("click", registerBeforeUnload); + document.addEventListener("click", linkClickListener); + document.addEventListener("submit", formSubmitListener); + }; + + return { + addPreventCondition: (condition) => { + if (typeof condition !== "function") { + return; + } + + if (preventUnloadConditions.length < 1) { + // The unload listeners are global, so only the first call to this + // function should result to registering these listeners. + registerPreventUnloadListeners(); + } + + preventUnloadConditions.push(condition); + } + }; +}; + +const unloadPreventer = createUnloadPreventer(); + +export const preventUnload = (condition) => unloadPreventer.addPreventCondition(condition); diff --git a/decidim-core/app/views/layouts/decidim/_js_configuration.html.erb b/decidim-core/app/views/layouts/decidim/_js_configuration.html.erb index 43b73d0ed5a92..8086da8df1f4f 100644 --- a/decidim-core/app/views/layouts/decidim/_js_configuration.html.erb +++ b/decidim-core/app/views/layouts/decidim/_js_configuration.html.erb @@ -7,6 +7,7 @@ js_configs = { "mentionsModal": { "removeRecipient": t("decidim.shared.mentions_modal.remove_recipient", name: "%name%") }, + "confirmUnload": t("decidim.shared.confirm_unload"), emojis: I18n.t("emojis").deep_transform_keys { |k| k.to_s.camelize(:lower) }, editor: I18n.t("editor"), date: I18n.t("date"), diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml index f506ca68ae265..60ecf72115402 100644 --- a/decidim-core/config/locales/en.yml +++ b/decidim-core/config/locales/en.yml @@ -1484,6 +1484,7 @@ en: close_modal: Close modal ok: OK title: Confirm + confirm_unload: This page contains unsaved changes. Are you sure you want to leave this page? embed: title: Embedded video content extended_navigation_bar: diff --git a/decidim-dev/lib/decidim/dev/test/rspec_support/confirmation_helpers.rb b/decidim-dev/lib/decidim/dev/test/rspec_support/confirmation_helpers.rb index 1a01b9413f5d8..fb1473c6d185b 100644 --- a/decidim-dev/lib/decidim/dev/test/rspec_support/confirmation_helpers.rb +++ b/decidim-dev/lib/decidim/dev/test/rspec_support/confirmation_helpers.rb @@ -47,15 +47,15 @@ def dismiss_confirm(_text = nil) # Used to accept the "onbeforeunload" event's normal browser confirm modal # as this cannot be overridden. Original confirm dismiss implementation in # Capybara. - def accept_page_unload(text = nil, **options, &) - page.send(:accept_modal, :confirm, text, options, &) + def accept_page_unload(text = nil, **, &) + accept_confirm(text, &) end # Used to dismiss the "onbeforeunload" event's normal browser confirm modal # as this cannot be overridden. Original confirm dismiss implementation in # Capybara. - def dismiss_page_unload(text = nil, **options, &) - page.send(:dismiss_modal, :confirm, text, options, &) + def dismiss_page_unload(text = nil, **, &) + dismiss_confirm(text, &) end end diff --git a/decidim-forms/app/packs/src/decidim/forms/forms.js b/decidim-forms/app/packs/src/decidim/forms/forms.js index 646d748bc7272..a4dd5ad4f41df 100644 --- a/decidim-forms/app/packs/src/decidim/forms/forms.js +++ b/decidim-forms/app/packs/src/decidim/forms/forms.js @@ -11,6 +11,7 @@ import "dragula/dist/dragula.css"; import createOptionAttachedInputs from "src/decidim/forms/option_attached_inputs.component" import createDisplayConditions from "src/decidim/forms/display_conditions.component" import createMaxChoicesAlertComponent from "src/decidim/forms/max_choices_alert.component" +import { preventUnload } from "src/decidim/utilities/dom" $(() => { $(".js-radio-button-collection, .js-check-box-collection").each((idx, el) => { @@ -45,30 +46,33 @@ $(() => { }); }); - const $form = $("form.answer-questionnaire"); - if ($form.length > 0) { - $form.find("input, textarea, select").on("change", () => { - $form.data("changed", true); + const form = document.querySelector("form.answer-questionnaire"); + if (form) { + const safePath = form.dataset.safePath.split("?")[0]; + let exitUrl = ""; + document.addEventListener("click", (event) => { + const link = event.target?.closest("a"); + if (link) { + exitUrl = link.href; + } }); - const safePath = $form.data("safe-path").split("?")[0]; - $(document).on("click", "a", (event) => { - window.exitUrl = event.currentTarget.href; - }); + // The submit listener has to be registered through jQuery because the + // custom confirm dialog does not dispatch the "submit" event normally. $(document).on("submit", "form", (event) => { - window.exitUrl = event.currentTarget.action; + exitUrl = event.currentTarget.action; }); - window.addEventListener("beforeunload", (event) => { - const exitUrl = window.exitUrl; - const hasChanged = $form.data("changed"); - window.exitUrl = null; + let hasChanged = false; + const controls = form.querySelectorAll("input, textarea, select"); + const changeListener = () => { + if (!hasChanged) { + hasChanged = true; + controls.forEach((control) => control.removeEventListener("change", changeListener)); - if (!hasChanged || (exitUrl && exitUrl.includes(safePath))) { - return; + preventUnload(() => !exitUrl.includes(safePath)); } - - event.returnValue = true; - }); + }; + controls.forEach((control) => control.addEventListener("change", changeListener)); } }) From 31c58f063548b6bc26e45c8d73f205076e4b1904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Fri, 9 Aug 2024 21:33:26 +0200 Subject: [PATCH 17/22] Fix character counter disposition and spacing with WYSIWYG (#13264) * Fix character counter disposition and spacing with WYSIWYG * Fix title capitalization * Fix spec --- .../app/packs/src/decidim/input_character_counter.js | 2 +- decidim-core/app/packs/stylesheets/decidim/editor.scss | 2 +- decidim-proposals/config/locales/en.yml | 2 +- decidim-proposals/spec/shared/proposals_wizards_examples.rb | 4 ++-- decidim-proposals/spec/system/new_proposals_spec.rb | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/decidim-core/app/packs/src/decidim/input_character_counter.js b/decidim-core/app/packs/src/decidim/input_character_counter.js index 221c22f27aa66..536f69bd00c62 100644 --- a/decidim-core/app/packs/src/decidim/input_character_counter.js +++ b/decidim-core/app/packs/src/decidim/input_character_counter.js @@ -74,7 +74,7 @@ export default class InputCharacterCounter { // If input is a hidden for WYSIWYG editor add it at the end if (this.$input.parent().is(".editor")) { - this.$input.parent().after(this.$target); + this.$input.parent().append(container); } else { const wrapper = document.createElement("span") wrapper.className = "input-character-counter" diff --git a/decidim-core/app/packs/stylesheets/decidim/editor.scss b/decidim-core/app/packs/stylesheets/decidim/editor.scss index c1b64b9e4f83b..5035beaa02391 100644 --- a/decidim-core/app/packs/stylesheets/decidim/editor.scss +++ b/decidim-core/app/packs/stylesheets/decidim/editor.scss @@ -21,7 +21,7 @@ } .editor-container { - @apply editor-props editor-suggestions-props flex flex-col mb-6 border editor-border; + @apply editor-props editor-suggestions-props flex flex-col mt-4 border editor-border; &.editor-disabled { .editor-input .ProseMirror { diff --git a/decidim-proposals/config/locales/en.yml b/decidim-proposals/config/locales/en.yml index efad62021cfcb..b362ef27dec06 100644 --- a/decidim-proposals/config/locales/en.yml +++ b/decidim-proposals/config/locales/en.yml @@ -843,7 +843,7 @@ en: discard: Discard this draft discard_confirmation: Are you sure you want to discard this proposal draft? send: Preview - title: Edit Proposal Draft + title: Edit proposal draft edit_form_fields: marker_added: Marker added to the map. filters: diff --git a/decidim-proposals/spec/shared/proposals_wizards_examples.rb b/decidim-proposals/spec/shared/proposals_wizards_examples.rb index 282ec4d72e396..f521e9c0b6e3d 100644 --- a/decidim-proposals/spec/shared/proposals_wizards_examples.rb +++ b/decidim-proposals/spec/shared/proposals_wizards_examples.rb @@ -93,7 +93,7 @@ end it "redirects to edit the proposal draft" do - expect(page).to have_content("Edit Proposal Draft") + expect(page).to have_content("Edit proposal draft") end end @@ -239,7 +239,7 @@ end it "redirects to edit the proposal draft" do - expect(page).to have_content("Edit Proposal Draft") + expect(page).to have_content("Edit proposal draft") end end diff --git a/decidim-proposals/spec/system/new_proposals_spec.rb b/decidim-proposals/spec/system/new_proposals_spec.rb index 474fdd94e83ef..8d140a7d460de 100644 --- a/decidim-proposals/spec/system/new_proposals_spec.rb +++ b/decidim-proposals/spec/system/new_proposals_spec.rb @@ -43,9 +43,9 @@ it "has helper character counter" do within "form.new_proposal" do - editor = find(".editor") - page.scroll_to(editor) - expect(editor.sibling("[id^=characters_]:not([id$=_sr])")).to have_content("At least 15 characters", count: 1) + within ".editor .input-character-counter__text" do + expect(page).to have_content("At least 15 characters", count: 1) + end end end From 34a172450607b8cdc732009014474e3babcc4a50 Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Mon, 12 Aug 2024 00:15:20 +0300 Subject: [PATCH 18/22] Do not disclose Decidim version through the API (#13280) * Do not disclose Decidim version through the API * Fix specs related to the API change --- RELEASE_NOTES.md | 14 +++++++++++++- decidim-api/lib/decidim/api.rb | 4 ++++ decidim-core/lib/decidim/api/types/decidim_type.rb | 6 +++++- decidim-core/spec/lib/query_extensions_spec.rb | 14 ++++++++++++-- decidim-core/spec/types/decidim_spec.rb | 12 +++++++++++- decidim-core/spec/types/decidim_type_spec.rb | 12 +++++++++++- 6 files changed, 56 insertions(+), 6 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 184a233fadd08..29bef8190f3e4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -74,7 +74,19 @@ You can read more about this change on PR [#XXXX](https://github.com/decidim/dec ## 5. Changes in APIs -### 5.1. [[TITLE OF THE CHANGE]] +### 5.1. Decidim version number no longer disclosed through the GraphQL API by default + +In previous Decidim versions, you could request the running Decidim version through the following API query against the GraphQL API: + +```graphql +query { decidim { version } } +``` + +This no longer returns the running Decidim version by default and instead it will result to `null` being reported as the version number. + +If you would like to re-enable exposing the Decidim version number through the GraphQL API, you may do so by setting the `DECIDIM_API_DISCLOSE_SYSTEM_VERSION` environment variable to `true`. However, this is highly discouraged but may be required for some automation or integrations. + +### 5.2. [[TITLE OF THE CHANGE]] In order to [[REASONING (e.g. improve the maintenance of the code base)]] we have changed... diff --git a/decidim-api/lib/decidim/api.rb b/decidim-api/lib/decidim/api.rb index 455c8759a6289..900b2043a7998 100644 --- a/decidim-api/lib/decidim/api.rb +++ b/decidim-api/lib/decidim/api.rb @@ -24,6 +24,10 @@ module Api 15 end + config_accessor :disclose_system_version do + %w(1 true yes).include?(ENV.fetch("DECIDIM_API_DISCLOSE_SYSTEM_VERSION", nil)) + end + # This declares all the types an interface or union can resolve to. This needs # to be done in order to be able to have them found. This is a shortcoming of # graphql-ruby and the way it deals with loading types, in combination with diff --git a/decidim-core/lib/decidim/api/types/decidim_type.rb b/decidim-core/lib/decidim/api/types/decidim_type.rb index 27eb4f6734b6a..941d299c09cd9 100644 --- a/decidim-core/lib/decidim/api/types/decidim_type.rb +++ b/decidim-core/lib/decidim/api/types/decidim_type.rb @@ -6,8 +6,12 @@ module Core class DecidimType < Decidim::Api::Types::BaseObject description "Decidim's framework-related properties." - field :version, GraphQL::Types::String, "The current decidim's version of this deployment.", null: false + field :version, GraphQL::Types::String, "The current decidim's version of this deployment.", null: true field :application_name, GraphQL::Types::String, "The current installation's name.", null: false + + def version + object.version if Decidim::Api.disclose_system_version + end end end end diff --git a/decidim-core/spec/lib/query_extensions_spec.rb b/decidim-core/spec/lib/query_extensions_spec.rb index b3735bf2c7997..0e7c1221d0fd8 100644 --- a/decidim-core/spec/lib/query_extensions_spec.rb +++ b/decidim-core/spec/lib/query_extensions_spec.rb @@ -34,8 +34,18 @@ module Core describe "decidim" do let(:query) { %({ decidim { version }}) } - it "returns the right version" do - expect(response["decidim"]).to include("version" => Decidim.version) + it "returns nil" do + expect(response["decidim"]).to include("version" => nil) + end + + context "when disclosing system version is enabled" do + before do + allow(Decidim::Api).to receive(:disclose_system_version).and_return(true) + end + + it "returns the right version" do + expect(response["decidim"]).to include("version" => Decidim.version) + end end end diff --git a/decidim-core/spec/types/decidim_spec.rb b/decidim-core/spec/types/decidim_spec.rb index 8819d11b35dbe..5f7d665a0c926 100644 --- a/decidim-core/spec/types/decidim_spec.rb +++ b/decidim-core/spec/types/decidim_spec.rb @@ -28,8 +28,18 @@ it "has decidim" do expect(response["decidim"]).to eq({ "applicationName" => "My Application Name", - "version" => Decidim::Core.version + "version" => nil }) end + + context "when disclosing system version is enabled" do + before do + allow(Decidim::Api).to receive(:disclose_system_version).and_return(true) + end + + it "discloses the version number" do + expect(response["decidim"]).to include("version" => Decidim::Core.version) + end + end end end diff --git a/decidim-core/spec/types/decidim_type_spec.rb b/decidim-core/spec/types/decidim_type_spec.rb index b747f0fe62b6c..3aa6bd7d7dc46 100644 --- a/decidim-core/spec/types/decidim_type_spec.rb +++ b/decidim-core/spec/types/decidim_type_spec.rb @@ -18,7 +18,17 @@ module Core let(:query) { "{ version }" } it "returns the version" do - expect(response).to eq("version" => Decidim.version) + expect(response).to eq("version" => nil) + end + + context "when disclosing system version is enabled" do + before do + allow(Decidim::Api).to receive(:disclose_system_version).and_return(true) + end + + it "returns the version" do + expect(response).to eq("version" => Decidim.version) + end end end From 5d51db54c4ccfc850880fac4ef2a2a235871f9ad Mon Sep 17 00:00:00 2001 From: Antti Hukkanen Date: Mon, 12 Aug 2024 01:07:36 +0300 Subject: [PATCH 19/22] Fix deleting a component which has reminders associated with it (#13281) * Fix deleting a component which has reminders associated with it * Change the spec to use the `reminder` test factory --- .../app/commands/decidim/admin/destroy_component.rb | 1 + .../decidim/admin/destroy_component_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/decidim-admin/app/commands/decidim/admin/destroy_component.rb b/decidim-admin/app/commands/decidim/admin/destroy_component.rb index d73c1daa835e9..e83023b2ec9bc 100644 --- a/decidim-admin/app/commands/decidim/admin/destroy_component.rb +++ b/decidim-admin/app/commands/decidim/admin/destroy_component.rb @@ -7,6 +7,7 @@ class DestroyComponent < Decidim::Commands::DestroyResource private def run_before_hooks + Decidim::Reminder.where(component: resource).destroy_all resource.manifest.run_hooks(:before_destroy, resource) end diff --git a/decidim-admin/spec/commands/decidim/admin/destroy_component_spec.rb b/decidim-admin/spec/commands/decidim/admin/destroy_component_spec.rb index 0299bb28e44d9..67f2bbbc41e62 100644 --- a/decidim-admin/spec/commands/decidim/admin/destroy_component_spec.rb +++ b/decidim-admin/spec/commands/decidim/admin/destroy_component_spec.rb @@ -42,5 +42,18 @@ module Decidim::Admin expect(result_component).not_to be_persisted end end + + context "when the component has a reminder associated with it" do + let!(:reminder) { create(:reminder, user: current_user, component:) } + + it "destroys the component" do + expect { subject.call }.to broadcast(:ok) + expect(Decidim::Component.where(id: component.id)).not_to exist + end + + it "destroys the associated reminders" do + expect { subject.call }.to change(Decidim::Reminder, :count).by(-1) + end + end end end From 14c0207871c4288a192ac1b6588ae18113c5a7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Pereira=20de=20Lucena?= Date: Mon, 12 Aug 2024 11:56:38 +0200 Subject: [PATCH 20/22] Fix exceptions with `decidim-templates` when not added explicitly (#13263) * Remove decidim-templates dependency from decidim meta-gem * Make decidim-proposals a development dependency to decidim-templates * Fix bug with checking for the decidim-templates gem on templates choosers' partials --------- Co-authored-by: Alexandru Emil Lupu --- Gemfile.lock | 2 -- decidim-admin/app/views/decidim/admin/block_user/new.html.erb | 2 +- decidim-generators/Gemfile.lock | 2 -- .../decidim/proposals/admin/proposal_answers/_form.html.erb | 2 +- decidim-templates/decidim-templates.gemspec | 2 +- decidim.gemspec | 1 - 6 files changed, 3 insertions(+), 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7b1ea44818882..36735fa4519bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,7 +20,6 @@ PATH decidim-sortitions (= 0.30.0.dev) decidim-surveys (= 0.30.0.dev) decidim-system (= 0.30.0.dev) - decidim-templates (= 0.30.0.dev) decidim-verifications (= 0.30.0.dev) decidim-accountability (0.30.0.dev) decidim-comments (= 0.30.0.dev) @@ -192,7 +191,6 @@ PATH decidim-templates (0.30.0.dev) decidim-core (= 0.30.0.dev) decidim-forms (= 0.30.0.dev) - decidim-proposals (= 0.30.0.dev) decidim-verifications (0.30.0.dev) decidim-core (= 0.30.0.dev) diff --git a/decidim-admin/app/views/decidim/admin/block_user/new.html.erb b/decidim-admin/app/views/decidim/admin/block_user/new.html.erb index e62f0e08d20f8..bd96f01d00b9c 100644 --- a/decidim-admin/app/views/decidim/admin/block_user/new.html.erb +++ b/decidim-admin/app/views/decidim/admin/block_user/new.html.erb @@ -19,7 +19,7 @@ <%= cell("decidim/announcement", t(".already_reported_html"), callout_class: "alert" ) %> <% end %> - <% if defined?(Decidim::Templates) %> + <% if Decidim.module_installed?(:templates) %> <%= render "decidim/templates/admin/block_user_templates/template_chooser", form: f %> <% end %> diff --git a/decidim-generators/Gemfile.lock b/decidim-generators/Gemfile.lock index 3d6f66c0c0c9e..80c789fbe7bc7 100644 --- a/decidim-generators/Gemfile.lock +++ b/decidim-generators/Gemfile.lock @@ -20,7 +20,6 @@ PATH decidim-sortitions (= 0.30.0.dev) decidim-surveys (= 0.30.0.dev) decidim-system (= 0.30.0.dev) - decidim-templates (= 0.30.0.dev) decidim-verifications (= 0.30.0.dev) decidim-accountability (0.30.0.dev) decidim-comments (= 0.30.0.dev) @@ -192,7 +191,6 @@ PATH decidim-templates (0.30.0.dev) decidim-core (= 0.30.0.dev) decidim-forms (= 0.30.0.dev) - decidim-proposals (= 0.30.0.dev) decidim-verifications (0.30.0.dev) decidim-core (= 0.30.0.dev) diff --git a/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb b/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb index 4eac51cc3893f..13a1435c554ac 100644 --- a/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb +++ b/decidim-proposals/app/views/decidim/proposals/admin/proposal_answers/_form.html.erb @@ -5,7 +5,7 @@

    <%= t ".title", title: present(proposal).title %>

    - <% if templates_available? %> + <% if Decidim.module_installed?(:templates) %> <%= render "decidim/templates/admin/proposal_answer_templates/template_chooser", form: f %> <% end %> diff --git a/decidim-templates/decidim-templates.gemspec b/decidim-templates/decidim-templates.gemspec index 061abbcc59154..fdffa26a8d3ba 100644 --- a/decidim-templates/decidim-templates.gemspec +++ b/decidim-templates/decidim-templates.gemspec @@ -32,10 +32,10 @@ Gem::Specification.new do |s| s.add_dependency "decidim-core", Decidim::Templates.version s.add_dependency "decidim-forms", Decidim::Templates.version - s.add_dependency "decidim-proposals", Decidim::Templates.version s.add_development_dependency "decidim-admin", Decidim::Templates.version s.add_development_dependency "decidim-dev", Decidim::Templates.version s.add_development_dependency "decidim-participatory_processes", Decidim::Templates.version + s.add_development_dependency "decidim-proposals", Decidim::Templates.version s.add_development_dependency "decidim-surveys", Decidim::Templates.version end diff --git a/decidim.gemspec b/decidim.gemspec index 4f48c763c3f48..3a1bcbd12f5ca 100644 --- a/decidim.gemspec +++ b/decidim.gemspec @@ -63,7 +63,6 @@ Gem::Specification.new do |s| s.add_dependency "decidim-sortitions", Decidim.version s.add_dependency "decidim-surveys", Decidim.version s.add_dependency "decidim-system", Decidim.version - s.add_dependency "decidim-templates", Decidim.version s.add_dependency "decidim-verifications", Decidim.version s.add_development_dependency "bundler", "~> 2.2", ">= 2.2.18" From 89af37f8454aa90016705e266482bcc47a315915 Mon Sep 17 00:00:00 2001 From: Elvia Benedith <116598037+ElviaBth@users.noreply.github.com> Date: Mon, 12 Aug 2024 17:21:25 +0200 Subject: [PATCH 21/22] Add opt-in to newsletter when using OmniAuth registration method (#13077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create tos_fields partial * create new_tos_field template * add validation for tos_agreement * check authentication_spec.rb * fix create_omniauth_registration_spec.rb * fix authentication_spec.rb * add more test cases to authentication_spec.rb * modify method name * apply corrections * fix spec * fix authentication flow in specs * Update decidim-core/app/commands/decidim/create_omniauth_registration.rb Co-authored-by: Alexandru Emil Lupu * remove trailing whitespace * fix authentication_spec --------- Co-authored-by: Ivan Vergés Co-authored-by: Alexandru Emil Lupu --- .../decidim/create_omniauth_registration.rb | 16 ++- .../omniauth_registrations_controller.rb | 9 +- .../decidim/omniauth_registration_form.rb | 13 +++ .../packs/src/decidim/user_registrations.js | 13 +++ .../omniauth_registrations/new.html.erb | 6 +- .../new_tos_fields.html.erb | 29 +++++ .../decidim/devise/registrations/new.html.erb | 17 +-- .../devise/shared/_tos_fields.html.erb | 16 +++ decidim-core/config/locales/en.yml | 4 + .../create_omniauth_registration_spec.rb | 9 +- .../spec/system/authentication_spec.rb | 108 +++++++++++++++--- 11 files changed, 198 insertions(+), 42 deletions(-) create mode 100644 decidim-core/app/views/decidim/devise/omniauth_registrations/new_tos_fields.html.erb create mode 100644 decidim-core/app/views/decidim/devise/shared/_tos_fields.html.erb diff --git a/decidim-core/app/commands/decidim/create_omniauth_registration.rb b/decidim-core/app/commands/decidim/create_omniauth_registration.rb index 5ddd4457032c9..84cecd17b1502 100644 --- a/decidim-core/app/commands/decidim/create_omniauth_registration.rb +++ b/decidim-core/app/commands/decidim/create_omniauth_registration.rb @@ -36,6 +36,8 @@ def call trigger_omniauth_registration broadcast(:ok, @user) + rescue NeedTosAcceptance + broadcast(:add_tos_errors, @user) rescue ActiveRecord::RecordInvalid => e broadcast(:error, e.record) end @@ -73,10 +75,12 @@ def create_or_find_user file = url.open @user.avatar.attach(io: file, filename:) end + @user.tos_agreement = form.tos_agreement + @user.accepted_tos_version = Time.current + raise NeedTosAcceptance if @user.tos_agreement.blank? + @user.skip_confirmation! if verified_email - @user.tos_agreement = "1" @user.save! - @user.after_confirmation if verified_email end end @@ -129,11 +133,17 @@ def trigger_omniauth_registration name: form.name, nickname: form.normalized_nickname, avatar_url: form.avatar_url, - raw_data: form.raw_data + raw_data: form.raw_data, + tos_agreement: form.tos_agreement, + newsletter_notifications_at: form.newsletter_at, + accepted_tos_version: form.current_organization.tos_version ) end end + class NeedTosAcceptance < StandardError + end + class InvalidOauthSignature < StandardError end end diff --git a/decidim-core/app/controllers/decidim/devise/omniauth_registrations_controller.rb b/decidim-core/app/controllers/decidim/devise/omniauth_registrations_controller.rb index cbe73973113d8..f3d6673ae5f7c 100644 --- a/decidim-core/app/controllers/decidim/devise/omniauth_registrations_controller.rb +++ b/decidim-core/app/controllers/decidim/devise/omniauth_registrations_controller.rb @@ -7,6 +7,7 @@ class OmniauthRegistrationsController < ::Devise::OmniauthCallbacksController include FormFactory include Decidim::DeviseControllers include Decidim::DeviseAuthenticationMethods + include NeedsTosAccepted def new @form = form(OmniauthRegistrationForm).from_params(params[:user]) @@ -36,6 +37,12 @@ def create render :new end + on(:add_tos_errors) do + set_flash_message :alert, :add_tos_errors if @form.valid_tos? + session[:verified_email] = verified_email + render :new_tos_fields + end + on(:error) do |user| if user.errors[:email] set_flash_message :alert, :failure, kind: @form.provider.capitalize, reason: t("decidim.devise.omniauth_registrations.create.email_already_exists") @@ -75,7 +82,7 @@ def user_params_from_oauth_hash end def verified_email - @verified_email ||= oauth_data.dig(:info, :email) + @verified_email ||= oauth_data.dig(:info, :email).presence || session[:verified_email] end def oauth_hash diff --git a/decidim-core/app/forms/decidim/omniauth_registration_form.rb b/decidim-core/app/forms/decidim/omniauth_registration_form.rb index be3c0da669079..0ce7fd15f2afa 100644 --- a/decidim-core/app/forms/decidim/omniauth_registration_form.rb +++ b/decidim-core/app/forms/decidim/omniauth_registration_form.rb @@ -11,6 +11,7 @@ class OmniauthRegistrationForm < Form attribute :provider, String attribute :uid, String attribute :tos_agreement, Boolean + attribute :newsletter, Boolean attribute :oauth_signature, String attribute :avatar_url, String attribute :raw_data, Hash @@ -27,5 +28,17 @@ def self.create_signature(provider, uid) def normalized_nickname UserBaseEntity.nicknamize(nickname || name, organization: current_organization) end + + def newsletter_at + return nil unless newsletter? + + Time.current + end + + def valid_tos? + return if tos_agreement.nil? + + errors.add :tos_agreement, :accepted + end end end diff --git a/decidim-core/app/packs/src/decidim/user_registrations.js b/decidim-core/app/packs/src/decidim/user_registrations.js index 2bf007c250372..ffe87e3e8419c 100644 --- a/decidim-core/app/packs/src/decidim/user_registrations.js +++ b/decidim-core/app/packs/src/decidim/user_registrations.js @@ -2,6 +2,7 @@ import PasswordToggler from "src/decidim/password_toggler"; $(() => { const $userRegistrationForm = $("#register-form"); + const $userOmniauthRegistrationForm = $("#omniauth-register-form"); const $userGroupFields = $userRegistrationForm.find(".user-group-fields"); const userPassword = document.querySelector(".user-password"); const inputSelector = 'input[name="user[sign_up_as]"]'; @@ -19,9 +20,11 @@ $(() => { const checkNewsletter = (check) => { $userRegistrationForm.find(newsletterSelector).prop("checked", check); + $userOmniauthRegistrationForm.find(newsletterSelector).prop("checked", check); $newsletterModal.data("continue", true); window.Decidim.currentDialogs["sign-up-newsletter-modal"].close() $userRegistrationForm.submit(); + $userOmniauthRegistrationForm.submit(); } setGroupFieldsVisibility($userRegistrationForm.find(`${inputSelector}:checked`).val()); @@ -42,6 +45,16 @@ $(() => { } }); + $userOmniauthRegistrationForm.on("submit", (event) => { + const newsletterChecked = $userOmniauthRegistrationForm.find(newsletterSelector); + if (!$newsletterModal.data("continue")) { + if (!newsletterChecked.prop("checked")) { + event.preventDefault(); + window.Decidim.currentDialogs["sign-up-newsletter-modal"].open() + } + } + }); + $newsletterModal.find("[data-check]").on("click", (event) => { checkNewsletter($(event.target).data("check")); }); diff --git a/decidim-core/app/views/decidim/devise/omniauth_registrations/new.html.erb b/decidim-core/app/views/decidim/devise/omniauth_registrations/new.html.erb index 9565e5089ecd9..67d845f426072 100644 --- a/decidim-core/app/views/decidim/devise/omniauth_registrations/new.html.erb +++ b/decidim-core/app/views/decidim/devise/omniauth_registrations/new.html.erb @@ -13,7 +13,7 @@ <%= form_required_explanation %> - <%= decidim_form_for(@form, namespace: "registration", as: resource_name, url: omniauth_registrations_path(resource_name)) do |f| %> + <%= decidim_form_for(@form, namespace: "registration", as: resource_name, url: omniauth_registrations_path(resource_name), html: { id: "omniauth-register-form" }) do |f| %>
    <%= f.text_field :name, help_text: t("decidim.devise.omniauth_registrations.new.username_help"), autocomplete: "name", placeholder: "John Doe" %> @@ -22,6 +22,8 @@ <%= f.email_field :email, autocomplete: "email", placeholder: t("placeholder_email", scope: "decidim.devise.shared") %> + <%= render partial: "decidim/devise/shared/tos_fields", locals: { form: f, user: :user } %> + <%= f.hidden_field :uid %> <%= f.hidden_field :provider %> <%= f.hidden_field :oauth_signature %> @@ -35,3 +37,5 @@
    <% end %> <% end %> + +<%= render "decidim/devise/shared/newsletter_modal" %> diff --git a/decidim-core/app/views/decidim/devise/omniauth_registrations/new_tos_fields.html.erb b/decidim-core/app/views/decidim/devise/omniauth_registrations/new_tos_fields.html.erb new file mode 100644 index 0000000000000..85dbe1cfcf78e --- /dev/null +++ b/decidim-core/app/views/decidim/devise/omniauth_registrations/new_tos_fields.html.erb @@ -0,0 +1,29 @@ +<%= render layout: "layouts/decidim/shared/layout_center" do %> +
    +

    <%= t("decidim.devise.omniauth_registrations.new_tos_fields.sign_up_title") %>

    +
    + + <%= decidim_form_for(@form, namespace: "registration", as: resource_name, url: omniauth_registrations_path(resource_name), html: { id: "omniauth-register-form" }) do |f| %> + +
    + <%= f.hidden_field :name %> + <%= f.hidden_field :nickname %> + <%= f.hidden_field :email %> + + <%= render partial: "decidim/devise/shared/tos_fields", locals: { form: f, user: :user } %> + + <%= f.hidden_field :uid %> + <%= f.hidden_field :provider %> + <%= f.hidden_field :oauth_signature %> +
    + +
    + +
    + <% end %> +<% end %> + +<%= render "decidim/devise/shared/newsletter_modal" %> diff --git a/decidim-core/app/views/decidim/devise/registrations/new.html.erb b/decidim-core/app/views/decidim/devise/registrations/new.html.erb index 17da235460ef2..fa1cb1cb85bd0 100644 --- a/decidim-core/app/views/decidim/devise/registrations/new.html.erb +++ b/decidim-core/app/views/decidim/devise/registrations/new.html.erb @@ -40,22 +40,7 @@ <%= render partial: "decidim/account/password_fields", locals: { form: f, user: :user } %> -
    -

    <%= t("decidim.devise.registrations.new.tos_title") %>

    - -
    - <% terms_of_service_summary_content_blocks.each do |content_block| %> - <%= cell content_block.manifest.cell, content_block %> - <% end %> -
    - - <%= f.check_box :tos_agreement, label: t("decidim.devise.registrations.new.tos_agreement", link: link_to(t("decidim.devise.registrations.new.terms"), page_path("terms-of-service"))), label_options: { class: "form__wrapper-checkbox-label" } %> -
    - -
    -

    <%= t("decidim.devise.registrations.new.newsletter_title") %>

    - <%= f.check_box :newsletter, label: t("decidim.devise.registrations.new.newsletter"), checked: @form.newsletter, label_options: { class: "form__wrapper-checkbox-label" } %> -
    + <%= render partial: "decidim/devise/shared/tos_fields", locals: { form: f, user: :user } %>