Merge branch 'demarches-simplifiees:main' into poc-self_hosted_runners
3
.github/workflows/ci.yml
vendored
|
@ -80,6 +80,9 @@ jobs:
|
|||
- name: Setup the app runtime and dependencies
|
||||
uses: ./.github/actions/ci-setup-rails
|
||||
|
||||
- name: Install fonts pickable by ImageMagick
|
||||
run: sudo apt-get install -y gsfonts
|
||||
|
||||
- name: Pre-compile assets
|
||||
uses: ./.github/actions/ci-setup-assets
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ Vous souhaitez y apporter des changements ou des améliorations ? Lisez notre [
|
|||
#### Tous environnements
|
||||
|
||||
- postgresql
|
||||
- imagemagick et gsfonts pour générer les filigranes sur les titres d'identité.
|
||||
|
||||
#### Développement
|
||||
|
||||
|
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 480 B |
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 878 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
app/assets/images/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 9.7 KiB |
|
@ -62,11 +62,6 @@
|
|||
width: 110px;
|
||||
}
|
||||
|
||||
.action-col {
|
||||
text-align: right;
|
||||
padding-left: $default-spacer;
|
||||
padding-right: $default-spacer;
|
||||
}
|
||||
|
||||
.follow-col {
|
||||
width: 450px;
|
||||
|
|
|
@ -15,10 +15,6 @@ class Dossiers::NotifiedToggleComponent < ApplicationComponent
|
|||
sorted_by_notifications? && order_desc?
|
||||
end
|
||||
|
||||
def icon_class_name
|
||||
active? ? 'fr-fi-checkbox' : 'fr-fi-checkbox-blank'
|
||||
end
|
||||
|
||||
def order_desc?
|
||||
current_order == 'desc'
|
||||
end
|
||||
|
|
|
@ -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
|
||||
.fr-toggle
|
||||
= check_box_tag :order, opposite_order, active?, class: 'fr-toggle__input'
|
||||
= label_tag :order, t('.show_notified_first'), class: 'fr-toggle__label fr-pl-1w'
|
||||
= submit_tag t('.show_notified_first'), data: {"checkbox-target": 'submit' }, class: 'visually-hidden'
|
||||
.fr-fieldset__element.fr-m-0
|
||||
.fr-checkbox-group.fr-checkbox-group--sm
|
||||
= 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'
|
||||
|
|
|
@ -4,7 +4,7 @@ fr:
|
|||
Le routage permet d’acheminer les dossiers vers différents groupes d’instructeurs.
|
||||
routing_configuration_notice_2_html: |
|
||||
<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>
|
||||
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: |
|
||||
|
|
|
@ -81,6 +81,8 @@ class Procedure::OneGroupeManagementComponent < ApplicationComponent
|
|||
case @revision.types_de_champ_public.find_by(stable_id: targeted_champ.stable_id).type_champ
|
||||
when TypeDeChamp.type_champs.fetch(:departements)
|
||||
departements_for_select
|
||||
when TypeDeChamp.type_champs.fetch(:regions)
|
||||
regions_for_select
|
||||
when TypeDeChamp.type_champs.fetch(:drop_down_list)
|
||||
targeted_champ
|
||||
.options(@revision.types_de_champ_public)
|
||||
|
@ -91,4 +93,8 @@ class Procedure::OneGroupeManagementComponent < ApplicationComponent
|
|||
def departements_for_select
|
||||
APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", constant(_1[:code]).to_json] }
|
||||
end
|
||||
|
||||
def regions_for_select
|
||||
APIGeoService.regions.map { ["#{_1[:code]} – #{_1[:name]}", constant(_1[:code]).to_json] }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,23 +43,13 @@ module Administrateurs
|
|||
case tdc.type_champ
|
||||
when TypeDeChamp.type_champs.fetch(:departements)
|
||||
tdc_options = APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
|
||||
tdc_options.each do |code_and_name, code|
|
||||
routing_rule = ds_eq(champ_value(stable_id), constant(code))
|
||||
@procedure
|
||||
.groupe_instructeurs
|
||||
.find_or_create_by(label: code_and_name)
|
||||
.update(instructeurs: [current_administrateur.instructeur], routing_rule:)
|
||||
end
|
||||
|
||||
create_groups_from_territorial_tdc(tdc_options, stable_id)
|
||||
when TypeDeChamp.type_champs.fetch(:regions)
|
||||
tdc_options = APIGeoService.regions.map { ["#{_1[:code]} – #{_1[:name]}", _1[:code]] }
|
||||
create_groups_from_territorial_tdc(tdc_options, stable_id)
|
||||
when TypeDeChamp.type_champs.fetch(:drop_down_list)
|
||||
tdc_options = tdc.drop_down_options.reject(&:empty?)
|
||||
tdc_options.each do |option_label|
|
||||
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
|
||||
create_groups_from_drop_down_list_tdc(tdc_options, stable_id)
|
||||
end
|
||||
|
||||
if tdc.drop_down_other?
|
||||
|
@ -460,5 +450,25 @@ module Administrateurs
|
|||
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"
|
||||
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
|
||||
|
|
|
@ -21,7 +21,7 @@ class ApplicationController < ActionController::Base
|
|||
around_action :switch_locale
|
||||
|
||||
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
|
||||
Current.request_id = request.uuid
|
||||
|
@ -62,6 +62,10 @@ class ApplicationController < ActionController::Base
|
|||
current_user&.expert
|
||||
end
|
||||
|
||||
def current_expert_not_instructeur?
|
||||
current_user&.expert? && !current_user&.instructeur?
|
||||
end
|
||||
|
||||
def expert_signed_in?
|
||||
current_expert.present?
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -73,6 +73,8 @@ module Instructeurs
|
|||
@avis = Avis.new
|
||||
if @dossier.procedure.experts_require_administrateur_invitation?
|
||||
@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
|
||||
|
||||
|
@ -81,6 +83,8 @@ module Instructeurs
|
|||
@avis = Avis.new
|
||||
if @dossier.procedure.experts_require_administrateur_invitation?
|
||||
@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
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class ArchiveCreationJob < ApplicationJob
|
||||
discard_on ActiveRecord::RecordNotFound
|
||||
|
||||
queue_as :archives
|
||||
|
||||
def max_run_time
|
||||
|
|
|
@ -10,77 +10,18 @@ class TitreIdentiteWatermarkJob < ApplicationJob
|
|||
# (to avoid modifying the file while it is being scanned).
|
||||
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)
|
||||
return if blob.watermark_done?
|
||||
raise FileNotScannedYetError if blob.virus_scanner.pending?
|
||||
|
||||
blob.open do |file|
|
||||
watermark = resize_watermark(file)
|
||||
|
||||
if watermark.present?
|
||||
processed = watermark_image(file, watermark)
|
||||
Tempfile.create(["watermarked", File.extname(file)]) do |output|
|
||||
processed = WatermarkService.new.process(file, output)
|
||||
return if processed.blank?
|
||||
|
||||
blob.upload(processed)
|
||||
blob.touch(:watermarked_at)
|
||||
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
|
||||
|
|
17
app/models/contact_information.rb
Normal 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
|
|
@ -1304,6 +1304,10 @@ class Dossier < ApplicationRecord
|
|||
)
|
||||
end
|
||||
|
||||
def service
|
||||
groupe_instructeur&.contact_information || procedure.service
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_missing_traitemets
|
||||
|
|
|
@ -13,6 +13,7 @@ class GroupeInstructeur < ApplicationRecord
|
|||
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 :contact_information
|
||||
|
||||
validates :label, presence: true, allow_nil: false
|
||||
validates :label, uniqueness: { scope: :procedure }
|
||||
|
@ -106,6 +107,8 @@ class GroupeInstructeur < ApplicationRecord
|
|||
options = case routing_tdc.type_champ
|
||||
when TypeDeChamp.type_champs.fetch(:departements)
|
||||
APIGeoService.departements.map { _1[:code] }
|
||||
when TypeDeChamp.type_champs.fetch(:regions)
|
||||
APIGeoService.regions.map { _1[:code] }
|
||||
when TypeDeChamp.type_champs.fetch(:drop_down_list)
|
||||
routing_tdc.options_with_drop_down_other
|
||||
end
|
||||
|
|
|
@ -44,7 +44,7 @@ class Logic::ChampValue < Logic::Term
|
|||
targeted_champ.selected
|
||||
when "Champs::MultipleDropDownListChamp"
|
||||
targeted_champ.selected_options
|
||||
when "Champs::DepartementChamp"
|
||||
when "Champs::DepartementChamp", "Champs::RegionChamp"
|
||||
targeted_champ.code
|
||||
end
|
||||
end
|
||||
|
|
|
@ -469,7 +469,7 @@ class Procedure < ApplicationRecord
|
|||
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|
|
||||
PiecesJustificativesService.clone_attachments(original, kopy)
|
||||
end
|
||||
|
|
|
@ -224,7 +224,7 @@ class ProcedureRevision < ApplicationRecord
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
private
|
||||
|
|
118
app/services/watermark_service.rb
Normal 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
|
|
@ -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
|
|
@ -13,3 +13,6 @@
|
|||
instructeurs: @instructeurs,
|
||||
available_instructeur_emails: @available_instructeur_emails,
|
||||
disabled_as_super_admin: administrateur_as_manager? }
|
||||
= render partial: 'administrateurs/groupe_instructeurs/contact_information',
|
||||
locals: { procedure: @procedure,
|
||||
groupe_instructeur: @groupe_instructeur }
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
|
||||
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
|
||||
%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
|
||||
|
||||
%title
|
||||
= content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME
|
||||
|
||||
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16")
|
||||
= 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")
|
||||
= render partial: "layouts/favicons"
|
||||
|
||||
= vite_client_tag
|
||||
= vite_typescript_tag 'playground'
|
||||
|
|
39
app/views/instructeurs/contact_informations/_form.html.haml
Normal 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 s’ils 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 qu’en pied de page lors du dépôt d’un dossier.
|
||||
%br
|
||||
%br
|
||||
⚠️ En cas d’informations 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'
|
10
app/views/instructeurs/contact_informations/edit.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
|
||||
['Groupes d’instructeurs', 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 }
|
10
app/views/instructeurs/contact_informations/new.html.haml
Normal file
|
@ -0,0 +1,10 @@
|
|||
= render partial: 'administrateurs/breadcrumbs',
|
||||
locals: { steps: [[@procedure.libelle.truncate_words(10), instructeur_procedure_path(@procedure)],
|
||||
['Groupes d’instructeurs', 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 }
|
|
@ -48,3 +48,20 @@
|
|||
class: 'button' }
|
||||
|
||||
= 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
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
- if i > 0
|
||||
= " ou "
|
||||
= 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)}"
|
||||
|
|
|
@ -65,23 +65,7 @@
|
|||
= render Dossiers::NotifiedToggleComponent.new(procedure: @procedure, procedure_presentation: @procedure_presentation)
|
||||
|
||||
.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'
|
||||
= 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')
|
||||
|
@ -122,10 +106,26 @@
|
|||
- @procedure_presentation.displayed_fields_for_headers.each do |field|
|
||||
= render partial: "header_field", locals: { field: field, classname: field['classname'] }
|
||||
|
||||
%th.action-col.follow-col
|
||||
%th.follow-col
|
||||
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
|
||||
= render Dossiers::BatchSelectMoreComponent.new(dossiers_count: @dossiers_count, filtered_sorted_ids: @filtered_sorted_ids)
|
||||
|
@ -179,7 +179,7 @@
|
|||
%span.cell-link
|
||||
= 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
|
||||
= render partial: 'instructeurs/procedures/dossier_actions', locals: { procedure_id: @procedure.id,
|
||||
dossier_id: p.dossier_id,
|
||||
|
@ -191,6 +191,7 @@
|
|||
sva_svr: @procedure.sva_svr_enabled?,
|
||||
turbo: false,
|
||||
with_menu: false }
|
||||
|
||||
%tfoot
|
||||
%tr
|
||||
%td.force-table-100{ colspan: @procedure_presentation.displayed_fields_for_headers.size + 2 }
|
||||
|
|
5
app/views/layouts/_favicons.html.haml
Normal 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" }
|
|
@ -4,14 +4,14 @@
|
|||
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
|
||||
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
|
||||
%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
|
||||
|
||||
%title
|
||||
= content_for?(:title) ? "#{sanitize(yield(:title))} · #{APPLICATION_NAME}" : APPLICATION_NAME
|
||||
|
||||
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16")
|
||||
= 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")
|
||||
= render partial: "layouts/favicons"
|
||||
|
||||
= Gon::Base.render_data(camel_case: true, init: true, nonce: request.content_security_policy_nonce)
|
||||
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
|
||||
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
|
||||
%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
|
||||
|
||||
%title
|
||||
= content_for?(:title) ? "#{yield(:title)} · #{APPLICATION_NAME}" : APPLICATION_NAME
|
||||
|
||||
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16")
|
||||
= 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")
|
||||
= render partial: "layouts/favicons"
|
||||
|
||||
= vite_client_tag
|
||||
= vite_javascript_tag 'application'
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
%meta{ "http-equiv": "Content-Type", content: "text/html; charset=UTF-8" }
|
||||
%meta{ "http-equiv": "X-UA-Compatible", content: "IE=edge" }
|
||||
%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
|
||||
|
||||
%title
|
||||
= t("dynamics.page_title")
|
||||
|
||||
= favicon_link_tag(image_url("#{FAVICON_16PX_SRC}"), type: "image/png", sizes: "16x16")
|
||||
= 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")
|
||||
= render partial: "layouts/favicons"
|
||||
|
||||
%body
|
||||
= yield
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
%th Démarche
|
||||
%th Demandeur
|
||||
%th.status-col Statut
|
||||
%th.action-col.follow-col
|
||||
%th.follow-col
|
||||
%tbody
|
||||
- @projected_dossiers.each do |p|
|
||||
- procedure_libelle, user_email, procedure_id = p.columns
|
||||
|
@ -67,7 +67,7 @@
|
|||
|
||||
|
||||
- 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|
|
||||
- menu.with_button_inner_html do
|
||||
Actions
|
||||
|
@ -86,12 +86,12 @@
|
|||
|
||||
- elsif instructeur_dossier
|
||||
- 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
|
||||
= t('views.instructeurs.dossiers.restore')
|
||||
|
||||
- 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
|
||||
= render partial: "instructeurs/procedures/dossier_actions",
|
||||
locals: { procedure_id: procedure_id,
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
= hidden_field_tag 'avis[emails]', nil
|
||||
.fr-input-group
|
||||
= react_component("ComboMultiple",
|
||||
options: @dossier.procedure.experts_require_administrateur_invitation ? @experts_emails : [],
|
||||
options: current_expert_not_instructeur? ? [] : @experts_emails,
|
||||
selected: [], disabled: [],
|
||||
label: 'Emails',
|
||||
group: '.ask-avis',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%footer.fr-footer.footer-procedure#footer{ role: "contentinfo" }
|
||||
- service = procedure.service
|
||||
- service = dossier&.service || procedure.service
|
||||
.fr-footer__top.fr-mb-0
|
||||
.fr-container
|
||||
.fr-grid-row.fr-grid-row--start.fr-grid-row--gutters
|
||||
|
@ -21,6 +21,7 @@
|
|||
= I18n.t('users.procedure_footer.contact.email.link')
|
||||
= link_to service.email, "mailto:#{service.email}", class: "fr-footer__top-link"
|
||||
|
||||
- if service.present?
|
||||
- if service.telephone.present? || service.horaires.present?
|
||||
%li
|
||||
- horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}"
|
||||
|
|
|
@ -46,9 +46,12 @@ DS_ENV="staging"
|
|||
# STATUS_PAGE_URL=""
|
||||
|
||||
# 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_32PX_SRC="favicons/32x32.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"
|
||||
# HEADER_LOGO_SRC="marianne.png"
|
||||
|
@ -68,9 +71,6 @@ DS_ENV="staging"
|
|||
# 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"
|
||||
|
||||
# Instance customization: watermark for identity documents
|
||||
# WATERMARK_FILE=""
|
||||
|
||||
# Enabling maintenance mode
|
||||
# MAINTENANCE_MODE="true"
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
# Favicons
|
||||
FAVICON_16PX_SRC = ENV.fetch("FAVICON_16PX_SRC", "favicons/16x16.png")
|
||||
FAVICON_32PX_SRC = ENV.fetch("FAVICON_32PX_SRC", "favicons/32x32.png")
|
||||
FAVICON_96PX_SRC = ENV.fetch("FAVICON_96PX_SRC", "favicons/96x96.png")
|
||||
FAVICONS_SRC = {
|
||||
"16px" => ENV.fetch("FAVICON_16PX_SRC", "favicons/16x16.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_SRC = ENV.fetch("HEADER_LOGO_SRC", "marianne.png")
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
WATERMARK_FILE = ENV.fetch('WATERMARK_FILE', 'watermark.png')
|
|
@ -395,7 +395,7 @@ en:
|
|||
batch_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"
|
||||
personalize: Personalize the table
|
||||
personalize: Personalize
|
||||
show_deleted_dossiers: Show deleted files
|
||||
follow_file: Follow-up the file
|
||||
save: Save
|
||||
|
|
|
@ -397,7 +397,7 @@ fr:
|
|||
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"
|
||||
show_deleted_dossiers: Afficher les dossiers supprimés
|
||||
personalize: Personnaliser le tableau
|
||||
personalize: Personnaliser
|
||||
download: Télécharger un dossier
|
||||
follow_file: Suivre le dossier
|
||||
stop_follow: Ne plus suivre
|
||||
|
|
|
@ -5,7 +5,7 @@ fr:
|
|||
one: 'Service'
|
||||
other: 'Services'
|
||||
attributes:
|
||||
service:
|
||||
service: &service
|
||||
adresse: 'Adresse postale'
|
||||
email: 'Email de contact'
|
||||
telephone: 'Téléphone'
|
||||
|
@ -27,6 +27,7 @@ fr:
|
|||
Exemple : Du lundi au vendredi de 9h30 à 17h30, le samedi de 9h30 à 12h.
|
||||
adresse: |
|
||||
Indiquez l’adresse à laquelle un usager peut vous contacter, par exemple s’il n’est pas en capacité de compléter son formulaire en ligne.
|
||||
contact_information: *service
|
||||
|
||||
errors:
|
||||
models:
|
||||
|
|
|
@ -392,6 +392,7 @@ Rails.application.routes.draw do
|
|||
resources :archives, only: [:index, :create]
|
||||
|
||||
resources :groupes, only: [:index, :show], controller: 'groupe_instructeurs' do
|
||||
resource :contact_information
|
||||
member do
|
||||
post 'add_instructeur'
|
||||
delete 'remove_instructeur'
|
||||
|
|
15
db/migrate/20230809151357_create_contact_informations.rb
Normal 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
|
16
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# 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
|
||||
enable_extension "pgcrypto"
|
||||
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"
|
||||
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|
|
||||
t.boolean "closed", default: 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", "experts"
|
||||
add_foreign_key "commentaires", "instructeurs"
|
||||
add_foreign_key "contact_informations", "groupe_instructeurs"
|
||||
add_foreign_key "dossier_assignments", "dossiers"
|
||||
add_foreign_key "dossier_batch_operations", "batch_operations"
|
||||
add_foreign_key "dossier_batch_operations", "dossiers"
|
||||
|
|
|
@ -22,4 +22,32 @@ namespace :pjs do
|
|||
|
||||
blobs.in_batches { |batch| batch.ids.each { |id| PjsMigrationJob.perform_later(id) } }
|
||||
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
|
||||
|
|
|
@ -761,6 +761,26 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do
|
|||
expect(procedure3.routing_enabled).to be_truthy
|
||||
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
|
||||
|
||||
describe '#wizard' do
|
||||
|
|
|
@ -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
|
11
spec/factories/contact_information.rb
Normal 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
|
62
spec/models/contact_information_spec.rb
Normal 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
|
|
@ -76,6 +76,12 @@ describe Logic::ChampValue do
|
|||
it { is_expected.to eq(true) }
|
||||
end
|
||||
|
||||
context 'region tdc' do
|
||||
let(:champ) { create(:champ_regions, value: 'La Réunion') }
|
||||
|
||||
it { is_expected.to eq('04') }
|
||||
end
|
||||
|
||||
describe 'errors' do
|
||||
let(:champ) { create(:champ) }
|
||||
|
||||
|
|
|
@ -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: :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: :regions, libelle: 'l4')
|
||||
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
|
||||
|
|
|
@ -563,7 +563,7 @@ describe Procedure do
|
|||
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(: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_2) { create(:instructeur) }
|
||||
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
|
||||
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
|
||||
|
||||
it 'should reset duree_conservation_etendue_par_ds' do
|
||||
|
@ -707,6 +712,13 @@ describe Procedure do
|
|||
expect(subject.service).to eq(nil)
|
||||
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
|
||||
subject.draft_revision.types_de_champ_public.each do |stc|
|
||||
expect(stc.old_pj).to be_nil
|
||||
|
|
|
@ -92,6 +92,25 @@ describe RoutingEngine, type: :model do
|
|||
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
|
||||
let(:procedure) do
|
||||
create(:procedure,
|
||||
|
|
23
spec/services/watermark_service_spec.rb
Normal 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
|