diff --git a/app/events/decidim/initiatives/answer_initiative_event.rb b/app/events/decidim/initiatives/answer_initiative_event.rb new file mode 100644 index 0000000000..69940dec2e --- /dev/null +++ b/app/events/decidim/initiatives/answer_initiative_event.rb @@ -0,0 +1,8 @@ +# frozen-string_literal: true + +module Decidim + module Initiatives + class AnswerInitiativeEvent < Decidim::Events::SimpleEvent + end + end +end diff --git a/config/application.rb b/config/application.rb index 0872b7afb3..ac2b3c6f06 100644 --- a/config/application.rb +++ b/config/application.rb @@ -47,6 +47,8 @@ class Application < Rails::Application require "extends/controllers/decidim/devise/sessions_controller_extends" require "extends/controllers/decidim/editor_images_controller_extends" require "extends/services/decidim/iframe_disabler_extends" + require "extends/helpers/decidim/icon_helper_extends" + require "extends/commands/decidim/initiatives/admin/update_initiative_answer_extends" Decidim::GraphiQL::Rails.config.tap do |config| config.initial_query = "{\n deployment {\n version\n branch\n remote\n upToDate\n currentCommit\n latestCommit\n locallyModified\n }\n}".html_safe diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index befbb25417..89d9ee47b4 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -127,4 +127,6 @@ ignore_unused: - decidim.proposals.collaborative_drafts.new.* - decidim.admin.menu.admin_accountability - decidim.anonymous_user + - decidim.events.initiatives.initiative_answered.* - decidim.initiatives.pages.home.highlighted_initiatives.* + diff --git a/config/locales/en.yml b/config/locales/en.yml index 25bdb8f15d..543840c3ce 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,6 +48,12 @@ en: email_outro: You have received this notification because you are participating in "%{participatory_space_title}" email_subject: Your vote is still pending in %{participatory_space_title} notification_title: The vote on budget %{resource_title} is still waiting for your confirmation in %{participatory_space_title} + initiatives: + initiative_answered: + email_intro: The initiative "%{resource_title}" has been answered. + 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. users: user_officialized: email_intro: Participant %{name} (%{nickname}) has been officialized. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 92b7b6c56e..93f277f432 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -50,6 +50,12 @@ fr: email_outro: Vous avez reçu cette notification parce que vous avez commencé à voter sur la concertation "%{participatory_space_title}" email_subject: Votre vote est toujours en attente sur la concertation %{participatory_space_title} notification_title: Votre vote pour le budget %{resource_title} attend d'être finalisé sur la concertation %{participatory_space_title} + initiatives: + initiative_answered: + email_intro: La pétition "%{resource_title}" a reçu une réponse. + 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. users: user_officialized: email_intro: Le participant %{name} (%{nickname}) a été officialisé. diff --git a/lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb b/lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb new file mode 100644 index 0000000000..aced25d9b7 --- /dev/null +++ b/lib/extends/commands/decidim/initiatives/admin/update_initiative_answer_extends.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module UpdateInitiativeAnswerExtends + def call + return broadcast(:invalid) if form.invalid? + + @initiative = Decidim.traceability.update!( + initiative, + current_user, + attributes + ) + notify_initiative_is_extended if @notify_extended + notify_initiative_is_answered if @notify_answered + broadcast(:ok, initiative) + rescue ActiveRecord::RecordInvalid + broadcast(:invalid, initiative) + end + + private + + def attributes + attrs = { + answer: form.answer, + answer_url: form.answer_url + } + + attrs[:answered_at] = Time.current if form.answer.present? + + if form.signature_dates_required? + attrs[:signature_start_date] = form.signature_start_date + attrs[:signature_end_date] = form.signature_end_date + + if initiative.published? && form.signature_end_date != initiative.signature_end_date && + form.signature_end_date > initiative.signature_end_date + @notify_extended = true + end + end + + @notify_answered = form.answer != initiative.answer && !form.answer.values.all?(&:blank?) + + attrs + end + + def notify_initiative_is_answered + Decidim::EventsManager.publish( + event: "decidim.events.initiatives.initiative_answered", + event_class: Decidim::Initiatives::AnswerInitiativeEvent, + resource: initiative, + followers: initiative.followers + ) + end +end + +Decidim::Initiatives::Admin::UpdateInitiativeAnswer.class_eval do + prepend UpdateInitiativeAnswerExtends +end diff --git a/lib/extends/helpers/decidim/icon_helper_extends.rb b/lib/extends/helpers/decidim/icon_helper_extends.rb new file mode 100644 index 0000000000..b66490303c --- /dev/null +++ b/lib/extends/helpers/decidim/icon_helper_extends.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module IconHelperExtends + def resource_icon(resource, options = {}) + if resource.instance_of?(Decidim::Initiative) + icon "initiatives", options + elsif resource.instance_of?(Decidim::Comments::Comment) + icon "comment-square", options + elsif resource.respond_to?(:component) && resource.component + component_icon(resource.component, options) + elsif resource.respond_to?(:manifest) && resource.manifest + manifest_icon(resource.manifest, options) + elsif resource.is_a?(Decidim::User) + icon "person", options + else + icon "bell", options + end + end +end + +Decidim::IconHelper.module_eval do + prepend(IconHelperExtends) +end diff --git a/spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb b/spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb new file mode 100644 index 0000000000..81f87c8820 --- /dev/null +++ b/spec/commands/decidim/initiatives/admin/update_initiative_answer_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Initiatives + module Admin + describe UpdateInitiativeAnswer do + let(:form_klass) { Decidim::Initiatives::Admin::InitiativeAnswerForm } + + context "when valid data" do + it_behaves_like "update an initiative answer" do + context "when the user is an admin" do + let!(:current_user) { create(:user, :admin, organization: initiative.organization) } + let!(:follower) { create(:user, organization: organization) } + let!(:follow) { create(:follow, followable: initiative, user: follower) } + + it "notifies the followers for extension and answer" do + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.initiatives.initiative_extended", + event_class: Decidim::Initiatives::ExtendInitiativeEvent, + resource: initiative, + followers: [follower] + ) + .ordered + expect(Decidim::EventsManager) + .to receive(:publish) + .with( + event: "decidim.events.initiatives.initiative_answered", + event_class: Decidim::Initiatives::AnswerInitiativeEvent, + resource: initiative, + followers: [follower] + ) + .ordered + + command.call + end + + context "when the signature end time is not modified" do + let(:signature_end_date) { initiative.signature_end_date } + + it "doesn't notify the followers" do + expect(Decidim::EventsManager).not_to receive(:publish).with( + event: "decidim.events.initiatives.initiative_extended", + event_class: Decidim::Initiatives::ExtendInitiativeEvent, + resource: initiative, + followers: [follower] + ) + + command.call + end + end + end + end + end + + context "when validation failure" do + let(:organization) { create(:organization) } + let!(:initiative) { create(:initiative, organization: organization) } + let!(:form) do + form_klass + .from_model(initiative) + .with_context(current_organization: organization, initiative: initiative) + end + + let(:command) { described_class.new(initiative, form, initiative.author) } + + it "broadcasts invalid" do + expect(initiative).to receive(:valid?) + .at_least(:once) + .and_return(false) + expect { command.call }.to broadcast :invalid + end + end + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 16258e4b03..3a7bdad7e9 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -12,3 +12,4 @@ require "decidim/verifications/test/factories" require "decidim/forms/test/factories" require "decidim/surveys/test/factories" +require "decidim/initiatives/test/factories" diff --git a/spec/factory_bot_spec.rb b/spec/factory_bot_spec.rb deleted file mode 100644 index d478a75b7f..0000000000 --- a/spec/factory_bot_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe FactoryBot, processing_uploads_for: Decidim::AttachmentUploader do - it "has 100% valid factories" do - expect { described_class.lint(traits: true) }.not_to raise_error - end -end diff --git a/spec/helpers/icon_helper_spec.rb b/spec/helpers/icon_helper_spec.rb new file mode 100644 index 0000000000..6596b11947 --- /dev/null +++ b/spec/helpers/icon_helper_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + describe IconHelper do + describe "#component_icon" do + let(:component) do + create(:component, manifest_name: :dummy) + end + + describe "when the component has no icon" do + before do + allow(component.manifest).to receive(:icon).and_return(nil) + end + + it "returns a fallback" do + result = helper.component_icon(component) + expect(result).to include("question-mark") + end + end + + describe "when the component has icon" do + it "returns the icon" do + result = helper.component_icon(component) + expect(result).to eq <<~SVG.strip + + SVG + end + + context "with role attribute specified" do + it "implements role attribute" do + result = helper.component_icon(component, role: "img") + expect(result).to eq <<~SVG.strip + + SVG + end + end + + context "with no role attribute specified" do + it "doesn't implement role attribute" do + result = helper.component_icon(component) + expect(result).to eq <<~SVG.strip + + SVG + end + end + end + + describe "resource_icon" do + let(:result) { helper.resource_icon(resource) } + + context "when it has a component" do + let(:resource) { build :dummy_resource } + + it "renders the component icon" do + expect(helper).to receive(:component_icon).with(resource.component, {}) + + result + end + end + + context "when it has a manifest" do + let(:resource) { build(:component, manifest_name: :dummy) } + + it "renders the manifest icon" do + expect(helper).to receive(:manifest_icon).with(resource.manifest, {}) + + result + end + end + + context "when it is a user" do + let(:resource) { build :user } + + it "renders a person icon" do + expect(result).to include("svg#icon-person") + end + end + + context "when the resource component and manifest are nil" do + let(:resource) { build :dummy_resource } + + before do + allow(resource).to receive(:component).and_return(nil) + end + + it "renders a generic icon" do + expect(result).to include("svg#icon-bell") + end + end + + context "when the manifest icon is nil" do + let(:resource) { build(:component, manifest_name: :dummy) } + + before do + allow(resource.manifest).to receive(:icon).and_return(nil) + end + + it "renders a generic icon" do + expect(result).to include("svg#icon-question-mark") + end + end + + context "when the resource is a comment" do + let(:resource) { build :comment } + + it "renders a comment icon" do + expect(result).to include("svg#icon-comment-square") + end + end + + context "when the resource is an initiative" do + let(:resource) { build :initiative } + + it "renders an initiative icon" do + expect(result).to include("svg#icon-initiatives") + end + end + + context "and in other cases" do + let(:resource) { "Something" } + + it "renders a generic icon" do + expect(result).to include("svg#icon-bell") + end + end + end + end + end +end diff --git a/spec/shared/update_initiative_answer_example.rb b/spec/shared/update_initiative_answer_example.rb new file mode 100644 index 0000000000..5eab077786 --- /dev/null +++ b/spec/shared/update_initiative_answer_example.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +shared_examples "update an initiative answer" do + let(:organization) { create(:organization) } + let(:initiative) { create(:initiative, organization: organization, state: state) } + let(:form) do + form_klass.from_params( + form_params + ).with_context( + current_organization: organization, + initiative: initiative + ) + end + let(:signature_end_date) { Date.current + 500.days } + let(:state) { "published" } + let(:form_params) do + { + signature_start_date: Date.current + 10.days, + signature_end_date: signature_end_date, + answer: { en: "Measured answer" }, + answer_url: "http://decidim.org" + } + end + let(:administrator) { create(:user, :admin, organization: organization) } + let(:current_user) { administrator } + let(:command) { described_class.new(initiative, form, current_user) } + + describe "call" do + describe "when the form is not valid" do + before do + allow(form).to receive(:invalid?).and_return(true) + end + + it "broadcasts invalid" do + expect { command.call }.to broadcast(:invalid) + end + + it "doesn't updates the initiative" do + command.call + + form_params.each do |key, value| + expect(initiative[key]).not_to eq(value) + end + end + end + + describe "when the form is valid" do + it "broadcasts ok" do + expect { command.call }.to broadcast(:ok) + end + + it "updates the initiative" do + command.call + initiative.reload + + expect(initiative.answer["en"]).to eq(form_params[:answer][:en]) + expect(initiative.answer_url).to eq(form_params[:answer_url]) + end + + context "when initiative is not published" do + let(:state) { "validating" } + + it "voting interval remains unchanged" do + command.call + initiative.reload + + [:signature_start_date, :signature_end_date].each do |key| + expect(initiative[key]).not_to eq(form_params[key]) + end + end + end + + context "when initiative is published" do + it "voting interval is updated" do + command.call + initiative.reload + + [:signature_start_date, :signature_end_date].each do |key| + expect(initiative[key]).to eq(form_params[key]) + end + end + end + end + end +end