diff --git a/.rubocop_rails.yml b/.rubocop_rails.yml index e3067271ee..b0e45aa030 100644 --- a/.rubocop_rails.yml +++ b/.rubocop_rails.yml @@ -109,3 +109,7 @@ RSpec/MultipleMemoizedHelpers: RSpec/AnyInstance: Enabled: false + +RSpec/BeEq: + Exclude: + - spec/events/decidim/proposals/author_confirmation_proposal_event_spec.rb \ No newline at end of file diff --git a/Gemfile b/Gemfile index 4eb3c8f52b..a33f8c0198 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem "decidim-half_signup", git: "https://github.com/OpenSourcePolitics/decidim-m gem "decidim-homepage_interactive_map", git: "https://github.com/OpenSourcePolitics/decidim-module-homepage_interactive_map.git", branch: DECIDIM_BRANCH gem "decidim-phone_authorization_handler", git: "https://github.com/OpenSourcePolitics/decidim-module_phone_authorization_handler", branch: "release/0.27-stable" gem "decidim-simple_proposal", git: "https://github.com/OpenSourcePolitics/decidim-module-simple_proposal", branch: DECIDIM_BRANCH -gem "decidim-spam_detection", git: "https://github.com/OpenSourcePolitics/decidim-spam_detection.git", tag: "4.1.1" +gem "decidim-spam_detection", git: "https://github.com/OpenSourcePolitics/decidim-spam_detection.git", tag: "4.1.2" gem "decidim-survey_multiple_answers", git: "https://github.com/OpenSourcePolitics/decidim-module-survey_multiple_answers" gem "decidim-term_customizer", git: "https://github.com/OpenSourcePolitics/decidim-module-term_customizer.git", branch: "fix/email_with_precompile" diff --git a/Gemfile.lock b/Gemfile.lock index ed7ec8559f..f8408c2dbd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -108,10 +108,10 @@ GIT GIT remote: https://github.com/OpenSourcePolitics/decidim-spam_detection.git - revision: fb2ee4624b728ce6f73603bfb84eda1d9b4e04d4 - tag: 4.1.1 + revision: 5e4f92f19b903228b8349fb002d735e900d63ed4 + tag: 4.1.2 specs: - decidim-spam_detection (4.1.1) + decidim-spam_detection (4.1.2) decidim-core (~> 0.27.0) GIT @@ -1226,4 +1226,4 @@ RUBY VERSION ruby 3.0.6p216 BUNDLED WITH - 2.5.10 + 2.5.22 diff --git a/app/commands/decidim/proposals/publish_proposal.rb b/app/commands/decidim/proposals/publish_proposal.rb new file mode 100644 index 0000000000..a637eaed4f --- /dev/null +++ b/app/commands/decidim/proposals/publish_proposal.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + # A command with all the business logic when a user publishes a draft proposal. + class PublishProposal < Decidim::Command + include Decidim::AnonymousProposals::AnonymousBehaviorCommandsConcern + + # Public: Initializes the command. + # + # proposal - The proposal to publish. + # current_user - The current user. + # override: decidim-module-anonymous_proposals/app/commands/decidim/anonymous_proposals/publish_proposal_command_overrides.rb + def initialize(proposal, current_user) + @proposal = proposal + @is_anonymous = allow_anonymous_proposals? && (current_user.blank? || proposal.authored_by?(anonymous_group)) + set_current_user(current_user) + end + + # Executes the command. Broadcasts these events: + # + # - :ok when everything is valid and the proposal is published. + # - :invalid if the proposal's author is not the current user. + # + # Returns nothing. + def call + return broadcast(:invalid) unless @proposal.authored_by?(@current_user) + + transaction do + publish_proposal + increment_scores + send_notification + send_notification_to_participatory_space + send_publication_notification + end + + broadcast(:ok, @proposal) + end + + private + + # This will be the PaperTrail version that is + # shown in the version control feature (1 of 1) + # + # For an attribute to appear in the new version it has to be reset + # and reassigned, as PaperTrail only keeps track of object CHANGES. + def publish_proposal + title = reset(:title) + body = reset(:body) + + Decidim.traceability.perform_action!( + "publish", + @proposal, + @current_user, + visibility: "public-only" + ) do + @proposal.update title: title, body: body, published_at: Time.current + end + end + + # Reset the attribute to an empty string and return the old value + def reset(attribute) + attribute_value = @proposal[attribute] + PaperTrail.request(enabled: false) do + # rubocop:disable Rails/SkipsModelValidations + @proposal.update_attribute attribute, "" + # rubocop:enable Rails/SkipsModelValidations + end + attribute_value + end + + def send_notification + return if @proposal.coauthorships.empty? + + Decidim::EventsManager.publish( + event: "decidim.events.proposals.proposal_published", + event_class: Decidim::Proposals::PublishProposalEvent, + resource: @proposal, + followers: coauthors_followers + ) + end + + def send_publication_notification + Decidim::EventsManager.publish( + event: "decidim.events.proposals.author_confirmation_proposal_event", + event_class: Decidim::Proposals::AuthorConfirmationProposalEvent, + resource: @proposal, + affected_users: [@proposal.creator_identity], + extra: { force_email: true }, + force_send: true + ) + end + + def send_notification_to_participatory_space + Decidim::EventsManager.publish( + event: "decidim.events.proposals.proposal_published", + event_class: Decidim::Proposals::PublishProposalEvent, + resource: @proposal, + followers: @proposal.participatory_space.followers - coauthors_followers, + extra: { + participatory_space: true + } + ) + end + + def coauthors_followers + @coauthors_followers ||= @proposal.authors.flat_map(&:followers) + end + + def increment_scores + @proposal.coauthorships.find_each do |coauthorship| + if coauthorship.user_group + Decidim::Gamification.increment_score(coauthorship.user_group, :proposals) + else + Decidim::Gamification.increment_score(coauthorship.author, :proposals) + end + end + end + + # override: decidim-module-anonymous_proposals/app/commands/decidim/anonymous_proposals/publish_proposal_command_overrides.rb + def component + @component ||= @proposal.component + end + end + end +end diff --git a/app/controllers/decidim/assemblies/assemblies_controller.rb b/app/controllers/decidim/assemblies/assemblies_controller.rb index 6fa700f10d..c409bde9a2 100644 --- a/app/controllers/decidim/assemblies/assemblies_controller.rb +++ b/app/controllers/decidim/assemblies/assemblies_controller.rb @@ -89,10 +89,11 @@ def assembly_participatory_processes @assembly_participatory_processes ||= @current_participatory_space.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes") else @assembly_participatory_processes = @current_participatory_space.linked_participatory_space_resources(:participatory_processes, "included_participatory_processes") + @active_processes ||= @assembly_participatory_processes.select(&:active?) sorted_by_date = { - active: @assembly_participatory_processes.active_spaces.sort_by(&:end_date), - future: @assembly_participatory_processes.future_spaces.sort_by(&:start_date), - past: @assembly_participatory_processes.past_spaces.sort_by(&:end_date).reverse + active: @active_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + processes_without_end_date(@active_processes), + future: @assembly_participatory_processes.upcoming.sort_by(&:start_date), + past: @assembly_participatory_processes.past.sort_by(&:end_date).reverse } @assembly_participatory_processes = sorted_by_date end @@ -101,6 +102,10 @@ def assembly_participatory_processes def current_assemblies_settings @current_assemblies_settings ||= Decidim::AssembliesSetting.find_or_create_by(decidim_organization_id: current_organization.id) end + + def processes_without_end_date(processes) + processes.select { |process| process.end_date.nil? } + end end end end diff --git a/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb b/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb index e1d755b7a3..72b7cf0f25 100644 --- a/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb +++ b/app/controllers/decidim/participatory_processes/participatory_processes_controller.rb @@ -136,7 +136,7 @@ def linked_assemblies def custom_sort(date) case date when "active" - @participatory_processes.sort_by(&:end_date) + @participatory_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + processes_without_end_date(@participatory_processes) when "past" @participatory_processes.sort_by(&:end_date).reverse when "upcoming" @@ -149,11 +149,16 @@ def custom_sort(date) end def sort_all_processes - actives = @participatory_processes.select(&:active?).sort_by(&:end_date) + @actives_processes ||= @participatory_processes.select(&:active?) + actives = @actives_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + processes_without_end_date(@actives_processes) pasts = @participatory_processes.select(&:past?).sort_by(&:end_date).reverse upcomings = @participatory_processes.select(&:upcoming?).sort_by(&:start_date) (actives + upcomings + pasts) end + + def processes_without_end_date(processes) + processes.select { |process| process.end_date.nil? } + end end end end diff --git a/app/events/decidim/proposals/author_confirmation_proposal_event.rb b/app/events/decidim/proposals/author_confirmation_proposal_event.rb new file mode 100644 index 0000000000..22745650a4 --- /dev/null +++ b/app/events/decidim/proposals/author_confirmation_proposal_event.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Decidim + module Proposals + class AuthorConfirmationProposalEvent < Decidim::Events::SimpleEvent + def self.model_name + ActiveModel::Name.new(self, nil, I18n.t("decidim.events.proposals.author_confirmation_proposal_event.email_subject")) + end + + def resource_title + translated_attribute(resource.title) + end + end + end +end diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index be5b5b9147..fef7c18f22 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -164,5 +164,8 @@ ignore_unused: - decidim.newsletters.unsubscribe.error - decidim.newsletters.unsubscribe.token_error - decidim.half_signup.quick_auth.sms_verification.text_message + - decidim.events.proposals.author_confirmation_proposal_event.email_intro + - decidim.events.proposals.author_confirmation_proposal_event.email_outro + - decidim.events.proposals.author_confirmation_proposal_event.notification_title diff --git a/config/locales/en.yml b/config/locales/en.yml index e0f8a670f8..87a51ec503 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -95,6 +95,12 @@ en: email_outro: You have received this notification because you are following the initiative "%{resource_title}". email_subject: Initiative "%{resource_title}" has been answered notification_title: The initiative %{resource_title} has been answered. + proposals: + author_confirmation_proposal_event: + email_intro: 'Your proposal " %{resource_title} " was successfully received and is now public. Thank you for participating ! You can view it here:' + email_outro: You received this notification because you are the author of the proposal. You can unfollow it by visiting the proposal page (" %{resource_title} ") and clicking on " Unfollow ". + email_subject: Your proposal has been published! + notification_title: Your proposal %{resource_title} is now live. users: user_officialized: email_intro: Participant %{name} (%{nickname}) has been officialized. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ee98ffd1d8..71410c5096 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -97,6 +97,12 @@ fr: email_outro: Vous avez reçu cette notification parce que vous suivez la pétition "%{resource_title}". email_subject: La pétition "%{resource_title}" a reçu une réponse. notification_title: La pétition %{resource_title} a reçu une réponse. + proposals: + author_confirmation_proposal_event: + email_intro: 'Votre proposition « %{resource_title} » a été reçue avec succès et est maintenant publique. Merci pour votre participation ! Vous pouvez la consulter ici :' + email_outro: Vous recevez cette notification car vous êtes l’auteur de la proposition. Vous pouvez vous désabonner en visitant la page de la proposition (« %{resource_title} ») et en cliquant sur « Ne plus suivre ». + email_subject: Votre proposition a été publiée ! + notification_title: Votre proposition %{resource_title} est maintenant en ligne. users: user_officialized: email_intro: Le participant %{name} (%{nickname}) a été officialisé. diff --git a/spec/controllers/assemblies_controller_spec.rb b/spec/controllers/assemblies_controller_spec.rb index f28fd0e2c6..5d90dee6a5 100644 --- a/spec/controllers/assemblies_controller_spec.rb +++ b/spec/controllers/assemblies_controller_spec.rb @@ -146,11 +146,12 @@ module Assemblies context "when sort_by_date variable is true" do before do allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(true) + active_processes.first.update(end_date: nil) end - it "includes only participatory processes related to the assembly, actives one by end_date then upcoming ones by start_date then past ones by end_date reversed" do + it "includes only participatory processes related to the assembly, actives one by end_date with end_date nil last, then upcoming ones by start_date then past ones by end_date reversed" do sorted_participatory_processes = { - active: participatory_processes.select(&:active?).sort_by(&:end_date), + active: participatory_processes.select { |process| process.active? && !process.end_date.nil? }.sort_by(&:end_date) + participatory_processes.select { |process| process.active? && process.end_date.nil? }, future: participatory_processes.select(&:upcoming?).sort_by(&:start_date), past: participatory_processes.select(&:past?).sort_by(&:end_date).reverse } diff --git a/spec/controllers/participatory_processes_controller_spec.rb b/spec/controllers/participatory_processes_controller_spec.rb index 52d34d206b..977ccc12b6 100644 --- a/spec/controllers/participatory_processes_controller_spec.rb +++ b/spec/controllers/participatory_processes_controller_spec.rb @@ -137,11 +137,12 @@ module ParticipatoryProcesses context "and sort_by_date is true" do before do allow(Rails.application.secrets).to receive(:dig).with(:decidim, :participatory_processes, :sort_by_date).and_return(true) + active_processes.first.update(end_date: nil) end # search.with_date will default to "active" it "orders active processes by end date" do - expect(controller.helpers.participatory_processes).to eq(active_processes.sort_by(&:end_date)) + expect(controller.helpers.participatory_processes).to eq(active_processes.reject { |process| process.end_date.nil? }.sort_by(&:end_date) + active_processes.select { |process| process.end_date.nil? }) end end end