diff --git a/app/decorators/decidim/admin/import/importer_decorator.rb b/app/decorators/decidim/admin/import/importer_decorator.rb new file mode 100644 index 00000000..c7801230 --- /dev/null +++ b/app/decorators/decidim/admin/import/importer_decorator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Decidim::Admin::Import::ImporterDecorator + def self.decorate + Decidim::Admin::Import::Importer.class_eval do + def import! + finished_collection = collection.map { |elem| object_to_manage_without_notify_uniquely?(elem) ? elem.finish_without_notif! : elem.finish! } + notify_collection(finished_collection, collection.first) + end + + def notify_collection(collection, klass) + recipients = collection.flat_map { |elem| elem.participatory_space.followers.where(notification_types: %w(all followed-only)).uniq }.uniq + case klass + when Decidim::Proposals::Import::ProposalCreator + recipients.each { |recipient| ProposalsMailer.notify_massive_import(collection, recipient).deliver_later } + when Decidim::Proposals::Import::ProposalAnswerCreator + recipients.each { |recipient| ProposalsAnswersMailer.notify_massive_import(collection, recipient).deliver_later } + end + end + + def object_to_manage_without_notify_uniquely?(obj) + # These types of object must be notified in group, not uniquely + [Decidim::Proposals::Import::ProposalCreator, Decidim::Proposals::Import::ProposalAnswerCreator].include? obj.class + end + end + end +end + +::Decidim::Admin::Import::ImporterDecorator.decorate diff --git a/app/decorators/decidim/proposals/import/proposal_answer_creator_decorator.rb b/app/decorators/decidim/proposals/import/proposal_answer_creator_decorator.rb new file mode 100644 index 00000000..37a6f0aa --- /dev/null +++ b/app/decorators/decidim/proposals/import/proposal_answer_creator_decorator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Decidim::Proposals::Import::ProposalAnswerCreatorDecorator + def self.decorate + Decidim::Proposals::Import::ProposalAnswerCreator.class_eval do + def finish_without_notif! + Decidim.traceability.perform_action!( + "answer", + resource, + current_user + ) do + resource.try(:save!) + end + resource + end + end + end +end + +Decidim::Proposals::Import::ProposalAnswerCreatorDecorator.decorate diff --git a/app/decorators/decidim/proposals/import/proposal_creator_decorator.rb b/app/decorators/decidim/proposals/import/proposal_creator_decorator.rb new file mode 100644 index 00000000..558aa0a0 --- /dev/null +++ b/app/decorators/decidim/proposals/import/proposal_creator_decorator.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Decidim::Proposals::Import::ProposalCreatorDecorator + def self.decorate + Decidim::Proposals::Import::ProposalCreator.class_eval do + def finish_without_notif! + Decidim.traceability.perform_action!(:create, self.class.resource_klass, context[:current_user], visibility: "admin-only") do + resource.save! + resource + end + resource + end + end + end +end + +::Decidim::Proposals::Import::ProposalCreatorDecorator.decorate diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 5cc63a0c..61d626b2 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" + default from: Decidim.mailer_sender layout "mailer" end diff --git a/app/mailers/proposals_answers_mailer.rb b/app/mailers/proposals_answers_mailer.rb new file mode 100644 index 00000000..47aae758 --- /dev/null +++ b/app/mailers/proposals_answers_mailer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ProposalsAnswersMailer < ApplicationMailer + include Decidim::SanitizeHelper + include Decidim::ComponentPathHelper + + layout "decidim/mailer" + + def notify_massive_import(answers_collection, user) + @organization = user.organization + first_answer = answers_collection.first + @participatory_space_title = decidim_sanitize_translated(first_answer.participatory_space.title) if first_answer.present? + @user = user + @component_url = manage_component_url(first_answer.component) + I18n.locale = user.locale if user.locale.present? + + subject = I18n.t("proposals_answers_imported.email_subject", scope: "decidim.events.proposals", participatory_space_title: @participatory_space_title) + mail(to: user.email, subject: subject) + end + + private + + def manage_component_url(component) + Decidim::EngineRouter.main_proxy(component).root_url + end +end diff --git a/app/mailers/proposals_mailer.rb b/app/mailers/proposals_mailer.rb new file mode 100644 index 00000000..100f6979 --- /dev/null +++ b/app/mailers/proposals_mailer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ProposalsMailer < ApplicationMailer + include Decidim::SanitizeHelper + include Decidim::ComponentPathHelper + + layout "decidim/mailer" + + def notify_massive_import(proposals_collection, user) + @organization = user.organization + first_proposal = proposals_collection.first + @participatory_space_title = decidim_sanitize_translated(first_proposal.participatory_space.title) if first_proposal.present? + @user = user + @component_url = manage_component_url(first_proposal.component) + I18n.locale = user.locale if user.locale.present? + + subject = I18n.t("proposals_imported.email_subject", scope: "decidim.events.proposals", participatory_space_title: @participatory_space_title) + mail(to: user.email, subject: subject) + end + + private + + def manage_component_url(component) + Decidim::EngineRouter.main_proxy(component).root_url + end +end diff --git a/app/views/proposals_answers_mailer/notify_massive_import.html.erb b/app/views/proposals_answers_mailer/notify_massive_import.html.erb new file mode 100644 index 00000000..b006060b --- /dev/null +++ b/app/views/proposals_answers_mailer/notify_massive_import.html.erb @@ -0,0 +1,4 @@ +

<%= I18n.t("decidim.events.email_event.email_greeting", user_name: @user.name) %>

+

<%= I18n.t("decidim.events.proposals.proposals_answers_imported.email_intro", participatory_space_title: @participatory_space_title).html_safe %>

+

<%= I18n.t("decidim.events.proposals.proposals_answers_imported.email_link", participatory_space_url: @component_url).html_safe %>

+

<%= I18n.t("decidim.events.proposals.proposals_answers_imported.email_outro", participatory_space_title: @participatory_space_title) %>

diff --git a/app/views/proposals_mailer/notify_massive_import.html.erb b/app/views/proposals_mailer/notify_massive_import.html.erb new file mode 100644 index 00000000..c08dd09c --- /dev/null +++ b/app/views/proposals_mailer/notify_massive_import.html.erb @@ -0,0 +1,4 @@ +

<%= I18n.t("decidim.events.email_event.email_greeting", user_name: @user.name) %>

+

<%= I18n.t("decidim.events.proposals.proposals_imported.email_intro", participatory_space_title: @participatory_space_title).html_safe %>

+

<%= I18n.t("decidim.events.proposals.proposals_imported.email_link", participatory_space_url: @component_url).html_safe %>

+

<%= I18n.t("decidim.events.proposals.proposals_imported.email_outro", participatory_space_title: @participatory_space_title) %>

diff --git a/config/locales/ca_proposals.yml b/config/locales/ca_proposals.yml index 1dad909e..3e4a0e9c 100644 --- a/config/locales/ca_proposals.yml +++ b/config/locales/ca_proposals.yml @@ -14,6 +14,18 @@ ca: step: endorsements_blocked: Adhesions bloquejades endorsements_enabled: Adhesions habilitades + events: + proposals: + proposals_answers_imported: + email_intro: S'ha donat resposta a propostes de l'espai "%{participatory_space_title}". + email_link: "Pots llegir les respostes des d'aquesta pàgina: enllaç." + email_outro: Has rebut aquesta notificació perquè estas seguint l'espai "%{participatory_space_title}". Pots deixar de rebre notificacions seguint l'enllaç anterior. + email_subject: Noves respostes a propostes de %{participatory_space_title} + proposals_imported: + email_intro: S'han afegit noves propostes a l'espai "%{participatory_space_title}". + email_link: Pots veure les noves propostes seguint aquest enllaç. + email_outro: Has rebut aquesta notificació perquè estas seguint l'espai "%{participatory_space_title}". Pots deixar de rebre notificacions seguint l'enllaç anterior. + email_subject: Noves propostes afegides a %{participatory_space_title} proposals: application_helper: filter_type_values: diff --git a/config/locales/en_proposals.yml b/config/locales/en_proposals.yml index c2f1d27f..9003c059 100644 --- a/config/locales/en_proposals.yml +++ b/config/locales/en_proposals.yml @@ -1,6 +1,18 @@ --- en: decidim: + events: + proposals: + proposals_answers_imported: + email_intro: Proposals have been answered in the "%{participatory_space_title}" space. + email_link: "You can read the answers from this page: link." + email_outro: You have received this notification because you are following "%{participatory_space_title}". You can unfollow it from the previous link. + email_subject: New responses to proposals in %{participatory_space_title} + proposals_imported: + email_intro: New proposals has been added to "%{participatory_space_title}". + email_link: You can see the new proposals by following this link. + email_outro: You have received this notification because you are following "%{participatory_space_title}". You can unfollow it from the previous link. + email_subject: New proposals added to %{participatory_space_title} proposals: proposals: show: diff --git a/config/locales/es_proposals.yml b/config/locales/es_proposals.yml index d75a5ba1..4ee4cabe 100644 --- a/config/locales/es_proposals.yml +++ b/config/locales/es_proposals.yml @@ -1,7 +1,19 @@ --- es: decidim: + events: + proposals: + proposals_answers_imported: + email_intro: Se ha dado respuesta a propuestas del espacio "%{participatory_space_title}". + email_link: "Puedes leer las respuestas desde esta página: enlace." + email_outro: Has recibido esta notificación porque estas siguiendo el espacio "%{participatory_space_title}". Puedes dejar de recibir notificaciones siguiendo el enlace anterior. + email_subject: Nuevas respuestas a propuestas de %{participatory_space_title} + proposals_imported: + email_intro: Se han añadido nuevas propuestas al espacio "%{participatory_space_title}". + email_link: Puedes ver las nuevas propuestas siguiendo este enlace. + email_outro: Has recibido esta notificación porque estas siguiendo el espacio "%{participatory_space_title}". Puedes dejar de recibir notificaciones siguiendo el enlace anterior. + email_subject: Nuevas propuestas añadidas a %{participatory_space_title} proposals: proposals: show: - proposal_in_evaluation_reason: 'Esta propuesta está en evaluación porque:' \ No newline at end of file + proposal_in_evaluation_reason: 'Esta propuesta está en evaluación porque:' diff --git a/config/locales/oc_proposals.yml b/config/locales/oc_proposals.yml index 8cfbb685..14c972ef 100644 --- a/config/locales/oc_proposals.yml +++ b/config/locales/oc_proposals.yml @@ -255,6 +255,16 @@ oc: email_outro: Has rebut aquesta notificació perquè estàs seguint "%{participatory_space_title}". Pots deixar de rebre notificacions seguint l'enllaç anterior. email_subject: Nova proposta "%{resource_title}" afegida a %{participatory_space_title} notification_title: La proposta %{resource_title} s'ha afegit a %{participatory_space_title} + proposals_answers_imported: + email_intro: S'a dat responsa a prepauses der espaci "%{participatory_space_title}". + email_link: "Pòdes liéger es responses dempús d'aguesta pagina: ligam." + email_outro: As arrecebut aguesta notificacion pr'amor qu'estas en tot seguir er espaci "%{participatory_space_title}". Pòdes quitar d'arrecéber notificacions en tot seguir eth ligam anterior. + email_subject: Naues responses a prepauses en %{participatory_space_title} + proposals_imported: + email_intro: S'han ahijut naues prepauses ar espaci "%{participatory_space_title}", que seguisses. + email_link: Pòdes veir es naues prepauses en tot seguir aguest lligam. + email_outro: As arrecebut aguesta notificacion pr'amor qu'estas en tot seguir er espaci "%{participatory_space_title}". Pòdes quitar d'arrecéber notificacions en tot seguir eth ligam anterior. + email_subject: Naues prepauses ahijudes en %{participatory_space_title} proposal_rejected: affected_user: email_intro: 'La teva proposta "%{resource_title}" ha estat rebutjada. Pots llegir la resposta en aquesta pàgina:' diff --git a/db/schema.rb b/db/schema.rb index bbf0c17d..990a504c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -198,9 +198,8 @@ t.string "facebook_handler" t.string "youtube_handler" t.string "github_handler" - t.bigint "decidim_assemblies_type_id" t.boolean "destacat", default: false - t.boolean "show_home", default: false + t.bigint "decidim_assemblies_type_id" t.integer "weight", default: 1, null: false t.integer "follows_count", default: 0, null: false t.jsonb "announcement" diff --git a/decidim-process-extended/config/locales/ca.yml b/decidim-process-extended/config/locales/ca.yml index 6eaaab98..06d6e10f 100644 --- a/decidim-process-extended/config/locales/ca.yml +++ b/decidim-process-extended/config/locales/ca.yml @@ -47,6 +47,9 @@ ca: left: Restant title: Té %{limit} suports a distribuir votes: Suports + events: + publish_proposals_event: + email_intro: S'han afegit noves propostes a l'espai "%{resource_title}". Pots veure les noves propostes seguint aquest enllaç. activemodel: attributes: participatory_process: diff --git a/decidim-process-extended/config/locales/en.yml b/decidim-process-extended/config/locales/en.yml index bbbebcd5..9be5d4d9 100644 --- a/decidim-process-extended/config/locales/en.yml +++ b/decidim-process-extended/config/locales/en.yml @@ -47,6 +47,9 @@ en: left: Left title: You have %{limit} supports to distribute votes: Votes + events: + publish_proposals_event: + email_intro: New proposals has been added to "%{resource_title}". You can see the new proposals by following this link. activemodel: attributes: participatory_process: diff --git a/decidim-process-extended/config/locales/es.yml b/decidim-process-extended/config/locales/es.yml index 86d2c1d2..80c62554 100644 --- a/decidim-process-extended/config/locales/es.yml +++ b/decidim-process-extended/config/locales/es.yml @@ -47,6 +47,9 @@ es: left: Restante title: Tiene %{limit} soportes a distribuir votes: Soportes + events: + publish_proposals_event: + email_intro: Se han añadido nuevas propuestas al espacio "%{resource_title}. Puedes ver las nuevas propuestas siguiendo este enlace. activemodel: attributes: participatory_process: diff --git a/decidim-process-extended/config/locales/oc.yml b/decidim-process-extended/config/locales/oc.yml index b87100e3..4a1fc7a0 100644 --- a/decidim-process-extended/config/locales/oc.yml +++ b/decidim-process-extended/config/locales/oc.yml @@ -27,6 +27,9 @@ oc: application: attachments: meetings_available: Hi ha trobades programades actualment + events: + publish_proposals_event: + email_intro: S'han ahijut naues prepauses ar espaci "%{participatory_space_title}", que seguisses. Pòdes veir es naues prepauses en tot seguir aguest lligam. activemodel: attributes: participatory_process: diff --git a/lib/assets/import_answers.csv b/lib/assets/import_answers.csv new file mode 100644 index 00000000..c7b6e0d1 --- /dev/null +++ b/lib/assets/import_answers.csv @@ -0,0 +1,4 @@ +id;state;answer/ca;answer/es;answer/oc;answer/en +89117;accepted;Example answer;Example answer;Example answer;Example answer +89117;rejected;Example answer;Example answer;Example answer;Example answer +89117;evaluating;Example answer;Example answer;Example answer;Example answer diff --git a/spec/decorators/decidim/admin/import/importer_decorator_spec.rb b/spec/decorators/decidim/admin/import/importer_decorator_spec.rb new file mode 100644 index 00000000..39822115 --- /dev/null +++ b/spec/decorators/decidim/admin/import/importer_decorator_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "rails_helper" +require "decidim/proposals/import/proposal_creator_decorator" + +describe Decidim::Admin::Import::Importer do + subject { described_class.new(file: blob, reader: reader, creator: creator, context: context) } + + let(:organization) { create(:organization, available_locales: [:en]) } + let(:user) { create(:user, organization: organization) } + let(:follower) { create(:user, organization: organization, notification_types: "all") } + let(:context) do + { + current_organization: organization, + current_user: user, + current_component: current_component, + current_participatory_space: participatory_process + } + end + let(:participatory_process) { create :participatory_process, organization: organization } + let(:current_component) { create :component, manifest_name: :proposals, participatory_space: participatory_process } + let(:reader) { Decidim::Admin::Import::Readers::CSV } + + describe "#notify_collection" do + context "when imported collection are proposals" do + let(:creator) { Decidim::Proposals::Import::ProposalCreator } + let(:blob) { upload_test_file(Decidim::Dev.asset("import_proposals.csv"), return_blob: true) } + + before do + participatory_process.followers << follower + ActionMailer::Base.deliveries.clear + allow(ProposalsMailer).to receive(:notify_massive_import).and_call_original + end + + it_behaves_like "proposal importer" + + it "will call ProposalsMailer.notify_massive_import with the collection" do + subject.prepare + subject.import! + expect(ProposalsMailer).to have_received(:notify_massive_import) + end + end + + context "when imported collection are answers proposals" do + let(:creator) { Decidim::Proposals::Import::ProposalAnswerCreator } + let(:blob) { upload_test_file(Rails.root.join("lib", "assets", "import_answers.csv").to_s, return_blob: true) } + + before do + participatory_process.followers << follower + create(:proposal, :evaluating, component: current_component, id: 89_117) + ActionMailer::Base.deliveries.clear + allow(ProposalsAnswersMailer).to receive(:notify_massive_import).and_call_original + end + + it "will call ProposalsMailer.notify_massive_import with the collection" do + subject.prepare + subject.import! + expect(ProposalsAnswersMailer).to have_received(:notify_massive_import) + end + end + end +end diff --git a/spec/decorators/decidim/proposals/import/proposal_answer_creator_spec.rb b/spec/decorators/decidim/proposals/import/proposal_answer_creator_spec.rb new file mode 100644 index 00000000..ba7bb9e1 --- /dev/null +++ b/spec/decorators/decidim/proposals/import/proposal_answer_creator_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "rails_helper" +require "decidim/proposals/import/proposal_answer_creator" + +describe Decidim::Proposals::Import::ProposalAnswerCreator do + subject { described_class.new(data, context) } + + let(:proposal) { create(:proposal, state: state, component: component) } + let!(:moment) { Time.current } + let(:data) do + { + id: proposal.id, + state: state, + "answer/en": Faker::Lorem.paragraph + } + end + let(:organization) { create(:organization, available_locales: [:en]) } + let(:user) { create(:user, organization: organization) } + let(:context) do + { + current_organization: organization, + current_user: user, + current_component: component, + current_participatory_space: participatory_process + } + end + let(:participatory_process) { create :participatory_process, organization: organization } + let(:component) { create :component, manifest_name: :proposals, participatory_space: participatory_process } + let(:state) { %w(evaluating accepted rejected).sample } + + describe "#finish_without_notif!" do + it "saves the answer proposal" do + record = subject.produce + subject.finish! + expect(record.new_record?).to be(false) + end + + it "creates an admin log record" do + record = subject.produce + subject.finish! + + log = Decidim::ActionLog.last + expect(log.resource).to eq(record) + expect(log.action).to eq("answer") + end + + context "when proposal state changes" do + let!(:proposal) { create(:proposal, :evaluating, component: component) } + let(:state) { "accepted" } + + it "returns broadcast :ok" do + expect(subject.finish!).to eq({ ok: [] }) + end + end + end +end diff --git a/spec/decorators/decidim/proposals/import/proposal_creator_decorator_spec.rb b/spec/decorators/decidim/proposals/import/proposal_creator_decorator_spec.rb new file mode 100644 index 00000000..98ee9698 --- /dev/null +++ b/spec/decorators/decidim/proposals/import/proposal_creator_decorator_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "rails_helper" +require "decidim/proposals/import/proposal_creator" + +describe Decidim::Proposals::Import::ProposalCreator do + subject { described_class.new(data, context) } + + let!(:moment) { Time.current } + let(:data) do + { + id: 1337, + category: category, + scope: scope, + "title/en": Faker::Lorem.sentence, + "body/en": Faker::Lorem.paragraph(sentence_count: 3), + address: "#{Faker::Address.street_name}, #{Faker::Address.city}", + latitude: Faker::Address.latitude, + longitude: Faker::Address.longitude, + component: component, + published_at: moment + } + end + let(:organization) { create(:organization, available_locales: [:en]) } + let(:user) { create(:user, organization: organization) } + let(:context) do + { + current_organization: organization, + current_user: user, + current_component: component, + current_participatory_space: participatory_process + } + end + let(:participatory_process) { create :participatory_process, organization: organization } + let(:component) { create :component, manifest_name: :proposals, participatory_space: participatory_process } + let(:scope) { create :scope, organization: organization } + let(:category) { create :category, participatory_space: participatory_process } + + describe "#finish_without_notif!" do + context "when a proposals file are created" do + it "saves the proposal" do + record = subject.produce + subject.finish! + expect(record.new_record?).to be(false) + end + + it "creates admin log" do + record = subject.produce + expect { subject.finish! }.to change(Decidim::ActionLog, :count).by(1) + expect(Decidim::ActionLog.last.user).to eq(user) + expect(Decidim::ActionLog.last.resource).to eq(record) + expect(Decidim::ActionLog.last.visibility).to eq("admin-only") + end + end + end +end diff --git a/spec/shared/proposal_importer_examples.rb b/spec/shared/proposal_importer_examples.rb new file mode 100644 index 00000000..29345e7b --- /dev/null +++ b/spec/shared/proposal_importer_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +shared_context "with proposal import data" do + let(:expected_data) do + [ + { + title: { + "en" => "Esse qui. Ut." + }, + body: { + "en" => "Aliquid in ut. Laboriosam consequatur consequatur. Unde dolorem omnis. +Et earum aut. Quis enim quis. Dolore corporis et. Quia vel ex." + }, + component: current_component + }, + { + title: { + "en" => "Nihil id." + }, + body: { + "en" => "Atque qui aut. Quia et incidunt. Qui nihil dolore. +Delectus asperiores nihil. Sapiente omnis culpa. Eos at voluptatem." + }, + component: current_component + }, + { + title: { + "en" => "Suspendisse lobortis" + }, + body: { + "en" => "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + }, + component: current_component + } + ] + end +end + +shared_examples "proposal importer" do + include_context "with proposal import data" + + describe "#collection" do + it "creates a collection of creators" do + expect(subject.collection.length).to be(3) + + # Check that produced data matches with expected data + expected_data.each_with_index do |data, index| + data.each do |key, value| + expect(subject.collection[index].produce.send(key)).to eq(value) + end + end + end + end +end