Merge branch 'main' into feature/prefill_repetible
This commit is contained in:
commit
dbb92e7fd3
154 changed files with 1498 additions and 557 deletions
31
Gemfile.lock
31
Gemfile.lock
|
@ -179,7 +179,7 @@ GEM
|
|||
activerecord (>= 3.1.0, < 7)
|
||||
delayed_cron_job (0.7.4)
|
||||
delayed_job (>= 4.1)
|
||||
delayed_job (4.1.10)
|
||||
delayed_job (4.1.11)
|
||||
activesupport (>= 3.0, < 8.0)
|
||||
delayed_job_active_record (4.1.5)
|
||||
activerecord (>= 3.0, < 6.2)
|
||||
|
@ -249,15 +249,16 @@ GEM
|
|||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
ffi (1.15.5)
|
||||
flipper (0.24.1)
|
||||
flipper-active_record (0.24.1)
|
||||
flipper (0.26.0)
|
||||
concurrent-ruby (< 2)
|
||||
flipper-active_record (0.26.0)
|
||||
activerecord (>= 4.2, < 8)
|
||||
flipper (~> 0.24.1)
|
||||
flipper-ui (0.24.1)
|
||||
flipper (~> 0.26.0)
|
||||
flipper-ui (0.26.0)
|
||||
erubi (>= 1.0.0, < 2.0.0)
|
||||
flipper (~> 0.24.1)
|
||||
flipper (~> 0.26.0)
|
||||
rack (>= 1.4, < 3)
|
||||
rack-protection (>= 1.5.3, <= 2.2.0)
|
||||
rack-protection (>= 1.5.3, <= 4.0.0)
|
||||
sanitize (< 7)
|
||||
fog-core (2.2.3)
|
||||
builder
|
||||
|
@ -424,7 +425,7 @@ GEM
|
|||
msgpack (1.4.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.1.1)
|
||||
mustermann (1.1.1)
|
||||
mustermann (3.0.0)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
net-imap (0.2.3)
|
||||
digest
|
||||
|
@ -441,7 +442,7 @@ GEM
|
|||
net-protocol
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.14.0)
|
||||
nokogiri (1.14.1)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
open4 (1.3.4)
|
||||
|
@ -506,7 +507,7 @@ GEM
|
|||
httpclient
|
||||
json-jwt (>= 1.11.0)
|
||||
rack (>= 2.1.0)
|
||||
rack-protection (2.2.0)
|
||||
rack-protection (3.0.5)
|
||||
rack
|
||||
rack-proxy (0.7.4)
|
||||
rack
|
||||
|
@ -683,10 +684,10 @@ GEM
|
|||
simple_xlsx_reader (1.0.4)
|
||||
nokogiri
|
||||
rubyzip
|
||||
sinatra (2.2.0)
|
||||
mustermann (~> 1.0)
|
||||
rack (~> 2.2)
|
||||
rack-protection (= 2.2.0)
|
||||
sinatra (3.0.5)
|
||||
mustermann (~> 3.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-protection (= 3.0.5)
|
||||
tilt (~> 2.0)
|
||||
skylight (5.3.4)
|
||||
activesupport (>= 5.2.0)
|
||||
|
@ -729,7 +730,7 @@ GEM
|
|||
railties (>= 6.0.0)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.5)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ulid-ruby (1.0.2)
|
||||
unf (0.1.4)
|
||||
|
|
BIN
app/assets/images/mailer/logo-services-plus.png
Normal file
BIN
app/assets/images/mailer/logo-services-plus.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -173,6 +173,7 @@
|
|||
input[type=email],
|
||||
input[type=password],
|
||||
input[type=date],
|
||||
input[type=datetime-local],
|
||||
input[type=number],
|
||||
input[type=tel],
|
||||
textarea,
|
||||
|
@ -207,6 +208,7 @@
|
|||
input[type=date],
|
||||
input[type=number],
|
||||
input[type=tel],
|
||||
input[type=datetime-local],
|
||||
textarea,
|
||||
select {
|
||||
border-radius: 4px;
|
||||
|
@ -248,6 +250,7 @@
|
|||
input[type=password],
|
||||
input[type=date],
|
||||
input[type=number],
|
||||
input[type=datetime-local],
|
||||
input[type=tel],
|
||||
textarea {
|
||||
@media (max-width: $two-columns-breakpoint) {
|
||||
|
@ -284,7 +287,8 @@
|
|||
&:not([size]) {
|
||||
&[type='date'],
|
||||
&[type='tel'],
|
||||
&[type='number'] {
|
||||
&[type='number'],
|
||||
&[type='datetime-local'] {
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
.container {
|
||||
a {
|
||||
cursor: pointer;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
&.hoverable {
|
||||
tbody tr:hover {
|
||||
background: $light-grey;
|
||||
background-color: $light-grey;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@
|
|||
.table {
|
||||
&.hoverable {
|
||||
tbody tr:hover {
|
||||
background: $white;
|
||||
background-color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
app/components/dossiers/batch_select_more_component.rb
Normal file
35
app/components/dossiers/batch_select_more_component.rb
Normal file
|
@ -0,0 +1,35 @@
|
|||
class Dossiers::BatchSelectMoreComponent < ApplicationComponent
|
||||
def initialize(dossiers_count:, filtered_sorted_ids:)
|
||||
@dossiers_count = dossiers_count
|
||||
@filtered_sorted_ids = filtered_sorted_ids
|
||||
end
|
||||
|
||||
def not_selected_button_data
|
||||
{
|
||||
action: "batch-operation#onSelectMore",
|
||||
dossiers: @filtered_sorted_ids.first(Instructeurs::ProceduresController::BATCH_SELECTION_LIMIT).join(',')
|
||||
}
|
||||
end
|
||||
|
||||
def selected_button_data
|
||||
{
|
||||
action: "batch-operation#onDeleteSelection"
|
||||
}
|
||||
end
|
||||
|
||||
def not_selected_text
|
||||
if @dossiers_count <= Instructeurs::ProceduresController::BATCH_SELECTION_LIMIT
|
||||
t(".select_all", dossiers_count: @dossiers_count)
|
||||
else
|
||||
t(".select_all_limit", dossiers_count: @dossiers_count, limit: Instructeurs::ProceduresController::BATCH_SELECTION_LIMIT)
|
||||
end
|
||||
end
|
||||
|
||||
def selected_text
|
||||
if @dossiers_count <= Instructeurs::ProceduresController::BATCH_SELECTION_LIMIT
|
||||
t(".selected_all", dossiers_count: @dossiers_count)
|
||||
else
|
||||
t(".selected_all_limit", limit: Instructeurs::ProceduresController::BATCH_SELECTION_LIMIT)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
en:
|
||||
pagination_files_selected_html: All <span id='dynamic_number'>n</span> files from this page are selected.
|
||||
select_all: "Select all %{dossiers_count} files."
|
||||
select_all_limit: "Select first %{limit} files on %{dossiers_count}"
|
||||
selected_all: "All %{dossiers_count} files are selected."
|
||||
selected_all_limit: "%{limit} files are selected."
|
||||
delete_selection: "Delete selection"
|
|
@ -0,0 +1,7 @@
|
|||
fr:
|
||||
pagination_files_selected_html: Les <span id='dynamic_number'>n</span> dossiers de cette page sont sélectionnés.
|
||||
select_all: "Sélectionner tous les %{dossiers_count} dossiers."
|
||||
select_all_limit: "Sélectionner les %{limit} premiers dossiers sur les %{dossiers_count}"
|
||||
selected_all: "Tous les %{dossiers_count} dossiers sont sélectionnés."
|
||||
selected_all_limit: "%{limit} dossiers sont sélectionnés."
|
||||
delete_selection: "Effacer la sélection"
|
|
@ -0,0 +1,15 @@
|
|||
%tr#js_batch_select_more.fr-background-alt--blue-france.hidden
|
||||
%td.fr-py-2w.text-center{ colspan: 100 }
|
||||
#not_selected
|
||||
%p
|
||||
= t('.pagination_files_selected_html')
|
||||
%button.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline{ data: not_selected_button_data }
|
||||
= not_selected_text
|
||||
|
||||
#selected.hidden
|
||||
%p
|
||||
= selected_text
|
||||
%button.fr-btn.fr-btn--sm.fr-btn--tertiary-no-outline{ data: selected_button_data }
|
||||
= t(".delete_selection")
|
||||
|
||||
= hidden_field_tag :"batch_operation[dossier_ids][]", "", form: dom_id(BatchOperation.new), id: dom_id(BatchOperation.new, "input_multiple_ids")
|
|
@ -7,4 +7,4 @@
|
|||
= render EditableChamp::ChampLabelContentComponent.new champ: @champ, seen_at: @seen_at
|
||||
|
||||
- if @champ.description.present?
|
||||
.notice{ id: @champ.describedby_id }= string_to_html(@champ.description)
|
||||
.notice{ id: @champ.describedby_id }= string_to_html(@champ.description, allow_a: true)
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
class EditableChamp::DatetimeComponent < EditableChamp::EditableChampBaseComponent
|
||||
def datetime_start_year(date)
|
||||
if date == nil || date.year == 0 || date.year >= Date.today.year - 1
|
||||
Date.today.year - 1
|
||||
def formatted_value_for_datetime_locale
|
||||
if @champ.valid? && @champ.value.present?
|
||||
# convert to a format that the datetime-local input can understand
|
||||
DateTime.iso8601(@champ.value).strftime('%Y-%m-%dT%H:%M')
|
||||
else
|
||||
date.year
|
||||
@champ.value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,2 @@
|
|||
- parsed_value = @champ.value.present? ? Time.zone.parse(@champ.value) : nil
|
||||
|
||||
.datetime
|
||||
= @form.datetime_select(:value, id: @champ.input_id, aria: { describedby: @champ.describedby_id }, selected: parsed_value, start_year: datetime_start_year(parsed_value), end_year: Date.today.year + 50, minute_step: 5, include_blank: true)
|
||||
= @form.datetime_field(:value, value: formatted_value_for_datetime_locale, id: @champ.input_id, aria: { describedby: @champ.describedby_id })
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- if @champ.block?
|
||||
%h3.header-subsection= @champ.libelle
|
||||
- if @champ.description.present?
|
||||
%p.notice= string_to_html(@champ.description, false)
|
||||
%p.notice= string_to_html(@champ.description, false, allow_a: true)
|
||||
|
||||
- elsif has_label?(@champ)
|
||||
= render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
= render Dsfr::CalloutComponent.new(title: @champ.libelle, extra_class_names: ['fr-mb-2w', 'fr-callout--blue-cumulus']) do |c|
|
||||
- c.with_body do
|
||||
|
||||
= string_to_html(@champ.description)
|
||||
= string_to_html(@champ.description, allow_a: true)
|
||||
|
||||
- if @champ.collapsible_explanation_enabled? && @champ.collapsible_explanation_text.present?
|
||||
%div
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
= @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do
|
||||
- sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.type_de_champ.mandatory? ? tag.span(' *', class: 'mandatory') : ''))
|
||||
- if @champ.drop_down_secondary_description.present?
|
||||
.notice{ id: "#{@champ.describedby_id}-secondary" }= string_to_html(@champ.drop_down_secondary_description)
|
||||
.notice{ id: "#{@champ.describedby_id}-secondary" }= string_to_html(@champ.drop_down_secondary_description, allow_a: true)
|
||||
= @form.select :secondary_value,
|
||||
@champ.secondary_options[@champ.primary_value],
|
||||
{},
|
||||
|
|
|
@ -31,6 +31,7 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent
|
|||
def humanize(error)
|
||||
case error
|
||||
in { type: :not_available }
|
||||
in { type: :incompatible, stable_id: nil }
|
||||
t('not_available', scope: '.errors')
|
||||
in { type: :unmanaged, stable_id: stable_id }
|
||||
targeted_champ = @upper_tdcs.find { |tdc| tdc.stable_id == stable_id }
|
||||
|
|
|
@ -10,6 +10,10 @@ class TypesDeChampEditor::EstimatedFillDurationComponent < ApplicationComponent
|
|||
@is_annotation
|
||||
end
|
||||
|
||||
def render?
|
||||
@revision.procedure.estimated_duration_visible?
|
||||
end
|
||||
|
||||
def show?
|
||||
!annotations? && @revision.types_de_champ_public.present?
|
||||
end
|
||||
|
|
|
@ -7,6 +7,10 @@ module Administrateurs
|
|||
id = params[:procedure_id] || params[:id]
|
||||
|
||||
@procedure = current_administrateur.procedures.find(id)
|
||||
|
||||
Sentry.configure_scope do |scope|
|
||||
scope.set_tags(procedure: @procedure.id)
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
flash.alert = 'Démarche inexistante'
|
||||
redirect_to admin_procedures_path, status: 404
|
||||
|
|
|
@ -123,10 +123,6 @@ module Administrateurs
|
|||
instructeurs.each { groupe_instructeur.add(_1) }
|
||||
|
||||
flash[:notice] = if procedure.routing_enabled?
|
||||
GroupeInstructeurMailer
|
||||
.add_instructeurs(groupe_instructeur, instructeurs, current_administrateur.email)
|
||||
.deliver_later
|
||||
|
||||
t('.assignment',
|
||||
count: instructeurs.size,
|
||||
emails: instructeurs.map(&:email).join(', '),
|
||||
|
@ -196,7 +192,7 @@ module Administrateurs
|
|||
end
|
||||
|
||||
def import
|
||||
if procedure.publiee?
|
||||
if procedure.publiee_or_close?
|
||||
if !CSV_ACCEPTED_CONTENT_TYPES.include?(group_csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type)
|
||||
flash[:alert] = "Importation impossible : veuillez importer un fichier CSV"
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ module Administrateurs
|
|||
@dossier = dossier
|
||||
@logo_url = procedure.logo_url
|
||||
@service = procedure.service
|
||||
@rendered_template = sanitize(mail_template.body_for_dossier(dossier))
|
||||
@rendered_template = sanitize(mail_template.body_for_dossier(dossier), scrubber: Sanitizers::MailScrubber.new)
|
||||
@actions = mail_template.actions_for_dossier(dossier)
|
||||
|
||||
render(template: 'notification_mailer/send_notification', layout: 'mailers/notifications_layout')
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
class API::V2::GraphqlController < API::V2::BaseController
|
||||
include GraphqlOperationLogConcern
|
||||
|
||||
def execute
|
||||
result = API::V2::Schema.execute(query,
|
||||
variables: variables,
|
||||
|
@ -24,7 +22,8 @@ class API::V2::GraphqlController < API::V2::BaseController
|
|||
super
|
||||
|
||||
payload.merge!({
|
||||
graphql_operation: operation_log(query(fallback: ''), params[:operationName], to_unsafe_hash(params[:variables]))
|
||||
graphql_query: query(fallback: params[:queryId]),
|
||||
graphql_variables: to_unsafe_hash(params[:variables]).to_json
|
||||
})
|
||||
end
|
||||
|
||||
|
|
|
@ -198,7 +198,9 @@ class ApplicationController < ActionController::Base
|
|||
payload.merge!({
|
||||
user_agent: request.user_agent,
|
||||
user_id: current_user&.id,
|
||||
user_roles: current_user_roles
|
||||
user_roles: current_user_roles,
|
||||
client_ip: request.headers['X-Forwarded-For'],
|
||||
request_id: request.headers['X-Request-ID']
|
||||
}.compact)
|
||||
|
||||
if browser.known?
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
module GraphqlOperationLogConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# This method parses GraphQL query and creates a short description of the query. It is useful for logging.
|
||||
def operation_log(query, operation_name, variables)
|
||||
return "NoQuery" if query.nil?
|
||||
|
||||
operation = parse_graphql_query(query, operation_name)
|
||||
|
||||
return "InvalidQuery" if operation.nil?
|
||||
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
|
||||
|
||||
message = "#{operation.operation_type}: "
|
||||
message += if operation.name.present?
|
||||
"#{operation.name} { "
|
||||
else
|
||||
"{ "
|
||||
end
|
||||
message += operation.selections.map(&:name).join(', ')
|
||||
message += " } "
|
||||
message += if variables.present?
|
||||
variables.flat_map do |(name, value)|
|
||||
format_graphql_variable(name, value)
|
||||
end
|
||||
else
|
||||
operation.selections.flat_map(&:arguments).flat_map do |argument|
|
||||
format_graphql_variable(argument.name, argument.value)
|
||||
end
|
||||
end.join(', ')
|
||||
|
||||
message.strip
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_graphql_query(query, operation_name)
|
||||
operations = GraphQL.parse(query).children.filter do |node|
|
||||
node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
|
||||
end
|
||||
if operations.size == 1
|
||||
operations.first
|
||||
else
|
||||
operations.find { |node| node.name == operation_name }
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def format_graphql_variable(name, value)
|
||||
if value.is_a?(Hash)
|
||||
value.map do |(name, value)|
|
||||
format_graphql_variable(name, value)
|
||||
end
|
||||
elsif value.is_a?(GraphQL::Language::Nodes::InputObject)
|
||||
value.arguments.map do |argument|
|
||||
format_graphql_variable(argument.name, argument.value)
|
||||
end
|
||||
else
|
||||
"#{name}: \"#{value.to_s.truncate(10)}\""
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,17 +1,31 @@
|
|||
module QueryParamsStoreConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
helper_method :stored_query_params?
|
||||
end
|
||||
|
||||
def store_query_params
|
||||
# Don't override already stored params, because we could do goings and comings with authentication, and
|
||||
# lost previously stored params
|
||||
return if session[:stored_params].present? || request.query_parameters.empty?
|
||||
return if stored_query_params? || filtered_query_params.empty?
|
||||
|
||||
session[:stored_params] = request.query_parameters.to_json
|
||||
session[:stored_params] = filtered_query_params.to_json
|
||||
end
|
||||
|
||||
def retrieve_and_delete_stored_query_params
|
||||
return {} if session[:stored_params].blank?
|
||||
return {} unless stored_query_params?
|
||||
|
||||
JSON.parse(session.delete(:stored_params))
|
||||
end
|
||||
|
||||
def stored_query_params?
|
||||
session[:stored_params].present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filtered_query_params
|
||||
request.query_parameters.except(:locale, "locale")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,17 +14,12 @@ module Instructeurs
|
|||
end
|
||||
end
|
||||
|
||||
def revive
|
||||
def remind
|
||||
avis = Avis.find(params[:id])
|
||||
if avis.revivable_by?(current_instructeur)
|
||||
if avis.answer.blank?
|
||||
AvisMailer.avis_invitation(avis).deliver_later
|
||||
flash.notice = "Un mail de relance a été envoyé à #{avis.expert.email}"
|
||||
redirect_back(fallback_location: avis_instructeur_dossier_path(avis.procedure, avis.dossier))
|
||||
else
|
||||
flash.alert = "#{avis.expert.email} a déjà donné son avis"
|
||||
redirect_back(fallback_location: avis_instructeur_dossier_path(avis.procedure, avis.dossier))
|
||||
end
|
||||
if avis.remind_by!(current_instructeur)
|
||||
AvisMailer.avis_invitation(avis).deliver_later
|
||||
flash.notice = "Un mail de relance a été envoyé à #{avis.expert.email}"
|
||||
redirect_back(fallback_location: avis_instructeur_dossier_path(avis.procedure, avis.dossier))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,6 +14,7 @@ module Instructeurs
|
|||
def batch_operation_params
|
||||
params.require(:batch_operation)
|
||||
.permit(:operation, :motivation, :justificatif_motivation, dossier_ids: [])
|
||||
.merge(dossier_ids: params['batch_operation']['dossier_ids'].join(',').split(',').uniq)
|
||||
.merge(instructeur: current_instructeur)
|
||||
end
|
||||
|
||||
|
|
|
@ -22,9 +22,6 @@ module Instructeurs
|
|||
else
|
||||
groupe_instructeur.add(instructeur)
|
||||
flash[:notice] = "L’instructeur « #{instructeur_email} » a été affecté au groupe."
|
||||
GroupeInstructeurMailer
|
||||
.add_instructeurs(groupe_instructeur, [instructeur], current_user.email)
|
||||
.deliver_later
|
||||
end
|
||||
|
||||
redirect_to instructeur_groupe_path(procedure, groupe_instructeur)
|
||||
|
|
|
@ -4,6 +4,7 @@ module Instructeurs
|
|||
before_action :ensure_not_super_admin!, only: [:download_export]
|
||||
|
||||
ITEMS_PER_PAGE = 25
|
||||
BATCH_SELECTION_LIMIT = 500
|
||||
|
||||
def index
|
||||
@procedures = current_instructeur
|
||||
|
@ -77,13 +78,13 @@ module Instructeurs
|
|||
@has_termine_notifications = notifications[:termines].present?
|
||||
@not_archived_notifications_dossier_ids = notifications[:en_cours] + notifications[:termines]
|
||||
|
||||
filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: dossiers_count)
|
||||
@filtered_sorted_ids = procedure_presentation.filtered_sorted_ids(dossiers, statut, count: dossiers_count)
|
||||
|
||||
page = params[:page].presence || 1
|
||||
|
||||
@dossiers_count = filtered_sorted_ids.size
|
||||
@dossiers_count = @filtered_sorted_ids.size
|
||||
@filtered_sorted_paginated_ids = Kaminari
|
||||
.paginate_array(filtered_sorted_ids)
|
||||
.paginate_array(@filtered_sorted_ids)
|
||||
.page(page)
|
||||
.per(ITEMS_PER_PAGE)
|
||||
|
||||
|
|
|
@ -30,6 +30,13 @@ module Manager
|
|||
redirect_to manager_user_path(user)
|
||||
end
|
||||
|
||||
def resend_reset_password_instructions
|
||||
user = User.find(params[:id])
|
||||
user.send_reset_password_instructions
|
||||
flash[:notice] = "L'email de réinitialisation du mot de passe a été renvoyé."
|
||||
redirect_to manager_user_path(user)
|
||||
end
|
||||
|
||||
def enable_feature
|
||||
user = User.find(params[:id])
|
||||
|
||||
|
|
|
@ -19,9 +19,7 @@ module Users
|
|||
end
|
||||
|
||||
def destroy
|
||||
transfer = DossierTransfer
|
||||
.joins(:dossiers)
|
||||
.find_by!(id: params[:id], dossiers: { user: current_user })
|
||||
transfer = DossierTransfer.find_by!(id: params[:id], email: current_user.email)
|
||||
|
||||
transfer.destroy_and_nullify
|
||||
redirect_to dossiers_path
|
||||
|
|
|
@ -38,6 +38,7 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
procedure_expires_when_termine_enabled: Field::Boolean,
|
||||
duree_conservation_dossiers_dans_ds: Field::Number,
|
||||
max_duree_conservation_dossiers_dans_ds: Field::Number,
|
||||
estimated_duration_visible: Field::Boolean,
|
||||
tags: Field::Text
|
||||
}.freeze
|
||||
|
||||
|
@ -88,7 +89,8 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
:attestation_template,
|
||||
:procedure_expires_when_termine_enabled,
|
||||
:duree_conservation_dossiers_dans_ds,
|
||||
:max_duree_conservation_dossiers_dans_ds
|
||||
:max_duree_conservation_dossiers_dans_ds,
|
||||
:estimated_duration_visible
|
||||
].freeze
|
||||
|
||||
# FORM_ATTRIBUTES
|
||||
|
@ -97,7 +99,8 @@ class ProcedureDashboard < Administrate::BaseDashboard
|
|||
FORM_ATTRIBUTES = [
|
||||
:procedure_expires_when_termine_enabled,
|
||||
:duree_conservation_dossiers_dans_ds,
|
||||
:max_duree_conservation_dossiers_dans_ds
|
||||
:max_duree_conservation_dossiers_dans_ds,
|
||||
:estimated_duration_visible
|
||||
].freeze
|
||||
|
||||
# Overwrite this method to customize how procedures are displayed
|
||||
|
|
|
@ -12,7 +12,7 @@ module Loaders
|
|||
private
|
||||
|
||||
def query(keys)
|
||||
::Dossier.visible_by_administration.where(id: keys)
|
||||
::Dossier.visible_by_administration.for_api_v2.where(id: keys)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,12 +26,6 @@ module Mutations
|
|||
result[:warnings] = [warning]
|
||||
end
|
||||
|
||||
if groupe_instructeur.procedure.routing_enabled? && instructeurs.present?
|
||||
GroupeInstructeurMailer
|
||||
.add_instructeurs(groupe_instructeur, instructeurs, current_administrateur.email)
|
||||
.deliver_later
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
|
|
@ -76,7 +76,7 @@ module Types
|
|||
end
|
||||
|
||||
def groupe_instructeur
|
||||
Loaders::Record.for(GroupeInstructeur).load(object.groupe_instructeur_id)
|
||||
Loaders::Record.for(GroupeInstructeur, includes: [:procedure]).load(object.groupe_instructeur_id)
|
||||
end
|
||||
|
||||
def demandeur
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
module StringToHtmlHelper
|
||||
def string_to_html(str, wrapper_tag = 'p')
|
||||
def string_to_html(str, wrapper_tag = 'p', allow_a: false)
|
||||
return nil if str.blank?
|
||||
html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag })
|
||||
with_links = Anchored::Linker.auto_link(html_formatted, target: '_blank', rel: 'noopener')
|
||||
sanitize(with_links, attributes: ['target', 'rel', 'href'])
|
||||
|
||||
tags = if allow_a
|
||||
Rails.configuration.action_view.sanitized_allowed_tags + ['a']
|
||||
else
|
||||
Rails.configuration.action_view.sanitized_allowed_tags
|
||||
end
|
||||
|
||||
sanitize(with_links, tags:, attributes: ['target', 'rel', 'href'])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ApplicationController } from './application_controller';
|
||||
import { disable, enable } from '@utils';
|
||||
import { disable, enable, show, hide } from '@utils';
|
||||
import invariant from 'tiny-invariant';
|
||||
|
||||
export class BatchOperationController extends ApplicationController {
|
||||
|
@ -11,6 +11,7 @@ export class BatchOperationController extends ApplicationController {
|
|||
|
||||
onCheckOne() {
|
||||
this.toggleSubmitButtonWhenNeeded();
|
||||
deleteSelection();
|
||||
}
|
||||
|
||||
onCheckAll(event: Event) {
|
||||
|
@ -18,6 +19,38 @@ export class BatchOperationController extends ApplicationController {
|
|||
|
||||
this.inputTargets.forEach((e) => (e.checked = target.checked));
|
||||
this.toggleSubmitButtonWhenNeeded();
|
||||
|
||||
const pagination = document.querySelector('tfoot .pagination');
|
||||
if (pagination) {
|
||||
displayNotice(this.inputTargets);
|
||||
}
|
||||
}
|
||||
|
||||
onSelectMore(event: {
|
||||
preventDefault: () => void;
|
||||
target: HTMLInputElement;
|
||||
}) {
|
||||
event.preventDefault();
|
||||
|
||||
const target = event.target as HTMLInputElement;
|
||||
const dossierIds = target.getAttribute('data-dossiers');
|
||||
|
||||
const hidden_input_multiple_ids = document.querySelector<HTMLInputElement>(
|
||||
'#input_multiple_ids_batch_operation'
|
||||
);
|
||||
if (hidden_input_multiple_ids) {
|
||||
hidden_input_multiple_ids.value = dossierIds || '';
|
||||
}
|
||||
|
||||
hide(document.querySelector('#not_selected'));
|
||||
show(document.querySelector('#selected'));
|
||||
}
|
||||
|
||||
onDeleteSelection(event: { preventDefault: () => void }) {
|
||||
event.preventDefault();
|
||||
emptyCheckboxes();
|
||||
deleteSelection();
|
||||
this.toggleSubmitButtonWhenNeeded();
|
||||
}
|
||||
|
||||
toggleSubmitButtonWhenNeeded() {
|
||||
|
@ -63,3 +96,44 @@ function switchButton(button: HTMLButtonElement, flag: boolean) {
|
|||
button.querySelectorAll('button').forEach((button) => disable(button));
|
||||
}
|
||||
}
|
||||
|
||||
function displayNotice(inputs: HTMLInputElement[]) {
|
||||
const checkbox_all = document.querySelector<HTMLInputElement>(
|
||||
'#checkbox_all_batch_operation'
|
||||
);
|
||||
if (checkbox_all) {
|
||||
if (checkbox_all.checked) {
|
||||
show(document.querySelector('#js_batch_select_more'));
|
||||
hide(document.querySelector('#selected'));
|
||||
show(document.querySelector('#not_selected'));
|
||||
} else {
|
||||
hide(document.querySelector('#js_batch_select_more'));
|
||||
deleteSelection();
|
||||
}
|
||||
}
|
||||
|
||||
const dynamic_number = document.querySelector('#dynamic_number');
|
||||
|
||||
if (dynamic_number) {
|
||||
dynamic_number.textContent = inputs.length.toString();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteSelection() {
|
||||
const hidden_input_multiple_ids = document.querySelector<HTMLInputElement>(
|
||||
'#input_multiple_ids_batch_operation'
|
||||
);
|
||||
|
||||
if (hidden_input_multiple_ids) {
|
||||
hidden_input_multiple_ids.value = '';
|
||||
}
|
||||
|
||||
hide(document.querySelector('#js_batch_select_more'));
|
||||
}
|
||||
|
||||
function emptyCheckboxes() {
|
||||
const inputs = document.querySelectorAll<HTMLInputElement>(
|
||||
'div[data-controller="batch-operation"] input[type=checkbox]'
|
||||
);
|
||||
inputs.forEach((e) => (e.checked = false));
|
||||
}
|
||||
|
|
|
@ -154,6 +154,13 @@ export class MenuButtonController extends ApplicationController {
|
|||
switch (event.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
if (this.isOpen) {
|
||||
this.close();
|
||||
} else {
|
||||
this.open();
|
||||
}
|
||||
stopPropagation = true;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'Down':
|
||||
this.open();
|
||||
|
|
|
@ -83,7 +83,8 @@ export class AutoUpload {
|
|||
if (error.failureReason == FAILURE_CONNECTIVITY) {
|
||||
return {
|
||||
title:
|
||||
'Le fichier n’a pas pu être envoyé. Vérifiez votre connexion à Internet, puis ré-essayez.',
|
||||
'Le fichier n’a pas pu être envoyé. Vérifiez votre connexion à Internet, puis ré-essayez. Vérifiez aussi que le pare-feu de votre appareil ou votre réseau autorise l’envoi de fichier vers ' +
|
||||
window.location.host,
|
||||
retry: true
|
||||
};
|
||||
} else if (error.code == ERROR_CODE_READ) {
|
||||
|
|
|
@ -10,6 +10,7 @@ if (enabled && key) {
|
|||
Sentry.init({
|
||||
dsn: key,
|
||||
environment,
|
||||
tracesSampleRate: 0.1,
|
||||
ignoreErrors: [
|
||||
// Ignore errors generated by a Microsoft crawler.
|
||||
// See https://forum.sentry.io/t/unhandledrejection-non-error-promise-rejection-captured-with-value/14062
|
||||
|
|
7
app/jobs/cron/purge_old_email_event_job.rb
Normal file
7
app/jobs/cron/purge_old_email_event_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class Cron::PurgeOldEmailEventJob < Cron::CronJob
|
||||
self.schedule_expression = "every week at 3:00"
|
||||
|
||||
def perform
|
||||
EmailEvent.outdated.destroy_all
|
||||
end
|
||||
end
|
|
@ -3,7 +3,13 @@ class ExportJob < ApplicationJob
|
|||
|
||||
discard_on ActiveRecord::RecordNotFound
|
||||
|
||||
before_perform do |job|
|
||||
Sentry.set_tags(procedure_id: job.arguments.first.procedure.id)
|
||||
end
|
||||
|
||||
def perform(export)
|
||||
return if export.generated?
|
||||
|
||||
export.compute_with_safe_stale_for_purge do
|
||||
export.compute
|
||||
end
|
||||
|
|
17
app/jobs/migrations/backfill_dossier_repetition_job.rb
Normal file
17
app/jobs/migrations/backfill_dossier_repetition_job.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
class Migrations::BackfillDossierRepetitionJob < ApplicationJob
|
||||
def perform(dossier_ids)
|
||||
Dossier.where(id: dossier_ids)
|
||||
.includes(:champs, revision: :types_de_champ)
|
||||
.find_each do |dossier|
|
||||
dossier
|
||||
.revision
|
||||
.types_de_champ
|
||||
.filter do |type_de_champ|
|
||||
type_de_champ.type_champ == 'repetition' && dossier.champs.none? { _1.type_de_champ_id == type_de_champ.id }
|
||||
end
|
||||
.each do |type_de_champ|
|
||||
dossier.champs << type_de_champ.champ.build
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -47,8 +47,12 @@ class BalancerDeliveryMethod
|
|||
def delivery_method(mail)
|
||||
return mail[FORCE_DELIVERY_METHOD_HEADER].value.to_sym if force_delivery_method?(mail)
|
||||
|
||||
@delivery_methods
|
||||
compatible_delivery_methods_for(mail)
|
||||
.flat_map { |delivery_method, weight| [delivery_method] * weight }
|
||||
.sample(random: self.class.random)
|
||||
end
|
||||
|
||||
def compatible_delivery_methods_for(mail)
|
||||
@delivery_methods.reject { |delivery_method, _weight| delivery_method.to_s == 'dolist_api' && !Dolist::API.sendable?(mail) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
require "support/jsv"
|
||||
|
||||
class Dolist::API
|
||||
CONTACT_URL = "https://apiv9.dolist.net/v1/contacts/read?AccountID=%{account_id}"
|
||||
EMAIL_LOGS_URL = "https://apiv9.dolist.net/v1/statistics/email/sendings/transactional/search?AccountID=%{account_id}"
|
||||
EMAIL_KEY = 7
|
||||
DOLIST_WEB_DASHBOARD = "https://campaign.dolist.net/#/%{account_id}/contacts/%{contact_id}/sendings"
|
||||
EMAIL_MESSAGES_ADRESSES_REPLIES = "https://apiv9.dolist.net/v1/email/messages/addresses/replies?AccountID=%{account_id}"
|
||||
EMAIL_MESSAGES_ADRESSES_PACKSENDERS = "https://apiv9.dolist.net/v1/email/messages/addresses/packsenders?AccountID=%{account_id}"
|
||||
EMAIL_SENDING_TRANSACTIONAL = "https://apiv9.dolist.net/v1/email/sendings/transactional?AccountID=%{account_id}"
|
||||
EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT = "https://apiv9.dolist.net/v1/email/sendings/transactional/attachment?AccountID=%{account_id}"
|
||||
EMAIL_SENDING_TRANSACTIONAL_SEARCH = "https://apiv9.dolist.net/v1/email/sendings/transactional/search?AccountID=%{account_id}"
|
||||
|
||||
class_attribute :limit_remaining, :limit_reset_at
|
||||
|
||||
|
@ -23,12 +30,77 @@ class Dolist::API
|
|||
|
||||
sleep (limit_reset_at - Time.zone.now).ceil
|
||||
end
|
||||
|
||||
def sendable?(mail)
|
||||
return false if mail.to.blank? # recipient are mandatory
|
||||
return false if mail.bcc.present? # no bcc support
|
||||
|
||||
# Mail having attachments are not yet supported in our account
|
||||
mail.attachments.none? { !_1.inline? }
|
||||
end
|
||||
end
|
||||
|
||||
def properly_configured?
|
||||
client_key.present?
|
||||
end
|
||||
|
||||
def send_email(mail)
|
||||
if mail.attachments.any? { !_1.inline? }
|
||||
return send_email_with_attachment(mail)
|
||||
end
|
||||
|
||||
body = { "TransactionalSending": prepare_mail_body(mail) }
|
||||
|
||||
url = format_url(EMAIL_SENDING_TRANSACTIONAL)
|
||||
post(url, body.to_json)
|
||||
end
|
||||
|
||||
def send_email_with_attachment(mail)
|
||||
uri = URI(format_url(EMAIL_SENDING_TRANSACTIONAL_ATTACHMENT))
|
||||
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
|
||||
default_headers.each do |key, value|
|
||||
next if key.to_s == "Content-Type"
|
||||
request[key] = value
|
||||
end
|
||||
|
||||
boundary = "---011000010111000001101001" # any random string not present in the body
|
||||
request.content_type = "multipart/form-data; boundary=#{boundary}"
|
||||
|
||||
body = "--#{boundary}\r\n"
|
||||
|
||||
base64_files(mail.attachments).each do |file|
|
||||
body << "Content-Disposition: form-data; name=\"#{file.field_name}\"; filename=\"#{file.filename}\"\r\n"
|
||||
body << "Content-Type: #{file.mime_type}\r\n"
|
||||
body << "\r\n"
|
||||
body << file.content
|
||||
body << "\r\n"
|
||||
end
|
||||
|
||||
body << "\r\n--#{boundary}\r\n"
|
||||
body << "Content-Disposition: form-data; name=\"TransactionalSending\"\r\n"
|
||||
body << "Content-Type: text/plain; charset=utf-8\r\n"
|
||||
body << "\r\n"
|
||||
body << prepare_mail_body(mail).to_jsv
|
||||
|
||||
body << "\r\n--#{boundary}--\r\n"
|
||||
body << "\r\n"
|
||||
|
||||
request.body = body
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
|
||||
response = http.request(request)
|
||||
|
||||
if response.body.empty?
|
||||
fail "Dolist API returned an empty response"
|
||||
else
|
||||
JSON.parse(response.body)
|
||||
end
|
||||
end
|
||||
|
||||
def sent_mails(email_address)
|
||||
contact_id = fetch_contact_id(email_address)
|
||||
if contact_id.nil?
|
||||
|
@ -44,9 +116,47 @@ class Dolist::API
|
|||
[]
|
||||
end
|
||||
|
||||
def senders
|
||||
get format_url(EMAIL_MESSAGES_ADRESSES_PACKSENDERS)
|
||||
end
|
||||
|
||||
def replies
|
||||
get format_url(EMAIL_MESSAGES_ADRESSES_REPLIES)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def headers
|
||||
def format_url(base)
|
||||
format(base, account_id: account_id)
|
||||
end
|
||||
|
||||
def sender_id
|
||||
Rails.cache.fetch("dolist_api_sender_id", expires_in: 1.hour) do
|
||||
senders.dig("ItemList", 0, "Sender", "ID")
|
||||
end
|
||||
end
|
||||
|
||||
def get(url)
|
||||
response = Typhoeus.get(url, headers: default_headers).tap do
|
||||
self.class.save_rate_limit_headers(_1.headers)
|
||||
end
|
||||
|
||||
JSON.parse(response.response_body)
|
||||
end
|
||||
|
||||
def post(url, body)
|
||||
response = Typhoeus.post(url, body:, headers: default_headers).tap do
|
||||
self.class.save_rate_limit_headers(_1.headers)
|
||||
end
|
||||
|
||||
if response.response_body.empty?
|
||||
fail "Empty response from Dolist API"
|
||||
else
|
||||
JSON.parse(response.response_body)
|
||||
end
|
||||
end
|
||||
|
||||
def default_headers
|
||||
{
|
||||
"Content-Type": 'application/json',
|
||||
"Accept": 'application/json',
|
||||
|
@ -82,12 +192,32 @@ class Dolist::API
|
|||
post(url, body)["ItemList"]
|
||||
end
|
||||
|
||||
def post(url, body)
|
||||
response = Typhoeus.post(url, body:, headers:).tap do
|
||||
self.class.save_rate_limit_headers(_1.headers)
|
||||
end
|
||||
|
||||
JSON.parse(response.response_body)
|
||||
def prepare_mail_body(mail)
|
||||
{
|
||||
"Type": "TransactionalService",
|
||||
"Contact": {
|
||||
"FieldList": [
|
||||
{
|
||||
"ID": EMAIL_KEY,
|
||||
"Value": mail.to.first
|
||||
}
|
||||
]
|
||||
},
|
||||
"Message": {
|
||||
"Name": mail['X-Dolist-Message-Name'].value,
|
||||
"Subject": mail.subject,
|
||||
"SenderID": sender_id,
|
||||
"ForceHttp": true,
|
||||
"Format": "html",
|
||||
"DisableOpenTracking": true,
|
||||
"IsTrackingValidated": true
|
||||
},
|
||||
"MessageContent": {
|
||||
"SourceCode": mail_source_code(mail),
|
||||
"EncodingType": "UTF8",
|
||||
"EnableTrackingDetection": false
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def to_sent_mail(email_address, contact_id, dolist_message)
|
||||
|
@ -112,4 +242,24 @@ class Dolist::API
|
|||
status
|
||||
end
|
||||
end
|
||||
|
||||
def mail_source_code(mail)
|
||||
if mail.html_part.nil? && mail.text_part.nil?
|
||||
mail.decoded
|
||||
else
|
||||
mail.html_part.body.decoded
|
||||
end
|
||||
end
|
||||
|
||||
def base64_files(attachments)
|
||||
attachments.map do |attachment|
|
||||
raise ArgumentError, "Dolist API does not support non PDF attachments. Given #{attachment.filename} which has mime_type=#{attachment.mime_type}" unless attachment.mime_type == "application/pdf"
|
||||
|
||||
field_name = File.basename(attachment.filename, File.extname(attachment.filename))
|
||||
attachment_content = attachment.body.decoded
|
||||
attachment_base64 = Base64.strict_encode64(attachment_content)
|
||||
|
||||
Dolist::Base64File.new(field_name:, filename: attachment.filename, mime_type: attachment.mime_type, content: attachment_base64)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
3
app/lib/dolist/base64_file.rb
Normal file
3
app/lib/dolist/base64_file.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
module Dolist
|
||||
Base64File = Struct.new(:field_name, :filename, :mime_type, :content, keyword_init: true)
|
||||
end
|
|
@ -3,4 +3,10 @@
|
|||
# so it would be shallowed otherwise.
|
||||
#
|
||||
# TODO: add a test which verify that the error will permit the job to retry
|
||||
class MailDeliveryError < Exception; end # rubocop:disable Lint/InheritException
|
||||
class MailDeliveryError < Exception # rubocop:disable Lint/InheritException
|
||||
def initialize(original_exception)
|
||||
super(original_exception.message)
|
||||
|
||||
set_backtrace(original_exception.backtrace)
|
||||
end
|
||||
end
|
||||
|
|
12
app/lib/sanitizers/mail_scrubber.rb
Normal file
12
app/lib/sanitizers/mail_scrubber.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
module Sanitizers
|
||||
class MailScrubber < Rails::Html::PermitScrubber
|
||||
def initialize
|
||||
super
|
||||
self.tags = Rails.application.config.action_view.sanitized_allowed_tags + ['a']
|
||||
end
|
||||
|
||||
def skip_node?(node)
|
||||
node.text?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,17 +1,6 @@
|
|||
class GroupeInstructeurMailer < ApplicationMailer
|
||||
layout 'mailers/layout'
|
||||
|
||||
def add_instructeurs(group, new_instructeurs, current_instructeur_email)
|
||||
@new_instructeur_emails = new_instructeurs.map(&:email)
|
||||
@group = group
|
||||
@current_instructeur_email = current_instructeur_email
|
||||
|
||||
subject = "Ajout d’un instructeur dans le groupe \"#{group.label}\""
|
||||
|
||||
emails = @group.instructeurs.map(&:email)
|
||||
mail(bcc: emails, subject: subject)
|
||||
end
|
||||
|
||||
def remove_instructeurs(group, removed_instructeurs, current_instructeur_email)
|
||||
@removed_instructeur_emails = removed_instructeurs.map(&:email)
|
||||
@group = group
|
||||
|
@ -22,15 +11,4 @@ class GroupeInstructeurMailer < ApplicationMailer
|
|||
emails = @group.instructeurs.map(&:email)
|
||||
mail(bcc: emails, subject: subject)
|
||||
end
|
||||
|
||||
def remove_instructeur(group, instructeur, current_instructeur_email)
|
||||
@email = instructeur.email
|
||||
@group = group
|
||||
@current_instructeur_email = current_instructeur_email
|
||||
|
||||
subject = "Suppression d’un instructeur dans le groupe \"#{group.label}\""
|
||||
|
||||
emails = @group.instructeurs.map(&:email)
|
||||
mail(bcc: emails, subject: subject)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,7 @@ class NotificationMailer < ApplicationMailer
|
|||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
before_action :set_dossier
|
||||
before_action :set_services_publics_plus, only: :send_notification
|
||||
after_action :create_commentaire_for_notification
|
||||
|
||||
helper ServiceHelper
|
||||
|
@ -20,7 +21,7 @@ class NotificationMailer < ApplicationMailer
|
|||
def send_notification
|
||||
@service = @dossier.procedure.service
|
||||
@logo_url = attach_logo(@dossier.procedure)
|
||||
@rendered_template = sanitize(@body)
|
||||
@rendered_template = sanitize(@body, scrubber: Sanitizers::MailScrubber.new)
|
||||
attachments[@attachment[:filename]] = @attachment[:content] if @attachment.present?
|
||||
|
||||
I18n.with_locale(@dossier.user_locale) do
|
||||
|
@ -50,6 +51,12 @@ class NotificationMailer < ApplicationMailer
|
|||
|
||||
private
|
||||
|
||||
def set_services_publics_plus
|
||||
return unless Dossier::TERMINE.include?(params[:state])
|
||||
|
||||
@services_publics_plus_url = ENV['SERVICES_PUBLICS_PLUS_URL'].presence
|
||||
end
|
||||
|
||||
def set_dossier
|
||||
@dossier = params[:dossier]
|
||||
|
||||
|
|
|
@ -84,8 +84,8 @@ class Avis < ApplicationRecord
|
|||
revoked_at.present?
|
||||
end
|
||||
|
||||
def revivable_by?(reviver)
|
||||
revokable_by?(reviver)
|
||||
def remindable_by?(reminder)
|
||||
revokable_by?(reminder)
|
||||
end
|
||||
|
||||
def revokable_by?(revocator)
|
||||
|
@ -101,4 +101,9 @@ class Avis < ApplicationRecord
|
|||
destroy!
|
||||
end
|
||||
end
|
||||
|
||||
def remind_by!(revocator)
|
||||
return false if !remindable_by?(revocator) || answer.present?
|
||||
update!(reminded_at: Time.zone.now)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,7 +95,7 @@ class BatchOperation < ApplicationRecord
|
|||
def track_processed_dossier(success, dossier)
|
||||
dossiers.delete(dossier)
|
||||
touch(:run_at) if called_for_first_time?
|
||||
touch(:finished_at) if called_for_last_time?(dossier)
|
||||
touch(:finished_at)
|
||||
|
||||
if success
|
||||
dossier_operation(dossier).done!
|
||||
|
@ -124,10 +124,6 @@ class BatchOperation < ApplicationRecord
|
|||
run_at.nil?
|
||||
end
|
||||
|
||||
def called_for_last_time?(dossier_to_ignore)
|
||||
dossiers.count.zero?
|
||||
end
|
||||
|
||||
def total_count
|
||||
dossier_operations.size
|
||||
end
|
||||
|
@ -144,6 +140,10 @@ class BatchOperation < ApplicationRecord
|
|||
dossier_operations.error.present?
|
||||
end
|
||||
|
||||
def finished_at
|
||||
dossiers.empty? ? super : nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dossier_operation(dossier)
|
||||
|
|
|
@ -36,10 +36,6 @@ class Champs::DatetimeChamp < Champ
|
|||
value.present? ? I18n.l(Time.zone.parse(value)) : ""
|
||||
end
|
||||
|
||||
def html_label?
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def convert_to_iso8601
|
||||
|
|
|
@ -33,7 +33,7 @@ class Champs::RepetitionChamp < Champ
|
|||
transaction do
|
||||
row_id = ULID.generate
|
||||
revision.children_of(type_de_champ).each do |type_de_champ|
|
||||
added_champs << type_de_champ.champ.build(row_id:)
|
||||
added_champs << type_de_champ.build_champ(row_id:)
|
||||
end
|
||||
self.champs << added_champs
|
||||
end
|
||||
|
|
|
@ -14,12 +14,12 @@ module DossierRebaseConcern
|
|||
end
|
||||
|
||||
def can_rebase?
|
||||
revision != procedure.published_revision &&
|
||||
procedure.published_revision.present? && revision != procedure.published_revision &&
|
||||
(brouillon? || accepted_en_construction_changes? || accepted_en_instruction_changes?)
|
||||
end
|
||||
|
||||
def pending_changes
|
||||
revision.compare(procedure.published_revision)
|
||||
procedure.published_revision.present? ? revision.compare(procedure.published_revision) : []
|
||||
end
|
||||
|
||||
def can_rebase_mandatory_change?(stable_id)
|
||||
|
@ -58,25 +58,30 @@ module DossierRebaseConcern
|
|||
.joins(:type_de_champ)
|
||||
.group_by(&:stable_id)
|
||||
.transform_values { Champ.where(id: _1) }
|
||||
.tap { _1.default = Champ.none }
|
||||
|
||||
# add champ
|
||||
changes_by_op[:add]
|
||||
.each { add_new_champs_for_revision(target_coordinates_by_stable_id[_1.stable_id]) }
|
||||
.map { target_coordinates_by_stable_id[_1.stable_id] }
|
||||
# add parent champs first so we can then add children
|
||||
.sort_by { _1.child? ? 1 : 0 }
|
||||
.each { add_new_champs_for_revision(_1) }
|
||||
|
||||
# remove champ
|
||||
changes_by_op[:remove]
|
||||
.each { champs_by_stable_id[_1.stable_id].destroy_all }
|
||||
changes_by_op[:remove].each { champs_by_stable_id[_1.stable_id].destroy_all }
|
||||
|
||||
# update champ
|
||||
if brouillon?
|
||||
changes_by_op[:update]
|
||||
.each { apply(_1, champs_by_stable_id[_1.stable_id]) }
|
||||
changes_by_op[:update].each { apply(_1, champs_by_stable_id[_1.stable_id]) }
|
||||
end
|
||||
|
||||
# due to repetition tdc clone on update or erase
|
||||
# we must reassign tdc to the latest version
|
||||
champs_by_stable_id
|
||||
.filter_map { |stable_id, champs| [target_coordinates_by_stable_id[stable_id].type_de_champ_id, champs] if champs.present? }
|
||||
.each { |type_de_champ_id, champs| champs.update_all(type_de_champ_id:) }
|
||||
champs_by_stable_id.each do |stable_id, champs|
|
||||
if target_coordinates_by_stable_id[stable_id].present? && champs.present?
|
||||
champs.update_all(type_de_champ_id: target_coordinates_by_stable_id[stable_id].type_de_champ_id)
|
||||
end
|
||||
end
|
||||
|
||||
# update dossier revision
|
||||
update_column(:revision_id, target_revision.id)
|
||||
|
@ -120,13 +125,14 @@ module DossierRebaseConcern
|
|||
if target_coordinate.child?
|
||||
# If this type de champ is a child, we create a new champ for each row of the parent
|
||||
parent_stable_id = target_coordinate.parent.stable_id
|
||||
champs_repetition = champs
|
||||
.includes(:champs, :type_de_champ)
|
||||
.where(type_de_champ: { stable_id: parent_stable_id })
|
||||
|
||||
champs_repetition.each do |champ_repetition|
|
||||
champ_repetition.champs.map(&:row_id).uniq.each do |row_id|
|
||||
create_champ(target_coordinate, champ_repetition, row_id:)
|
||||
champs.filter { _1.stable_id == parent_stable_id }.each do |champ_repetition|
|
||||
if champ_repetition.champs.present?
|
||||
champ_repetition.champs.map(&:row_id).uniq.each do |row_id|
|
||||
create_champ(target_coordinate, champ_repetition, row_id:)
|
||||
end
|
||||
elsif champ_repetition.mandatory?
|
||||
create_champ(target_coordinate, champ_repetition, row_id: ULID.generate)
|
||||
end
|
||||
end
|
||||
else
|
||||
|
@ -135,10 +141,9 @@ module DossierRebaseConcern
|
|||
end
|
||||
|
||||
def create_champ(target_coordinate, parent, row_id: nil)
|
||||
params = { revision: target_coordinate.revision, rebased_at: Time.zone.now, row_id: }.compact
|
||||
champ = target_coordinate
|
||||
.type_de_champ
|
||||
.build_champ(params)
|
||||
.build_champ(rebased_at: Time.zone.now, row_id:)
|
||||
parent.champs << champ
|
||||
end
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ class Dossier < ApplicationRecord
|
|||
belongs_to :user, optional: true
|
||||
belongs_to :parent_dossier, class_name: 'Dossier', optional: true
|
||||
belongs_to :batch_operation, optional: true
|
||||
has_many :dossier_batch_operations
|
||||
has_many :dossier_batch_operations, dependent: :destroy
|
||||
has_many :batch_operations, through: :dossier_batch_operations
|
||||
has_one :france_connect_information, through: :user
|
||||
|
||||
|
@ -507,6 +507,12 @@ class Dossier < ApplicationRecord
|
|||
revision.build_champs_private.each do |champ|
|
||||
champs_private << champ
|
||||
end
|
||||
champs_public.filter { _1.repetition? && _1.mandatory? }.each do |champ|
|
||||
champ.add_row(revision)
|
||||
end
|
||||
champs_private.filter(&:repetition?).each do |champ|
|
||||
champ.add_row(revision)
|
||||
end
|
||||
end
|
||||
|
||||
def build_default_individual
|
||||
|
|
|
@ -10,15 +10,21 @@
|
|||
# to :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# message_id :string
|
||||
#
|
||||
class EmailEvent < ApplicationRecord
|
||||
RETENTION_DURATION = 1.month
|
||||
|
||||
enum status: {
|
||||
dispatched: 'dispatched',
|
||||
dispatch_error: 'dispatch_error'
|
||||
}
|
||||
|
||||
scope :dolist, -> { where(method: 'dolist') }
|
||||
scope :dolist, -> { dolist_smtp.or(dolist_api) }
|
||||
scope :dolist_smtp, -> { where(method: 'dolist_smtp') }
|
||||
scope :dolist_api, -> { where(method: 'dolist_api') }
|
||||
scope :sendinblue, -> { where(method: 'sendinblue') }
|
||||
scope :outdated, -> { where("created_at < ?", RETENTION_DURATION.ago) }
|
||||
|
||||
class << self
|
||||
def create_from_message!(message, status:)
|
||||
|
@ -30,6 +36,7 @@ class EmailEvent < ApplicationRecord
|
|||
subject: message.subject || "",
|
||||
processed_at: message.date,
|
||||
method: ActionMailer::Base.delivery_methods.key(message.delivery_method.class),
|
||||
message_id: message.message_id,
|
||||
status:
|
||||
)
|
||||
rescue StandardError => error
|
||||
|
|
|
@ -163,6 +163,10 @@ class Export < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def procedure
|
||||
groupe_instructeurs.first.procedure
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_snapshot!
|
||||
|
@ -204,8 +208,4 @@ class Export < ApplicationRecord
|
|||
service.to_geo_json
|
||||
end
|
||||
end
|
||||
|
||||
def procedure
|
||||
groupe_instructeurs.first.procedure
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
# duree_conservation_dossiers_dans_ds :integer
|
||||
# duree_conservation_etendue_par_ds :boolean default(FALSE)
|
||||
# encrypted_api_particulier_token :string
|
||||
# estimated_duration_visible :boolean default(TRUE), not null
|
||||
# euro_flag :boolean default(FALSE)
|
||||
# experts_require_administrateur_invitation :boolean default(FALSE)
|
||||
# for_individual :boolean default(FALSE)
|
||||
|
@ -822,9 +823,13 @@ class Procedure < ApplicationRecord
|
|||
published_at || created_at
|
||||
end
|
||||
|
||||
def publiee_or_close?
|
||||
publiee? || close?
|
||||
end
|
||||
|
||||
def self.tags
|
||||
unnest = Arel::Nodes::NamedFunction.new('UNNEST', [self.arel_table[:tags]])
|
||||
query = self.select(unnest.as('tags')).publiees_ou_closes.distinct.order('tags')
|
||||
query = self.select(unnest.as('tags')).publiees.distinct.order('tags')
|
||||
self.connection.query(query.to_sql).flatten
|
||||
end
|
||||
|
||||
|
|
|
@ -34,12 +34,12 @@ class ProcedureRevision < ApplicationRecord
|
|||
|
||||
def build_champs_public
|
||||
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
|
||||
types_de_champ_public.reload.map { |tdc| tdc.build_champ(revision: self) }
|
||||
types_de_champ_public.reload.map(&:build_champ)
|
||||
end
|
||||
|
||||
def build_champs_private
|
||||
# reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc
|
||||
types_de_champ_private.reload.map { |tdc| tdc.build_champ(revision: self) }
|
||||
types_de_champ_private.reload.map(&:build_champ)
|
||||
end
|
||||
|
||||
def add_type_de_champ(params)
|
||||
|
@ -272,7 +272,7 @@ class ProcedureRevision < ApplicationRecord
|
|||
.map { [from_h[_1], to_h[_1]] }
|
||||
.flat_map { |from, to| compare_type_de_champ(from.type_de_champ, to.type_de_champ, from_coordinates, to_coordinates) }
|
||||
|
||||
(removed + added + moved + changed).sort_by { _1.op == :remove ? from_sids[_1.stable_id] : to_sids[_1.stable_id] }
|
||||
(removed + added + moved + changed).sort_by { _1.op == :remove ? from_sids.index(_1.stable_id) : to_sids.index(_1.stable_id) }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -178,12 +178,10 @@ class TypeDeChamp < ApplicationRecord
|
|||
|
||||
has_many :champ, inverse_of: :type_de_champ, dependent: :destroy do
|
||||
def build(params = {})
|
||||
params.delete(:revision)
|
||||
super(params.merge(proxy_association.owner.params_for_champ))
|
||||
end
|
||||
|
||||
def create(params = {})
|
||||
params.delete(:revision)
|
||||
super(params.merge(proxy_association.owner.params_for_champ))
|
||||
end
|
||||
end
|
||||
|
@ -231,8 +229,8 @@ class TypeDeChamp < ApplicationRecord
|
|||
}
|
||||
end
|
||||
|
||||
def build_champ(params)
|
||||
dynamic_type.build_champ(params)
|
||||
def build_champ(params = {})
|
||||
champ.build(params)
|
||||
end
|
||||
|
||||
def check_mandatory
|
||||
|
|
|
@ -1,11 +1,4 @@
|
|||
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||
def build_champ(params)
|
||||
revision = params[:revision]
|
||||
champ = super
|
||||
champ.add_row(revision)
|
||||
champ
|
||||
end
|
||||
|
||||
def estimated_fill_duration(revision)
|
||||
estimated_rows_in_repetition = 2.5
|
||||
|
||||
|
|
|
@ -51,10 +51,6 @@ class TypesDeChamp::TypeDeChampBase
|
|||
(words / READ_WORDS_PER_SECOND).round.seconds
|
||||
end
|
||||
|
||||
def build_champ(params)
|
||||
@type_de_champ.champ.build(params)
|
||||
end
|
||||
|
||||
def filter_to_human(filter_value)
|
||||
filter_value
|
||||
end
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
= f.submit t('.button.add_group'), class: "button primary send"
|
||||
|
||||
- csv_max_size = Administrateurs::GroupeInstructeursController::CSV_MAX_SIZE
|
||||
- if procedure.publiee?
|
||||
- if procedure.publiee_or_close?
|
||||
= form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do
|
||||
= label_tag t('.csv_import.title')
|
||||
%p.notice
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
%td= procedure.administrateurs.count
|
||||
%td= t procedure.aasm_state, scope: 'activerecord.attributes.procedure.aasm_state'
|
||||
%td= l(procedure.published_at, format: :message_date_without_time)
|
||||
%td= link_to('Cloner', admin_procedure_clone_path(procedure.id, from_new_from_existing: true), 'data-method' => :put, class: 'fr-btn fr-btn--tertiary fr-btn--sm')
|
||||
|
||||
|
||||
- if show_detail
|
||||
%tr.procedure{ id: "procedure_detail_#{procedure.id}" }
|
||||
|
|
|
@ -100,18 +100,18 @@
|
|||
%p.explication
|
||||
Si votre démarche s’adresse indifféremment à une personne morale ou un particulier, choisissez l'option « Particuliers ». Vous pourrez ajouter un champ SIRET directement dans le formulaire.
|
||||
|
||||
%h3.header-subsection Ajouter des tags
|
||||
%p.explication Les tags sont des mots ou des expressions que vous attribuez aux démarches pour décrire leur contenu et pour les retrouver. Les tags sont partagés avec la communauté, ce qui vous permet de voir les tags attribués aux démarches créées par les autres administrateurs.
|
||||
= hidden_field_tag 'procedure[tags]', nil
|
||||
= react_component("ComboMultiple",
|
||||
options: Procedure.tags,
|
||||
selected: @procedure.tags,
|
||||
disabled: [],
|
||||
label: 'Tags',
|
||||
group: '.procedure-form__column--form',
|
||||
name: 'tags',
|
||||
describedby: 'procedure-tags',
|
||||
acceptNewValues: true)
|
||||
%h3.header-subsection Ajouter des tags
|
||||
%p.explication Les tags sont des mots ou des expressions que vous attribuez aux démarches pour décrire leur contenu et pour les retrouver. Les tags sont partagés avec la communauté, ce qui vous permet de voir les tags attribués aux démarches créées par les autres administrateurs.
|
||||
= hidden_field_tag 'procedure[tags]', nil
|
||||
= react_component("ComboMultiple",
|
||||
options: Procedure.tags,
|
||||
selected: @procedure.tags,
|
||||
disabled: [],
|
||||
label: 'Tags',
|
||||
group: '.procedure-form__column--form',
|
||||
name: 'tags',
|
||||
describedby: 'procedure-tags',
|
||||
acceptNewValues: true)
|
||||
|
||||
%details.procedure-form__options-details
|
||||
%summary.procedure-form__options-summary
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
.fr-table.fr-table--bordered
|
||||
%table#all-demarches
|
||||
%caption
|
||||
= "#{@procedures.total_count} démarches"
|
||||
= "#{@procedures.total_count} #{t('pluralize.procedures', count: @procedures.total_count)}"
|
||||
%span.hidden.spinner{ 'aria-hidden': 'true', 'data-turbo-target': 'spinner' }
|
||||
- if @filter.libelle
|
||||
.selected-query.fr-mb-2w
|
||||
|
@ -40,10 +40,11 @@
|
|||
%tr
|
||||
%th{ scope: 'col' }
|
||||
%th{ scope: 'col' } Démarche
|
||||
%th{ scope: 'col' } N°
|
||||
%th{ scope: 'col' } №
|
||||
%th{ scope: 'col' } Administrateurs
|
||||
%th{ scope: 'col' } Statut
|
||||
%th{ scope: 'col' } Date
|
||||
%th{ scope: 'col' } Action
|
||||
%tbody{ 'data-turbo': 'true' }
|
||||
- @procedures.each do |procedure|
|
||||
= render partial: 'detail', locals: { procedure: procedure, show_detail: false }
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
= link_to "Nouvelle Démarche", new_from_existing_admin_procedures_path, id: 'new-procedure', class: 'fr-btn'
|
||||
.fr-container
|
||||
|
||||
%nav.tabs
|
||||
%nav.tabs{ role: 'navigation', 'aria-label': t('views.users.dossiers.secondary_menu') }
|
||||
%ul
|
||||
= tab_item(t('pluralize.published', count: @procedures_publiees.count), admin_procedures_path(statut: 'publiees'), active: @statut == 'publiees', badge: number_with_html_delimiter(@procedures_publiees_count))
|
||||
= tab_item('En test', admin_procedures_path(statut: 'brouillons'), active: @statut == 'brouillons', badge: number_with_html_delimiter(@procedures_draft_count))
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
|
||||
- if @champ_id
|
||||
= turbo_stream.show "attachment-multiple-empty-#{@champ_id}"
|
||||
= turbo_stream.focus_all "#attachment-multiple-empty-#{@champ_id} input"
|
||||
|
||||
= turbo_stream.show_all ".attachment-input-#{@attachment.id}"
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
= turbo_stream.replace @champ.input_group_id do
|
||||
= render EditableChamp::EditableChampComponent.new champ: @champ, form: form
|
||||
|
||||
- @champ.piece_justificative_file.attachments.each do |attachment|
|
||||
= turbo_stream.focus_all "button[data-toggle-target=\".attachment-input-#{attachment.id}\"]"
|
||||
- last_attached_file = @champ.piece_justificative_file.attachments.last
|
||||
- if last_attached_file
|
||||
= turbo_stream.focus_all "#persisted_row_attachment_#{last_attached_file.id} .attachment-filename a"
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
= fields_for @champ.input_name, @champ do |form|
|
||||
= turbo_stream.append dom_id(@champ, :rows), render(EditableChamp::RepetitionRowComponent.new(form: form, champ: @champ, row: @champs))
|
||||
|
||||
- if @champs.present?
|
||||
= fields_for @champ.input_name, @champ do |form|
|
||||
= turbo_stream.append dom_id(@champ, :rows), render(EditableChamp::RepetitionRowComponent.new(form: form, champ: @champ, row: @champs))
|
||||
|
|
|
@ -16,15 +16,18 @@
|
|||
- drafts = dossiers.merge(Dossier.state_brouillon)
|
||||
- not_drafts = dossiers.merge(Dossier.state_not_brouillon)
|
||||
|
||||
- if dossiers.empty?
|
||||
= link_to t('views.commencer.show.start_procedure'), url_for_new_dossier(@revision), class: 'fr-btn fr-btn--lg fr-my-2w'
|
||||
|
||||
- elsif @prefilled_dossier
|
||||
- if @prefilled_dossier
|
||||
%h2.huge-title= t('views.commencer.show.prefilled_draft')
|
||||
%p
|
||||
= t('views.commencer.show.prefilled_draft_detail_html', time_ago: time_ago_in_words(@prefilled_dossier.created_at), procedure: @prefilled_dossier.procedure.libelle)
|
||||
= link_to t('views.commencer.show.continue_file'), brouillon_dossier_path(@prefilled_dossier), class: 'fr-btn fr-btn--lg fr-my-2w'
|
||||
= link_to t('views.commencer.show.start_new_file'), url_for_new_dossier(@revision), class: 'fr-btn fr-btn--lg fr-btn--secondary fr-my-2w'
|
||||
%p= t('views.commencer.show.prefilled_draft_detail_html', time_ago: time_ago_in_words(@prefilled_dossier.created_at), procedure: @procedure.libelle)
|
||||
= link_to t('views.commencer.show.go_to_prefilled_file'), brouillon_dossier_path(@prefilled_dossier), class: 'fr-btn fr-btn--lg fr-my-2w'
|
||||
|
||||
- elsif stored_query_params?
|
||||
%h2.huge-title= t('views.commencer.show.prefilled_draft')
|
||||
%p= t('views.commencer.show.prefill_dossier_detail_html', procedure: @procedure.libelle)
|
||||
= link_to t('views.commencer.show.go_to_prefilled_file'), url_for_new_dossier(@revision), class: 'fr-btn fr-btn--lg fr-my-2w'
|
||||
|
||||
- elsif dossiers.empty?
|
||||
= link_to t('views.commencer.show.start_procedure'), url_for_new_dossier(@revision), class: 'fr-btn fr-btn--lg fr-my-2w'
|
||||
|
||||
- elsif drafts.size == 1 && not_drafts.empty?
|
||||
- dossier = drafts.first
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
= round_button 'Changer mon mot de passe', edit_password_url(@resource, reset_password_token: @token), :primary
|
||||
|
||||
%p Cet email invalide les emails similaires que vous avez pu demander précédemment.
|
||||
|
||||
%p
|
||||
Si vous n’avez pas effectué une telle demande, merci d’ignorer cet email. Votre mot de passe ne sera pas changé.
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
- content_for(:title, "Instructeurs de la démarche #{@procedure.libelle}")
|
||||
|
||||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [link_to(@procedure.libelle, instructeur_procedure_path(@procedure)), 'Instructeurs'] }
|
||||
locals: { steps: [[@procedure.libelle, instructeur_procedure_path(@procedure)],
|
||||
['Instructeurs']] }
|
||||
|
||||
.container.groupe-instructeur
|
||||
%h1
|
||||
|
|
|
@ -99,9 +99,10 @@
|
|||
%div{ data: batch_operation_component.render? ? { controller: 'batch-operation' } : {} }
|
||||
.flex.align-center.fr-mt-2w
|
||||
%span.fr-h6.fr-mb-0.fr-mr-2w
|
||||
= t('views.instructeurs.dossiers.dossiers_count', count: @dossiers_count)
|
||||
= page_entries_info @filtered_sorted_paginated_ids
|
||||
|
||||
= render batch_operation_component
|
||||
|
||||
.fr-table.fr-table--bordered
|
||||
%table.table.dossiers-table.hoverable
|
||||
%thead
|
||||
|
@ -124,6 +125,8 @@
|
|||
%tr
|
||||
|
||||
%tbody
|
||||
= render Dossiers::BatchSelectMoreComponent.new(dossiers_count: @dossiers_count, filtered_sorted_ids: @filtered_sorted_ids)
|
||||
|
||||
- @projected_dossiers.each do |p|
|
||||
- path = instructeur_dossier_path(@procedure, p.dossier_id)
|
||||
%tr{ class: [p.hidden_by_user_at.present? && "file-hidden-by-user"] }
|
||||
|
|
|
@ -36,10 +36,13 @@
|
|||
%span.fr-text--xs.fr-text-mention--grey
|
||||
= t('en_attente', scope: 'views.shared.avis')
|
||||
|
|
||||
%span= link_to(t('revive', scope: 'helpers.label'), revive_instructeur_avis_path(avis.procedure, avis), data: { confirm: t('revive', scope: 'helpers.confirmation', email: avis.expert.email) })
|
||||
%span= link_to(t('remind', scope: 'helpers.label'), remind_instructeur_avis_path(avis.procedure, avis), data: { confirm: t('remind', scope: 'helpers.confirmation', email: avis.expert.email) })
|
||||
- if avis.revokable_by?(current_instructeur)
|
||||
|
|
||||
= link_to(t('revoke', scope: 'helpers.label'), revoquer_instructeur_avis_path(avis.procedure, avis), data: { confirm: t('revoke', scope: 'helpers.confirmation', email: avis.expert.email) }, method: :patch)
|
||||
- if avis.reminded_at
|
||||
%span.date.fr-text--xs.fr-text-mention--grey{ class: highlight_if_unseen_class(avis_seen_at, avis.reminded_at) }
|
||||
= t('relance_effectuee_le', scope: 'views.shared.avis', date: l(avis.reminded_at, format: '%d/%m/%y à %H:%M'))
|
||||
- if avis.introduction_file.attached?
|
||||
= render Attachment::ShowComponent.new(attachment: avis.introduction_file.attachment)
|
||||
.answer-body.mb-3
|
||||
|
|
|
@ -11,4 +11,4 @@
|
|||
- else
|
||||
= t('views.invites.dropdown.invite_to_edit')
|
||||
- menu.with_form do
|
||||
= render partial: "invites/form", locals: { dossier: dossier, invites: invites }
|
||||
= render partial: "invites/form", locals: { dossier: dossier, invites: invites, morphing: morphing }
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
#invites-form
|
||||
- if invites.present?
|
||||
%h4= t('views.invites.form.invite_to_participate')
|
||||
%ul
|
||||
- invites.each do |invite|
|
||||
%li
|
||||
= invite.email
|
||||
%small{ 'data-turbo': 'true' }
|
||||
= link_to t('views.invites.form.withdraw_permission'), invite_path(invite), data: { turbo_method: :delete, turbo_confirm: t('views.invites.form.want_to_withdraw_permission') }
|
||||
#invite-list{ morphing ? { tabindex: "-1" } : {} }
|
||||
%h4= t('views.invites.form.invite_to_participate')
|
||||
%ul
|
||||
- invites.each do |invite|
|
||||
%li
|
||||
= invite.email
|
||||
%small{ 'data-turbo': 'true' }
|
||||
= link_to t('views.invites.form.withdraw_permission'), invite_path(invite), data: { turbo_method: :delete, turbo_confirm: t('views.invites.form.want_to_withdraw_permission') }
|
||||
|
||||
%p= t('views.invites.form.edit_dossier')
|
||||
- if dossier.brouillon?
|
||||
%p= t('views.invites.form.submit_dossier_yourself')
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
= turbo_stream.replace_all '.invite-user-action', partial: 'invites/dropdown', locals: { dossier: @dossier }
|
||||
= turbo_stream.focus 'invite_email'
|
||||
= turbo_stream.replace_all '.invite-user-action', partial: 'invites/dropdown', locals: { dossier: @dossier, morphing: true }
|
||||
= turbo_stream.focus 'invite-list'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- if @dossier.present?
|
||||
= turbo_stream.replace_all '.invite-user-action', partial: 'invites/dropdown', locals: { dossier: @dossier }
|
||||
= turbo_stream.replace_all '.invite-user-action', partial: 'invites/dropdown', locals: { dossier: @dossier, morphing: true }
|
||||
- if @dossier.invites.empty?
|
||||
= turbo_stream.focus 'invite_email'
|
||||
= turbo_stream.focus 'invite-list'
|
||||
- else
|
||||
= turbo_stream.focus_all '#invites-form ul a:first-child'
|
||||
= turbo_stream.focus 'invite_email'
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
= render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_dossiers_path }
|
||||
|
||||
- has_header = [is_instructeur_context, is_expert_context, is_user_context]
|
||||
#burger-menu.fr-header__menu.fr-modal{ "aria-labelledby" => "burger_button" }
|
||||
#burger-menu.fr-header__menu.fr-modal{ "aria-label" => t('layouts.header.label_modal') }
|
||||
.fr-container
|
||||
%button#burger_button.fr-btn--close.fr-btn{ "aria-controls" => "burger-menu", :title => t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header])
|
||||
.fr-header__menu-links
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
#search-modal.fr-header__search.fr-modal
|
||||
#search-modal.fr-header__search.fr-modal{ "aria-label": t('views.users.dossiers.search.search_file') }
|
||||
.fr-container.fr-container-lg--fluid
|
||||
%button.fr-btn--close.fr-btn{ "aria-controls" => "search-modal", :title => t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header])
|
||||
#search-473.fr-search-bar
|
||||
#search-473.fr-search-bar.fr-search-bar--lg
|
||||
= form_tag "#{search_endpoint}", method: :get, :role => "search", class: "flex width-100" do
|
||||
= label_tag "q", t('views.users.dossiers.search.search_file'), class: 'fr-label'
|
||||
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.placeholder'), class: "fr-input"
|
||||
%button.fr-btn{ title: t('views.users.dossiers.search.search_file') }
|
||||
= t('views.users.dossiers.search.search_file')
|
||||
= text_field_tag "q", "#{@search_terms if @search_terms.present?}", placeholder: t('views.users.dossiers.search.search_file'), class: "fr-input"
|
||||
%button.fr-btn
|
||||
= t('views.users.dossiers.search.simple')
|
||||
|
|
16
app/views/layouts/mailers/_services_publics_plus.html.haml
Normal file
16
app/views/layouts/mailers/_services_publics_plus.html.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
= vertical_margin(20)
|
||||
|
||||
%div{ style: 'background-color: #F5F5FE; padding: 20px;' }
|
||||
%table{ width: "100%", border: "0", cellspacing: "0", cellpadding: "0" }
|
||||
%tr
|
||||
%td{ width: "70%", valign: "top", align: "center" }
|
||||
%p{ style: 'margin: 0' }
|
||||
J’aide les services publics à s’améliorer :
|
||||
%br
|
||||
= link_to 'Je donne mon avis avec Services Publics +', @services_publics_plus_url, target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
%td{ width: "30%" }
|
||||
= link_to @services_publics_plus_url, target: '_blank', rel: 'noopener noreferrer' do
|
||||
= image_tag('mailer/logo-services-plus.png', height: 39, width: 121, style: 'display:block; vertical-align: middle', alt: "Services Publics +")
|
||||
|
||||
= vertical_margin(20)
|
|
@ -108,6 +108,7 @@
|
|||
<div class="mj-column-per-100 outlook-group-fix" style="vertical-align:top;display:inline-block;direction:ltr;font-size:13px;text-align:left;width:100%;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" border="0">
|
||||
<tbody>
|
||||
<%= yield(:procedure_logo) %>
|
||||
<tr>
|
||||
<td style="word-wrap:break-word;font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-bottom:0px;" align="left">
|
||||
<div class="" style="cursor:auto;color:#55575d;font-family:Helvetica, Arial, sans-serif;font-size:13px;line-height:22px;text-align:left;">
|
||||
|
|
|
@ -108,6 +108,11 @@ https://www.demarches-simplifiees.fr/users/password/new
|
|||
|
||||
Cordialement</pre>
|
||||
<% end %>
|
||||
|
||||
|
||||
<p><strong>Mot de passe perdu ?</strong> Vous pouvez <%= link_to('renvoyer l’email de réinitialisation', [:resend_reset_password_instructions, namespace, :user], method: :post, class: 'button') %>
|
||||
<small>Attention au téléscopage: cet email invalide les liens des emails similaires précédents.</small></p>
|
||||
|
||||
<p>
|
||||
<strong>Compte <a href="https://app-smtp.sendinblue.com/block">bloqué</a> chez Sendinblue ?</strong>
|
||||
Vous pouvez le <%= link_to('débloquer', manager_user_unblock_email_path(@user), method: :put, class: 'button') %>
|
||||
|
|
|
@ -6,5 +6,8 @@
|
|||
- if @actions.present?
|
||||
= render 'notification_mailer/actions', actions: @actions, dossier: @dossier
|
||||
|
||||
- if @services_publics_plus_url.present?
|
||||
= render 'layouts/mailers/services_publics_plus'
|
||||
|
||||
- content_for :footer do
|
||||
= render 'layouts/mailers/service_footer', service: @service, dossier: @dossier
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
%h1.procedure-title
|
||||
= procedure.libelle
|
||||
|
||||
- if procedure.persisted?
|
||||
- if procedure.persisted? && procedure.estimated_duration_visible?
|
||||
%p.procedure-configuration.procedure-configuration--fill-duration
|
||||
%span.icon.clock
|
||||
= t('shared.procedure_description.estimated_fill_duration', estimated_minutes: estimated_fill_duration_minutes(procedure))
|
||||
|
@ -23,5 +23,5 @@
|
|||
|
||||
.procedure-description
|
||||
.procedure-description-body.read-more-enabled.read-more-collapsed
|
||||
= h string_to_html(procedure.description)
|
||||
= h string_to_html(procedure.description, allow_a: true)
|
||||
= button_tag "Afficher la description complète", class: 'button read-more-button'
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
= render(partial: 'users/dossiers/procedure_removed_banner', locals: { dossier: dossier })
|
||||
- elsif current_user.owns?(dossier)
|
||||
.header-actions
|
||||
= render partial: 'invites/dropdown', locals: { dossier: dossier }
|
||||
= render partial: 'invites/dropdown', locals: { dossier: dossier, morphing: false }
|
||||
|
||||
- unless dossier.read_only?
|
||||
= render partial: 'users/dossiers/identity_dropdown', locals: { dossier: dossier }
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
- else
|
||||
%h1.page-title= t('views.users.dossiers.index.dossiers')
|
||||
%nav.tabs
|
||||
%nav.tabs{ role: 'navigation', 'aria-label': t('views.users.dossiers.secondary_menu') }
|
||||
%ul
|
||||
- if @user_dossiers.present?
|
||||
= tab_item(t('pluralize.en_cours', count: @user_dossiers.count),
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
= render(partial: 'users/dossiers/procedure_removed_banner', locals: { dossier: dossier })
|
||||
- elsif current_user.owns?(dossier)
|
||||
.header-actions
|
||||
= render partial: 'invites/dropdown', locals: { dossier: dossier }
|
||||
= render partial: 'invites/dropdown', locals: { dossier: dossier, morphing: false }
|
||||
- if dossier.can_be_updated_by_user? && !current_page?(modifier_dossier_path(dossier))
|
||||
= link_to t('views.users.dossiers.show.header.edit_dossier'), modifier_dossier_path(dossier), class: 'fr-btn fr-btn-sm',
|
||||
title: { label: t('views.users.dossiers.show.header.edit_dossier_title') }
|
||||
|
|
|
@ -41,7 +41,7 @@ module TPS
|
|||
config.assets.precompile += ['.woff']
|
||||
|
||||
default_allowed_tags = ActionView::Base.sanitized_allowed_tags
|
||||
config.action_view.sanitized_allowed_tags = default_allowed_tags + ['u'] - ['img']
|
||||
config.action_view.sanitized_allowed_tags = default_allowed_tags + ['u'] - ['img', 'a']
|
||||
|
||||
# ActionDispatch's IP spoofing detection is quite limited, and often rejects
|
||||
# legitimate requests from misconfigured proxies (such as mobile telcos).
|
||||
|
|
|
@ -163,3 +163,6 @@ DOLIST_LOGIN_URL="https://clientpreprod.dolist.net"
|
|||
SUPPORT_WEBHOOK_URL=""
|
||||
# rappel web de sendinblue
|
||||
SIB_WEBHOOK_URL=""
|
||||
|
||||
# ServicesPublics+ tracking url shown to user when dossier is terminated.
|
||||
SERVICES_PUBLICS_PLUS_URL=""
|
||||
|
|
|
@ -52,7 +52,7 @@ Rails.application.configure do
|
|||
|
||||
# Use the lowest log level to ensure availability of diagnostic information
|
||||
# when problems arise.
|
||||
config.log_level = :debug
|
||||
config.log_level = ENV["DS_ENV"] == "staging" ? :debug : :info
|
||||
|
||||
# Prepend all log lines with the following tags.
|
||||
# config.log_tags = [ :subdomain, :uuid ]
|
||||
|
@ -80,12 +80,13 @@ Rails.application.configure do
|
|||
else
|
||||
sendinblue_weigth = ENV.fetch('SENDINBLUE_BALANCING_VALUE') { 0 }.to_i
|
||||
dolist_weigth = ENV.fetch('DOLIST_BALANCING_VALUE') { 0 }.to_i
|
||||
|
||||
dolist_api_weight = ENV.fetch('DOLIST_API_BALANCING_VALUE') { 0 }.to_i
|
||||
ActionMailer::Base.add_delivery_method :balancer, BalancerDeliveryMethod
|
||||
config.action_mailer.balancer_settings = {
|
||||
sendinblue: sendinblue_weigth,
|
||||
dolist: dolist_weigth,
|
||||
mailjet: 100 - (sendinblue_weigth + dolist_weigth)
|
||||
dolist_smtp: dolist_weigth,
|
||||
dolist_api: dolist_api_weight,
|
||||
mailjet: 100 - (sendinblue_weigth + dolist_weigth + dolist_api_weight)
|
||||
}
|
||||
config.action_mailer.delivery_method = :balancer
|
||||
end
|
||||
|
|
|
@ -109,6 +109,7 @@ ignore_unused:
|
|||
- 'time.formats.default'
|
||||
- 'instructeurs.dossiers.filterable_state.*'
|
||||
- 'views.prefill_descriptions.edit.possible_values.*'
|
||||
- 'helpers.page_entries_info.*'
|
||||
# - '{devise,kaminari,will_paginate}.*'
|
||||
# - 'simple_form.{yes,no}'
|
||||
# - 'simple_form.{placeholders,hints,labels}.*'
|
||||
|
|
|
@ -5,17 +5,29 @@ ActiveSupport.on_load(:action_mailer) do
|
|||
mail.from(ENV['DOLIST_NO_REPLY_EMAIL'])
|
||||
mail.sender(ENV['DOLIST_NO_REPLY_EMAIL'])
|
||||
mail['X-ACCOUNT-ID'] = Rails.application.secrets.dolist[:account_id]
|
||||
|
||||
mail['X-Dolist-Sending-Type'] = 'TransactionalService' # send even if the target is not active
|
||||
|
||||
super(mail)
|
||||
end
|
||||
end
|
||||
|
||||
class ApiSender
|
||||
def initialize(mail); end
|
||||
|
||||
def deliver!(mail)
|
||||
response = Dolist::API.new.send_email(mail)
|
||||
|
||||
if response&.dig("Result")
|
||||
mail.message_id = response.dig("Result")
|
||||
else
|
||||
fail "DoList delivery error. Body: #{response}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActionMailer::Base.add_delivery_method :dolist, Dolist::SMTP
|
||||
|
||||
ActionMailer::Base.dolist_settings = {
|
||||
ActionMailer::Base.add_delivery_method :dolist_smtp, Dolist::SMTP
|
||||
ActionMailer::Base.dolist_smtp_settings = {
|
||||
user_name: Rails.application.secrets.dolist[:username],
|
||||
password: Rails.application.secrets.dolist[:password],
|
||||
address: 'smtp.dolist.net',
|
||||
|
@ -23,4 +35,6 @@ ActiveSupport.on_load(:action_mailer) do
|
|||
authentication: 'plain',
|
||||
enable_starttls_auto: true
|
||||
}
|
||||
|
||||
ActionMailer::Base.add_delivery_method :dolist_api, Dolist::ApiSender
|
||||
end
|
||||
|
|
|
@ -14,10 +14,13 @@ Rails.application.configure do
|
|||
user_email: event.payload[:user_email],
|
||||
user_roles: event.payload[:user_roles],
|
||||
user_agent: event.payload[:user_agent],
|
||||
graphql_operation: event.payload[:graphql_operation],
|
||||
graphql_query: event.payload[:graphql_query],
|
||||
graphql_variables: event.payload[:graphql_variables],
|
||||
browser: event.payload[:browser],
|
||||
browser_version: event.payload[:browser_version],
|
||||
platform: event.payload[:platform]
|
||||
platform: event.payload[:platform],
|
||||
client_ip: event.payload[:client_ip],
|
||||
request_id: event.payload[:request_id]
|
||||
}.compact
|
||||
end
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@ en:
|
|||
are_you_new: First time on %{app_name}?
|
||||
my_account: My account
|
||||
header:
|
||||
label_modal: "Burger menu"
|
||||
close_modal: 'Close'
|
||||
back: "Back"
|
||||
back_title: "Revenir sur le site de mon administration"
|
||||
|
@ -92,11 +93,13 @@ en:
|
|||
existing_dossiers: You already have files for this procedure
|
||||
show_dossiers: View my current files
|
||||
prefilled_draft: "You have a prefilled file"
|
||||
prefilled_draft_detail_html: "You prefilled a file for the \"%{procedure}\" procedure <strong>%{time_ago} ago</strong>"
|
||||
prefilled_draft_detail_html: "You are ready to continue a prefilled file for the \"%{procedure}\" procedure, started <strong>%{time_ago} ago</strong>."
|
||||
prefill_dossier_detail_html: "You are ready to continue a prefilled file for the \"%{procedure}\" procedure."
|
||||
already_draft: "You already started to fill a file"
|
||||
already_draft_detail_html: "You started to fill a file for the \"%{procedure}\" procedure <strong>%{time_ago} ago</strong>"
|
||||
already_not_draft: "You already submitted a file"
|
||||
already_not_draft_detail_html: "You submitted a file for the \"%{procedure}\" procedure <strong>%{time_ago} ago</strong>."
|
||||
go_to_prefilled_file: 'Continue to fill my prefilled file'
|
||||
continue_file: "Continue to fill my file"
|
||||
start_new_file: "Start a new file"
|
||||
show_my_submitted_file: 'Show my submitted file'
|
||||
|
@ -238,10 +241,6 @@ en:
|
|||
batch_operation:
|
||||
enabled: "Add this file to the selection for the bulk operation"
|
||||
disabled: "Impossible to add this file to the selection because it is already in a bulk operation"
|
||||
dossiers_count:
|
||||
zero: 0 file
|
||||
one: 1 file
|
||||
other: "%{count} files"
|
||||
personalize: Personalize the table
|
||||
show_deleted_dossiers: Show deleted files
|
||||
follow_file: Follow-up the file
|
||||
|
@ -318,8 +317,9 @@ en:
|
|||
demande:
|
||||
edit_dossier: "Edit file"
|
||||
search:
|
||||
placeholder: Search a file
|
||||
search_file: Search
|
||||
search_file: Search a file
|
||||
simple: Search
|
||||
secondary_menu: Secondary menu
|
||||
index:
|
||||
dossiers: "Files"
|
||||
dossiers_list:
|
||||
|
|
|
@ -59,6 +59,7 @@ fr:
|
|||
are_you_new: Vous êtes nouveau sur %{app_name} ?
|
||||
my_account: Mon compte
|
||||
header:
|
||||
label_modal: "Menu en-tête de page"
|
||||
close_modal: 'Fermer'
|
||||
back: "Revenir en arrière"
|
||||
back_title: "Revenir sur le site de mon administration"
|
||||
|
@ -83,11 +84,13 @@ fr:
|
|||
existing_dossiers: Vous avez déjà des dossiers pour cette démarche
|
||||
show_dossiers: Voir mes dossiers en cours
|
||||
prefilled_draft: "Vous avez un dossier prérempli"
|
||||
prefilled_draft_detail_html: "Il y a <strong>%{time_ago}</strong>, vous avez prérempli un dossier sur la démarche « %{procedure} »."
|
||||
prefilled_draft_detail_html: "Vous êtes prêt·e à poursuivre un dossier prérempli sur la démarche « %{procedure} », commencé il y a <strong>%{time_ago}</strong>."
|
||||
prefill_dossier_detail_html: "Vous êtes prêt·e à poursuivre un dossier prérempli sur la démarche « %{procedure} »."
|
||||
already_draft: "Vous avez déjà commencé à remplir un dossier"
|
||||
already_draft_detail_html: "Il y a <strong>%{time_ago}</strong>, vous avez commencé à remplir un dossier sur la démarche « %{procedure} »."
|
||||
already_not_draft: "Vous avez déjà déposé un dossier"
|
||||
already_not_draft_detail_html: "Il y a <strong>%{time_ago}</strong>, vous avez déposé un dossier sur la démarche « %{procedure} »."
|
||||
go_to_prefilled_file: 'Poursuivre mon dossier prérempli'
|
||||
continue_file: 'Continuer à remplir mon dossier'
|
||||
start_new_file: 'Commencer un nouveau dossier'
|
||||
show_my_submitted_file: 'Voir mon dossier déposé'
|
||||
|
@ -233,10 +236,6 @@ fr:
|
|||
batch_operation:
|
||||
enabled: "Ajouter ce dossier à la selection pour un traitement de masse"
|
||||
disabled: "Impossible d'ajouter ce dossier à la selection car il est déjà dans un traitement de masse"
|
||||
dossiers_count:
|
||||
zero: 0 dossier
|
||||
one: 1 dossier
|
||||
other: "%{count} dossiers"
|
||||
show_deleted_dossiers: Afficher les dossiers supprimés
|
||||
personalize: Personnaliser le tableau
|
||||
download: Télécharger un dossier
|
||||
|
@ -314,8 +313,9 @@ fr:
|
|||
demande:
|
||||
edit_dossier: "Modifier le dossier"
|
||||
search:
|
||||
placeholder: Rechercher un dossier
|
||||
search_file: Rechercher
|
||||
search_file: Rechercher un dossier
|
||||
simple: Rechercher
|
||||
secondary_menu: Menu secondaire
|
||||
index:
|
||||
dossiers: "Dossiers"
|
||||
dossiers_list:
|
||||
|
@ -545,6 +545,9 @@ fr:
|
|||
deleted:
|
||||
one: Supprimée
|
||||
other: Supprimées
|
||||
procedures:
|
||||
one: Démarche
|
||||
other: Démarches
|
||||
users:
|
||||
dossiers:
|
||||
test_procedure: "Ce dossier est déposé sur une démarche en test. Toute modification de la démarche par l’administrateur (ajout d’un champ, publication de la démarche...) entraînera sa suppression."
|
||||
|
|
20
config/locales/kaminari.en.yml
Normal file
20
config/locales/kaminari.en.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
en:
|
||||
helpers:
|
||||
page_entries_info:
|
||||
entry:
|
||||
zero: "file"
|
||||
one: "file"
|
||||
other: "files"
|
||||
more_pages:
|
||||
display_entries: "%{first} - %{last} <span class='fr-text--sm'>in %{total} %{entry_name}</span>"
|
||||
one_page:
|
||||
display_entries:
|
||||
one: "<b>%{count}</b> %{entry_name}"
|
||||
other: "%{count} <span class='fr-text--sm'>in %{count} %{entry_name}</span>"
|
||||
views:
|
||||
pagination:
|
||||
first: "« First"
|
||||
last: Last »
|
||||
next: Next ›
|
||||
previous: "‹ Prev"
|
||||
truncate: "…"
|
20
config/locales/kaminari.fr.yml
Normal file
20
config/locales/kaminari.fr.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
fr:
|
||||
helpers:
|
||||
page_entries_info:
|
||||
entry:
|
||||
zero: "dossier"
|
||||
one: "dossier"
|
||||
other: "dossiers"
|
||||
more_pages:
|
||||
display_entries: "%{first} - %{last} <span class='fr-text--sm'>sur %{total} %{entry_name}</span>"
|
||||
one_page:
|
||||
display_entries:
|
||||
one: "<b>%{count}</b> %{entry_name}"
|
||||
other: "%{count} <span class='fr-text--sm'>sur %{count} %{entry_name}</span>"
|
||||
views:
|
||||
pagination:
|
||||
first: "« Premier"
|
||||
last: Dernier »
|
||||
next: Suivant ›
|
||||
previous: "‹ Précédent"
|
||||
truncate: "…"
|
|
@ -13,9 +13,9 @@ fr:
|
|||
one: Inviter aussi l’expert sur le dossier lié n° %{ids}
|
||||
other: Inviter aussi l’expert sur les dossiers liés n° %{ids}
|
||||
revoke: Révoquer la demande d’avis
|
||||
revive: Relancer l’expert
|
||||
remind: Relancer l’expert
|
||||
hint:
|
||||
confidentiel: "Cet avis n’est pas affiché avec les autres experts consultés"
|
||||
confirmation:
|
||||
revoke: "Souhaitez-vous révoquer la demande d’avis à %{email} ?"
|
||||
revive: "Souhaitez-vous relancer %{email} ?"
|
||||
remind: "Souhaitez-vous relancer %{email} ?"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue