Implement letter generation
This commit is contained in:
parent
2e7b03e9ef
commit
2705ad1611
3
Gemfile
3
Gemfile
@ -14,6 +14,9 @@ gem "puma", "~> 5.0"
|
|||||||
gem "pundit"
|
gem "pundit"
|
||||||
gem "rails", "~> 7.0.3"
|
gem "rails", "~> 7.0.3"
|
||||||
gem "ransack"
|
gem "ransack"
|
||||||
|
gem "nokogiri"
|
||||||
|
gem "rubyzip"
|
||||||
|
gem "combine_pdf"
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem "debug", platforms: %i[ mri mingw x64_mingw ]
|
gem "debug", platforms: %i[ mri mingw x64_mingw ]
|
||||||
|
@ -82,6 +82,9 @@ GEM
|
|||||||
bcrypt (>= 3.1.1)
|
bcrypt (>= 3.1.1)
|
||||||
email_validator (~> 2.0)
|
email_validator (~> 2.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
|
combine_pdf (1.0.22)
|
||||||
|
matrix
|
||||||
|
ruby-rc4 (>= 0.1.5)
|
||||||
concurrent-ruby (1.1.10)
|
concurrent-ruby (1.1.10)
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
debug (1.5.0)
|
debug (1.5.0)
|
||||||
@ -115,6 +118,7 @@ GEM
|
|||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
mini_mime (>= 0.1.1)
|
mini_mime (>= 0.1.1)
|
||||||
marcel (1.0.2)
|
marcel (1.0.2)
|
||||||
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
minitest (5.16.0)
|
minitest (5.16.0)
|
||||||
@ -188,6 +192,8 @@ GEM
|
|||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
request_store (1.5.1)
|
request_store (1.5.1)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
|
ruby-rc4 (0.1.5)
|
||||||
|
rubyzip (2.3.2)
|
||||||
strscan (3.0.3)
|
strscan (3.0.3)
|
||||||
thor (1.2.1)
|
thor (1.2.1)
|
||||||
timecop (0.9.5)
|
timecop (0.9.5)
|
||||||
@ -210,9 +216,11 @@ PLATFORMS
|
|||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
bootsnap
|
bootsnap
|
||||||
clearance
|
clearance
|
||||||
|
combine_pdf
|
||||||
debug
|
debug
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
importmap-rails
|
importmap-rails
|
||||||
|
nokogiri
|
||||||
paper_trail
|
paper_trail
|
||||||
pg (~> 1.1)
|
pg (~> 1.1)
|
||||||
propshaft
|
propshaft
|
||||||
@ -220,6 +228,7 @@ DEPENDENCIES
|
|||||||
pundit
|
pundit
|
||||||
rails (~> 7.0.3)
|
rails (~> 7.0.3)
|
||||||
ransack
|
ransack
|
||||||
|
rubyzip
|
||||||
timecop
|
timecop
|
||||||
web-console
|
web-console
|
||||||
|
|
||||||
|
@ -23,3 +23,5 @@ ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li { padding: 5px 0px; }
|
li { padding: 5px 0px; }
|
||||||
|
|
||||||
|
table.noborder { border: 0; margin: 0; }
|
||||||
|
38
app/controllers/concerns/member_filter.rb
Normal file
38
app/controllers/concerns/member_filter.rb
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
module MemberFilter
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
def filtered_members
|
||||||
|
members = Member.all.order(sort_params.merge(number: :asc))
|
||||||
|
|
||||||
|
filters = params.permit(:prefers_postal, :display_name, :email, :identification_number, status: [], category: [])
|
||||||
|
|
||||||
|
Rails.logger.info filters
|
||||||
|
|
||||||
|
status = filters.fetch(:status, []) - ['any', '']
|
||||||
|
category = filters.fetch(:category, []) - ['any', '']
|
||||||
|
|
||||||
|
members = members.where(prefers_postal: true) if filters[:prefers_postal] == 'yes'
|
||||||
|
members = members.where(prefers_postal: false) if filters[:prefers_postal] == 'no'
|
||||||
|
members = members.where(status: status) if status != []
|
||||||
|
members = members.where(category: category) if category != []
|
||||||
|
|
||||||
|
members.ransack(
|
||||||
|
display_name_i_cont: filters[:display_name],
|
||||||
|
email_i_cont: filters[:email],
|
||||||
|
identification_number_i_cont: filters[:identification_number],
|
||||||
|
).result
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_params
|
||||||
|
field, direction = params.fetch(:sort, "").split(".")
|
||||||
|
|
||||||
|
directions = %w[ asc desc ]
|
||||||
|
fields = %w[ number expires_on joined_on email status display_name ]
|
||||||
|
|
||||||
|
if directions.include?(direction) && fields.include?(field)
|
||||||
|
{ field => direction }
|
||||||
|
else
|
||||||
|
{ number: :asc }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
app/controllers/letters_controller.rb
Normal file
14
app/controllers/letters_controller.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class LettersController < ApplicationController
|
||||||
|
before_action :require_login
|
||||||
|
|
||||||
|
include MemberFilter
|
||||||
|
|
||||||
|
# POST /letters
|
||||||
|
def create
|
||||||
|
members = filtered_members
|
||||||
|
|
||||||
|
pdf = Letters.generate(params[:template], members)
|
||||||
|
|
||||||
|
send_data pdf, filename: "members.pdf", type: "application/pdf"
|
||||||
|
end
|
||||||
|
end
|
@ -3,13 +3,11 @@ class MembersController < ApplicationController
|
|||||||
before_action :set_member, only: %i[ show edit update delete destroy ]
|
before_action :set_member, only: %i[ show edit update delete destroy ]
|
||||||
helper_method :sort_params
|
helper_method :sort_params
|
||||||
|
|
||||||
|
include MemberFilter
|
||||||
|
|
||||||
# GET /members
|
# GET /members
|
||||||
def index
|
def index
|
||||||
params.delete(:status) if params[:status] == 'any'
|
@members = filtered_members
|
||||||
params.delete(:category) if params[:category] == 'any'
|
|
||||||
|
|
||||||
@members = Member.all.order(sort_params.merge(number: :asc))
|
|
||||||
@members = @members.ransack(display_name_or_email_or_identification_number_i_cont: params[:q], status_cont: params[:status], category_cont: params[:category]).result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /members/1
|
# GET /members/1
|
||||||
@ -68,17 +66,4 @@ class MembersController < ApplicationController
|
|||||||
def member_params
|
def member_params
|
||||||
params.fetch(:member, {}).permit(:display_name, :email, :identification_number, :category, :address, :joined_on, :expires_on, :wants_mailing_list, :prefers_postal)
|
params.fetch(:member, {}).permit(:display_name, :email, :identification_number, :category, :address, :joined_on, :expires_on, :wants_mailing_list, :prefers_postal)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_params
|
|
||||||
field, direction = params.fetch(:sort, "").split(".")
|
|
||||||
|
|
||||||
directions = %w[ asc desc ]
|
|
||||||
fields = %w[ number expires_on joined_on email status display_name ]
|
|
||||||
|
|
||||||
if directions.include?(direction) && fields.include?(field)
|
|
||||||
{ field => direction }
|
|
||||||
else
|
|
||||||
{ number: :asc }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
19
app/controllers/notifications_controller.rb
Normal file
19
app/controllers/notifications_controller.rb
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class NotificationsController < ApplicationController
|
||||||
|
before_action :require_login
|
||||||
|
before_action :set_notification
|
||||||
|
|
||||||
|
# POST /notifications/1/deliver
|
||||||
|
def deliver
|
||||||
|
@notification.deliver!
|
||||||
|
|
||||||
|
redirect_to @notification.member
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Use callbacks to share common setup or constraints between actions.
|
||||||
|
def set_notification
|
||||||
|
@notification = Notification.find(params[:id])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
68
app/lib/letters.rb
Normal file
68
app/lib/letters.rb
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
require 'zip'
|
||||||
|
require 'nokogiri'
|
||||||
|
require 'combine_pdf'
|
||||||
|
|
||||||
|
module Letters
|
||||||
|
def self.apply_template(io, params)
|
||||||
|
Zip::OutputStream.write_buffer do |out|
|
||||||
|
Zip::File.open(io) do |zip|
|
||||||
|
zip.each do |entry|
|
||||||
|
pp entry.name
|
||||||
|
out.put_next_entry(entry.name)
|
||||||
|
if entry.name == "content.xml"
|
||||||
|
out.write apply_template_xml(entry.get_input_stream.read, params)
|
||||||
|
elsif !entry.directory?
|
||||||
|
out.write entry.get_input_stream.read
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.apply_template_xml(xml, params)
|
||||||
|
doc = Nokogiri::XML(xml)
|
||||||
|
|
||||||
|
doc.xpath("//*[contains(text(), 'DISPLAY_NAME')]").each do |node|
|
||||||
|
node.content = node.content.gsub("DISPLAY_NAME", params["DISPLAY_NAME"])
|
||||||
|
end
|
||||||
|
|
||||||
|
address_lines = params['ADDRESS'].split("\n")
|
||||||
|
doc.xpath("//*[contains(text(), 'ADDRESS')]").each do |node|
|
||||||
|
newnodes = [node] + address_lines[1..].map do |line|
|
||||||
|
node.clone.tap do |c|
|
||||||
|
c.content = c.content.gsub("ADDRESS", line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
node.content = node.content.gsub("ADDRESS", address_lines.first)
|
||||||
|
|
||||||
|
newnodes.each_cons(2) do |p, n|
|
||||||
|
p.add_next_sibling(n)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
doc.to_xml(save_with: 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.generate(template, members)
|
||||||
|
Dir.mktmpdir do |directory|
|
||||||
|
members.each do |member|
|
||||||
|
odt = apply_template(template, {
|
||||||
|
"DISPLAY_NAME" => member.display_name,
|
||||||
|
"ADDRESS" => member.address,
|
||||||
|
})
|
||||||
|
|
||||||
|
File.open("#{directory}/#{member.number}.odt", "wb") { |out| out.write(odt.string) }
|
||||||
|
end
|
||||||
|
|
||||||
|
`libreoffice --convert-to pdf --outdir #{directory} #{directory}/*.odt`
|
||||||
|
|
||||||
|
pdf = CombinePDF.new
|
||||||
|
|
||||||
|
Dir["#{directory}/*.pdf"].each do |file|
|
||||||
|
pdf << CombinePDF.load(file)
|
||||||
|
end
|
||||||
|
|
||||||
|
pdf.to_pdf
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -1,4 +1,5 @@
|
|||||||
class NotificationMailer < ApplicationMailer
|
class NotificationMailer < ApplicationMailer
|
||||||
|
before_action :set_notification
|
||||||
|
|
||||||
# Subject can be set in your I18n file at config/locales/en.yml
|
# Subject can be set in your I18n file at config/locales/en.yml
|
||||||
# with the following lookup:
|
# with the following lookup:
|
||||||
@ -6,63 +7,32 @@ class NotificationMailer < ApplicationMailer
|
|||||||
# en.notification_mailer.expiration_in_60d.subject
|
# en.notification_mailer.expiration_in_60d.subject
|
||||||
#
|
#
|
||||||
def expiration_in_60d
|
def expiration_in_60d
|
||||||
@notification = params[:notification]
|
mail to: @notification.member.email
|
||||||
|
|
||||||
mail to: params[:notification].member.email
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subject can be set in your I18n file at config/locales/en.yml
|
|
||||||
# with the following lookup:
|
|
||||||
#
|
|
||||||
# en.notification_mailer.expiration_in_30d.subject
|
|
||||||
#
|
|
||||||
def expiration_in_30d
|
def expiration_in_30d
|
||||||
@notification = params[:notification]
|
mail to: @notification.member.email
|
||||||
|
|
||||||
mail to: params[:notification].member.email
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subject can be set in your I18n file at config/locales/en.yml
|
|
||||||
# with the following lookup:
|
|
||||||
#
|
|
||||||
# en.notification_mailer.expired.subject
|
|
||||||
#
|
|
||||||
def expired
|
def expired
|
||||||
@notification = params[:notification]
|
mail to: @notification.member.email
|
||||||
|
|
||||||
mail to: params[:notification].member.email
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subject can be set in your I18n file at config/locales/en.yml
|
|
||||||
# with the following lookup:
|
|
||||||
#
|
|
||||||
# en.notification_mailer.expired_30d_ago.subject
|
|
||||||
#
|
|
||||||
def expired_30d_ago
|
def expired_30d_ago
|
||||||
@notification = params[:notification]
|
mail to: @notification.member.email
|
||||||
|
|
||||||
mail to: params[:notification].member.email
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subject can be set in your I18n file at config/locales/en.yml
|
|
||||||
# with the following lookup:
|
|
||||||
#
|
|
||||||
# en.notification_mailer.expired_60d_ago.subject
|
|
||||||
#
|
|
||||||
def expired_60d_ago
|
def expired_60d_ago
|
||||||
@notification = params[:notification]
|
mail to: @notification.member.email
|
||||||
|
|
||||||
mail to: params[:notification].member.email
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Subject can be set in your I18n file at config/locales/en.yml
|
|
||||||
# with the following lookup:
|
|
||||||
#
|
|
||||||
# en.notification_mailer.cancelled.subject
|
|
||||||
#
|
|
||||||
def cancelled
|
def cancelled
|
||||||
@notification = params[:notification]
|
mail to: @notification.member.email
|
||||||
|
end
|
||||||
|
|
||||||
mail to: params[:notification].member.email
|
private
|
||||||
|
def set_notification
|
||||||
|
@notification = params[:notification]
|
||||||
|
@link = @notification.member.regular_ifthenpay_link
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,18 +2,77 @@
|
|||||||
|
|
||||||
<h1><%= t 'members.index.title' %></h1>
|
<h1><%= t 'members.index.title' %></h1>
|
||||||
|
|
||||||
<%= link_to t('members.index.actions.new'), new_member_path %>
|
<p><%= link_to t('members.index.actions.new'), new_member_path %></p>
|
||||||
|
|
||||||
<%= form_with url: members_path, method: :get do |form| %>
|
<%= form_with url: members_path, method: :get do |form| %>
|
||||||
<%= form.text_field :q %>
|
<fieldset>
|
||||||
<%= form.select :status, %w[ any active passive pending expired cancelled ], selected: params[:status] %>
|
<legend>Filtrar</legend>
|
||||||
<%= form.select :category, %w[ any student employed unemployed retired ], selected: params[:category], multiple: true %>
|
|
||||||
<%= form.submit 'Search', name: '' %>
|
<table class='noborder lined'>
|
||||||
<% if params[:q].present? || params[:status].present? || params[:category].present? %>
|
<tr>
|
||||||
<%= link_to t('members.index.actions.clear_search'), members_path %>
|
<td><%= t 'members.attributes.display_name' %></td>
|
||||||
<% end %>
|
<td><%= form.text_field :display_name, value: params[:display_name] %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><%= t 'members.attributes.email' %></td>
|
||||||
|
<td><%= form.text_field :email, value: params[:email] %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><%= t 'members.attributes.identification_number' %></td>
|
||||||
|
<td><%= form.text_field :identification_number, value: params[:identification_number] %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><%= t 'members.attributes.status' %></td>
|
||||||
|
<td><%= form.select :status, %w[ any active passive pending expired cancelled ], { selected: params[:status] }, { multiple: true } %></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><%= t 'members.attributes.category' %></td>
|
||||||
|
<td>
|
||||||
|
<%= form.select :category, %w[ any student employed unemployed retired ], { selected: params[:category] }, { multiple: true } %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><%= t 'members.attributes.prefers_postal' %></td>
|
||||||
|
<td>
|
||||||
|
<%= form.select :prefers_postal, %w[ any yes no ], selected: params[:prefers_postal] %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p><%= form.submit 'Search', name: '' %></p>
|
||||||
|
|
||||||
|
<% if params[:q].present? || params[:status].present? || params[:category].present? %>
|
||||||
|
<%= link_to t('members.index.actions.clear_search'), members_path %>
|
||||||
|
<% end %>
|
||||||
|
</fieldset>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<%= form_with url: letters_path do |form| %>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Gerar PDF</legend>
|
||||||
|
|
||||||
|
<% (params.fetch(:status, []) - ['any', '']).each do |status| %>
|
||||||
|
<%= form.hidden_field 'status[]', value: status %>
|
||||||
|
<% end %>
|
||||||
|
<% (params.fetch(:category, []) - ['any', '']).each do |category| %>
|
||||||
|
<%= form.hidden_field 'category[]', value: category %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= form.hidden_field :display_name, value: params[:display_name] %>
|
||||||
|
<%= form.hidden_field :email, value: params[:email] %>
|
||||||
|
<%= form.hidden_field :identification_number, value: params[:identification_number] %>
|
||||||
|
<%= form.hidden_field :prefers_postal, value: params[:prefers_postal] %>
|
||||||
|
|
||||||
|
<%= form.file_field :template, required: true %>
|
||||||
|
<%= form.submit 'Generate PDF' %>
|
||||||
|
</fieldset>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
<table class='zebra'>
|
<table class='zebra'>
|
||||||
<tr>
|
<tr>
|
||||||
<th><%= link_to_current_with_sort t('members.attributes.number'), 'number.asc' %></th>
|
<th><%= link_to_current_with_sort t('members.attributes.number'), 'number.asc' %></th>
|
||||||
|
@ -54,12 +54,20 @@
|
|||||||
<th><%= t('notifications.attributes.to_be_sent_on') %></th>
|
<th><%= t('notifications.attributes.to_be_sent_on') %></th>
|
||||||
<th><%= t('notifications.attributes.template') %></th>
|
<th><%= t('notifications.attributes.template') %></th>
|
||||||
<th><%= t('notifications.attributes.status') %></th>
|
<th><%= t('notifications.attributes.status') %></th>
|
||||||
|
<th><%= t('members.show.contribution_actions') %></th>
|
||||||
</tr>
|
</tr>
|
||||||
<% @member.notifications.order(to_be_sent_on: :desc).each do |notification| %>
|
<% @member.notifications.order(to_be_sent_on: :desc).each do |notification| %>
|
||||||
<tr>
|
<tr>
|
||||||
<td><%= notification.to_be_sent_on %></td>
|
<td><%= notification.to_be_sent_on %></td>
|
||||||
<td><code><%= notification.template %></code></td>
|
<td><code><%= notification.template %></code></td>
|
||||||
<td><%= notification_status(notification.status) %></td>
|
<td><%= notification_status(notification.status) %></td>
|
||||||
|
<td>
|
||||||
|
<% if notification.status == 'scheduled' %>
|
||||||
|
<%= form_with url: deliver_notification_path(notification) do |form| %>
|
||||||
|
<%= form.submit t('members.show.actions.deliver_notification') %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<% end %>
|
||||||
</table>
|
</table>
|
||||||
|
@ -30,8 +30,10 @@ pt:
|
|||||||
edit: "Editar detalhes"
|
edit: "Editar detalhes"
|
||||||
edit_contribution: "Editar"
|
edit_contribution: "Editar"
|
||||||
delete_contribution: "Apagar"
|
delete_contribution: "Apagar"
|
||||||
|
deliver_notification: "Enviar agora"
|
||||||
contribution_history: "Histórico de contribuições"
|
contribution_history: "Histórico de contribuições"
|
||||||
notifications: "Notificações por correio electrónico"
|
notifications: "Notificações por correio electrónico"
|
||||||
|
contribution_actions: "Acções"
|
||||||
edit:
|
edit:
|
||||||
title: "Editar detalhes de membro"
|
title: "Editar detalhes de membro"
|
||||||
actions:
|
actions:
|
||||||
|
@ -19,5 +19,12 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :notifications, only: [] do
|
||||||
|
member do
|
||||||
|
post :deliver
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
resource :board, only: [:edit, :update]
|
resource :board, only: [:edit, :update]
|
||||||
|
resource :letters, only: [:create]
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user