From 075dd3827b51bef77161ae74891b3f6b400ab569 Mon Sep 17 00:00:00 2001 From: Guillaume MORET <90462045+AyakorK@users.noreply.github.com> Date: Mon, 19 Aug 2024 09:27:33 +0200 Subject: [PATCH] backport: Export issue for Processes Administrators (#573) Co-authored-by: Lucie Grau <87868063+luciegrau@users.noreply.github.com> --- app/jobs/decidim/export_job.rb | 31 +++++ config/initializers/extends.rb | 1 + .../proposal_serializer_extend.rb | 32 +++++ spec/jobs/export_job_spec.rb | 125 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 app/jobs/decidim/export_job.rb create mode 100644 lib/extends/lib/decidim/phone_authorization_handler/proposal_serializer_extend.rb create mode 100644 spec/jobs/export_job_spec.rb diff --git a/app/jobs/decidim/export_job.rb b/app/jobs/decidim/export_job.rb new file mode 100644 index 0000000..aa3477a --- /dev/null +++ b/app/jobs/decidim/export_job.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Decidim + class ExportJob < ApplicationJob + queue_as :exports + + def perform(user, component, name, format, resource_id = nil) + export_manifest = component.manifest.export_manifests.find do |manifest| + manifest.name == name.to_sym + end + + collection = export_manifest.collection.call(component, user, resource_id) + serializer = export_manifest.serializer + + export_data = if (serializer == Decidim::Proposals::ProposalSerializer) && (user.admin? || admin_of_process?(user, component)) + Decidim::Exporters.find_exporter(format).new(collection, serializer).admin_export + else + Decidim::Exporters.find_exporter(format).new(collection, serializer).export + end + ExportMailer.export(user, name, export_data).deliver_now + end + + private + + def admin_of_process?(user, component) + return unless component.respond_to?(:participatory_space) + + Decidim::ParticipatoryProcessUserRole.exists?(decidim_user_id: user.id, decidim_participatory_process_id: component.participatory_space.id, role: "admin") + end + end +end diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index c8bf791..653d04f 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -4,6 +4,7 @@ require "extends/cells/decidim/content_blocks/hero_cell_extends" require "extends/uploaders/decidim/application_uploader_extends" require "extends/lib/decidim/proposals/imports/proposal_answer_creator_extends" +require "extends/lib/decidim/phone_authorization_handler/proposal_serializer_extend" require "decidim/exporters/serializer" require "extends/lib/decidim/forms/user_answers_serializer_extend" diff --git a/lib/extends/lib/decidim/phone_authorization_handler/proposal_serializer_extend.rb b/lib/extends/lib/decidim/phone_authorization_handler/proposal_serializer_extend.rb new file mode 100644 index 0000000..e314e8d --- /dev/null +++ b/lib/extends/lib/decidim/phone_authorization_handler/proposal_serializer_extend.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module ProposalSerializerExtend + def author_metadata + author_metadata = { + name: "", + nickname: "", + email: "", + phone_number: "" + } + + if proposal.creator.decidim_author_type == "Decidim::UserBaseEntity" + begin + user = Decidim::User.find(proposal.creator_author.id) + author_metadata[:name] = user.try(:name).presence || "" + author_metadata[:nickname] = user.try(:nickname).presence || "" + author_metadata[:email] = user.try(:email).presence || "" + author_metadata[:phone_number] = phone_number(user.id) + rescue ActiveRecord::RecordNotFound => e + Rails.logger.error "User not found: #{e.message}" + author_metadata[:name] = "" + author_metadata[:nickname] = "" + author_metadata[:email] = "" + author_metadata[:phone_number] = "" + end + end + + author_metadata + end +end + +Decidim::PhoneAuthorizationHandler::Extends::ProposalSerializerExtend.prepend(ProposalSerializerExtend) diff --git a/spec/jobs/export_job_spec.rb b/spec/jobs/export_job_spec.rb new file mode 100644 index 0000000..3769693 --- /dev/null +++ b/spec/jobs/export_job_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require "spec_helper" + +module Decidim + module Admin + describe ExportJob do + let!(:component) { create(:component, manifest_name: "dummy") } + let(:organization) { component.organization } + let!(:user) { create(:user, organization: organization) } + let!(:admin) { create(:user, :admin, organization: organization) } + let!(:admin_of_the_process) { create(:user, organization: organization) } + let!(:participatory_process) { create(:participatory_process, organization: organization) } + let(:proposal) { create(:proposal) } + let(:collection) { [proposal] } # Use an array with the instance_double + let(:export_manifest) do + instance_double( + # rubocop:disable RSpec/VerifiedDoubleReference + "Decidim::ComponentExportManifest", + # rubocop:enable RSpec/VerifiedDoubleReference + name: :proposals, + collection: ->(_component, _user, _resource_id) { collection }, + serializer: Decidim::Proposals::ProposalSerializer + ) + end + + before do + component.update!(participatory_space: participatory_process) + create(:participatory_process_user_role, user: admin_of_the_process, participatory_process: participatory_process, role: "admin") + + allow(component.manifest).to receive(:export_manifests).and_return([export_manifest]) + end + + it "sends an email with the result of the export" do + ExportJob.perform_now(user, component, "proposals", "CSV") + + email = last_email + expect(email.subject).to include("proposals") + attachment = email.attachments.first + + expect(attachment.read.length).to be_positive + expect(attachment.mime_type).to eq("application/zip") + expect(attachment.filename).to match(/^proposals-[0-9]+-[0-9]+-[0-9]+-[0-9]+\.zip$/) + end + + describe "CSV" do + it "uses the CSV exporter" do + export_data = double + + expect(Decidim::Exporters::CSV) + .to(receive(:new).with(anything, Decidim::Proposals::ProposalSerializer)) + .and_return(double(export: export_data)) + + expect(ExportMailer) + .to(receive(:export).with(user, anything, export_data)) + .and_return(double(deliver_now: true)) + + ExportJob.perform_now(user, component, "proposals", "CSV") + end + end + + describe "JSON" do + it "uses the JSON exporter" do + export_data = double + + expect(Decidim::Exporters::JSON) + .to(receive(:new).with(anything, Decidim::Proposals::ProposalSerializer)) + .and_return(double(export: export_data)) + + expect(ExportMailer) + .to(receive(:export).with(user, anything, export_data)) + .and_return(double(deliver_now: true)) + + ExportJob.perform_now(user, component, "proposals", "JSON") + end + end + + describe "Admin export" do + let(:serializer) { Decidim::Proposals::ProposalSerializer } + + before do + allow(Decidim::Exporters::CSV) + .to(receive(:new).with(anything, serializer)) + .and_return(double(export: "normal export data")) + end + + it "allows admin to access admin_export" do + expect(Decidim::Exporters::CSV) + .to(receive(:new).with(anything, serializer)) + .and_return(double(admin_export: "admin export data")) + + expect(ExportMailer) + .to(receive(:export).with(admin, anything, "admin export data")) + .and_return(double(deliver_now: true)) + + ExportJob.perform_now(admin, component, "proposals", "CSV") + end + + it "allows admin of the process to access admin_export" do + expect(Decidim::Exporters::CSV) + .to(receive(:new).with(anything, serializer)) + .and_return(double(admin_export: "admin export data")) + + expect(ExportMailer) + .to(receive(:export).with(admin_of_the_process, anything, "admin export data")) + .and_return(double(deliver_now: true)) + + ExportJob.perform_now(admin_of_the_process, component, "proposals", "CSV") + end + + it "does not allow normal user to access admin_export" do + expect(Decidim::Exporters::CSV) + .to(receive(:new).with(anything, serializer)) + .and_return(double(export: "normal export data")) + + expect(ExportMailer) + .to(receive(:export).with(user, anything, "normal export data")) + .and_return(double(deliver_now: true)) + + ExportJob.perform_now(user, component, "proposals", "CSV") + end + end + end + end +end