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 "rails", "~> 7.0.3"
|
||||
gem "ransack"
|
||||
gem "nokogiri"
|
||||
gem "rubyzip"
|
||||
gem "combine_pdf"
|
||||
|
||||
group :development, :test do
|
||||
gem "debug", platforms: %i[ mri mingw x64_mingw ]
|
||||
|
@ -82,6 +82,9 @@ GEM
|
||||
bcrypt (>= 3.1.1)
|
||||
email_validator (~> 2.0)
|
||||
railties (>= 5.0)
|
||||
combine_pdf (1.0.22)
|
||||
matrix
|
||||
ruby-rc4 (>= 0.1.5)
|
||||
concurrent-ruby (1.1.10)
|
||||
crass (1.0.6)
|
||||
debug (1.5.0)
|
||||
@ -115,6 +118,7 @@ GEM
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.2)
|
||||
matrix (0.4.2)
|
||||
method_source (1.0.0)
|
||||
mini_mime (1.1.2)
|
||||
minitest (5.16.0)
|
||||
@ -188,6 +192,8 @@ GEM
|
||||
io-console (~> 0.5)
|
||||
request_store (1.5.1)
|
||||
rack (>= 1.4)
|
||||
ruby-rc4 (0.1.5)
|
||||
rubyzip (2.3.2)
|
||||
strscan (3.0.3)
|
||||
thor (1.2.1)
|
||||
timecop (0.9.5)
|
||||
@ -210,9 +216,11 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
bootsnap
|
||||
clearance
|
||||
combine_pdf
|
||||
debug
|
||||
dotenv-rails
|
||||
importmap-rails
|
||||
nokogiri
|
||||
paper_trail
|
||||
pg (~> 1.1)
|
||||
propshaft
|
||||
@ -220,6 +228,7 @@ DEPENDENCIES
|
||||
pundit
|
||||
rails (~> 7.0.3)
|
||||
ransack
|
||||
rubyzip
|
||||
timecop
|
||||
web-console
|
||||
|
||||
|
@ -23,3 +23,5 @@ ul {
|
||||
}
|
||||
|
||||
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 ]
|
||||
helper_method :sort_params
|
||||
|
||||
include MemberFilter
|
||||
|
||||
# GET /members
|
||||
def index
|
||||
params.delete(:status) if params[:status] == 'any'
|
||||
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
|
||||
@members = filtered_members
|
||||
end
|
||||
|
||||
# GET /members/1
|
||||
@ -68,17 +66,4 @@ class MembersController < ApplicationController
|
||||
def member_params
|
||||
params.fetch(:member, {}).permit(:display_name, :email, :identification_number, :category, :address, :joined_on, :expires_on, :wants_mailing_list, :prefers_postal)
|
||||
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
|
||||
|
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
|
||||
before_action :set_notification
|
||||
|
||||
# Subject can be set in your I18n file at config/locales/en.yml
|
||||
# with the following lookup:
|
||||
@ -6,63 +7,32 @@ class NotificationMailer < ApplicationMailer
|
||||
# en.notification_mailer.expiration_in_60d.subject
|
||||
#
|
||||
def expiration_in_60d
|
||||
@notification = params[:notification]
|
||||
|
||||
mail to: params[:notification].member.email
|
||||
mail to: @notification.member.email
|
||||
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
|
||||
@notification = params[:notification]
|
||||
|
||||
mail to: params[:notification].member.email
|
||||
mail to: @notification.member.email
|
||||
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
|
||||
@notification = params[:notification]
|
||||
|
||||
mail to: params[:notification].member.email
|
||||
mail to: @notification.member.email
|
||||
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
|
||||
@notification = params[:notification]
|
||||
|
||||
mail to: params[:notification].member.email
|
||||
mail to: @notification.member.email
|
||||
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
|
||||
@notification = params[:notification]
|
||||
|
||||
mail to: params[:notification].member.email
|
||||
mail to: @notification.member.email
|
||||
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
|
||||
@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
|
||||
|
@ -2,18 +2,77 @@
|
||||
|
||||
<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.text_field :q %>
|
||||
<%= form.select :status, %w[ any active passive pending expired cancelled ], selected: params[:status] %>
|
||||
<%= form.select :category, %w[ any student employed unemployed retired ], selected: params[:category], multiple: true %>
|
||||
<%= form.submit 'Search', name: '' %>
|
||||
<fieldset>
|
||||
<legend>Filtrar</legend>
|
||||
|
||||
<table class='noborder lined'>
|
||||
<tr>
|
||||
<td><%= t 'members.attributes.display_name' %></td>
|
||||
<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 %>
|
||||
|
||||
<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'>
|
||||
<tr>
|
||||
<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.template') %></th>
|
||||
<th><%= t('notifications.attributes.status') %></th>
|
||||
<th><%= t('members.show.contribution_actions') %></th>
|
||||
</tr>
|
||||
<% @member.notifications.order(to_be_sent_on: :desc).each do |notification| %>
|
||||
<tr>
|
||||
<td><%= notification.to_be_sent_on %></td>
|
||||
<td><code><%= notification.template %></code></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>
|
||||
<% end %>
|
||||
</table>
|
||||
|
@ -30,8 +30,10 @@ pt:
|
||||
edit: "Editar detalhes"
|
||||
edit_contribution: "Editar"
|
||||
delete_contribution: "Apagar"
|
||||
deliver_notification: "Enviar agora"
|
||||
contribution_history: "Histórico de contribuições"
|
||||
notifications: "Notificações por correio electrónico"
|
||||
contribution_actions: "Acções"
|
||||
edit:
|
||||
title: "Editar detalhes de membro"
|
||||
actions:
|
||||
|
@ -19,5 +19,12 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resource :board, only: [:edit, :update]
|
||||
resources :notifications, only: [] do
|
||||
member do
|
||||
post :deliver
|
||||
end
|
||||
end
|
||||
|
||||
resource :board, only: [:edit, :update]
|
||||
resource :letters, only: [:create]
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user