diff --git a/app/queries/concerns/decidim/privacy/admin_newsletter_recipients_extensions.rb b/app/queries/concerns/decidim/privacy/admin_newsletter_recipients_extensions.rb new file mode 100644 index 0000000..b42f464 --- /dev/null +++ b/app/queries/concerns/decidim/privacy/admin_newsletter_recipients_extensions.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Decidim + module Privacy + module AdminNewsletterRecipientsExtensions + extend ActiveSupport::Concern + + included do + def query + recipients = recipients_base_query + recipients = recipients.interested_in_scopes(@form.scope_ids) if @form.scope_ids.present? + + followers = recipients.where(id: user_id_of_followers) if @form.send_to_followers + + participants = recipients.where(id: participant_ids) if @form.send_to_participants + + recipients = participants if @form.send_to_participants + recipients = followers if @form.send_to_followers + recipients = (followers + participants).uniq if @form.send_to_followers && @form.send_to_participants + + recipients + end + end + + private + + def recipients_base_query + Decidim::User + .entire_collection + .where(organization: @form.current_organization) + .where.not(newsletter_notifications_at: nil) + .where.not(email: nil) + .where.not(confirmed_at: nil) + .not_deleted + end + end + end +end diff --git a/lib/decidim/privacy/engine.rb b/lib/decidim/privacy/engine.rb index 44cf3e7..0fb8d54 100644 --- a/lib/decidim/privacy/engine.rb +++ b/lib/decidim/privacy/engine.rb @@ -202,6 +202,9 @@ class Engine < ::Rails::Engine Decidim::StatsUsersCount.include( Decidim::Privacy::StatsUsersCountExtensions ) + Decidim::Admin::NewsletterRecipients.include( + Decidim::Privacy::AdminNewsletterRecipientsExtensions + ) # Initialize concerns for each installed Decidim-module if Decidim.module_installed? :budgets diff --git a/spec/queries/decidim/admin/newsletter_recipients_spec.rb b/spec/queries/decidim/admin/newsletter_recipients_spec.rb new file mode 100644 index 0000000..0839ebe --- /dev/null +++ b/spec/queries/decidim/admin/newsletter_recipients_spec.rb @@ -0,0 +1,176 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Decidim::Admin::NewsletterRecipients do + subject { described_class.new(form) } + + let(:newsletter) { create(:newsletter) } + let(:organization) { newsletter.organization } + let(:send_to_all_users) { true } + let(:send_to_followers) { false } + let(:send_to_participants) { false } + let(:participatory_space_types) { [] } + let(:scope_ids) { [] } + + let(:form_params) do + { + send_to_all_users:, + send_to_followers:, + send_to_participants:, + participatory_space_types:, + scope_ids: + } + end + + let(:form) do + Decidim::Admin::SelectiveNewsletterForm.from_params( + form_params + ).with_context( + current_organization: organization + ) + end + + describe "querying recipients" do + context "when sending to all users" do + let!(:recipients) { create_list(:user, 5, :confirmed, newsletter_notifications_at: Time.current, organization:) } + + it "returns all users" do + expect(subject.query).to match_array recipients + expect(recipients.count).to eq 5 + end + + context "with the scope_ids array containing an empty value" do + let(:scope_ids) { [""] } + + it "returns all users" do + expect(subject.query).to match_array recipients + expect(recipients.count).to eq 5 + end + end + end + + context "when sending to followers" do + let!(:recipients) { create_list(:user, 3, :confirmed, newsletter_notifications_at: Time.current, organization:) } + let(:send_to_all_users) { false } + let(:send_to_followers) { true } + let(:participatory_processes) { create_list(:participatory_process, 2, organization:) } + let(:participatory_space_types) do + [ + { "id" => nil, + "manifest_name" => "participatory_processes", + "ids" => [participatory_processes.first.id.to_s] }, + { "id" => nil, + "manifest_name" => "assemblies", + "ids" => [] }, + { "id" => nil, + "manifest_name" => "conferences", + "ids" => [] }, + { "id" => nil, + "manifest_name" => "initiatives", + "ids" => [] } + ] + end + + context "when recipients follow the participatory space" do + before do + recipients.each do |follower| + create(:follow, followable: participatory_processes.first, user: follower) + end + end + + it "returns all users" do + expect(subject.query).to match_array recipients + expect(recipients.count).to eq 3 + end + end + end + + context "when sending to participants" do + let(:send_to_all_users) { false } + let(:send_to_participants) { true } + let!(:component) { create(:dummy_component, organization:) } + let(:participatory_space_types) do + [ + { "id" => nil, + "manifest_name" => "participatory_processes", + "ids" => [component.participatory_space.id.to_s] }, + { "id" => nil, + "manifest_name" => "assemblies", + "ids" => [] }, + { "id" => nil, + "manifest_name" => "conferences", + "ids" => [] }, + { "id" => nil, + "manifest_name" => "initiatives", + "ids" => [] } + ] + end + + context "when recipients participate to the participatory space" do + let!(:authors) do + create_list(:user, 3, :confirmed, organization:, newsletter_notifications_at: Time.current) + end + + before do + authors.each do |participant| + create(:dummy_resource, :published, component:, author: participant) + end + end + + it "returns all users" do + expect(subject.query).to match_array authors + expect(authors.count).to eq 3 + end + + context "and other comment in other participatory spaces" do + # non participant commentator (comments into other spaces) + let!(:non_participant) { create(:user, :confirmed, newsletter_notifications_at: Time.current, organization:) } + let!(:component_out_of_newsletter) { create(:dummy_component, organization:) } + let!(:resource_out_of_newsletter) { create(:dummy_resource, :published, author: non_participant, component: component_out_of_newsletter) } + let!(:outlier_comment) { create(:comment, author: non_participant, commentable: resource_out_of_newsletter) } + # participant commentator + let!(:commentator_participant) { create(:user, :confirmed, newsletter_notifications_at: Time.current, organization:) } + let!(:resource_in_newsletter) { create(:dummy_resource, :published, author: authors.first, component:) } + let!(:comment_in_newsletter) { create(:comment, author: commentator_participant, commentable: resource_in_newsletter) } + + let(:recipients) { authors + [commentator_participant] } + + it "returns only commenters in the selected spaces" do + expect(subject.query).to match_array(recipients) + expect(recipients.count).to eq 4 + end + end + end + end + + context "with scopes segment" do + let(:scopes) do + create_list(:scope, 5, organization:) + end + let(:scope_ids) { scopes.pluck(:id) } + + context "when recipients interested in scopes" do + let!(:recipients) do + create_list(:user, 3, :confirmed, organization:, newsletter_notifications_at: Time.current, extended_data: { "interested_scopes" => scopes.first.id }) + end + + it "returns all users" do + expect(subject.query).to match_array recipients + expect(recipients.count).to eq 3 + end + end + + context "when interest not match the selected scopes" do + let(:user_interset) { create(:scope, organization:) } + let!(:recipients) do + create_list(:user, 3, :confirmed, organization:, newsletter_notifications_at: Time.current, extended_data: { "interested_scopes" => user_interset.id }) + end + + it "do not return recipients" do + expect(subject.query).to be_empty + end + end + end + end +end