Merge pull request #4034 from betagouv/dev

2019-07-03-01
This commit is contained in:
Paul Chavard 2019-07-03 16:13:03 +02:00 committed by GitHub
commit 53db2c95ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1872 additions and 633 deletions

View file

@ -1,6 +1,7 @@
source 'https://rubygems.org'
gem 'aasm'
gem 'actiontext', github: 'kobaltz/actiontext', branch: 'archive', require: 'action_text'
gem 'active_link_to' # Automatically set a class on active links
gem 'active_model_serializers'
gem 'activestorage-openstack', git: 'https://github.com/fredZen/activestorage-openstack.git', branch: 'frederic/fix_upload_signature'
@ -103,6 +104,7 @@ group :development, :test do
gem 'rspec-rails'
gem 'rspec_junit_formatter', require: false
gem 'ruby-debug-ide', require: false
gem 'simple_xlsx_reader'
gem 'spring' # Spring speeds up development by keeping your application running in the background
gem 'spring-commands-rspec'
end

View file

@ -1,3 +1,12 @@
GIT
remote: git://github.com/kobaltz/actiontext.git
revision: ef59c4ba99d1b7614dd47f5a294eef553224db88
branch: archive
specs:
actiontext (0.1.0)
nokogiri
rails (>= 5.2.0)
GIT
remote: https://github.com/fredZen/activestorage-openstack.git
revision: c71d5107a51701eab9d9267dd0000e6c1cf3e39a
@ -576,6 +585,9 @@ GEM
simple_form (4.1.0)
actionpack (>= 5.0)
activemodel (>= 5.0)
simple_xlsx_reader (1.0.4)
nokogiri
rubyzip
sinatra (2.0.5)
mustermann (~> 1.0)
rack (~> 2.0)
@ -666,6 +678,7 @@ PLATFORMS
DEPENDENCIES
aasm
actiontext!
active_link_to
active_model_serializers
activestorage-openstack!
@ -747,6 +760,7 @@ DEPENDENCIES
sentry-raven
shoulda-matchers
simple_form
simple_xlsx_reader
skylight
smart_listing
spreadsheet_architect

View file

@ -63,7 +63,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
AutoArchiveProcedureJob.set(cron: "* * * * *").perform_later
WeeklyOverviewJob.set(cron: "0 7 * * 1").perform_later
AutoReceiveDossiersForProcedureJob.set(cron: "* * * * *").perform_later(procedure_declaratoire_id, Dossier.states.fetch(:en_instruction))
DeclarativeProceduresJob.set(cron: "* * * * *").perform_later
UpdateAdministrateurUsageStatisticsJob.set(cron: "0 10 * * *").perform_later
FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later
Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later

View file

@ -0,0 +1,36 @@
//
// Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
// the trix-editor content (whether displayed or under editing). Feel free to incorporate this
// inclusion directly in any other asset bundle and remove this file.
//
// = require trix
// We need to override trix.csss image gallery styles to accommodate the
// <action-text-attachment> element we wrap around attachments. Otherwise,
// images in galleries will be squished by the max-width: 33%; rule.
.trix-content {
.attachment-gallery {
> action-text-attachment,
> .attachment {
flex: 1 0 33%;
padding: 0 0.5em;
max-width: 33%;
}
&.attachment-gallery--2,
&.attachment-gallery--4 {
> action-text-attachment,
> .attachment {
flex-basis: 50%;
max-width: 50%;
}
}
}
action-text-attachment {
.attachment {
padding: 0 !important;
max-width: 100% !important;
}
}
}

View file

@ -183,7 +183,7 @@
color: $black;
padding: $default-padding;
h4 {
.title {
font-size: 24px;
}
@ -266,6 +266,10 @@
color: $black;
margin-bottom: $default-spacer;
}
&.with-top-border {
border-top: 1px solid $grey;
}
}
.dropdown-form {

View file

@ -93,6 +93,13 @@ module Gestionnaires
render partial: 'state_button_refresh', locals: { dossier: dossier }
end
def repasser_en_instruction
flash.notice = "Le dossier #{dossier.id} a été repassé en instruction."
dossier.repasser_en_instruction!(current_gestionnaire)
render partial: 'state_button_refresh', locals: { dossier: dossier }
end
def terminer
motivation = params[:dossier] && params[:dossier][:motivation]
justificatif = params[:dossier] && params[:dossier][:justificatif_motivation]

View file

@ -168,7 +168,7 @@ module Gestionnaires
end
def download_dossiers
options = params.permit(:limit, :since, tables: [])
options = params.permit(:version, :limit, :since, tables: [])
respond_to do |format|
format.csv do

View file

@ -20,16 +20,6 @@ module Manager
# Custom actions
#
def change_state_to_instruction
dossier = Dossier.find(params[:id])
dossier.update(state: Dossier.states.fetch(:en_instruction), processed_at: nil, motivation: nil)
dossier.attestation&.destroy
logger.info("Le dossier #{dossier.id} est repassé en instruction par #{current_administration.email}")
flash[:notice] = "Le dossier #{dossier.id} est repassé en instruction"
DossierMailer.notify_revert_to_instruction(dossier).deliver_later
redirect_to manager_dossier_path(dossier)
end
def hide
dossier = Dossier.find(params[:id])
deleted_dossier = dossier.hide!(current_administration)

View file

@ -5,7 +5,7 @@ module DossierHelper
elsif dossier.sans_suite?
'without-continuation'
elsif dossier.refuse?
'refuse'
'refused'
end
end

View file

@ -54,6 +54,13 @@ module ProcedureHelper
}
end
def procedure_dossiers_download_path(procedure, format:, version:)
download_dossiers_gestionnaire_procedure_path(format: format,
procedure_id: procedure.id,
tables: [:etablissements],
version: version)
end
private
TOGGLES = {

View file

@ -1,7 +1,7 @@
import '../shared/polyfills';
import Turbolinks from 'turbolinks';
import Rails from 'rails-ujs';
import * as ActiveStorage from 'activestorage';
import Rails from '@rails/ujs';
import * as ActiveStorage from '@rails/activestorage';
import jQuery from 'jquery';
import '../shared/activestorage/ujs';

View file

@ -1,7 +1,8 @@
import '../shared/polyfills';
import Turbolinks from 'turbolinks';
import Rails from 'rails-ujs';
import * as ActiveStorage from 'activestorage';
import Rails from '@rails/ujs';
import * as ActiveStorage from '@rails/activestorage';
import '@rails/actiontext';
import Chartkick from 'chartkick';
import Highcharts from 'highcharts';

View file

@ -1,4 +1,4 @@
import { DirectUpload } from 'activestorage';
import { DirectUpload } from '@rails/activestorage';
import ProgressBar from './progress-bar';
/**

View file

@ -1,4 +1,4 @@
import Rails from 'rails-ujs';
import Rails from '@rails/ujs';
import jQuery from 'jquery';
import { delegate } from '@utils';

View file

@ -1,4 +1,4 @@
import Rails from 'rails-ujs';
import Rails from '@rails/ujs';
import $ from 'jquery';
import debounce from 'debounce';

View file

@ -0,0 +1,7 @@
class DeclarativeProceduresJob < ApplicationJob
queue_as :cron
def perform(*args)
Procedure.declarative.find_each(&:process_dossiers!)
end
end

View file

@ -37,6 +37,16 @@ class Avis < ApplicationRecord
Avis.find_by(id: avis_id)&.email == email
end
def spreadsheet_columns
[
['Dossier ID', dossier_id.to_s],
['Question / Introduction', :introduction],
['Réponse', :answer],
['Créé le', :created_at],
['Répondu le', :updated_at]
]
end
private
def notify_gestionnaire

View file

@ -20,4 +20,25 @@ class Champs::RepetitionChamp < Champ
def search_terms
# The user cannot enter any information here so it doesnt make much sense to search
end
class Row < Hashie::Dash
property :index
property :dossier_id
property :champs
def spreadsheet_columns
[
['Dossier ID', :dossier_id],
['Ligne', :index]
] + exported_champs
end
private
def exported_champs
champs.reject(&:exclude_from_export?).map do |champ|
[champ.libelle, champ.for_export]
end
end
end
end

View file

@ -11,6 +11,15 @@ module MailTemplateConcern
replace_tags(body, dossier)
end
def update_rich_body
self.rich_body = self.body
end
included do
has_rich_text :rich_body
before_save :update_rich_body
end
module ClassMethods
def default_for_procedure(procedure)
template_name = default_template_name_for_procedure(procedure)

View file

@ -40,6 +40,54 @@ class Dossier < ApplicationRecord
accepts_nested_attributes_for :champs
accepts_nested_attributes_for :champs_private
include AASM
aasm whiny_persistence: true, column: :state, enum: true do
state :brouillon, initial: true
state :en_construction
state :en_instruction
state :accepte
state :refuse
state :sans_suite
event :passer_en_construction, after: :after_passer_en_construction do
transitions from: :brouillon, to: :en_construction
end
event :passer_en_instruction, after: :after_passer_en_instruction do
transitions from: :en_construction, to: :en_instruction
end
event :passer_automatiquement_en_instruction, after: :after_passer_automatiquement_en_instruction do
transitions from: :en_construction, to: :en_instruction
end
event :repasser_en_construction, after: :after_repasser_en_construction do
transitions from: :en_instruction, to: :en_construction
end
event :accepter, after: :after_accepter do
transitions from: :en_instruction, to: :accepte
end
event :accepter_automatiquement, after: :after_accepter_automatiquement do
transitions from: :en_construction, to: :accepte
end
event :refuser, after: :after_refuser do
transitions from: :en_instruction, to: :refuse
end
event :classer_sans_suite, after: :after_classer_sans_suite do
transitions from: :en_instruction, to: :sans_suite
end
event :repasser_en_instruction, after: :after_repasser_en_instruction do
transitions from: :refuse, to: :en_instruction
transitions from: :sans_suite, to: :en_instruction
end
end
default_scope { where(hidden_at: nil) }
scope :state_brouillon, -> { where(state: states.fetch(:brouillon)) }
scope :state_not_brouillon, -> { where.not(state: states.fetch(:brouillon)) }
@ -59,7 +107,7 @@ class Dossier < ApplicationRecord
scope :en_construction, -> { not_archived.state_en_construction }
scope :en_instruction, -> { not_archived.state_en_instruction }
scope :termine, -> { not_archived.state_termine }
scope :downloadable_sorted, -> { state_not_brouillon.includes(:etablissement, :user, :individual, :followers_gestionnaires, champs: { etablissement: [], type_de_champ: :drop_down_list }, champs_private: { etablissement: [], type_de_champ: :drop_down_list }).order(en_construction_at: 'asc') }
scope :downloadable_sorted, -> { state_not_brouillon.includes(:etablissement, :user, :individual, :followers_gestionnaires, :avis, champs: { etablissement: [:champ], type_de_champ: :drop_down_list }, champs_private: { etablissement: [:champ], type_de_champ: :drop_down_list }).order(en_construction_at: 'asc') }
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
scope :followed_by, -> (gestionnaire) { joins(:follows).where(follows: { gestionnaire: gestionnaire }) }
@ -282,55 +330,85 @@ class Dossier < ApplicationRecord
DossierMailer.notify_deletion_to_user(deleted_dossier, user.email).deliver_later
end
def passer_en_instruction!(gestionnaire)
en_instruction!
def after_passer_en_instruction(gestionnaire)
gestionnaire.follow(self)
log_dossier_operation(gestionnaire, :passer_en_instruction)
end
def passer_automatiquement_en_instruction!
en_instruction!
def after_passer_automatiquement_en_instruction
log_automatic_dossier_operation(:passer_en_instruction)
end
def repasser_en_construction!(gestionnaire)
def after_repasser_en_construction(gestionnaire)
self.en_instruction_at = nil
en_construction!
save!
log_dossier_operation(gestionnaire, :repasser_en_construction)
end
def accepter!(gestionnaire, motivation, justificatif = nil)
def after_repasser_en_instruction(gestionnaire)
self.processed_at = nil
self.motivation = nil
attestation&.destroy
save!
DossierMailer.notify_revert_to_instruction(self).deliver_later
log_dossier_operation(gestionnaire, :repasser_en_instruction)
end
def after_accepter(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
self.en_instruction_at ||= Time.zone.now
if justificatif
self.justificatif_motivation.attach(justificatif)
end
accepte!
if attestation.nil?
update(attestation: build_attestation)
self.attestation = build_attestation
end
save!
NotificationMailer.send_closed_notification(self).deliver_later
log_dossier_operation(gestionnaire, :accepter, self)
end
def accepter_automatiquement!
def after_accepter_automatiquement
self.en_instruction_at ||= Time.zone.now
accepte!
if attestation.nil?
update(attestation: build_attestation)
self.attestation = build_attestation
end
save!
NotificationMailer.send_closed_notification(self).deliver_later
log_automatic_dossier_operation(:accepter, self)
end
def after_refuser(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
if justificatif
self.justificatif_motivation.attach(justificatif)
end
save!
NotificationMailer.send_refused_notification(self).deliver_later
log_dossier_operation(gestionnaire, :refuser, self)
end
def after_classer_sans_suite(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
if justificatif
self.justificatif_motivation.attach(justificatif)
end
save!
NotificationMailer.send_without_continuation_notification(self).deliver_later
log_dossier_operation(gestionnaire, :classer_sans_suite, self)
end
def hide!(administration)
update(hidden_at: Time.zone.now)
@ -338,30 +416,6 @@ class Dossier < ApplicationRecord
log_dossier_operation(administration, :supprimer, self)
end
def refuser!(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
self.en_instruction_at ||= Time.zone.now
if justificatif
self.justificatif_motivation.attach(justificatif)
end
refuse!
NotificationMailer.send_refused_notification(self).deliver_later
log_dossier_operation(gestionnaire, :refuser, self)
end
def classer_sans_suite!(gestionnaire, motivation, justificatif = nil)
self.motivation = motivation
self.en_instruction_at ||= Time.zone.now
if justificatif
self.justificatif_motivation.attach(justificatif)
end
sans_suite!
NotificationMailer.send_without_continuation_notification(self).deliver_later
log_dossier_operation(gestionnaire, :classer_sans_suite, self)
end
def check_mandatory_champs
(champs + champs.select(&:repetition?).flat_map(&:champs))
.select(&:mandatory_and_blank?)
@ -380,6 +434,37 @@ class Dossier < ApplicationRecord
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
def spreadsheet_columns
[
['ID', id.to_s],
['Email', user.email],
['Civilité', individual&.gender],
['Nom', individual&.nom],
['Prénom', individual&.prenom],
['Date de naissance', individual&.birthdate],
['Archivé', :archived],
['État du dossier', I18n.t(state, scope: [:activerecord, :attributes, :dossier, :state])],
['Dernière mise à jour le', :updated_at],
['Passé en construction le', :en_instruction_at],
['Passé en instruction le', :en_construction_at],
['Traité le', :processed_at],
['Motivation de la décision', :motivation],
['Instructeurs', followers_gestionnaires.map(&:email).join(' ')]
] + champs_for_export + annotations_for_export
end
def champs_for_export
champs.reject(&:exclude_from_export?).map do |champ|
[champ.libelle, champ.for_export]
end
end
def annotations_for_export
champs_private.reject(&:exclude_from_export?).map do |champ|
[champ.libelle, champ.for_export]
end
end
private
def log_dossier_operation(author, operation, subject = nil)
@ -411,13 +496,13 @@ class Dossier < ApplicationRecord
end
def send_dossier_received
if saved_change_to_state? && en_instruction?
if saved_change_to_state? && en_instruction? && !procedure.declarative_accepte?
NotificationMailer.send_dossier_received(self).deliver_later
end
end
def send_draft_notification_email
if brouillon?
if brouillon? && !procedure.declarative?
DossierMailer.notify_new_draft(self).deliver_later
end
end

View file

@ -2,6 +2,7 @@ class DossierOperationLog < ApplicationRecord
enum operation: {
passer_en_instruction: 'passer_en_instruction',
repasser_en_construction: 'repasser_en_construction',
repasser_en_instruction: 'repasser_en_instruction',
accepter: 'accepter',
refuser: 'refuser',
classer_sans_suite: 'classer_sans_suite',

View file

@ -33,6 +33,43 @@ class Etablissement < ApplicationRecord
]
end
def spreadsheet_columns
[
['Dossier ID', :dossier_id_for_export],
['Champ', :libelle_for_export],
['Établissement SIRET', :siret],
['Établissement siège social', :siege_social],
['Établissement NAF', :naf],
['Établissement libellé NAF', :libelle_naf],
['Établissement Adresse', :adresse],
['Établissement numero voie', :numero_voie],
['Établissement type voie', :type_voie],
['Établissement nom voie', :nom_voie],
['Établissement complément adresse', :complement_adresse],
['Établissement code postal', :code_postal],
['Établissement localité', :localite],
['Établissement code INSEE localité', :code_insee_localite],
['Entreprise SIREN', :entreprise_siren],
['Entreprise capital social', :entreprise_capital_social],
['Entreprise numero TVA intracommunautaire', :entreprise_numero_tva_intracommunautaire],
['Entreprise forme juridique', :entreprise_forme_juridique],
['Entreprise forme juridique code', :entreprise_forme_juridique_code],
['Entreprise nom commercial', :entreprise_nom_commercial],
['Entreprise raison sociale', :entreprise_raison_sociale],
['Entreprise SIRET siège social', :entreprise_siret_siege_social],
['Entreprise code effectif entreprise', :entreprise_code_effectif_entreprise],
['Entreprise date de création', :entreprise_date_creation],
['Entreprise nom', :entreprise_nom],
['Entreprise prénom', :entreprise_prenom],
['Association RNA', :association_rna],
['Association titre', :association_titre],
['Association objet', :association_objet],
['Association date de création', :association_date_creation],
['Association date de déclaration', :association_date_declaration],
['Association date de publication', :association_date_publication]
]
end
def siren
entreprise_siren
end
@ -71,4 +108,18 @@ class Etablissement < ApplicationRecord
inline_adresse: inline_adresse
)
end
private
def dossier_id_for_export
if dossier_id
dossier_id.to_s
elsif champ
champ.dossier_id.to_s
end
end
def libelle_for_export
champ&.libelle
end
end

View file

@ -46,6 +46,7 @@ class Procedure < ApplicationRecord
scope :created_during, -> (range) { where(created_at: range) }
scope :cloned_from_library, -> { where(cloned_from_library: true) }
scope :avec_lien, -> { where.not(path: nil) }
scope :declarative, -> { where.not(declarative_with_state: nil) }
scope :for_api, -> {
includes(
@ -57,6 +58,11 @@ class Procedure < ApplicationRecord
)
}
enum declarative_with_state: {
en_instruction: 'en_instruction',
accepte: 'accepte'
}
validates :libelle, presence: true, allow_blank: false, allow_nil: false
validates :description, presence: true, allow_blank: false, allow_nil: false
validates :administrateurs, presence: true
@ -141,6 +147,14 @@ class Procedure < ApplicationRecord
module_api_carto&.use_api_carto? && module_api_carto&.migrated?
end
def declarative?
declarative_with_state.present?
end
def declarative_accepte?
declarative_with_state == Procedure.declarative_with_states.fetch(:accepte)
end
# Warning: dossier after_save build_default_champs must be removed
# to save a dossier created from this method
def new_dossier
@ -282,7 +296,13 @@ class Procedure < ApplicationRecord
end
def export(options = {})
ProcedureExportService.new(self, **options.to_h.symbolize_keys)
version = options.delete(:version)
if version == 'v2'
options.delete(:tables)
ProcedureExportV2Service.new(self, **options.to_h.symbolize_keys)
else
ProcedureExportService.new(self, **options.to_h.symbolize_keys)
end
end
def to_csv(options = {})
@ -431,6 +451,19 @@ class Procedure < ApplicationRecord
update!(collection_attribute_name => attributes)
end
def process_dossiers!
case declarative_with_state
when Procedure.declarative_with_states.fetch(:en_instruction)
dossiers
.state_en_construction
.find_each(&:passer_automatiquement_en_instruction!)
when Procedure.declarative_with_states.fetch(:accepte)
dossiers
.state_en_construction
.find_each(&:accepter_automatiquement!)
end
end
private
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)

View file

@ -0,0 +1,88 @@
class ProcedureExportV2Service
attr_reader :dossiers
def initialize(procedure, ids: nil, since: nil, limit: nil)
@procedure = procedure
@dossiers = procedure.dossiers.downloadable_sorted
if ids
@dossiers = @dossiers.where(id: ids)
end
if since
@dossiers = @dossiers.since(since)
end
if limit
@dossiers = @dossiers.limit(limit)
end
@tables = [:dossiers, :etablissements, :avis] + champs_repetables_options
end
def to_csv(table = :dossiers)
SpreadsheetArchitect.to_csv(options_for(table))
end
def to_xlsx
# We recursively build multi page spreadsheet
@tables.reduce(nil) do |package, table|
SpreadsheetArchitect.to_axlsx_package(options_for(table), package)
end.to_stream.read
end
def to_ods
# We recursively build multi page spreadsheet
@tables.reduce(nil) do |spreadsheet, table|
SpreadsheetArchitect.to_rodf_spreadsheet(options_for(table), spreadsheet)
end.bytes
end
private
def etablissements
@etablissements ||= dossiers.flat_map do |dossier|
[dossier.champs, dossier.champs_private]
.flatten
.select { |champ| champ.is_a?(Champs::SiretChamp) }
end.map(&:etablissement).compact + dossiers.map(&:etablissement).compact
end
def avis
@avis ||= dossiers.flat_map(&:avis)
end
def champs_repetables
@champs_repetables ||= dossiers.flat_map do |dossier|
[dossier.champs, dossier.champs_private]
.flatten
.select { |champ| champ.is_a?(Champs::RepetitionChamp) }
end
end
def champs_repetables_options
champs_repetables.map do |champ|
[
champ.libelle,
champ.rows.each_with_index.map do |champs, index|
Champs::RepetitionChamp::Row.new(index: index + 1, dossier_id: champ.dossier_id.to_s, champs: champs)
end
]
end
end
DEFAULT_STYLES = {
header_style: { background_color: "000000", color: "FFFFFF", font_size: 12, bold: true },
row_style: { background_color: nil, color: "000000", font_size: 12 }
}
def options_for(table)
case table
when :dossiers
{ instances: dossiers.to_a, sheet_name: 'Dossiers' }.merge(DEFAULT_STYLES)
when :etablissements
{ instances: etablissements.to_a, sheet_name: 'Etablissements' }.merge(DEFAULT_STYLES)
when :avis
{ instances: avis.to_a, sheet_name: 'Avis' }.merge(DEFAULT_STYLES)
when Array
# We have to truncate the label here as spreadsheets have a (30 char) limit on length.
{ instances: table.last, sheet_name: table.first.to_s.truncate(30) }.merge(DEFAULT_STYLES)
end
end
end

View file

@ -0,0 +1,14 @@
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_fit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>

View file

@ -58,14 +58,24 @@
= dossier_display_state(dossier, lower: true)
.dropdown-content.fade-in-down.terminated
- if dossier.motivation.present?
%h4 Motivation
%h4.title Motivation
%p.dossier-motivation= dossier.motivation
= render partial: 'users/dossiers/show/download_justificatif', locals: { dossier: dossier }
- if dossier.attestation.present?
%h4 Attestation
%h4.title Attestation
%p.attestation L'acceptation du dossier a envoyé automatiquement une attestation au demandeur
= link_to "Voir l'attestation", attestation_gestionnaire_dossier_path(dossier.procedure, dossier), target: '_blank', rel: 'noopener', class: 'button'
- if dossier.refuse? || dossier.sans_suite?
%ul.dropdown-items.with-top-border
%li
= link_to repasser_en_instruction_gestionnaire_dossier_path(dossier.procedure, dossier), method: :post, data: { remote:true, confirm: "Voulez vous remettre le dossier #{dossier.id} en instruction ?" } do
%span.icon.in-progress
.dropdown-description
%h4 Repasser en instruction
Lusager sera notifié que son dossier est réexaminé.
- else
%span.label{ class: button_or_label_class(dossier) }
= dossier_display_state(dossier, lower: true)

View file

@ -2,11 +2,23 @@
%span.dropdown
%button.button.dropdown-button
Télécharger tous les dossiers
.dropdown-content.fade-in-down
- old_format_limit_date = Date.parse("Oct 31 2019")
- export_v1_enabled = old_format_limit_date > Time.zone.today
- export_v2_enabled = Flipflop.procedure_export_v2_enabled? || !export_v1_enabled
- old_format_message = export_v1_enabled && export_v2_enabled ? "(ancien format, jusquau #{old_format_limit_date.strftime('%d/%m/%Y')})" : ''
.dropdown-content.fade-in-down{ style: export_v1_enabled && export_v2_enabled ? 'width: 330px' : '' }
%ul.dropdown-items
%li
= link_to "Au format .csv", download_dossiers_gestionnaire_procedure_path(format: :csv, procedure_id: procedure.id), target: "_blank", rel: "noopener"
%li
= link_to "Au format .xlsx", download_dossiers_gestionnaire_procedure_path(format: :xlsx, procedure_id: procedure.id, tables: [:etablissements]), target: "_blank", rel: "noopener"
%li
= link_to "Au format .ods", download_dossiers_gestionnaire_procedure_path(format: :ods, procedure_id: procedure.id, tables: [:etablissements]), target: "_blank", rel: "noopener"
- if export_v2_enabled
%li
= link_to "Au format .xlsx", procedure_dossiers_download_path(procedure, format: :xlsx, version: 'v2'), target: "_blank", rel: "noopener"
%li
= link_to "Au format .ods", procedure_dossiers_download_path(procedure, format: :ods, version: 'v2'), target: "_blank", rel: "noopener"
%li
= link_to "Au format .csv", procedure_dossiers_download_path(procedure, format: :csv, version: 'v2'), target: "_blank", rel: "noopener"
- if export_v1_enabled
%li
= link_to "Au format .xlsx #{old_format_message}", procedure_dossiers_download_path(procedure, format: :xlsx, version: 'v1'), target: "_blank", rel: "noopener"
%li
= link_to "Au format .ods #{old_format_message}", procedure_dossiers_download_path(procedure, format: :ods, version: 'v1'), target: "_blank", rel: "noopener"
%li
= link_to "Au format .csv #{old_format_message}", procedure_dossiers_download_path(procedure, format: :csv, version: 'v1'), target: "_blank", rel: "noopener"

View file

@ -28,9 +28,6 @@ as well as a link to its edit page.
</h1>
<div>
<% if dossier.termine? %>
<%= link_to 'Repasser en instruction', change_state_to_instruction_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Repasser en instruction ?" } %>
<% end %>
<% if dossier.hidden_at.nil? %>
<%= link_to 'Supprimer le dossier', hide_manager_dossier_path(dossier), method: :post, class: 'button', data: { confirm: "Confirmez vous la suppression du dossier ?" } %>
<% end %>

View file

@ -26,8 +26,8 @@
%h1.role-panel-title Vous souhaitez effectuer une demande auprès d'une administration ?
%p.role-panel-explanation Réalisez vos demandes en toute simplicité et retrouvez vos dossiers en ligne
= link_to "Voir les démarches disponibles",
LISTE_DES_DEMARCHES_URL,
= link_to "Comment trouver ma démarche ?",
COMMENT_TROUVER_MA_DEMARCHE_URL,
target: "_blank",
rel: "noopener noreferrer",
class: "role-panel-button-primary"

View file

@ -30,6 +30,7 @@ module.exports = function(api) {
{
forceAllTransforms: true,
useBuiltIns: 'entry',
corejs: 2,
modules: false,
exclude: ['transform-typeof-symbol']
}

View file

@ -16,6 +16,7 @@ Flipflop.configure do
feature :web_hook
feature :enable_email_login_token
feature :procedure_export_v2_enabled
feature :operation_log_serialize_subject
group :development do

View file

@ -4,7 +4,7 @@
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path
# Rails.application.config.assets.paths << Emoji.images_path
Rails.application.config.assets.paths << Rails.root.join('node_modules', 'trix', 'dist')
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.

View file

@ -25,5 +25,6 @@ API_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "api"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
FAQ_URL = "https://faq.demarches-simplifiees.fr"
FAQ_ADMIN_URL = "https://faq.demarches-simplifiees.fr/collection/1-administrateur"
COMMENT_TROUVER_MA_DEMARCHE_URL = [FAQ_URL, "article", "59-comment-trouver-ma-demarche"].join("/")
STATUS_PAGE_URL = "https://status.demarches-simplifiees.fr"
MATOMO_IFRAME_URL = "https://stats.data.gouv.fr/index.php?module=CoreAdminHome&action=optOut&language=fr&&fontColor=333333&fontSize=16px&fontFamily=Muli"

View file

@ -15,7 +15,6 @@ Rails.application.routes.draw do
end
resources :dossiers, only: [:index, :show] do
post 'change_state_to_instruction', on: :member
post 'hide', on: :member
end
@ -325,6 +324,7 @@ Rails.application.routes.draw do
post 'commentaire' => 'dossiers#create_commentaire'
post 'passer-en-instruction' => 'dossiers#passer_en_instruction'
post 'repasser-en-construction' => 'dossiers#repasser_en_construction'
post 'repasser-en-instruction' => 'dossiers#repasser_en_instruction'
post 'terminer'
post 'send-to-instructeurs' => 'dossiers#send_to_instructeurs'
post 'avis' => 'dossiers#create_avis'

View file

@ -0,0 +1,15 @@
# This migration comes from action_text (originally 201805281641)
class CreateActionTextTables < ActiveRecord::Migration[5.2]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
t.text :body, limit: 16777215
t.references :record, null: false, polymorphic: true, index: false
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
t.index [:record_type, :record_id, :name], name: "index_action_text_rich_texts_uniqueness", unique: true
end
end
end

View file

@ -0,0 +1,6 @@
class AddDeclarativeWithStateToProcedures < ActiveRecord::Migration[5.2]
def change
add_column :procedures, :declarative_with_state, :string
add_index :procedures, :declarative_with_state
end
end

View file

@ -16,6 +16,16 @@ ActiveRecord::Schema.define(version: 2019_06_27_132911) do
enable_extension "plpgsql"
enable_extension "unaccent"
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body"
t.string "record_type", null: false
t.bigint "record_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
@ -492,6 +502,8 @@ ActiveRecord::Schema.define(version: 2019_06_27_132911) do
t.boolean "durees_conservation_required", default: true
t.string "path"
t.boolean "expects_multiple_submissions", default: false, null: false
t.string "declarative_with_state"
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"
t.index ["parent_procedure_id"], name: "index_procedures_on_parent_procedure_id"
t.index ["service_id"], name: "index_procedures_on_service_id"

View file

@ -0,0 +1,26 @@
namespace :after_party do
desc 'Deployment task: migrate_mail_body_to_actiontext'
task migrate_mail_body_to_actiontext: :environment do
puts "Running deploy task 'migrate_mail_body_to_actiontext'"
# Put your task implementation HERE.
[Mails::InitiatedMail, Mails::ReceivedMail, Mails::ClosedMail, Mails::WithoutContinuationMail, Mails::RefusedMail].each do |mt_class|
progress = ProgressReport.new(mt_class.all.count)
mt_class.all.each do |mt|
if mt.body.present?
mt.rich_body = mt.body
mt.save
end
progress.inc
end
progress.finish
end
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20190410131747'
end # task :migrate_mail_body_to_actiontext
end # namespace :after_party

View file

@ -0,0 +1,22 @@
namespace :after_party do
desc 'Deployment task: set_declarative_procedures'
task set_declarative_procedures: :environment do
puts "Running deploy task 'set_declarative_procedures'"
Delayed::Job.where.not(cron: nil).find_each do |job|
job_data = YAML.load_dj(job.handler).job_data
if job_data['job_class'] == 'AutoReceiveDossiersForProcedureJob'
procedure_id, state = job_data['arguments']
procedure = Procedure.find(procedure_id)
procedure.declarative_with_state = state
procedure.save!
job.delete
end
end
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20190523122639'
end
end

View file

@ -16,6 +16,6 @@ namespace :after_party do
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord.create version: '20190521131030'
AfterParty::TaskRecord.create version: '20190701131030'
end
end

View file

@ -1,26 +1,28 @@
{
"dependencies": {
"@babel/preset-react": "^7.0.0",
"@fortawesome/fontawesome-svg-core": "^1.2.18",
"@fortawesome/free-solid-svg-icons": "^5.8.2",
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"@fortawesome/react-fontawesome": "^0.1.4",
"@rails/webpacker": "4.0.2",
"@sentry/browser": "^5.2.1",
"@rails/actiontext": "^6.0.0-alpha",
"@rails/activestorage": "^6.0.0-alpha",
"@rails/ujs": "^6.0.0-alpha",
"@rails/webpacker": "4.0.7",
"@sentry/browser": "^5.4.3",
"@turf/area": "^6.0.1",
"activestorage": "^5.2.3",
"autocomplete.js": "^0.36.0",
"babel-plugin-macros": "^2.5.1",
"babel-plugin-macros": "^2.6.1",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"chartkick": "^3.0.2",
"core-js": "^2.0.0",
"debounce": "^1.2.0",
"dom4": "^2.1.4",
"dom4": "^2.1.5",
"highcharts": "^6.1.2",
"intersection-observer": "^0.7.0",
"jquery": "^3.4.1",
"leaflet": "^1.4.0",
"leaflet-freedraw": "^2.10.0",
"prop-types": "^15.7.2",
"rails-ujs": "^5.2.3",
"ramda": "=0.24.1",
"react": "^16.8.6",
"react-dom": "^16.8.6",
@ -32,15 +34,15 @@
"turbolinks": "^5.2.0"
},
"devDependencies": {
"babel-eslint": "^10.0.1",
"babel-eslint": "^10.0.2",
"eclint": "^2.8.1",
"eslint": "^5.16.0",
"eslint-config-prettier": "^4.2.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.13.0",
"eslint-plugin-react-hooks": "^1.6.0",
"prettier": "^1.17.1",
"webpack-dev-server": "^3.3.1"
"eslint-plugin-react": "^7.14.2",
"eslint-plugin-react-hooks": "^1.6.1",
"prettier": "^1.18.2",
"webpack-dev-server": "^3.7.2"
},
"scripts": {
"lint:ec": "eclint check $({ git ls-files ; find vendor -type f ; echo 'db/schema.rb' ; } | sort | uniq -u)",

View file

@ -0,0 +1,82 @@
require 'rails_helper'
RSpec.describe DeclarativeProceduresJob, type: :job do
describe "perform" do
let(:date) { Time.utc(2017, 9, 1, 10, 5, 0) }
let(:instruction_date) { date + 120 }
let(:state) { nil }
let(:procedure) { create(:procedure, :with_gestionnaire, declarative_with_state: state) }
let(:nouveau_dossier1) { create(:dossier, :en_construction, procedure: procedure) }
let(:nouveau_dossier2) { create(:dossier, :en_construction, procedure: procedure) }
let(:dossier_recu) { create(:dossier, :en_instruction, procedure: procedure) }
let(:dossier_brouillon) { create(:dossier, procedure: procedure) }
before do
Timecop.freeze(date)
dossiers = [
nouveau_dossier1,
nouveau_dossier2,
dossier_recu,
dossier_brouillon
]
create(:attestation_template, procedure: procedure)
DeclarativeProceduresJob.new.perform
dossiers.each(&:reload)
end
after { Timecop.return }
context "with some dossiers" do
context "en_construction" do
let(:state) { Dossier.states.fetch(:en_instruction) }
let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last }
it {
expect(nouveau_dossier1.en_instruction?).to be true
expect(nouveau_dossier1.en_instruction_at).to eq(date)
expect(last_operation.operation).to eq('passer_en_instruction')
expect(last_operation.automatic_operation?).to be_truthy
expect(nouveau_dossier2.en_instruction?).to be true
expect(nouveau_dossier2.en_instruction_at).to eq(date)
expect(dossier_recu.en_instruction?).to be true
expect(dossier_recu.en_instruction_at).to eq(instruction_date)
expect(dossier_brouillon.brouillon?).to be true
expect(dossier_brouillon.en_instruction_at).to eq(nil)
}
end
context "accepte" do
let(:state) { Dossier.states.fetch(:accepte) }
let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last }
it {
expect(nouveau_dossier1.accepte?).to be true
expect(nouveau_dossier1.en_instruction_at).to eq(date)
expect(nouveau_dossier1.processed_at).to eq(date)
expect(nouveau_dossier1.attestation).to be_present
expect(last_operation.operation).to eq('accepter')
expect(last_operation.automatic_operation?).to be_truthy
expect(nouveau_dossier2.accepte?).to be true
expect(nouveau_dossier2.en_instruction_at).to eq(date)
expect(nouveau_dossier2.processed_at).to eq(date)
expect(nouveau_dossier2.attestation).to be_present
expect(dossier_recu.en_instruction?).to be true
expect(dossier_recu.en_instruction_at).to eq(instruction_date)
expect(dossier_recu.processed_at).to eq(nil)
expect(dossier_brouillon.brouillon?).to be true
expect(dossier_brouillon.en_instruction_at).to eq(nil)
expect(dossier_brouillon.processed_at).to eq(nil)
}
end
end
end
end

View file

@ -0,0 +1,24 @@
describe '20190410131747_migrate_mail_body_to_actiontext.rake' do
let(:rake_task) { Rake::Task['after_party:migrate_mail_body_to_actiontext'] }
let!(:closed_mail) { create(:closed_mail, body: body) }
before do
rake_task.invoke
closed_mail.reload
end
after { rake_task.reenable }
context 'with a plain text body' do
let(:body) { "Test de body" }
it { expect(closed_mail.rich_body.to_plain_text).to eq(closed_mail.body) }
end
context 'with a html text body' do
let(:body) { "Test de body<br>" }
it { expect(closed_mail.rich_body.to_s.squish).to eq("<div class=\"trix-content\"> #{closed_mail.body} </div>".squish) }
end
end

View file

@ -113,4 +113,10 @@ describe MailTemplateConcern do
expect(initiated_mail.body_for_dossier(dossier2)).to eq("n #{dossier2.id}")
end
end
describe '#update_rich_body' do
before { initiated_mail.update(body: "Voici le corps du mail") }
it { expect(initiated_mail.rich_body.to_plain_text).to eq(initiated_mail.body) }
end
end

View file

@ -794,7 +794,7 @@ describe Dossier do
end
describe '#accepter!' do
let(:dossier) { create(:dossier) }
let(:dossier) { create(:dossier, :en_instruction) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let!(:gestionnaire) { create(:gestionnaire) }
@ -813,7 +813,7 @@ describe Dossier do
after { Timecop.return }
it { expect(dossier.motivation).to eq('motivation') }
it { expect(dossier.en_instruction_at).to eq(now) }
it { expect(dossier.en_instruction_at).to eq(dossier.en_instruction_at) }
it { expect(dossier.processed_at).to eq(now) }
it { expect(dossier.state).to eq('accepte') }
it { expect(last_operation.operation).to eq('accepter') }
@ -826,7 +826,7 @@ describe Dossier do
end
describe '#accepter_automatiquement!' do
let(:dossier) { create(:dossier) }
let(:dossier) { create(:dossier, :en_construction) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let!(:now) { Time.zone.parse('01/01/2100') }
let(:attestation) { Attestation.new }
@ -853,7 +853,7 @@ describe Dossier do
end
describe '#passer_en_instruction!' do
let(:dossier) { create(:dossier) }
let(:dossier) { create(:dossier, :en_construction) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let(:gestionnaire) { create(:gestionnaire) }
@ -870,7 +870,7 @@ describe Dossier do
end
describe '#passer_automatiquement_en_instruction!' do
let(:dossier) { create(:dossier) }
let(:dossier) { create(:dossier, :en_construction) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let(:gestionnaire) { create(:gestionnaire) }
@ -987,4 +987,28 @@ describe Dossier do
it { expect(last_operation.operation).to eq('supprimer') }
it { expect(last_operation.automatic_operation?).to be_falsey }
end
describe '#repasser_en_instruction!' do
let(:dossier) { create(:dossier, :refuse, :with_attestation) }
let!(:gestionnaire) { create(:gestionnaire) }
let(:last_operation) { dossier.dossier_operation_logs.last }
before do
Timecop.freeze
allow(DossierMailer).to receive(:notify_revert_to_instruction)
.and_return(double(deliver_later: true))
dossier.repasser_en_instruction!(gestionnaire)
dossier.reload
end
it { expect(dossier.state).to eq('en_instruction') }
it { expect(dossier.processed_at).to be_nil }
it { expect(dossier.motivation).to be_nil }
it { expect(dossier.attestation).to be_nil }
it { expect(last_operation.operation).to eq('repasser_en_instruction') }
it { expect(JSON.parse(last_operation.serialized.download)['author']['email']).to eq(gestionnaire.email) }
it { expect(DossierMailer).to have_received(:notify_revert_to_instruction).with(dossier) }
after { Timecop.return }
end
end

View file

@ -0,0 +1,181 @@
require 'spec_helper'
describe ProcedureExportV2Service do
describe 'to_data' do
let(:procedure) { create(:procedure, :published, :with_all_champs) }
subject do
Tempfile.create do |f|
f << ProcedureExportV2Service.new(procedure).to_xlsx
f.rewind
SimpleXlsxReader.open(f.path)
end
end
let(:dossiers_sheet) { subject.sheets.first }
let(:etablissements_sheet) { subject.sheets.second }
let(:avis_sheet) { subject.sheets.third }
let(:repetition_sheet) { subject.sheets.fourth }
before do
# change one tdc place to check if the header is ordered
tdc_first = procedure.types_de_champ.first
tdc_last = procedure.types_de_champ.last
tdc_first.update(order_place: tdc_last.order_place + 1)
procedure.reload
end
context 'dossiers' do
it 'should have sheets' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis'])
end
end
context 'with dossier' do
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
it 'should have headers' do
expect(dossiers_sheet.headers).to eq([
"ID",
"Email",
"Civilité",
"Nom",
"Prénom",
"Date de naissance",
"Archivé",
"État du dossier",
"Dernière mise à jour le",
"Passé en construction le",
"Passé en instruction le",
"Traité le",
"Motivation de la décision",
"Instructeurs",
"textarea",
"date",
"datetime",
"number",
"decimal_number",
"integer_number",
"checkbox",
"civilite",
"email",
"phone",
"address",
"yes_no",
"simple_drop_down_list",
"multiple_drop_down_list",
"linked_drop_down_list",
"pays",
"regions",
"departements",
"engagement",
"dossier_link",
"piece_justificative",
"siret",
"carte",
"text"
])
end
it 'should have data' do
expect(dossiers_sheet.data.size).to eq(1)
expect(etablissements_sheet.data.size).to eq(1)
end
end
context 'with etablissement' do
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :with_entreprise, procedure: procedure) }
it 'should have headers' do
expect(etablissements_sheet.headers).to eq([
"Dossier ID",
"Champ",
"Établissement SIRET",
"Établissement siège social",
"Établissement NAF",
"Établissement libellé NAF",
"Établissement Adresse",
"Établissement numero voie",
"Établissement type voie",
"Établissement nom voie",
"Établissement complément adresse",
"Établissement code postal",
"Établissement localité",
"Établissement code INSEE localité",
"Entreprise SIREN",
"Entreprise capital social",
"Entreprise numero TVA intracommunautaire",
"Entreprise forme juridique",
"Entreprise forme juridique code",
"Entreprise nom commercial",
"Entreprise raison sociale",
"Entreprise SIRET siège social",
"Entreprise code effectif entreprise",
"Entreprise date de création",
"Entreprise nom",
"Entreprise prénom",
"Association RNA",
"Association titre",
"Association objet",
"Association date de création",
"Association date de déclaration",
"Association date de publication"
])
end
it 'should have data' do
expect(etablissements_sheet.data.size).to eq(2)
end
end
context 'with avis' do
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
let!(:avis) { create(:avis, :with_answer, dossier: dossier) }
it 'should have headers' do
expect(avis_sheet.headers).to eq([
"Dossier ID",
"Question / Introduction",
"Réponse",
"Créé le",
"Répondu le"
])
end
it 'should have data' do
expect(avis_sheet.data.size).to eq(1)
end
end
context 'with repetitions' do
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
let(:champ_repetition) { dossier.champs.find { |champ| champ.type_champ == 'repetition' } }
let(:type_de_champ_text) { create(:type_de_champ_text, order_place: 0, parent: champ_repetition.type_de_champ) }
let(:type_de_champ_number) { create(:type_de_champ_number, order_place: 1, parent: champ_repetition.type_de_champ) }
before do
create(:champ_text, row: 0, type_de_champ: type_de_champ_text, parent: champ_repetition)
create(:champ_number, row: 0, type_de_champ: type_de_champ_number, parent: champ_repetition)
create(:champ_text, row: 1, type_de_champ: type_de_champ_text, parent: champ_repetition)
create(:champ_number, row: 1, type_de_champ: type_de_champ_number, parent: champ_repetition)
end
it 'should have sheets' do
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle])
end
it 'should have headers' do
expect(repetition_sheet.headers).to eq([
"Dossier ID",
"Ligne",
type_de_champ_text.libelle,
type_de_champ_number.libelle
])
end
it 'should have data' do
expect(repetition_sheet.data.size).to eq(2)
end
end
end
end

1505
yarn.lock

File diff suppressed because it is too large Load diff