Compare commits
No commits in common. "bed4fee11b4bb3637db191284bd84ab20624257b" and "2e7b03e9ef96119b196cdbf8e09ff918a3ad3a4a" have entirely different histories.
bed4fee11b
...
2e7b03e9ef
@ -1,3 +0,0 @@
|
|||||||
log/*
|
|
||||||
tmp/*
|
|
||||||
Dockerfile
|
|
12
Dockerfile
12
Dockerfile
@ -1,12 +0,0 @@
|
|||||||
FROM ruby:3.1.2-alpine
|
|
||||||
|
|
||||||
RUN apk add build-base libpq-dev tzdata
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ADD Gemfile* ./
|
|
||||||
|
|
||||||
RUN bundle
|
|
||||||
|
|
||||||
ADD . ./
|
|
||||||
|
|
||||||
CMD ["./entrypoint.sh"]
|
|
3
Gemfile
3
Gemfile
@ -14,9 +14,6 @@ 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,9 +82,6 @@ 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)
|
||||||
@ -118,7 +115,6 @@ 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)
|
||||||
@ -192,8 +188,6 @@ 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)
|
||||||
@ -216,11 +210,9 @@ 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
|
||||||
@ -228,7 +220,6 @@ DEPENDENCIES
|
|||||||
pundit
|
pundit
|
||||||
rails (~> 7.0.3)
|
rails (~> 7.0.3)
|
||||||
ransack
|
ransack
|
||||||
rubyzip
|
|
||||||
timecop
|
timecop
|
||||||
web-console
|
web-console
|
||||||
|
|
||||||
|
@ -23,5 +23,3 @@ ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
li { padding: 5px 0px; }
|
li { padding: 5px 0px; }
|
||||||
|
|
||||||
table.noborder { border: 0; margin: 0; }
|
|
||||||
|
@ -1,38 +0,0 @@
|
|||||||
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
|
|
@ -1,14 +0,0 @@
|
|||||||
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,11 +3,13 @@ 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
|
||||||
@members = filtered_members
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /members/1
|
# GET /members/1
|
||||||
@ -66,4 +68,17 @@ 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
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
|||||||
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,5 +1,4 @@
|
|||||||
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:
|
||||||
@ -7,32 +6,63 @@ 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
|
||||||
mail to: @notification.member.email
|
|
||||||
end
|
|
||||||
|
|
||||||
def expiration_in_30d
|
|
||||||
mail to: @notification.member.email
|
|
||||||
end
|
|
||||||
|
|
||||||
def expired
|
|
||||||
mail to: @notification.member.email
|
|
||||||
end
|
|
||||||
|
|
||||||
def expired_30d_ago
|
|
||||||
mail to: @notification.member.email
|
|
||||||
end
|
|
||||||
|
|
||||||
def expired_60d_ago
|
|
||||||
mail to: @notification.member.email
|
|
||||||
end
|
|
||||||
|
|
||||||
def cancelled
|
|
||||||
mail to: @notification.member.email
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def set_notification
|
|
||||||
@notification = params[:notification]
|
@notification = params[:notification]
|
||||||
@link = @notification.member.regular_ifthenpay_link
|
|
||||||
|
mail to: params[: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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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: params[:notification].member.email
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,77 +2,18 @@
|
|||||||
|
|
||||||
<h1><%= t 'members.index.title' %></h1>
|
<h1><%= t 'members.index.title' %></h1>
|
||||||
|
|
||||||
<p><%= link_to t('members.index.actions.new'), new_member_path %></p>
|
<%= link_to t('members.index.actions.new'), new_member_path %>
|
||||||
|
|
||||||
<%= form_with url: members_path, method: :get do |form| %>
|
<%= form_with url: members_path, method: :get do |form| %>
|
||||||
<fieldset>
|
<%= form.text_field :q %>
|
||||||
<legend>Filtrar</legend>
|
<%= 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 %>
|
||||||
<table class='noborder lined'>
|
<%= form.submit 'Search', name: '' %>
|
||||||
<tr>
|
<% if params[:q].present? || params[:status].present? || params[:category].present? %>
|
||||||
<td><%= t 'members.attributes.display_name' %></td>
|
<%= link_to t('members.index.actions.clear_search'), members_path %>
|
||||||
<td><%= form.text_field :display_name, value: params[:display_name] %></td>
|
<% end %>
|
||||||
</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,20 +54,12 @@
|
|||||||
<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,10 +30,8 @@ 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,12 +19,5 @@ 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
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
bin/rails db:migrate &&
|
|
||||||
bin/rails server
|
|
Loading…
Reference in New Issue
Block a user