Skip to content

Commit

Permalink
Add recipient selection and preview for Newsletters (decidim#13680)
Browse files Browse the repository at this point in the history
Co-authored-by: Ivan Vergés <ivan@pokecode.net>
  • Loading branch information
antopalidi and microstudi authored Dec 16, 2024
1 parent 28afd3a commit 3834c44
Show file tree
Hide file tree
Showing 23 changed files with 1,196 additions and 303 deletions.
10 changes: 10 additions & 0 deletions decidim-admin/app/cells/decidim/admin/multi_select_picker/show.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<%= select_tag field_name,
options_for_select(
cell_options_for_select.map { |option| [option[0], option[1]] },
selected_values
),
id: select_id,
class: css_classes,
placeholder:,
multiple: true,
data: { multiselect: true } %>
38 changes: 38 additions & 0 deletions decidim-admin/app/cells/decidim/admin/multi_select_picker_cell.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

module Decidim
module Admin
# Universal cell for rendering multi-select pickers
class MultiSelectPickerCell < Decidim::ViewModel
include ActionView::Helpers::FormOptionsHelper

def show
render :show
end

def cell_options_for_select
context[:options_for_select] || []
end

def selected_values
context[:selected_values] || []
end

def select_id
context[:select_id]
end

def field_name
context[:field_name]
end

def placeholder
context[:placeholder] || ""
end

def css_classes
context[:class] || ""
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class NewslettersController < Decidim::Admin::ApplicationController
include Decidim::NewslettersHelper
include Decidim::Admin::NewslettersHelper
include Paginable
helper_method :newsletter, :recipients_count_query, :content_block
helper_method :newsletter, :recipients_count_query, :content_block, :selected_options, :newsletter_params

def index
enforce_permission_to :index, :newsletter
Expand Down Expand Up @@ -103,20 +103,22 @@ def destroy

def select_recipients_to_deliver
enforce_permission_to(:update, :newsletter, newsletter:)
@form = form(SelectiveNewsletterForm).from_model(newsletter)
@form.send_to_all_users = current_user.admin?

@form = if newsletter_params.present?
form(SelectiveNewsletterForm).from_params(newsletter_params)
else
form(SelectiveNewsletterForm).from_model(newsletter)
end
end

def recipients_count
data = params.permit(newsletter: {}).to_h[:newsletter]

@form = form(SelectiveNewsletterForm).from_params(data)
@form = form(SelectiveNewsletterForm).from_params(newsletter_params)
render plain: recipients_count_query
end

def deliver
enforce_permission_to(:update, :newsletter, newsletter:)
@form = form(SelectiveNewsletterForm).from_params(params)
@form = form(SelectiveNewsletterForm).from_params(newsletter_params)

DeliverNewsletter.call(newsletter, @form) do
on(:ok) do
Expand All @@ -136,8 +138,49 @@ def deliver
end
end

def confirm_recipients
enforce_permission_to(:update, :newsletter, newsletter:)
@form = form(SelectiveNewsletterForm).from_params(newsletter_params)
@recipients = NewsletterRecipients.for(@form).order(:email)
@recipients = paginate(@recipients)

render :confirm_recipients
end

private

def newsletter_params
params.fetch(:newsletter, {}).permit(
:send_to_all_users,
:send_to_verified_users,
:send_to_followers,
:send_to_participants,
:send_to_private_members,
verification_types: [],
participatory_space_types: {
assemblies: [:manifest_name, { ids: [] }],
conferences: [:manifest_name, { ids: [] }],
initiatives: [:manifest_name, { ids: [] }],
participatory_processes: [:manifest_name, { ids: [] }]
}
)
end

def selected_options(key)
@selected_options ||= {}
@selected_options[key] ||= extract_selected_ids(params[:newsletter], key)
end

def extract_selected_ids(newsletter_params, key)
return {} unless newsletter_params.present? && newsletter_params[key].present?

if newsletter_params[key].is_a?(Array)
{ key => newsletter_params[key].map(&:to_s) }
else
(newsletter_params[key] || {}).transform_values { |space| space["ids"] || [] }
end
end

def collection
@collection ||= Newsletter.where(organization: current_organization)
end
Expand Down
57 changes: 46 additions & 11 deletions decidim-admin/app/forms/decidim/admin/selective_newsletter_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,33 @@ class SelectiveNewsletterForm < Decidim::Form
mimic :newsletter

attribute :participatory_space_types, Array[SelectiveNewsletterParticipatorySpaceTypeForm]
attribute :scope_ids, Array
attribute :verification_types, Array[String]
attribute :send_to_all_users, Boolean
attribute :send_to_verified_users, Boolean
attribute :send_to_participants, Boolean
attribute :send_to_followers, Boolean
attribute :send_to_private_members, Boolean

validates :send_to_all_users, presence: true, unless: ->(form) { form.send_to_participants.present? || form.send_to_followers.present? }
validates :send_to_followers, presence: true, if: ->(form) { form.send_to_all_users.blank? && form.send_to_participants.blank? }
validates :send_to_participants, presence: true, if: ->(form) { form.send_to_all_users.blank? && form.send_to_followers.blank? }
validates :send_to_all_users, presence: true, unless: :other_groups_selected_for_all_users?
validates :send_to_verified_users, presence: true, unless: :other_groups_selected_for_verified_users?
validates :send_to_followers, presence: true, if: :only_followers_selected?
validates :send_to_participants, presence: true, if: :only_participants_selected?
validates :send_to_private_members, presence: true, if: :only_private_members_selected?

validate :at_least_one_participatory_space_selected

def map_model(_newsletter)
def map_model(newsletter)
self.participatory_space_types = Decidim.participatory_space_manifests.map do |manifest|
SelectiveNewsletterParticipatorySpaceTypeForm.from_model(manifest:)
end
end

# Make sure the empty scope is not passed because then some logic could
# assume erroneously that some scope is selected.
def scope_ids
super.select(&:presence)
self.verification_types = newsletter.organization.available_authorizations
end

private

def at_least_one_participatory_space_selected
return if send_to_all_users && current_user.admin?
return if (send_to_all_users || send_to_verified_users) && current_user.admin?

errors.add(:base, :at_least_one_space) if spaces_selected.blank?
end
Expand All @@ -44,6 +44,41 @@ def spaces_selected
[type.manifest_name, spaces] if spaces.present?
end.compact
end

def other_groups_selected_for_all_users?
send_to_verified_users.present? ||
send_to_participants.present? ||
send_to_followers.present? ||
send_to_private_members.present?
end

def other_groups_selected_for_verified_users?
send_to_all_users.present? ||
send_to_participants.present? ||
send_to_followers.present? ||
send_to_private_members.present?
end

def only_followers_selected?
send_to_all_users.blank? &&
send_to_participants.blank? &&
send_to_private_members.blank? &&
send_to_verified_users.blank?
end

def only_participants_selected?
send_to_all_users.blank? &&
send_to_followers.blank? &&
send_to_private_members.blank? &&
send_to_verified_users.blank?
end

def only_private_members_selected?
send_to_all_users.blank? &&
send_to_followers.blank? &&
send_to_participants.blank? &&
send_to_verified_users.blank?
end
end
end
end
84 changes: 57 additions & 27 deletions decidim-admin/app/helpers/decidim/admin/newsletters_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ module Decidim
module Admin
# This module includes helpers to manage newsletters in admin layout
module NewslettersHelper
def find_verification_types_for_select(organization)
available_verifications = organization.available_authorizations
available_verifications.map do |verification_type|
[t("decidim.authorization_handlers.#{verification_type}.name"), verification_type]
end
end

def participatory_spaces_for_select(form_object)
content_tag :div do
@form.participatory_space_types.each do |space_type|
Expand All @@ -17,22 +24,32 @@ def participatory_space_types_form_object(form_object, space_type)

html = ""
form_object.fields_for "participatory_space_types[#{space_type.manifest_name}]", space_type do |ff|
html += participatory_space_title(space_type)
html += ff.hidden_field :manifest_name, value: space_type.manifest_name
html += select_tag_participatory_spaces(space_type.manifest_name, spaces_for_select(space_type.manifest_name.to_sym), ff)
end
html.html_safe
end

def participatory_space_title(space_type)
return unless space_type

content_tag :h4 do
t("activerecord.models.decidim/#{space_type.manifest_name.singularize}.other")
end
end

def select_tag_participatory_spaces(manifest_name, spaces, child_form)
return unless spaces

content_tag :div, class: "#{manifest_name}-block spaces-block-tag cell small-12 medium-6" do
child_form.select :ids, options_for_select(spaces),
{ prompt: t("select_recipients_to_deliver.none", scope: "decidim.admin.newsletters"),
label: t("activerecord.models.decidim/#{manifest_name.singularize}.other"),
include_hidden: false },
multiple: true, size: [spaces.size, 10].min, class: "chosen-select"
end
raw(cell("decidim/admin/multi_select_picker", nil, context: {
select_id: "#{manifest_name}-spaces-select",
field_name: "#{child_form.object_name}[ids][]",
options_for_select: spaces,
selected_values: selected_options(:participatory_space_types)[manifest_name] || [],
placeholder: t("select_recipients_to_deliver.select_#{manifest_name}", scope: "decidim.admin.newsletters"),
class: "mb-2"
}))
end

def spaces_for_select(manifest_name)
Expand All @@ -45,21 +62,35 @@ def spaces_for_select(manifest_name)
def selective_newsletter_to(newsletter)
return content_tag(:strong, t("index.not_sent", scope: "decidim.admin.newsletters"), class: "text-warning") unless newsletter.sent?
return content_tag(:strong, t("index.all_users", scope: "decidim.admin.newsletters"), class: "text-success") if newsletter.sent? && newsletter.extended_data.blank?
return sent_to_verified_users(newsletter) if newsletter.sent_to_verified_users?

content_tag :div do
concat sent_to_users newsletter
concat sent_to_spaces newsletter
concat sent_to_scopes newsletter
end
end

def sent_to_users(newsletter)
content_tag :p, style: "margin-bottom:0;" do
concat content_tag(:strong, t("index.has_been_sent_to", scope: "decidim.admin.newsletters"), class: "text-success")
concat content_tag(:strong, t("index.all_users", scope: "decidim.admin.newsletters")) if newsletter.sended_to_all_users?
concat content_tag(:strong, t("index.followers", scope: "decidim.admin.newsletters")) if newsletter.sended_to_followers?
concat t("index.and", scope: "decidim.admin.newsletters") if newsletter.sended_to_followers? && newsletter.sended_to_participants?
concat content_tag(:strong, t("index.participants", scope: "decidim.admin.newsletters")) if newsletter.sended_to_participants?

recipients = []

recipients << content_tag(:strong, t("index.all_users", scope: "decidim.admin.newsletters")) if newsletter.sent_to_all_users?
recipients << content_tag(:strong, t("index.verified_users", scope: "decidim.admin.newsletters")) if newsletter.sent_to_verified_users?
recipients << content_tag(:strong, t("index.followers", scope: "decidim.admin.newsletters")) if newsletter.sent_to_followers?
recipients << content_tag(:strong, t("index.participants", scope: "decidim.admin.newsletters")) if newsletter.sent_to_participants?
recipients << content_tag(:strong, t("index.private_members", scope: "decidim.admin.newsletters")) if newsletter.sent_to_private_members?

concat recipients.join(t("index.and", scope: "decidim.admin.newsletters")).html_safe
end
end

def sent_to_verified_users(newsletter)
content_tag :p, style: "margin-bottom:0;" do
concat content_tag(:strong, t("index.has_been_sent_to", scope: "decidim.admin.newsletters"), class: "text-success")
concat content_tag(:strong, t("index.verified_users", scope: "decidim.admin.newsletters"))
concat content_tag(:p, t("index.verification_types", scope: "decidim.admin.newsletters", types: selected_verification_types(newsletter)))
end
end

Expand All @@ -68,12 +99,14 @@ def sent_to_spaces(newsletter)
newsletter.sent_to_participatory_spaces.try(:each) do |type|
next if type["ids"].blank?

ids = parse_ids(type["ids"])

html += t("index.segmented_to", scope: "decidim.admin.newsletters", subject: t("activerecord.models.decidim/#{type["manifest_name"].singularize}.other"))
if type["ids"].include?("all")
if ids.include?("all")
html += "<strong> #{t("index.all", scope: "decidim.admin.newsletters")} </strong>"
else
Decidim.find_participatory_space_manifest(type["manifest_name"].to_sym)
.participatory_spaces.call(current_organization).where(id: type["ids"]).each do |space|
.participatory_spaces.call(current_organization).where(id: ids).each do |space|
html += "<strong>#{decidim_escape_translated(space.title)}</strong>"
end
end
Expand All @@ -83,19 +116,6 @@ def sent_to_spaces(newsletter)
html.html_safe
end

def sent_to_scopes(newsletter)
content_tag :p, style: "margin-bottom:0;" do
concat t("index.segmented_to", scope: "decidim.admin.newsletters", subject: nil)
if newsletter.sent_scopes.any?
newsletter.sent_scopes.each do |scope|
concat content_tag(:strong, decidim_escape_translated(scope.name).to_s)
end
else
concat content_tag(:strong, t("index.no_scopes", scope: "decidim.admin.newsletters"))
end
end
end

def organization_participatory_space(manifest_name)
@organization_participatory_spaces ||= {}
@organization_participatory_spaces[manifest_name] ||= Decidim
Expand Down Expand Up @@ -144,6 +164,16 @@ def newsletter_recipients_count_callout_announcement
body:
}
end

def parse_ids(ids)
ids.size == 1 && ids.first.is_a?(String) ? ids.first.split.map(&:strip) : ids
end

def selected_verification_types(newsletter)
newsletter.sent_to_users_with_verification_types&.map do |type|
I18n.t("decidim.authorization_handlers.#{type}.name")
end&.join(", ")
end
end
end
end
4 changes: 3 additions & 1 deletion decidim-admin/app/jobs/decidim/admin/newsletter_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ def perform(newsletter, form, recipients_ids)
def extended_data
{
send_to_all_users: @form["send_to_all_users"],
send_to_verified_users: @form["send_to_verified_users"],
send_to_followers: @form["send_to_followers"],
send_to_participants: @form["send_to_participants"],
send_to_private_members: @form["send_to_private_members"],
participatory_space_types: @form["participatory_space_types"],
scope_ids: @form["scope_ids"]
verification_types: @form["verification_types"]
}
end

Expand Down
Loading

0 comments on commit 3834c44

Please sign in to comment.