Merge branch 'dev'
This commit is contained in:
commit
46cb8a7a92
18 changed files with 433 additions and 4 deletions
|
@ -27,4 +27,21 @@
|
|||
font-size: 11px;
|
||||
margin-bottom: $default-spacer * 2;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: $orange-bg;
|
||||
padding: $default-spacer;
|
||||
margin-bottom: $default-spacer * 2;
|
||||
border-radius: 3px;
|
||||
|
||||
h4 {
|
||||
margin-top: $default-spacer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
padding-left: $default-spacer * 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
app/jobs/etablissement_update_job.rb
Normal file
19
app/jobs/etablissement_update_job.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
class EtablissementUpdateJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(dossier, siret)
|
||||
etablissement_attributes = SIRETService.fetch(siret, dossier.procedure_id, dossier)
|
||||
|
||||
if etablissement_attributes.present?
|
||||
if dossier.entreprise.present?
|
||||
dossier.entreprise.destroy
|
||||
end
|
||||
if dossier.etablissement.present?
|
||||
dossier.etablissement.destroy
|
||||
end
|
||||
etablissement_attributes = ActionController::Parameters.new(etablissement_attributes).permit!
|
||||
etablissement = dossier.build_etablissement(etablissement_attributes)
|
||||
etablissement.save
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,6 +17,20 @@ class AttestationTemplate < ApplicationRecord
|
|||
Attestation.new(title: replace_tags(title, dossier), pdf: build_pdf(dossier))
|
||||
end
|
||||
|
||||
def unspecified_champs_for_dossier(dossier)
|
||||
all_champs_with_libelle_index = (dossier.champs + dossier.champs_private)
|
||||
.reduce({}) do |acc, champ|
|
||||
acc[champ.libelle] = champ
|
||||
acc
|
||||
end
|
||||
|
||||
used_tags.map do |used_tag|
|
||||
corresponding_champ = all_champs_with_libelle_index[used_tag]
|
||||
|
||||
corresponding_champ.value.blank? ? corresponding_champ : nil
|
||||
end.compact
|
||||
end
|
||||
|
||||
def dup
|
||||
result = AttestationTemplate.new(title: title, body: body, footer: footer, activated: activated)
|
||||
|
||||
|
@ -33,6 +47,17 @@ class AttestationTemplate < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def used_tags
|
||||
delimiters_regex = /--(?<capture>((?!--).)*)--/
|
||||
|
||||
# We can't use flat_map as scan will return 3 levels of array,
|
||||
# using flat_map would give us 2, whereas flatten will
|
||||
# give us 1, which is what we want
|
||||
[title, body]
|
||||
.map { |str| str.scan(delimiters_regex) }
|
||||
.flatten
|
||||
end
|
||||
|
||||
def logo_signature_file_size
|
||||
%i[logo signature]
|
||||
.select { |file_name| send(file_name).present? }
|
||||
|
|
|
@ -283,8 +283,18 @@ class Dossier < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def unspecified_attestation_champs
|
||||
attestation_template = procedure.attestation_template
|
||||
|
||||
if attestation_template&.activated?
|
||||
attestation_template.unspecified_champs_for_dossier(self)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def build_attestation
|
||||
if procedure.attestation_template.present? && procedure.attestation_template.activated?
|
||||
if procedure.attestation_template&.activated?
|
||||
procedure.attestation_template.attestation_for(self)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -265,6 +265,18 @@ class Procedure < ApplicationRecord
|
|||
update_attribute('whitelisted_at', DateTime.now)
|
||||
end
|
||||
|
||||
def closed_mail_template_attestation_inconsistency_state
|
||||
# As an optimization, don’t check the predefined templates (they are presumed correct)
|
||||
if closed_mail.present?
|
||||
tag_present = closed_mail.body.include?("--lien attestation--")
|
||||
if attestation_template&.activated? && !tag_present
|
||||
:missing_tag
|
||||
elsif !attestation_template&.activated? && tag_present
|
||||
:extraneous_tag
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def field_hash(label, table, column)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
- case @procedure.closed_mail_template_attestation_inconsistency_state
|
||||
- when :missing_tag
|
||||
.center
|
||||
.alert{ class: flash_class('alert') }
|
||||
Cette procédure comporte une attestation, mais l’accusé d’acceptation ne la mentionne pas :
|
||||
- if !@procedure.locked?
|
||||
= link_to('désactivez l’attestation', edit_admin_procedure_attestation_template_path(@procedure))
|
||||
ou
|
||||
= link_to('ajoutez la balise', edit_admin_procedure_mail_template_path(@procedure, Mails::ClosedMail::SLUG))
|
||||
%code{ style: "white-space: pre-wrap;" }
|
||||
\--lien attestation--
|
||||
à l’accusé d’acceptation
|
||||
- when :extraneous_tag
|
||||
.center
|
||||
.alert{ class: flash_class('alert') }
|
||||
Cette procédure ne comporte pas d’attestation, mais l’accusé d’acceptation en mentionne une :
|
||||
= link_to('activez l’attestation', edit_admin_procedure_attestation_template_path(@procedure))
|
||||
ou
|
||||
= link_to('enlevez la balise', edit_admin_procedure_mail_template_path(@procedure, Mails::ClosedMail::SLUG))
|
||||
%code{ style: "white-space: pre-wrap;" }
|
||||
\--lien attestation--
|
||||
de l’accusé d’acceptation
|
|
@ -1,3 +1,4 @@
|
|||
= render partial: 'admin/closed_mail_template_attestation_inconsistency_alert'
|
||||
#attestation-template-edit.row.white-back
|
||||
= form_for @attestation_template, url: admin_procedure_attestation_template_path do |f|
|
||||
.row
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- if params[:id] == 'closed_mail'
|
||||
= render partial: 'admin/closed_mail_template_attestation_inconsistency_alert'
|
||||
.white-back
|
||||
%h3
|
||||
= @mail_template.class.const_get(:DISPLAYED_NAME)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
= render partial: 'admin/closed_mail_template_attestation_inconsistency_alert'
|
||||
#custom-mails
|
||||
.wrapper
|
||||
%h1 E-mails personnalisables
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
= render partial: 'admin/closed_mail_template_attestation_inconsistency_alert'
|
||||
.row.white-back
|
||||
#procedure_show
|
||||
- if @facade.procedure.brouillon?
|
||||
|
|
|
@ -8,6 +8,25 @@
|
|||
- if title == 'Accepter'
|
||||
%p.help
|
||||
L'acceptation du dossier envoie automatiquement une attestation à l'usager.
|
||||
|
||||
- unspecified_attestation_champs = dossier.unspecified_attestation_champs
|
||||
- if unspecified_attestation_champs.present?
|
||||
.warning
|
||||
Attention, les valeurs suivantes n'ont pas été renseignées mais sont nécessaires pour pouvoir envoyer une attestation valide :
|
||||
- unspecified_champs, unspecified_annotations_privees = unspecified_attestation_champs.partition(&:private)
|
||||
|
||||
- if unspecified_champs.present?
|
||||
%h4 Champs de la demande
|
||||
%ul
|
||||
- unspecified_attestation_champs.each do |unspecified_champ|
|
||||
%li= unspecified_champ.libelle
|
||||
|
||||
- if unspecified_annotations_privees.present?
|
||||
%h4 Annotations privées
|
||||
%ul
|
||||
- unspecified_annotations_privees.each do |unspecified_annotations_privee|
|
||||
%li= unspecified_annotations_privee.libelle
|
||||
|
||||
.text-right
|
||||
%span.button{ onclick: 'DS.motivationCancel();' } Annuler
|
||||
= button_tag 'Valider la décision', name: :process_action, value: process_action, class: 'button primary', title: title, data: { confirm: confirm }
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
%h1= @dossier.procedure.libelle
|
||||
|
||||
.container
|
||||
%p
|
||||
Pour vous aider à remplir votre dossier, vous pouvez consulter
|
||||
= link_to 'le guide de cette démarche', @dossier.procedure.lien_notice, { target: '_blank' }
|
||||
- if @dossier.procedure.lien_notice.present?
|
||||
%p
|
||||
Pour vous aider à remplir votre dossier, vous pouvez consulter
|
||||
= link_to 'le guide de cette démarche', @dossier.procedure.lien_notice, { target: '_blank' }
|
||||
|
||||
%p.thanks Les champs avec une asterisque (*) sont obligatoires.
|
||||
|
||||
|
|
83
lib/mailers/attestation_closed_mail_discrepancy_mailer.rb
Normal file
83
lib/mailers/attestation_closed_mail_discrepancy_mailer.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
module Mailers
|
||||
class AttestationClosedMailDiscrepancyMailer < ApplicationMailer
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
def missing_attestation_tag_email(admin, procedures)
|
||||
procedures = procedures.sort_by(&:id)
|
||||
mail(to: admin.email, subject: subject(procedures), body: body(procedures))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subject(procedures)
|
||||
if procedures.count == 1
|
||||
procedure_ids = "votre procédure nº #{procedures.first.id}"
|
||||
else
|
||||
procedure_ids = 'vos procédures nº ' + procedures.map{ |p| p.id }.join(', ')
|
||||
end
|
||||
"demarches-simplifiees.fr – mise à jour nécessaire de l’accusé d’acceptation de #{procedure_ids}"
|
||||
end
|
||||
|
||||
def body(procedures)
|
||||
<<~HEREDOC
|
||||
Bonjour,
|
||||
|
||||
Pour des raisons de confidentialité, le mode de transmission des attestations aux usagers évolue.
|
||||
|
||||
À compter du 30 avril, les mails d’accusé d’acceptation émis par demarches-simplifiees.fr ne
|
||||
comporteront plus d’attestation en pièce jointe comme c’est le cas aujourd’hui.
|
||||
|
||||
À la place, le mail contiendra un lien permettant à l’usager de télécharger son
|
||||
attestation dirctement dans son espace sécurisé sur demarches-simplifiees.fr.
|
||||
|
||||
Ce lien de téléchargement est généré par la balise --lien attestation--.
|
||||
|
||||
#{detail_procedures(procedures)}
|
||||
|
||||
Pour toute question vous pouvez nous joindre par téléphone au 01 76 42 02 87
|
||||
ou sur l’adresse email contact@demarches-simplifiees.fr.
|
||||
-- \nL’équipe demarches-simplifiees.fr
|
||||
HEREDOC
|
||||
end
|
||||
|
||||
def detail_procedures(procedures)
|
||||
if procedures.count == 1
|
||||
p = procedures.first
|
||||
|
||||
<<~HEREDOC.chomp
|
||||
Vous êtes administrateur de la procédure suivante :
|
||||
#{p.libelle} (nº #{p.id})
|
||||
|
||||
Cette procédure donne lieu à l’émission d’une attestation, et son accusé
|
||||
d’acceptation a été personnalisé. Pour respecter la rédaction de votre accusé
|
||||
d’acceptation, nous ne prendrons pas l’initiative d’y ajouter la balise --lien attestation--.
|
||||
|
||||
Afin que vos usagers puissent continuer à accéder facilement à leurs attestations
|
||||
dans leurs démarches futures, nous vous invitons à ajouter à votre convenance la
|
||||
balise --lien attestation-- dans votre accusé d’acceptation. Vous pouvez le faire en
|
||||
cliquant sur le lien suivant :
|
||||
|
||||
#{edit_admin_procedure_mail_template_url(p, Mails::ClosedMail::SLUG)}
|
||||
HEREDOC
|
||||
else
|
||||
liste_procedures = procedures.map { |p| "- #{p.libelle} (nº #{p.id}) – #{edit_admin_procedure_mail_template_url(p, Mails::ClosedMail::SLUG)}" }.join("\n")
|
||||
|
||||
<<~HEREDOC.chomp
|
||||
Vous êtes administrateur sur plusieurs procédures qui donnent lieu à l’émission
|
||||
d’une attestation, et dont l’accusé d’acceptation a été personnalisé. Pour respecter
|
||||
la rédaction de vos accusés d’acceptation, nous ne prendrons pas l’initiative d’y
|
||||
ajouter de balise --lien attestation--.
|
||||
|
||||
Afin que vos usagers puissent continuer à accéder facilement à leurs attestations
|
||||
dans leurs démarches futures, nous vous invitons à ajouter à votre convenance la
|
||||
balise --lien attestation-- dans vos accusés d’acceptation.
|
||||
|
||||
Vous trouverez ci-après la liste des démarches concernées, ainsi que les liens vous
|
||||
permettant d’éditer les accusés d’acceptation correspondants.
|
||||
|
||||
#{liste_procedures}
|
||||
HEREDOC
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
namespace :'2018_04_03_attestation_closed_mail_discrepancy' do
|
||||
task mail_adminstrators: :environment do
|
||||
Administrateur.includes(:procedures).find_each(batch_size: 10) do |admin|
|
||||
procedures = admin.procedures.where(archived_at: nil).select { |p| p.closed_mail_template_attestation_inconsistency_state == :missing_tag }
|
||||
if procedures.any?
|
||||
Mailers::AttestationClosedMailDiscrepancyMailer.missing_attestation_tag_email(admin, procedures).deliver_later
|
||||
print "#{admin.email}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
namespace :'2018_04_04_fetch_etablissement_with_no_entreprise' do
|
||||
task fetch: :environment do
|
||||
dossiers = Entreprise.joins('LEFT JOIN etablissements et ON entreprises.id = et.entreprise_id')
|
||||
.where('et.id IS NULL')
|
||||
.map(&:dossier_id).map { |id| Dossier.unscoped.find_by(id: id) }.compact
|
||||
|
||||
dossiers.each do |dossier|
|
||||
siret = dossier.entreprise.siret_siege_social
|
||||
|
||||
puts "Fetch siret: #{siret} for dossier: #{dossier.id}"
|
||||
|
||||
if siret
|
||||
EtablissementUpdateJob.perform_later(dossier, siret)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -643,6 +643,64 @@ describe Dossier do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#unspecified_attestation_champs" do
|
||||
let(:procedure) { create(:procedure, attestation_template: attestation_template) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure, state: :en_instruction) }
|
||||
|
||||
subject { dossier.unspecified_attestation_champs.map(&:libelle) }
|
||||
|
||||
context "without attestation template" do
|
||||
let(:attestation_template) { nil }
|
||||
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
|
||||
context "with attestation template" do
|
||||
# Test all combinations:
|
||||
# - with tag specified and unspecified
|
||||
# - with tag in body and tag in title
|
||||
# - with tag correponsing to a champ and an annotation privée
|
||||
# - with a dash in the champ libelle / tag
|
||||
let(:title) { "voici --specified champ-in-title-- un --unspecified champ-in-title-- beau --specified annotation privée-in-title-- titre --unspecified annotation privée-in-title-- non" }
|
||||
let(:body) { "voici --specified champ-in-body-- un --unspecified champ-in-body-- beau --specified annotation privée-in-body-- body --unspecified annotation privée-in-body-- non ?" }
|
||||
let(:attestation_template) { create(:attestation_template, title: title, body: body, activated: activated) }
|
||||
|
||||
context "which is disabled" do
|
||||
let(:activated) { false }
|
||||
|
||||
it { is_expected.to eq([]) }
|
||||
end
|
||||
|
||||
context "wich is enabled" do
|
||||
let(:activated) { true }
|
||||
|
||||
let!(:tdc_1) { create(:type_de_champ, libelle: "specified champ-in-title", procedure: procedure) }
|
||||
let!(:tdc_2) { create(:type_de_champ, libelle: "unspecified champ-in-title", procedure: procedure) }
|
||||
let!(:tdc_3) { create(:type_de_champ, libelle: "specified champ-in-body", procedure: procedure) }
|
||||
let!(:tdc_4) { create(:type_de_champ, libelle: "unspecified champ-in-body", procedure: procedure) }
|
||||
let!(:tdc_5) { create(:type_de_champ, private: true, libelle: "specified annotation privée-in-title", procedure: procedure) }
|
||||
let!(:tdc_6) { create(:type_de_champ, private: true, libelle: "unspecified annotation privée-in-title", procedure: procedure) }
|
||||
let!(:tdc_7) { create(:type_de_champ, private: true, libelle: "specified annotation privée-in-body", procedure: procedure) }
|
||||
let!(:tdc_8) { create(:type_de_champ, private: true, libelle: "unspecified annotation privée-in-body", procedure: procedure) }
|
||||
|
||||
before do
|
||||
(dossier.champs + dossier.champs_private)
|
||||
.select { |c| c.libelle.match?(/^specified/) }
|
||||
.each { |c| c.update_attribute(:value, "specified") }
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq([
|
||||
"unspecified champ-in-title",
|
||||
"unspecified annotation privée-in-title",
|
||||
"unspecified champ-in-body",
|
||||
"unspecified annotation privée-in-body"
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_attestation' do
|
||||
let(:attestation_template) { nil }
|
||||
let(:procedure) { create(:procedure, attestation_template: attestation_template) }
|
||||
|
|
|
@ -67,6 +67,90 @@ describe Procedure do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#closed_mail_template_attestation_inconsistency_state' do
|
||||
let(:procedure_without_attestation) { create(:procedure, closed_mail: closed_mail) }
|
||||
let(:procedure_with_active_attestation) do
|
||||
procedure = create(:procedure, closed_mail: closed_mail)
|
||||
create(:attestation_template, procedure: procedure, activated: true)
|
||||
procedure
|
||||
end
|
||||
let(:procedure_with_inactive_attestation) do
|
||||
procedure = create(:procedure, closed_mail: closed_mail)
|
||||
create(:attestation_template, procedure: procedure, activated: false)
|
||||
procedure
|
||||
end
|
||||
|
||||
subject { procedure.closed_mail_template_attestation_inconsistency_state }
|
||||
|
||||
context 'with a custom mail template' do
|
||||
context 'that contains a lien attestation tag' do
|
||||
let(:closed_mail) { create(:closed_mail, body: '--lien attestation--') }
|
||||
|
||||
context 'when the procedure doesn’t have an attestation' do
|
||||
let(:procedure) { procedure_without_attestation }
|
||||
|
||||
it { is_expected.to eq(:extraneous_tag) }
|
||||
end
|
||||
|
||||
context 'when the procedure has an active attestation' do
|
||||
let(:procedure) { procedure_with_active_attestation }
|
||||
|
||||
it { is_expected.to be(nil) }
|
||||
end
|
||||
|
||||
context 'when the procedure has an inactive attestation' do
|
||||
let(:procedure) { procedure_with_inactive_attestation }
|
||||
|
||||
it { is_expected.to eq(:extraneous_tag) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'that doesn’t contain a lien attestation tag' do
|
||||
let(:closed_mail) { create(:closed_mail) }
|
||||
|
||||
context 'when the procedure doesn’t have an attestation' do
|
||||
let(:procedure) { procedure_without_attestation }
|
||||
|
||||
it { is_expected.to be(nil) }
|
||||
end
|
||||
|
||||
context 'when the procedure has an active attestation' do
|
||||
let(:procedure) { procedure_with_active_attestation }
|
||||
|
||||
it { is_expected.to eq(:missing_tag) }
|
||||
end
|
||||
|
||||
context 'when the procedure has an inactive attestation' do
|
||||
let(:procedure) { procedure_with_inactive_attestation }
|
||||
|
||||
it { is_expected.to be(nil) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the default mail template' do
|
||||
let(:closed_mail) { nil }
|
||||
|
||||
context 'when the procedure doesn’t have an attestation' do
|
||||
let(:procedure) { procedure_without_attestation }
|
||||
|
||||
it { is_expected.to be(nil) }
|
||||
end
|
||||
|
||||
context 'when the procedure has an active attestation' do
|
||||
let(:procedure) { procedure_with_active_attestation }
|
||||
|
||||
it { is_expected.to be(nil) }
|
||||
end
|
||||
|
||||
context 'when the procedure has an inactive attestation' do
|
||||
let(:procedure) { procedure_with_inactive_attestation }
|
||||
|
||||
it { is_expected.to be(nil) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validation' do
|
||||
context 'libelle' do
|
||||
it { is_expected.not_to allow_value(nil).for(:libelle) }
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'admin/_closed_mail_template_attestation_inconsistency_alert.html.haml', type: :view do
|
||||
let(:procedure) { create(:procedure, closed_mail: closed_mail, published_at: published_at) }
|
||||
let(:published_at) { nil }
|
||||
|
||||
def alert
|
||||
assign(:procedure, procedure)
|
||||
render
|
||||
rendered
|
||||
end
|
||||
|
||||
context 'when there is no inconsistency' do
|
||||
let(:closed_mail) { nil }
|
||||
|
||||
it { expect(alert).to be_empty }
|
||||
end
|
||||
|
||||
context 'when there is no active attestation but the closed mail template mentions one' do
|
||||
let(:closed_mail) { create(:closed_mail, body: '--lien attestation--') }
|
||||
|
||||
it { expect(alert).to include("Cette procédure ne comporte pas d’attestation, mais l’accusé d’acceptation en mentionne une") }
|
||||
it { expect(alert).to include(edit_admin_procedure_attestation_template_path(procedure)) }
|
||||
it { expect(alert).to include(edit_admin_procedure_mail_template_path(procedure, Mails::ClosedMail::SLUG)) }
|
||||
end
|
||||
|
||||
context 'when there is an active attestation but the closed mail template does not mention it' do
|
||||
let(:closed_mail) { create(:closed_mail) }
|
||||
let!(:attestation_template) { create(:attestation_template, procedure: procedure, activated: true) }
|
||||
|
||||
it { expect(alert).to include("Cette procédure comporte une attestation, mais l’accusé d’acceptation ne la mentionne pas") }
|
||||
it { expect(alert).to include(edit_admin_procedure_mail_template_path(procedure, Mails::ClosedMail::SLUG)) }
|
||||
|
||||
context 'when the procedure has been published, the attestation cannot be deactivated' do
|
||||
let(:published_at) { Time.now }
|
||||
|
||||
it { expect(procedure.locked?).to be_truthy }
|
||||
it { expect(alert).not_to include(edit_admin_procedure_attestation_template_path(procedure)) }
|
||||
end
|
||||
|
||||
context 'when the procedure is still a draft' do
|
||||
it { expect(procedure.locked?).to be_falsey }
|
||||
it { expect(alert).to include(edit_admin_procedure_attestation_template_path(procedure)) }
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue