commit
392d38ce35
87 changed files with 161 additions and 10762 deletions
Binary file not shown.
Before Width: | Height: | Size: 115 KiB |
Binary file not shown.
Before Width: | Height: | Size: 202 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 34 KiB |
|
@ -25,7 +25,6 @@
|
|||
// = require login
|
||||
// = require main_container
|
||||
// = require navbar
|
||||
// = require pieces_justificatives_fields
|
||||
// = require pj_modal
|
||||
// = require print
|
||||
// = require procedure
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
@import "colors";
|
||||
|
||||
$default-space: 15px;
|
||||
|
||||
.tour-de-france {
|
||||
.blue-panel {
|
||||
background-color: $blue;
|
||||
}
|
||||
|
||||
.white-panel {
|
||||
padding-top: 2 * $default-space;
|
||||
padding-bottom: 2 * $default-space;
|
||||
}
|
||||
|
||||
.photos-panel {
|
||||
background: linear-gradient(#FFFFFF, #FFFFFF 50%, $light-grey 50%, $light-grey 100%);
|
||||
padding-top: 2 * $default-space;
|
||||
padding-bottom: 2 * $default-space;
|
||||
}
|
||||
|
||||
.grey-panel {
|
||||
background-color: $light-grey;
|
||||
padding-top: 2 * $default-space;
|
||||
padding-bottom: 2 * $default-space;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: $default-space;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: $default-space;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $grey;
|
||||
margin-bottom: $default-space;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cards {
|
||||
.card:nth-of-type(even) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
$card-margin-bottom: 2 * $default-space;
|
||||
|
||||
.card {
|
||||
padding: 15px;
|
||||
margin-bottom: $card-margin-bottom;
|
||||
border-radius: 5px;
|
||||
box-shadow: none;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
min-height: 230px;
|
||||
}
|
||||
|
||||
$card-half-horizontal-spacing: 4 * $default-space;
|
||||
|
||||
.card-half {
|
||||
width: calc((100% - #{$card-half-horizontal-spacing}) / 2);
|
||||
margin-right: 3 * $default-space;
|
||||
}
|
||||
|
||||
.tour-de-france-logo {
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
padding-top: 2 * $default-space;
|
||||
padding-bottom: 2 * $default-space;
|
||||
}
|
||||
|
||||
.atelier-photo {
|
||||
width: 380px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
.pieces-justificatives-fields {
|
||||
.form-inline > .form-group {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
class Admin::PiecesJustificativesController < AdminController
|
||||
before_action :retrieve_procedure
|
||||
before_action :procedure_locked?
|
||||
before_action :reset_procedure, only: [:update, :destroy, :move_up, :move_down]
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
if @procedure.update(update_params)
|
||||
flash.now.notice = 'Modifications sauvegardées'
|
||||
else
|
||||
flash.now.notice = 'Une erreur est survenue'
|
||||
end
|
||||
render 'show', format: :js
|
||||
end
|
||||
|
||||
def destroy
|
||||
@procedure.types_de_piece_justificative.find(params[:id]).destroy
|
||||
|
||||
render 'show', format: :js
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
render json: { message: 'Type de piece justificative not found' }, status: 404
|
||||
end
|
||||
|
||||
def move_up
|
||||
index = params[:index].to_i - 1
|
||||
if @procedure.switch_types_de_piece_justificative index
|
||||
render 'show', format: :js
|
||||
else
|
||||
render json: {}, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
def move_down
|
||||
if @procedure.switch_types_de_piece_justificative params[:index].to_i
|
||||
render 'show', format: :js
|
||||
else
|
||||
render json: {}, status: 400
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_params
|
||||
params
|
||||
.require(:procedure)
|
||||
.permit(types_de_piece_justificative_attributes: [:libelle, :description, :id, :order_place, :mandatory, :lien_demarche])
|
||||
end
|
||||
end
|
|
@ -59,7 +59,4 @@ class RootController < ApplicationController
|
|||
|
||||
def suivi
|
||||
end
|
||||
|
||||
def tour_de_france
|
||||
end
|
||||
end
|
||||
|
|
|
@ -132,7 +132,6 @@ module Users
|
|||
end
|
||||
|
||||
# FIXME:
|
||||
# - remove PiecesJustificativesService
|
||||
# - delegate draft save logic to champ ?
|
||||
def update_brouillon
|
||||
@dossier = dossier_with_champs
|
||||
|
@ -158,8 +157,6 @@ module Users
|
|||
@dossier = dossier_with_champs
|
||||
end
|
||||
|
||||
# FIXME:
|
||||
# - remove PiecesJustificativesService
|
||||
def update
|
||||
@dossier = dossier_with_champs
|
||||
|
||||
|
@ -297,7 +294,7 @@ module Users
|
|||
end
|
||||
|
||||
def update_dossier_and_compute_errors
|
||||
errors = PiecesJustificativesService.upload!(@dossier, current_user, params)
|
||||
errors = []
|
||||
|
||||
if champs_params[:dossier] && !@dossier.update(champs_params[:dossier])
|
||||
errors += @dossier.errors.full_messages
|
||||
|
@ -305,7 +302,6 @@ module Users
|
|||
|
||||
if !save_draft?
|
||||
errors += @dossier.check_mandatory_champs
|
||||
errors += PiecesJustificativesService.missing_pj_error_messages(@dossier)
|
||||
end
|
||||
|
||||
errors
|
||||
|
|
|
@ -15,9 +15,14 @@ class Users::SessionsController < Sessions::SessionsController
|
|||
# POST /resource/sign_in
|
||||
def create
|
||||
remember_me = params[:user][:remember_me] == '1'
|
||||
try_to_authenticate(User, remember_me)
|
||||
try_to_authenticate(Gestionnaire, remember_me)
|
||||
try_to_authenticate(Administrateur, remember_me)
|
||||
|
||||
if resource_locked?(try_to_authenticate(User, remember_me)) ||
|
||||
resource_locked?(try_to_authenticate(Gestionnaire, remember_me)) ||
|
||||
resource_locked?(try_to_authenticate(Administrateur, remember_me))
|
||||
flash.alert = 'Votre compte est verrouillé.'
|
||||
new
|
||||
return render :new, status: 401
|
||||
end
|
||||
|
||||
if user_signed_in?
|
||||
current_user.update(loged_in_with_france_connect: nil)
|
||||
|
@ -109,5 +114,10 @@ class Users::SessionsController < Sessions::SessionsController
|
|||
resource.force_sync_credentials
|
||||
end
|
||||
end
|
||||
resource
|
||||
end
|
||||
|
||||
def resource_locked?(resource)
|
||||
resource.present? && resource.access_locked?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,7 +8,6 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
# which determines how the attribute is displayed
|
||||
# on pages throughout the dashboard.
|
||||
ATTRIBUTE_TYPES = {
|
||||
types_de_piece_justificative: TypesDePieceJustificativeCollectionField,
|
||||
types_de_champ: TypesDeChampCollectionField,
|
||||
types_de_champ_private: TypesDeChampCollectionField,
|
||||
path: ProcedureLinkField,
|
||||
|
@ -72,7 +71,6 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:archived_at,
|
||||
:types_de_champ,
|
||||
:types_de_champ_private,
|
||||
:types_de_piece_justificative,
|
||||
:for_individual,
|
||||
:auto_archive_on,
|
||||
:gestionnaires,
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "administrate/field/base"
|
||||
|
||||
class TypesDePieceJustificativeCollectionField < Administrate::Field::Base
|
||||
def to_s
|
||||
data
|
||||
end
|
||||
end
|
|
@ -1,55 +0,0 @@
|
|||
module AdminFormulaireHelper
|
||||
BASE_CLASSES = ['btn', 'btn-default', 'form-control', 'fa']
|
||||
|
||||
def button_up(procedure, kind, index, url)
|
||||
if display_up_button?(index, procedure, kind)
|
||||
button(up_classes, "btn_up_#{index}", url)
|
||||
end
|
||||
end
|
||||
|
||||
def button_down(procedure, kind, index, url)
|
||||
if display_down_button?(index, procedure, kind)
|
||||
button(down_classes, "btn_down_#{index}", url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def button(classes, id, url)
|
||||
link_to(
|
||||
'',
|
||||
url,
|
||||
class: classes,
|
||||
id: id,
|
||||
remote: true,
|
||||
method: :post
|
||||
)
|
||||
end
|
||||
|
||||
def up_classes
|
||||
BASE_CLASSES + ['fa-chevron-up']
|
||||
end
|
||||
|
||||
def down_classes
|
||||
BASE_CLASSES + ['fa-chevron-down']
|
||||
end
|
||||
|
||||
def display_up_button?(index, procedure, kind)
|
||||
index != 0 && count_type_de_champ(procedure, kind) > 1
|
||||
end
|
||||
|
||||
def display_down_button?(index, procedure, kind)
|
||||
(index + 1) < count_type_de_champ(procedure, kind)
|
||||
end
|
||||
|
||||
def count_type_de_champ(procedure, kind)
|
||||
case kind
|
||||
when "public"
|
||||
@count_type_de_champ_public ||= procedure.types_de_champ.count
|
||||
when "private"
|
||||
@count_type_de_champ_private ||= procedure.types_de_champ_private.count
|
||||
when "piece_justificative"
|
||||
@count_type_de_piece_justificative ||= procedure.types_de_piece_justificative.count
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +0,0 @@
|
|||
module PieceJustificativeHelper
|
||||
def display_pj_filename(pj)
|
||||
truncate(pj.original_filename, length: 60)
|
||||
end
|
||||
end
|
|
@ -1,94 +0,0 @@
|
|||
# Source: https://github.com/gitlabhq/gitlabhq/blob/master/lib/file_size_validator.rb
|
||||
class FileSizeValidator < ActiveModel::EachValidator
|
||||
MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze
|
||||
CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
|
||||
|
||||
DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
|
||||
RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
|
||||
|
||||
def initialize(options)
|
||||
range = options.delete(:in) || options.delete(:within)
|
||||
|
||||
if range.present?
|
||||
if !range.is_a?(Range)
|
||||
raise ArgumentError, ":in and :within must be a Range"
|
||||
end
|
||||
|
||||
options[:minimum], options[:maximum] = range.begin, range.end
|
||||
|
||||
if range.exclude_end?
|
||||
options[:maximum] -= 1
|
||||
end
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
keys = CHECKS.keys & options.keys
|
||||
|
||||
if keys.empty?
|
||||
raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
|
||||
end
|
||||
|
||||
keys.each do |key|
|
||||
value = options[key]
|
||||
|
||||
if !(value.is_a?(Integer) && value >= 0) && !value.is_a?(Symbol)
|
||||
raise ArgumentError, ":#{key} must be a nonnegative Integer or symbol"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
if !value.is_a?(CarrierWave::Uploader::Base)
|
||||
raise(ArgumentError, "A CarrierWave::Uploader::Base object was expected")
|
||||
end
|
||||
|
||||
if value.is_a?(String)
|
||||
value = (options[:tokenizer] || DEFAULT_TOKENIZER).call(value)
|
||||
end
|
||||
|
||||
CHECKS.each do |key, validity_check|
|
||||
if !check_value = options[key]
|
||||
next
|
||||
end
|
||||
|
||||
check_value =
|
||||
case check_value
|
||||
when Integer
|
||||
check_value
|
||||
when Symbol
|
||||
record.send(check_value)
|
||||
end
|
||||
|
||||
if key == :maximum
|
||||
value ||= []
|
||||
end
|
||||
|
||||
value_size = value.size
|
||||
if value_size.send(validity_check, check_value)
|
||||
next
|
||||
end
|
||||
|
||||
errors_options = options.except(*RESERVED_OPTIONS)
|
||||
errors_options[:file_size] = help.number_to_human_size check_value
|
||||
|
||||
default_message = options[MESSAGES[key]]
|
||||
if default_message
|
||||
errors_options[:message] ||= default_message
|
||||
end
|
||||
|
||||
record.errors.add(attribute, MESSAGES[key], errors_options)
|
||||
end
|
||||
end
|
||||
|
||||
def help
|
||||
Helper.instance
|
||||
end
|
||||
|
||||
class Helper
|
||||
include Singleton
|
||||
include ActionView::Helpers::NumberHelper
|
||||
end
|
||||
end
|
|
@ -4,7 +4,7 @@ class Administrateur < ApplicationRecord
|
|||
include ActiveRecord::SecureToken
|
||||
|
||||
devise :database_authenticatable, :registerable, :async,
|
||||
:recoverable, :rememberable, :trackable, :validatable
|
||||
:recoverable, :rememberable, :trackable, :validatable, :lockable
|
||||
|
||||
has_and_belongs_to_many :gestionnaires
|
||||
has_many :administrateurs_procedures
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Administration < ApplicationRecord
|
||||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable and :omniauthable
|
||||
devise :database_authenticatable, :rememberable, :trackable, :validatable, :omniauthable, :async, omniauth_providers: [:github]
|
||||
devise :database_authenticatable, :rememberable, :trackable, :validatable, :omniauthable, :lockable, :async, omniauth_providers: [:github]
|
||||
|
||||
def self.from_omniauth(params)
|
||||
find_by(email: params["info"]["email"])
|
||||
|
|
|
@ -21,7 +21,6 @@ class Dossier < ApplicationRecord
|
|||
has_one :individual, dependent: :destroy
|
||||
has_one :attestation, dependent: :destroy
|
||||
|
||||
has_many :pieces_justificatives, inverse_of: :dossier, dependent: :destroy
|
||||
has_one_attached :justificatif_motivation
|
||||
|
||||
has_many :champs, -> { root.public_only.ordered }, inverse_of: :dossier, dependent: :destroy
|
||||
|
@ -128,7 +127,6 @@ class Dossier < ApplicationRecord
|
|||
:etablissement,
|
||||
piece_justificative_file_attachment: :blob
|
||||
],
|
||||
pieces_justificatives: [],
|
||||
avis: [],
|
||||
etablissement: [],
|
||||
individual: [],
|
||||
|
@ -138,7 +136,6 @@ class Dossier < ApplicationRecord
|
|||
accepts_nested_attributes_for :individual
|
||||
|
||||
delegate :siret, :siren, to: :etablissement, allow_nil: true
|
||||
delegate :types_de_piece_justificative, to: :procedure
|
||||
delegate :types_de_champ, to: :procedure
|
||||
delegate :france_connect_information, to: :user
|
||||
|
||||
|
@ -165,18 +162,6 @@ class Dossier < ApplicationRecord
|
|||
self.private_search_terms = champs_private.flat_map(&:search_terms).compact.join(' ')
|
||||
end
|
||||
|
||||
def was_piece_justificative_uploaded_for_type_id?(type_id)
|
||||
pieces_justificatives.where(type_de_piece_justificative_id: type_id).count > 0
|
||||
end
|
||||
|
||||
def retrieve_last_piece_justificative_by_type(type)
|
||||
pieces_justificatives.where(type_de_piece_justificative_id: type).last
|
||||
end
|
||||
|
||||
def retrieve_all_piece_justificative_by_type(type)
|
||||
pieces_justificatives.where(type_de_piece_justificative_id: type).order(created_at: :DESC)
|
||||
end
|
||||
|
||||
def build_default_champs
|
||||
procedure.build_champs.each do |champ|
|
||||
champs << champ
|
||||
|
|
|
@ -3,7 +3,7 @@ class Gestionnaire < ApplicationRecord
|
|||
include EmailSanitizableConcern
|
||||
|
||||
devise :database_authenticatable, :registerable, :async,
|
||||
:recoverable, :rememberable, :trackable, :validatable
|
||||
:recoverable, :rememberable, :trackable, :validatable, :lockable
|
||||
|
||||
has_and_belongs_to_many :administrateurs
|
||||
|
||||
|
@ -93,11 +93,7 @@ class Gestionnaire < ApplicationRecord
|
|||
.find_by(gestionnaire: self, dossier: dossier)
|
||||
|
||||
if follow.present?
|
||||
champs_publiques = follow.dossier.champs.updated_since?(follow.demande_seen_at).any?
|
||||
|
||||
pieces_justificatives = follow.dossier.pieces_justificatives.updated_since?(follow.demande_seen_at).any?
|
||||
|
||||
demande = champs_publiques || pieces_justificatives
|
||||
demande = follow.dossier.champs.updated_since?(follow.demande_seen_at).any?
|
||||
|
||||
annotations_privees = follow.dossier.champs_private.updated_since?(follow.annotations_privees_seen_at).any?
|
||||
|
||||
|
@ -154,10 +150,6 @@ class Gestionnaire < ApplicationRecord
|
|||
.joins(:champs)
|
||||
.where('champs.updated_at > follows.demande_seen_at')
|
||||
|
||||
updated_pieces_justificatives = dossiers
|
||||
.joins(:pieces_justificatives)
|
||||
.where('pieces_justificatives.updated_at > follows.demande_seen_at')
|
||||
|
||||
updated_annotations = dossiers
|
||||
.joins(:champs_private)
|
||||
.where('champs.updated_at > follows.annotations_privees_seen_at')
|
||||
|
@ -174,7 +166,6 @@ class Gestionnaire < ApplicationRecord
|
|||
|
||||
[
|
||||
updated_demandes,
|
||||
updated_pieces_justificatives,
|
||||
updated_annotations,
|
||||
updated_avis,
|
||||
updated_messagerie
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
class PieceJustificative < ApplicationRecord
|
||||
belongs_to :dossier, inverse_of: :pieces_justificatives, touch: true
|
||||
belongs_to :type_de_piece_justificative
|
||||
has_one :commentaire
|
||||
|
||||
belongs_to :user
|
||||
|
||||
delegate :api_entreprise, :libelle, :order_place, to: :type_de_piece_justificative
|
||||
|
||||
alias_attribute :type, :type_de_piece_justificative_id
|
||||
|
||||
mount_uploader :content, PieceJustificativeUploader
|
||||
validates :content, :file_size => { :maximum => 20.megabytes }
|
||||
validates :content, presence: true, allow_blank: false, allow_nil: false
|
||||
|
||||
scope :updated_since?, -> (date) { where('pieces_justificatives.updated_at > ?', date) }
|
||||
|
||||
def empty?
|
||||
content.blank?
|
||||
end
|
||||
|
||||
def libelle
|
||||
if type_de_piece_justificative.nil?
|
||||
return content.to_s
|
||||
else
|
||||
type_de_piece_justificative.libelle
|
||||
end
|
||||
end
|
||||
|
||||
def content_url
|
||||
if content.url.present?
|
||||
if Flipflop.remote_storage?
|
||||
(RemoteDownloader.new content.filename).url
|
||||
else
|
||||
(LocalDownloader.new content.path,
|
||||
(type_de_piece_justificative.nil? ? content.original_filename : type_de_piece_justificative.libelle)).url
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.accept_format
|
||||
" application/pdf,
|
||||
application/msword,
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document,
|
||||
application/vnd.ms-excel,
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,
|
||||
application/vnd.ms-powerpoint,
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation,
|
||||
application/vnd.oasis.opendocument.text,
|
||||
application/vnd.oasis.opendocument.presentation,
|
||||
application/vnd.oasis.opendocument.spreadsheet,
|
||||
image/png,
|
||||
image/jpeg
|
||||
"
|
||||
end
|
||||
end
|
|
@ -3,7 +3,6 @@ require Rails.root.join('lib', 'percentile')
|
|||
class Procedure < ApplicationRecord
|
||||
MAX_DUREE_CONSERVATION = 36
|
||||
|
||||
has_many :types_de_piece_justificative, -> { ordered }, inverse_of: :procedure, dependent: :destroy
|
||||
has_many :types_de_champ, -> { root.public_only.ordered }, inverse_of: :procedure, dependent: :destroy
|
||||
has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', inverse_of: :procedure, dependent: :destroy
|
||||
has_many :dossiers, dependent: :restrict_with_exception
|
||||
|
@ -31,7 +30,6 @@ class Procedure < ApplicationRecord
|
|||
|
||||
accepts_nested_attributes_for :types_de_champ, reject_if: proc { |attributes| attributes['libelle'].blank? }, allow_destroy: true
|
||||
accepts_nested_attributes_for :types_de_champ_private, reject_if: proc { |attributes| attributes['libelle'].blank? }, allow_destroy: true
|
||||
accepts_nested_attributes_for :types_de_piece_justificative, reject_if: proc { |attributes| attributes['libelle'].blank? }, allow_destroy: true
|
||||
|
||||
mount_uploader :logo, ProcedureLogoUploader
|
||||
|
||||
|
@ -51,7 +49,6 @@ class Procedure < ApplicationRecord
|
|||
:administrateurs,
|
||||
:types_de_champ_private,
|
||||
:types_de_champ,
|
||||
:types_de_piece_justificative,
|
||||
:module_api_carto
|
||||
)
|
||||
}
|
||||
|
@ -187,10 +184,6 @@ class Procedure < ApplicationRecord
|
|||
switch_list_order(types_de_champ_private, index_of_first_element)
|
||||
end
|
||||
|
||||
def switch_types_de_piece_justificative(index_of_first_element)
|
||||
switch_list_order(types_de_piece_justificative, index_of_first_element)
|
||||
end
|
||||
|
||||
def switch_list_order(list, index_of_first_element)
|
||||
if index_of_first_element < 0 ||
|
||||
index_of_first_element == list.count - 1 ||
|
||||
|
@ -225,7 +218,6 @@ class Procedure < ApplicationRecord
|
|||
procedure.remote_logo_url = self.logo_url
|
||||
procedure.lien_notice = nil
|
||||
|
||||
procedure.types_de_champ += PiecesJustificativesService.types_pj_as_types_de_champ(self)
|
||||
if is_different_admin || from_library
|
||||
procedure.types_de_champ.each { |tdc| tdc.options&.delete(:old_pj) }
|
||||
end
|
||||
|
@ -280,10 +272,6 @@ class Procedure < ApplicationRecord
|
|||
whitelisted_at.present?
|
||||
end
|
||||
|
||||
def has_old_pjs?
|
||||
types_de_piece_justificative.any?
|
||||
end
|
||||
|
||||
def total_dossier
|
||||
self.dossiers.state_not_brouillon.size
|
||||
end
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
class TypeDePieceJustificative < ApplicationRecord
|
||||
has_many :pieces_justificatives, dependent: :destroy
|
||||
|
||||
belongs_to :procedure
|
||||
|
||||
validates :libelle, presence: true, allow_blank: false, allow_nil: false
|
||||
|
||||
validates :lien_demarche, format: { with: URI.regexp }, allow_blank: true, allow_nil: true
|
||||
scope :ordered, -> { order(order_place: :asc) }
|
||||
end
|
|
@ -10,12 +10,11 @@ class User < ApplicationRecord
|
|||
# Include default devise modules. Others available are:
|
||||
# :confirmable, :lockable, :timeoutable and :omniauthable
|
||||
devise :database_authenticatable, :registerable, :async,
|
||||
:recoverable, :rememberable, :trackable, :validatable, :confirmable
|
||||
:recoverable, :rememberable, :trackable, :validatable, :confirmable, :lockable
|
||||
|
||||
has_many :dossiers, dependent: :destroy
|
||||
has_many :invites, dependent: :destroy
|
||||
has_many :dossiers_invites, through: :invites, source: :dossier
|
||||
has_many :piece_justificative, dependent: :destroy
|
||||
has_many :feedbacks, dependent: :destroy
|
||||
has_one :france_connect_information, dependent: :destroy
|
||||
|
||||
|
|
|
@ -50,7 +50,6 @@ class DossierSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def pieces_justificatives
|
||||
ActiveModelSerializers::SerializableResource.new(object.pieces_justificatives).serializable_hash +
|
||||
PiecesJustificativesService.serialize_champs_as_pjs(object)
|
||||
end
|
||||
|
||||
|
@ -61,7 +60,6 @@ class DossierSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def types_de_piece_justificative
|
||||
ActiveModelSerializers::SerializableResource.new(object.types_de_piece_justificative).serializable_hash +
|
||||
PiecesJustificativesService.serialize_types_de_champ_as_type_pj(object)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class PieceJustificativeSerializer < ActiveModel::Serializer
|
||||
attributes :created_at,
|
||||
:type_de_piece_justificative_id,
|
||||
:content_url
|
||||
|
||||
has_one :user
|
||||
|
||||
def created_at
|
||||
object.created_at&.in_time_zone('UTC')
|
||||
end
|
||||
end
|
|
@ -49,7 +49,6 @@ class ProcedureSerializer < ActiveModel::Serializer
|
|||
end
|
||||
|
||||
def types_de_piece_justificative
|
||||
ActiveModelSerializers::SerializableResource.new(object.types_de_piece_justificative).serializable_hash +
|
||||
PiecesJustificativesService.serialize_types_de_champ_as_type_pj(object)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
class TypeDePieceJustificativeSerializer < ActiveModel::Serializer
|
||||
attributes :id,
|
||||
:libelle,
|
||||
:description,
|
||||
:order_place,
|
||||
:lien_demarche
|
||||
end
|
|
@ -1,173 +0,0 @@
|
|||
class CarrierwaveActiveStorageMigrationService
|
||||
def ensure_openstack_copy_possible!(uploader)
|
||||
ensure_active_storage_openstack!
|
||||
ensure_carrierwave_openstack!(uploader)
|
||||
ensure_active_storage_and_carrierwave_credetials_match(uploader)
|
||||
end
|
||||
|
||||
def ensure_active_storage_openstack!
|
||||
# If we manage to get the client, it means that ActiveStorage is on OpenStack
|
||||
openstack_client!
|
||||
end
|
||||
|
||||
def openstack_client!
|
||||
@openstack_client ||= active_storage_openstack_client!
|
||||
end
|
||||
|
||||
def active_storage_openstack_client!
|
||||
service = ActiveStorage::Blob.service
|
||||
|
||||
if defined?(ActiveStorage::Service::DsProxyService) &&
|
||||
service.is_a?(ActiveStorage::Service::DsProxyService)
|
||||
service = service.wrapped
|
||||
end
|
||||
|
||||
if !defined?(ActiveStorage::Service::OpenStackService) ||
|
||||
!service.is_a?(ActiveStorage::Service::OpenStackService)
|
||||
raise StandardError, 'ActiveStorage must be backed by OpenStack'
|
||||
end
|
||||
|
||||
service.client
|
||||
end
|
||||
|
||||
def ensure_carrierwave_openstack!(uploader)
|
||||
storage = fog_client!(uploader)
|
||||
|
||||
if !defined?(Fog::OpenStack::Storage::Real) ||
|
||||
!storage.is_a?(Fog::OpenStack::Storage::Real)
|
||||
raise StandardError, 'Carrierwave must be backed by OpenStack'
|
||||
end
|
||||
end
|
||||
|
||||
def fog_client!(uploader)
|
||||
storage = uploader.new.send(:storage)
|
||||
|
||||
if !defined?(CarrierWave::Storage::Fog) ||
|
||||
!storage.is_a?(CarrierWave::Storage::Fog)
|
||||
raise StandardError, 'Carrierwave must be backed by a Fog provider'
|
||||
end
|
||||
|
||||
storage.connection
|
||||
end
|
||||
|
||||
# OpenStack Swift's COPY object command works across different buckets, but they still need
|
||||
# to be on the same object store. This method tries to ensure that Carrierwave and ActiveStorage
|
||||
# are indeed pointing to the same Swift store.
|
||||
def ensure_active_storage_and_carrierwave_credetials_match(uploader)
|
||||
auth_keys = [
|
||||
:openstack_tenant,
|
||||
:openstack_api_key,
|
||||
:openstack_username,
|
||||
:openstack_region,
|
||||
:openstack_management_url
|
||||
]
|
||||
|
||||
active_storage_creds = openstack_client!.credentials.slice(*auth_keys)
|
||||
carrierwave_creds = fog_client!(uploader).credentials.slice(*auth_keys)
|
||||
|
||||
if active_storage_creds != carrierwave_creds
|
||||
raise StandardError, "Active Storage and Carrierwave credentials must match"
|
||||
end
|
||||
end
|
||||
|
||||
# If identify is true, force ActiveStorage to examine the beginning of the file
|
||||
# to determine its MIME type. This identification does not happen immediately,
|
||||
# but when the first attachment that references this blob is created.
|
||||
def make_blob(uploader, created_at, filename: nil, identify: false)
|
||||
content_type = uploader.content_type
|
||||
identified = content_type.present? && !identify
|
||||
|
||||
ActiveStorage::Blob.create(
|
||||
filename: filename || uploader.filename,
|
||||
content_type: content_type,
|
||||
byte_size: uploader.size,
|
||||
checksum: checksum(uploader),
|
||||
created_at: created_at,
|
||||
metadata: { identified: identified, virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
)
|
||||
end
|
||||
|
||||
def make_empty_blob(uploader, created_at, filename: nil)
|
||||
content_type = uploader.content_type || 'text/plain'
|
||||
|
||||
blob = ActiveStorage::Blob.build_after_upload(
|
||||
io: StringIO.new('File not found when migrating from CarrierWave.'),
|
||||
filename: filename || uploader.filename,
|
||||
content_type: content_type || 'text/plain',
|
||||
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
)
|
||||
blob.created_at = created_at
|
||||
blob.save!
|
||||
blob
|
||||
end
|
||||
|
||||
def checksum(uploader)
|
||||
hex_to_base64(uploader.file.send(:file).etag)
|
||||
end
|
||||
|
||||
def hex_to_base64(hexdigest)
|
||||
[[hexdigest].pack("H*")].pack("m0")
|
||||
end
|
||||
|
||||
def copy_from_carrierwave_to_active_storage!(source_name, blob)
|
||||
openstack_client!.copy_object(
|
||||
carrierwave_container_name,
|
||||
source_name,
|
||||
active_storage_container_name,
|
||||
blob.key
|
||||
)
|
||||
|
||||
fix_content_type(blob)
|
||||
end
|
||||
|
||||
def carrierwave_container_name
|
||||
Rails.application.secrets.fog[:directory]
|
||||
end
|
||||
|
||||
def active_storage_container_name
|
||||
ENV['FOG_ACTIVESTORAGE_DIRECTORY']
|
||||
end
|
||||
|
||||
def delete_from_active_storage!(blob)
|
||||
openstack_client!.delete_object(
|
||||
active_storage_container_name,
|
||||
blob.key
|
||||
)
|
||||
end
|
||||
|
||||
# Before calling this method, you must make sure the file has been uploaded for the blob.
|
||||
# Otherwise, this method might fail if it needs to read the beginning of the file to
|
||||
# update the blob’s MIME type.
|
||||
def make_attachment(model, attachment_name, blob)
|
||||
attachment = ActiveStorage::Attachment.create(
|
||||
name: attachment_name,
|
||||
record_type: model.class.base_class.name,
|
||||
record_id: model.id,
|
||||
blob: blob,
|
||||
created_at: model.updated_at.iso8601
|
||||
)
|
||||
|
||||
# Making the attachment may have triggerred MIME type auto detection on the blob,
|
||||
# so we make sure to sync that potentially new MIME type to the object in OpenStack
|
||||
fix_content_type(blob)
|
||||
|
||||
attachment
|
||||
end
|
||||
|
||||
def fix_content_type(blob, retry_delay: 5)
|
||||
retries ||= 0
|
||||
# In OpenStack, ActiveStorage cannot inject the MIME type on the fly during direct
|
||||
# download. Instead, the MIME type needs to be stored statically on the file object
|
||||
# in OpenStack. This is what this call does.
|
||||
blob.service.change_content_type(blob.key, blob.content_type)
|
||||
rescue
|
||||
# When we quickly create a new attachment, and then change its content type,
|
||||
# the Object Storage may not be synchronized yet. It this cas, it will return a
|
||||
# "409 Conflict" error.
|
||||
#
|
||||
# Wait for a while, then try again twice (before giving up).
|
||||
sleep(retry_delay)
|
||||
retry if (retries += 1) < 3
|
||||
raise
|
||||
end
|
||||
end
|
|
@ -1,187 +0,0 @@
|
|||
require Rails.root.join("lib", "tasks", "task_helper")
|
||||
|
||||
class PieceJustificativeToChampPieceJointeMigrationService
|
||||
def initialize(**params)
|
||||
params.each do |key, value|
|
||||
instance_variable_set("@#{key}", value)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_correct_storage_configuration!
|
||||
storage_service.ensure_openstack_copy_possible!(PieceJustificativeUploader)
|
||||
end
|
||||
|
||||
def procedures_with_pjs_in_range(ids_range)
|
||||
procedures_with_pj = Procedure.unscope(where: :hidden_at).joins(:types_de_piece_justificative).distinct
|
||||
procedures_with_pj.where(id: ids_range)
|
||||
end
|
||||
|
||||
def number_of_champs_to_migrate(procedure)
|
||||
(procedure.types_de_piece_justificative.count + 1) * procedure.dossiers.unscope(where: :hidden_at).count
|
||||
end
|
||||
|
||||
def convert_procedure_pjs_to_champ_pjs(procedure, &progress)
|
||||
types_de_champ_pj = PiecesJustificativesService.types_pj_as_types_de_champ(procedure)
|
||||
populate_champs_pjs!(procedure, types_de_champ_pj, &progress)
|
||||
|
||||
# Only destroy the old types PJ once everything has been safely migrated to
|
||||
# champs PJs.
|
||||
|
||||
# First destroy the individual PJ champs on all dossiers.
|
||||
# It will cascade and destroy the PJs, and delete the linked objects from remote storage.
|
||||
procedure.dossiers.unscope(where: :hidden_at).includes(:champs).find_each do |dossier|
|
||||
destroy_pieces_justificatives(dossier)
|
||||
end
|
||||
|
||||
# Now we can destroy the type de champ themselves,
|
||||
# without cascading the timestamp update on all attached dossiers.
|
||||
procedure.types_de_piece_justificative.destroy_all
|
||||
end
|
||||
|
||||
def storage_service
|
||||
@storage_service ||= CarrierwaveActiveStorageMigrationService.new
|
||||
end
|
||||
|
||||
def populate_champs_pjs!(procedure, types_de_champ_pj, &progress)
|
||||
procedure.types_de_champ += types_de_champ_pj
|
||||
|
||||
# Unscope to make sure all dossiers are migrated, even the soft-deleted ones
|
||||
procedure.dossiers.unscope(where: :hidden_at).includes(:champs).find_each do |dossier|
|
||||
migrate_dossier!(dossier, types_de_champ_pj, &progress)
|
||||
end
|
||||
|
||||
rescue StandardError, SignalException
|
||||
# If anything goes wrong, we roll back the migration by destroying the newly created
|
||||
# types de champ, champs blobs and attachments.
|
||||
rake_puts "Error received. Rolling back migration of procedure #{procedure.id}…"
|
||||
rollback_migration!(types_de_champ_pj)
|
||||
rake_puts "Migration of procedure #{procedure.id} rolled back."
|
||||
|
||||
# Reraise the exception to abort the migration.
|
||||
raise
|
||||
end
|
||||
|
||||
def migrate_dossier!(dossier, types_de_champ_pj)
|
||||
# Add the new pieces justificatives champs to the dossier
|
||||
champs_pj = types_de_champ_pj.map(&:build_champ)
|
||||
preserving_updated_at(dossier) do
|
||||
dossier.champs += champs_pj
|
||||
end
|
||||
|
||||
# Copy the dossier old pieces jointes to the new champs
|
||||
# (even if the champs already existed, so that we ensure a clean state)
|
||||
champs_pj.each do |champ|
|
||||
type_pj_id = champ.type_de_champ.old_pj&.fetch('stable_id', nil)
|
||||
pj = dossier.retrieve_last_piece_justificative_by_type(type_pj_id)
|
||||
|
||||
if pj.present?
|
||||
preserving_updated_at(dossier) do
|
||||
convert_pj_to_champ!(pj, champ)
|
||||
end
|
||||
|
||||
champ.update_columns(
|
||||
updated_at: pj.updated_at,
|
||||
created_at: pj.created_at
|
||||
)
|
||||
else
|
||||
champ.update_columns(
|
||||
created_at: dossier.created_at,
|
||||
# Set an updated_at date that won't cause notifications to appear
|
||||
# on gestionnaires' dashboard.
|
||||
updated_at: dossier.created_at
|
||||
)
|
||||
end
|
||||
|
||||
yield if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
def convert_pj_to_champ!(pj, champ)
|
||||
actual_file_exists = pj.content.file.send(:file)
|
||||
if actual_file_exists
|
||||
blob = make_blob(pj)
|
||||
|
||||
# Upload the file before creating the attachment to make sure MIME type
|
||||
# identification doesn’t fail.
|
||||
storage_service.copy_from_carrierwave_to_active_storage!(pj.content.path, blob)
|
||||
attachment = storage_service.make_attachment(champ, 'piece_justificative_file', blob)
|
||||
|
||||
else
|
||||
make_empty_blob(pj)
|
||||
rake_puts "Notice: attached file for champ #{champ.id} not found. An empty blob has been attached instead."
|
||||
end
|
||||
|
||||
# By reloading, we force ActiveStorage to look at the attachment again, and see
|
||||
# that one exists now. We do this so that, if we need to roll back and destroy the champ,
|
||||
# the blob, the attachment and the actual file on OpenStack also get deleted.
|
||||
champ.reload
|
||||
rescue StandardError, SignalException
|
||||
# Destroy partially attached object that the more general rescue in `populate_champs_pjs!`
|
||||
# might not be able to handle.
|
||||
|
||||
if blob&.key.present?
|
||||
begin
|
||||
storage_service.delete_from_active_storage!(blob)
|
||||
rescue => e
|
||||
# The cleanup attempt failed, perhaps because the object had not been
|
||||
# successfully copied to the Active Storage bucket yet.
|
||||
# Continue trying to clean up the rest anyway.
|
||||
pp e
|
||||
end
|
||||
end
|
||||
|
||||
blob&.destroy
|
||||
attachment&.destroy
|
||||
champ.reload
|
||||
|
||||
# Reraise the exception to abort the migration.
|
||||
raise
|
||||
end
|
||||
|
||||
def rollback_migration!(types_de_champ_pj)
|
||||
types_de_champ_pj.each do |type_champ|
|
||||
# First destroy all the individual champs on dossiers
|
||||
type_champ.champ.each do |champ|
|
||||
begin
|
||||
destroy_champ_pj(Dossier.unscope(where: :hidden_at).find(champ.dossier_id), champ)
|
||||
rescue => e
|
||||
rake_puts e
|
||||
rake_puts "Rolling back of champ #{champ.id} failed. Continuing to roll back…"
|
||||
end
|
||||
end
|
||||
# Now we can destroy the type de champ itself,
|
||||
# without cascading the timestamp update on all attached dossiers.
|
||||
type_champ.reload.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def make_blob(pj)
|
||||
storage_service.make_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename)
|
||||
end
|
||||
|
||||
def make_empty_blob(pj)
|
||||
storage_service.make_empty_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename)
|
||||
end
|
||||
|
||||
def preserving_updated_at(model)
|
||||
original_modification_date = model.updated_at
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
model.update_column(:updated_at, original_modification_date)
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_pieces_justificatives(dossier)
|
||||
preserving_updated_at(dossier) do
|
||||
dossier.pieces_justificatives.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_champ_pj(dossier, champ)
|
||||
preserving_updated_at(dossier) do
|
||||
champ.piece_justificative_file.purge
|
||||
champ.destroy
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,75 +1,4 @@
|
|||
class PiecesJustificativesService
|
||||
def self.upload!(dossier, user, params)
|
||||
tpj_contents = dossier.types_de_piece_justificative
|
||||
.map { |tpj| [tpj, params["piece_justificative_#{tpj.id}"]] }
|
||||
.select { |_, content| content.present? }
|
||||
|
||||
without_virus, with_virus = tpj_contents
|
||||
.partition { |_, content| ClamavService.safe_file?(content.path) }
|
||||
|
||||
errors = with_virus
|
||||
.map { |_, content| "#{content.original_filename} : virus détecté" }
|
||||
|
||||
errors + without_virus
|
||||
.map { |tpj, content| save_pj(content, dossier, tpj, user) }
|
||||
.compact()
|
||||
end
|
||||
|
||||
def self.save_pj(content, dossier, tpj, user)
|
||||
pj = PieceJustificative.new(content: content,
|
||||
dossier: dossier,
|
||||
type_de_piece_justificative: tpj,
|
||||
user: user)
|
||||
|
||||
pj.save ? nil : "le fichier #{content.original_filename} (#{pj.libelle.truncate(200)}) n'a pas pu être sauvegardé"
|
||||
end
|
||||
|
||||
def self.missing_pj_error_messages(dossier)
|
||||
mandatory_pjs = dossier.types_de_piece_justificative.select(&:mandatory)
|
||||
present_pjs = dossier.pieces_justificatives.map(&:type_de_piece_justificative)
|
||||
missing_pjs = mandatory_pjs - present_pjs
|
||||
|
||||
missing_pjs.map { |pj| "La pièce jointe #{pj.libelle.truncate(200)} doit être fournie." }
|
||||
end
|
||||
|
||||
def self.types_pj_as_types_de_champ(procedure)
|
||||
max_order_place = procedure.types_de_champ.pluck(:order_place).compact.max || -1
|
||||
order_place = max_order_place + 1
|
||||
|
||||
types_de_champ = [
|
||||
TypeDeChamp.new(
|
||||
libelle: "Pièces jointes",
|
||||
type_champ: TypeDeChamp.type_champs.fetch(:header_section),
|
||||
order_place: order_place
|
||||
)
|
||||
]
|
||||
types_de_champ += procedure.types_de_piece_justificative.map do |tpj|
|
||||
order_place += 1
|
||||
description = tpj.description
|
||||
if tpj.lien_demarche.present?
|
||||
if description.present?
|
||||
description += "\n"
|
||||
end
|
||||
description += "Récupérer le formulaire vierge pour mon dossier : #{tpj.lien_demarche}"
|
||||
end
|
||||
TypeDeChamp.new(
|
||||
libelle: tpj.libelle,
|
||||
type_champ: TypeDeChamp.type_champs.fetch(:piece_justificative),
|
||||
description: description,
|
||||
order_place: order_place,
|
||||
mandatory: tpj.mandatory,
|
||||
old_pj: {
|
||||
stable_id: tpj.id
|
||||
}
|
||||
)
|
||||
end
|
||||
if types_de_champ.count > 1
|
||||
types_de_champ
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def self.liste_pieces_justificatives(dossier)
|
||||
dossier.champs
|
||||
.select { |c| c.type_champ == TypeDeChamp.type_champs.fetch(:piece_justificative) }
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
class PieceJustificativeUploader < BaseUploader
|
||||
before :cache, :set_original_filename
|
||||
|
||||
# Choose what kind of storage to use for this uploader:
|
||||
if Flipflop.remote_storage?
|
||||
storage :fog
|
||||
else
|
||||
storage :file
|
||||
end
|
||||
|
||||
# Override the directory where uploaded files will be stored.
|
||||
# This is a sensible default for uploaders that are meant to be mounted:
|
||||
def store_dir
|
||||
if !Flipflop.remote_storage?
|
||||
"./uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
|
||||
end
|
||||
end
|
||||
|
||||
# Add a white list of extensions which are allowed to be uploaded.
|
||||
# For images you might use something like this:
|
||||
def extension_whitelist
|
||||
['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'jpg', 'jpeg', 'png']
|
||||
end
|
||||
|
||||
def filename
|
||||
if original_filename.present? || model.content_secure_token
|
||||
if Flipflop.remote_storage?
|
||||
filename = "#{model.class.to_s.underscore}-#{secure_token}.#{file.extension&.downcase}"
|
||||
else
|
||||
filename = "#{model.class.to_s.underscore}.#{file.extension&.downcase}"
|
||||
end
|
||||
end
|
||||
filename
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def secure_token
|
||||
model.content_secure_token ||= generate_secure_token
|
||||
end
|
||||
|
||||
def generate_secure_token
|
||||
SecureRandom.uuid
|
||||
end
|
||||
|
||||
def set_original_filename(file)
|
||||
if file.respond_to?(:original_filename)
|
||||
model.original_filename ||= file.original_filename
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
.pieces-justificatives-fields
|
||||
= f.fields_for :types_de_piece_justificative, types_de_piece_justificative, remote: true do |ff|
|
||||
.form-inline
|
||||
.form-group
|
||||
%h4 Libellé
|
||||
= ff.text_field :libelle, class: 'form-control libelle', placeholder: 'Libellé'
|
||||
.form-group
|
||||
%h4 Description
|
||||
= ff.text_area :description, class: 'form-control description', placeholder: 'Description'
|
||||
.form-group
|
||||
%h4
|
||||
Lien du formulaire vierge
|
||||
%small
|
||||
(optionel)
|
||||
= ff.url_field :lien_demarche, class: 'form-control', placeholder: 'Lien du document vierge'
|
||||
|
||||
.form-group
|
||||
= ff.hidden_field :order_place, value: ff.index
|
||||
= ff.hidden_field :id
|
||||
- if ff.object.id.present?
|
||||
.form-group
|
||||
%br
|
||||
= button_up(@procedure, "piece_justificative", ff.index, move_up_admin_procedure_pieces_justificatives_path(@procedure, ff.index))
|
||||
= button_down(@procedure, "piece_justificative", ff.index, move_down_admin_procedure_pieces_justificatives_path(@procedure, ff.index))
|
||||
|
||||
.form-group
|
||||
%h4 Obligatoire ?
|
||||
.center
|
||||
= ff.check_box :mandatory
|
||||
|
||||
.form-group
|
||||
%br
|
||||
- if ff.object.id.nil?
|
||||
= f.submit('Ajouter la pièce', class: 'btn btn-success', id: 'add_piece_justificative')
|
||||
- else
|
||||
= link_to("", admin_procedure_piece_justificative_path(@procedure, ff.object.id), method: :delete, remote: true, id: "delete_type_de_piece_justificative_#{ff.object.id}", class: %w(form-control btn btn-danger fa fa-trash-o) )
|
|
@ -1,7 +0,0 @@
|
|||
= form_for [:admin, @procedure], url: admin_procedure_pieces_justificatives_path(@procedure), remote: true do |f|
|
||||
#liste_piece_justificative
|
||||
= render partial: 'fields', locals: { types_de_piece_justificative: @procedure.types_de_piece_justificative, f: f }
|
||||
= f.submit "Enregistrer", class: 'btn btn-success', id: :save
|
||||
%hr
|
||||
#new_type_de_piece_justificative
|
||||
= render partial: 'fields', locals: { types_de_piece_justificative: TypeDePieceJustificative.new, f: f }
|
|
@ -1,25 +0,0 @@
|
|||
.row.white-back
|
||||
.alert.alert-info
|
||||
.form-group
|
||||
%p
|
||||
Pour vos nouveaux besoins de pièces jointes, nous vous invitons à
|
||||
= link_to(champs_procedure_path(@procedure)) do
|
||||
rajouter des champs
|
||||
\ <em>pièce justificative</em> à votre formulaire.
|
||||
|
||||
%p
|
||||
Ils offrent les avantages suivants :
|
||||
|
||||
%ul
|
||||
%li Pièces justificatives au fil du formulaire, pour un déroulé plus fluide
|
||||
%li Possibilité de fournir à l’usager un fichier type à remplir et renvoyer
|
||||
%li Possibilité pour l’usager de supprimer les documents joints par erreur
|
||||
%li Support des pièces de grande taille (jusqu’à 200 Mo par pièce)
|
||||
%li Pas de limite de soumission simultanée de plusieurs pièces, pour une expérience usager plus confortable
|
||||
|
||||
= link_to(champs_procedure_path(@procedure), class: 'btn btn-success') do
|
||||
Ajouter un champ PJ
|
||||
|
||||
- if @procedure.has_old_pjs?
|
||||
#piece_justificative_form
|
||||
= render 'form'
|
|
@ -1,2 +0,0 @@
|
|||
<%= render_flash(timeout: 3000, sticky: true) %>
|
||||
<%= render_to_element('#piece_justificative_form', partial: 'admin/pieces_justificatives/form', locals: { procedure: @procedure }) %>
|
|
@ -1,4 +0,0 @@
|
|||
.field-unit__label
|
||||
= f.label field.attribute
|
||||
.field-unit__field
|
||||
= f.text_field field.attribute
|
|
@ -1 +0,0 @@
|
|||
= field.to_s
|
|
@ -1,12 +0,0 @@
|
|||
- if field.data.any?
|
||||
%table.collection-data{ "aria-labelledby": "page-title" }
|
||||
%thead
|
||||
%tr
|
||||
%td.cell-label Libelle
|
||||
%tbody
|
||||
- field.data.order(:order_place).each do |f|
|
||||
%tr
|
||||
%td.cell-data
|
||||
= f.libelle
|
||||
- else
|
||||
Aucun
|
|
@ -17,20 +17,6 @@
|
|||
- if champs.any?
|
||||
= render partial: "shared/dossiers/champs", locals: { champs: champs, dossier: @dossier, demande_seen_at: nil, profile: 'instructeur' }
|
||||
|
||||
- if @dossier.types_de_piece_justificative.any?
|
||||
%h3 Pièces jointes
|
||||
|
||||
%table
|
||||
- @dossier.procedure.types_de_piece_justificative.each do |type_de_piece_justificative|
|
||||
%tr
|
||||
%th= "#{type_de_piece_justificative.libelle} :"
|
||||
%td
|
||||
- pj = @dossier.retrieve_last_piece_justificative_by_type(type_de_piece_justificative.id)
|
||||
- if pj.present?
|
||||
#{pj.original_filename}
|
||||
- else
|
||||
Pièce non fournie
|
||||
|
||||
%h2 Annotations privées
|
||||
|
||||
- champs_annotations_privees = @dossier.champs_private
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
= render partial: 'layouts/left_panels/left_panel_admin_procedurescontroller_navbar', locals: { active: 'Pieces' }
|
|
@ -42,11 +42,6 @@
|
|||
.procedure-list-element{ class: ('active' if active == 'Champs') }
|
||||
Champs
|
||||
|
||||
- if !@procedure.locked?
|
||||
%a#onglet-pieces{ href: url_for(admin_procedure_pieces_justificatives_path(@procedure)) }
|
||||
.procedure-list-element{ class: ('active' if active == 'Pieces') }
|
||||
Pièces jointes
|
||||
|
||||
- if !@procedure.locked?
|
||||
%a#onglet-private-champs{ href: annotations_procedure_path(@procedure) }
|
||||
.procedure-list-element{ class: ('active' if active == 'Annotations privées') }
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
- case notification.type_notif
|
||||
- when "commentaire"
|
||||
.type-notif.fa.fa-comments-o
|
||||
- when "submitted"
|
||||
.type-notif.fa.fa-thumbs-o-up
|
||||
- when "champs"
|
||||
.type-notif.fa.fa-list-alt
|
||||
- when "piece_justificative"
|
||||
.type-notif.fa.fa-chain
|
|
@ -1,83 +0,0 @@
|
|||
- content_for(:title, 'Tour de France 2018')
|
||||
|
||||
- content_for :footer do
|
||||
= render partial: "root/footer"
|
||||
|
||||
.tour-de-france
|
||||
.blue-panel
|
||||
.small-container
|
||||
%img.tour-de-france-logo{ src: image_url("tour-de-france/logo.svg") }
|
||||
|
||||
.white-panel
|
||||
.small-container
|
||||
%h2 Présentation
|
||||
|
||||
%p
|
||||
Cet automne, l’équipe de demarches-simplifiees.fr se lance dans un Tour de France afin de sensibiliser les agents publics aux bénéfices du numérique pour leur métier. Notre mission est de faciliter la mise en ligne de nouveaux services et d’accompagner les administrations d’État et territoriales dans la dématérialisation.
|
||||
|
||||
%p
|
||||
Que vous ayez un profil SI ou métier, ce sera pour vous l’occasion de vous former à l’utilisation de demarches-simplifiees.fr et de mettre en place des services en ligne pour les formulaires que vous traitez encore en papier.
|
||||
|
||||
%p
|
||||
La journée type se déroulera en deux temps :
|
||||
|
||||
.cards
|
||||
.card.card-half.pull-left
|
||||
%h3 Matin
|
||||
%h4 9 h 30 - 12 h 00
|
||||
|
||||
Interventions consacrées aux enjeux généraux de la dématérialisation, à la présentation de demarches-simplifiees.fr et à des retours d’expériences d’agents utilisant déjà cette plateforme sur le territoire.
|
||||
|
||||
.card.card-half.pull-right
|
||||
%h3 Après-midi
|
||||
%h4 13 h 30 - 16 h 30
|
||||
Ateliers de formation avec une réflexion étape par étape sur les problématiques liées au passage à la dématérialisation. Vous pourrez identifier des cas d’usage, apprendre à créer des formulaires en ligne et organiser l’instruction sur demarches-simplifiees.fr.
|
||||
|
||||
%p
|
||||
Ces événements sont une opportunité pour tous les agents publics d’améliorer leur environnement de travail et le service rendu aux usagers, n’hésitez pas à relayer l’information auprès des services déconcentrés et des collectivités.
|
||||
|
||||
.photos-panel
|
||||
.small-container
|
||||
%img.atelier-photo.pull-left{ src: image_url("tour-de-france/atelier1.jpg") }
|
||||
%img.atelier-photo.pull-right{ src: image_url("tour-de-france/atelier2.jpg") }
|
||||
.clearfix
|
||||
|
||||
.grey-panel
|
||||
.small-container
|
||||
%h2.below-photos Inscription
|
||||
|
||||
%p
|
||||
Vous souhaitez participer à l’événement dans votre région ? Inscrivez-vous à l’événement en remplissant le formulaire via le lien correspondant à votre région ci-dessous.
|
||||
|
||||
%p
|
||||
Les places aux ateliers sont restreintes, ainsi si vous faite la demande de participation dans le formulaire d’inscription vous recevrez une confirmation ultérieure par email.
|
||||
|
||||
%ul
|
||||
%li
|
||||
= link_to("Étape Normandie, le 26 septembre à Rouen", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-etape-normandie", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Occitanie, le 15 octobre à Toulouse", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-occitanie", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Nouvelle-Aquitaine, le 18 octobre à Bordeaux", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-aquitaine", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Martinique, le 24 octobre à Fort-de-France", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-martinique", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape PACA, le 6 novembre à Marseille", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-paca", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Corse, le 8 novembre à Ajaccio", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-corse", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Ile-de-France, le 13 novembre à Paris", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-iledefrance", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Bourgogne Franche-Comté, le 14 novembre à Dijon", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-bourgogne-fc", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Pays de la Loire, le 20 novembre à Nantes", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-paysloire", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Bretagne, le 22 novembre à Rennes", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-bretagne", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Hauts-de-France, le 27 novembre à Lille", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-hautsdefrance", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Grand-Est, le 29 novembre à Metz", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-fr-grand-est", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Centre Val-de-Loire, le 5 décembre à Orleans", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-centre-vdl", target: "_blank", rel: "noopener")
|
||||
%li
|
||||
= link_to("Étape Auvergne-Rhone-Alpes, le 7 décembre à Lyon", "https://www.demarches-simplifiees.fr/commencer/tour-de-france-demarches-simplifiees-auvergne-rhon", target: "_blank", rel: "noopener")
|
|
@ -26,8 +26,3 @@
|
|||
- if champs.any?
|
||||
.card
|
||||
= render partial: "shared/dossiers/champs", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile }
|
||||
|
||||
- if dossier.types_de_piece_justificative.any?
|
||||
.tab-title Pièces jointes
|
||||
.card
|
||||
= render partial: "shared/dossiers/pieces_jointes", locals: { dossier: dossier, demande_seen_at: demande_seen_at }
|
||||
|
|
|
@ -31,45 +31,6 @@
|
|||
= render partial: "shared/dossiers/editable_champs/editable_champ",
|
||||
locals: { champ: champ, form: champ_form }
|
||||
|
||||
- tpjs = dossier.types_de_piece_justificative.order('order_place ASC')
|
||||
- if tpjs.present?
|
||||
.card.featured
|
||||
.card-title
|
||||
Pièces jointes
|
||||
|
||||
.warning
|
||||
- if tpjs.many?
|
||||
Pour éviter toute erreur, nous vous conseillons de limiter la taille de chaque pièce jointe à 20 Mo, et de les ajouter une par une, en enregistrant votre
|
||||
= dossier.brouillon? ? "brouillon" : "dossier"
|
||||
après chaque ajout.
|
||||
- else
|
||||
Pour éviter toute erreur, nous vous conseillons de limiter la taille de votre pièce jointe à 20 Mo.
|
||||
|
||||
- tpjs.each do |tpj|
|
||||
.pj-input
|
||||
%label{ for: "piece_justificative_#{tpj.id}" }
|
||||
= tpj.libelle
|
||||
- if tpj.mandatory?
|
||||
%span.mandatory *
|
||||
|
||||
%p.piece-description= tpj.description
|
||||
|
||||
- if tpj.lien_demarche.present?
|
||||
%p.piece-description
|
||||
Récupérer le formulaire vierge pour mon dossier :
|
||||
= link_to "Télécharger", tpj.lien_demarche, target: :blank, rel: :noopener
|
||||
|
||||
- if dossier.was_piece_justificative_uploaded_for_type_id?(tpj.id)
|
||||
- pj = dossier.retrieve_last_piece_justificative_by_type(tpj.id)
|
||||
%p
|
||||
Pièce jointe déjà importée :
|
||||
= link_to pj.original_filename, pj.content_url, target: :blank, rel: :noopener
|
||||
|
||||
= file_field_tag "piece_justificative_#{tpj.id}",
|
||||
accept: PieceJustificative.accept_format,
|
||||
max_file_size: 6.megabytes,
|
||||
required: (tpj.mandatory? && !dossier.was_piece_justificative_uploaded_for_type_id?(tpj.id))
|
||||
|
||||
- if !apercu
|
||||
.send-wrapper
|
||||
- if dossier.brouillon?
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
%table.table.vertical.pj.dossier-champs
|
||||
%tbody
|
||||
- dossier.procedure.types_de_piece_justificative.each do |type_de_piece_justificative|
|
||||
- pjs = dossier.retrieve_all_piece_justificative_by_type(type_de_piece_justificative.id).to_ary.dup
|
||||
- pj = pjs.shift if pjs.present?
|
||||
%tr
|
||||
%th= "#{type_de_piece_justificative.libelle} :"
|
||||
- if pj
|
||||
%td
|
||||
%span{ class: highlight_if_unseen_class(demande_seen_at, pj.updated_at) }
|
||||
= display_pj_filename(pj)
|
||||
·
|
||||
= link_to "Télécharger", pj.content_url, class: "link", target: :blank, rel: :noopener
|
||||
- if pjs.present?
|
||||
%br
|
||||
%span.dropdown
|
||||
%button.button.dropdown-button
|
||||
anciennes versions
|
||||
.dropdown-content.fade-in-down
|
||||
%ul.dropdown-items
|
||||
- pjs.each do |pj|
|
||||
%li
|
||||
= link_to pj.content_url, { target: :blank, rel: :noopener } do
|
||||
%span.filename= display_pj_filename(pj)
|
||||
%span
|
||||
ajoutée le #{try_format_datetime(pj.created_at)}
|
||||
- else
|
||||
%td Pièce non fournie
|
||||
%td.updated-at
|
||||
- if pj
|
||||
%span{ class: highlight_if_unseen_class(demande_seen_at, pj.updated_at) }
|
||||
modifié le
|
||||
= try_format_datetime(pj.updated_at)
|
|
@ -161,27 +161,27 @@ Devise.setup do |config|
|
|||
# Defines which strategy will be used to lock an account.
|
||||
# :failed_attempts = Locks an account after a number of failed attempts to sign in.
|
||||
# :none = No lock strategy. You should handle locking by yourself.
|
||||
# config.lock_strategy = :failed_attempts
|
||||
config.lock_strategy = :failed_attempts
|
||||
|
||||
# Defines which key will be used when locking and unlocking an account
|
||||
# config.unlock_keys = [ :email ]
|
||||
config.unlock_keys = [:email]
|
||||
|
||||
# Defines which strategy will be used to unlock an account.
|
||||
# :email = Sends an unlock link to the user email
|
||||
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
|
||||
# :both = Enables both strategies
|
||||
# :none = No unlock strategy. You should handle unlocking by yourself.
|
||||
# config.unlock_strategy = :both
|
||||
config.unlock_strategy = :both
|
||||
|
||||
# Number of authentication tries before locking an account if lock_strategy
|
||||
# is failed attempts.
|
||||
# config.maximum_attempts = 20
|
||||
config.maximum_attempts = 6
|
||||
|
||||
# Time interval to unlock the account if :time is enabled as unlock_strategy.
|
||||
# config.unlock_in = 1.hour
|
||||
config.unlock_in = 1.hour
|
||||
|
||||
# Warn on the last attempt before the account is locked.
|
||||
# config.last_attempt_warning = true
|
||||
config.last_attempt_warning = true
|
||||
|
||||
# ==> Configuration for :recoverable
|
||||
#
|
||||
|
|
|
@ -10,8 +10,6 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
# inflect.uncountable %w( fish sheep )
|
||||
inflect.acronym 'API'
|
||||
inflect.acronym 'RNA'
|
||||
inflect.irregular 'piece_justificative', 'pieces_justificatives'
|
||||
inflect.irregular 'type_de_piece_justificative', 'types_de_piece_justificative'
|
||||
inflect.irregular 'type_de_champ', 'types_de_champ'
|
||||
inflect.irregular 'type_de_champ_private', 'types_de_champ_private'
|
||||
inflect.irregular 'assign_to', 'assign_tos'
|
||||
|
|
|
@ -138,10 +138,6 @@ fr:
|
|||
attributes:
|
||||
footer:
|
||||
too_long: ": le pied de page est trop long."
|
||||
piece_justificative:
|
||||
attributes:
|
||||
content:
|
||||
size_too_big: "La taille du fichier joint est trop importante. Elle doit être inférieure à 20Mo."
|
||||
user:
|
||||
attributes:
|
||||
reset_password_token:
|
||||
|
|
|
@ -138,7 +138,6 @@ Rails.application.routes.draw do
|
|||
get 'attachments/:id', to: 'attachments#show', as: :attachment
|
||||
delete 'attachments/:id', to: 'attachments#destroy'
|
||||
|
||||
get 'tour-de-france' => 'root#tour_de_france'
|
||||
get "patron" => "root#patron"
|
||||
get "accessibilite" => "root#accessibilite"
|
||||
get "suivi" => "root#suivi"
|
||||
|
@ -194,13 +193,6 @@ Rails.application.routes.draw do
|
|||
delete :delete_notice
|
||||
end
|
||||
|
||||
resource :pieces_justificatives, only: [:show, :update]
|
||||
resources :pieces_justificatives, only: :destroy
|
||||
resource :pieces_justificatives, only: [:show, :update] do
|
||||
post '/:index/move_up' => 'pieces_justificatives#move_up', as: :move_up
|
||||
post '/:index/move_down' => 'pieces_justificatives#move_down', as: :move_down
|
||||
end
|
||||
|
||||
resources :mail_templates, only: [:index, :edit, :update]
|
||||
|
||||
put 'archive' => 'procedures#archive', as: :archive
|
||||
|
|
23
db/migrate/20190522131959_add_lockable_to_devise.rb
Normal file
23
db/migrate/20190522131959_add_lockable_to_devise.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
class AddLockableToDevise < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :users, :failed_attempts, :integer, default: 0, null: false
|
||||
add_column :users, :unlock_token, :string
|
||||
add_column :users, :locked_at, :datetime
|
||||
add_index :users, :unlock_token, unique: true
|
||||
|
||||
add_column :gestionnaires, :failed_attempts, :integer, default: 0, null: false
|
||||
add_column :gestionnaires, :unlock_token, :string
|
||||
add_column :gestionnaires, :locked_at, :datetime
|
||||
add_index :gestionnaires, :unlock_token, unique: true
|
||||
|
||||
add_column :administrateurs, :failed_attempts, :integer, default: 0, null: false
|
||||
add_column :administrateurs, :unlock_token, :string
|
||||
add_column :administrateurs, :locked_at, :datetime
|
||||
add_index :administrateurs, :unlock_token, unique: true
|
||||
|
||||
add_column :administrations, :failed_attempts, :integer, default: 0, null: false
|
||||
add_column :administrations, :unlock_token, :string
|
||||
add_column :administrations, :locked_at, :datetime
|
||||
add_index :administrations, :unlock_token, unique: true
|
||||
end
|
||||
end
|
16
db/schema.rb
16
db/schema.rb
|
@ -63,8 +63,12 @@ ActiveRecord::Schema.define(version: 2019_07_17_151228) do
|
|||
t.boolean "active", default: false
|
||||
t.jsonb "features", default: {}, null: false
|
||||
t.string "encrypted_token"
|
||||
t.integer "failed_attempts", default: 0, null: false
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at"
|
||||
t.index ["email"], name: "index_administrateurs_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_administrateurs_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_administrateurs_on_unlock_token", unique: true
|
||||
end
|
||||
|
||||
create_table "administrateurs_gestionnaires", id: false, force: :cascade do |t|
|
||||
|
@ -100,8 +104,12 @@ ActiveRecord::Schema.define(version: 2019_07_17_151228) do
|
|||
t.string "last_sign_in_ip"
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.integer "failed_attempts", default: 0, null: false
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at"
|
||||
t.index ["email"], name: "index_administrations_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_administrations_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_administrations_on_unlock_token", unique: true
|
||||
end
|
||||
|
||||
create_table "assign_tos", id: :serial, force: :cascade do |t|
|
||||
|
@ -399,8 +407,12 @@ ActiveRecord::Schema.define(version: 2019_07_17_151228) do
|
|||
t.text "encrypted_login_token"
|
||||
t.datetime "login_token_created_at"
|
||||
t.jsonb "features", default: {"enable_email_login_token"=>true}, null: false
|
||||
t.integer "failed_attempts", default: 0, null: false
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at"
|
||||
t.index ["email"], name: "index_gestionnaires_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_gestionnaires_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_gestionnaires_on_unlock_token", unique: true
|
||||
end
|
||||
|
||||
create_table "individuals", id: :serial, force: :cascade do |t|
|
||||
|
@ -605,10 +617,14 @@ ActiveRecord::Schema.define(version: 2019_07_17_151228) do
|
|||
t.string "confirmation_token"
|
||||
t.datetime "confirmed_at"
|
||||
t.datetime "confirmation_sent_at"
|
||||
t.integer "failed_attempts", default: 0, null: false
|
||||
t.string "unlock_token"
|
||||
t.datetime "locked_at"
|
||||
t.text "unconfirmed_email"
|
||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||
t.index ["email"], name: "index_users_on_email", unique: true
|
||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
|
||||
end
|
||||
|
||||
create_table "virus_scans", force: :cascade do |t|
|
||||
|
|
|
@ -1,291 +0,0 @@
|
|||
module Tasks
|
||||
class DossierProcedureMigrator
|
||||
# Migrates dossiers from an old source procedure to a revised destination procedure.
|
||||
|
||||
class ChampMapping
|
||||
def initialize(source_procedure, destination_procedure, is_private)
|
||||
@source_procedure = source_procedure
|
||||
@destination_procedure = destination_procedure
|
||||
@is_private = is_private
|
||||
|
||||
@expected_source_types_de_champ = {}
|
||||
@expected_destination_types_de_champ = {}
|
||||
@source_to_destination_mapping = {}
|
||||
@source_champs_to_discard = Set[]
|
||||
@destination_champ_computations = []
|
||||
|
||||
setup_mapping
|
||||
end
|
||||
|
||||
def check_source_destination_consistency
|
||||
check_champs_consistency("#{privacy_label}source", @expected_source_types_de_champ, types_de_champ(@source_procedure))
|
||||
check_champs_consistency("#{privacy_label}destination", @expected_destination_types_de_champ, types_de_champ(@destination_procedure))
|
||||
end
|
||||
|
||||
def can_migrate?(dossier)
|
||||
true
|
||||
end
|
||||
|
||||
def migrate(dossier)
|
||||
# Since we’re going to iterate and change the champs at the same time,
|
||||
# we use to_a to make the list static and avoid nasty surprises
|
||||
original_champs = champs(dossier).to_a
|
||||
|
||||
compute_new_champs(dossier)
|
||||
|
||||
original_champs.each do |c|
|
||||
tdc_to = destination_type_de_champ(c)
|
||||
if tdc_to.present?
|
||||
c.update_columns(type_de_champ_id: tdc_to.id)
|
||||
elsif discard_champ?(c)
|
||||
champs(dossier).destroy(c)
|
||||
else
|
||||
fail "Unhandled source #{privacy_label}type de champ #{c.type_de_champ.order_place}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def compute_new_champs(dossier)
|
||||
@destination_champ_computations.each do |tdc, block|
|
||||
champs(dossier) << block.call(dossier, tdc)
|
||||
end
|
||||
end
|
||||
|
||||
def destination_type_de_champ(champ)
|
||||
@source_to_destination_mapping[champ.type_de_champ.order_place]
|
||||
end
|
||||
|
||||
def discard_champ?(champ)
|
||||
@source_champs_to_discard.member?(champ.type_de_champ.order_place)
|
||||
end
|
||||
|
||||
def setup_mapping
|
||||
end
|
||||
|
||||
def champs(dossier)
|
||||
@is_private ? dossier.champs_private : dossier.champs
|
||||
end
|
||||
|
||||
def types_de_champ(procedure)
|
||||
@is_private ? procedure.types_de_champ_private : procedure.types_de_champ
|
||||
end
|
||||
|
||||
def privacy_label
|
||||
@is_private ? 'private ' : ''
|
||||
end
|
||||
|
||||
def check_champs_consistency(label, expected_tdcs, actual_tdcs)
|
||||
if actual_tdcs.size != expected_tdcs.size
|
||||
raise "Incorrect #{label} size #{actual_tdcs.size} (expected #{expected_tdcs.size})"
|
||||
end
|
||||
actual_tdcs.each { |tdc| check_champ_consistency(label, expected_tdcs[tdc.order_place], tdc) }
|
||||
end
|
||||
|
||||
def check_champ_consistency(label, expected_tdc, actual_tdc)
|
||||
errors = []
|
||||
if actual_tdc.libelle != expected_tdc['libelle']
|
||||
errors.append("incorrect libelle #{actual_tdc.libelle} (expected #{expected_tdc['libelle']})")
|
||||
end
|
||||
if actual_tdc.type_champ != expected_tdc['type_champ']
|
||||
errors.append("incorrect type champ #{actual_tdc.type_champ} (expected #{expected_tdc['type_champ']})")
|
||||
end
|
||||
if (!actual_tdc.mandatory) && expected_tdc['mandatory']
|
||||
errors.append("champ should be mandatory")
|
||||
end
|
||||
drop_down = actual_tdc.drop_down_list.presence&.options&.presence
|
||||
if drop_down != expected_tdc['drop_down']
|
||||
errors.append("incorrect drop down list #{drop_down} (expected #{expected_tdc['drop_down']})")
|
||||
end
|
||||
if errors.present?
|
||||
fail "On #{label} type de champ #{actual_tdc.order_place} (#{actual_tdc.libelle}) " + errors.join(', ')
|
||||
end
|
||||
end
|
||||
|
||||
def map_source_to_destination_champ(source_order_place, destination_order_place, source_overrides: {}, destination_overrides: {})
|
||||
destination_type_de_champ = types_de_champ(@destination_procedure).find_by(order_place: destination_order_place)
|
||||
@expected_source_types_de_champ[source_order_place] =
|
||||
type_de_champ_to_expectation(destination_type_de_champ)
|
||||
.merge!(source_overrides)
|
||||
@expected_destination_types_de_champ[destination_order_place] =
|
||||
type_de_champ_to_expectation(types_de_champ(@source_procedure).find_by(order_place: source_order_place))
|
||||
.merge!({ "mandatory" => false }) # Even if the source was mandatory, it’s ok for the destination to be optional
|
||||
.merge!(destination_overrides)
|
||||
@source_to_destination_mapping[source_order_place] = destination_type_de_champ
|
||||
end
|
||||
|
||||
def discard_source_champ(source_type_de_champ)
|
||||
@expected_source_types_de_champ[source_type_de_champ.order_place] = type_de_champ_to_expectation(source_type_de_champ)
|
||||
@source_champs_to_discard << source_type_de_champ.order_place
|
||||
end
|
||||
|
||||
def compute_destination_champ(destination_type_de_champ, &block)
|
||||
@expected_destination_types_de_champ[destination_type_de_champ.order_place] = type_de_champ_to_expectation(destination_type_de_champ)
|
||||
@destination_champ_computations << [types_de_champ(@destination_procedure).find_by(order_place: destination_type_de_champ.order_place), block]
|
||||
end
|
||||
|
||||
def type_de_champ_to_expectation(tdc)
|
||||
if tdc.present?
|
||||
expectation = tdc.as_json(only: [:libelle, :type_champ, :mandatory])
|
||||
expectation['drop_down'] = tdc.drop_down_list.presence&.options&.presence
|
||||
expectation
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PieceJustificativeMapping
|
||||
def initialize(source_procedure, destination_procedure)
|
||||
@source_procedure = source_procedure
|
||||
@destination_procedure = destination_procedure
|
||||
|
||||
@expected_source_pj = {}
|
||||
@expected_destination_pj = {}
|
||||
@source_to_destination_mapping = {}
|
||||
|
||||
setup_mapping
|
||||
end
|
||||
|
||||
def check_source_destination_consistency
|
||||
check_pjs_consistency('source', @expected_source_pj, @source_procedure.types_de_piece_justificative)
|
||||
check_pjs_consistency('destination', @expected_destination_pj, @destination_procedure.types_de_piece_justificative)
|
||||
end
|
||||
|
||||
def can_migrate?(dossier)
|
||||
true
|
||||
end
|
||||
|
||||
def migrate(dossier)
|
||||
# Since we’re going to iterate and change the pjs at the same time,
|
||||
# we use to_a to make the list static and avoid nasty surprises
|
||||
original_pjs = dossier.pieces_justificatives.to_a
|
||||
|
||||
original_pjs.each do |pj|
|
||||
pj_to = destination_pj(pj)
|
||||
if pj_to.present?
|
||||
pj.update_columns(type_de_piece_justificative_id: pj_to.id)
|
||||
elsif discard_pj?(pj)
|
||||
dossier.pieces_justificatives.destroy(pj)
|
||||
else
|
||||
fail "Unhandled source pièce justificative #{c.type_de_piece_justificative.order_place}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def destination_pj(pj)
|
||||
@source_to_destination_mapping[pj.order_place]
|
||||
end
|
||||
|
||||
def discard_pj?(champ)
|
||||
@source_pjs_to_discard.member?(pj.order_place)
|
||||
end
|
||||
|
||||
def setup_mapping
|
||||
end
|
||||
|
||||
def map_source_to_destination_pj(source_order_place, destination_order_place, source_overrides: {}, destination_overrides: {})
|
||||
destination_pj = @destination_procedure.types_de_piece_justificative.find_by(order_place: destination_order_place)
|
||||
@expected_source_pj[source_order_place] =
|
||||
pj_to_expectation(destination_pj)
|
||||
.merge!(source_overrides)
|
||||
@expected_destination_pj[destination_order_place] =
|
||||
pj_to_expectation(@source_procedure.types_de_piece_justificative.find_by(order_place: source_order_place))
|
||||
.merge!({ "mandatory" => false }) # Even if the source was mandatory, it’s ok for the destination to be optional
|
||||
.merge!(destination_overrides)
|
||||
@source_to_destination_mapping[source_order_place] = destination_pj
|
||||
end
|
||||
|
||||
def discard_source_pj(source_pj)
|
||||
@expected_source_pj[source_pj.order_place] = pj_to_expectation(source_pj)
|
||||
@source_pjs_to_discard << source_pj.order_place
|
||||
end
|
||||
|
||||
def leave_destination_pj_blank(destination_pj)
|
||||
@expected_destination_pj[destination_pj.order_place] = pj_to_expectation(destination_pj)
|
||||
end
|
||||
|
||||
def pj_to_expectation(pj)
|
||||
pj&.as_json(only: [:libelle, :mandatory]) || {}
|
||||
end
|
||||
|
||||
def check_pjs_consistency(label, expected_pjs, actual_pjs)
|
||||
if actual_pjs.size != expected_pjs.size
|
||||
raise "Incorrect #{label} pièce justificative count #{actual_pjs.size} (expected #{expected_pjs.size})"
|
||||
end
|
||||
actual_pjs.each { |pj| check_pj_consistency(label, expected_pjs[pj.order_place], pj) }
|
||||
end
|
||||
|
||||
def check_pj_consistency(label, expected_pj, actual_pj)
|
||||
errors = []
|
||||
if actual_pj.libelle != expected_pj['libelle']
|
||||
errors.append("incorrect libelle #{actual_pj.libelle} (expected #{expected_pj['libelle']})")
|
||||
end
|
||||
if (!actual_pj.mandatory) && expected_pj['mandatory']
|
||||
errors.append("pj should be mandatory")
|
||||
end
|
||||
if errors.present?
|
||||
fail "On #{label} type de pièce justificative #{actual_pj.order_place} (#{actual_pj.libelle}) " + errors.join(', ')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(source_procedure, destination_procedure, champ_mapping, private_champ_mapping = ChampMapping, piece_justificative_mapping = PieceJustificativeMapping, &block)
|
||||
@source_procedure = source_procedure
|
||||
@destination_procedure = destination_procedure
|
||||
@champ_mapping = champ_mapping.new(source_procedure, destination_procedure, false)
|
||||
@private_champ_mapping = private_champ_mapping.new(source_procedure, destination_procedure, true)
|
||||
@piece_justificative_mapping = piece_justificative_mapping.new(source_procedure, destination_procedure)
|
||||
|
||||
if block_given?
|
||||
@on_dossier_migration = block
|
||||
end
|
||||
end
|
||||
|
||||
def migrate_procedure
|
||||
check_consistency
|
||||
migrate_dossiers
|
||||
migrate_gestionnaires
|
||||
publish_destination_procedure_in_place_of_source
|
||||
end
|
||||
|
||||
def check_consistency
|
||||
check_same_administrateur
|
||||
@champ_mapping.check_source_destination_consistency
|
||||
@private_champ_mapping.check_source_destination_consistency
|
||||
@piece_justificative_mapping.check_source_destination_consistency
|
||||
end
|
||||
|
||||
def check_same_administrateur
|
||||
if @source_procedure.administrateur_ids.sort != @destination_procedure.administrateur_ids.sort
|
||||
raise "Mismatching administrateurs #{@source_procedure.administrateurs.pluck(:email)} → #{@destination_procedure.administrateurs.pluck(:email)}"
|
||||
end
|
||||
end
|
||||
|
||||
def migrate_dossiers
|
||||
@source_procedure.dossiers.find_each(batch_size: 100) do |d|
|
||||
if @champ_mapping.can_migrate?(d) && @private_champ_mapping.can_migrate?(d) && @piece_justificative_mapping.can_migrate?(d)
|
||||
@champ_mapping.migrate(d)
|
||||
@private_champ_mapping.migrate(d)
|
||||
@piece_justificative_mapping.migrate(d)
|
||||
|
||||
# Use update_columns to avoid triggering build_default_champs
|
||||
d.update_columns(procedure_id: @destination_procedure.id)
|
||||
@on_dossier_migration&.call(d)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def migrate_gestionnaires
|
||||
@source_procedure.gestionnaires.find_each(batch_size: 100) { |g| g.assign_to_procedure(@destination_procedure) }
|
||||
end
|
||||
|
||||
def publish_destination_procedure_in_place_of_source
|
||||
@destination_procedure.publish!(@source_procedure.path)
|
||||
@source_procedure.archive
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,52 +0,0 @@
|
|||
require Rails.root.join("lib", "tasks", "task_helper")
|
||||
|
||||
namespace :pieces_justificatives do
|
||||
desc <<~EOD
|
||||
Migrate the PJ to champs for a single PROCEDURE_ID.
|
||||
EOD
|
||||
task migrate_procedure_to_champs: :environment do
|
||||
procedure_id = ENV['PROCEDURE_ID']
|
||||
procedure = Procedure.find(procedure_id)
|
||||
|
||||
service = PieceJustificativeToChampPieceJointeMigrationService.new
|
||||
service.ensure_correct_storage_configuration!
|
||||
|
||||
progress = ProgressReport.new(service.number_of_champs_to_migrate(procedure))
|
||||
|
||||
service.convert_procedure_pjs_to_champ_pjs(procedure) do
|
||||
progress.inc
|
||||
end
|
||||
|
||||
progress.finish
|
||||
end
|
||||
|
||||
desc <<~EOD
|
||||
Migrate the PJ to champs for several procedures ids, from RANGE_START to RANGE_END.
|
||||
EOD
|
||||
task migrate_procedures_range_to_champs: :environment do
|
||||
if ENV['RANGE_START'].nil? || ENV['RANGE_END'].nil?
|
||||
fail "RANGE_START and RANGE_END must be specified"
|
||||
end
|
||||
procedures_range = ENV['RANGE_START']..ENV['RANGE_END']
|
||||
|
||||
service = PieceJustificativeToChampPieceJointeMigrationService.new
|
||||
service.ensure_correct_storage_configuration!
|
||||
procedures_to_migrate = service.procedures_with_pjs_in_range(procedures_range)
|
||||
|
||||
total_number_of_champs_to_migrate = procedures_to_migrate
|
||||
.map { |p| service.number_of_champs_to_migrate(p) }
|
||||
.sum
|
||||
progress = ProgressReport.new(total_number_of_champs_to_migrate)
|
||||
|
||||
procedures_to_migrate.find_each do |procedure|
|
||||
rake_puts ''
|
||||
rake_puts "Migrating procedure #{procedure.id}…"
|
||||
|
||||
service.convert_procedure_pjs_to_champ_pjs(procedure) do
|
||||
progress.inc
|
||||
end
|
||||
end
|
||||
|
||||
progress.finish
|
||||
end
|
||||
end
|
|
@ -1,170 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Admin::PiecesJustificativesController, type: :controller do
|
||||
let(:admin) { create(:administrateur) }
|
||||
let(:procedure) { create(:procedure, administrateur: admin) }
|
||||
before do
|
||||
sign_in admin
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
let(:procedure_id) { procedure.id }
|
||||
|
||||
subject { get :show, params: { procedure_id: procedure_id } }
|
||||
|
||||
context 'when procedure is not found' do
|
||||
let(:procedure_id) { 9_999_999 }
|
||||
it { expect(subject.status).to eq(404) }
|
||||
end
|
||||
|
||||
context 'when procedure is published' do
|
||||
let(:procedure) { create(:procedure, :published, administrateur: admin) }
|
||||
it { is_expected.to redirect_to admin_procedure_path id: procedure_id }
|
||||
end
|
||||
|
||||
context 'when procedure does not belong to admin' do
|
||||
let(:admin_2) { create(:administrateur) }
|
||||
let(:procedure) { create(:procedure, administrateur: admin_2) }
|
||||
it { expect(subject.status).to eq(404) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let(:procedure_id) { procedure.id }
|
||||
let(:libelle) { 'RIB' }
|
||||
let(:description) { "relevé d'identité bancaire" }
|
||||
let(:update_params) do
|
||||
{
|
||||
types_de_piece_justificative_attributes:
|
||||
{
|
||||
'0' =>
|
||||
{
|
||||
libelle: libelle,
|
||||
description: description
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
let(:request) { put :update, params: { procedure_id: procedure_id, format: :js, procedure: update_params } }
|
||||
subject { request }
|
||||
|
||||
it { is_expected.to render_template('show') }
|
||||
it { expect { subject }.to change(TypeDePieceJustificative, :count).by(1) }
|
||||
it 'adds type de pj to procedure' do
|
||||
request
|
||||
procedure.reload
|
||||
pj = procedure.types_de_piece_justificative.first
|
||||
expect(pj.libelle).to eq(libelle)
|
||||
expect(pj.description).to eq(description)
|
||||
end
|
||||
|
||||
context 'when procedure is not found' do
|
||||
let(:procedure_id) { 9_999_999 }
|
||||
it { expect(subject.status).to eq(404) }
|
||||
end
|
||||
|
||||
context 'when libelle is blank' do
|
||||
let(:libelle) { '' }
|
||||
it { expect { subject }.not_to change(TypeDePieceJustificative, :count) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
let!(:pj) { create(:type_de_piece_justificative, procedure: procedure) }
|
||||
let(:procedure_id) { procedure.id }
|
||||
let(:pj_id) { pj.id }
|
||||
let(:request) { delete :destroy, params: { procedure_id: procedure_id, id: pj_id } }
|
||||
subject { request }
|
||||
context 'when procedure is not found' do
|
||||
let(:procedure_id) { 9_999_999 }
|
||||
it { expect(subject.status).to eq(404) }
|
||||
end
|
||||
context 'when pj id does not exist' do
|
||||
let(:pj_id) { 9_999_999 }
|
||||
it { expect(subject.status).to eq(404) }
|
||||
end
|
||||
context 'when pj id exist but is not linked to procedure' do
|
||||
let(:procedure_1) { create(:procedure, administrateur: admin) }
|
||||
let!(:pj_1) { create(:type_de_piece_justificative, procedure: procedure_1) }
|
||||
let(:pj_id) { pj_1 }
|
||||
it { expect(subject.status).to eq(404) }
|
||||
end
|
||||
context 'when pj is found' do
|
||||
it { expect(subject.status).to eq(200) }
|
||||
it { expect { subject }.to change(TypeDePieceJustificative, :count).by(-1) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #move_up' do
|
||||
subject { post :move_up, params: { procedure_id: procedure.id, index: index, format: :js } }
|
||||
|
||||
context 'when procedure have no type de champ' do
|
||||
let(:index) { 0 }
|
||||
it { expect(subject.status).to eq(400) }
|
||||
end
|
||||
context 'when procedure have only one type de champ' do
|
||||
let(:index) { 1 }
|
||||
let!(:type_de_piece_justificative) { create(:type_de_piece_justificative, procedure: procedure) }
|
||||
it { expect(subject.status).to eq(400) }
|
||||
end
|
||||
context 'when procedure have tow type de champs' do
|
||||
context 'when index == 0' do
|
||||
let(:index) { 0 }
|
||||
let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure) }
|
||||
let!(:type_de_piece_justificative_2) { create(:type_de_piece_justificative, procedure: procedure) }
|
||||
it { expect(subject.status).to eq(400) }
|
||||
end
|
||||
context 'when index > 0' do
|
||||
let(:index) { 1 }
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
|
||||
let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
|
||||
|
||||
it { expect(subject.status).to eq(200) }
|
||||
it { expect(subject).to render_template('show') }
|
||||
it 'changes order places' do
|
||||
post :move_up, params: { procedure_id: procedure.id, index: index, format: :js }
|
||||
type_de_piece_justificative_0.reload
|
||||
type_de_piece_justificative_1.reload
|
||||
expect(type_de_piece_justificative_0.order_place).to eq(1)
|
||||
expect(type_de_piece_justificative_1.order_place).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #move_down' do
|
||||
let(:request) { post :move_down, params: { procedure_id: procedure.id, index: index, format: :js } }
|
||||
let(:index) { 0 }
|
||||
|
||||
subject { request }
|
||||
|
||||
context 'when procedure have no type de champ' do
|
||||
it { expect(subject.status).to eq(400) }
|
||||
end
|
||||
context 'when procedure have only one type de champ' do
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure) }
|
||||
it { expect(subject.status).to eq(400) }
|
||||
end
|
||||
context 'when procedure have 2 type de champ' do
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
|
||||
let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
|
||||
context 'when index represent last type_de_piece_justificative' do
|
||||
let(:index) { 1 }
|
||||
it { expect(subject.status).to eq(400) }
|
||||
end
|
||||
context 'when index does not represent last type_de_piece_justificative' do
|
||||
let(:index) { 0 }
|
||||
it { expect(subject.status).to eq(200) }
|
||||
it { expect(subject).to render_template('show') }
|
||||
it 'changes order place' do
|
||||
request
|
||||
type_de_piece_justificative_0.reload
|
||||
type_de_piece_justificative_1.reload
|
||||
expect(type_de_piece_justificative_0.order_place).to eq(1)
|
||||
expect(type_de_piece_justificative_1.order_place).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -248,7 +248,7 @@ describe Admin::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
let!(:procedure) { create(:procedure, :with_type_de_champ, :with_two_type_de_piece_justificative, administrateur: admin) }
|
||||
let!(:procedure) { create(:procedure, :with_type_de_champ, administrateur: admin) }
|
||||
|
||||
context 'when administrateur is not connected' do
|
||||
before do
|
||||
|
@ -302,7 +302,7 @@ describe Admin::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
context 'when procedure is brouillon' do
|
||||
let(:procedure) { create(:procedure_with_dossiers, :with_path, :with_type_de_champ, :with_two_type_de_piece_justificative, administrateur: admin) }
|
||||
let(:procedure) { create(:procedure_with_dossiers, :with_path, :with_type_de_champ, administrateur: admin) }
|
||||
let!(:dossiers_count) { procedure.dossiers.count }
|
||||
|
||||
describe 'dossiers are dropped' do
|
||||
|
@ -316,7 +316,7 @@ describe Admin::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
context 'when procedure is published' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :with_two_type_de_piece_justificative, :published, administrateur: admin) }
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :published, administrateur: admin) }
|
||||
|
||||
subject { update_procedure }
|
||||
|
||||
|
@ -774,7 +774,7 @@ describe Admin::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
describe 'PATCH #monavis' do
|
||||
let!(:procedure) { create(:procedure, :with_type_de_champ, :with_two_type_de_piece_justificative, administrateur: admin) }
|
||||
let!(:procedure) { create(:procedure, administrateur: admin) }
|
||||
let(:procedure_params) {
|
||||
{
|
||||
monavis_embed: monavis_embed
|
||||
|
@ -835,7 +835,7 @@ describe Admin::ProceduresController, type: :controller do
|
|||
end
|
||||
|
||||
context 'when procedure is published' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :with_two_type_de_piece_justificative, :published, administrateur: admin) }
|
||||
let(:procedure) { create(:procedure, :published, administrateur: admin) }
|
||||
|
||||
subject { update_monavis }
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
|||
describe API::V1::DossiersController do
|
||||
let(:admin) { create(:administrateur) }
|
||||
let(:token) { admin.renew_api_token }
|
||||
let(:procedure) { create(:procedure, :with_two_type_de_piece_justificative, :with_type_de_champ, :with_type_de_champ_private, administrateur: admin) }
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private, administrateur: admin) }
|
||||
let(:wrong_procedure) { create(:procedure) }
|
||||
|
||||
it { expect(described_class).to be < APIController }
|
||||
|
@ -204,49 +204,13 @@ describe API::V1::DossiersController do
|
|||
it { expect(subject.keys).to match_array(field_list) }
|
||||
end
|
||||
|
||||
describe 'types_de_piece_justificative' do
|
||||
let(:field_list) { [:id, :libelle, :description] }
|
||||
subject { super()[:types_de_piece_justificative] }
|
||||
|
||||
it { expect(subject.length).to eq 2 }
|
||||
|
||||
describe 'first type de piece justificative' do
|
||||
subject { super().first }
|
||||
|
||||
it { expect(subject.key?(:id)).to be_truthy }
|
||||
it { expect(subject[:libelle]).to eq('RIB') }
|
||||
it { expect(subject[:description]).to eq('Releve identité bancaire') }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'piece justificative', vcr: { cassette_name: 'controllers_api_v1_dossiers_controller_piece_justificative' } do
|
||||
before do
|
||||
create :piece_justificative, :rib, dossier: dossier, type_de_piece_justificative: dossier.procedure.types_de_piece_justificative.first, user: dossier.user
|
||||
end
|
||||
|
||||
let(:field_list) { [:url, :created_at, :type_de_piece_justificative_id] }
|
||||
subject { super()[:pieces_justificatives].first }
|
||||
|
||||
it { expect(subject.key?(:content_url)).to be_truthy }
|
||||
it { expect(subject[:created_at]).not_to be_nil }
|
||||
it { expect(subject[:type_de_piece_justificative_id]).not_to be_nil }
|
||||
|
||||
it { expect(subject.key?(:user)).to be_truthy }
|
||||
|
||||
describe 'user' do
|
||||
subject { super()[:user] }
|
||||
|
||||
it { expect(subject[:email]).not_to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'champs' do
|
||||
let(:field_list) { [:url] }
|
||||
subject { super()[:champs] }
|
||||
|
||||
it { expect(subject.length).to eq 1 }
|
||||
|
||||
describe 'first champs' do
|
||||
describe 'first champ' do
|
||||
subject { super().first }
|
||||
|
||||
it { expect(subject.key?(:value)).to be_truthy }
|
||||
|
|
|
@ -25,7 +25,7 @@ describe API::V1::ProceduresController, type: :controller do
|
|||
it { is_expected.to have_http_status(200) }
|
||||
|
||||
describe 'body' do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :with_two_type_de_piece_justificative, administrateur: admin) }
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, administrateur: admin) }
|
||||
let(:response) { get :show, params: { id: procedure.id, token: token } }
|
||||
|
||||
subject { JSON.parse(response.body, symbolize_names: true)[:procedure] }
|
||||
|
@ -51,19 +51,6 @@ describe API::V1::ProceduresController, type: :controller do
|
|||
it { expect(subject[:order_place]).to eq(champ.order_place) }
|
||||
it { expect(subject[:description]).to eq(champ.description) }
|
||||
end
|
||||
|
||||
it { is_expected.to have_key(:types_de_piece_justificative) }
|
||||
it { expect(subject[:types_de_piece_justificative]).to be_an(Array) }
|
||||
|
||||
describe 'type_de_piece_jointe' do
|
||||
subject { super()[:types_de_piece_justificative][0] }
|
||||
|
||||
let(:pj) { procedure.types_de_piece_justificative.first }
|
||||
|
||||
it { expect(subject[:id]).to eq(pj.id) }
|
||||
it { expect(subject[:libelle]).to eq(pj.libelle) }
|
||||
it { expect(subject[:description]).to eq(pj.description) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -476,29 +476,16 @@ describe Users::DossiersController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the pj service returns an error' do
|
||||
before do
|
||||
expect(PiecesJustificativesService).to receive(:upload!).and_return(['nop'])
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it { expect(response).to render_template(:brouillon) }
|
||||
it { expect(flash.alert).to eq(['nop']) }
|
||||
end
|
||||
|
||||
context 'when a mandatory champ is missing' do
|
||||
let(:value) { nil }
|
||||
|
||||
before do
|
||||
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
|
||||
allow(PiecesJustificativesService).to receive(:missing_pj_error_messages).and_return(['pj'])
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it { expect(response).to render_template(:brouillon) }
|
||||
it { expect(flash.alert).to eq(['Le champ l doit être rempli.', 'pj']) }
|
||||
it { expect(flash.alert).to eq(['Le champ l doit être rempli.']) }
|
||||
|
||||
context 'and the user saves a draft' do
|
||||
let(:payload) { submit_payload.merge(save_draft: true) }
|
||||
|
@ -511,7 +498,7 @@ describe Users::DossiersController, type: :controller do
|
|||
let!(:dossier) { create(:dossier, :en_construction, user: user) }
|
||||
|
||||
it { expect(response).to render_template(:brouillon) }
|
||||
it { expect(flash.alert).to eq(['Le champ l doit être rempli.', 'pj']) }
|
||||
it { expect(flash.alert).to eq(['Le champ l doit être rempli.']) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -535,8 +522,6 @@ describe Users::DossiersController, type: :controller do
|
|||
|
||||
before do
|
||||
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
|
||||
allow(PiecesJustificativesService).to receive(:missing_pj_error_messages).and_return(['pj'])
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
|
@ -644,29 +629,16 @@ describe Users::DossiersController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the pj service returns an error' do
|
||||
before do
|
||||
expect(PiecesJustificativesService).to receive(:upload!).and_return(['nop'])
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it { expect(response).to render_template(:modifier) }
|
||||
it { expect(flash.alert).to eq(['nop']) }
|
||||
end
|
||||
|
||||
context 'when a mandatory champ is missing' do
|
||||
let(:value) { nil }
|
||||
|
||||
before do
|
||||
first_champ.type_de_champ.update(mandatory: true, libelle: 'l')
|
||||
allow(PiecesJustificativesService).to receive(:missing_pj_error_messages).and_return(['pj'])
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it { expect(response).to render_template(:modifier) }
|
||||
it { expect(flash.alert).to eq(['Le champ l doit être rempli.', 'pj']) }
|
||||
it { expect(flash.alert).to eq(['Le champ l doit être rempli.']) }
|
||||
end
|
||||
|
||||
context 'when dossier has no champ' do
|
||||
|
|
|
@ -6,7 +6,7 @@ FactoryBot.define do
|
|||
|
||||
before(:create) do |dossier, _evaluator|
|
||||
if !dossier.procedure
|
||||
procedure = create(:procedure, :published, :with_two_type_de_piece_justificative, :with_type_de_champ, :with_type_de_champ_private)
|
||||
procedure = create(:procedure, :published, :with_type_de_champ, :with_type_de_champ_private)
|
||||
dossier.procedure = procedure
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
FactoryBot.define do
|
||||
factory :piece_justificative do
|
||||
trait :rib do
|
||||
content { Rack::Test::UploadedFile.new("./spec/fixtures/files/RIB.pdf", 'application/pdf') }
|
||||
end
|
||||
|
||||
trait :contrat do
|
||||
content { Rack::Test::UploadedFile.new("./spec/fixtures/files/Contrat.pdf", 'application/pdf') }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -135,17 +135,6 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
# Deprecated
|
||||
trait :with_two_type_de_piece_justificative do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
rib = create(:type_de_piece_justificative, :rib, order_place: 1)
|
||||
msa = create(:type_de_piece_justificative, :msa, order_place: 2)
|
||||
|
||||
procedure.types_de_piece_justificative << rib
|
||||
procedure.types_de_piece_justificative << msa
|
||||
end
|
||||
end
|
||||
|
||||
trait :published do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.publish!(procedure.administrateurs.first, generate(:published_path), procedure.lien_site_web)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
FactoryBot.define do
|
||||
factory :type_de_piece_justificative do
|
||||
libelle { 'RIB' }
|
||||
description { 'Releve identité bancaire' }
|
||||
|
||||
trait :rib do
|
||||
libelle { 'RIB' }
|
||||
description { 'Releve identité bancaire' }
|
||||
api_entreprise { false }
|
||||
end
|
||||
|
||||
trait :msa do
|
||||
libelle { 'Attestation MSA' }
|
||||
description { 'recuperation automatique' }
|
||||
api_entreprise { true }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -65,13 +65,6 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
|||
procedure.update(service: create(:service))
|
||||
end
|
||||
|
||||
context 'With old PJ' do
|
||||
before do
|
||||
# Create a dummy PJ, because adding PJs is no longer allowed on procedures that
|
||||
# do not already have one
|
||||
Procedure.last.types_de_piece_justificative.create(libelle: "dummy PJ")
|
||||
end
|
||||
|
||||
scenario 'Add champ, add file, visualize them in procedure preview' do
|
||||
page.refresh
|
||||
expect(page).to have_current_path(champs_procedure_path(Procedure.last))
|
||||
|
@ -87,18 +80,11 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
|||
expect(page).to have_selector('#champ-1-libelle')
|
||||
|
||||
click_on Procedure.last.libelle
|
||||
click_on 'onglet-pieces'
|
||||
expect(page).to have_current_path(admin_procedure_pieces_justificatives_path(Procedure.last))
|
||||
fill_in 'procedure_types_de_piece_justificative_attributes_0_libelle', with: 'libelle de piece'
|
||||
click_on 'add_piece_justificative'
|
||||
expect(page).to have_current_path(admin_procedure_pieces_justificatives_path(Procedure.last))
|
||||
expect(page).to have_selector('#procedure_types_de_piece_justificative_attributes_1_libelle')
|
||||
|
||||
preview_window = window_opened_by { click_on 'onglet-preview' }
|
||||
within_window(preview_window) do
|
||||
expect(page).to have_current_path(apercu_procedure_path(Procedure.last))
|
||||
expect(page).to have_field('libelle de champ')
|
||||
expect(page).to have_field('libelle de piece')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -110,14 +96,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
|||
expect(page).to have_content('Formulaire enregistré')
|
||||
|
||||
click_on Procedure.last.libelle
|
||||
click_on 'onglet-pieces'
|
||||
|
||||
expect(page).to have_current_path(admin_procedure_pieces_justificatives_path(Procedure.last))
|
||||
fill_in 'procedure_types_de_piece_justificative_attributes_0_libelle', with: 'libelle de piece'
|
||||
click_on 'add_piece_justificative'
|
||||
|
||||
click_on 'onglet-infos'
|
||||
expect(page).to have_current_path(admin_procedure_path(Procedure.last))
|
||||
|
||||
expect(page).to have_selector('#publish-procedure', visible: true)
|
||||
find('#publish-procedure').click
|
||||
|
||||
|
@ -131,5 +111,4 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
|||
expect(page).to have_selector('.procedure-lien')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ feature 'Creating a new dossier:' do
|
|||
end
|
||||
|
||||
context 'when the procedure has identification by individual' do
|
||||
let(:procedure) { create(:procedure, :published, :for_individual, :with_service, :with_type_de_champ, :with_two_type_de_piece_justificative, ask_birthday: ask_birthday) }
|
||||
let(:procedure) { create(:procedure, :published, :for_individual, :with_service, ask_birthday: ask_birthday) }
|
||||
let(:ask_birthday) { false }
|
||||
let(:expected_birthday) { nil }
|
||||
|
||||
|
@ -63,7 +63,7 @@ feature 'Creating a new dossier:' do
|
|||
end
|
||||
|
||||
context 'when identifying through SIRET' do
|
||||
let(:procedure) { create(:procedure, :published, :with_service, :with_type_de_champ, :with_two_type_de_piece_justificative) }
|
||||
let(:procedure) { create(:procedure, :published, :with_service, :with_type_de_champ) }
|
||||
let(:dossier) { procedure.dossiers.last }
|
||||
|
||||
before do
|
||||
|
|
7833
spec/fixtures/cassettes/model_piece_justificative.yml
vendored
7833
spec/fixtures/cassettes/model_piece_justificative.yml
vendored
File diff suppressed because it is too large
Load diff
|
@ -1,62 +0,0 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AdminFormulaireHelper, type: :helper do
|
||||
let(:procedure) { create(:procedure) }
|
||||
let(:kind) { 'piece_justificative' }
|
||||
let(:url) { 'http://localhost' }
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
|
||||
let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
|
||||
let!(:type_de_piece_justificative_2) { create(:type_de_piece_justificative, procedure: procedure, order_place: 2) }
|
||||
|
||||
describe '#button_up' do
|
||||
describe 'with first piece justificative' do
|
||||
let(:index) { 0 }
|
||||
|
||||
it 'returns a button up' do
|
||||
expect(button_up(procedure, kind, index, url)).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with second out of three piece justificative' do
|
||||
let(:index) { 1 }
|
||||
|
||||
it 'returns a button up' do
|
||||
expect(button_up(procedure, kind, index, url)).to match(/fa-chevron-up/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with last piece justificative' do
|
||||
let(:index) { 2 }
|
||||
|
||||
it 'returns a button up' do
|
||||
expect(button_up(procedure, kind, index, url)).to match(/fa-chevron-up/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#button_down' do
|
||||
describe 'with first piece justificative' do
|
||||
let(:index) { 0 }
|
||||
|
||||
it 'returns a button down' do
|
||||
expect(button_down(procedure, kind, index, url)).to match(/fa-chevron-down/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with second out of three piece justificative' do
|
||||
let(:index) { 1 }
|
||||
|
||||
it 'returns a button down' do
|
||||
expect(button_down(procedure, kind, index, url)).to match(/fa-chevron-down/)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with last piece justificative' do
|
||||
let(:index) { 2 }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(button_down(procedure, kind, index, url)).to be(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +1,7 @@
|
|||
describe ActiveStorage::DownloadableFile do
|
||||
let(:tpjs) { [tpj_not_mandatory] }
|
||||
let!(:tpj_not_mandatory) do
|
||||
TypeDePieceJustificative.create(libelle: 'not mandatory', mandatory: false)
|
||||
end
|
||||
let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) }
|
||||
let(:dossier) { Dossier.create(procedure: procedure) }
|
||||
let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) }
|
||||
let(:dossier) { Dossier.create(procedure: procedure) }
|
||||
let(:list) { ActiveStorage::DownloadableFile.create_list_from_dossier(dossier) }
|
||||
let(:dossier) { create(:dossier) }
|
||||
|
||||
subject(:list) { ActiveStorage::DownloadableFile.create_list_from_dossier(dossier) }
|
||||
|
||||
describe 'create_list_from_dossier' do
|
||||
context 'when no piece_justificative is present' do
|
||||
|
@ -15,9 +9,8 @@ describe ActiveStorage::DownloadableFile do
|
|||
end
|
||||
|
||||
context 'when there is a piece_justificative' do
|
||||
let (:pj) { create(:champ, :piece_justificative, :with_piece_justificative_file) }
|
||||
before do
|
||||
dossier.champs = [pj]
|
||||
dossier.champs << create(:champ, :piece_justificative, :with_piece_justificative_file)
|
||||
end
|
||||
|
||||
it { expect(list.length).to be 1 }
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe FileSizeValidator, lib: true do
|
||||
let(:validator) { FileSizeValidator.new(options) }
|
||||
let(:attachment) { PieceJustificativeUploader.new }
|
||||
let(:note) { create(:piece_justificative, :contrat) }
|
||||
|
||||
describe 'options uses an integer' do
|
||||
let(:options) { { maximum: 10, attributes: { content: attachment } } }
|
||||
|
||||
it 'attachment exceeds maximum limit' do
|
||||
allow(attachment).to receive(:size) { 100 }
|
||||
validator.validate_each(note, :content, attachment)
|
||||
expect(note.errors).to have_key(:content)
|
||||
end
|
||||
|
||||
it 'attachment under maximum limit' do
|
||||
allow(attachment).to receive(:size) { 1 }
|
||||
validator.validate_each(note, :content, attachment)
|
||||
expect(note.errors).not_to have_key(:content)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'options uses a symbol' do
|
||||
let(:options) do
|
||||
{
|
||||
maximum: :test,
|
||||
attributes: { content: attachment }
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(note).to receive(:test) { 10 }
|
||||
end
|
||||
|
||||
it 'attachment exceeds maximum limit' do
|
||||
allow(attachment).to receive(:size) { 100 }
|
||||
validator.validate_each(note, :content, attachment)
|
||||
expect(note.errors).to have_key(:content)
|
||||
end
|
||||
|
||||
it 'attachment under maximum limit' do
|
||||
allow(attachment).to receive(:size) { 1 }
|
||||
validator.validate_each(note, :content, attachment)
|
||||
expect(note.errors).not_to have_key(:content)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,51 +0,0 @@
|
|||
describe 'pieces_justificatives' do
|
||||
describe 'migrate_procedure_to_champs' do
|
||||
let(:rake_task) { Rake::Task['pieces_justificatives:migrate_procedure_to_champs'] }
|
||||
let(:procedure) { create(:procedure, :with_two_type_de_piece_justificative) }
|
||||
|
||||
before do
|
||||
ENV['PROCEDURE_ID'] = procedure.id.to_s
|
||||
|
||||
allow_any_instance_of(PieceJustificativeToChampPieceJointeMigrationService).to receive(:ensure_correct_storage_configuration!)
|
||||
|
||||
rake_task.invoke
|
||||
end
|
||||
|
||||
after { rake_task.reenable }
|
||||
|
||||
it 'migrates the procedure' do
|
||||
expect(procedure.reload.types_de_piece_justificative).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe 'migrate_procedures_range_to_champs' do
|
||||
let(:rake_task) { Rake::Task['pieces_justificatives:migrate_procedures_range_to_champs'] }
|
||||
let(:procedure_in_range_1) { create(:procedure, :with_two_type_de_piece_justificative) }
|
||||
let(:procedure_in_range_2) { create(:procedure, :with_two_type_de_piece_justificative) }
|
||||
let(:procedure_out_of_range) { create(:procedure, :with_two_type_de_piece_justificative) }
|
||||
|
||||
before do
|
||||
procedure_in_range_1
|
||||
procedure_in_range_2
|
||||
procedure_out_of_range
|
||||
|
||||
ENV['RANGE_START'] = procedure_in_range_1.id.to_s
|
||||
ENV['RANGE_END'] = procedure_in_range_2.id.to_s
|
||||
|
||||
allow_any_instance_of(PieceJustificativeToChampPieceJointeMigrationService).to receive(:ensure_correct_storage_configuration!)
|
||||
|
||||
rake_task.invoke
|
||||
end
|
||||
|
||||
after { rake_task.reenable }
|
||||
|
||||
it 'migrates procedures in the ids range' do
|
||||
expect(procedure_in_range_1.reload.types_de_piece_justificative).to be_empty
|
||||
expect(procedure_in_range_2.reload.types_de_piece_justificative).to be_empty
|
||||
end
|
||||
|
||||
it 'doesn’t migrate procedures not in the range' do
|
||||
expect(procedure_out_of_range.reload.types_de_piece_justificative).to be_present
|
||||
end
|
||||
end
|
||||
end
|
|
@ -91,50 +91,6 @@ describe Dossier do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#types_de_piece_justificative' do
|
||||
subject { dossier.types_de_piece_justificative }
|
||||
it 'returns list of required piece justificative' do
|
||||
expect(subject.size).to eq(2)
|
||||
expect(subject).to include(TypeDePieceJustificative.find(TypeDePieceJustificative.first.id))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#types_de_piece_justificative' do
|
||||
subject { dossier.types_de_piece_justificative }
|
||||
it 'returns list of required piece justificative' do
|
||||
expect(subject.size).to eq(2)
|
||||
expect(subject).to include(TypeDePieceJustificative.find(TypeDePieceJustificative.first.id))
|
||||
end
|
||||
end
|
||||
|
||||
describe '#retrieve_last_piece_justificative_by_type', vcr: { cassette_name: 'models_dossier_retrieve_last_piece_justificative_by_type' } do
|
||||
let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative }
|
||||
|
||||
subject { dossier.retrieve_last_piece_justificative_by_type types_de_pj_dossier.first }
|
||||
|
||||
before do
|
||||
create :piece_justificative, :rib, dossier: dossier, type_de_piece_justificative: types_de_pj_dossier.first
|
||||
end
|
||||
|
||||
it 'returns piece justificative with given type' do
|
||||
expect(subject.type).to eq(types_de_pj_dossier.first.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#retrieve_all_piece_justificative_by_type' do
|
||||
let(:types_de_pj_dossier) { dossier.procedure.types_de_piece_justificative }
|
||||
|
||||
subject { dossier.retrieve_all_piece_justificative_by_type types_de_pj_dossier.first }
|
||||
|
||||
before do
|
||||
create :piece_justificative, :rib, dossier: dossier, type_de_piece_justificative: types_de_pj_dossier.first
|
||||
end
|
||||
|
||||
it 'returns a list of the piece justificative' do
|
||||
expect(subject).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_default_champs' do
|
||||
context 'when dossier is linked to a procedure with type_de_champ_public and private' do
|
||||
let(:dossier) { create(:dossier, user: user) }
|
||||
|
@ -581,12 +537,6 @@ describe Dossier do
|
|||
|
||||
it { is_expected.not_to eq(modif_date) }
|
||||
|
||||
context 'when a piece justificative is modified' do
|
||||
before { dossier.pieces_justificatives << create(:piece_justificative, :contrat) }
|
||||
|
||||
it { is_expected.to eq(modif_date) }
|
||||
end
|
||||
|
||||
context 'when a champ is modified' do
|
||||
before { dossier.champs.first.update_attribute('value', 'yop') }
|
||||
|
||||
|
|
|
@ -251,12 +251,6 @@ describe Gestionnaire, type: :model do
|
|||
it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) }
|
||||
end
|
||||
|
||||
context 'when there is a modification on a piece jusitificative' do
|
||||
before { dossier.pieces_justificatives << create(:piece_justificative, :contrat) }
|
||||
|
||||
it { is_expected.to match({ demande: true, annotations_privees: false, avis: false, messagerie: false }) }
|
||||
end
|
||||
|
||||
context 'when there is a modification on private champs' do
|
||||
before { dossier.champs_private.first.update_attribute('value', 'toto') }
|
||||
|
||||
|
@ -326,12 +320,6 @@ describe Gestionnaire, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when there is a modification on a piece justificative' do
|
||||
before { dossier.pieces_justificatives << create(:piece_justificative, :contrat) }
|
||||
|
||||
it { is_expected.to match([dossier.id]) }
|
||||
end
|
||||
|
||||
context 'when there is a modification on public champs on a followed dossier from another procedure' do
|
||||
before { dossier_on_procedure_2.champs.first.update_attribute('value', 'toto') }
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PieceJustificative do
|
||||
describe 'validations' do
|
||||
context 'content' do
|
||||
it { is_expected.not_to allow_value(nil).for(:content) }
|
||||
it { is_expected.not_to allow_value('').for(:content) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#empty?', vcr: { cassette_name: 'model_piece_justificative' } do
|
||||
let(:piece_justificative) { create(:piece_justificative, content: content) }
|
||||
subject { piece_justificative.empty? }
|
||||
|
||||
context 'when content exist' do
|
||||
let(:content) { File.open('./spec/fixtures/files/piece_justificative_388.pdf') }
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -381,8 +381,6 @@ describe Procedure do
|
|||
let!(:type_de_champ_private_0) { create(:type_de_champ, :private, procedure: procedure, order_place: 0) }
|
||||
let!(:type_de_champ_private_1) { create(:type_de_champ, :private, procedure: procedure, order_place: 1) }
|
||||
let!(:type_de_champ_private_2) { create(:type_de_champ_drop_down_list, :private, procedure: procedure, order_place: 2) }
|
||||
let!(:piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
|
||||
let!(:piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
|
||||
let(:received_mail) { create(:received_mail) }
|
||||
let(:from_library) { false }
|
||||
let(:administrateur) { procedure.administrateurs.first }
|
||||
|
@ -408,7 +406,7 @@ describe Procedure do
|
|||
it 'should duplicate specific objects with different id' do
|
||||
expect(subject.id).not_to eq(procedure.id)
|
||||
|
||||
expect(subject.types_de_champ.size).to eq(procedure.types_de_champ.size + 1 + procedure.types_de_piece_justificative.size)
|
||||
expect(subject.types_de_champ.size).to eq(procedure.types_de_champ.size)
|
||||
expect(subject.types_de_champ_private.size).to eq procedure.types_de_champ_private.size
|
||||
expect(subject.types_de_champ.map(&:drop_down_list).compact.size).to eq procedure.types_de_champ.map(&:drop_down_list).compact.size
|
||||
expect(subject.types_de_champ_private.map(&:drop_down_list).compact.size).to eq procedure.types_de_champ_private.map(&:drop_down_list).compact.size
|
||||
|
@ -430,18 +428,6 @@ describe Procedure do
|
|||
expect(cloned_procedure).to have_same_attributes_as(procedure, except: ["path"])
|
||||
end
|
||||
|
||||
it 'should not clone piece justificatives but create corresponding champs' do
|
||||
expect(subject.types_de_piece_justificative.size).to eq(0)
|
||||
|
||||
champs_pj = subject.types_de_champ[procedure.types_de_champ.size + 1, procedure.types_de_piece_justificative.size]
|
||||
champs_pj.zip(procedure.types_de_piece_justificative).each do |stc, ptpj|
|
||||
expect(stc.libelle).to eq(ptpj.libelle)
|
||||
expect(stc.description).to eq(ptpj.description)
|
||||
expect(stc.mandatory).to eq(ptpj.mandatory)
|
||||
expect(stc.old_pj[:stable_id]).to eq(ptpj.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the procedure is cloned from the library' do
|
||||
let(:from_library) { true }
|
||||
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe TypeDePieceJustificative do
|
||||
let!(:procedure) { create(:procedure) }
|
||||
|
||||
describe 'validation' do
|
||||
context 'libelle' do
|
||||
it { is_expected.not_to allow_value(nil).for(:libelle) }
|
||||
it { is_expected.not_to allow_value('').for(:libelle) }
|
||||
it { is_expected.to allow_value('RIB').for(:libelle) }
|
||||
end
|
||||
|
||||
context 'order_place' do
|
||||
# it { is_expected.not_to allow_value(nil).for(:order_place) }
|
||||
# it { is_expected.not_to allow_value('').for(:order_place) }
|
||||
it { is_expected.to allow_value(1).for(:order_place) }
|
||||
end
|
||||
|
||||
context 'lien_demarche' do
|
||||
it { is_expected.to allow_value(nil).for(:lien_demarche) }
|
||||
it { is_expected.to allow_value('').for(:lien_demarche) }
|
||||
it { is_expected.not_to allow_value('not-a-link').for(:lien_demarche) }
|
||||
it { is_expected.to allow_value('http://link').for(:lien_demarche) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -46,26 +46,17 @@ describe DossierSerializer do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a type PJ was cloned to a type champ PJ' do
|
||||
let(:original_procedure) do
|
||||
p = create(:procedure, :published)
|
||||
p.types_de_piece_justificative.create(
|
||||
context 'when a type de champ PJ was cloned from a legacy PJ' do
|
||||
let(:original_pj_id) { 3 }
|
||||
let(:cloned_type_de_champ) do
|
||||
tdc = create(:type_de_champ_piece_justificative,
|
||||
libelle: "Vidéo de votre demande de subvention",
|
||||
description: "Pour optimiser vos chances, soignez la chorégraphie et privilégiez le chant polyphonique",
|
||||
lien_demarche: "https://www.dance-academy.gouv.fr",
|
||||
order_place: 0
|
||||
)
|
||||
p
|
||||
description: "Pour optimiser vos chances, soignez la chorégraphie et privilégiez le chant polyphonique.\r\nRécupérer le formulaire vierge pour mon dossier : https://www.dance-academy.gouv.fr",
|
||||
order_place: 0)
|
||||
tdc.old_pj = { stable_id: original_pj_id }
|
||||
tdc
|
||||
end
|
||||
|
||||
let(:procedure) do
|
||||
p = original_procedure.clone(original_procedure.administrateurs.first, false)
|
||||
p.save
|
||||
p
|
||||
end
|
||||
|
||||
let(:type_pj) { original_procedure.types_de_piece_justificative.first }
|
||||
let(:migrated_type_champ) { procedure.types_de_champ.find_by(libelle: type_pj.libelle) }
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ: [cloned_type_de_champ]) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:champ_pj) { dossier.champs.last }
|
||||
|
||||
|
@ -79,18 +70,18 @@ describe DossierSerializer do
|
|||
is_expected.to include(
|
||||
types_de_piece_justificative: [
|
||||
{
|
||||
"id" => type_pj.id,
|
||||
"libelle" => type_pj.libelle,
|
||||
"description" => type_pj.description,
|
||||
"lien_demarche" => type_pj.lien_demarche,
|
||||
"order_place" => type_pj.order_place
|
||||
"id" => original_pj_id,
|
||||
"libelle" => cloned_type_de_champ.libelle,
|
||||
"description" => 'Pour optimiser vos chances, soignez la chorégraphie et privilégiez le chant polyphonique.',
|
||||
"lien_demarche" => 'https://www.dance-academy.gouv.fr',
|
||||
"order_place" => cloned_type_de_champ.order_place
|
||||
}
|
||||
],
|
||||
pieces_justificatives: [
|
||||
{
|
||||
"content_url" => champ_pj.for_api,
|
||||
"created_at" => champ_pj.created_at.in_time_zone('UTC').iso8601(3),
|
||||
"type_de_piece_justificative_id" => type_pj.id,
|
||||
"type_de_piece_justificative_id" => original_pj_id,
|
||||
"user" => a_hash_including("id" => dossier.user.id)
|
||||
}
|
||||
]
|
||||
|
@ -98,7 +89,7 @@ describe DossierSerializer do
|
|||
end
|
||||
|
||||
it "does not expose the PJ as a champ" do
|
||||
expect(subject[:champs]).not_to include(a_hash_including(type_de_champ: a_hash_including(id: migrated_type_champ.id)))
|
||||
expect(subject[:champs]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,21 +10,16 @@ describe ProcedureSerializer do
|
|||
end
|
||||
|
||||
context 'when a type PJ was cloned to a type champ PJ' do
|
||||
let(:original_procedure) do
|
||||
p = create(:procedure, :published)
|
||||
p.types_de_piece_justificative.create(
|
||||
let(:original_pj_id) { 3 }
|
||||
let(:cloned_type_de_champ) do
|
||||
tdc = create(:type_de_champ_piece_justificative,
|
||||
libelle: "Vidéo de votre demande de subvention",
|
||||
description: "Pour optimiser vos chances, soignez la chorégraphie et privilégiez le chant polyphonique",
|
||||
lien_demarche: "https://www.dance-academy.gouv.fr",
|
||||
order_place: 0
|
||||
)
|
||||
p
|
||||
description: "Pour optimiser vos chances, soignez la chorégraphie et privilégiez le chant polyphonique.\r\nRécupérer le formulaire vierge pour mon dossier : https://www.dance-academy.gouv.fr",
|
||||
order_place: 0)
|
||||
tdc.old_pj = { stable_id: original_pj_id }
|
||||
tdc
|
||||
end
|
||||
|
||||
let(:procedure) { original_procedure.clone(original_procedure.administrateurs.first, false) }
|
||||
|
||||
let(:type_pj) { original_procedure.types_de_piece_justificative.first }
|
||||
let(:migrated_type_champ) { procedure.types_de_champ.find_by(libelle: type_pj.libelle) }
|
||||
let(:procedure) { create(:procedure, :published, types_de_champ: [cloned_type_de_champ]) }
|
||||
|
||||
subject { ProcedureSerializer.new(procedure).serializable_hash }
|
||||
|
||||
|
@ -32,18 +27,18 @@ describe ProcedureSerializer do
|
|||
is_expected.to include(
|
||||
types_de_piece_justificative: [
|
||||
{
|
||||
"id" => type_pj.id,
|
||||
"libelle" => type_pj.libelle,
|
||||
"description" => type_pj.description,
|
||||
"lien_demarche" => type_pj.lien_demarche,
|
||||
"order_place" => type_pj.order_place
|
||||
"id" => original_pj_id,
|
||||
"libelle" => cloned_type_de_champ.libelle,
|
||||
"description" => 'Pour optimiser vos chances, soignez la chorégraphie et privilégiez le chant polyphonique.',
|
||||
"lien_demarche" => 'https://www.dance-academy.gouv.fr',
|
||||
"order_place" => cloned_type_de_champ.order_place
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
it "is not exposed as a type de champ" do
|
||||
expect(subject[:types_de_champ]).not_to include(a_hash_including(libelle: type_pj.libelle))
|
||||
expect(subject[:types_de_champ]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CarrierwaveActiveStorageMigrationService do
|
||||
let(:service) { CarrierwaveActiveStorageMigrationService.new }
|
||||
|
||||
describe '#hex_to_base64' do
|
||||
it { expect(service.hex_to_base64('deadbeef')).to eq('3q2+7w==') }
|
||||
end
|
||||
|
||||
describe '.make_blob' do
|
||||
let(:pj) { create(:piece_justificative, :rib, updated_at: Time.zone.local(2019, 01, 01, 12, 00)) }
|
||||
let(:identify) { false }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:checksum).and_return('cafe')
|
||||
end
|
||||
|
||||
subject(:blob) { service.make_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename, identify: identify) }
|
||||
|
||||
it { expect(blob.created_at).to eq pj.updated_at }
|
||||
|
||||
it 'marks the blob as already scanned by the antivirus' do
|
||||
expect(blob.metadata[:virus_scan_result]).to eq(ActiveStorage::VirusScanner::SAFE)
|
||||
end
|
||||
|
||||
it 'sets the blob MIME type from the file' do
|
||||
expect(blob.identified).to be true
|
||||
expect(blob.content_type).to eq 'application/pdf'
|
||||
end
|
||||
|
||||
context 'when asking for explicit MIME type identification' do
|
||||
let(:identify) { true }
|
||||
|
||||
it 'marks the file as needing MIME type detection' do
|
||||
expect(blob.identified).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.make_empty_blob' do
|
||||
let(:pj) { create(:piece_justificative, :rib, updated_at: Time.zone.local(2019, 01, 01, 12, 00)) }
|
||||
|
||||
before 'set the underlying stored file as missing' do
|
||||
allow(pj.content.file).to receive(:file).and_return(nil)
|
||||
end
|
||||
|
||||
subject(:blob) { service.make_empty_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename) }
|
||||
|
||||
it { expect(blob.created_at).to eq pj.updated_at }
|
||||
|
||||
it 'marks the blob as already scanned by the antivirus' do
|
||||
expect(blob.metadata[:virus_scan_result]).to eq(ActiveStorage::VirusScanner::SAFE)
|
||||
end
|
||||
|
||||
it 'sets the blob MIME type from the file' do
|
||||
expect(blob.identified).to be true
|
||||
expect(blob.content_type).to eq 'application/pdf'
|
||||
end
|
||||
|
||||
context 'when the file metadata are also missing' do
|
||||
before do
|
||||
allow(pj).to receive(:original_filename).and_return(nil)
|
||||
allow(pj.content).to receive(:content_type).and_return(nil)
|
||||
end
|
||||
|
||||
it 'fallbacks on default values' do
|
||||
expect(blob.filename).to eq pj.content.filename
|
||||
expect(blob.content_type).to eq 'text/plain'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fix_content_type' do
|
||||
let(:pj) { create(:piece_justificative, :rib, updated_at: Time.zone.local(2019, 01, 01, 12, 00)) }
|
||||
let(:blob) { service.make_empty_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename) }
|
||||
|
||||
context 'when the request is ok' do
|
||||
it 'succeeds' do
|
||||
expect(blob.service).to receive(:change_content_type).and_return(true)
|
||||
expect { service.fix_content_type(blob) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the request fails initially' do
|
||||
it 'retries the request' do
|
||||
expect(blob.service).to receive(:change_content_type).and_raise(StandardError).ordered
|
||||
expect(blob.service).to receive(:change_content_type).and_return(true).ordered
|
||||
expect { service.fix_content_type(blob, retry_delay: 0.01) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the request fails too many times' do
|
||||
it 'gives up' do
|
||||
expect(blob.service).to receive(:change_content_type).and_raise(StandardError).thrice
|
||||
expect { service.fix_content_type(blob, retry_delay: 0.01) }.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,317 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PieceJustificativeToChampPieceJointeMigrationService do
|
||||
let(:service) { PieceJustificativeToChampPieceJointeMigrationService.new(storage_service: storage_service) }
|
||||
let(:storage_service) { CarrierwaveActiveStorageMigrationService.new }
|
||||
let(:pj_uploader) { class_double(PieceJustificativeUploader) }
|
||||
let(:pj_service) { class_double(PiecesJustificativesService) }
|
||||
|
||||
let(:procedure) { create(:procedure, types_de_piece_justificative: types_pj) }
|
||||
let(:types_pj) { [create(:type_de_piece_justificative)] }
|
||||
|
||||
let!(:dossier) { make_dossier }
|
||||
|
||||
let(:pjs) { [] }
|
||||
|
||||
def make_dossier(hidden: false)
|
||||
create(:dossier,
|
||||
procedure: procedure,
|
||||
pieces_justificatives: pjs,
|
||||
hidden_at: hidden ? Time.zone.now : nil)
|
||||
end
|
||||
|
||||
def make_pjs
|
||||
types_pj.map do |tpj|
|
||||
create(:piece_justificative, :contrat, type_de_piece_justificative: tpj)
|
||||
end
|
||||
end
|
||||
|
||||
def timestamps(dossier)
|
||||
# Reload dossier because the resolution of in-database timestamps is
|
||||
# different from the resolution of in-memory timestamps, causing the
|
||||
# tests to fail on fractional time differences.
|
||||
dossier.reload
|
||||
|
||||
{
|
||||
created_at: dossier.created_at,
|
||||
updated_at: dossier.updated_at
|
||||
}
|
||||
end
|
||||
|
||||
def expect_storage_service_to_convert_object
|
||||
expect(storage_service).to receive(:make_blob)
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||
expect(storage_service).to receive(:make_attachment)
|
||||
end
|
||||
|
||||
describe '.number_of_champs_to_migrate' do
|
||||
let!(:other_dossier) { make_dossier }
|
||||
|
||||
it 'reports the numbers of champs to be migrated' do
|
||||
expect(service.number_of_champs_to_migrate(procedure)).to eq(4)
|
||||
end
|
||||
|
||||
context 'when the procedure has hidden dossiers' do
|
||||
let!(:hidden_dossier) { make_dossier(hidden: true) }
|
||||
|
||||
it 'reports the numbers of champs including those of hidden dossiers' do
|
||||
expect(service.number_of_champs_to_migrate(procedure)).to eq(6)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversion succeeds' do
|
||||
context 'for the procedure' do
|
||||
it 'types de champ are created for the "pièces jointes" header and for each PJ' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { procedure.types_de_champ.count }
|
||||
.by(types_pj.count + 1)
|
||||
end
|
||||
|
||||
it 'the old types de pj are removed' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { procedure.types_de_piece_justificative.count }
|
||||
.to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'no notifications are sent to instructeurs' do
|
||||
let!(:initial_dossier_timestamps) { timestamps(dossier) }
|
||||
|
||||
context 'when there is a PJ' do
|
||||
let(:pjs) { make_pjs }
|
||||
|
||||
before do
|
||||
# Reload PJ because the resolution of in-database timestamps is
|
||||
# different from the resolution of in-memory timestamps, causing the
|
||||
# tests to fail on fractional time differences.
|
||||
pjs.last.reload
|
||||
|
||||
expect_storage_service_to_convert_object
|
||||
Timecop.travel(1.hour) { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
|
||||
# Reload the dossier to see the newly created champs
|
||||
dossier.reload
|
||||
end
|
||||
|
||||
it 'the champ has the same timestamps as the PJ' do
|
||||
expect(dossier.champs.last.created_at).to eq(pjs.last.created_at)
|
||||
expect(dossier.champs.last.updated_at).to eq(pjs.last.updated_at)
|
||||
end
|
||||
|
||||
it 'does not change the dossier timestamps' do
|
||||
expect(dossier.created_at).to eq(initial_dossier_timestamps[:created_at])
|
||||
expect(dossier.updated_at).to eq(initial_dossier_timestamps[:updated_at])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is no PJ' do
|
||||
before do
|
||||
Timecop.travel(1.hour) { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
|
||||
# Reload the dossier to see the newly created champs
|
||||
dossier.reload
|
||||
end
|
||||
|
||||
it 'the champ doesn’t trigger a notification' do
|
||||
expect(dossier.champs.last.created_at).to eq(initial_dossier_timestamps[:created_at])
|
||||
expect(dossier.champs.last.updated_at).to eq(initial_dossier_timestamps[:created_at])
|
||||
end
|
||||
|
||||
it 'does not change the dossier timestamps' do
|
||||
expect(dossier.created_at).to eq(initial_dossier_timestamps[:created_at])
|
||||
expect(dossier.updated_at).to eq(initial_dossier_timestamps[:updated_at])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for the dossier' do
|
||||
let(:pjs) { make_pjs }
|
||||
|
||||
before { expect_storage_service_to_convert_object }
|
||||
|
||||
it 'champs are created for the "pièces jointes" header and for each PJ' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.champs.count }
|
||||
.by(types_pj.count + 1)
|
||||
end
|
||||
|
||||
it 'the old pjs are removed' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.pieces_justificatives.count }
|
||||
.to(0)
|
||||
end
|
||||
|
||||
context 'when the procedure has several dossiers' do
|
||||
let!(:other_dossier) { make_dossier }
|
||||
|
||||
it 'sends progress callback for each migrated champ' do
|
||||
number_of_champs_to_migrate = service.number_of_champs_to_migrate(procedure)
|
||||
|
||||
progress_count = 0
|
||||
service.convert_procedure_pjs_to_champ_pjs(procedure) do
|
||||
progress_count += 1
|
||||
end
|
||||
|
||||
expect(progress_count).to eq(number_of_champs_to_migrate)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dossier is soft-deleted it still gets converted' do
|
||||
let(:pjs) { make_pjs }
|
||||
let!(:dossier) { make_dossier(hidden: true) }
|
||||
|
||||
before { expect_storage_service_to_convert_object }
|
||||
|
||||
it 'champs are created for the "pièces jointes" header and for each PJ' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.champs.count }
|
||||
.by(types_pj.count + 1)
|
||||
end
|
||||
|
||||
it 'the old pjs are removed' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to change { dossier.pieces_justificatives.count }
|
||||
.to(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are several pjs for one type' do
|
||||
let(:pjs) { make_pjs + make_pjs }
|
||||
|
||||
it 'only converts the most recent PJ for each type PJ' do
|
||||
expect(storage_service).to receive(:make_blob).exactly(types_pj.count)
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!).exactly(types_pj.count)
|
||||
expect(storage_service).to receive(:make_attachment).exactly(types_pj.count)
|
||||
|
||||
service.convert_procedure_pjs_to_champ_pjs(procedure)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'cleanup when conversion fails' do
|
||||
let(:pjs) { make_pjs }
|
||||
let(:exception) { 'LOL no!' }
|
||||
|
||||
let!(:failing_dossier) do
|
||||
create(
|
||||
:dossier,
|
||||
procedure: procedure,
|
||||
pieces_justificatives: make_pjs
|
||||
)
|
||||
end
|
||||
|
||||
let!(:initial_dossier_timestamps) { timestamps(dossier) }
|
||||
let!(:initial_failing_dossier_timestamps) { timestamps(failing_dossier) }
|
||||
|
||||
before do
|
||||
allow(storage_service).to receive(:checksum).and_return('cafe')
|
||||
allow(storage_service).to receive(:fix_content_type)
|
||||
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||
.and_raise(exception)
|
||||
|
||||
expect(storage_service).to receive(:delete_from_active_storage!)
|
||||
end
|
||||
|
||||
def try_convert(procedure)
|
||||
Timecop.travel(1.hour) { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
rescue StandardError, SignalException => e
|
||||
dossier.reload
|
||||
failing_dossier.reload
|
||||
e
|
||||
end
|
||||
|
||||
it 'passes on the exception' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||
.to raise_error('LOL no!')
|
||||
end
|
||||
|
||||
it 'does not create champs' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { dossier.champs.count }
|
||||
end
|
||||
|
||||
it 'does not remove any old pjs' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { dossier.pieces_justificatives.count }
|
||||
end
|
||||
|
||||
it 'does not creates types de champ' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { procedure.types_de_champ.count }
|
||||
end
|
||||
|
||||
it 'does not remove old types de pj' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { procedure.types_de_piece_justificative.count }
|
||||
end
|
||||
|
||||
it 'does not change the dossiers timestamps' do
|
||||
try_convert(procedure)
|
||||
expect(dossier.updated_at).to eq(initial_dossier_timestamps[:updated_at])
|
||||
expect(failing_dossier.updated_at).to eq(initial_failing_dossier_timestamps[:updated_at])
|
||||
end
|
||||
|
||||
it 'does not leave stale blobs behind' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { ActiveStorage::Blob.count }
|
||||
end
|
||||
|
||||
it 'does not leave stale attachments behind' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { ActiveStorage::Attachment.count }
|
||||
end
|
||||
|
||||
context 'when some dossiers to roll back are hidden' do
|
||||
before do
|
||||
dossier.update_column(:hidden_at, Time.zone.now)
|
||||
end
|
||||
|
||||
it 'does not create champs' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { dossier.champs.count }
|
||||
end
|
||||
|
||||
it 'does not change the hidden dossier timestamps' do
|
||||
try_convert(procedure)
|
||||
expect(dossier.updated_at).to eq(initial_dossier_timestamps[:updated_at])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when receiving a Signal interruption (like Ctrl+C)' do
|
||||
let(:exception) { Interrupt }
|
||||
|
||||
it 'handles the exception as well' do
|
||||
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }.to raise_error { Interrupt }
|
||||
end
|
||||
|
||||
it 'does not create champs' do
|
||||
expect { try_convert(procedure) }.not_to change { dossier.champs.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when rolling back a dossier fails' do
|
||||
before do
|
||||
allow(service).to receive(:destroy_champ_pj)
|
||||
.with(having_attributes(id: dossier.id), anything)
|
||||
.and_raise(StandardError)
|
||||
allow(service).to receive(:destroy_champ_pj)
|
||||
.with(any_args)
|
||||
.and_call_original
|
||||
end
|
||||
|
||||
it 'continues to roll back the other dossiers' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { failing_dossier.champs.count }
|
||||
end
|
||||
|
||||
it 'does not creates types de champ on the procedure' do
|
||||
expect { try_convert(procedure) }
|
||||
.not_to change { procedure.types_de_champ.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,159 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PiecesJustificativesService do
|
||||
let(:user) { create(:user) }
|
||||
let(:safe_file) { true }
|
||||
|
||||
before :each do
|
||||
allow(ClamavService).to receive(:safe_file?).and_return(safe_file)
|
||||
end
|
||||
|
||||
let(:hash) { {} }
|
||||
let!(:tpj_not_mandatory) do
|
||||
TypeDePieceJustificative.create(libelle: 'not mandatory', mandatory: false)
|
||||
end
|
||||
let!(:tpj_mandatory) do
|
||||
TypeDePieceJustificative.create(libelle: 'justificatif', mandatory: true)
|
||||
end
|
||||
let(:procedure) { Procedure.create(types_de_piece_justificative: tpjs) }
|
||||
let(:dossier) { Dossier.create(procedure: procedure) }
|
||||
let(:errors) { PiecesJustificativesService.upload!(dossier, user, hash) }
|
||||
let(:tpjs) { [tpj_not_mandatory] }
|
||||
|
||||
let(:attachment_list) { PiecesJustificativesService.liste_pieces_justificatives(dossier) }
|
||||
let(:poids_total) { PiecesJustificativesService.pieces_justificatives_total_size(dossier) }
|
||||
|
||||
describe 'self.upload!' do
|
||||
context 'when no params are given' do
|
||||
it { expect(errors).to eq([]) }
|
||||
end
|
||||
|
||||
context 'when there is something wrong with file save' do
|
||||
let(:hash) do
|
||||
{
|
||||
"piece_justificative_#{tpj_not_mandatory.id}" =>
|
||||
double(path: '', original_filename: 'filename')
|
||||
}
|
||||
end
|
||||
|
||||
it { expect(errors).to match(["le fichier filename (not mandatory) n'a pas pu être sauvegardé"]) }
|
||||
end
|
||||
|
||||
context 'when a virus is provided' do
|
||||
let(:safe_file) { false }
|
||||
let(:hash) do
|
||||
{
|
||||
"piece_justificative_#{tpj_not_mandatory.id}" =>
|
||||
double(path: '', original_filename: 'bad_file')
|
||||
}
|
||||
end
|
||||
|
||||
it { expect(errors).to match(['bad_file : virus détecté']) }
|
||||
end
|
||||
|
||||
context 'when a regular file is provided' do
|
||||
let(:content) { double(path: '', original_filename: 'filename') }
|
||||
let(:hash) do
|
||||
{
|
||||
"piece_justificative_#{tpj_not_mandatory.id}" =>
|
||||
content
|
||||
}
|
||||
end
|
||||
|
||||
before :each do
|
||||
expect(PiecesJustificativesService).to receive(:save_pj)
|
||||
.with(content, dossier, tpj_not_mandatory, user)
|
||||
.and_return(nil)
|
||||
end
|
||||
|
||||
it 'is saved' do
|
||||
expect(errors).to match([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'missing_pj_error_messages' do
|
||||
let(:errors) { PiecesJustificativesService.missing_pj_error_messages(dossier) }
|
||||
let(:tpjs) { [tpj_mandatory] }
|
||||
|
||||
context 'when no params are given' do
|
||||
it { expect(errors).to match(['La pièce jointe justificatif doit être fournie.']) }
|
||||
end
|
||||
|
||||
context 'when the piece justificative is provided' do
|
||||
before :each do
|
||||
# we are messing around piece_justificative
|
||||
# because directly doubling carrierwave params seems complicated
|
||||
piece_justificative_double = double(type_de_piece_justificative: tpj_mandatory)
|
||||
expect(dossier).to receive(:pieces_justificatives).and_return([piece_justificative_double])
|
||||
end
|
||||
|
||||
it {
|
||||
expect(errors).to match([])
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe '#attachment_list' do
|
||||
context 'when no piece_justificative is present' do
|
||||
it { expect(attachment_list).to match([]) }
|
||||
it { expect(poids_total).to be 0 }
|
||||
end
|
||||
|
||||
context 'when there is a piece_justificative' do
|
||||
let (:pj) { create(:champ, :piece_justificative, :with_piece_justificative_file) }
|
||||
before do
|
||||
dossier.champs = [pj]
|
||||
end
|
||||
|
||||
it { expect(attachment_list).not_to be_empty }
|
||||
it { expect(poids_total).to be pj.piece_justificative_file.byte_size }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'types_pj_as_types_de_champ' do
|
||||
subject { PiecesJustificativesService.types_pj_as_types_de_champ(procedure) }
|
||||
|
||||
it 'generates one header champ, plus one champ per PJ' do
|
||||
expect(subject.pluck(:libelle)).to contain_exactly("Pièces jointes", "not mandatory")
|
||||
end
|
||||
|
||||
it 'remembers the id of the PJ that got converted into a champ' do
|
||||
expect(subject.map(&:old_pj)).to include({ 'stable_id' => tpj_not_mandatory.id })
|
||||
end
|
||||
|
||||
context 'without pre-existing champs' do
|
||||
it 'generates a sequence of order_places incrementing from zero' do
|
||||
expect(subject.pluck(:order_place)).to contain_exactly(0, 1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pre-existing champs' do
|
||||
let(:procedure) do
|
||||
create(
|
||||
:procedure,
|
||||
types_de_piece_justificative: tpjs,
|
||||
types_de_champ: [build(:type_de_champ, order_place: 0), build(:type_de_champ, order_place: 1)]
|
||||
)
|
||||
end
|
||||
|
||||
it 'generates a sequence of incrementing order_places that continues where the last type de champ left off' do
|
||||
expect(subject.pluck(:order_place)).to contain_exactly(2, 3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pre-existing champs without an order place' do
|
||||
let(:procedure) do
|
||||
create(
|
||||
:procedure,
|
||||
types_de_piece_justificative: tpjs,
|
||||
types_de_champ: [build(:type_de_champ, order_place: 0), build(:type_de_champ, order_place: nil)]
|
||||
)
|
||||
end
|
||||
|
||||
it 'ignores champs without an order place' do
|
||||
expect(subject.pluck(:order_place)).to contain_exactly(1, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -134,7 +134,6 @@ RSpec.configure do |config|
|
|||
config.include FactoryBot::Syntax::Methods
|
||||
|
||||
config.before(:each) do
|
||||
allow_any_instance_of(PieceJustificativeUploader).to receive(:generate_secure_token).and_return("3dbb3535-5388-4a37-bc2d-778327b9f997")
|
||||
allow_any_instance_of(ProcedureLogoUploader).to receive(:generate_secure_token).and_return("3dbb3535-5388-4a37-bc2d-778327b9f998")
|
||||
|
||||
Flipflop::FeatureSet.current.test!.reset!
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PieceJustificativeUploader do
|
||||
let(:pj) { create(:piece_justificative, :rib) }
|
||||
|
||||
it { expect(pj.content.filename).to eq 'piece_justificative.pdf' }
|
||||
|
||||
context 'when extension is nil' do
|
||||
it do
|
||||
expect(pj.content.file).to receive(:extension).and_return(nil)
|
||||
expect(pj.content.filename).to eq 'piece_justificative.'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,48 +0,0 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'admin/pieces_justificatives/show.html.haml', type: :view do
|
||||
let(:procedure) { create(:procedure) }
|
||||
|
||||
describe 'fields sorted' do
|
||||
let(:first_libelle) { 'salut la compagnie' }
|
||||
let(:last_libelle) { 'je suis bien sur la page' }
|
||||
let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1, libelle: last_libelle) }
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0, libelle: first_libelle) }
|
||||
before do
|
||||
procedure.reload
|
||||
assign(:procedure, procedure)
|
||||
render
|
||||
end
|
||||
it 'sorts by order place' do
|
||||
expect(rendered).to match(/#{first_libelle}.*#{last_libelle}/m)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'arrow button' do
|
||||
subject do
|
||||
procedure.reload
|
||||
assign(:procedure, procedure)
|
||||
render
|
||||
rendered
|
||||
end
|
||||
context 'when there is no field in database' do
|
||||
it { expect(subject).not_to have_css('.fa-chevron-down') }
|
||||
it { expect(subject).not_to have_css('.fa-chevron-up') }
|
||||
end
|
||||
context 'when there is only one field in database' do
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
|
||||
it { expect(subject).not_to have_css('#btn_down_0') }
|
||||
it { expect(subject).not_to have_css('#btn_up_0') }
|
||||
it { expect(subject).not_to have_css('#btn_up_1') }
|
||||
it { expect(subject).not_to have_css('#btn_down_1') }
|
||||
end
|
||||
context 'when there are 2 fields in database' do
|
||||
let!(:type_de_piece_justificative_0) { create(:type_de_piece_justificative, procedure: procedure, order_place: 0) }
|
||||
let!(:type_de_piece_justificative_1) { create(:type_de_piece_justificative, procedure: procedure, order_place: 1) }
|
||||
it { expect(subject).to have_css('#btn_down_0') }
|
||||
it { expect(subject).not_to have_css('#btn_up_0') }
|
||||
it { expect(subject).to have_css('#btn_up_1') }
|
||||
it { expect(subject).not_to have_css('#btn_down_1') }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,5 @@ describe 'gestionnaires/dossiers/show.html.haml', type: :view do
|
|||
it 'renders the dossier infos' do
|
||||
expect(rendered).to have_text('Identité')
|
||||
expect(rendered).to have_text('Demande')
|
||||
expect(rendered).to have_text('Pièces jointes')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -51,12 +51,4 @@ describe 'shared/dossiers/demande.html.haml', type: :view do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dossier has pièces justificatives' do
|
||||
let(:procedure) { create(:procedure, :published, :with_two_type_de_piece_justificative) }
|
||||
|
||||
it 'renders the pièces justificatives' do
|
||||
expect(rendered).to have_text('Pièces jointes')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'users/dossiers/brouillon.html.haml', type: :view do
|
||||
let(:procedure) { create(:procedure, :with_two_type_de_piece_justificative, :with_notice, for_individual: true) }
|
||||
let(:dossier) { create(:dossier, :with_entreprise, :with_service, state: Dossier.states.fetch(:brouillon), procedure: procedure) }
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ, :with_notice, :with_service, for_individual: true) }
|
||||
let(:dossier) { create(:dossier, :with_entreprise, state: Dossier.states.fetch(:brouillon), procedure: procedure) }
|
||||
let(:footer) { view.content_for(:footer) }
|
||||
|
||||
before do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'users/dossiers/demande.html.haml', type: :view do
|
||||
let(:procedure) { create(:procedure, :published, :with_two_type_de_piece_justificative, :with_type_de_champ, :with_type_de_champ_private) }
|
||||
let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_type_de_champ_private) }
|
||||
let(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) }
|
||||
|
||||
before do
|
||||
|
@ -19,7 +19,6 @@ describe 'users/dossiers/demande.html.haml', type: :view do
|
|||
expect(rendered).to have_text('Déposé le')
|
||||
expect(rendered).to have_text('Identité')
|
||||
expect(rendered).to have_text('Demande')
|
||||
expect(rendered).to have_text('Pièces jointes')
|
||||
end
|
||||
|
||||
context 'when the dossier is editable' do
|
||||
|
|
Loading…
Reference in a new issue