Merge branch 'demarches-simplifiees:main' into poc-self_hosted_runners

This commit is contained in:
kleph 2023-09-13 14:15:38 +02:00 committed by GitHub
commit 09d7f0b91a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 758 additions and 146 deletions

View file

@ -80,6 +80,9 @@ jobs:
- name: Setup the app runtime and dependencies - name: Setup the app runtime and dependencies
uses: ./.github/actions/ci-setup-rails uses: ./.github/actions/ci-setup-rails
- name: Install fonts pickable by ImageMagick
run: sudo apt-get install -y gsfonts
- name: Pre-compile assets - name: Pre-compile assets
uses: ./.github/actions/ci-setup-assets uses: ./.github/actions/ci-setup-assets

View file

@ -17,6 +17,7 @@ Vous souhaitez y apporter des changements ou des améliorations ? Lisez notre [
#### Tous environnements #### Tous environnements
- postgresql - postgresql
- imagemagick et gsfonts pour générer les filigranes sur les titres d'identité.
#### Développement #### Développement

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 889 B

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

View file

@ -62,11 +62,6 @@
width: 110px; width: 110px;
} }
.action-col {
text-align: right;
padding-left: $default-spacer;
padding-right: $default-spacer;
}
.follow-col { .follow-col {
width: 450px; width: 450px;

View file

@ -15,10 +15,6 @@ class Dossiers::NotifiedToggleComponent < ApplicationComponent
sorted_by_notifications? && order_desc? sorted_by_notifications? && order_desc?
end end
def icon_class_name
active? ? 'fr-fi-checkbox' : 'fr-fi-checkbox-blank'
end
def order_desc? def order_desc?
current_order == 'desc' current_order == 'desc'
end end

View file

@ -1,5 +1,6 @@
= form_tag update_sort_instructeur_procedure_path(procedure_id: @procedure.id, table: 'notifications', column: 'notifications', order: opposite_order), method: :get, data: { controller: 'autosubmit' } do = form_tag update_sort_instructeur_procedure_path(procedure_id: @procedure.id, table: 'notifications', column: 'notifications', order: opposite_order), method: :get, data: { controller: 'autosubmit' } do
.fr-toggle .fr-fieldset__element.fr-m-0
= check_box_tag :order, opposite_order, active?, class: 'fr-toggle__input' .fr-checkbox-group.fr-checkbox-group--sm
= label_tag :order, t('.show_notified_first'), class: 'fr-toggle__label fr-pl-1w' = check_box_tag :order, opposite_order, active?
= label_tag :order, t('.show_notified_first'), class: 'fr-label'
= submit_tag t('.show_notified_first'), data: {"checkbox-target": 'submit' }, class: 'visually-hidden' = submit_tag t('.show_notified_first'), data: {"checkbox-target": 'submit' }, class: 'visually-hidden'

View file

@ -4,7 +4,7 @@ fr:
Le routage permet dacheminer les dossiers vers différents groupes dinstructeurs. Le routage permet dacheminer les dossiers vers différents groupes dinstructeurs.
routing_configuration_notice_2_html: | routing_configuration_notice_2_html: |
<p>Pour le configurer, votre formulaire doit comporter <p>Pour le configurer, votre formulaire doit comporter
au moins un champ de type « choix simple » ou « départements ».</p> au moins un champ de type « choix simple », « départements » ou « régions ».</p>
<p>Ajoutez ce champ dans la page <a href="%{path}">« Configuration des champs »</a>.</p> <p>Ajoutez ce champ dans la page <a href="%{path}">« Configuration des champs »</a>.</p>
delete_title: Aucun dossier ne sera supprimé. Les groupes d'instructeurs vont être supprimés. Seuls les instructeurs du groupe « %{defaut_label} » resteront affectés à la démarche. delete_title: Aucun dossier ne sera supprimé. Les groupes d'instructeurs vont être supprimés. Seuls les instructeurs du groupe « %{defaut_label} » resteront affectés à la démarche.
delete_confirmation: | delete_confirmation: |

View file

@ -81,6 +81,8 @@ class Procedure::OneGroupeManagementComponent < ApplicationComponent
case @revision.types_de_champ_public.find_by(stable_id: targeted_champ.stable_id).type_champ case @revision.types_de_champ_public.find_by(stable_id: targeted_champ.stable_id).type_champ
when TypeDeChamp.type_champs.fetch(:departements) when TypeDeChamp.type_champs.fetch(:departements)
departements_for_select departements_for_select
when TypeDeChamp.type_champs.fetch(:regions)
regions_for_select
when TypeDeChamp.type_champs.fetch(:drop_down_list) when TypeDeChamp.type_champs.fetch(:drop_down_list)
targeted_champ targeted_champ
.options(@revision.types_de_champ_public) .options(@revision.types_de_champ_public)
@ -91,4 +93,8 @@ class Procedure::OneGroupeManagementComponent < ApplicationComponent
def departements_for_select def departements_for_select
APIGeoService.departements.map { ["#{_1[:code]} #{_1[:name]}", constant(_1[:code]).to_json] } APIGeoService.departements.map { ["#{_1[:code]} #{_1[:name]}", constant(_1[:code]).to_json] }
end end
def regions_for_select
APIGeoService.regions.map { ["#{_1[:code]} #{_1[:name]}", constant(_1[:code]).to_json] }
end
end end

View file

@ -43,23 +43,13 @@ module Administrateurs
case tdc.type_champ case tdc.type_champ
when TypeDeChamp.type_champs.fetch(:departements) when TypeDeChamp.type_champs.fetch(:departements)
tdc_options = APIGeoService.departements.map { ["#{_1[:code]} #{_1[:name]}", _1[:code]] } tdc_options = APIGeoService.departements.map { ["#{_1[:code]} #{_1[:name]}", _1[:code]] }
tdc_options.each do |code_and_name, code| create_groups_from_territorial_tdc(tdc_options, stable_id)
routing_rule = ds_eq(champ_value(stable_id), constant(code)) when TypeDeChamp.type_champs.fetch(:regions)
@procedure tdc_options = APIGeoService.regions.map { ["#{_1[:code]} #{_1[:name]}", _1[:code]] }
.groupe_instructeurs create_groups_from_territorial_tdc(tdc_options, stable_id)
.find_or_create_by(label: code_and_name)
.update(instructeurs: [current_administrateur.instructeur], routing_rule:)
end
when TypeDeChamp.type_champs.fetch(:drop_down_list) when TypeDeChamp.type_champs.fetch(:drop_down_list)
tdc_options = tdc.drop_down_options.reject(&:empty?) tdc_options = tdc.drop_down_options.reject(&:empty?)
tdc_options.each do |option_label| create_groups_from_drop_down_list_tdc(tdc_options, stable_id)
routing_rule = ds_eq(champ_value(stable_id), constant(option_label))
@procedure
.groupe_instructeurs
.find_or_create_by(label: option_label)
.update(instructeurs: [current_administrateur.instructeur], routing_rule:)
end
end end
if tdc.drop_down_other? if tdc.drop_down_other?
@ -460,5 +450,25 @@ module Administrateurs
def flash_message_for_invalid_csv def flash_message_for_invalid_csv
flash[:alert] = "Importation impossible, veuillez importer un csv suivant #{view_context.link_to('ce modèle', "/csv/import-instructeurs-test.csv")} pour une procédure sans routage ou #{view_context.link_to('celui-ci', "/csv/#{I18n.locale}/import-groupe-test.csv")} pour une procédure routée" flash[:alert] = "Importation impossible, veuillez importer un csv suivant #{view_context.link_to('ce modèle', "/csv/import-instructeurs-test.csv")} pour une procédure sans routage ou #{view_context.link_to('celui-ci', "/csv/#{I18n.locale}/import-groupe-test.csv")} pour une procédure routée"
end end
def create_groups_from_territorial_tdc(tdc_options, stable_id)
tdc_options.each do |label, code|
routing_rule = ds_eq(champ_value(stable_id), constant(code))
@procedure
.groupe_instructeurs
.find_or_create_by(label: label)
.update(instructeurs: [current_administrateur.instructeur], routing_rule:)
end
end
def create_groups_from_drop_down_list_tdc(tdc_options, stable_id)
tdc_options.each do |label|
routing_rule = ds_eq(champ_value(stable_id), constant(label))
@procedure
.groupe_instructeurs
.find_or_create_by(label: label)
.update(instructeurs: [current_administrateur.instructeur], routing_rule:)
end
end
end end
end end

View file

@ -21,7 +21,7 @@ class ApplicationController < ActionController::Base
around_action :switch_locale around_action :switch_locale
helper_method :multiple_devise_profile_connect?, :instructeur_signed_in?, :current_instructeur, :current_expert, :expert_signed_in?, helper_method :multiple_devise_profile_connect?, :instructeur_signed_in?, :current_instructeur, :current_expert, :expert_signed_in?,
:administrateur_signed_in?, :current_administrateur, :current_account, :localization_enabled?, :set_locale :administrateur_signed_in?, :current_administrateur, :current_account, :localization_enabled?, :set_locale, :current_expert_not_instructeur?
before_action do before_action do
Current.request_id = request.uuid Current.request_id = request.uuid
@ -62,6 +62,10 @@ class ApplicationController < ActionController::Base
current_user&.expert current_user&.expert
end end
def current_expert_not_instructeur?
current_user&.expert? && !current_user&.instructeur?
end
def expert_signed_in? def expert_signed_in?
current_expert.present? current_expert.present?
end end

View file

@ -0,0 +1,60 @@
module Instructeurs
class ContactInformationsController < InstructeurController
def new
assign_procedure_and_groupe_instructeur
@contact_information = @groupe_instructeur.build_contact_information
end
def create
assign_procedure_and_groupe_instructeur
@contact_information = @groupe_instructeur.build_contact_information(contact_information_params)
if @contact_information.save
redirect_to_groupe_instructeur("Les informations de contact ont bien été ajoutées")
else
flash[:alert] = @contact_information.errors.full_messages
render :new
end
end
def edit
assign_procedure_and_groupe_instructeur
@contact_information = @groupe_instructeur.contact_information
end
def update
assign_procedure_and_groupe_instructeur
@contact_information = @groupe_instructeur.contact_information
if @contact_information.update(contact_information_params)
redirect_to_groupe_instructeur("Les informations de contact ont bien été modifiées")
else
flash[:alert] = @contact_information.errors.full_messages
render :edit
end
end
def destroy
assign_procedure_and_groupe_instructeur
@groupe_instructeur.contact_information.destroy
redirect_to_groupe_instructeur("Les informations de contact ont bien été supprimées")
end
private
def redirect_to_groupe_instructeur(notice)
if params[:from_admin] == "true"
redirect_to admin_procedure_groupe_instructeur_path(@procedure, @groupe_instructeur), notice: notice
else
redirect_to instructeur_groupe_path(@procedure, @groupe_instructeur), notice: notice
end
end
def assign_procedure_and_groupe_instructeur
@procedure = current_instructeur.procedures.find params[:procedure_id]
@groupe_instructeur = current_instructeur.groupe_instructeurs.find params[:groupe_id]
end
def contact_information_params
params.require(:contact_information).permit(:nom, :email, :telephone, :horaires, :adresse)
end
end
end

View file

@ -73,6 +73,8 @@ module Instructeurs
@avis = Avis.new @avis = Avis.new
if @dossier.procedure.experts_require_administrateur_invitation? if @dossier.procedure.experts_require_administrateur_invitation?
@experts_emails = dossier.procedure.experts_procedures.where(revoked_at: nil).map(&:expert).map(&:email).sort @experts_emails = dossier.procedure.experts_procedures.where(revoked_at: nil).map(&:expert).map(&:email).sort
else
@experts_emails = @dossier.procedure.experts.map(&:email).sort
end end
end end
@ -81,6 +83,8 @@ module Instructeurs
@avis = Avis.new @avis = Avis.new
if @dossier.procedure.experts_require_administrateur_invitation? if @dossier.procedure.experts_require_administrateur_invitation?
@experts_emails = dossier.procedure.experts_procedures.where(revoked_at: nil).map(&:expert).map(&:email).sort @experts_emails = dossier.procedure.experts_procedures.where(revoked_at: nil).map(&:expert).map(&:email).sort
else
@experts_emails = @dossier.procedure.experts.map(&:email).sort
end end
end end

View file

@ -1,4 +1,6 @@
class ArchiveCreationJob < ApplicationJob class ArchiveCreationJob < ApplicationJob
discard_on ActiveRecord::RecordNotFound
queue_as :archives queue_as :archives
def max_run_time def max_run_time

View file

@ -10,77 +10,18 @@ class TitreIdentiteWatermarkJob < ApplicationJob
# (to avoid modifying the file while it is being scanned). # (to avoid modifying the file while it is being scanned).
retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10 retry_on FileNotScannedYetError, wait: :exponentially_longer, attempts: 10
MAX_IMAGE_SIZE = 1500
SCALE = 0.9
WATERMARK = URI.parse(WATERMARK_FILE).is_a?(URI::HTTP) ? WATERMARK_FILE : Rails.root.join("app/assets/images/#{WATERMARK_FILE}")
def perform(blob) def perform(blob)
return if blob.watermark_done? return if blob.watermark_done?
raise FileNotScannedYetError if blob.virus_scanner.pending? raise FileNotScannedYetError if blob.virus_scanner.pending?
blob.open do |file| blob.open do |file|
watermark = resize_watermark(file) Tempfile.create(["watermarked", File.extname(file)]) do |output|
processed = WatermarkService.new.process(file, output)
if watermark.present? return if processed.blank?
processed = watermark_image(file, watermark)
blob.upload(processed) blob.upload(processed)
blob.touch(:watermarked_at) blob.touch(:watermarked_at)
end end
end end
end end
private
def watermark_image(file, watermark)
ImageProcessing::MiniMagick
.source(file)
.convert("png")
.resize_to_limit(MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
.composite(watermark, mode: "over", gravity: "center")
.call
end
def resize_watermark(file)
metadata = image_metadata(file)
if metadata[:width].present? && metadata[:height].present?
width = [metadata[:width], MAX_IMAGE_SIZE].min * SCALE
height = [metadata[:height], MAX_IMAGE_SIZE].min * SCALE
diagonal = Math.sqrt(height**2 + width**2)
angle = Math.asin(height / diagonal) * 180 / Math::PI
ImageProcessing::MiniMagick
.source(WATERMARK)
.resize_to_limit(diagonal, diagonal / 2)
.rotate(-angle, background: :transparent)
.call
end
end
def image_metadata(file)
read_image(file) do |image|
if rotated_image?(image)
{ width: image.height, height: image.width }
else
{ width: image.width, height: image.height }
end
end
end
def read_image(file)
require "mini_magick"
image = MiniMagick::Image.new(file.path)
if image.valid?
yield image
else
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
{}
end
end
def rotated_image?(image)
['RightTop', 'LeftBottom'].include?(image["%[orientation]"])
end
end end

View file

@ -0,0 +1,17 @@
class ContactInformation < ApplicationRecord
belongs_to :groupe_instructeur
validates :nom, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :nom, uniqueness: { scope: :groupe_instructeur, message: 'existe déjà' }
validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :telephone, phone: { possible: true, allow_blank: false }
validates :horaires, presence: { message: 'doivent être renseignés' }, allow_nil: false
validates :adresse, presence: { message: 'doit être renseignée' }, allow_nil: false
validates :groupe_instructeur, presence: { message: 'doit être renseigné' }, allow_nil: false
def telephone_url
if telephone.present?
"tel:#{telephone.gsub(/[[:blank:]]/, '')}"
end
end
end

View file

@ -1304,6 +1304,10 @@ class Dossier < ApplicationRecord
) )
end end
def service
groupe_instructeur&.contact_information || procedure.service
end
private private
def create_missing_traitemets def create_missing_traitemets

View file

@ -13,6 +13,7 @@ class GroupeInstructeur < ApplicationRecord
has_and_belongs_to_many :bulk_messages, dependent: :destroy has_and_belongs_to_many :bulk_messages, dependent: :destroy
has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur has_one :defaut_procedure, -> { with_discarded }, class_name: 'Procedure', foreign_key: :defaut_groupe_instructeur_id, dependent: :nullify, inverse_of: :defaut_groupe_instructeur
has_one :contact_information
validates :label, presence: true, allow_nil: false validates :label, presence: true, allow_nil: false
validates :label, uniqueness: { scope: :procedure } validates :label, uniqueness: { scope: :procedure }
@ -106,6 +107,8 @@ class GroupeInstructeur < ApplicationRecord
options = case routing_tdc.type_champ options = case routing_tdc.type_champ
when TypeDeChamp.type_champs.fetch(:departements) when TypeDeChamp.type_champs.fetch(:departements)
APIGeoService.departements.map { _1[:code] } APIGeoService.departements.map { _1[:code] }
when TypeDeChamp.type_champs.fetch(:regions)
APIGeoService.regions.map { _1[:code] }
when TypeDeChamp.type_champs.fetch(:drop_down_list) when TypeDeChamp.type_champs.fetch(:drop_down_list)
routing_tdc.options_with_drop_down_other routing_tdc.options_with_drop_down_other
end end

View file

@ -44,7 +44,7 @@ class Logic::ChampValue < Logic::Term
targeted_champ.selected targeted_champ.selected
when "Champs::MultipleDropDownListChamp" when "Champs::MultipleDropDownListChamp"
targeted_champ.selected_options targeted_champ.selected_options
when "Champs::DepartementChamp" when "Champs::DepartementChamp", "Champs::RegionChamp"
targeted_champ.code targeted_champ.code
end end
end end

View file

@ -469,7 +469,7 @@ class Procedure < ApplicationRecord
dossier_submitted_message: [] dossier_submitted_message: []
} }
} }
include_list[:groupe_instructeurs] = :instructeurs if !is_different_admin include_list[:groupe_instructeurs] = [:instructeurs, :contact_information] if !is_different_admin
procedure = self.deep_clone(include: include_list) do |original, kopy| procedure = self.deep_clone(include: include_list) do |original, kopy|
PiecesJustificativesService.clone_attachments(original, kopy) PiecesJustificativesService.clone_attachments(original, kopy)
end end

View file

@ -224,7 +224,7 @@ class ProcedureRevision < ApplicationRecord
end end
def routable_types_de_champ def routable_types_de_champ
types_de_champ_public.filter { |tdc| [:drop_down_list, :departements].include?(tdc.type_champ.to_sym) } types_de_champ_public.filter { |tdc| [:drop_down_list, :departements, :regions].include?(tdc.type_champ.to_sym) }
end end
private private

View file

@ -0,0 +1,118 @@
class WatermarkService
POINTSIZE = 20
KERNING = 1.2
ANGLE = 45
FILL_COLOR = "rgba(0,0,0,0.4)"
attr_reader :text
attr_reader :text_length
def initialize(text = APPLICATION_NAME)
@text = " #{text} " # give more space around each occurence
@text_length = @text.length
end
def process(file, output)
metadata = image_metadata(file)
return if metadata.blank?
watermark_image(file, output, metadata)
output
end
private
def watermark_image(file, output, metadata)
MiniMagick::Tool::Convert.new do |convert|
setup_conversion_commands(convert, file)
apply_watermark(convert, metadata)
convert << output.to_path
end
end
def setup_conversion_commands(convert, file)
convert << file.to_path
convert << "-pointsize"
convert << POINTSIZE
convert << "-kerning"
convert << KERNING
convert << "-fill"
convert << FILL_COLOR
convert << "-gravity"
convert << "northwest"
end
# Parcourt l'image ligne par ligne et colonne par colonne en y apposant un filigrane
# en alternant un décalage horizontal sur chaque ligne
def apply_watermark(convert, metadata)
stride_x, stride_y, initial_offsets_x, initial_offset_y = calculate_watermark_params
0.step(by: stride_y, to: metadata[:height] + stride_y * 2).with_index do |offset_y, index|
initial_offset_x = initial_offsets_x[index % 2]
0.step(by: stride_x, to: metadata[:width] + stride_x * 2) do |offset_x|
x = initial_offset_x + offset_x
y = initial_offset_y + offset_y
draw_text(convert, x, y)
end
end
end
def calculate_watermark_params
# Approximation de la longueur du texte, qui marche bien pour les constantes par défaut
char_width_approx = POINTSIZE / 2
char_height_approx = POINTSIZE * 3 / 4
# Dimensions du rectangle de texte
text_width_approx = char_width_approx * text_length * Math.cos(ANGLE * (Math::PI / 180)).abs
text_height_approx = char_width_approx * text_length * Math.sin(ANGLE * (Math::PI / 180)).abs + char_height_approx
diagonal_length = Math.sqrt(text_width_approx**2 + text_height_approx**2)
# Calcul des décalages entre chaque colonne et ligne
# afin que chaque occurence "suive" la précédente
stride_x = ((diagonal_length + char_width_approx) / Math.cos(ANGLE * (Math::PI / 180)))
stride_y = text_height_approx
initial_offsets_x = [0, (0 - stride_x / 2).round] # Motif de damier en alternant le décalage horizontal
initial_offset_y = 0 - stride_y # Offset négatif pour mieux couvrir le nord ouest
[stride_x.round, stride_y.round, initial_offsets_x, initial_offset_y.round]
end
def draw_text(convert, x, y)
# A chaque insertion de texte, positionne le curseur, définit la rotation, puis réinitialise ces paramètres pour la prochaine occurence
# Note: x and y can be negative value
convert << "-draw"
convert << "translate #{x},#{y} rotate #{-ANGLE} text 0,0 '#{text}' rotate #{ANGLE} translate #{-x},#{-y}"
end
def image_metadata(file)
read_image(file) do |image|
width = image.width
height = image.height
if rotated_image?(image)
width, height = height, width
end
{ width: width, height: height }
end
end
def read_image(file)
image = MiniMagick::Image.new(file.to_path)
if image.valid?
yield image
else
Rails.logger.info "Skipping image analysis because ImageMagick doesn't support the file #{file}"
nil
end
end
def rotated_image?(image)
['RightTop', 'LeftBottom'].include?(image["%[orientation]"])
end
end

View file

@ -0,0 +1,23 @@
.card.mt-2
.card-title Informations de contact
- service = groupe_instructeur.contact_information
- if service.nil?
= "Le groupe #{groupe_instructeur.label} n'a pas d'informations de contact. Les informations de contact affichées à l'usager seront celles du service de la procédure"
%p.mt-3
- if groupe_instructeur.instructeurs.include?(current_administrateur.user.instructeur)
= link_to "+ Ajouter des informations de contact", new_instructeur_groupe_contact_information_path(procedure_id: procedure.id, groupe_id: groupe_instructeur.id, from_admin: true), class: "fr-btn"
- else
Si vous souhaitez créer un service pour ce groupe, vous devez faire partie du groupe instructeur
- else
%p.mt-3
- if groupe_instructeur.instructeurs.include?(current_administrateur.user.instructeur)
= link_to "Modifier les informations de contact", edit_instructeur_groupe_contact_information_path(procedure_id: procedure.id, groupe_id: groupe_instructeur.id, from_admin: true), class: "fr-btn"
- else
Si vous souhaitez modifier ce service, vous devez faire partie du groupe instructeur
%p.mt-3= service.nom
= render SimpleFormatComponent.new(service.adresse, class_names_map: {paragraph: 'fr-footer__content-desc'})
= service.email
- if service.telephone.present?
%p= service.telephone
- if service.horaires.present?
%p= service.horaires

View file

@ -13,3 +13,6 @@
instructeurs: @instructeurs, instructeurs: @instructeurs,
available_instructeur_emails: @available_instructeur_emails, available_instructeur_emails: @available_instructeur_emails,
disabled_as_super_admin: administrateur_as_manager? } disabled_as_super_admin: administrateur_as_manager? }
= render partial: 'administrateurs/groupe_instructeurs/contact_information',
locals: { procedure: @procedure,
groupe_instructeur: @groupe_instructeur }

View file

@ -4,14 +4,14 @@
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" } %meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" } %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
%meta{ name: "viewport", content: "width=device-width, initial-scale=1" } %meta{ name: "viewport", content: "width=device-width, initial-scale=1" }
%meta{ name: "application-name", content: APPLICATION_NAME }
%meta{ name: "apple-mobile-web-app-title", content: APPLICATION_NAME }
= csrf_meta_tags = csrf_meta_tags
%title %title
= content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME = content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16") = render partial: "layouts/favicons"
= favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32")
= favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96")
= vite_client_tag = vite_client_tag
= vite_typescript_tag 'playground' = vite_typescript_tag 'playground'

View file

@ -0,0 +1,39 @@
= form_with url: instructeur_groupe_contact_information_path, model: @contact_information, local: true do |f|
= hidden_field_tag :from_admin, params[:from_admin]
= render Dsfr::CalloutComponent.new(title: "Informations de contact") do |c|
- c.body do
Votre démarche est hébergée par #{APPLICATION_NAME} mais nous ne pouvons pas assurer le support des démarches. Et malgré la dématérialisation, les usagers se posent parfois des questions légitimes sur le processus administratif.
%br
%br
%strong Il est donc indispensable que les usagers puissent vous contacter
par le moyen de leur choix sils ont des questions sur votre démarche.
%br
%br
Ces informations de contact seront visibles par les utilisateurs de la démarche, affichées dans le menu « Aide », ainsi quen pied de page lors du dépôt dun dossier.
%br
%br
⚠️ En cas dinformations invalides, #{APPLICATION_NAME} se réserve le droit de suspendre la publication de la démarche.
= render Dsfr::InputComponent.new(form: f, attribute: :nom, input_type: :text_field) do |c|
- c.with_hint do
Indiquez le nom à utiliser pour contacter le groupe instructeur
(Exemple: Secrétariat de la Mairie)
= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field)
= render Dsfr::InputComponent.new(form: f, attribute: :telephone, input_type: :telephone_field)
= render Dsfr::InputComponent.new(form: f, attribute: :horaires, input_type: :text_area)
= render Dsfr::InputComponent.new(form: f, attribute: :adresse, input_type: :text_area)
- if procedure_id.present?
= hidden_field_tag :procedure_id, procedure_id
.sticky-action-footer
= f.submit "Enregistrer", class: "fr-btn fr-mr-2w"
= link_to "Annuler", instructeur_groupe_path(@groupe_instructeur, procedure_id: procedure_id), class: "fr-btn fr-btn--secondary"
- if [ "edit", "update"].include? params[:action]
= link_to 'Supprimer',
instructeur_groupe_contact_information_path(procedure_id: @procedure.id, groupe_id: @groupe_instructeur.id),
method: :delete,
data: { confirm: "Confirmez vous la suppression de ces informations de contact ?" },
class: 'fr-btn fr-btn--secondary'

View file

@ -0,0 +1,10 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
['Groupes dinstructeurs', instructeur_groupes_path(@procedure)],
[@groupe_instructeur.label, instructeur_groupe_path(@groupe_instructeur, procedure_id: @procedure.id) ],
['Service']]}
.container
%h1 Modifier les informations de contact
= render partial: 'form',
locals: { contact_information: @contact_information, procedure_id: @procedure.id }

View file

@ -0,0 +1,10 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
['Groupes dinstructeurs', instructeur_groupes_path(@procedure)],
[@groupe_instructeur.label, instructeur_groupe_path(@groupe_instructeur, procedure_id: @procedure.id) ],
['Service']]}
.container
%h1 Informations de contact
= render partial: 'form',
locals: { contact_information: @contact_information, procedure_id: @procedure.id }

View file

@ -48,3 +48,20 @@
class: 'button' } class: 'button' }
= paginate @instructeurs, views_prefix: 'shared' = paginate @instructeurs, views_prefix: 'shared'
.card.mt-2
.card-title Informations de contact
- service = @groupe_instructeur.contact_information
- if service.nil?
= "Le groupe #{@groupe_instructeur.label} n'a pas d'informations de contact. Les informations de contact affichées à l'usager seront celles du service de la procédure"
%p.mt-3
= link_to "+ Ajouter des informations de contact", new_instructeur_groupe_contact_information_path(procedure_id: @procedure.id, groupe_id: @groupe_instructeur.id), class: "fr-btn"
- else
%p.mt-3
= link_to "Modifier les informations de contact", edit_instructeur_groupe_contact_information_path(procedure_id: @procedure.id, groupe_id: @groupe_instructeur.id), class: "fr-btn"
%p.mt-3= service.nom
= render SimpleFormatComponent.new(service.adresse, class_names_map: {paragraph: 'fr-footer__content-desc'})
= service.email
- if service.telephone.present?
%p= service.telephone
- if service.horaires.present?
%p= service.horaires

View file

@ -7,5 +7,5 @@
- if i > 0 - if i > 0
= " ou " = " ou "
= link_to remove_filter_instructeur_procedure_path(procedure, { statut: statut, field: "#{filter['table']}/#{filter['column']}", value: filter['value'] }), = link_to remove_filter_instructeur_procedure_path(procedure, { statut: statut, field: "#{filter['table']}/#{filter['column']}", value: filter['value'] }),
class: "fr-tag fr-tag--dismiss fr-mb-1w", aria: { label: "Retirer le filtre #{filter['column']}" } do class: "fr-tag fr-tag--dismiss fr-my-1w", aria: { label: "Retirer le filtre #{filter['column']}" } do
= "#{filter['label'].truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}" = "#{filter['label'].truncate(50)} : #{procedure_presentation.human_value_for_filter(filter)}"

View file

@ -65,23 +65,7 @@
= render Dossiers::NotifiedToggleComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation) = render Dossiers::NotifiedToggleComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation)
.fr-ml-auto .fr-ml-auto
= render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm', 'fr-btn--secondary'] }, menu_options: { id: 'custom-menu' }) do |menu|
- menu.with_button_inner_html do
= t('views.instructeurs.dossiers.personalize')
- menu.with_form do
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
= hidden_field_tag :values, nil
= react_component("ComboMultiple",
options: @displayable_fields_for_select,
selected: @displayable_fields_selected,
disabled: [],
label: 'Colonne à afficher',
group: '.columns-form',
name: 'values')
= submit_tag t('views.instructeurs.dossiers.save'), class: 'fr-btn fr-btn--secondary'
.fr-ml-2w
- if @statut == 'archives' - if @statut == 'archives'
= link_to deleted_dossiers_instructeur_procedure_path(@procedure), class: "fr-link fr-icon-delete-line fr-link--icon-left fr-mr-2w" do = link_to deleted_dossiers_instructeur_procedure_path(@procedure), class: "fr-link fr-icon-delete-line fr-link--icon-left fr-mr-2w" do
= t('views.instructeurs.dossiers.show_deleted_dossiers') = t('views.instructeurs.dossiers.show_deleted_dossiers')
@ -122,10 +106,26 @@
- @procedure_presentation.displayed_fields_for_headers.each do |field| - @procedure_presentation.displayed_fields_for_headers.each do |field|
= render partial: "header_field", locals: { field: field, classname: field['classname'] } = render partial: "header_field", locals: { field: field, classname: field['classname'] }
%th.action-col.follow-col %th.follow-col
Actions Actions
%tr %th.text-right
= render Dropdown::MenuComponent.new(wrapper: :span, button_options: { class: ['fr-btn--sm', 'fr-btn--tertiary-no-outline', 'fr-btn--icon-right', 'fr-icon-settings-5-line'] }, menu_options: { id: 'custom-menu' }) do |menu|
- menu.with_button_inner_html do
= t('views.instructeurs.dossiers.personalize')
- menu.with_form do
= form_tag update_displayed_fields_instructeur_procedure_path(@procedure), method: :patch, class: 'dropdown-form large columns-form' do
= hidden_field_tag :values, nil
= react_component("ComboMultiple",
options: @displayable_fields_for_select,
selected: @displayable_fields_selected,
disabled: [],
label: 'Colonne à afficher',
group: '.columns-form',
name: 'values')
= submit_tag t('views.instructeurs.dossiers.save'), class: 'fr-btn fr-btn--secondary'
%tbody %tbody
= render Dossiers::BatchSelectMoreComponent.new(dossiers_count: @dossiers_count, filtered_sorted_ids: @filtered_sorted_ids) = render Dossiers::BatchSelectMoreComponent.new(dossiers_count: @dossiers_count, filtered_sorted_ids: @filtered_sorted_ids)
@ -179,7 +179,7 @@
%span.cell-link %span.cell-link
= link_to_if p.hidden_by_administration_at.blank?, render(Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: p, procedure: @procedure)), path = link_to_if p.hidden_by_administration_at.blank?, render(Instructeurs::SVASVRDecisionBadgeComponent.new(projection_or_dossier: p, procedure: @procedure)), path
%td.action-col.follow-col %td.follow-col{ colspan:'2' }
%ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right %ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
= render partial: 'instructeurs/procedures/dossier_actions', locals: { procedure_id: @procedure.id, = render partial: 'instructeurs/procedures/dossier_actions', locals: { procedure_id: @procedure.id,
dossier_id: p.dossier_id, dossier_id: p.dossier_id,
@ -191,6 +191,7 @@
sva_svr: @procedure.sva_svr_enabled?, sva_svr: @procedure.sva_svr_enabled?,
turbo: false, turbo: false,
with_menu: false } with_menu: false }
%tfoot %tfoot
%tr %tr
%td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 } %td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 }

View file

@ -0,0 +1,5 @@
= favicon_link_tag(image_url(FAVICONS_SRC["16px"]), type: "image/png", sizes: "16x16") if FAVICONS_SRC.key?("16px")
= favicon_link_tag(image_url(FAVICONS_SRC["32px"]), type: "image/png", sizes: "32x32") if FAVICONS_SRC.key?("32px")
= favicon_link_tag(image_url(FAVICONS_SRC["96px"]), type: "image/png", sizes: "96x96") if FAVICONS_SRC.key?("96px")
= favicon_link_tag(image_url(FAVICONS_SRC["apple_touch"]), type: nil, sizes: "152x152", rel: "apple-touch-icon") if FAVICONS_SRC.key?("apple_touch")
%meta{ name: "theme-color", content: "#ffffff" }

View file

@ -4,14 +4,14 @@
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" } %meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" } %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
%meta{ name: "viewport", content: "width=device-width, initial-scale=1" } %meta{ name: "viewport", content: "width=device-width, initial-scale=1" }
%meta{ name: "application-name", content: APPLICATION_NAME }
%meta{ name: "apple-mobile-web-app-title", content: APPLICATION_NAME }
= csrf_meta_tags = csrf_meta_tags
%title %title
= content_for?(:title) ? "#{sanitize(yield(:title))} · #{APPLICATION_NAME}" : APPLICATION_NAME = content_for?(:title) ? "#{sanitize(yield(:title))} · #{APPLICATION_NAME}" : APPLICATION_NAME
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16") = render partial: "layouts/favicons"
= favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32")
= favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96")
= Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce) = Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce)

View file

@ -4,14 +4,14 @@
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" } %meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" } %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
%meta{ name: "viewport", content: "width=device-width, initial-scale=1" } %meta{ name: "viewport", content: "width=device-width, initial-scale=1" }
%meta{ name: "application-name", content: APPLICATION_NAME }
%meta{ name: "apple-mobile-web-app-title", content: APPLICATION_NAME }
= csrf_meta_tags = csrf_meta_tags
%title %title
= content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME = content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16") = render partial: "layouts/favicons"
= favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32")
= favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96")
= vite_client_tag = vite_client_tag
= vite_javascript_tag 'application' = vite_javascript_tag 'application'

View file

@ -3,14 +3,14 @@
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" } %meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" } %meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
%meta{ name: "viewport", content: "width=device-width, initial-scale=1" } %meta{ name: "viewport", content: "width=device-width, initial-scale=1" }
%meta{ name: "application-name", content: APPLICATION_NAME }
%meta{ name: "apple-mobile-web-app-title", content: APPLICATION_NAME }
= csrf_meta_tags = csrf_meta_tags
%title %title
= t("dynamics.page_title") = t("dynamics.page_title")
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16") = render partial: "layouts/favicons"
= favicon_link_tag(image_url("#{FAVICON_32PX_SRC}"), type: "image/png", sizes: "32x32")
= favicon_link_tag(image_url("#{FAVICON_96PX_SRC}"), type: "image/png", sizes: "96x96")
%body %body
= yield = yield

View file

@ -21,7 +21,7 @@
%th Démarche %th Démarche
%th Demandeur %th Demandeur
%th.status-col Statut %th.status-col Statut
%th.action-col.follow-col %th.follow-col
%tbody %tbody
- @projected_dossiers.each do |p| - @projected_dossiers.each do |p|
- procedure_libelle, user_email, procedure_id = p.columns - procedure_libelle, user_email, procedure_id = p.columns
@ -67,7 +67,7 @@
- if instructeur_dossier && expert_dossier - if instructeur_dossier && expert_dossier
%td.action-col.follow-col %td.follow-col
= render Dropdown::MenuComponent.new(wrapper: :div, button_options: {class: ['fr-btn--sm']}) do |menu| = render Dropdown::MenuComponent.new(wrapper: :div, button_options: {class: ['fr-btn--sm']}) do |menu|
- menu.with_button_inner_html do - menu.with_button_inner_html do
Actions Actions
@ -86,12 +86,12 @@
- elsif instructeur_dossier - elsif instructeur_dossier
- if hidden_by_administration - if hidden_by_administration
%td.action-col.follow-col %td.follow-col
= link_to restore_instructeur_dossier_path(procedure_id, p.dossier_id), method: :patch, class: "button primary" do = link_to restore_instructeur_dossier_path(procedure_id, p.dossier_id), method: :patch, class: "button primary" do
= t('views.instructeurs.dossiers.restore') = t('views.instructeurs.dossiers.restore')
- else - else
%td.action-col.follow-col %td.follow-col
%ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right %ul.inline.fr-btns-group.fr-btns-group--sm.fr-btns-group--inline.fr-btns-group--icon-right
= render partial: "instructeurs/procedures/dossier_actions", = render partial: "instructeurs/procedures/dossier_actions",
locals: { procedure_id: procedure_id, locals: { procedure_id: procedure_id,

View file

@ -13,7 +13,7 @@
= hidden_field_tag 'avis[emails]', nil = hidden_field_tag 'avis[emails]', nil
.fr-input-group .fr-input-group
= react_component("ComboMultiple", = react_component("ComboMultiple",
options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [], options: current_expert_not_instructeur? ? [] : @experts_emails,
selected: [], disabled: [], selected: [], disabled: [],
label: 'Emails', label: 'Emails',
group: '.ask-avis', group: '.ask-avis',

View file

@ -1,5 +1,5 @@
%footer.fr-footer.footer-procedure#footer{ role: "contentinfo" } %footer.fr-footer.footer-procedure#footer{ role: "contentinfo" }
- service = procedure.service - service = dossier&.service || procedure.service
.fr-footer__top.fr-mb-0 .fr-footer__top.fr-mb-0
.fr-container .fr-container
.fr-grid-row.fr-grid-row--start.fr-grid-row--gutters .fr-grid-row.fr-grid-row--start.fr-grid-row--gutters
@ -21,6 +21,7 @@
= I18n.t('users.procedure_footer.contact.email.link') = I18n.t('users.procedure_footer.contact.email.link')
= link_to service.email, "mailto:#{service.email}", class: "fr-footer__top-link" = link_to service.email, "mailto:#{service.email}", class: "fr-footer__top-link"
- if service.present?
- if service.telephone.present? || service.horaires.present? - if service.telephone.present? || service.horaires.present?
%li %li
- horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}" - horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}"

View file

@ -46,9 +46,12 @@ DS_ENV="staging"
# STATUS_PAGE_URL="" # STATUS_PAGE_URL=""
# Instance customization: Favicons ---> to be put in "app/assets/images" # Instance customization: Favicons ---> to be put in "app/assets/images"
# Search "real favicon generator" to find websites generating all these formats from a single image source.
# An empty string disable the icon if you don't care.
# FAVICON_16PX_SRC="favicons/16x16.png" # FAVICON_16PX_SRC="favicons/16x16.png"
# FAVICON_32PX_SRC="favicons/32x32.png" # FAVICON_32PX_SRC="favicons/32x32.png"
# FAVICON_96PX_SRC="favicons/96x96.png" # FAVICON_96PX_SRC="favicons/96x96.png"
# FAVICON_APPLE_TOUCH_152PX_SRC="favicons/apple-touch-icon.png"
# Instance customization: Application logo ---> to be put in "app/assets/images" # Instance customization: Application logo ---> to be put in "app/assets/images"
# HEADER_LOGO_SRC="marianne.png" # HEADER_LOGO_SRC="marianne.png"
@ -68,9 +71,6 @@ DS_ENV="staging"
# Instance customization: PDF export logo ---> to be put in "app/assets/images" # Instance customization: PDF export logo ---> to be put in "app/assets/images"
# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.png" # DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.png"
# Instance customization: watermark for identity documents
# WATERMARK_FILE=""
# Enabling maintenance mode # Enabling maintenance mode
# MAINTENANCE_MODE="true" # MAINTENANCE_MODE="true"

View file

@ -1,7 +1,10 @@
# Favicons # Favicons
FAVICON_16PX_SRC = ENV.fetch("FAVICON_16PX_SRC", "favicons/16x16.png") FAVICONS_SRC = {
FAVICON_32PX_SRC = ENV.fetch("FAVICON_32PX_SRC", "favicons/32x32.png") "16px" => ENV.fetch("FAVICON_16PX_SRC", "favicons/16x16.png"),
FAVICON_96PX_SRC = ENV.fetch("FAVICON_96PX_SRC", "favicons/96x96.png") "32px" => ENV.fetch("FAVICON_32PX_SRC", "favicons/32x32.png"),
"96px" => ENV.fetch("FAVICON_96PX_SRC", "favicons/96x96.png"),
"apple_touch" => ENV.fetch("FAVICON_APPLE_TOUCH_152PX_SRC", "favicons/apple-touch-icon.png")
}.compact_blank.freeze
# Header logo # Header logo
HEADER_LOGO_SRC = ENV.fetch("HEADER_LOGO_SRC", "marianne.png") HEADER_LOGO_SRC = ENV.fetch("HEADER_LOGO_SRC", "marianne.png")

View file

@ -1 +0,0 @@
WATERMARK_FILE = ENV.fetch('WATERMARK_FILE', 'watermark.png')

View file

@ -395,7 +395,7 @@ en:
batch_operation: batch_operation:
enabled: "Add file %{dossier_id} to the selection for the bulk operation" enabled: "Add file %{dossier_id} to the selection for the bulk operation"
disabled: "Impossible to add file %{dossier_id} to the selection because it is already in a bulk operation" disabled: "Impossible to add file %{dossier_id} to the selection because it is already in a bulk operation"
personalize: Personalize the table personalize: Personalize
show_deleted_dossiers: Show deleted files show_deleted_dossiers: Show deleted files
follow_file: Follow-up the file follow_file: Follow-up the file
save: Save save: Save

View file

@ -397,7 +397,7 @@ fr:
enabled: "Ajouter le dossier %{dossier_id} à la sélection pour un traitement de masse" enabled: "Ajouter le dossier %{dossier_id} à la sélection pour un traitement de masse"
disabled: "Impossible d'ajouter le dossier %{dossier_id} à la selection car il est déjà dans un traitement de masse" disabled: "Impossible d'ajouter le dossier %{dossier_id} à la selection car il est déjà dans un traitement de masse"
show_deleted_dossiers: Afficher les dossiers supprimés show_deleted_dossiers: Afficher les dossiers supprimés
personalize: Personnaliser le tableau personalize: Personnaliser
download: Télécharger un dossier download: Télécharger un dossier
follow_file: Suivre le dossier follow_file: Suivre le dossier
stop_follow: Ne plus suivre stop_follow: Ne plus suivre

View file

@ -5,7 +5,7 @@ fr:
one: 'Service' one: 'Service'
other: 'Services' other: 'Services'
attributes: attributes:
service: service: &service
adresse: 'Adresse postale' adresse: 'Adresse postale'
email: 'Email de contact' email: 'Email de contact'
telephone: 'Téléphone' telephone: 'Téléphone'
@ -27,6 +27,7 @@ fr:
Exemple : Du lundi au vendredi de 9h30 à 17h30, le samedi de 9h30 à 12h. Exemple : Du lundi au vendredi de 9h30 à 17h30, le samedi de 9h30 à 12h.
adresse: | adresse: |
Indiquez ladresse à laquelle un usager peut vous contacter, par exemple sil nest pas en capacité de compléter son formulaire en ligne. Indiquez ladresse à laquelle un usager peut vous contacter, par exemple sil nest pas en capacité de compléter son formulaire en ligne.
contact_information: *service
errors: errors:
models: models:

View file

@ -392,6 +392,7 @@ Rails.application.routes.draw do
resources :archives, only: [:index, :create] resources :archives, only: [:index, :create]
resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do
resource :contact_information
member do member do
post 'add_instructeur' post 'add_instructeur'
delete 'remove_instructeur' delete 'remove_instructeur'

View file

@ -0,0 +1,15 @@
class CreateContactInformations < ActiveRecord::Migration[7.0]
def change
create_table :contact_informations do |t|
t.belongs_to :groupe_instructeur, null: false, foreign_key: true
t.text :adresse, null: false
t.string :email, null: false
t.text :horaires, null: false
t.string :nom, null: false
t.string :telephone, null: false
t.timestamps
end
add_index :contact_informations, [:groupe_instructeur_id, :nom], unique: true, name: 'index_contact_informations_on_gi_and_nom'
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_08_02_161011) do ActiveRecord::Schema[7.0].define(version: 2023_08_09_151357) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -612,6 +612,19 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_02_161011) do
t.index ["source"], name: "index_geo_areas_on_source" t.index ["source"], name: "index_geo_areas_on_source"
end end
create_table "contact_informations", force: :cascade do |t|
t.text "adresse", null: false
t.datetime "created_at", null: false
t.string "email", null: false
t.bigint "groupe_instructeur_id", null: false
t.text "horaires", null: false
t.string "nom", null: false
t.string "telephone", null: false
t.datetime "updated_at", null: false
t.index ["groupe_instructeur_id", "nom"], name: "index_contact_informations_on_gi_and_nom", unique: true
t.index ["groupe_instructeur_id"], name: "index_contact_informations_on_groupe_instructeur_id"
end
create_table "groupe_instructeurs", force: :cascade do |t| create_table "groupe_instructeurs", force: :cascade do |t|
t.boolean "closed", default: false t.boolean "closed", default: false
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
@ -1044,6 +1057,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_02_161011) do
add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "dossiers"
add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "experts"
add_foreign_key "commentaires", "instructeurs" add_foreign_key "commentaires", "instructeurs"
add_foreign_key "contact_informations", "groupe_instructeurs"
add_foreign_key "dossier_assignments", "dossiers" add_foreign_key "dossier_assignments", "dossiers"
add_foreign_key "dossier_batch_operations", "batch_operations" add_foreign_key "dossier_batch_operations", "batch_operations"
add_foreign_key "dossier_batch_operations", "dossiers" add_foreign_key "dossier_batch_operations", "dossiers"

View file

@ -22,4 +22,32 @@ namespace :pjs do
blobs.in_batches { |batch| batch.ids.each { |id| PjsMigrationJob.perform_later(id) } } blobs.in_batches { |batch| batch.ids.each { |id| PjsMigrationJob.perform_later(id) } }
end end
desc "Watermark demo. Usage: noglob rake pjs:watermark_demo[tmp/carte-identite-demo-1.jpg]"
task :watermark_demo, [:file_path] => :environment do |_t, args|
file = Pathname.new(args[:file_path])
output_file = Rails.root.join('tmp', "#{file.basename(file.extname)}_watermarked#{file.extname}")
processed = WatermarkService.new.process(file, output_file)
if processed
rake_puts "Watermarked: #{processed}"
else
rake_puts "File #{file} not watermarked. Read application log for more information"
end
end
desc "Watermark demo all defined demo files. Usage: noglob rake pjs:watermark_demo_all"
task :watermark_demo_all => :environment do
# You must have these filenames in tmp/ to run this demo (download ID cards specimens)
filenames = [
"carte-identite-demo-1.jpg", "carte-identite-demo-2.jpg", "carte-identite-demo-3.png", "carte-identite-demo-4.jpg",
"carte-identite-demo-5.jpg", "passeport-1.jpg", "passeport-2.jpg"
]
filenames.each do |file|
Rake::Task["pjs:watermark_demo"].invoke("tmp/#{file}")
Rake::Task["pjs:watermark_demo"].reenable
end
end
end end

View file

@ -761,6 +761,26 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
expect(procedure3.routing_enabled).to be_truthy expect(procedure3.routing_enabled).to be_truthy
end end
end end
context 'with a regions type de champ' do
let!(:procedure3) do
create(:procedure,
types_de_champ_public: [{ type: :regions }],
administrateurs: [admin])
end
let!(:regions_tdc) { procedure3.draft_revision.types_de_champ.first }
before { post :create_simple_routing, params: { procedure_id: procedure3.id, create_simple_routing: { stable_id: regions_tdc.stable_id } } }
it do
expect(response).to redirect_to(admin_procedure_groupe_instructeurs_path(procedure3))
expect(flash.notice).to eq 'Les groupes instructeurs ont été ajoutés'
expect(procedure3.groupe_instructeurs.pluck(:label)).to include("01 Guadeloupe")
expect(procedure3.reload.defaut_groupe_instructeur.routing_rule).to eq(ds_eq(champ_value(regions_tdc.stable_id), constant('01')))
expect(procedure3.routing_enabled).to be_truthy
end
end
end end
describe '#wizard' do describe '#wizard' do

View file

@ -0,0 +1,123 @@
describe Instructeurs::ContactInformationsController, type: :controller do
let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure) }
let(:assign_to) { create(:assign_to, instructeur: instructeur, groupe_instructeur: build(:groupe_instructeur, procedure: procedure)) }
let(:gi) { assign_to.groupe_instructeur }
let(:from_admin) { nil }
before do
sign_in(instructeur.user)
end
describe '#create' do
context 'when submitting a new contact_information' do
let(:params) do
{
contact_information: {
nom: 'super service',
email: 'email@toto.com',
telephone: '1234',
horaires: 'horaires',
adresse: 'adresse'
},
procedure_id: procedure.id,
groupe_id: gi.id,
from_admin: from_admin
}
end
it do
post :create, params: params
expect(flash.alert).to be_nil
expect(flash.notice).to eq('Les informations de contact ont bien été ajoutées')
expect(ContactInformation.last.nom).to eq('super service')
expect(ContactInformation.last.email).to eq('email@toto.com')
expect(ContactInformation.last.telephone).to eq('1234')
expect(ContactInformation.last.horaires).to eq('horaires')
expect(ContactInformation.last.adresse).to eq('adresse')
end
context 'from admin' do
let(:from_admin) { true }
it do
post :create, params: params
expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(gi, procedure_id: procedure.id))
end
end
end
context 'when submitting an invalid contact_information' do
before do
post :create, params: params
end
let(:params) {
{
contact_information: {
nom: 'super service'
},
procedure_id: procedure.id,
groupe_id: gi.id
}
}
it { expect(flash.alert).not_to be_nil }
it { expect(response).to render_template(:new) }
it { expect(assigns(:contact_information).nom).to eq('super service') }
end
end
describe '#update' do
let(:contact_information) { create(:contact_information, groupe_instructeur: gi) }
let(:contact_information_params) {
{
nom: 'nom'
}
}
let(:params) {
{
id: contact_information.id,
contact_information: contact_information_params,
procedure_id: procedure.id,
groupe_id: gi.id,
from_admin: from_admin
}
}
before do
patch :update, params: params
end
context 'when updating a contact_information' do
it { expect(flash.alert).to be_nil }
it { expect(flash.notice).to eq('Les informations de contact ont bien été modifiées') }
it { expect(ContactInformation.last.nom).to eq('nom') }
it { expect(response).to redirect_to(instructeur_groupe_path(gi, procedure_id: procedure.id)) }
end
context 'when updating a contact_information as an admin' do
let(:from_admin) { true }
it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(gi, procedure_id: procedure.id)) }
end
context 'when updating a contact_information with invalid data' do
let(:contact_information_params) { { nom: '' } }
it { expect(flash.alert).not_to be_nil }
it { expect(response).to render_template(:edit) }
end
end
describe '#destroy' do
let(:contact_information) { create(:contact_information, groupe_instructeur: gi) }
before do
delete :destroy, params: { id: contact_information.id, procedure_id: procedure.id, groupe_id: gi.id }
end
it { expect { contact_information.reload }.to raise_error(ActiveRecord::RecordNotFound) }
it { expect(flash.alert).to be_nil }
it { expect(flash.notice).to eq("Les informations de contact ont bien été supprimées") }
it { expect(response).to redirect_to(instructeur_groupe_path(gi, procedure_id: procedure.id)) }
end
end

View file

@ -0,0 +1,11 @@
FactoryBot.define do
factory :contact_information do
sequence(:nom) { |n| "Service #{n}" }
email { 'email@toto.com' }
telephone { '1234' }
horaires { 'de 9 h à 18 h' }
adresse { 'adresse' }
association :groupe_instructeur
end
end

View file

@ -0,0 +1,62 @@
describe ContactInformation, type: :model do
describe 'validation' do
let(:gi) { create(:groupe_instructeur) }
let(:params) do
{
nom: 'service des jardins',
email: 'super@email.com',
telephone: '012345678',
horaires: 'du lundi au vendredi',
adresse: '12 rue des schtroumpfs',
groupe_instructeur_id: gi.id
}
end
subject { ContactInformation.new(params) }
it { expect(subject).to be_valid }
it 'should forbid invalid phone numbers' do
invalid_phone_numbers = ["1", "Néant", "01 60 50 40 30 20"]
invalid_phone_numbers.each do |tel|
subject.telephone = tel
expect(subject).not_to be_valid
end
end
it 'should not accept no phone numbers' do
subject.telephone = nil
expect(subject).not_to be_valid
end
it 'should accept valid phone numbers' do
valid_phone_numbers = ["3646", "273115", "0160376983", "01 60 50 40 30 ", "+33160504030"]
valid_phone_numbers.each do |tel|
subject.telephone = tel
expect(subject).to be_valid
end
end
context 'when a contact information already exists' do
before { ContactInformation.create(params) }
context 'checks uniqueness of administrateur, name couple' do
it { expect(ContactInformation.create(params)).not_to be_valid }
end
end
context 'of nom' do
it 'should be set' do
expect(ContactInformation.new(params.except(:nom))).not_to be_valid
end
end
context 'of groupe instructeur' do
it 'should be set' do
expect(ContactInformation.new(params.except(:groupe_instructeur_id))).not_to be_valid
end
end
end
end

View file

@ -76,6 +76,12 @@ describe Logic::ChampValue do
it { is_expected.to eq(true) } it { is_expected.to eq(true) }
end end
context 'region tdc' do
let(:champ) { create(:champ_regions, value: 'La Réunion') }
it { is_expected.to eq('04') }
end
describe 'errors' do describe 'errors' do
let(:champ) { create(:champ) } let(:champ) { create(:champ) }

View file

@ -948,9 +948,10 @@ describe ProcedureRevision do
p.draft_revision.add_type_de_champ(type_champ: :text, libelle: 'l1') p.draft_revision.add_type_de_champ(type_champ: :text, libelle: 'l1')
p.draft_revision.add_type_de_champ(type_champ: :drop_down_list, libelle: 'l2') p.draft_revision.add_type_de_champ(type_champ: :drop_down_list, libelle: 'l2')
p.draft_revision.add_type_de_champ(type_champ: :departements, libelle: 'l3') p.draft_revision.add_type_de_champ(type_champ: :departements, libelle: 'l3')
p.draft_revision.add_type_de_champ(type_champ: :regions, libelle: 'l4')
end end
end end
it { expect(draft.routable_types_de_champ.pluck(:libelle)).to eq(['l2', 'l3']) } it { expect(draft.routable_types_de_champ.pluck(:libelle)).to eq(['l2', 'l3', 'l4']) }
end end
end end

View file

@ -563,7 +563,7 @@ describe Procedure do
let(:logo) { Rack::Test::UploadedFile.new('spec/fixtures/files/white.png', 'image/png') } let(:logo) { Rack::Test::UploadedFile.new('spec/fixtures/files/white.png', 'image/png') }
let(:signature) { Rack::Test::UploadedFile.new('spec/fixtures/files/black.png', 'image/png') } let(:signature) { Rack::Test::UploadedFile.new('spec/fixtures/files/black.png', 'image/png') }
let(:groupe_instructeur_1) { create(:groupe_instructeur, procedure: procedure, label: "groupe_1") } let(:groupe_instructeur_1) { create(:groupe_instructeur, procedure: procedure, label: "groupe_1", contact_information: create(:contact_information)) }
let(:instructeur_1) { create(:instructeur) } let(:instructeur_1) { create(:instructeur) }
let(:instructeur_2) { create(:instructeur) } let(:instructeur_2) { create(:instructeur) }
let!(:assign_to_1) { create(:assign_to, procedure: procedure, groupe_instructeur: groupe_instructeur_1, instructeur: instructeur_1) } let!(:assign_to_1) { create(:assign_to, procedure: procedure, groupe_instructeur: groupe_instructeur_1, instructeur: instructeur_1) }
@ -594,6 +594,11 @@ describe Procedure do
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
end end
it 'should clone groupe instructeur services' do
expect(procedure.groupe_instructeurs.last.contact_information).not_to eq nil
expect(subject.groupe_instructeurs.last.contact_information).not_to eq nil
end
end end
it 'should reset duree_conservation_etendue_par_ds' do it 'should reset duree_conservation_etendue_par_ds' do
@ -707,6 +712,13 @@ describe Procedure do
expect(subject.service).to eq(nil) expect(subject.service).to eq(nil)
end end
context 'with groupe instructeur services' do
it 'should not clone groupe instructeur services' do
expect(procedure.groupe_instructeurs.last.contact_information).not_to eq nil
expect(subject.groupe_instructeurs.last.contact_information).to eq nil
end
end
it 'should discard old pj information' do it 'should discard old pj information' do
subject.draft_revision.types_de_champ_public.each do |stc| subject.draft_revision.types_de_champ_public.each do |stc|
expect(stc.old_pj).to be_nil expect(stc.old_pj).to be_nil

View file

@ -92,6 +92,25 @@ describe RoutingEngine, type: :model do
end end
end end
context 'with a regions type de champ' do
let(:procedure) do
create(:procedure, types_de_champ_public: [{ type: :regions }]).tap do |p|
p.groupe_instructeurs.create(label: 'a third group')
end
end
let(:regions_tdc) { procedure.draft_revision.types_de_champ.first }
context 'with a matching rule' do
before do
gi_2.update(routing_rule: ds_eq(champ_value(regions_tdc.stable_id), constant('04')))
dossier.champs.first.update(value: 'La Réunion')
end
it { is_expected.to eq(gi_2) }
end
end
context 'routing rules priorities' do context 'routing rules priorities' do
let(:procedure) do let(:procedure) do
create(:procedure, create(:procedure,

View file

@ -0,0 +1,23 @@
RSpec.describe WatermarkService do
let(:image) { file_fixture("logo_test_procedure.png") }
let(:watermark_service) { WatermarkService.new }
describe '#process' do
it 'returns a tempfile if watermarking succeeds' do
Tempfile.create do |output|
watermark_service.process(image, output)
# output size should always be a little greater than input size
expect(output.size).to be_between(image.size, image.size * 1.5)
end
end
it 'returns nil if metadata is blank' do
allow(watermark_service).to receive(:image_metadata).and_return(nil)
Tempfile.create do |output|
expect(watermark_service.process(image.to_path, output)).to be_nil
expect(output.size).to eq(0)
end
end
end
end