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