Compare commits

...

33 commits

Author SHA1 Message Date
Paul Chavard
812edf8297
Merge pull request #8962 from mfo/US/fix-date-input-edge
ETQ usager utilisant microsoft Edge, les champs de type date sont mal alignés
2023-04-26 15:32:18 +00:00
Paul Chavard
c1ac3ad12a
Merge pull request #8963 from tchak/fix-revision-changes-delete-champs-message
ETQ administrateur, je veux être averti si des données seront supprimées sur les dossiers lors de la publication d’une nouvelle révision
2023-04-26 15:12:31 +00:00
Paul Chavard
f235802249
Merge pull request #8899 from demarches-simplifiees/8738-validate-adresse-electronique
ETQ usager, je veux que les champs de type adresse électronique soit validé
2023-04-26 15:04:43 +00:00
Paul Chavard
63aec4f556
Merge pull request #8921 from demarches-simplifiees/8827-user-path-translations-fix
ETQ usager, je veux que mon interface soit traduite en anglais
2023-04-26 15:03:12 +00:00
Lisa Durand
e393132fd8
Merge pull request #8936 from demarches-simplifiees/add-yes-no-column-for-instructeurs
[instructeur] ajout du nb de réponses oui/non aux avis dans le tableau d'une procedure
2023-04-26 09:05:58 +00:00
LeSim
1a704f0479
Merge pull request #8966 from mfo/US/fix-avis-remind-with-question
ETQ instructeur, je peux relancer une demande d'avis meme si celui ci contient une question
2023-04-26 08:09:41 +00:00
LeSim
c8ed0532ed
Merge pull request #8948 from colinux/fix-geoarea-geometry-blank
ETQ Usager, champ carte: ne permet pas d'enregistrer une geometry null pour ne pas casser les exports
2023-04-26 10:15:25 +02:00
LeSim
14c9012b87
Merge pull request #8967 from demarches-simplifiees/fix-conditionel-eq-multi-dropdown
ETQ administrateur empêche une condition d'égalité de s'applique à un champ choix multiple
2023-04-26 10:09:02 +02:00
Colin Darie
21a829ec1e fix(conditionel): error when using operator eq/not_eq with a multi dropdown 2023-04-26 09:58:00 +02:00
LeSim
52dff40197
Merge pull request #8954 from demarches-simplifiees/fix_include_in_logic
Corrige la logic dans le cas de comparaise avec une liste de choix multiple
2023-04-26 09:52:12 +02:00
simon lehericey
f1bcb84832 fix: replace ds_eq operator by ds_include when targeted_champ is a multiple_drop_down_list 2023-04-26 09:37:03 +02:00
Martin
1ae40f1a22 correctif(instructeurs/avis_controller#remind): ETQ instructeur, je peux relancer une demande d'avis meme si celui ci contient une question 2023-04-26 07:46:21 +02:00
Martin
6a2d2ae0a7 bug(instructeurs/avis_controller#remind): ETQ instructeur, je peux relancer une demande d'avis meme si celui ci contient une question 2023-04-26 07:46:21 +02:00
Martin
9c695cf39f correctif(ui.edge): les champs de type date etaient mal alignés 2023-04-26 07:15:51 +02:00
Colin Darie
02fab28ad6
Merge pull request #8964 from colinux/pdf-no-unbreakable
ETQ Utilisateur, je ne veux pas de caractère invalide dans l'état du dossier en PDF
2023-04-25 22:29:47 +00:00
Colin Darie
0a2b24aea2 fix(pdf): remove unbreakable spaces from dossier state, which are invalid in PDF 2023-04-25 18:36:49 +02:00
Lisa Durand
d52ee477b0 remove '+' and use variable in I18n 2023-04-25 14:30:39 +02:00
Lisa Durand
384b7f9fac simplify humanized groupe instructeur filter 2023-04-25 14:15:40 +02:00
Julie Salha
e6004d83f3
Merge branch 'main' into 8827-user-path-translations-fix 2023-04-25 09:36:05 +02:00
Colin Darie
f5c8271e26
fix(geoarea): fix again new geo_areas#geometry nil 2023-04-22 16:23:39 +02:00
Colin Darie
25956c5141
fix(geoarea): geometry must not be nil 2023-04-22 16:23:39 +02:00
Lisa Durand
b52a2ca972 fix spec 2023-04-21 10:21:54 +02:00
krichtof
c2461f230c validate value for email champ 2023-04-20 18:27:02 +02:00
Lisa Durand
34697a3085 add missing translation for filter groupe instructeur 2023-04-20 17:32:36 +02:00
Lisa Durand
65730bcfcb display avis_anwser yes/no in table 2023-04-20 15:55:31 +02:00
Colin Darie
e5df31fd66 fix(yaml): yes & no are reserved keywords in yaml, so they have to be quoted 2023-04-19 18:26:40 +02:00
Julie Salha
f80cccba93 reset component yes no structure 2023-04-18 14:54:01 +02:00
Julie Salha
f4ffbbf042 add missing translations 2023-04-18 14:21:09 +02:00
Julie Salha
f039b6687a add translations account confirmation page 2023-04-18 14:11:58 +02:00
Julie Salha
b1dfc83c17 add translations upload group notice 2023-04-18 14:04:32 +02:00
Julie Salha
be16cb6f5e add missing translations footer email change password 2023-04-18 13:56:50 +02:00
Julie Salha
c37a54b65b add translations profile identification tokens 2023-04-18 13:51:41 +02:00
Julie Salha
93c5c52e19 add translations for yes-no radios form and update dsfr styles 2023-04-18 13:43:11 +02:00
46 changed files with 464 additions and 43 deletions

View file

@ -61,7 +61,7 @@
} }
input[type='date'] { input[type='date'] {
display: inline; display: inline-block;
} }
} }

View file

@ -0,0 +1,4 @@
en:
"yes": "Yes"
"no": "No"
legend: "Yes/No"

View file

@ -0,0 +1,4 @@
fr:
"yes": "Oui"
"no": "Non"
legend: "Oui/Non"

View file

@ -1,8 +1,11 @@
%fieldset.radios %fieldset.fr-fieldset
%legend.fr-fieldset__legend.visually-hidden
= t(".legend")
%label{ for: @champ.yes_input_id } %label{ for: @champ.yes_input_id }
= @form.radio_button :value, true, id: @champ.yes_input_id = @form.radio_button :value, true, id: @champ.yes_input_id
Oui = t(".yes")
%label{ for: @champ.no_input_id } %label{ for: @champ.no_input_id }
= @form.radio_button :value, false, id: @champ.no_input_id = @form.radio_button :value, false, id: @champ.no_input_id
Non = t(".no")

View file

@ -0,0 +1,5 @@
en:
tokens_title: API identification tokens
first_paragraph: These tokens are needed to make calls to the
second_paragraph: If you already have applications that use a token and you revoke it, access to the API will be blocked for those applications.
action: Create and display a new token

View file

@ -0,0 +1,5 @@
fr:
tokens_title: Jetons didentification de lAPI (token)
first_paragraph: Ces jetons sont nécessaire pour effectuer des appels vers lAPI de
second_paragraph: Si vous avez déjà des applications qui utilisent un jeton et vous le révoquez, laccès à lAPI sera bloqué pour ces applications.
action: Créer et afficher un nouveau jeton

View file

@ -1,7 +1,11 @@
.card.no-list{ 'data-turbo': 'true', id: dom_id(current_administrateur, :profil_api_token) } .card.no-list{ 'data-turbo': 'true', id: dom_id(current_administrateur, :profil_api_token) }
.card-title Jetons didentification de lAPI (token) .card-title
%p Ces jetons sont nécessaire pour effectuer des appels vers lAPI de #{APPLICATION_NAME}. = t('.tokens_title')
%p Si vous avez déjà des applications qui utilisent un jeton et vous le révoquez, laccès à lAPI sera bloqué pour ces applications. %p
= t('.first_paragraph')
#{APPLICATION_NAME}.
%p
= t('.second_paragraph')
= render Dsfr::ListComponent.new do |list| = render Dsfr::ListComponent.new do |list|
- api_and_packed_tokens.each do |(api_token, packed_token)| - api_and_packed_tokens.each do |(api_token, packed_token)|
@ -11,5 +15,4 @@
.fr-card__content .fr-card__content
= render Profile::APITokenComponent.new(api_token:, packed_token:) = render Profile::APITokenComponent.new(api_token:, packed_token:)
%br = button_to t('.action'), api_tokens_path, method: :post, class: "fr-btn fr-btn--secondary"
= button_to "Créer et afficher un nouveau jeton", api_tokens_path, method: :post, class: "fr-btn fr-btn--secondary"

View file

@ -0,0 +1,16 @@
en:
allowed_full_access_html: This token has access to <strong>all</strong> the procedures attached to your administrator account
allowed_procedures_html:
zero: This token has no access to <strong>any</strong> process.
one: This token has access to a selected process
other: This token has access to %{count} selected steps
security_one: For security reasons, it will not be re-posted, please note.
security_two: For security reasons, we can only show it to you when it is created.
action_all: Allow access to all procedures
action_choice: Allow access only to selected steps
add: Add
delete: Delete
token_procedures: This token has access to the procedures
revoke_token: Revoke token
reading_writing: Reading and writing
reading: Read only

View file

@ -4,3 +4,13 @@ fr:
zero: Ce jeton na accès à <strong>aucune</strong> démarche zero: Ce jeton na accès à <strong>aucune</strong> démarche
one: Ce jeton a accès a une démarche sélectionnée one: Ce jeton a accès a une démarche sélectionnée
other: Ce jeton a accès a %{count} démarches sélectionnées other: Ce jeton a accès a %{count} démarches sélectionnées
security_one: Pour des raisons de sécurité, il ne sera plus ré-affiché, notez-le bien.
security_two: Pour des raisons de sécurité, nous ne pouvons vous lafficher que lors de sa création.
action_all: Autoriser laccès a toutes les démarches
action_choice: Autoriser laccès seulement a des démarches choisies
add: Ajouter
delete: Supprimer
token_procedures: Ce jeton a accès aux démarches
revoke_token: Révoquer le jeton
reading_writing: En lecture et écriture
reading: En lecture seule

View file

@ -13,10 +13,12 @@
- button = render Dsfr::CopyButtonComponent.new(text: @packed_token, title: "Copier le jeton dans le presse-papier", success: "Le jeton a été copié dans le presse-papier") - button = render Dsfr::CopyButtonComponent.new(text: @packed_token, title: "Copier le jeton dans le presse-papier", success: "Le jeton a été copié dans le presse-papier")
= "#{@packed_token} #{button}" = "#{@packed_token} #{button}"
%p Pour des raisons de sécurité, il ne sera plus ré-affiché, notez-le bien. %p
= t('.security_one')
- else - else
%p Pour des raisons de sécurité, nous ne pouvons vous lafficher que lors de sa création. %p
= t('.security_two')
- if @api_token.full_access? - if @api_token.full_access?
%p.fr-text--lg %p.fr-text--lg
@ -26,33 +28,35 @@
= t('.allowed_procedures_html', count: @api_token.allowed_procedures.size) = t('.allowed_procedures_html', count: @api_token.allowed_procedures.size)
- if @api_token.allowed_procedures.empty? - if @api_token.allowed_procedures.empty?
= button_to "Autoriser laccès a toutes les démarches", @api_token, method: :patch, params: { api_token: { disallow_procedure_id: '0' } }, class: "fr-btn fr-btn--secondary" = button_to t('.action_all'), @api_token, method: :patch, params: { api_token: { disallow_procedure_id: '0' } }, class: "fr-btn fr-btn--secondary"
- else - else
%ul %ul
- @api_token.allowed_procedures.each do |procedure| - @api_token.allowed_procedures.each do |procedure|
%li.flex.justify-between.align-center %li.flex.justify-between.align-center
.truncate-80 .truncate-80
= "#{procedure.id} #{procedure.libelle}" = "#{procedure.id} #{procedure.libelle}"
= button_to "Supprimer", @api_token, method: :patch, params: { api_token: { disallow_procedure_id: procedure.id } }, class: "fr-btn fr-btn--secondary" = button_to t('.delete'), @api_token, method: :patch, params: { api_token: { disallow_procedure_id: procedure.id } }, class: "fr-btn fr-btn--secondary"
.fr-card__end .fr-card__end
= form_for @api_token, namespace: dom_id(@api_token, :allowed_procedures), html: { class: 'form form-ds-fr-white mb-3', data: { turbo: true } } do |f| = form_for @api_token, namespace: dom_id(@api_token, :allowed_procedures), html: { class: 'form form-ds-fr-white mb-3', data: { turbo: true } } do |f|
= f.label :allowed_procedure_ids do = f.label :allowed_procedure_ids do
Autoriser laccès seulement a des démarches choisies = t('.action_choice')
- @api_token.allowed_procedures.each do |procedure| - @api_token.allowed_procedures.each do |procedure|
= f.hidden_field :allowed_procedure_ids, value: procedure.id, multiple: true, id: dom_id(procedure, :allowed_procedure) = f.hidden_field :allowed_procedure_ids, value: procedure.id, multiple: true, id: dom_id(procedure, :allowed_procedure)
.flex.justify-between.align-center{ 'data-turbo-force': true } .flex.justify-between.align-center{ 'data-turbo-force': true }
= f.select :allowed_procedure_ids, procedures_to_allow_options, procedures_to_allow_select_options, { class: 'no-margin width-66 small', name: "api_token[allowed_procedure_ids][]" } = f.select :allowed_procedure_ids, procedures_to_allow_options, procedures_to_allow_select_options, { class: 'no-margin width-66 small', name: "api_token[allowed_procedure_ids][]" }
= f.button type: :submit, class: "fr-btn fr-btn--secondary" do = f.button type: :submit, class: "fr-btn fr-btn--secondary" do
Ajouter = t('.add')
= form_for @api_token, namespace: dom_id(@api_token, :write_access), html: { class: 'form form-ds-fr-white mb-3', data: { turbo: true, controller: 'autosubmit' } } do |f| = form_for @api_token, namespace: dom_id(@api_token, :write_access), html: { class: 'form form-ds-fr-white mb-3', data: { turbo: true, controller: 'autosubmit' } } do |f|
= f.label :write_access do = f.label :write_access do
Ce jeton a accès aux démarches = t('.token_procedures')
%label.toggle-switch.no-margin %label.toggle-switch.no-margin
= f.check_box :write_access, class: 'toggle-switch-checkbox' = f.check_box :write_access, class: 'toggle-switch-checkbox'
%span.toggle-switch-control.round %span.toggle-switch-control.round
%span.toggle-switch-label.on En lecture et écriture %span.toggle-switch-label.on
%span.toggle-switch-label.off En lecture seule = t('.reading_writing')
%span.toggle-switch-label.off
= t('.reading')
= button_to "Révoquer le jeton", api_token_path(@api_token), method: :delete, class: "fr-btn fr-btn--secondary", data: { turbo_confirm: "Confirmez-vous la révocation de ce jeton ? Les applications qui lutilisent actuellement seront bloquées." } = button_to t('.revoke_token'), api_token_path(@api_token), method: :delete, class: "fr-btn fr-btn--secondary", data: { turbo_confirm: "Confirmez-vous la révocation de ce jeton ? Les applications qui lutilisent actuellement seront bloquées." }

View file

@ -56,6 +56,10 @@ class TypesDeChampEditor::ConditionsErrorsComponent < ApplicationComponent
right: right.to_s.downcase) right: right.to_s.downcase)
in { type: :required_list } in { type: :required_list }
t('required_list', scope: '.errors') t('required_list', scope: '.errors')
in { type: :required_include, operator_name: "Logic::Eq" }
t("required_include.eq", scope: '.errors')
in { type: :required_include, operator_name: "Logic::NotEq" }
t("required_include.not_eq", scope: '.errors')
else else
nil nil
end end

View file

@ -1,9 +1,12 @@
--- ---
fr: en:
errors: errors:
not_available: "A targeted field is not available." not_available: "A targeted field is not available."
unmanaged: "The field « %{libelle} » is a « %{type_champ} » and cannot be used as conditional source." unmanaged: "The field « %{libelle} » is a « %{type_champ} » and cannot be used as conditional source."
incompatible: "The field « %{libelle} » is a « %{type_champ} ». It cannot be %{operator} « %{right} »." incompatible: "The field « %{libelle} » is a « %{type_champ} ». It cannot be %{operator} « %{right} »."
required_number: "« %{operator} » applies only to number." required_number: "« %{operator} » applies only to number."
required_list: "The « include » operator only applies to simple or multiple choice." required_list: "The « include » operator only applies to simple or multiple choice."
required_include:
eq: "The « is » operator does not apply to multiple dropdown list. Select the « includes » operator."
not_eq: "The « is not » operator does not apply to multiple dropdown list."
not_included: "« %{right} » is not included in « %{libelle} »." not_included: "« %{right} » is not included in « %{libelle} »."

View file

@ -6,4 +6,7 @@ fr:
incompatible: "Le champ « %{libelle} » est de type « %{type_champ} ». Il ne peut pas être %{operator} « %{right} »." incompatible: "Le champ « %{libelle} » est de type « %{type_champ} ». Il ne peut pas être %{operator} « %{right} »."
required_number: "« %{operator} » ne s'applique qu'à des nombres." required_number: "« %{operator} » ne s'applique qu'à des nombres."
required_list: "Lʼopérateur « inclus » ne s'applique qu'au choix simple ou multiple." required_list: "Lʼopérateur « inclus » ne s'applique qu'au choix simple ou multiple."
required_include:
eq: "Lʼopérateur « est » ne s'applique pas au choix multiple. Sélectionnez lopérateur « contient »."
not_eq: "Lʼopérateur « nest pas » ne s'applique pas au choix multiple."
not_included: "« %{right} » ne fait pas partie de « %{libelle} »." not_included: "« %{right} » ne fait pas partie de « %{libelle} »."

View file

@ -111,7 +111,7 @@ class Avis < ApplicationRecord
def remind_by!(revocator) def remind_by!(revocator)
return false if !remindable_by?(revocator) || answer.present? return false if !remindable_by?(revocator) || answer.present?
update!(reminded_at: Time.zone.now) update_column(:reminded_at, Time.zone.now)
end end
private private

View file

@ -21,4 +21,11 @@
# type_de_champ_id :integer # type_de_champ_id :integer
# #
class Champs::EmailChamp < Champs::TextChamp class Champs::EmailChamp < Champs::TextChamp
validates :value,
format: {
with: Devise.email_regexp,
message: I18n.t('invalid', scope: 'activerecord.errors.models.email_champ.attributes.value')
},
allow_nil: true,
if: -> { validation_context != :brouillon }
end end

View file

@ -51,7 +51,7 @@ class GeoArea < ApplicationRecord
scope :selections_utilisateur, -> { where(source: sources.fetch(:selection_utilisateur)) } scope :selections_utilisateur, -> { where(source: sources.fetch(:selection_utilisateur)) }
scope :cadastres, -> { where(source: sources.fetch(:cadastre)) } scope :cadastres, -> { where(source: sources.fetch(:cadastre)) }
validates :geometry, geo_json: true, allow_blank: false validates :geometry, geo_json: true, allow_nil: false
before_validation :normalize_geometry before_validation :normalize_geometry
def to_feature def to_feature

View file

@ -20,6 +20,12 @@ class Logic::Eq < Logic::BinaryOperator
stable_id: @left.stable_id, stable_id: @left.stable_id,
right: @right right: @right
} }
elsif @left.type(type_de_champs) == :enums
errors << {
type: :required_include,
stable_id: @left.try(:stable_id),
operator_name: self.class.name
}
end end
errors + @left.errors(type_de_champs) + @right.errors(type_de_champs) errors + @left.errors(type_de_champs) + @right.errors(type_de_champs)

View file

@ -60,7 +60,8 @@ class ProcedurePresentation < ApplicationRecord
fields.push( fields.push(
field_hash('user', 'email', type: :text), field_hash('user', 'email', type: :text),
field_hash('followers_instructeurs', 'email', type: :text), field_hash('followers_instructeurs', 'email', type: :text),
field_hash('groupe_instructeur', 'id', type: :enum) field_hash('groupe_instructeur', 'id', type: :enum),
field_hash('avis', 'id', type: :text)
) )
if procedure.for_individual if procedure.for_individual
@ -246,7 +247,7 @@ class ProcedurePresentation < ApplicationRecord
Dossier.human_attribute_name("state.#{filter['value']}") Dossier.human_attribute_name("state.#{filter['value']}")
elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id' elsif filter['table'] == 'groupe_instructeur' && filter['column'] == 'id'
instructeur.groupe_instructeurs instructeur.groupe_instructeurs
.find { _1.id == filter['value'].to_i }&.label || "Groupe Instucteur #{filter['value']}" .find { _1.id == filter['value'].to_i }&.label || filter['value']
else else
filter['value'] filter['value']
end end

View file

@ -92,6 +92,14 @@ class DossierProjectionService
.group_by { |dossier_id, _| dossier_id } .group_by { |dossier_id, _| dossier_id }
.to_h { |dossier_id, dossier_id_emails| [dossier_id, dossier_id_emails.sort.map { |_, email| email }&.join(', ')] } .to_h { |dossier_id, dossier_id_emails| [dossier_id, dossier_id_emails.sort.map { |_, email| email }&.join(', ')] }
# rubocop:enable Style/HashTransformValues # rubocop:enable Style/HashTransformValues
when 'avis'
# rubocop:disable Style/HashTransformValues
fields[0][:id_value_h] = Avis
.where(dossier_id: dossiers_ids)
.pluck('dossier_id', 'question_answer')
.group_by { |dossier_id, _| dossier_id }
.to_h { |dossier_id, question_answer| [dossier_id, question_answer.map { |_, answer| answer }&.compact&.tally&.map { |k, v| I18n.t("helpers.label.question_answer_with_count.#{k}", count: v) }&.join(' / ')] }
# rubocop:enable Style/HashTransformValues
end end
end end

View file

@ -1,9 +1,13 @@
class GeoJSONValidator < ActiveModel::EachValidator class GeoJSONValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
if options[:allow_nil] == false && value.nil?
record.errors.add(attribute, :blank, message: options[:message] || "ne peut pas être vide")
end
begin begin
RGeo::GeoJSON.decode(value.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) RGeo::GeoJSON.decode(value.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory)
rescue RGeo::Error::InvalidGeometry rescue RGeo::Error::InvalidGeometry
record.errors[attribute] << (options[:message] || "n'est pas un GeoJSON valide") record.errors.add(attribute, :invalid_geometry, message: options[:message] || "n'est pas un GeoJSON valide")
end end
end end
end end

View file

@ -10,6 +10,10 @@ def maybe_start_new_page(pdf, size)
end end
end end
def clean_string(str)
str.tr(' ', ' ') # replace non breaking space, which are invalid in pdf
end
def text_box(pdf, text, x, width) def text_box(pdf, text, x, width)
box = ::Prawn::Text::Box.new(text.to_s, box = ::Prawn::Text::Box.new(text.to_s,
document: pdf, document: pdf,
@ -158,7 +162,7 @@ def add_single_champ(pdf, champ)
pdf.indent(default_margin) do pdf.indent(default_margin) do
champ.geo_areas.each do |area| champ.geo_areas.each do |area|
pdf.text "- #{area.label}".tr(' ', ' ') # replace non breaking space, which are invalid in pdf pdf.text "- #{clean_string(area.label)}"
end end
end end
end end
@ -219,7 +223,7 @@ end
def add_etat_dossier(pdf, dossier) def add_etat_dossier(pdf, dossier)
pdf.pad_bottom(default_margin) do pdf.pad_bottom(default_margin) do
pdf.text "Ce dossier est <b>#{dossier_display_state(dossier, lower: true)}</b>.", inline_format: true pdf.text "Ce dossier est <b>#{clean_string(dossier_display_state(dossier, lower: true))}</b>.", inline_format: true
end end
end end

View file

@ -5,4 +5,5 @@
= service.nom = service.nom
- else - else
-# The WORD JOINER unicode entity (&#8288;) prevents email clients from auto-linking the signature -# The WORD JOINER unicode entity (&#8288;) prevents email clients from auto-linking the signature
Léquipe #{APPLICATION_NAME.gsub(".","&#8288;.").html_safe} = t('.team')
#{APPLICATION_NAME.gsub(".","&#8288;.").html_safe}

View file

@ -61,7 +61,8 @@
.fr-upload-group .fr-upload-group
= label_tag :piece_jointe, class: 'fr-label' do = label_tag :piece_jointe, class: 'fr-label' do
= t('pj', scope: [:utils]) = t('pj', scope: [:utils])
%span.fr-hint-text Taille maximale : 200 Mo. Formats supportés : jpg, png, pdf. %span.fr-hint-text
= t('.notice_upload_group')
%p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } } %p.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } }
= t('.notice_pj_product') = t('.notice_pj_product')

View file

@ -31,8 +31,10 @@
- else - else
.blank-tab .blank-tab
%h2.empty-text Aucun dossier. %h2.empty-text
= t("views.users.dossiers.account_creation.empty")
%p.empty-text-details %p.empty-text-details
Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche. = t("views.users.dossiers.account_creation.detail_one")
%br %p.empty-text-details
Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx. = t("views.users.dossiers.account_creation.detail_two")
#{APPLICATION_BASE_URL}/commencer/xxx.

View file

@ -35,8 +35,10 @@
- else - else
.blank-tab .blank-tab
%h2.empty-text Aucun dossier. %h2.empty-text
= t("views.users.dossiers.account_creation.empty")
%p.empty-text-details %p.empty-text-details
Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche. = t("views.users.dossiers.account_creation.detail_one")
%br %p.empty-text-details
Celui ci doit ressembler à #{APPLICATION_BASE_URL}/commencer/xxx. = t("views.users.dossiers.account_creation.detail_two")
#{APPLICATION_BASE_URL}/commencer/xxx.

View file

@ -385,6 +385,10 @@ en:
dossier_not_in_instructor_group: "File no. %{dossier_id} of the “%{procedure_libelle}” procedure corresponds to your search, but it is attached to the “%{groupe_instructeur_label}” instructor group." dossier_not_in_instructor_group: "File no. %{dossier_id} of the “%{procedure_libelle}” procedure corresponds to your search, but it is attached to the “%{groupe_instructeur_label}” instructor group."
users: users:
dossiers: dossiers:
account_creation:
empty: "No file"
detail_one: "To complete a procedure, contact your administration and ask for the link to the procedure."
detail_two: "This one should look like"
archived_dossier: "Your file will be kept %{duree_conservation_dossiers_dans_ds} more months" archived_dossier: "Your file will be kept %{duree_conservation_dossiers_dans_ds} more months"
identite: identite:
identity_data: Identity data identity_data: Identity data
@ -561,6 +565,10 @@ en:
attributes: attributes:
email: email:
taken: ': Invitation already sent' taken: ': Invitation already sent'
email_champ:
attributes:
value:
invalid: "is invalid. Fill in a valid email address, example: john.doe@example.fr"
user: user:
attributes: &error_attributes attributes: &error_attributes

View file

@ -386,6 +386,10 @@ fr:
dossier_not_in_instructor_group: "Le dossier n° %{dossier_id} de la procédure « %{procedure_libelle} » correspond à votre recherche mais il est rattaché au groupe dinstructeurs « %{groupe_instructeur_label} »." dossier_not_in_instructor_group: "Le dossier n° %{dossier_id} de la procédure « %{procedure_libelle} » correspond à votre recherche mais il est rattaché au groupe dinstructeurs « %{groupe_instructeur_label} »."
users: users:
dossiers: dossiers:
account_creation:
empty: "Aucun dossier"
detail_one: "Pour remplir une démarche, contactez votre administration en lui demandant le lien de la démarche."
detail_two: "Celui ci doit ressembler à"
archived_dossier: "Votre dossier sera conservé %{duree_conservation_dossiers_dans_ds} mois supplémentaire" archived_dossier: "Votre dossier sera conservé %{duree_conservation_dossiers_dans_ds} mois supplémentaire"
identite: identite:
identity_data: Données didentité identity_data: Données didentité
@ -562,6 +566,10 @@ fr:
attributes: attributes:
email: email:
taken: ': Invitation déjà envoyée' taken: ': Invitation déjà envoyée'
email_champ:
attributes:
value:
invalid: "est invalide. Saisir une adresse éléctronique valide, exemple : john.doe@exemple.fr"
user: user:
attributes: &error_attributes attributes: &error_attributes
reset_password_token: reset_password_token:

View file

@ -21,6 +21,9 @@ en:
question_answer: question_answer:
true: 'yes' true: 'yes'
false: 'no' false: 'no'
question_answer_with_count:
true: "yes : %{count}"
false: "no : %{count}"
confirmation: confirmation:
revoke: "Would you like to revoke the opinion request to %{email} ?" revoke: "Would you like to revoke the opinion request to %{email} ?"
remind: "Would you like to remind %{email} ?" remind: "Would you like to remind %{email} ?"

View file

@ -21,6 +21,9 @@ fr:
question_answer: question_answer:
true: oui true: oui
false: non false: non
question_answer_with_count:
true: "oui : %{count}"
false: "non : %{count}"
confirmation: confirmation:
revoke: "Souhaitez-vous révoquer la demande davis à %{email} ?" revoke: "Souhaitez-vous révoquer la demande davis à %{email} ?"
remind: "Souhaitez-vous relancer %{email} ?" remind: "Souhaitez-vous relancer %{email} ?"

View file

@ -28,6 +28,8 @@ en:
prenom: First name prenom: First name
nom: Last name nom: Last name
gender: Title gender: Title
avis:
id: Opinion
etablissement: etablissement:
entreprise_siren: SIREN entreprise_siren: SIREN
entreprise_forme_juridique: Forme juridique entreprise_forme_juridique: Forme juridique

View file

@ -28,6 +28,8 @@ fr:
prenom: Prénom prenom: Prénom
nom: Nom nom: Nom
gender: Civilité gender: Civilité
avis:
id: Avis
etablissement: etablissement:
entreprise_siren: SIREN entreprise_siren: SIREN
entreprise_forme_juridique: Forme juridique entreprise_forme_juridique: Forme juridique

View file

@ -11,3 +11,5 @@ en:
by_email: "By email :" by_email: "By email :"
by_phone: "By phone :" by_phone: "By phone :"
schedule: "Schedule :" schedule: "Schedule :"
signature:
team: "The team"

View file

@ -1,3 +1,4 @@
fr: fr:
layouts: layouts:
mailers: mailers:
@ -11,3 +12,5 @@ fr:
by_email: "Par email :" by_email: "Par email :"
by_phone: "Par téléphone :" by_phone: "Par téléphone :"
schedule: "Horaires :" schedule: "Horaires :"
signature:
team: "Léquipe"

View file

@ -8,6 +8,7 @@ en:
our_answer: 👉 Our answer our_answer: 👉 Our answer
notice_pj_product: A screenshot can help us identify the element to improve. notice_pj_product: A screenshot can help us identify the element to improve.
notice_pj_other: A screenshot can help us identify the issue. notice_pj_other: A screenshot can help us identify the issue.
notice_upload_group: "Maximum size: 200 MB. Supported formats: jpg, png, pdf."
procedure_info: procedure_info:
question: I've encountered a problem while completing my application question: I've encountered a problem while completing my application
answer_html: "<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled? answer_html: "<p>Are you sure that all the mandatory fields (<span class= mandatory> * </span>) are properly filled?

View file

@ -8,6 +8,7 @@ fr:
our_answer: 👉 Notre réponse our_answer: 👉 Notre réponse
notice_pj_product: Une capture décran peut nous aider à identifier plus facilement lendroit à améliorer. notice_pj_product: Une capture décran peut nous aider à identifier plus facilement lendroit à améliorer.
notice_pj_other: Une capture décran peut nous aider à identifier plus facilement le problème. notice_pj_other: Une capture décran peut nous aider à identifier plus facilement le problème.
notice_upload_group: "Taille maximale : 200 Mo. Formats supportés : jpg, png, pdf."
procedure_info: procedure_info:
question: Jai un problème lors du remplissage de mon dossier question: Jai un problème lors du remplissage de mon dossier
answer_html: "<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ? answer_html: "<p>Avez-vous bien vérifié que tous les champs obligatoires (<span class= mandatory> * </span>) sont remplis ?

View file

@ -0,0 +1,13 @@
namespace :after_party do
desc 'Deployment task: fix_geo_area_without_geometry_again'
task fix_geo_area_without_geometry_again: :environment do
puts "Running deploy task 'fix_geo_area_without_geometry_again'"
Rake::Task['after_party:fix_geo_area_without_geometry'].invoke
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -0,0 +1,54 @@
namespace :after_party do
desc 'Deployment task: fix_include_in_logic'
task fix_include_in_logic: :environment do
include Logic
puts "Running deploy task 'fix_include_in_logic'"
tdcs_with_condition = TypeDeChamp.where.not(condition: nil)
progress = ProgressReport.new(tdcs_with_condition.count)
tdcs_with_condition.find_each do |tdc|
begin
tdc.revisions.each do |revision|
tdcs = revision.types_de_champ.where(stable_id: tdc.condition.sources)
transformed_condition = transform_eq_to_include(tdc.condition, tdcs)
if (transformed_condition != tdc.condition)
rake_puts "found #{tdc.id}, original: #{tdc.condition.to_s(tdcs)}, correction: #{transformed_condition.to_s(tdcs)}!"
new_tdc = revision.find_and_ensure_exclusive_use(tdc.stable_id)
new_tdc.update_columns(condition: transformed_condition)
Champ.joins(:dossier).where(dossier: { revision: revision }, type_de_champ: tdc).update(type_de_champ: new_tdc)
end
end
rescue StandardError => e
rake_puts "problem with tdc #{tdc.id},\ncondition: #{tdc.read_attribute_before_type_cast('condition')},\nmessage: #{e.message}"
ensure
progress.inc
end
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
def transform_eq_to_include(condition, tdcs)
case condition
when Logic::NAryOperator
condition.class.new(condition.operands.map { transform_eq_to_include(_1, tdcs) })
when Eq
target = tdcs.find { _1.stable_id == condition.left.stable_id }
if target.type_champ == 'multiple_drop_down_list'
ds_include(condition.left, condition.right)
else
condition
end
else
condition
end
end
end

View file

@ -72,6 +72,22 @@ describe TypesDeChampEditor::ConditionsErrorsComponent, type: :component do
it { expect(page).to have_content("« another choice » ne fait pas partie de « #{tdc.libelle} ».") } it { expect(page).to have_content("« another choice » ne fait pas partie de « #{tdc.libelle} ».") }
end end
context 'when an eq operator applies to a multiple_drop_down' do
let(:tdc) { create(:type_de_champ_multiple_drop_down_list) }
let(:upper_tdcs) { [tdc] }
let(:conditions) { [ds_eq(champ_value(tdc.stable_id), constant(tdc.drop_down_list_enabled_non_empty_options.first))] }
it { expect(page).to have_content("« est » ne s'applique pas au choix multiple.") }
end
context 'when an not_eq operator applies to a multiple_drop_down' do
let(:tdc) { create(:type_de_champ_multiple_drop_down_list) }
let(:upper_tdcs) { [tdc] }
let(:conditions) { [ds_not_eq(champ_value(tdc.stable_id), constant(tdc.drop_down_list_enabled_non_empty_options.first))] }
it { expect(page).to have_content("« nest pas » ne s'applique pas au choix multiple.") }
end
context 'when target became unavailable but a right still references the value' do context 'when target became unavailable but a right still references the value' do
# Cf https://demarches-simplifiees.sentry.io/issues/3625488398/events/53164e105bc94d55a004d69f96d58fb2/?project=1429550 # Cf https://demarches-simplifiees.sentry.io/issues/3625488398/events/53164e105bc94d55a004d69f96d58fb2/?project=1429550
# However maybe we should not have empty at left with still a constant at right # However maybe we should not have empty at left with still a constant at right

View file

@ -9,12 +9,13 @@ describe Instructeurs::AvisController, type: :controller do
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let(:procedure) { create(:procedure, :published, instructeurs: [instructeur]) } let(:procedure) { create(:procedure, :published, instructeurs: [instructeur]) }
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) } let(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
let!(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure) }
let!(:avis_without_answer) { create(:avis, dossier: dossier, claimant: claimant, experts_procedure: experts_procedure) } let!(:avis_without_answer) { create(:avis, dossier: dossier, claimant: claimant, experts_procedure: experts_procedure) }
before { sign_in(instructeur.user) } before { sign_in(instructeur.user) }
describe "#revoker" do describe "#revoker" do
let!(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure) }
before do before do
patch :revoquer, params: { procedure_id: procedure.id, id: avis.id } patch :revoquer, params: { procedure_id: procedure.id, id: avis.id }
end end
@ -28,6 +29,19 @@ describe Instructeurs::AvisController, type: :controller do
before do before do
allow(AvisMailer).to receive(:avis_invitation).and_return(double(deliver_later: nil)) allow(AvisMailer).to receive(:avis_invitation).and_return(double(deliver_later: nil))
end end
context 'without question' do
let!(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure) }
it 'sends a reminder to the expert' do
get :remind, params: { procedure_id: procedure.id, id: avis.id }
expect(AvisMailer).to have_received(:avis_invitation).once.with(avis)
expect(flash.notice).to eq("Un mail de relance a été envoyé à #{avis.expert.email}")
expect(avis.reload.reminded_at).to be_present
end
end
context 'with question' do
let!(:avis) { create(:avis, dossier: dossier, claimant: instructeur, experts_procedure: experts_procedure, question_label: '123') }
it 'sends a reminder to the expert' do it 'sends a reminder to the expert' do
get :remind, params: { procedure_id: procedure.id, id: avis.id } get :remind, params: { procedure_id: procedure.id, id: avis.id }
@ -38,3 +52,4 @@ describe Instructeurs::AvisController, type: :controller do
end end
end end
end end
end

View file

@ -2,6 +2,7 @@ FactoryBot.define do
factory :geo_area do factory :geo_area do
association :champ association :champ
properties { {} } properties { {} }
geometry { {} }
trait :cadastre do trait :cadastre do
source { GeoArea.sources.fetch(:cadastre) } source { GeoArea.sources.fetch(:cadastre) }

View file

@ -0,0 +1,123 @@
describe '20230424154715_fix_include_in_logic.rake' do
let(:rake_task) { Rake::Task['after_party:fix_include_in_logic'] }
include Logic
subject(:run_task) { rake_task.invoke }
after { rake_task.reenable }
context 'test condition correction' do
let(:procedure) do
types_de_champ_public = [
{ type: :multiple_drop_down_list },
{ type: :drop_down_list },
{ type: :integer_number },
{ type: :text }
]
create(:procedure, :published, types_de_champ_public:)
end
def multiple_stable_id = procedure.reload.published_types_de_champ_public.first.stable_id
def simple_stable_id = procedure.reload.published_types_de_champ_public.second.stable_id
def integer_tdc = procedure.reload.published_types_de_champ_public.third
def text_tdc = procedure.reload.published_types_de_champ_public.last
before do
and_condition = ds_and([
# incorrect: should change ds_eq => ds_include
ds_eq(champ_value(multiple_stable_id), constant("a")),
# correct
ds_include(champ_value(multiple_stable_id), constant("b")),
# correct ds_eq because drop_down_list
ds_eq(champ_value(simple_stable_id), constant("c"))
])
text_tdc.update(condition: and_condition)
or_condition = ds_or([
# incorrect: should change ds_eq => ds_include
ds_eq(champ_value(multiple_stable_id), constant("a"))
])
integer_tdc.update(condition: or_condition)
end
it do
run_task
expected_and_condition = ds_and([
ds_include(champ_value(multiple_stable_id), constant("a")),
ds_include(champ_value(multiple_stable_id), constant("b")),
ds_eq(champ_value(simple_stable_id), constant("c"))
])
expect(text_tdc.condition).to eq(expected_and_condition)
expected_or_condition = ds_or([
ds_include(champ_value(multiple_stable_id), constant("a"))
])
expect(integer_tdc.condition).to eq(expected_or_condition)
end
end
context 'test revision scope' do
let(:procedure) do
types_de_champ_public = [
{ type: :drop_down_list, options: [:a, :b, :c] },
{ type: :text }
]
create(:procedure, types_de_champ_public:)
end
let(:initial_condition) { ds_eq(champ_value(drop_down_stable_id), constant('a')) }
def drop_down_stable_id = procedure.reload.draft_types_de_champ_public.first.stable_id
def draft_text = procedure.reload.draft_types_de_champ_public.last
def published_text = procedure.reload.published_types_de_champ_public.last
before do
draft_text.update(condition: initial_condition)
procedure.publish!
procedure.reload
draft_drop_down = procedure.draft_revision.find_and_ensure_exclusive_use(drop_down_stable_id)
draft_drop_down.update(type_champ: 'multiple_drop_down_list')
end
it do
expect(draft_text.condition).to eq(initial_condition)
expect(published_text.condition).to eq(initial_condition)
run_task
# the text condition is invalid for the draft revision
expect(draft_text.condition).to eq(ds_include(champ_value(drop_down_stable_id), constant('a')))
# the published_text condition is untouched as it s still valid
expect(published_text.condition).to eq(initial_condition)
end
end
context 'test champ change' do
let!(:procedure) { create(:procedure, :published, types_de_champ_public: [{ type: :multiple_drop_down_list, options: ['a'] }, { type: :text }]) }
let!(:dossier) { create(:dossier, procedure:) }
def multiple_stable_id = procedure.reload.published_types_de_champ_public.first.stable_id
def text_tdc = procedure.reload.published_types_de_champ_public.last
let(:initial_condition) { ds_eq(champ_value(multiple_stable_id), constant('a')) }
let(:fixed_condition) { ds_include(champ_value(multiple_stable_id), constant('a')) }
before do
text_tdc.update(condition: initial_condition)
end
it do
expect(dossier.reload.champs_public.last.type_de_champ.condition).to eq(initial_condition)
run_task
expect(dossier.reload.champs_public.last.type_de_champ.condition).to eq(fixed_condition)
end
end
end

View file

@ -0,0 +1,29 @@
describe Champs::EmailChamp do
subject { build(:champ_email, value: value).tap(&:valid?) }
describe '#valid?' do
context 'when the value is an email' do
let(:value) { 'jean@dupont.fr' }
it { is_expected.to be_valid }
end
context 'when the value is not an email' do
let(:value) { 'jean@' }
it { is_expected.to_not be_valid }
end
context 'when the value is blank' do
let(:value) { '' }
it { is_expected.to_not be_valid }
end
context 'when the value is nil' do
let(:value) { nil }
it { is_expected.to be_valid }
end
end
end

View file

@ -81,6 +81,16 @@ RSpec.describe GeoArea, type: :model do
let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon, champ: nil) } let(:geo_area) { build(:geo_area, :invalid_right_hand_rule_polygon, champ: nil) }
it { expect(geo_area.errors).to have_key(:geometry) } it { expect(geo_area.errors).to have_key(:geometry) }
end end
context "nil" do
let(:geo_area) { build(:geo_area, geometry: nil) }
it { expect(geo_area.errors).to have_key(:geometry) }
end
context "allow empty {}" do
let(:geo_area) { build(:geo_area, geometry: {}) }
it { expect(geo_area.errors).not_to have_key(:geometry) }
end
end end
end end

View file

@ -17,6 +17,19 @@ describe Logic::Eq do
} }
expect(ds_eq(constant(true), constant(1)).errors).to eq([expected]) expect(ds_eq(constant(true), constant(1)).errors).to eq([expected])
end end
it do
multiple_drop_down = create(:type_de_champ_multiple_drop_down_list)
first_option = multiple_drop_down.drop_down_list_enabled_non_empty_options.first
expected = {
operator_name: "Logic::Eq",
stable_id: multiple_drop_down.stable_id,
type: :required_include
}
expect(ds_eq(champ_value(multiple_drop_down.stable_id), constant(first_option)).errors([multiple_drop_down])).to eq([expected])
end
end end
describe '#==' do describe '#==' do

View file

@ -17,6 +17,19 @@ describe Logic::NotEq do
} }
expect(ds_not_eq(constant(true), constant(1)).errors).to eq([expected]) expect(ds_not_eq(constant(true), constant(1)).errors).to eq([expected])
end end
it do
multiple_drop_down = create(:type_de_champ_multiple_drop_down_list)
first_option = multiple_drop_down.drop_down_list_enabled_non_empty_options.first
expected = {
operator_name: "Logic::NotEq",
stable_id: multiple_drop_down.stable_id,
type: :required_include
}
expect(ds_not_eq(champ_value(multiple_drop_down.stable_id), constant(first_option)).errors([multiple_drop_down])).to eq([expected])
end
end end
describe '#==' do describe '#==' do

View file

@ -73,6 +73,7 @@ describe ProcedurePresentation do
{ "label" => 'Demandeur', "table" => 'user', "column" => 'email', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' }, { "label" => 'Demandeur', "table" => 'user', "column" => 'email', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Email instructeur', "table" => 'followers_instructeurs', "column" => 'email', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' }, { "label" => 'Email instructeur', "table" => 'followers_instructeurs', "column" => 'email', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Groupe instructeur', "table" => 'groupe_instructeur', "column" => 'id', 'classname' => '', 'virtual' => false, 'type' => :enum, "scope" => '' }, { "label" => 'Groupe instructeur', "table" => 'groupe_instructeur', "column" => 'id', 'classname' => '', 'virtual' => false, 'type' => :enum, "scope" => '' },
{ "label" => 'Avis', "table" => 'avis', "column" => 'id', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'SIREN', "table" => 'etablissement', "column" => 'entreprise_siren', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' }, { "label" => 'SIREN', "table" => 'etablissement', "column" => 'entreprise_siren', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Forme juridique', "table" => 'etablissement', "column" => 'entreprise_forme_juridique', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' }, { "label" => 'Forme juridique', "table" => 'etablissement', "column" => 'entreprise_forme_juridique', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },
{ "label" => 'Nom commercial', "table" => 'etablissement', "column" => 'entreprise_nom_commercial', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' }, { "label" => 'Nom commercial', "table" => 'etablissement', "column" => 'entreprise_nom_commercial', 'classname' => '', 'virtual' => false, 'type' => :text, "scope" => '' },