diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30423e65b..2c74df60a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,11 @@ name: Continuous Integration on: push: - branches: 'main' + branches: [main] pull_request: - branches: 'main' + branches: [main] + merge_group: + branches: [main] jobs: linters: diff --git a/Gemfile b/Gemfile index b7f37a435..e03fc22c8 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'devise-i18n' gem 'devise-two-factor' gem 'discard' gem 'dotenv-rails', require: 'dotenv/rails-now' # dotenv should always be loaded before rails +gem 'elastic-apm' gem 'flipper' gem 'flipper-active_record' gem 'flipper-ui' @@ -69,6 +70,7 @@ gem 'rack-attack' gem 'rails' gem 'rails-i18n' # Locales par défaut gem 'rake-progressbar', require: false +gem 'redcarpet' gem 'rexml' # add missing gem due to ruby3 (https://github.com/Shopify/bootsnap/issues/325) gem 'rgeo-geojson' gem 'rqrcode' diff --git a/Gemfile.lock b/Gemfile.lock index 872388057..5ac724b24 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -220,6 +220,10 @@ GEM dumb_delegator (1.0.0) ecma-re-validator (0.3.0) regexp_parser (~> 2.0) + elastic-apm (4.6.0) + concurrent-ruby (~> 1.0) + http (>= 3.0) + ruby2_keywords encryptor (3.0.0) erubi (1.12.0) et-orbi (1.2.4) @@ -249,6 +253,9 @@ GEM faraday-patron (1.0.0) faraday-rack (1.0.0) ffi (1.15.5) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake flipper (0.26.0) concurrent-ruby (< 2) flipper-active_record (0.26.0) @@ -323,9 +330,15 @@ GEM highline (2.0.3) html_tokenizer (0.0.7) htmlentities (4.3.4) + http (5.1.1) + addressable (~> 2.8) + http-cookie (~> 1.0) + http-form_data (~> 2.2) + llhttp-ffi (~> 0.4.0) http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) + http-form_data (2.3.0) http_accept_language (2.1.1) httpclient (2.8.3) i18n (1.12.0) @@ -390,6 +403,9 @@ GEM listen (3.4.1) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + llhttp-ffi (0.4.0) + ffi-compiler (~> 1.0) + rake (~> 13.0) lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) @@ -560,6 +576,7 @@ GEM rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) + redcarpet (3.6.0) regexp_parser (2.6.0) request_store (1.5.0) rack (>= 1.4) @@ -841,6 +858,7 @@ DEPENDENCIES devise-two-factor discard dotenv-rails + elastic-apm factory_bot flipper flipper-active_record @@ -896,6 +914,7 @@ DEPENDENCIES rails-erd rails-i18n rake-progressbar + redcarpet rexml rgeo-geojson rqrcode diff --git a/app/assets/stylesheets/help_dropdown.scss b/app/assets/stylesheets/help_dropdown.scss index 3e3fd1725..5c1e2e4df 100644 --- a/app/assets/stylesheets/help_dropdown.scss +++ b/app/assets/stylesheets/help_dropdown.scss @@ -3,16 +3,11 @@ .help-dropdown { .dropdown-content { - width: 340px; - } - - .dropdown-description { - font-size: 14px; + width: 360px; } } .help-dropdown-title { - font-size: 16px; color: $blue-france-500; } @@ -37,15 +32,5 @@ .help-dropdown-service-item { margin-top: $default-spacer; line-height: 18px; - - .icon { - vertical-align: middle; - margin-right: 5px; - - &.clock { - filter: contrast(0) brightness(120%); - vertical-align: -4px; - } - } } diff --git a/app/assets/stylesheets/password_complexity.scss b/app/assets/stylesheets/password_complexity.scss index 24fc2ef77..ee521b0b3 100644 --- a/app/assets/stylesheets/password_complexity.scss +++ b/app/assets/stylesheets/password_complexity.scss @@ -9,12 +9,10 @@ $complexity-color-3: #FFD000; $complexity-color-4: $green; .password-complexity { - margin-top: -24px; width: 100%; height: 12px; background: $complexity-bg; display: block; - margin-bottom: $default-spacer; text-align: center; border-radius: 8px; diff --git a/app/assets/stylesheets/personnes_impliquees.scss b/app/assets/stylesheets/personnes_impliquees.scss index 5fb6ae3fc..64e982129 100644 --- a/app/assets/stylesheets/personnes_impliquees.scss +++ b/app/assets/stylesheets/personnes_impliquees.scss @@ -10,7 +10,7 @@ } [data-react-component-value^="ComboMultiple"] { - margin-bottom: $default-fields-spacer; + margin-bottom: 0; [data-reach-combobox-token-list] { padding: 0.5 * $default-padding; diff --git a/app/assets/stylesheets/procedure_champs_editor.scss b/app/assets/stylesheets/procedure_champs_editor.scss index bb2e6fe1e..c4e20f0dc 100644 --- a/app/assets/stylesheets/procedure_champs_editor.scss +++ b/app/assets/stylesheets/procedure_champs_editor.scss @@ -89,7 +89,8 @@ color: $light-grey; } - .conditionnel { + .conditionnel, + p { color: $white; } } diff --git a/app/assets/stylesheets/procedure_context.scss b/app/assets/stylesheets/procedure_context.scss index ee5a96010..109724fab 100644 --- a/app/assets/stylesheets/procedure_context.scss +++ b/app/assets/stylesheets/procedure_context.scss @@ -5,8 +5,6 @@ $procedure-context-breakpoint: $two-columns-breakpoint; $procedure-description-line-height: 22px; .procedure-preview { - font-size: 24px; - .paperless-logo { width: 100%; margin-bottom: 60px; @@ -74,17 +72,6 @@ $procedure-description-line-height: 22px; border-bottom: 1px dotted $blue-france-500; } - .procedure-description { - font-size: 16px; - - p:not(:last-of-type) { - // Space the paragraphs by exactly one line height, - // so that the text always is truncated in the middle of a line, - // regarless of the number of paragraphs. - margin-bottom: 3 * $default-spacer; - } - } - .read-more-button { display: none; } @@ -100,7 +87,7 @@ $procedure-description-line-height: 22px; // If the text exceeds the max-height, // truncate it and displays the "Read more" button. &.read-more-enabled { - overflow: hidden; + overflow: auto; border-bottom: 1px solid $border-grey; + .read-more-button { diff --git a/app/assets/stylesheets/sections.scss b/app/assets/stylesheets/sections.scss new file mode 100644 index 000000000..fde56d976 --- /dev/null +++ b/app/assets/stylesheets/sections.scss @@ -0,0 +1,11 @@ +.counter-start-header-section { + counter-reset: headerSectionCounter; +} + +.header-section { + counter-increment: headerSectionCounter; + + &.header-section-counter::before { + content: counter(headerSectionCounter) ". "; + } +} diff --git a/app/components/attachment/delete_form_component.rb b/app/components/attachment/delete_form_component.rb new file mode 100644 index 000000000..2be43262e --- /dev/null +++ b/app/components/attachment/delete_form_component.rb @@ -0,0 +1,7 @@ +# Display a form for destroying a file attachment via a button, but since it might already be nested within a form +# put this component before the actual form containing the editcomponent +class Attachment::DeleteFormComponent < ApplicationComponent + def call + form_tag('/attachments/:id', method: :delete, data: { 'turbo-method': :delete, turbo: true }, id: dom_id(ActiveStorage::Attachment.new, :delete)) {} + end +end diff --git a/app/components/attachment/edit_component/edit_component.en.yml b/app/components/attachment/edit_component/edit_component.en.yml index 7e2a20625..b5ae8544f 100644 --- a/app/components/attachment/edit_component/edit_component.en.yml +++ b/app/components/attachment/edit_component/edit_component.en.yml @@ -7,6 +7,7 @@ en: delete_file: Delete file %{filename} replace: Replace replace_file: Replace file %{filename} + open_file: Open file %{filename} errors: uploading: "An error occurred while sending the file." virus_infected: "Virus detected, please send another file." diff --git a/app/components/attachment/edit_component/edit_component.fr.yml b/app/components/attachment/edit_component/edit_component.fr.yml index 19ba08a3a..d4e5b6811 100644 --- a/app/components/attachment/edit_component/edit_component.fr.yml +++ b/app/components/attachment/edit_component/edit_component.fr.yml @@ -7,6 +7,7 @@ fr: delete_file: Supprimer le fichier %{filename} replace: Remplacer replace_file: Remplacer le fichier %{filename} + open_file: Ouvrir le fichier %{filename} errors: uploading: "Une erreur s’est produite pendant l’envoi du fichier." virus_infected: "Virus détecté, merci d’envoyer un autre fichier." diff --git a/app/components/attachment/edit_component/edit_component.html.haml b/app/components/attachment/edit_component/edit_component.html.haml index bc54ef9f4..ea1fd3261 100644 --- a/app/components/attachment/edit_component/edit_component.html.haml +++ b/app/components/attachment/edit_component/edit_component.html.haml @@ -3,7 +3,8 @@ %div{ id: dom_id(attachment, :persisted_row) } .flex.flex-gap-2{ class: class_names("attachment-error": attachment.virus_scanner_error?) } - if user_can_destroy? - = link_to(t('.delete'), destroy_attachment_path, **remove_button_options, class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: t(".delete_file", filename: attachment.filename)) + = button_tag(name: "action", formaction: destroy_attachment_path, class: "fr-btn fr-btn--tertiary fr-btn--sm fr-icon-delete-line", title: t(".delete_file", filename: attachment.filename), form: dom_id(ActiveStorage::Attachment.new, :delete), data: {turbo: true, 'turbo-method': 'delete'}) do + = t('.delete') - elsif user_can_replace? = button_tag t('.replace'), **replace_button_options, class: "fr-btn fr-btn--tertiary fr-btn--sm", title: t(".replace_file", filename: attachment.filename) @@ -11,7 +12,7 @@ = render Dsfr::DownloadComponent.new(attachment:) - else .fr-py-1v - %span.attachment-filename.fr-mr-1w= link_to_if(viewable?, attachment.filename.to_s, helpers.url_for(attachment.blob), title: "Ouvrir le fichier #{attachment.filename.to_s}", **helpers.external_link_attributes) + %span.attachment-filename.fr-mr-1w= link_to_if(viewable?, attachment.filename.to_s, helpers.url_for(attachment.blob), title: t(".open_file", filename: attachment.filename), **helpers.external_link_attributes) = render Attachment::ProgressComponent.new(attachment: attachment) diff --git a/app/components/attachment/progress_bar_component.rb b/app/components/attachment/progress_bar_component.rb new file mode 100644 index 000000000..8e4605246 --- /dev/null +++ b/app/components/attachment/progress_bar_component.rb @@ -0,0 +1,2 @@ +class Attachment::ProgressBarComponent < ApplicationComponent +end diff --git a/app/components/attachment/progress_bar_component/progress_bar_component.en.yml b/app/components/attachment/progress_bar_component/progress_bar_component.en.yml new file mode 100644 index 000000000..2aad5a17c --- /dev/null +++ b/app/components/attachment/progress_bar_component/progress_bar_component.en.yml @@ -0,0 +1,3 @@ +--- +en: + loading: Loading diff --git a/app/components/attachment/progress_bar_component/progress_bar_component.fr.yml b/app/components/attachment/progress_bar_component/progress_bar_component.fr.yml new file mode 100644 index 000000000..3d31a7529 --- /dev/null +++ b/app/components/attachment/progress_bar_component/progress_bar_component.fr.yml @@ -0,0 +1,3 @@ +--- +fr: + loading: Chargement de fichier diff --git a/app/components/attachment/progress_bar_component/progress_bar_component.html.haml b/app/components/attachment/progress_bar_component/progress_bar_component.html.haml new file mode 100644 index 000000000..6675df0a5 --- /dev/null +++ b/app/components/attachment/progress_bar_component/progress_bar_component.html.haml @@ -0,0 +1,5 @@ +%template#progress-bar-template + .direct-upload + .direct-upload__progress{ role: "progressbar", 'aria-label': t(".loading"), tabindex: "0", 'aria-valuemin': "0", 'aria-valuemax': "100", max: "100", style: "width: 0%" } + %span.direct-upload__filename + %slot{ name: "filename" } diff --git a/app/components/attachment/progress_component/progress_component.html.haml b/app/components/attachment/progress_component/progress_component.html.haml index cf6232821..3bb657619 100644 --- a/app/components/attachment/progress_component/progress_component.html.haml +++ b/app/components/attachment/progress_component/progress_component.html.haml @@ -1,2 +1,2 @@ -%p.fr-badge.fr-badge--info.fr-badge--sm.fr-badge--no-icon +%p.fr-badge.fr-badge--info.fr-badge--sm.fr-badge--no-icon{ role: 'status' } = progress_label diff --git a/app/components/dossiers/message_component.rb b/app/components/dossiers/message_component.rb index a09f0ef78..16244c2a6 100644 --- a/app/components/dossiers/message_component.rb +++ b/app/components/dossiers/message_component.rb @@ -55,15 +55,6 @@ class Dossiers::MessageComponent < ApplicationComponent l(commentaire.created_at, format: is_current_year ? :message_date : :message_date_with_year) end - def commentaire_body - if commentaire.discarded? - t('.deleted_body') - else - body_formatted = commentaire.sent_by_system? ? commentaire.body : simple_format(commentaire.body) - sanitize(body_formatted) - end - end - def highlight? commentaire.created_at.present? && @messagerie_seen_at&.<(commentaire.created_at) end diff --git a/app/components/dossiers/message_component/message_component.html.haml b/app/components/dossiers/message_component/message_component.html.haml index 1a95635cc..2785815a2 100644 --- a/app/components/dossiers/message_component/message_component.html.haml +++ b/app/components/dossiers/message_component/message_component.html.haml @@ -8,7 +8,14 @@ %span.fr-text--xs.fr-text-mention--grey.font-weight-normal= t('.guest') %span.date{ class: ["fr-text--xs", "fr-text-mention--grey", "font-weight-normal", highlight_if_unseen_class], data: scroll_to_target } = commentaire_date - .rich-text= commentaire_body + .rich-text + - if commentaire.discarded? + %p= t('.deleted_body') + - elsif commentaire.sent_by_system? + = sanitize(commentaire.body, scrubber: Sanitizers::MailScrubber.new) + - else + = render SimpleFormatComponent.new(commentaire.body, allow_a: false) + .message-extras.flex.justify-start - if commentaire.soft_deletable?(connected_user) diff --git a/app/components/dsfr/callout_component.rb b/app/components/dsfr/callout_component.rb index 6a4e83c57..a84a67c03 100644 --- a/app/components/dsfr/callout_component.rb +++ b/app/components/dsfr/callout_component.rb @@ -1,6 +1,7 @@ # see: https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/mise-en-avant class Dsfr::CalloutComponent < ApplicationComponent renders_one :body + renders_one :html_body renders_one :bottom attr_reader :title, :theme, :icon, :extra_class_names diff --git a/app/components/dsfr/callout_component/callout_component.html.haml b/app/components/dsfr/callout_component/callout_component.html.haml index ced897a95..8cb47f83a 100644 --- a/app/components/dsfr/callout_component/callout_component.html.haml +++ b/app/components/dsfr/callout_component/callout_component.html.haml @@ -1,5 +1,8 @@ %div{ class: callout_class } - if title.present? %h3.fr-callout__title= title - %p.fr-callout__text= body + - if html_body? + .fr-callout__text= html_body + - if body? + %p.fr-callout__text= body = bottom diff --git a/app/components/dsfr/input_component.rb b/app/components/dsfr/input_component.rb index aa724a775..271762470 100644 --- a/app/components/dsfr/input_component.rb +++ b/app/components/dsfr/input_component.rb @@ -6,7 +6,7 @@ class Dsfr::InputComponent < ApplicationComponent # it uses aria-describedby on input and link it to yielded content renders_one :describedby - def initialize(form:, attribute:, input_type:, opts: {}, required: true) + def initialize(form:, attribute:, input_type: :text_field, opts: {}, required: true) @form = form @attribute = attribute @input_type = input_type @@ -40,19 +40,21 @@ class Dsfr::InputComponent < ApplicationComponent 'fr-mb-0': true, 'fr-input--error': errors_on_attribute?)) - if errors_on_attribute? || describedby - @opts = @opts.deep_merge(aria: { - describedby: error_message_id, - invalid: errors_on_attribute? + if errors_on_attribute? || describedby? + @opts.deep_merge!(aria: { + describedby: describedby_id, + invalid: errors_on_attribute? }) end + if @required @opts[:required] = true end + if email? - @opts = @opts.deep_merge(data: { + @opts.deep_merge!(data: { action: "blur->email-input#checkEmail", - 'email-input-target': 'input' + 'email-input-target': 'input' }) end @opts @@ -63,14 +65,14 @@ class Dsfr::InputComponent < ApplicationComponent errors.has_key?(attribute_or_rich_body) end - def error_message_id - dom_id(object, @attribute) - end - def error_messages errors.full_messages_for(attribute_or_rich_body) end + def describedby_id + dom_id(object, "#{@attribute}-messages") + end + # i18n lookups def label object.class.human_attribute_name(@attribute) @@ -89,6 +91,10 @@ class Dsfr::InputComponent < ApplicationComponent @input_type == :email_field end + def show_password_id + dom_id(object, "#{@attribute}_show_password") + end + private def hint? diff --git a/app/components/dsfr/input_component/input_component.html.haml b/app/components/dsfr/input_component/input_component.html.haml index a56c7fff8..4e52ec670 100644 --- a/app/components/dsfr/input_component/input_component.html.haml +++ b/app/components/dsfr/input_component/input_component.html.haml @@ -7,13 +7,13 @@ - if hint? %span.fr-hint-text= hint - = @form.send(@input_type, @attribute, input_opts) + = @form.public_send(@input_type, @attribute, input_opts) - if errors_on_attribute? - if error_messages.size == 1 - %p.fr-error-text{ id: error_message_id }= error_messages.first + %p.fr-error-text{ id: describedby_id }= error_messages.first - else - .fr-error-text{ id: error_message_id } + .fr-error-text{ id: describedby_id } %ul.list-style-type-none.fr-pl-0 - error_messages.map do |error_message| %li= error_message @@ -23,8 +23,8 @@ - if password? .fr-password__checkbox.fr-checkbox-group.fr-checkbox-group--sm - %input#show_password{ "aria-label" => t('.show_password.aria_label'), type: "checkbox" }/ - %label.fr--password__checkbox.fr-label{ for: "show_password" }= t('.show_password.label') + %input{ id: show_password_id, "aria-label" => t('.show_password.aria_label'), type: "checkbox" }/ + %label.fr--password__checkbox.fr-label{ for: show_password_id }= t('.show_password.label') - if email? .suspect-email.hidden{ data: { "email-input-target": 'ariaRegion'}, aria: { live: 'off' } } diff --git a/app/components/editable_champ/address_component.rb b/app/components/editable_champ/address_component.rb index 0b1db8bff..35ff5aeeb 100644 --- a/app/components/editable_champ/address_component.rb +++ b/app/components/editable_champ/address_component.rb @@ -1,3 +1,2 @@ -class EditableChamp::AddressComponent < EditableChamp::EditableChampBaseComponent - include ApplicationHelper +class EditableChamp::AddressComponent < EditableChamp::ComboSearchComponent end diff --git a/app/components/editable_champ/address_component/address_component.html.haml b/app/components/editable_champ/address_component/address_component.html.haml index da4fa6092..ef480948d 100644 --- a/app/components/editable_champ/address_component/address_component.html.haml +++ b/app/components/editable_champ/address_component/address_component.html.haml @@ -1,6 +1,11 @@ +- render_parent + = @form.hidden_field :value = @form.hidden_field :external_id + = react_component("ComboAdresseSearch", required: @champ.required?, id: @champ.input_id, - describedby: @champ.describedby_id) + describedby: @champ.describedby_id, + **react_combo_props, +) diff --git a/app/components/editable_champ/annuaire_education_component.rb b/app/components/editable_champ/annuaire_education_component.rb index 2dd780eb1..f5e394a0e 100644 --- a/app/components/editable_champ/annuaire_education_component.rb +++ b/app/components/editable_champ/annuaire_education_component.rb @@ -1,3 +1,2 @@ -class EditableChamp::AnnuaireEducationComponent < EditableChamp::EditableChampBaseComponent - include ApplicationHelper +class EditableChamp::AnnuaireEducationComponent < EditableChamp::ComboSearchComponent end diff --git a/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml b/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml index 1a60a1278..0dcfe97d0 100644 --- a/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml +++ b/app/components/editable_champ/annuaire_education_component/annuaire_education_component.html.haml @@ -1,6 +1,9 @@ +- render_parent + = @form.hidden_field :value = @form.hidden_field :external_id = react_component("ComboAnnuaireEducationSearch", required: @champ.required?, id: @champ.input_id, - describedby: @champ.describedby_id) + describedby: @champ.describedby_id, + **react_combo_props) diff --git a/app/components/editable_champ/carte_component.rb b/app/components/editable_champ/carte_component.rb index 737ffdf87..d4417ebb1 100644 --- a/app/components/editable_champ/carte_component.rb +++ b/app/components/editable_champ/carte_component.rb @@ -1,3 +1,9 @@ class EditableChamp::CarteComponent < EditableChamp::EditableChampBaseComponent include ApplicationHelper + + def initialize(**args) + super(**args) + + @autocomplete_component = EditableChamp::ComboSearchComponent.new(**args) + end end diff --git a/app/components/editable_champ/carte_component/carte_component.html.haml b/app/components/editable_champ/carte_component/carte_component.html.haml index eb3096b26..ce5df6c7e 100644 --- a/app/components/editable_champ/carte_component/carte_component.html.haml +++ b/app/components/editable_champ/carte_component/carte_component.html.haml @@ -1,4 +1,11 @@ -= react_component("MapEditor", featureCollection: @champ.to_feature_collection, url: champs_carte_features_path(@champ), options: @champ.render_options) += render @autocomplete_component + += react_component("MapEditor", + featureCollection: @champ.to_feature_collection, + url: champs_carte_features_path(@champ), + options: @champ.render_options, + autocompleteAnnounceTemplateId: @autocomplete_component.announce_template_id, + autocompleteScreenReaderInstructions: t("combo_search_component.screen_reader_instructions")) .geo-areas{ id: dom_id(@champ, :geo_areas) } = render partial: 'shared/champs/carte/geo_areas', locals: { champ: @champ, editing: true } diff --git a/app/components/editable_champ/champ_label_component.rb b/app/components/editable_champ/champ_label_component.rb index 3f8f8abf9..de0a6fea3 100644 --- a/app/components/editable_champ/champ_label_component.rb +++ b/app/components/editable_champ/champ_label_component.rb @@ -1,6 +1,4 @@ class EditableChamp::ChampLabelComponent < ApplicationComponent - include StringToHtmlHelper - def initialize(form:, champ:, seen_at: nil) @form, @champ, @seen_at = form, champ, seen_at end diff --git a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml index 1e5f85d2b..3bcd17000 100644 --- a/app/components/editable_champ/champ_label_component/champ_label_component.html.haml +++ b/app/components/editable_champ/champ_label_component/champ_label_component.html.haml @@ -7,4 +7,4 @@ = render EditableChamp::ChampLabelContentComponent.new champ: @champ, seen_at: @seen_at - if @champ.description.present? - .notice{ id: @champ.describedby_id }= string_to_html(@champ.description, allow_a: true) + .notice{ id: @champ.describedby_id }= render SimpleFormatComponent.new(@champ.description, allow_a: true) diff --git a/app/components/editable_champ/combo_search_component.rb b/app/components/editable_champ/combo_search_component.rb new file mode 100644 index 000000000..bbad7a600 --- /dev/null +++ b/app/components/editable_champ/combo_search_component.rb @@ -0,0 +1,17 @@ +class EditableChamp::ComboSearchComponent < EditableChamp::EditableChampBaseComponent + include ApplicationHelper + + def announce_template_id + @announce_template_id ||= dom_id(@champ, "aria-announce-template") + end + + # NOTE: because this template is called by `render_parent` from a child template, + # as of ViewComponent 2.x translations virtual paths are not properly propagated + # and we can't use the usual component namespacing. Instead we use global translations. + def react_combo_props + { + screenReaderInstructions: t("combo_search_component.screen_reader_instructions"), + announceTemplateId: announce_template_id + } + end +end diff --git a/app/components/editable_champ/combo_search_component/combo_search_component.html.haml b/app/components/editable_champ/combo_search_component/combo_search_component.html.haml new file mode 100644 index 000000000..9b2e14a56 --- /dev/null +++ b/app/components/editable_champ/combo_search_component/combo_search_component.html.haml @@ -0,0 +1,4 @@ +%template{ id: announce_template_id } + %slot{ "name": "0" }= t("combo_search_component.result_slot_html", count: 0) + %slot{ "name": "1" }= t("combo_search_component.result_slot_html", count: 1) + %slot{ "name": "many" }= t("combo_search_component.result_slot_html", count: 2) diff --git a/app/components/editable_champ/communes_component.rb b/app/components/editable_champ/communes_component.rb index cc5bfee0b..a586a994b 100644 --- a/app/components/editable_champ/communes_component.rb +++ b/app/components/editable_champ/communes_component.rb @@ -1,3 +1,2 @@ -class EditableChamp::CommunesComponent < EditableChamp::EditableChampBaseComponent - include ApplicationHelper +class EditableChamp::CommunesComponent < EditableChamp::ComboSearchComponent end diff --git a/app/components/editable_champ/communes_component/communes_component.html.haml b/app/components/editable_champ/communes_component/communes_component.html.haml index ba2f155a0..8723e541b 100644 --- a/app/components/editable_champ/communes_component/communes_component.html.haml +++ b/app/components/editable_champ/communes_component/communes_component.html.haml @@ -1,3 +1,4 @@ +- render_parent = @form.hidden_field :value = @form.hidden_field :external_id = @form.hidden_field :departement @@ -7,4 +8,5 @@ id: @champ.input_id, classNameDepartement: "width-33-desktop width-100-mobile", className: "width-66-desktop width-100-mobile", - describedby: @champ.describedby_id) + describedby: @champ.describedby_id, + **react_combo_props) diff --git a/app/components/editable_champ/editable_champ_component.rb b/app/components/editable_champ/editable_champ_component.rb index e74be2967..2b9391104 100644 --- a/app/components/editable_champ/editable_champ_component.rb +++ b/app/components/editable_champ/editable_champ_component.rb @@ -1,6 +1,4 @@ class EditableChamp::EditableChampComponent < ApplicationComponent - include StringToHtmlHelper - def initialize(form:, champ:, seen_at: nil) @form, @champ, @seen_at = form, champ, seen_at end diff --git a/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml b/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml index 081642438..8129db649 100644 --- a/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml +++ b/app/components/editable_champ/editable_champ_component/editable_champ_component.html.haml @@ -2,7 +2,7 @@ - if @champ.block? %h3.header-subsection= @champ.libelle - if @champ.description.present? - %p.notice= string_to_html(@champ.description, false, allow_a: true) + .notice= render SimpleFormatComponent.new(@champ.description, allow_a: true) - elsif has_label?(@champ) = render EditableChamp::ChampLabelComponent.new form: @form, champ: @champ, seen_at: @seen_at diff --git a/app/components/editable_champ/explication_component.rb b/app/components/editable_champ/explication_component.rb index f3173419d..8958b0985 100644 --- a/app/components/editable_champ/explication_component.rb +++ b/app/components/editable_champ/explication_component.rb @@ -1,3 +1,2 @@ class EditableChamp::ExplicationComponent < EditableChamp::EditableChampBaseComponent - include StringToHtmlHelper end diff --git a/app/components/editable_champ/explication_component/explication_component.html.haml b/app/components/editable_champ/explication_component/explication_component.html.haml index 7e4822b3c..c40e853cf 100644 --- a/app/components/editable_champ/explication_component/explication_component.html.haml +++ b/app/components/editable_champ/explication_component/explication_component.html.haml @@ -1,7 +1,7 @@ = render Dsfr::CalloutComponent.new(title: @champ.libelle, extra_class_names: ['fr-mb-2w', 'fr-callout--blue-cumulus']) do |c| - - c.with_body do + - c.with_html_body do - = string_to_html(@champ.description, allow_a: true) + = render SimpleFormatComponent.new(@champ.description, allow_a: true) - if @champ.collapsible_explanation_enabled? && @champ.collapsible_explanation_text.present? %div diff --git a/app/components/editable_champ/header_section_component/header_section_component.html.haml b/app/components/editable_champ/header_section_component/header_section_component.html.haml index deac95425..aec7d22bd 100644 --- a/app/components/editable_champ/header_section_component/header_section_component.html.haml +++ b/app/components/editable_champ/header_section_component/header_section_component.html.haml @@ -1,2 +1,2 @@ -%h2.header-section - = @champ.libelle_with_section_index +%h2.header-section{ class: @champ.dossier.auto_numbering_section_headers_for?(@champ) ? "header-section-counter" : nil } + = @champ.libelle diff --git a/app/components/editable_champ/linked_drop_down_list_component.rb b/app/components/editable_champ/linked_drop_down_list_component.rb index e31d49b44..a8d5a89a0 100644 --- a/app/components/editable_champ/linked_drop_down_list_component.rb +++ b/app/components/editable_champ/linked_drop_down_list_component.rb @@ -1,3 +1,2 @@ class EditableChamp::LinkedDropDownListComponent < EditableChamp::EditableChampBaseComponent - include StringToHtmlHelper end diff --git a/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml b/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml index cf451f3b0..75f82dd03 100644 --- a/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml +++ b/app/components/editable_champ/linked_drop_down_list_component/linked_drop_down_list_component.html.haml @@ -8,7 +8,7 @@ = @form.label :secondary_value, for: "#{@champ.input_id}-secondary" do - sanitize((@champ.drop_down_secondary_libelle.presence || "Valeur secondaire dépendant de la première") + (@champ.type_de_champ.mandatory? ? tag.span(' *', class: 'mandatory') : '')) - if @champ.drop_down_secondary_description.present? - .notice{ id: "#{@champ.describedby_id}-secondary" }= string_to_html(@champ.drop_down_secondary_description, allow_a: true) + .notice{ id: "#{@champ.describedby_id}-secondary" }= render SimpleFormatComponent.new(@champ.drop_down_secondary_description, allow_a: true) = @form.select :secondary_value, @champ.secondary_options[@champ.primary_value], {}, diff --git a/app/components/editable_champ/rna_component/rna_component.html.haml b/app/components/editable_champ/rna_component/rna_component.html.haml index c16edb4bf..ed6790383 100644 --- a/app/components/editable_champ/rna_component/rna_component.html.haml +++ b/app/components/editable_champ/rna_component/rna_component.html.haml @@ -2,11 +2,11 @@ id: @champ.input_id, aria: { describedby: @champ.describedby_id }, placeholder: t(".placeholder"), - data: { controller: 'turbo-input', turbo_input_url_value: champs_rna_path(@champ.id) }, + data: { controller: 'turbo-input', turbo_input_load_on_connect_value: @champ.prefilled? && @champ.value.present? && @champ.data.blank?, turbo_input_url_value: champs_rna_path(@champ.id) }, required: @champ.required?, pattern: "W[0-9]{9}", title: t(".title"), class: "width-33-desktop", maxlength: 10 .rna-info{ id: dom_id(@champ, :rna_info) } - = render 'shared/champs/rna/association', champ: @champ, network_error: false, rna: @champ.value + = render 'shared/champs/rna/association', champ: @champ, error: nil diff --git a/app/components/editable_champ/siret_component/siret_component.html.haml b/app/components/editable_champ/siret_component/siret_component.html.haml index 0bcdcdb47..1970d85cc 100644 --- a/app/components/editable_champ/siret_component/siret_component.html.haml +++ b/app/components/editable_champ/siret_component/siret_component.html.haml @@ -2,7 +2,7 @@ id: @champ.input_id, aria: { describedby: @champ.describedby_id }, placeholder: t(".placeholder"), - data: { controller: 'turbo-input', turbo_input_url_value: champs_siret_path(@champ.id) }, + data: { controller: 'turbo-input', turbo_input_load_on_connect_value: @champ.prefilled? && @champ.value.present? && @champ.etablissement.blank?, turbo_input_url_value: champs_siret_path(@champ.id) }, required: @champ.required?, pattern: "[0-9]{14}", title: t(".title"), diff --git a/app/components/password_complexity_component.rb b/app/components/password_complexity_component.rb new file mode 100644 index 000000000..32efd3908 --- /dev/null +++ b/app/components/password_complexity_component.rb @@ -0,0 +1,53 @@ +class PasswordComplexityComponent < ApplicationComponent + def initialize(length: nil, min_length: nil, score: nil, min_complexity: nil) + @length = length + @min_length = min_length + @score = score + @min_complexity = min_complexity + end + + private + + def filled? + !@length.nil? || !@score.nil? + end + + def alert_classes + class_names( + "fr-alert": true, + "fr-alert--sm": true, + "fr-alert--info": !success?, + "fr-alert--success": success? + ) + end + + def success? + return false if !filled? + + @length >= @min_length && @score >= @min_complexity + end + + def complexity_classes + [ + "password-complexity fr-mt-2w fr-mb-1w", + filled? ? "complexity-#{@length < @min_length ? @score / 2 : @score}" : nil + ] + end + + def title + return t(".title.empty") if !filled? + + return t(".title.too_short", min_length: @min_length) if @length < @min_length + + case @score + when 0..1 + return t(".title.weakest") + when 2...@min_complexity + return t(".title.weak") + when @min_complexity...4 + return t(".title.passable") + else + return t(".title.strong") + end + end +end diff --git a/app/components/password_complexity_component/password_complexity_component.en.yml b/app/components/password_complexity_component/password_complexity_component.en.yml new file mode 100644 index 000000000..d5930cd58 --- /dev/null +++ b/app/components/password_complexity_component/password_complexity_component.en.yml @@ -0,0 +1,10 @@ +--- +en: + title: + empty: Enter a password. + too_short: Password must be at least %{min_length} characters long. + passable: Password is acceptable. You can validate… or improve your password. + strong: Congratulations! Password is strong and secure enough. + weak: Vulnerable password. + weakest: Very vulnerable password. + hint: A short sentence with punctuation can be a very secure password. diff --git a/app/components/password_complexity_component/password_complexity_component.fr.yml b/app/components/password_complexity_component/password_complexity_component.fr.yml new file mode 100644 index 000000000..225a5775c --- /dev/null +++ b/app/components/password_complexity_component/password_complexity_component.fr.yml @@ -0,0 +1,10 @@ +--- +fr: + title: + empty: Inscrivez un mot de passe. + too_short: Le mot de passe doit faire au moins %{min_length} caractères. + passable: Mot de passe acceptable. Vous pouvez valider… ou améliorer votre mot de passe. + strong: Félicitations ! Mot de passe suffisamment fort et sécurisé. + weak: Mot de passe vulnérable. + weakest: Mot de passe très vulnérable. + hint: Une courte phrase avec ponctuation peut être un mot de passe très sécurisé. diff --git a/app/components/password_complexity_component/password_complexity_component.html.haml b/app/components/password_complexity_component/password_complexity_component.html.haml new file mode 100644 index 000000000..bfa7d5620 --- /dev/null +++ b/app/components/password_complexity_component/password_complexity_component.html.haml @@ -0,0 +1,6 @@ +%div{ class: complexity_classes } + +%div{ class: alert_classes } + %h3.fr-alert__title= title + - if !success? + %p= t(".hint") diff --git a/app/components/procedure/revision_changes_component/revision_changes_component.html.haml b/app/components/procedure/revision_changes_component/revision_changes_component.html.haml index 9a74529fb..6cf97bbe6 100644 --- a/app/components/procedure/revision_changes_component/revision_changes_component.html.haml +++ b/app/components/procedure/revision_changes_component/revision_changes_component.html.haml @@ -46,13 +46,13 @@ = t(".public.enable_mandatory", label: change.label) - if !total_dossiers.zero? && !change.can_rebase? %strong - = t(:breaking_change, count: total_dossiers) + = t('.breaking_change', count: total_dossiers) - else - list.with_item do = t(".public.disable_mandatory", label: change.label) - if !total_dossiers.zero? && !change.can_rebase? %strong - = t(:breaking_change, count: total_dossiers) + = t('.breaking_change', count: total_dossiers) - when :piece_justificative_template - list.with_item do = t(".#{prefix}.update_piece_justificative_template", label: change.label) @@ -115,19 +115,19 @@ = t(".#{prefix}.add_condition", label: change.label, to: change.to) - if !total_dossiers.zero? && !change.can_rebase? %strong - = t(:breaking_change, count: total_dossiers) + = t('.breaking_change', count: total_dossiers) - elsif change.to.nil? - list.with_item do = t(".#{prefix}.remove_condition", label: change.label) - if !total_dossiers.zero? && !change.can_rebase? %strong - = t(:breaking_change, count: total_dossiers) + = t('.breaking_change', count: total_dossiers) - else - list.with_item do = t(".#{prefix}.update_condition", label: change.label, to: change.to) - if !total_dossiers.zero? && !change.can_rebase? %strong - = t(:breaking_change, count: total_dossiers) + = t('.breaking_change', count: total_dossiers) - if @public_move_changes.present? - list.with_item do diff --git a/app/components/simple_format_component.rb b/app/components/simple_format_component.rb new file mode 100644 index 000000000..19a2d0e35 --- /dev/null +++ b/app/components/simple_format_component.rb @@ -0,0 +1,51 @@ +class SimpleFormatComponent < ApplicationComponent + # see: https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use + REDCARPET_EXTENSIONS = { + no_intra_emphasis: false, + tables: false, + fenced_code_blocks: false, + autolink: false, + disable_indented_code_blocks: false, + strikethrough: false, + lax_spacing: false, + space_after_headers: false, + superscript: false, + underline: false, + highlight: false, + quote: false, + footnotes: false + } + + # see: https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch + REDCARPET_RENDERER_OPTS = { + no_images: true + } + + def initialize(text, allow_a: true, class_names_map: {}) + @text = (text || "").gsub(/\R/, "\n\n") # force double \n otherwise a single one won't split paragraph + .split("\n\n") # + .map(&:lstrip) # this block prevent redcarpet to consider " text" as block code by lstriping + .join("\n\n") # + @allow_a = allow_a + @renderer = Redcarpet::Markdown.new( + Redcarpet::BareRenderer.new(link_attributes: external_link_attributes, class_names_map: class_names_map), + REDCARPET_EXTENSIONS.merge(autolink: @allow_a) + ) + end + + def external_link_attributes + { target: '_blank', rel: 'noopener noreferrer' } + end + + def tags + if @allow_a + Rails.configuration.action_view.sanitized_allowed_tags + ['a'] + else + Rails.configuration.action_view.sanitized_allowed_tags + end + end + + def attributes + ['target', 'rel', 'href', 'class'] + end +end diff --git a/app/components/simple_format_component/simple_format_component.html.haml b/app/components/simple_format_component/simple_format_component.html.haml new file mode 100644 index 000000000..61913e949 --- /dev/null +++ b/app/components/simple_format_component/simple_format_component.html.haml @@ -0,0 +1 @@ += sanitize(@renderer.render(@text), tags:, attributes:) diff --git a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml index d9e1260aa..972beea77 100644 --- a/app/components/types_de_champ_editor/champ_component/champ_component.html.haml +++ b/app/components/types_de_champ_editor/champ_component/champ_component.html.haml @@ -29,6 +29,10 @@ = form.check_box :mandatory, class: 'small-margin small', id: dom_id(type_de_champ, :mandatory) = form.label :mandatory, "Champ obligatoire", for: dom_id(type_de_champ, :mandatory) = form.text_field :libelle, class: 'small-margin small width-100', id: dom_id(type_de_champ, :libelle), data: input_autofocus + - if type_de_champ.header_section? + %p + %small Nous numérotons automatiquement les titres lorsqu’aucun de vos titres ne commence par un chiffre. + - if !type_de_champ.header_section? && !type_de_champ.titre_identite? .cell.mt-1 = form.label :description, "Description du champ (optionnel)", for: dom_id(type_de_champ, :description) diff --git a/app/controllers/administrateurs/groupe_instructeurs_controller.rb b/app/controllers/administrateurs/groupe_instructeurs_controller.rb index 63390b1f1..07ffe53f8 100644 --- a/app/controllers/administrateurs/groupe_instructeurs_controller.rb +++ b/app/controllers/administrateurs/groupe_instructeurs_controller.rb @@ -120,8 +120,6 @@ module Administrateurs end if instructeurs.present? - instructeurs.each { groupe_instructeur.add(_1) } - flash[:notice] = if procedure.routing_enabled? t('.assignment', count: instructeurs.size, @@ -130,6 +128,10 @@ module Administrateurs else "Les instructeurs ont bien été affectés à la démarche" end + + GroupeInstructeurMailer + .notify_added_instructeurs(groupe_instructeur, instructeurs, current_administrateur.email) + .deliver_later end if procedure.routing_enabled? @@ -146,15 +148,18 @@ module Administrateurs instructeur = groupe_instructeur.instructeurs.find_by(id: instructeur_id) if groupe_instructeur.remove(instructeur) - flash[:notice] = if procedure.routing_enabled? - GroupeInstructeurMailer - .remove_instructeurs(groupe_instructeur, [instructeur], current_administrateur.email) - .deliver_later - + flash[:notice] = if instructeur.in?(procedure.instructeurs) "L’instructeur « #{instructeur.email} » a été retiré du groupe." else "L’instructeur a bien été désaffecté de la démarche" end + GroupeInstructeurMailer + .notify_removed_instructeur(groupe_instructeur, instructeur, current_administrateur.email) + .deliver_later + + GroupeInstructeurMailer + .notify_group_when_instructeurs_removed(groupe_instructeur, [instructeur], current_administrateur.email) + .deliver_later else flash[:alert] = if procedure.routing_enabled? if instructeur.present? @@ -193,33 +198,56 @@ module Administrateurs def import if procedure.publiee_or_close? - if !CSV_ACCEPTED_CONTENT_TYPES.include?(group_csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type) + if !CSV_ACCEPTED_CONTENT_TYPES.include?(csv_file.content_type) && !CSV_ACCEPTED_CONTENT_TYPES.include?(marcel_content_type) flash[:alert] = "Importation impossible : veuillez importer un fichier CSV" - elsif group_csv_file.size > CSV_MAX_SIZE + elsif csv_file.size > CSV_MAX_SIZE flash[:alert] = "Importation impossible : le poids du fichier est supérieur à #{number_to_human_size(CSV_MAX_SIZE)}" else - file = group_csv_file.read + file = csv_file.read base_encoding = CharlockHolmes::EncodingDetector.detect(file) - groupes_emails = ACSV::CSV.new_for_ruby3(file.encode("UTF-8", base_encoding[:encoding], invalid: :replace, replace: ""), headers: true, header_converters: :downcase) - .map { |r| r.to_h.slice('groupe', 'email') } - groupes_emails_has_keys = groupes_emails.first.has_key?("groupe") && groupes_emails.first.has_key?("email") + if params[:group_csv_file] + groupes_emails = ACSV::CSV.new_for_ruby3(file.encode("UTF-8", base_encoding[:encoding], invalid: :replace, replace: ""), headers: true, header_converters: :downcase) + .map { |r| r.to_h.slice('groupe', 'email') } - if groupes_emails_has_keys.blank? - flash[:alert] = "Importation impossible, veuillez importer un csv #{view_context.link_to('suivant ce modèle', "/csv/#{I18n.locale}/import-groupe-test.csv")}" - else - add_instructeurs_and_get_errors = InstructeursImportService.import(procedure, groupes_emails) + groupes_emails_has_keys = groupes_emails.first.has_key?("groupe") && groupes_emails.first.has_key?("email") - if add_instructeurs_and_get_errors.empty? - flash[:notice] = "La liste des instructeurs a été importée avec succès" + if groupes_emails_has_keys.blank? + flash[:alert] = "Importation impossible, veuillez importer un csv #{view_context.link_to('suivant ce modèle', "/csv/#{I18n.locale}/import-groupe-test.csv")}" else - flash[:alert] = "Import terminé. Cependant les emails suivants ne sont pas pris en compte: #{add_instructeurs_and_get_errors.join(', ')}" + added_instructeurs_by_group, invalid_emails = InstructeursImportService.import_groupes(procedure, groupes_emails) + + added_instructeurs_by_group.each do |groupe, added_instructeurs| + GroupeInstructeurMailer + .notify_added_instructeurs(groupe, added_instructeurs, current_administrateur.email) + .deliver_later + end + + flash_message_for_import(invalid_emails) + end + + elsif params[:instructeurs_csv_file] + instructors_emails = ACSV::CSV.new_for_ruby3(file.encode("UTF-8", base_encoding[:encoding], invalid: :replace, replace: ""), headers: true, header_converters: :downcase) + .map(&:to_h) + + instructors_emails_has_key = instructors_emails.first.has_key?("email") && !instructors_emails.first.keys.many? + + if instructors_emails_has_key.blank? + flash[:alert] = "Importation impossible, veuillez importer un csv #{view_context.link_to('suivant ce modèle', "/csv/import-instructeurs-test.csv")}" + else + added_instructeurs, invalid_emails = InstructeursImportService.import_instructeurs(procedure, instructors_emails) + + GroupeInstructeurMailer + .notify_added_instructeurs(groupe_instructeur, added_instructeurs, current_administrateur.email) + .deliver_later + + flash_message_for_import(invalid_emails) end end + redirect_to admin_procedure_groupe_instructeurs_path(procedure) end - redirect_to admin_procedure_groupe_instructeurs_path(procedure) end end @@ -291,12 +319,12 @@ module Administrateurs (all - assigned).sort end - def group_csv_file - params[:group_csv_file] + def csv_file + params[:group_csv_file] || params[:instructeurs_csv_file] end def marcel_content_type - Marcel::MimeType.for(group_csv_file.read, name: group_csv_file.original_filename, declared_type: group_csv_file.content_type) + Marcel::MimeType.for(csv_file.read, name: csv_file.original_filename, declared_type: csv_file.content_type) end def instructeurs_self_management_enabled_params @@ -306,5 +334,13 @@ module Administrateurs def routing_enabled_params { routing_enabled: params.require(:routing) == 'enable' } end + + def flash_message_for_import(result) + if result.blank? + flash[:notice] = "La liste des instructeurs a été importée avec succès" + else + flash[:alert] = "Import terminé. Cependant les emails suivants ne sont pas pris en compte: #{result.join(', ')}" + end + end end end diff --git a/app/controllers/administrateurs/procedures_controller.rb b/app/controllers/administrateurs/procedures_controller.rb index c206b3724..62718fc3d 100644 --- a/app/controllers/administrateurs/procedures_controller.rb +++ b/app/controllers/administrateurs/procedures_controller.rb @@ -96,8 +96,20 @@ module Administrateurs @procedure = current_administrateur .procedures .includes( - published_revision: :types_de_champ, - draft_revision: :types_de_champ + published_revision: { + types_de_champ: [], + revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } } + }, + draft_revision: { + types_de_champ: [], + revision_types_de_champ: { type_de_champ: { piece_justificative_template_attachment: :blob } } + }, + attestation_template: [], + initiated_mail: [], + received_mail: [], + closed_mail: [], + refused_mail: [], + without_continuation_mail: [] ) .find(params[:id]) @@ -332,7 +344,35 @@ module Administrateurs end def champs - @procedure = Procedure.includes(draft_revision: { revision_types_de_champ_public: :type_de_champ }).find(@procedure.id) + @procedure = Procedure.includes(draft_revision: { + revision_types_de_champ: { + type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] }, + revision: [], + procedure: [] + }, + revision_types_de_champ_public: { + type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] }, + revision: [], + procedure: [] + }, + procedure: [] + }).find(@procedure.id) + end + + def annotations + @procedure = Procedure.includes(draft_revision: { + revision_types_de_champ: { + type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] }, + revision: [], + procedure: [] + }, + revision_types_de_champ_private: { + type_de_champ: { piece_justificative_template_attachment: :blob, revision: [], procedure: [] }, + revision: [], + procedure: [] + }, + procedure: [] + }).find(@procedure.id) end def detail @@ -370,7 +410,7 @@ module Administrateurs private def filter_procedures(filter) - procedures_result = Procedure.select(:id).joins(:procedures_zones).distinct.publiees_ou_closes + procedures_result = Procedure.select(:id).left_joins(:procedures_zones).distinct.publiees_ou_closes procedures_result = procedures_result.where(procedures_zones: { zone_id: filter.zone_ids }) if filter.zone_ids.present? procedures_result = procedures_result.where(aasm_state: filter.statuses) if filter.statuses.present? procedures_result = procedures_result.where("? = ANY(tags)", filter.tag) if filter.tag.present? @@ -378,7 +418,7 @@ module Administrateurs procedures_result = procedures_result.where('unaccent(libelle) ILIKE unaccent(?)', "%#{filter.libelle}%") if filter.libelle.present? procedures_sql = procedures_result.to_sql - sql = "select id, libelle, published_at, aasm_state, count(administrateurs_procedures.administrateur_id) as admin_count from administrateurs_procedures inner join procedures on procedures.id = administrateurs_procedures.procedure_id where procedures.id in (#{procedures_sql}) group by procedures.id order by published_at desc" + sql = "select id, libelle, published_at, aasm_state, estimated_dossiers_count, count(administrateurs_procedures.administrateur_id) as admin_count from administrateurs_procedures inner join procedures on procedures.id = administrateurs_procedures.procedure_id where procedures.id in (#{procedures_sql}) group by procedures.id order by published_at desc" ActiveRecord::Base.connection.execute(sql) end diff --git a/app/controllers/api/public/v1/dossiers_controller.rb b/app/controllers/api/public/v1/dossiers_controller.rb index 2e770d6ce..718036d6e 100644 --- a/app/controllers/api/public/v1/dossiers_controller.rb +++ b/app/controllers/api/public/v1/dossiers_controller.rb @@ -11,18 +11,43 @@ class API::Public::V1::DossiersController < API::Public::V1::BaseController dossier.build_default_individual if dossier.save dossier.prefill!(PrefillParams.new(dossier, params.to_unsafe_h).to_a) - render json: { - dossier_url: commencer_url(@procedure.path, prefill_token: dossier.prefill_token), - dossier_id: dossier.to_typed_id, - dossier_number: dossier.id - }, status: :created + render json: serialize_dossier(dossier), status: :created else render_bad_request(dossier.errors.full_messages.to_sentence) end end + def index + prefill_token = Array.wrap(params.fetch(:prefill_token, [])).flat_map { _1.split(',') } + dossiers = @procedure.dossiers.visible_by_user.prefilled.order(:created_at).where(prefill_token:) + if dossiers.present? + render json: dossiers.map { serialize_dossier(_1) } + else + render json: [] + end + end + private + def serialize_dossier(dossier) + if dossier.orphan? + { + dossier_url: commencer_url(@procedure.path, prefill_token: dossier.prefill_token), + state: :prefilled + } + else + { + state: dossier.state, + submitted_at: dossier.depose_at&.iso8601, + processed_at: dossier.processed_at&.iso8601 + } + end.merge( + dossier_id: dossier.to_typed_id, + dossier_number: dossier.id, + dossier_prefill_token: dossier.prefill_token + ).compact + end + def retrieve_procedure @procedure = Procedure.publiees_ou_brouillons.find_by(id: params[:id]) render_not_found("procedure", params[:id]) if @procedure.blank? diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f9ee04f23..9f880407a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -337,6 +337,8 @@ class ApplicationController < ActionController::Base extract_locale_from_accept_language_header || I18n.default_locale + gon.locale = locale + I18n.with_locale(locale, &action) end diff --git a/app/controllers/champs/rna_controller.rb b/app/controllers/champs/rna_controller.rb index a18d7d2d0..a8f49f7bd 100644 --- a/app/controllers/champs/rna_controller.rb +++ b/app/controllers/champs/rna_controller.rb @@ -3,14 +3,10 @@ class Champs::RNAController < ApplicationController def show @champ = policy_scope(Champ).find(params[:champ_id]) - @rna = read_param_value(@champ.input_name, 'value') - @network_error = false - begin - data = APIEntreprise::RNAAdapter.new(@rna, @champ.procedure_id).to_params - @champ.update!(data: data, value: @rna) - rescue APIEntreprise::API::Error, ActiveRecord::RecordInvalid => error - @network_error = true if error.try(:network_error?) && !APIEntrepriseService.api_up? - @champ.update(data: nil, value: nil) + rna = read_param_value(@champ.input_name, 'value') + + unless @champ.fetch_association!(rna) + @error = @champ.association_fetch_error_key end end end diff --git a/app/controllers/champs/siret_controller.rb b/app/controllers/champs/siret_controller.rb index 2ef8aefd5..23e4ab2ee 100644 --- a/app/controllers/champs/siret_controller.rb +++ b/app/controllers/champs/siret_controller.rb @@ -3,61 +3,11 @@ class Champs::SiretController < ApplicationController def show @champ = policy_scope(Champ).find(params[:champ_id]) - @siret = read_param_value(@champ.input_name, 'value') - @etablissement = @champ.etablissement - if @siret.empty? - return clear_siret_and_etablissement + if @champ.fetch_etablissement!(read_param_value(@champ.input_name, 'value'), current_user) + @siret = @champ.etablissement.siret + else + @siret = @champ.etablissement_fetch_error_key end - - if !Siret.new(siret: @siret).valid? - # i18n-tasks-use t('errors.messages.invalid_siret') - return siret_error(:invalid) - end - - begin - etablissement = find_etablissement_with_siret - rescue => error - if error.try(:network_error?) && !APIEntrepriseService.api_up? - # TODO: notify ops - etablissement = APIEntrepriseService.create_etablissement_as_degraded_mode(@champ, @siret, current_user.id) - - if !@champ.nil? - @champ.update!(value: etablissement.siret, etablissement: etablissement) - end - - @siret = :api_entreprise_down - return - else - Sentry.capture_exception(error, extra: { dossier_id: @champ.dossier_id, siret: @siret }) - # i18n-tasks-use t('errors.messages.siret_network_error') - return siret_error(:network_error) - end - end - if etablissement.nil? - # i18n-tasks-use t('errors.messages.siret_not_found') - return siret_error(:not_found) - end - - @etablissement = etablissement - if !@champ.nil? - @champ.update!(value: etablissement.siret, etablissement: etablissement) - end - end - - private - - def find_etablissement_with_siret - APIEntrepriseService.create_etablissement(@champ, @siret, current_user.id) - end - - def clear_siret_and_etablissement - @champ.update!(value: '') - @etablissement&.destroy - end - - def siret_error(error) - clear_siret_and_etablissement - @siret = error end end diff --git a/app/controllers/instructeurs/groupe_instructeurs_controller.rb b/app/controllers/instructeurs/groupe_instructeurs_controller.rb index 6eb25e131..398cb35ba 100644 --- a/app/controllers/instructeurs/groupe_instructeurs_controller.rb +++ b/app/controllers/instructeurs/groupe_instructeurs_controller.rb @@ -22,6 +22,10 @@ module Instructeurs else groupe_instructeur.add(instructeur) flash[:notice] = "L’instructeur « #{instructeur_email} » a été affecté au groupe." + + GroupeInstructeurMailer + .notify_added_instructeurs(groupe_instructeur, [instructeur], current_user.email) + .deliver_later end redirect_to instructeur_groupe_path(procedure, groupe_instructeur) @@ -35,7 +39,11 @@ module Instructeurs if groupe_instructeur.remove(instructeur) flash[:notice] = "L’instructeur « #{instructeur.email} » a été retiré du groupe." GroupeInstructeurMailer - .remove_instructeurs(groupe_instructeur, [instructeur], current_user.email) + .notify_removed_instructeur(groupe_instructeur, instructeur, current_user.email) + .deliver_later + + GroupeInstructeurMailer + .notify_group_when_instructeurs_removed(groupe_instructeur, [instructeur], current_user.email) .deliver_later else flash[:alert] = "L’instructeur « #{instructeur.email} » n’est pas dans le groupe." diff --git a/app/controllers/prefill_type_de_champs_controller.rb b/app/controllers/prefill_type_de_champs_controller.rb index 22f5f1a54..af541afcf 100644 --- a/app/controllers/prefill_type_de_champs_controller.rb +++ b/app/controllers/prefill_type_de_champs_controller.rb @@ -12,6 +12,6 @@ class PrefillTypeDeChampsController < ApplicationController end def set_prefill_type_de_champ - @type_de_champ = TypesDeChamp::PrefillTypeDeChamp.build(@procedure.active_revision.types_de_champ_public.fillable.find(params[:id])) + @type_de_champ = TypesDeChamp::PrefillTypeDeChamp.build(@procedure.active_revision.types_de_champ.fillable.find(params[:id]), @procedure.active_revision) end end diff --git a/app/graphql/api/v2/context.rb b/app/graphql/api/v2/context.rb index 0b23f1212..294387a61 100644 --- a/app/graphql/api/v2/context.rb +++ b/app/graphql/api/v2/context.rb @@ -42,10 +42,10 @@ class API::V2::Context < GraphQL::Query::Context return true end - # We are caching authorization logic because it is called for each node - # of the requested graph and can be expensive. Context is reset per request so it is safe. - self[:authorized] ||= Hash.new do |hash, demarche_id| - hash[demarche_id] = if self[:administrateur_id] + self[:authorized] ||= {} + + if self[:authorized][demarche.id].nil? + self[:authorized][demarche.id] = if self[:administrateur_id] demarche.administrateurs.map(&:id).include?(self[:administrateur_id]) elsif self[:token] APIToken.find_and_verify(self[:token], demarche.administrateurs).present? diff --git a/app/graphql/api/v2/stored_query.rb b/app/graphql/api/v2/stored_query.rb index a2e39eddf..465eaf2af 100644 --- a/app/graphql/api/v2/stored_query.rb +++ b/app/graphql/api/v2/stored_query.rb @@ -288,6 +288,7 @@ class API::V2::StoredQuery dateFermeture notice { url } deliberation { url } + demarcheUrl cadreJuridiqueUrl service @include(if: $includeService) { ...ServiceFragment diff --git a/app/graphql/mutations/groupe_instructeur_ajouter_instructeurs.rb b/app/graphql/mutations/groupe_instructeur_ajouter_instructeurs.rb index c536667ef..a6e6a6c1f 100644 --- a/app/graphql/mutations/groupe_instructeur_ajouter_instructeurs.rb +++ b/app/graphql/mutations/groupe_instructeur_ajouter_instructeurs.rb @@ -11,9 +11,8 @@ module Mutations def resolve(groupe_instructeur:, instructeurs:) ids, emails = partition_instructeurs_by(instructeurs) - instructeurs, invalid_emails = groupe_instructeur.add_instructeurs(ids:, emails:) + _, invalid_emails = groupe_instructeur.add_instructeurs(ids:, emails:) - instructeurs.each { groupe_instructeur.add(_1) } groupe_instructeur.reload result = { groupe_instructeur: } diff --git a/app/graphql/mutations/groupe_instructeur_creer.rb b/app/graphql/mutations/groupe_instructeur_creer.rb index d7a0a6331..465606fe1 100644 --- a/app/graphql/mutations/groupe_instructeur_creer.rb +++ b/app/graphql/mutations/groupe_instructeur_creer.rb @@ -29,9 +29,8 @@ module Mutations result = { groupe_instructeur: } if emails.present? || ids.present? - instructeurs, invalid_emails = groupe_instructeur.add_instructeurs(ids:, emails:) + _, invalid_emails = groupe_instructeur.add_instructeurs(ids:, emails:) - instructeurs.each { groupe_instructeur.add(_1) } groupe_instructeur.reload if invalid_emails.present? diff --git a/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb b/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb index 864f0c3c3..0c1b1617d 100644 --- a/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb +++ b/app/graphql/mutations/groupe_instructeur_supprimer_instructeurs.rb @@ -17,7 +17,7 @@ module Mutations if groupe_instructeur.procedure.routing_enabled? && instructeurs.present? GroupeInstructeurMailer - .remove_instructeurs(groupe_instructeur, instructeurs, current_administrateur.email) + .notify_group_when_instructeurs_removed(groupe_instructeur, instructeurs, current_administrateur.email) .deliver_later end diff --git a/app/graphql/types/champs/siret_champ_type.rb b/app/graphql/types/champs/siret_champ_type.rb index 045ac9a45..cf7f143b3 100644 --- a/app/graphql/types/champs/siret_champ_type.rb +++ b/app/graphql/types/champs/siret_champ_type.rb @@ -6,7 +6,10 @@ module Types::Champs def etablissement if object.etablissement_id.present? - Loaders::Record.for(Etablissement).load(object.etablissement_id) + Loaders::Record.for(Etablissement).load(object.etablissement_id).then do |etablissement| + return nil if etablissement.as_degraded_mode? + etablissement + end end end end diff --git a/app/graphql/types/demarche_descriptor_type.rb b/app/graphql/types/demarche_descriptor_type.rb index d0db6991a..f29c618db 100644 --- a/app/graphql/types/demarche_descriptor_type.rb +++ b/app/graphql/types/demarche_descriptor_type.rb @@ -75,7 +75,11 @@ Cela évite l’accès récursif aux dossiers." delegate :description, :opendata, :tags, to: :procedure def demarche_url - procedure.lien_demarche + if procedure.brouillon? + Rails.application.routes.url_helpers.commencer_test_url(path: procedure.path) + else + Rails.application.routes.url_helpers.commencer_url(path: procedure.path) + end end def dpo_url diff --git a/app/graphql/types/geo_area_type.rb b/app/graphql/types/geo_area_type.rb index 5827b3576..32bc7a26e 100644 --- a/app/graphql/types/geo_area_type.rb +++ b/app/graphql/types/geo_area_type.rb @@ -12,7 +12,7 @@ module Types global_id_field :id field :source, GeoAreaSource, null: false - field :geometry, Types::GeoJSON, null: false, method: :safe_geometry + field :geometry, Types::GeoJSON, null: false field :description, String, null: true definition_methods do diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index a070ec088..ff36e4f85 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -19,7 +19,7 @@ module Types field :demarches_publiques, DemarcheDescriptorType.connection_type, null: false, internal: true def demarches_publiques - Procedure.opendata.includes(draft_revision: :procedure, published_revision: :procedure) + Procedure.publiees_ou_closes.opendata.includes(draft_revision: :procedure, published_revision: :procedure) end def demarche_descriptor(demarche:) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 70b116d78..03e393d1d 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -142,7 +142,7 @@ module ApplicationHelper end def new_tab_suffix(title) - "#{title} — #{t('utils.new_tab')}" + "#{title} — #{I18n.t('utils.new_tab')}" end def download_details(attachment) diff --git a/app/helpers/sanitize_with_link_helper.rb b/app/helpers/sanitize_with_link_helper.rb new file mode 100644 index 000000000..64e5436b0 --- /dev/null +++ b/app/helpers/sanitize_with_link_helper.rb @@ -0,0 +1,6 @@ +module SanitizeWithLinkHelper + def sanitize_with_link(value) + tags = Rails.configuration.action_view.sanitized_allowed_tags + ['a'] + sanitize(value, tags:) + end +end diff --git a/app/helpers/string_to_html_helper.rb b/app/helpers/string_to_html_helper.rb deleted file mode 100644 index d41c8300a..000000000 --- a/app/helpers/string_to_html_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module StringToHtmlHelper - def string_to_html(str, wrapper_tag = 'p', allow_a: false) - return nil if str.blank? - html_formatted = simple_format(str, {}, { wrapper_tag: wrapper_tag }) - with_links = Anchored::Linker.auto_link(html_formatted, target: '_blank', rel: 'noopener') - - tags = if allow_a - Rails.configuration.action_view.sanitized_allowed_tags + ['a'] - else - Rails.configuration.action_view.sanitized_allowed_tags - end - - sanitize(with_links, tags:, attributes: ['target', 'rel', 'href']) - end -end diff --git a/app/javascript/components/ComboSearch.tsx b/app/javascript/components/ComboSearch.tsx index fe4687da1..2115d6be6 100644 --- a/app/javascript/components/ComboSearch.tsx +++ b/app/javascript/components/ComboSearch.tsx @@ -1,4 +1,10 @@ -import React, { useState, useRef, ChangeEventHandler } from 'react'; +import React, { + useState, + useEffect, + useRef, + useId, + ChangeEventHandler +} from 'react'; import { useDebounce } from 'use-debounce'; import { useQuery } from 'react-query'; import { @@ -18,7 +24,7 @@ type TransformResult = ( result: Result ) => [key: string, value: string, label?: string]; -export type ComboSearchProps = { +export type ComboSearchProps = { onChange?: (value: string | null, result?: Result) => void; value?: string; scope: string; @@ -32,6 +38,8 @@ export type ComboSearchProps = { className?: string; placeholder?: string; debounceDelay?: number; + screenReaderInstructions: string; + announceTemplateId: string; }; type QueryKey = readonly [ @@ -51,6 +59,8 @@ function ComboSearch({ transformResults = (_, results) => results as Result[], id, describedby, + screenReaderInstructions, + announceTemplateId, debounceDelay = 0, ...props }: ComboSearchProps) { @@ -127,6 +137,46 @@ function ComboSearch({ } }; + const [announceLive, setAnnounceLive] = useState(''); + const announceTimeout = useRef>(); + const announceTemplate = document.querySelector( + `#${announceTemplateId}` + ); + invariant(announceTemplate, `Missing #${announceTemplateId}`); + + const announceFragment = useRef( + announceTemplate.content.cloneNode(true) as DocumentFragment + ).current; + + useEffect(() => { + if (isSuccess) { + const slot = announceFragment.querySelector( + 'slot[name="' + (results.length <= 1 ? results.length : 'many') + '"]' + ); + + if (!slot) { + return; + } + + const countSlot = + slot.querySelector('slot[name="count"]'); + if (countSlot) { + countSlot.replaceWith(String(results.length)); + } + + setAnnounceLive(slot.textContent ?? ''); + } + + announceTimeout.current = setTimeout(() => { + setAnnounceLive(''); + }, 3000); + + return () => clearTimeout(announceTimeout.current); + }, [announceFragment, results.length, isSuccess]); + + const initInstrId = useId(); + const resultsId = useId(); + return ( ({ value={value ?? ''} autocomplete={false} id={id} - aria-describedby={describedby} + aria-describedby={describedby ?? initInstrId} + aria-owns={resultsId} /> {isSuccess && ( - + {results.length > 0 ? ( {results.map((result, index) => { @@ -156,6 +207,14 @@ function ComboSearch({ )} )} + {!describedby && ( + + {screenReaderInstructions} + + )} +
+ {announceLive} +
); } diff --git a/app/javascript/components/MapEditor/components/AddressInput.tsx b/app/javascript/components/MapEditor/components/AddressInput.tsx index f1745d97a..51b559852 100644 --- a/app/javascript/components/MapEditor/components/AddressInput.tsx +++ b/app/javascript/components/MapEditor/components/AddressInput.tsx @@ -2,8 +2,14 @@ import React from 'react'; import { fire } from '@utils'; import ComboAdresseSearch from '../../ComboAdresseSearch'; +import { ComboSearchProps } from '~/components/ComboSearch'; -export function AddressInput() { +export function AddressInput( + comboProps: Pick< + ComboSearchProps, + 'screenReaderInstructions' | 'announceTemplateId' + > +) { return (
{ fire(document, 'map:zoom', { feature }); }} + {...comboProps} />
); diff --git a/app/javascript/components/MapEditor/components/PointInput.tsx b/app/javascript/components/MapEditor/components/PointInput.tsx index 6bd7496a6..c7ef342b0 100644 --- a/app/javascript/components/MapEditor/components/PointInput.tsx +++ b/app/javascript/components/MapEditor/components/PointInput.tsx @@ -41,9 +41,11 @@ export function PointInput() { type="button" className="button mr-1" onClick={getCurrentPosition} - title="Localiser votre position" + title="Afficher votre position sur la carte" > - Localiser votre position + + Afficher votre position sur la carte + ) : null} diff --git a/app/javascript/components/MapEditor/index.tsx b/app/javascript/components/MapEditor/index.tsx index d5acd5710..7de243a35 100644 --- a/app/javascript/components/MapEditor/index.tsx +++ b/app/javascript/components/MapEditor/index.tsx @@ -12,15 +12,20 @@ import { AddressInput } from './components/AddressInput'; import { PointInput } from './components/PointInput'; import { ImportFileInput } from './components/ImportFileInput'; import { FlashMessage } from '../shared/FlashMessage'; +import { ComboSearchProps } from '../ComboSearch'; export default function MapEditor({ featureCollection: initialFeatureCollection, url, - options + options, + autocompleteAnnounceTemplateId, + autocompleteScreenReaderInstructions }: { featureCollection: FeatureCollection; url: string; options: { layers: string[] }; + autocompleteAnnounceTemplateId: ComboSearchProps['announceTemplateId']; + autocompleteScreenReaderInstructions: ComboSearchProps['screenReaderInstructions']; }) { const [cadastreEnabled, setCadastreEnabled] = useState(false); @@ -46,7 +51,10 @@ export default function MapEditor({ {error && } - + = async ({ @@ -55,6 +73,16 @@ const defaultQueryFn: QueryFunction = async ({ return matchSorter(await getPays(signal), term, { keys: ['label'] }); } + // BAN will error with queries less then 3 chars long + if (scope == 'adresse' && term.length < 3) { + return { + type: 'FeatureCollection', + version: 'draft', + features: [], + query: term + }; + } + const url = buildURL(scope, term, extra); return httpRequest(url, { csrf: false, signal }).json(); }; diff --git a/app/javascript/controllers/turbo_controller.ts b/app/javascript/controllers/turbo_controller.ts index bfd1b0027..1c55528b2 100644 --- a/app/javascript/controllers/turbo_controller.ts +++ b/app/javascript/controllers/turbo_controller.ts @@ -9,6 +9,10 @@ type StreamRenderEvent = CustomEvent<{ render(streamElement: StreamElement): void; }>; +type FrameRenderEvent = CustomEvent<{ + render(currentElement: Element, newElement: Element): void; +}>; + export class TurboController extends ApplicationController { static targets = ['spinner']; @@ -29,7 +33,7 @@ export class TurboController extends ApplicationController { connect() { this.#actions = new Actions({ - element: document.documentElement, + element: document.body, schema: { forceAttribute: 'data-turbo-force', hiddenClassName: 'hidden' }, debug: false }); @@ -46,14 +50,22 @@ export class TurboController extends ApplicationController { // prevent scroll on turbo form submits this.onGlobal('turbo:render', () => this.preventScrollIfNeeded()); - // reset state preserved for actions between pages - this.onGlobal('turbo:load', () => this.actions.reset()); - // see: https://turbo.hotwired.dev/handbook/streams#custom-actions this.onGlobal('turbo:before-stream-render', (event: StreamRenderEvent) => { event.detail.render = (streamElement: StreamElement) => this.actions.applyActions([parseTurboStream(streamElement)]); }); + + // see: https://turbo.hotwired.dev/handbook/frames#custom-rendering + this.onGlobal('turbo:before-frame-render', (event: FrameRenderEvent) => { + event.detail.render = (currentElement, newElement) => { + // There is a bug in morphdom when it comes to mutate a custom element. It will miserably + // crash. We mutate its content instead. + const fragment = document.createDocumentFragment(); + fragment.append(...newElement.childNodes); + this.actions.update({ targets: [currentElement], fragment }); + }; + }); } private startSpinner() { diff --git a/app/javascript/controllers/turbo_input_controller.tsx b/app/javascript/controllers/turbo_input_controller.tsx index d194a7f3c..e2ad35db3 100644 --- a/app/javascript/controllers/turbo_input_controller.tsx +++ b/app/javascript/controllers/turbo_input_controller.tsx @@ -4,13 +4,18 @@ import { ApplicationController } from './application_controller'; export class TurboInputController extends ApplicationController { static values = { - url: String + url: String, + loadOnConnect: { type: Boolean, default: false } }; declare readonly urlValue: string; + declare readonly loadOnConnectValue: boolean; connect(): void { this.on('input', () => this.debounce(this.load, 200)); + if (this.loadOnConnectValue) { + this.load(); + } } private load(): void { diff --git a/app/javascript/shared/activestorage/progress-bar.ts b/app/javascript/shared/activestorage/progress-bar.ts index 7541ffae4..3370d790d 100644 --- a/app/javascript/shared/activestorage/progress-bar.ts +++ b/app/javascript/shared/activestorage/progress-bar.ts @@ -1,3 +1,5 @@ +import invariant from 'tiny-invariant'; + const PENDING_CLASS = 'direct-upload--pending'; const ERROR_CLASS = 'direct-upload--error'; const COMPLETE_CLASS = 'direct-upload--complete'; @@ -18,7 +20,7 @@ export default class ProgressBar { static init(input: HTMLInputElement, id: string, file: File) { clearErrors(input); const html = this.render(id, file.name); - input.insertAdjacentHTML('beforebegin', html); + input.before(html); } static start(id: string) { @@ -53,10 +55,24 @@ export default class ProgressBar { } static render(id: string, filename: string) { - return `
-
- ${filename} -
`; + const template = document.querySelector( + '#progress-bar-template' + ); + invariant(template, 'Missing progress-bar-template'); + const fragment = template.content.cloneNode(true) as DocumentFragment; + const container = fragment.querySelector('.direct-upload'); + invariant(container, 'Missing .direct-upload element in template'); + const slot = container.querySelector( + 'slot[name="filename"]' + ); + invariant(slot, 'Missing "filename" slot in template'); + + container.id = `direct-upload-${id}`; + container.dataset.directUploadId = id; + container.classList.add(PENDING_CLASS); + slot.replaceWith(document.createTextNode(filename)); + + return container; } id: string; diff --git a/app/javascript/shared/track/crisp.ts b/app/javascript/shared/track/crisp.ts index 49cdeca6c..42c716153 100644 --- a/app/javascript/shared/track/crisp.ts +++ b/app/javascript/shared/track/crisp.ts @@ -1,11 +1,15 @@ import { getConfig } from '@utils'; const { - crisp: { key, enabled, administrateur } + crisp: { key, enabled, administrateur }, + locale } = getConfig(); declare const window: Window & typeof globalThis & { CRISP_WEBSITE_ID?: string | null; + CRISP_RUNTIME_CONFIG?: { + locale: string; + }; $crisp: ( | [cmd: string, key: string, value: unknown] | [key: string, value: unknown] @@ -15,6 +19,9 @@ declare const window: Window & if (enabled) { window.$crisp = []; window.CRISP_WEBSITE_ID = key; + window.CRISP_RUNTIME_CONFIG = { + locale: locale + }; const script = document.createElement('script'); const firstScript = document.getElementsByTagName('script')[0]; diff --git a/app/javascript/shared/utils.ts b/app/javascript/shared/utils.ts index ca1b0eae3..34d83333f 100644 --- a/app/javascript/shared/utils.ts +++ b/app/javascript/shared/utils.ts @@ -16,6 +16,7 @@ const Gon = z api_education_url: z.string().optional() }) .default({}), + locale: z.string().default('fr'), matomo: z .object({ cookieDomain: z.string().optional(), diff --git a/app/jobs/champ_fetch_external_data_job.rb b/app/jobs/champ_fetch_external_data_job.rb index 366586dda..790052863 100644 --- a/app/jobs/champ_fetch_external_data_job.rb +++ b/app/jobs/champ_fetch_external_data_job.rb @@ -1,11 +1,9 @@ class ChampFetchExternalDataJob < ApplicationJob def perform(champ, external_id) - if champ.external_id == external_id && champ.data.nil? - data = champ.fetch_external_data + return if champ.external_id != external_id + return if champ.data.present? + return if (data = champ.fetch_external_data).blank? - if data.present? - champ.update!(data: data) - end - end + champ.update_with_external_data!(data: data) end end diff --git a/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb b/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb new file mode 100644 index 000000000..00a8c9e5c --- /dev/null +++ b/app/jobs/migrations/normalize_departements_with_empty_external_id_job.rb @@ -0,0 +1,24 @@ +class Migrations::NormalizeDepartementsWithEmptyExternalIdJob < ApplicationJob + def perform(ids) + Champs::DepartementChamp.where(id: ids).find_each do |champ| + next unless champ.external_id == '' + + if champ.value.nil? + champ.update_columns(external_id: nil) + elsif champ.value == '' + champ.update_columns(external_id: nil, value: nil) + elsif champ.value == '85' + champ.update_columns(external_id: '85', value: 'Vendée') + elsif champ.value.present? + match = champ.value.match(/^(\w{2,3}) - (.+)/) + if match + code = match[1] + name = APIGeoService.departement_name(code) + champ.update_columns(external_id: code, value: name) + else + champ.update_columns(external_id: APIGeoService.departement_code(champ.value)) + end + end + end + end +end diff --git a/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb b/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb new file mode 100644 index 000000000..109ca6c9a --- /dev/null +++ b/app/jobs/migrations/normalize_departements_with_nil_external_id_job.rb @@ -0,0 +1,22 @@ +class Migrations::NormalizeDepartementsWithNilExternalIdJob < ApplicationJob + def perform(ids) + Champs::DepartementChamp.where(id: ids).find_each do |champ| + next unless champ.external_id.nil? + + if champ.value == '' + champ.update_columns(value: nil) + elsif champ.value == '85' + champ.update_columns(external_id: '85', value: 'Vendée') + elsif champ.value.present? + match = champ.value.match(/^(\w{2,3}) - (.+)/) + if match + code = match[1] + name = APIGeoService.departement_name(code) + champ.update_columns(external_id: code, value: name) + else + champ.update_columns(external_id: APIGeoService.departement_code(champ.value)) + end + end + end + end +end diff --git a/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb b/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb new file mode 100644 index 000000000..a024582d2 --- /dev/null +++ b/app/jobs/migrations/normalize_departements_with_present_external_id_job.rb @@ -0,0 +1,15 @@ +class Migrations::NormalizeDepartementsWithPresentExternalIdJob < ApplicationJob + def perform(ids) + Champs::DepartementChamp.where(id: ids).find_each do |champ| + next if champ.external_id.blank? + + if champ.value.blank? + champ.update_columns(value: APIGeoService.departement_name(champ.external_id)) + elsif (match = champ.value.match(/^(\w{2,3}) - (.+)/)) + code = match[1] + name = APIGeoService.departement_name(code) + champ.update_columns(external_id: code, value: name) + end + end + end +end diff --git a/app/jobs/migrations/normalize_geo_area_job.rb b/app/jobs/migrations/normalize_geo_area_job.rb new file mode 100644 index 000000000..e5f8e7d1d --- /dev/null +++ b/app/jobs/migrations/normalize_geo_area_job.rb @@ -0,0 +1,11 @@ +class Migrations::NormalizeGeoAreaJob < ApplicationJob + def perform(ids) + GeoArea.where(id: ids).find_each do |geo_area| + geojson = RGeo::GeoJSON.decode(geo_area.geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) + geometry = RGeo::GeoJSON.encode(geojson) + geo_area.update_column(:geometry, geometry) + rescue RGeo::Error::InvalidGeometry + geo_area.destroy + end + end +end diff --git a/app/lib/redcarpet/bare_renderer.rb b/app/lib/redcarpet/bare_renderer.rb new file mode 100644 index 000000000..645570662 --- /dev/null +++ b/app/lib/redcarpet/bare_renderer.rb @@ -0,0 +1,21 @@ +module Redcarpet + class BareRenderer < Redcarpet::Render::HTML + include ActionView::Helpers::TagHelper + + # won't use rubocop tag method because it is missing output buffer + # rubocop:disable Rails/ContentTag + def list(content, list_type) + tag = list_type == :ordered ? :ol : :ul + content_tag(tag, content, { class: @options[:class_names_map].fetch(:list) {} }, false) + end + + def list_item(content, list_type) + content_tag(:li, content.strip.gsub(/<\/?p>/, ''), {}, false) + end + + def paragraph(text) + content_tag(:p, text, { class: @options[:class_names_map].fetch(:paragraph) {} }, false) + end + # rubocop:enable Rails/ContentTag + end +end diff --git a/app/mailers/groupe_instructeur_mailer.rb b/app/mailers/groupe_instructeur_mailer.rb index cb2865bb8..f28fd5ca5 100644 --- a/app/mailers/groupe_instructeur_mailer.rb +++ b/app/mailers/groupe_instructeur_mailer.rb @@ -1,7 +1,7 @@ class GroupeInstructeurMailer < ApplicationMailer layout 'mailers/layout' - def remove_instructeurs(group, removed_instructeurs, current_instructeur_email) + def notify_group_when_instructeurs_removed(group, removed_instructeurs, current_instructeur_email) @removed_instructeur_emails = removed_instructeurs.map(&:email) @group = group @current_instructeur_email = current_instructeur_email @@ -11,4 +11,31 @@ class GroupeInstructeurMailer < ApplicationMailer emails = @group.instructeurs.map(&:email) mail(bcc: emails, subject: subject) end + + def notify_removed_instructeur(group, removed_instructeur, current_instructeur_email) + @group = group + @current_instructeur_email = current_instructeur_email + @still_assigned_to_procedure = removed_instructeur.in?(group.procedure.instructeurs) + subject = if @still_assigned_to_procedure + "Vous avez été retiré(e) du groupe \"#{group.label}\" de la démarche \"#{group.procedure.libelle}\"" + else + "Vous avez été désaffecté(e) de la démarche \"#{group.procedure.libelle}\"" + end + + mail(to: removed_instructeur.email, subject: subject) + end + + def notify_added_instructeurs(group, added_instructeurs, current_instructeur_email) + added_instructeur_emails = added_instructeurs.map(&:email) + @group = group + @current_instructeur_email = current_instructeur_email + + subject = if group.procedure.groupe_instructeurs.many? + "Vous avez été ajouté(e) au groupe \"#{group.label}\" de la démarche \"#{group.procedure.libelle}\"" + else + "Vous avez été affecté(e) à la démarche \"#{group.procedure.libelle}\"" + end + + mail(bcc: added_instructeur_emails, subject: subject) + end end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 89f79c1db..8f8bb6c57 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -7,6 +7,7 @@ # class NotificationMailer < ApplicationMailer include ActionView::Helpers::SanitizeHelper + include ActionView::Helpers::TextHelper before_action :set_dossier before_action :set_services_publics_plus, only: :send_notification @@ -67,7 +68,7 @@ class NotificationMailer < ApplicationMailer mail_template = @dossier.procedure.mail_template_for(params[:state]) @email = @dossier.user_email_for(:notification) - @subject = mail_template.subject_for_dossier(@dossier) + @subject = truncate(mail_template.subject_for_dossier(@dossier), length: 100) @body = mail_template.body_for_dossier(@dossier) @actions = mail_template.actions_for_dossier(@dossier) @attachment = mail_template.attachment_for_dossier(@dossier) diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 28787d3e9..1a770302e 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -19,7 +19,19 @@ class ApplicationRecord < ActiveRecord::Base GraphQL::Schema::UniqueWithinType.decode(id)[1] end + def self.stable_id_from_typed_id(prefixed_typed_id) + return nil unless prefixed_typed_id.starts_with?("champ_") + + self.id_from_typed_id(prefixed_typed_id.gsub("champ_", "")).to_i + rescue + nil + end + def to_typed_id GraphQL::Schema::UniqueWithinType.encode(self.class.name, id) end + + def to_typed_id_for_query + to_typed_id.delete("==") + end end diff --git a/app/models/champ.rb b/app/models/champ.rb index 02e6053e8..cadb9e395 100644 --- a/app/models/champ.rb +++ b/app/models/champ.rb @@ -53,6 +53,8 @@ class Champ < ApplicationRecord :repetition?, :block?, :dossier_link?, + :departement?, + :region?, :titre_identite?, :header_section?, :simple_drop_down_list?, @@ -72,6 +74,10 @@ class Champ < ApplicationRecord :refresh_after_update?, to: :type_de_champ + delegate :to_typed_id, :to_typed_id_for_query, to: :type_de_champ, prefix: true + + delegate :revision, to: :dossier, prefix: true + scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) } scope :public_only, -> { where(private: false) } scope :private_only, -> { where(private: true) } @@ -211,6 +217,10 @@ class Champ < ApplicationRecord raise NotImplemented.new(:fetch_external_data) end + def update_with_external_data!(data:) + update!(data: data) + end + def clone champ_attributes = [:parent_id, :private, :row_id, :type, :type_de_champ_id] value_attributes = private? ? [] : [:value, :value_json, :data, :external_id] diff --git a/app/models/champs/annuaire_education_champ.rb b/app/models/champs/annuaire_education_champ.rb index ffd064d1f..46a3ee389 100644 --- a/app/models/champs/annuaire_education_champ.rb +++ b/app/models/champs/annuaire_education_champ.rb @@ -28,4 +28,15 @@ class Champs::AnnuaireEducationChamp < Champs::TextChamp def fetch_external_data APIEducation::AnnuaireEducationAdapter.new(external_id).to_params end + + def update_with_external_data!(data:) + if data&.is_a?(Hash) && data['nom_etablissement'].present? && data['nom_commune'].present? && data['identifiant_de_l_etablissement'].present? + update!( + data: data, + value: "#{data['nom_etablissement']}, #{data['nom_commune']} (#{data['identifiant_de_l_etablissement']})" + ) + else + update!(data: data) + end + end end diff --git a/app/models/champs/carte_champ.rb b/app/models/champs/carte_champ.rb index 8d86789fc..208b72dfb 100644 --- a/app/models/champs/carte_champ.rb +++ b/app/models/champs/carte_champ.rb @@ -64,21 +64,14 @@ class Champs::CarteChamp < Champ end def bounding_box - factory = RGeo::Geographic.simple_mercator_factory - bounding_box = RGeo::Cartesian::BoundingBox.new(factory) - if geo_areas.present? - geo_areas.filter_map(&:rgeo_geometry).each do |geometry| - bounding_box.add(geometry) - end + GeojsonService.bbox(type: 'FeatureCollection', features: geo_areas.map(&:to_feature)) elsif dossier.present? point = dossier.geo_position - bounding_box.add(factory.point(point[:lon], point[:lat])) + GeojsonService.bbox(type: 'Feature', geometry: { type: 'Point', coordinates: [point[:lon], point[:lat]] }) else - bounding_box.add(factory.point(DEFAULT_LON, DEFAULT_LAT)) + GeojsonService.bbox(type: 'Feature', geometry: { type: 'Point', coordinates: [DEFAULT_LON, DEFAULT_LAT] }) end - - [bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates) end def to_feature_collection diff --git a/app/models/champs/departement_champ.rb b/app/models/champs/departement_champ.rb index 13edb4bb4..5a99ce8ad 100644 --- a/app/models/champs/departement_champ.rb +++ b/app/models/champs/departement_champ.rb @@ -21,6 +21,9 @@ # type_de_champ_id :integer # class Champs::DepartementChamp < Champs::TextChamp + validate :value_in_departement_names, unless: -> { value.nil? } + validate :external_id_in_departement_codes, unless: -> { external_id.nil? } + def for_export [name, code] end @@ -65,6 +68,9 @@ class Champs::DepartementChamp < Champs::TextChamp elsif code.blank? self.external_id = nil super(nil) + else + self.external_id = APIGeoService.departement_code(code) + super(code) end end @@ -73,4 +79,16 @@ class Champs::DepartementChamp < Champs::TextChamp def formatted_value blank? ? "" : "#{code} – #{name}" end + + def value_in_departement_names + return if value.in?(APIGeoService.departements.pluck(:name)) + + errors.add(:value, :not_in_departement_names) + end + + def external_id_in_departement_codes + return if external_id.in?(APIGeoService.departements.pluck(:code)) + + errors.add(:external_id, :not_in_departement_codes) + end end diff --git a/app/models/champs/dossier_link_champ.rb b/app/models/champs/dossier_link_champ.rb index a16936e7a..00c7f8869 100644 --- a/app/models/champs/dossier_link_champ.rb +++ b/app/models/champs/dossier_link_champ.rb @@ -21,4 +21,13 @@ # type_de_champ_id :integer # class Champs::DossierLinkChamp < Champ + validate :value_integerable, if: -> { value.present? }, on: :prefill + + private + + def value_integerable + Integer(value) + rescue ArgumentError + errors.add(:value, :not_integerable) + end end diff --git a/app/models/champs/epci_champ.rb b/app/models/champs/epci_champ.rb index ce48da5a5..45803bd8b 100644 --- a/app/models/champs/epci_champ.rb +++ b/app/models/champs/epci_champ.rb @@ -24,6 +24,10 @@ class Champs::EpciChamp < Champs::TextChamp store_accessor :value_json, :code_departement before_validation :on_departement_change + validate :code_departement_in_departement_codes, unless: -> { code_departement.nil? } + validate :external_id_in_departement_epci_codes, unless: -> { code_departement.nil? || external_id.nil? } + validate :value_in_departement_epci_names, unless: -> { code_departement.nil? || external_id.nil? || value.nil? } + def for_export [value, code, "#{code_departement} – #{departement_name}"] end @@ -74,4 +78,22 @@ class Champs::EpciChamp < Champs::TextChamp self.value = nil end end + + def code_departement_in_departement_codes + return if code_departement.in?(APIGeoService.departements.pluck(:code)) + + errors.add(:code_departement, :not_in_departement_codes) + end + + def external_id_in_departement_epci_codes + return if external_id.in?(APIGeoService.epcis(code_departement).pluck(:code)) + + errors.add(:external_id, :not_in_departement_epci_codes) + end + + def value_in_departement_epci_names + return if value.in?(APIGeoService.epcis(code_departement).pluck(:name)) + + errors.add(:value, :not_in_departement_epci_names) + end end diff --git a/app/models/champs/header_section_champ.rb b/app/models/champs/header_section_champ.rb index a397a73e4..4185db524 100644 --- a/app/models/champs/header_section_champ.rb +++ b/app/models/champs/header_section_champ.rb @@ -25,14 +25,6 @@ class Champs::HeaderSectionChamp < Champ # The user cannot enter any information here so it doesn’t make much sense to search end - def libelle_with_section_index - if sections&.none?(&:libelle_with_section_index?) - "#{section_index}. #{libelle}" - else - libelle - end - end - def libelle_with_section_index? libelle =~ /^\d/ end diff --git a/app/models/champs/multiple_drop_down_list_champ.rb b/app/models/champs/multiple_drop_down_list_champ.rb index 365332e30..4e041fd0b 100644 --- a/app/models/champs/multiple_drop_down_list_champ.rb +++ b/app/models/champs/multiple_drop_down_list_champ.rb @@ -23,6 +23,8 @@ class Champs::MultipleDropDownListChamp < Champ before_save :format_before_save + validate :values_are_in_options, if: -> { value.present? } + def options? drop_down_list_options? end @@ -90,4 +92,12 @@ class Champs::MultipleDropDownListChamp < Champ end end end + + def values_are_in_options + json = selected_options.reject(&:blank?) + return if json.empty? + return if (json - enabled_non_empty_options).empty? + + errors.add(:value, :not_in_options) + end end diff --git a/app/models/champs/rna_champ.rb b/app/models/champs/rna_champ.rb index bd1f08096..781e21014 100644 --- a/app/models/champs/rna_champ.rb +++ b/app/models/champs/rna_champ.rb @@ -21,6 +21,8 @@ # row_id :string # class Champs::RNAChamp < Champ + include RNAChampAssociationFetchableConcern + validates :value, allow_blank: true, format: { with: /\AW[0-9]{9}\z/, message: I18n.t(:not_a_rna, scope: 'activerecord.errors.messages') }, if: -> { validation_context != :brouillon } diff --git a/app/models/champs/siret_champ.rb b/app/models/champs/siret_champ.rb index 97c816fbf..0901c3227 100644 --- a/app/models/champs/siret_champ.rb +++ b/app/models/champs/siret_champ.rb @@ -21,6 +21,8 @@ # type_de_champ_id :integer # class Champs::SiretChamp < Champ + include SiretChampEtablissementFetchableConcern + def search_terms etablissement.present? ? etablissement.search_terms : [value] end diff --git a/app/models/concerns/dossier_prefillable_concern.rb b/app/models/concerns/dossier_prefillable_concern.rb index 245527e6f..3ecbe7a04 100644 --- a/app/models/concerns/dossier_prefillable_concern.rb +++ b/app/models/concerns/dossier_prefillable_concern.rb @@ -7,7 +7,7 @@ module DossierPrefillableConcern return unless champs_public_attributes.any? attr = { prefilled: true } - attr[:champs_public_attributes] = champs_public_attributes.map { |h| h.merge(prefilled: true) } + attr[:champs_public_all_attributes] = champs_public_attributes.map { |h| h.merge(prefilled: true) } assign_attributes(attr) save(validate: false) diff --git a/app/models/concerns/dossier_sections_concern.rb b/app/models/concerns/dossier_sections_concern.rb new file mode 100644 index 000000000..bd8166efe --- /dev/null +++ b/app/models/concerns/dossier_sections_concern.rb @@ -0,0 +1,35 @@ +module DossierSectionsConcern + extend ActiveSupport::Concern + + included do + def sections_for(champ) + @sections = Hash.new do |hash, parent| + case parent + when :public + hash[parent] = champs_public.filter(&:header_section?) + when :private + hash[parent] = champs_private.filter(&:header_section?) + else + hash[parent] = parent.champs.filter(&:header_section?) + end + end + @sections[champ.parent || (champ.public? ? :public : :private)] + end + + def auto_numbering_section_headers_for?(champ) + sections_for(champ)&.none?(&:libelle_with_section_index?) + end + + def index_for_section_header(champ) + champs = champ.private? ? champs_private : champs_public + + index = 1 + champs.each do |c| + return index if c.stable_id == champ.stable_id + next unless c.visible? + + index += 1 if c.type_de_champ.header_section? + end + end + end +end diff --git a/app/models/concerns/mail_template_concern.rb b/app/models/concerns/mail_template_concern.rb index e778c681d..a5035bbd8 100644 --- a/app/models/concerns/mail_template_concern.rb +++ b/app/models/concerns/mail_template_concern.rb @@ -10,7 +10,7 @@ module MailTemplateConcern end def subject_for_dossier(dossier) - replace_tags(subject, dossier) + replace_tags(subject, dossier).presence || replace_tags(self.class::DEFAULT_SUBJECT, dossier) end def body_for_dossier(dossier) diff --git a/app/models/concerns/procedure_stats_concern.rb b/app/models/concerns/procedure_stats_concern.rb index 63960833c..74f7dacd7 100644 --- a/app/models/concerns/procedure_stats_concern.rb +++ b/app/models/concerns/procedure_stats_concern.rb @@ -22,21 +22,20 @@ module ProcedureStatsConcern def stats_dossiers_funnel Rails.cache.fetch("#{cache_key_with_version}/stats_dossiers_funnel", expires_in: 12.hours) do [ - ['Démarrés', dossiers.count], - ['Déposés', dossiers.state_not_brouillon.count], - ['Instruction débutée', dossiers.state_instruction_commencee.count], - ['Traités', dossiers.state_termine.count] + ['Démarrés', dossiers.visible_by_user_or_administration.count + nb_dossiers_termines_supprimes], + ['Déposés', dossiers.visible_by_administration.count + nb_dossiers_termines_supprimes], + ['Instruction débutée', dossiers.visible_by_administration.state_instruction_commencee.count + nb_dossiers_termines_supprimes], + ['Traités', nb_dossiers_termines] ] end end def stats_termines_states - nb_dossiers_termines = dossiers.state_termine.count Rails.cache.fetch("#{cache_key_with_version}/stats_termines_states", expires_in: 12.hours) do [ - ['Acceptés', percentage(dossiers.where(state: :accepte).count, nb_dossiers_termines)], - ['Refusés', percentage(dossiers.where(state: :refuse).count, nb_dossiers_termines)], - ['Classés sans suite', percentage(dossiers.where(state: :sans_suite).count, nb_dossiers_termines)] + ['Acceptés', percentage(dossiers.visible_by_administration.state_accepte.count, nb_dossiers_termines)], + ['Refusés', percentage(dossiers.visible_by_administration.state_refuse.count, nb_dossiers_termines)], + ['Classés sans suite', percentage(dossiers.visible_by_administration.state_sans_suite.count, nb_dossiers_termines)] ] end end @@ -45,6 +44,7 @@ module ProcedureStatsConcern Rails.cache.fetch("#{cache_key_with_version}/stats_termines_by_week", expires_in: 12.hours) do now = Time.zone.now chart_data = dossiers.includes(:traitements) + .visible_by_administration .state_termine .where(traitements: { processed_at: (now.beginning_of_week - 6.months)..now.end_of_week }) @@ -95,6 +95,14 @@ module ProcedureStatsConcern private + def nb_dossiers_termines + @nb_dossiers_termines ||= dossiers.visible_by_administration.state_termine.count + nb_dossiers_termines_supprimes + end + + def nb_dossiers_termines_supprimes + @nb_dossiers_termines_supprimes ||= deleted_dossiers.state_termine.count + end + def first_processed_at Traitement.for_traitement_time_stats(self).pick(:processed_at) end diff --git a/app/models/concerns/rna_champ_association_fetchable_concern.rb b/app/models/concerns/rna_champ_association_fetchable_concern.rb new file mode 100644 index 000000000..87e74ddbd --- /dev/null +++ b/app/models/concerns/rna_champ_association_fetchable_concern.rb @@ -0,0 +1,27 @@ +module RNAChampAssociationFetchableConcern + extend ActiveSupport::Concern + + attr_reader :association_fetch_error_key + + def fetch_association!(rna) + self.value = rna + + return clear_association!(:empty) if rna.empty? + return clear_association!(:invalid) unless valid? + return clear_association!(:not_found) if (data = APIEntreprise::RNAAdapter.new(rna, procedure_id).to_params).blank? + + update!(data: data) + rescue APIEntreprise::API::Error => error + error_key = :network_error if error.try(:network_error?) && !APIEntrepriseService.api_up? + clear_association!(error_key) + end + + private + + def clear_association!(error) + @association_fetch_error_key = error + self.data = nil + save!(context: :brouillon) + false + end +end diff --git a/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb b/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb new file mode 100644 index 000000000..6a36176b6 --- /dev/null +++ b/app/models/concerns/siret_champ_etablissement_fetchable_concern.rb @@ -0,0 +1,46 @@ +module SiretChampEtablissementFetchableConcern + extend ActiveSupport::Concern + + attr_reader :etablissement_fetch_error_key + + def fetch_etablissement!(siret, user) + return clear_etablissement!(:empty) if siret.empty? + return clear_etablissement!(:invalid_length) if invalid_because?(siret, :length) # i18n-tasks-use t('errors.messages.invalid_siret_length') + return clear_etablissement!(:invalid_checksum) if invalid_because?(siret, :checksum) # i18n-tasks-use t('errors.messages.invalid_siret_checksum') + return clear_etablissement!(:not_found) unless (etablissement = APIEntrepriseService.create_etablissement(self, siret, user&.id)) # i18n-tasks-use t('errors.messages.siret_not_found') + + update!(value: siret, etablissement: etablissement) + rescue => error + if error.try(:network_error?) && !APIEntrepriseService.api_up? + # TODO: notify ops + update!( + value: siret, + etablissement: APIEntrepriseService.create_etablissement_as_degraded_mode(self, siret, user.id) + ) + @etablissement_fetch_error_key = :api_entreprise_down + false + else + Sentry.capture_exception(error, extra: { dossier_id: dossier_id, siret: siret }) + clear_etablissement!(:network_error) # i18n-tasks-use t('errors.messages.siret_network_error') + end + end + + private + + def clear_etablissement!(error_key) + @etablissement_fetch_error_key = error_key + + etablissement_to_destroy = etablissement + update!(etablissement: nil) + etablissement_to_destroy&.destroy + + false + end + + def invalid_because?(siret, criteria) + validatable_siret = Siret.new(siret: siret) + return false if validatable_siret.valid? + + validatable_siret.errors.details[:siret].any? && validatable_siret.errors.details[:siret].first[:error] == criteria + end +end diff --git a/app/models/concerns/tags_substitution_concern.rb b/app/models/concerns/tags_substitution_concern.rb index 972882f5b..86f5f1100 100644 --- a/app/models/concerns/tags_substitution_concern.rb +++ b/app/models/concerns/tags_substitution_concern.rb @@ -270,12 +270,12 @@ module TagsSubstitutionConcern end def champ_public_tags(dossier: nil) - types_de_champ = (dossier || procedure.active_revision).types_de_champ_public.not_condition + types_de_champ = (dossier || procedure.active_revision).types_de_champ_public.filter { !_1.condition? } types_de_champ_tags(types_de_champ, Dossier::SOUMIS) end def champ_private_tags(dossier: nil) - types_de_champ = (dossier || procedure.active_revision).types_de_champ_private.not_condition + types_de_champ = (dossier || procedure.active_revision).types_de_champ_private.filter { !_1.condition? } types_de_champ_tags(types_de_champ, Dossier::INSTRUCTION_COMMENCEE) end diff --git a/app/models/deleted_dossier.rb b/app/models/deleted_dossier.rb index 61bcd66f6..edb9632ac 100644 --- a/app/models/deleted_dossier.rb +++ b/app/models/deleted_dossier.rb @@ -20,6 +20,7 @@ class DeletedDossier < ApplicationRecord scope :order_by_updated_at, -> (order = :desc) { order(created_at: order) } scope :deleted_since, -> (since) { where('deleted_dossiers.deleted_at >= ?', since) } + scope :state_termine, -> { where(state: [states.fetch(:accepte), states.fetch(:refuse), states.fetch(:sans_suite)]) } enum reason: { user_request: 'user_request', @@ -30,6 +31,14 @@ class DeletedDossier < ApplicationRecord instructeur_request: 'instructeur_request' } + enum state: { + en_construction: 'en_construction', + en_instruction: 'en_instruction', + accepte: 'accepte', + refuse: 'refuse', + sans_suite: 'sans_suite' + } + def self.create_from_dossier(dossier, reason) return if !dossier.log_operations? diff --git a/app/models/dossier.rb b/app/models/dossier.rb index db8893a17..d2a446f9d 100644 --- a/app/models/dossier.rb +++ b/app/models/dossier.rb @@ -48,8 +48,9 @@ class Dossier < ApplicationRecord self.ignored_columns = [:en_construction_conservation_extension] include DossierFilteringConcern - include DossierRebaseConcern include DossierPrefillableConcern + include DossierRebaseConcern + include DossierSectionsConcern enum state: { brouillon: 'brouillon', @@ -224,6 +225,9 @@ class Dossier < ApplicationRecord scope :state_instruction_commencee, -> { where(state: INSTRUCTION_COMMENCEE) } scope :state_termine, -> { where(state: TERMINE) } scope :state_not_termine, -> { where.not(state: TERMINE) } + scope :state_accepte, -> { where(state: states.fetch(:accepte)) } + scope :state_refuse, -> { where(state: states.fetch(:refuse)) } + scope :state_sans_suite, -> { where(state: states.fetch(:sans_suite)) } scope :archived, -> { where(archived: true) } scope :not_archived, -> { where(archived: false) } @@ -874,6 +878,7 @@ class Dossier < ApplicationRecord .passer_en_construction .processed_at save! + procedure.compute_dossiers_count end def after_passer_en_instruction(h) @@ -1233,20 +1238,6 @@ class Dossier < ApplicationRecord termine_expired_to_delete.find_each(&:purge_discarded) end - def sections_for(champ) - @sections = Hash.new do |hash, parent| - case parent - when :public - hash[parent] = champs_public.filter(&:header_section?) - when :private - hash[parent] = champs_private.filter(&:header_section?) - else - hash[parent] = parent.champs.filter(&:header_section?) - end - end - @sections[champ.parent || (champ.public? ? :public : :private)] - end - def clone dossier_attributes = [:autorisation_donnees, :user_id, :revision_id, :groupe_instructeur_id] relationships = [:individual, :etablissement] @@ -1312,14 +1303,7 @@ class Dossier < ApplicationRecord end def bounding_box - factory = RGeo::Geographic.simple_mercator_factory - bounding_box = RGeo::Cartesian::BoundingBox.new(factory) - - geo_areas.filter_map(&:rgeo_geometry).each do |geometry| - bounding_box.add(geometry) - end - - [bounding_box.max_point, bounding_box.min_point].compact.flat_map(&:coordinates) + GeojsonService.bbox(type: 'FeatureCollection', features: geo_areas.map(&:to_feature)) end def log_dossier_operation(author, operation, subject = nil) diff --git a/app/models/geo_area.rb b/app/models/geo_area.rb index 024b20d4d..57040c80d 100644 --- a/app/models/geo_area.rb +++ b/app/models/geo_area.rb @@ -52,11 +52,12 @@ class GeoArea < ApplicationRecord scope :cadastres, -> { where(source: sources.fetch(:cadastre)) } validates :geometry, geo_json: true, allow_blank: false + before_validation :normalize_geometry def to_feature { type: 'Feature', - geometry: safe_geometry, + geometry: geometry.deep_symbolize_keys, properties: cadastre_properties.merge( source: source, area: area, @@ -96,16 +97,6 @@ class GeoArea < ApplicationRecord end end - def safe_geometry - RGeo::GeoJSON.encode(rgeo_geometry) - end - - def rgeo_geometry - RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) - rescue RGeo::Error::InvalidGeometry - nil - end - def area if polygon? GeojsonService.area(geometry.deep_symbolize_keys).round(1) @@ -120,7 +111,7 @@ class GeoArea < ApplicationRecord def location if point? - Geo::Coord.new(*rgeo_geometry.coordinates.reverse).to_s + Geo::Coord.new(*geometry['coordinates'].reverse).to_s end end @@ -238,4 +229,21 @@ class GeoArea < ApplicationRecord properties['id'] end end + + private + + def normalize_geometry + if geometry.present? + normalized_geometry = rgeo_geometry + if normalized_geometry.present? + self.geometry = RGeo::GeoJSON.encode(normalized_geometry) + end + end + end + + def rgeo_geometry + RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) + rescue RGeo::Error::InvalidGeometry + nil + end end diff --git a/app/models/groupe_instructeur.rb b/app/models/groupe_instructeur.rb index 30efb9956..3ea1e9334 100644 --- a/app/models/groupe_instructeur.rb +++ b/app/models/groupe_instructeur.rb @@ -66,6 +66,7 @@ class GroupeInstructeur < ApplicationRecord # We dont't want to assign a user to a groupe_instructeur if they are already assigned to it instructeurs_to_add -= instructeurs + instructeurs_to_add.each { add(_1) } [instructeurs_to_add, invalid_emails] end diff --git a/app/models/prefill_description.rb b/app/models/prefill_description.rb index 2148b3dce..a2c66ac92 100644 --- a/app/models/prefill_description.rb +++ b/app/models/prefill_description.rb @@ -15,7 +15,7 @@ class PrefillDescription < SimpleDelegator end def types_de_champ - TypesDeChamp::PrefillTypeDeChamp.wrap(active_revision.types_de_champ_public.fillable.partition(&:prefillable?).flatten) + TypesDeChamp::PrefillTypeDeChamp.wrap(active_revision.types_de_champ_public.fillable.partition(&:prefillable?).flatten, active_revision) end def include?(type_de_champ_id) @@ -27,7 +27,7 @@ class PrefillDescription < SimpleDelegator end def prefill_link - @prefill_link ||= commencer_url({ path: path }.merge(prefilled_champs_for_link)) + @prefill_link ||= CGI.unescape(commencer_url({ path: path }.merge(prefilled_champs_as_params))) end def prefill_query @@ -35,25 +35,21 @@ class PrefillDescription < SimpleDelegator <<~TEXT curl --request POST '#{api_public_v1_dossiers_url(self)}' \\ --header 'Content-Type: application/json' \\ - --data '{#{prefilled_champs_for_query}}' + --data '#{prefilled_champs_as_params.to_json}' TEXT end def prefilled_champs - @prefilled_champs ||= TypesDeChamp::PrefillTypeDeChamp.wrap(active_fillable_public_types_de_champ.where(id: selected_type_de_champ_ids)) + @prefilled_champs ||= TypesDeChamp::PrefillTypeDeChamp.wrap(active_fillable_public_types_de_champ.where(id: selected_type_de_champ_ids), active_revision) end private - def prefilled_champs_for_link - prefilled_champs.map { |type_de_champ| ["champ_#{type_de_champ.to_typed_id}", type_de_champ.example_value] }.to_h - end - - def prefilled_champs_for_query - prefilled_champs.map { |type_de_champ| "\"champ_#{type_de_champ.to_typed_id}\": \"#{type_de_champ.example_value}\"" } .join(', ') - end - def active_fillable_public_types_de_champ active_revision.types_de_champ_public.fillable end + + def prefilled_champs_as_params + prefilled_champs.map { |type_de_champ| ["champ_#{type_de_champ.to_typed_id_for_query}", type_de_champ.example_value] }.to_h + end end diff --git a/app/models/prefill_params.rb b/app/models/prefill_params.rb index e4ded1975..0623c763f 100644 --- a/app/models/prefill_params.rb +++ b/app/models/prefill_params.rb @@ -1,33 +1,27 @@ class PrefillParams + attr_reader :dossier, :params + def initialize(dossier, params) @dossier = dossier @params = params end def to_a - build_prefill_values.filter(&:prefillable?).map(&:to_h) + build_prefill_values.filter(&:prefillable?).map(&:champ_attributes).flatten end private def build_prefill_values - value_by_stable_id = @params - .map { |prefixed_typed_id, value| [stable_id_from_typed_id(prefixed_typed_id), value] } + value_by_stable_id = params + .map { |prefixed_typed_id, value| [Champ.stable_id_from_typed_id(prefixed_typed_id), value] } .filter { |stable_id, value| stable_id.present? && value.present? } .to_h - @dossier + dossier .find_champs_by_stable_ids(value_by_stable_id.keys) .map { |champ| [champ, value_by_stable_id[champ.stable_id]] } - .map { |champ, value| PrefillValue.new(champ:, value:) } - end - - def stable_id_from_typed_id(prefixed_typed_id) - return nil unless prefixed_typed_id.starts_with?("champ_") - - Champ.id_from_typed_id(prefixed_typed_id.gsub("champ_", "")).to_i - rescue - nil + .map { |champ, value| PrefillValue.new(champ:, value:, dossier:) } end class PrefillValue @@ -40,25 +34,29 @@ class PrefillParams TypeDeChamp.type_champs.fetch(:yes_no), TypeDeChamp.type_champs.fetch(:checkbox), TypeDeChamp.type_champs.fetch(:pays), - TypeDeChamp.type_champs.fetch(:regions) + TypeDeChamp.type_champs.fetch(:regions), + TypeDeChamp.type_champs.fetch(:departements), + TypeDeChamp.type_champs.fetch(:multiple_drop_down_list), + TypeDeChamp.type_champs.fetch(:epci), + TypeDeChamp.type_champs.fetch(:dossier_link) ] - attr_reader :champ, :value + attr_reader :champ, :value, :dossier - def initialize(champ:, value:) + def initialize(champ:, value:, dossier:) @champ = champ @value = value + @dossier = dossier end def prefillable? - champ.prefillable? && valid? + champ.prefillable? && valid? && champ_attributes.present? end - def to_h - { - id: champ.id, - value: value - } + def champ_attributes + @champ_attributes ||= TypesDeChamp::PrefillTypeDeChamp + .build(champ.type_de_champ, dossier.revision) + .to_assignable_attributes(champ, value) end private @@ -66,7 +64,7 @@ class PrefillParams def valid? return true unless NEED_VALIDATION_TYPES_DE_CHAMPS.include?(champ.type_champ) - champ.value = value + champ.assign_attributes(champ_attributes) champ.valid?(:prefill) end end diff --git a/app/models/procedure.rb b/app/models/procedure.rb index b6ef52750..1f8d42b72 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -15,10 +15,12 @@ # closed_at :datetime # declarative_with_state :string # description :string +# dossiers_count_computed_at :datetime # duree_conservation_dossiers_dans_ds :integer # duree_conservation_etendue_par_ds :boolean default(FALSE) # encrypted_api_particulier_token :string # estimated_duration_visible :boolean default(TRUE), not null +# estimated_dossiers_count :integer # euro_flag :boolean default(FALSE) # experts_require_administrateur_invitation :boolean default(FALSE) # for_individual :boolean default(FALSE) @@ -71,6 +73,8 @@ class Procedure < ApplicationRecord MIN_WEIGHT = 350000 + DOSSIERS_COUNT_EXPIRING = 1.hour + attr_encrypted :api_particulier_token has_many :revisions, -> { order(:id) }, class_name: 'ProcedureRevision', inverse_of: :procedure @@ -173,12 +177,15 @@ class Procedure < ApplicationRecord types_de_champ_for_tags.private_only end - def revision_ids_with_pending_dossiers - dossiers - .where.not(revision_id: [draft_revision_id, published_revision_id].compact) - .state_en_construction_ou_instruction - .distinct(:revision_id) - .pluck(:revision_id) + def revisions_with_pending_dossiers + @revisions_with_pending_dossiers ||= begin + ids = dossiers + .where.not(revision_id: [draft_revision_id, published_revision_id].compact) + .state_en_construction_ou_instruction + .distinct(:revision_id) + .pluck(:revision_id) + ProcedureRevision.includes(revision_types_de_champ: [:type_de_champ]).where(id: ids) + end end has_many :administrateurs_procedures, dependent: :delete_all @@ -833,7 +840,13 @@ class Procedure < ApplicationRecord self.connection.query(query.to_sql).flatten end - private + def compute_dossiers_count + now = Time.zone.now + if now > (self.dossiers_count_computed_at || self.created_at) + DOSSIERS_COUNT_EXPIRING + self.update(estimated_dossiers_count: self.dossiers.visible_by_administration.count, + dossiers_count_computed_at: now) + end + end def move_new_children_to_new_parent_coordinate(new_draft) children = new_draft.revision_types_de_champ diff --git a/app/models/procedure_presentation.rb b/app/models/procedure_presentation.rb index 4f536db34..f29885be5 100644 --- a/app/models/procedure_presentation.rb +++ b/app/models/procedure_presentation.rb @@ -89,8 +89,8 @@ class ProcedurePresentation < ApplicationRecord end fields.concat procedure.types_de_champ_for_procedure_presentation - .pluck(:libelle, :private, :stable_id) - .map { |(libelle, is_private, stable_id)| field_hash(is_private ? TYPE_DE_CHAMP_PRIVATE : TYPE_DE_CHAMP, stable_id.to_s, label: libelle, type: :text) } + .pluck(:type_champ, :libelle, :private, :stable_id) + .map { |(type_champ, libelle, is_private, stable_id)| field_hash(is_private ? TYPE_DE_CHAMP_PRIVATE : TYPE_DE_CHAMP, stable_id.to_s, label: libelle, type: (TypeDeChamp.options_for_select?(type_champ) ? :enum : :text)) } fields end @@ -335,6 +335,8 @@ class ProcedurePresentation < ApplicationRecord [_1.label, _1.id] end end + else + find_type_de_champ(field['column']).options_for_select end end diff --git a/app/models/procedure_revision.rb b/app/models/procedure_revision.rb index 7e94d0595..0f1196477 100644 --- a/app/models/procedure_revision.rb +++ b/app/models/procedure_revision.rb @@ -32,6 +32,8 @@ class ProcedureRevision < ApplicationRecord validate :conditions_are_valid? + delegate :path, to: :procedure, prefix: true + def build_champs_public # reload: it can be out of sync in test if some tdcs are added wihtout using add_tdc types_de_champ_public.reload.map(&:build_champ) @@ -132,7 +134,7 @@ class ProcedureRevision < ApplicationRecord end def draft? - procedure.draft_revision == self + procedure.draft_revision_id == id end def locked? @@ -172,11 +174,22 @@ class ProcedureRevision < ApplicationRecord end def children_of(tdc) - parent_coordinate_id = revision_types_de_champ.where(type_de_champ: tdc).select(:id) + if revision_types_de_champ.loaded? + parent_coordinate_id = revision_types_de_champ + .filter { _1.type_de_champ_id == tdc.id } + .map(&:id) - types_de_champ - .where(procedure_revision_types_de_champ: { parent_id: parent_coordinate_id }) - .order("procedure_revision_types_de_champ.position") + revision_types_de_champ + .filter { _1.parent_id.in?(parent_coordinate_id) } + .sort_by(&:position) + .map(&:type_de_champ) + else + parent_coordinate_id = revision_types_de_champ.where(type_de_champ: tdc).select(:id) + + types_de_champ + .where(procedure_revision_types_de_champ: { parent_id: parent_coordinate_id }) + .order("procedure_revision_types_de_champ.position") + end end def remove_children_of(tdc) @@ -380,7 +393,7 @@ class ProcedureRevision < ApplicationRecord public_tdcs .map.with_index - .filter_map { |tdc, i| tdc.condition.present? ? [tdc, i] : nil } + .filter_map { |tdc, i| tdc.condition? ? [tdc, i] : nil } .map { |tdc, i| [tdc, tdc.condition.errors(public_tdcs.take(i))] } .filter { |_tdc, errors| errors.present? } .each { |tdc, message| errors.add(:condition, message, type_de_champ: tdc) } diff --git a/app/models/stat.rb b/app/models/stat.rb index 097c0c592..d4662a410 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -63,16 +63,15 @@ class Stat < ApplicationRecord def deleted_dossiers_states sanitize_and_exec(DeletedDossier, <<-EOF SELECT - COUNT(*) FILTER ( WHERE state != 'brouillon' ) AS "not_brouillon", - COUNT(*) FILTER ( WHERE state != 'brouillon' and deleted_at BETWEEN :one_month_ago AND :now ) AS "dossiers_depose_avant_30_jours", - COUNT(*) FILTER ( WHERE state != 'brouillon' and deleted_at BETWEEN :two_months_ago AND :one_month_ago ) AS "dossiers_deposes_entre_60_et_30_jours", - COUNT(*) FILTER ( WHERE state = 'brouillon' ) AS "brouillon", + COUNT(*) AS "not_brouillon", + COUNT(*) FILTER ( WHERE deleted_at BETWEEN :one_month_ago AND :now ) AS "dossiers_depose_avant_30_jours", + COUNT(*) FILTER ( WHERE deleted_at BETWEEN :two_months_ago AND :one_month_ago ) AS "dossiers_deposes_entre_60_et_30_jours", COUNT(*) FILTER ( WHERE state = 'en_construction' ) AS "en_construction", COUNT(*) FILTER ( WHERE state = 'en_instruction' ) AS "en_instruction", COUNT(*) FILTER ( WHERE state in ('accepte', 'refuse', 'sans_suite') ) AS "termines" FROM deleted_dossiers EOF - ) + ).merge('brouillon' => 0) end def last_four_months_serie(associations_with_date_attribute) diff --git a/app/models/traitement.rb b/app/models/traitement.rb index a4f882810..459eb3247 100644 --- a/app/models/traitement.rb +++ b/app/models/traitement.rb @@ -21,7 +21,7 @@ class Traitement < ApplicationRecord scope :for_traitement_time_stats, -> (procedure) do includes(:dossier) .termine - .where(dossier: procedure.dossiers) + .where(dossier: procedure.dossiers.visible_by_administration) .where.not('dossiers.depose_at' => nil, processed_at: nil) .order(:processed_at) end diff --git a/app/models/type_de_champ.rb b/app/models/type_de_champ.rb index afa6a821a..9fd75e087 100644 --- a/app/models/type_de_champ.rb +++ b/app/models/type_de_champ.rb @@ -192,6 +192,7 @@ class TypeDeChamp < ApplicationRecord validates :type_champ, presence: true, allow_blank: false, allow_nil: false before_validation :check_mandatory + before_validation :normalize_libelle before_save :remove_piece_justificative_template, if: -> { type_champ_changed? } before_validation :remove_drop_down_list, if: -> { type_champ_changed? } before_save :remove_block, if: -> { type_champ_changed? } @@ -262,12 +263,21 @@ class TypeDeChamp < ApplicationRecord TypeDeChamp.type_champs.fetch(:iban), TypeDeChamp.type_champs.fetch(:civilite), TypeDeChamp.type_champs.fetch(:pays), + TypeDeChamp.type_champs.fetch(:regions), + TypeDeChamp.type_champs.fetch(:departements), + TypeDeChamp.type_champs.fetch(:communes), TypeDeChamp.type_champs.fetch(:date), TypeDeChamp.type_champs.fetch(:datetime), TypeDeChamp.type_champs.fetch(:yes_no), TypeDeChamp.type_champs.fetch(:checkbox), TypeDeChamp.type_champs.fetch(:drop_down_list), - TypeDeChamp.type_champs.fetch(:regions) + TypeDeChamp.type_champs.fetch(:repetition), + TypeDeChamp.type_champs.fetch(:multiple_drop_down_list), + TypeDeChamp.type_champs.fetch(:epci), + TypeDeChamp.type_champs.fetch(:annuaire_education), + TypeDeChamp.type_champs.fetch(:dossier_link), + TypeDeChamp.type_champs.fetch(:siret), + TypeDeChamp.type_champs.fetch(:rna) ]) end @@ -366,6 +376,14 @@ class TypeDeChamp < ApplicationRecord type_champ == TypeDeChamp.type_champs.fetch(:pole_emploi) end + def departement? + type_champ == TypeDeChamp.type_champs.fetch(:departements) + end + + def region? + type_champ == TypeDeChamp.type_champs.fetch(:regions) + end + def mesri? type_champ == TypeDeChamp.type_champs.fetch(:mesri) end @@ -402,6 +420,21 @@ class TypeDeChamp < ApplicationRecord self.drop_down_options = parse_drop_down_list_value(value) end + def self.options_for_select?(type_champs) + [ + TypeDeChamp.type_champs.fetch(:departements), + TypeDeChamp.type_champs.fetch(:regions) + ].include?(type_champs) + end + + def options_for_select + if departement? + APIGeoService.departements.map { ["#{_1[:code]} – #{_1[:name]}", _1[:name]] } + elsif region? + APIGeoService.regions.map { [_1[:name], _1[:name]] } + end + end + # historicaly we added a blank ("") option by default to avoid wrong selection # see self.parse_drop_down_list_value # then rails decided to add this blank ("") option when the select is required @@ -523,4 +556,8 @@ class TypeDeChamp < ApplicationRecord .remove_children_of(self) end end + + def normalize_libelle + self.libelle&.strip! + end end diff --git a/app/models/types_de_champ/prefill_annuaire_education_type_de_champ.rb b/app/models/types_de_champ/prefill_annuaire_education_type_de_champ.rb new file mode 100644 index 000000000..7790458d9 --- /dev/null +++ b/app/models/types_de_champ/prefill_annuaire_education_type_de_champ.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class TypesDeChamp::PrefillAnnuaireEducationTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp + def to_assignable_attributes(champ, value) + return nil if value.blank? + + { + id: champ.id, + external_id: value, + value: value + } + end +end diff --git a/app/models/types_de_champ/prefill_commune_type_de_champ.rb b/app/models/types_de_champ/prefill_commune_type_de_champ.rb new file mode 100644 index 000000000..03f5acdee --- /dev/null +++ b/app/models/types_de_champ/prefill_commune_type_de_champ.rb @@ -0,0 +1,50 @@ +class TypesDeChamp::PrefillCommuneTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp + def all_possible_values + departements.map do |departement| + "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/communes?codeDepartement=#{departement[:code]}" + end + end + + def example_value + departement_code = departements.pick(:code) + commune_code = APIGeoService.communes(departement_code).pick(:code) + [departement_code, commune_code] + end + + def to_assignable_attributes(champ, value) + return if value.blank? || !value.is_a?(Array) + return if (departement_code = value.first).blank? + return if (departement_name = APIGeoService.departement_name(departement_code)).blank? + return if !value.one? && (commune_code = value.second).blank? + return if !value.one? && (commune_name = APIGeoService.commune_name(departement_code, commune_code)).blank? + + if value.one? + departement_attributes(champ, departement_code, departement_name) + else + departement_and_commune_attributes(champ, departement_code, departement_name, commune_code, commune_name) + end + end + + private + + def departement_attributes(champ, departement_code, departement_name) + { + id: champ.id, + code_departement: departement_code, + departement: departement_name + } + end + + def departement_and_commune_attributes(champ, departement_code, departement_name, commune_code, commune_name) + postal_code = APIGeoService.commune_postal_codes(departement_code, commune_code).first + + departement_attributes(champ, departement_code, departement_name).merge( + external_id: commune_code, + value: "#{commune_name} (#{postal_code})" + ) + end + + def departements + @departements ||= APIGeoService.departements.sort_by { _1[:code] } + end +end diff --git a/app/models/types_de_champ/prefill_departement_type_de_champ.rb b/app/models/types_de_champ/prefill_departement_type_de_champ.rb new file mode 100644 index 000000000..4c92cad1d --- /dev/null +++ b/app/models/types_de_champ/prefill_departement_type_de_champ.rb @@ -0,0 +1,11 @@ +class TypesDeChamp::PrefillDepartementTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp + def all_possible_values + departements.map { |departement| "#{departement[:code]} (#{departement[:name]})" } + end + + private + + def departements + @departements ||= APIGeoService.departements.sort_by { |departement| departement[:code] } + end +end diff --git a/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb index 7e3a4ad22..6930905e7 100644 --- a/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_drop_down_list_type_de_champ.rb @@ -1,5 +1,5 @@ class TypesDeChamp::PrefillDropDownListTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp - def possible_values + def all_possible_values if drop_down_other? drop_down_list_enabled_non_empty_options.insert( 0, @@ -11,6 +11,6 @@ class TypesDeChamp::PrefillDropDownListTypeDeChamp < TypesDeChamp::PrefillTypeDe end def example_value - possible_values.first + all_possible_values.first end end diff --git a/app/models/types_de_champ/prefill_epci_type_de_champ.rb b/app/models/types_de_champ/prefill_epci_type_de_champ.rb new file mode 100644 index 000000000..f50c2e35b --- /dev/null +++ b/app/models/types_de_champ/prefill_epci_type_de_champ.rb @@ -0,0 +1,25 @@ +class TypesDeChamp::PrefillEpciTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp + def all_possible_values + departements.map do |departement| + "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/epcis?codeDepartement=#{departement[:code]}" + end + end + + def example_value + departement_code = departements.pick(:code) + epci_code = APIGeoService.epcis(departement_code).pick(:code) + [departement_code, epci_code] + end + + def to_assignable_attributes(champ, value) + return { id: champ.id, code_departement: nil, value: nil } if value.blank? || !value.is_a?(Array) + return { id: champ.id, code_departement: value.first, value: nil } if value.one? + { id: champ.id, code_departement: value.first, value: value.second } + end + + private + + def departements + @departements ||= APIGeoService.departements.sort_by { |departement| departement[:code] } + end +end diff --git a/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb b/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb new file mode 100644 index 000000000..17bea90a4 --- /dev/null +++ b/app/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ.rb @@ -0,0 +1,8 @@ +class TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp < TypesDeChamp::PrefillDropDownListTypeDeChamp + def example_value + return nil if all_possible_values.empty? + return all_possible_values.first if all_possible_values.one? + + [all_possible_values.first, all_possible_values.second] + end +end diff --git a/app/models/types_de_champ/prefill_pays_type_de_champ.rb b/app/models/types_de_champ/prefill_pays_type_de_champ.rb index 143f041e7..9c638c904 100644 --- a/app/models/types_de_champ/prefill_pays_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_pays_type_de_champ.rb @@ -1,12 +1,8 @@ class TypesDeChamp::PrefillPaysTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp - def possible_values + def all_possible_values countries.map { |country| "#{country[:code]} (#{country[:name]})" } end - def example_value - countries.pick(:code) - end - private def countries diff --git a/app/models/types_de_champ/prefill_region_type_de_champ.rb b/app/models/types_de_champ/prefill_region_type_de_champ.rb index 3084206a1..ae9d0501a 100644 --- a/app/models/types_de_champ/prefill_region_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_region_type_de_champ.rb @@ -1,5 +1,5 @@ class TypesDeChamp::PrefillRegionTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp - def possible_values + def all_possible_values regions.map { |region| "#{region[:code]} (#{region[:name]})" } end diff --git a/app/models/types_de_champ/prefill_repetition_type_de_champ.rb b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb new file mode 100644 index 000000000..61e3a15be --- /dev/null +++ b/app/models/types_de_champ/prefill_repetition_type_de_champ.rb @@ -0,0 +1,69 @@ +class TypesDeChamp::PrefillRepetitionTypeDeChamp < TypesDeChamp::PrefillTypeDeChamp + include ActionView::Helpers::UrlHelper + include ApplicationHelper + + def possible_values + [ + I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html"), + subchamps_all_possible_values + ].join("
").html_safe # rubocop:disable Rails/OutputSafety + end + + def example_value + [row_values_format, row_values_format] + end + + def to_assignable_attributes(champ, value) + return [] unless value.is_a?(Array) + + value.map.with_index do |repetition, index| + PrefillRepetitionRow.new(champ, repetition, index, @revision).to_assignable_attributes + end.reject(&:blank?) + end + + private + + def subchamps_all_possible_values + "
    " + prefillable_subchamps.map do |prefill_type_de_champ| + "
  • champ_#{prefill_type_de_champ.to_typed_id_for_query}: #{prefill_type_de_champ.possible_values}
  • " + end.join + "
" + end + + def row_values_format + @row_example_value ||= + prefillable_subchamps.map do |prefill_type_de_champ| + ["champ_#{prefill_type_de_champ.to_typed_id_for_query}", prefill_type_de_champ.example_value.to_s] + end.to_h + end + + def prefillable_subchamps + @prefillable_subchamps ||= + TypesDeChamp::PrefillTypeDeChamp.wrap(@revision.children_of(self).filter(&:prefillable?), @revision) + end + + class PrefillRepetitionRow + attr_reader :champ, :repetition, :index, :revision + + def initialize(champ, repetition, index, revision) + @champ = champ + @repetition = repetition + @index = index + @revision = revision + end + + def to_assignable_attributes + return unless repetition.is_a?(Hash) + + row = champ.rows[index] || champ.add_row(champ.dossier_revision) + + repetition.map do |key, value| + next unless key.is_a?(String) && key.starts_with?("champ_") + + subchamp = row.find { |champ| champ.stable_id == Champ.stable_id_from_typed_id(key) } + next unless subchamp + + TypesDeChamp::PrefillTypeDeChamp.build(subchamp.type_de_champ, revision).to_assignable_attributes(subchamp, value) + end.compact + end + end +end diff --git a/app/models/types_de_champ/prefill_type_de_champ.rb b/app/models/types_de_champ/prefill_type_de_champ.rb index 296c04c48..0dbadbb3c 100644 --- a/app/models/types_de_champ/prefill_type_de_champ.rb +++ b/app/models/types_de_champ/prefill_type_de_champ.rb @@ -1,24 +1,55 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator + include ActionView::Helpers::UrlHelper + include ApplicationHelper + POSSIBLE_VALUES_THRESHOLD = 5 - def self.build(type_de_champ) + def initialize(type_de_champ, revision) + super(type_de_champ) + @revision = revision + end + + def self.build(type_de_champ, revision) case type_de_champ.type_champ when TypeDeChamp.type_champs.fetch(:drop_down_list) - TypesDeChamp::PrefillDropDownListTypeDeChamp.new(type_de_champ) + TypesDeChamp::PrefillDropDownListTypeDeChamp.new(type_de_champ, revision) + when TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) + TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp.new(type_de_champ, revision) when TypeDeChamp.type_champs.fetch(:pays) - TypesDeChamp::PrefillPaysTypeDeChamp.new(type_de_champ) + TypesDeChamp::PrefillPaysTypeDeChamp.new(type_de_champ, revision) when TypeDeChamp.type_champs.fetch(:regions) - TypesDeChamp::PrefillRegionTypeDeChamp.new(type_de_champ) + TypesDeChamp::PrefillRegionTypeDeChamp.new(type_de_champ, revision) + when TypeDeChamp.type_champs.fetch(:repetition) + TypesDeChamp::PrefillRepetitionTypeDeChamp.new(type_de_champ, revision) + when TypeDeChamp.type_champs.fetch(:departements) + TypesDeChamp::PrefillDepartementTypeDeChamp.new(type_de_champ, revision) + when TypeDeChamp.type_champs.fetch(:communes) + TypesDeChamp::PrefillCommuneTypeDeChamp.new(type_de_champ, revision) + when TypeDeChamp.type_champs.fetch(:epci) + TypesDeChamp::PrefillEpciTypeDeChamp.new(type_de_champ, revision) + when TypeDeChamp.type_champs.fetch(:annuaire_education) + TypesDeChamp::PrefillAnnuaireEducationTypeDeChamp.new(type_de_champ, revision) else - new(type_de_champ) + new(type_de_champ, revision) end end - def self.wrap(collection) - collection.map { |type_de_champ| build(type_de_champ) } + def self.wrap(collection, revision) + collection.map { |type_de_champ| build(type_de_champ, revision) } end def possible_values + values = [] + values << description if description.present? + if too_many_possible_values? + values << link_to_all_possible_values + else + values << all_possible_values.to_sentence + end + values.compact.join('
').html_safe # rubocop:disable Rails/OutputSafety + end + + def all_possible_values [] end @@ -28,7 +59,28 @@ class TypesDeChamp::PrefillTypeDeChamp < SimpleDelegator I18n.t("views.prefill_descriptions.edit.examples.#{type_champ}") end + def to_assignable_attributes(champ, value) + { id: champ.id, value: value } + end + + private + + def link_to_all_possible_values + return unless prefillable? + + link_to( + I18n.t("views.prefill_descriptions.edit.possible_values.link.text"), + Rails.application.routes.url_helpers.prefill_type_de_champ_path(revision.procedure_path, self), + title: new_tab_suffix(I18n.t("views.prefill_descriptions.edit.possible_values.link.title")), + **external_link_attributes + ) + end + def too_many_possible_values? - possible_values.count > POSSIBLE_VALUES_THRESHOLD + all_possible_values.count > POSSIBLE_VALUES_THRESHOLD + end + + def description + @description ||= I18n.t("views.prefill_descriptions.edit.possible_values.#{type_champ}_html", default: nil)&.html_safe # rubocop:disable Rails/OutputSafety end end diff --git a/app/serializers/champ_serializer.rb b/app/serializers/champ_serializer.rb index 9802a4874..670979c81 100644 --- a/app/serializers/champ_serializer.rb +++ b/app/serializers/champ_serializer.rb @@ -13,7 +13,7 @@ class ChampSerializer < ActiveModel::Serializer def value case object when GeoArea - object.safe_geometry + object.geometry else object.for_api end diff --git a/app/serializers/geo_area_serializer.rb b/app/serializers/geo_area_serializer.rb index f0ff85ccb..39fb6cfd3 100644 --- a/app/serializers/geo_area_serializer.rb +++ b/app/serializers/geo_area_serializer.rb @@ -12,7 +12,7 @@ class GeoAreaSerializer < ActiveModel::Serializer attribute :code_arr, if: :include_cadastre? def geometry - object.safe_geometry + object.geometry end def include_cadastre? diff --git a/app/services/api_entreprise_service.rb b/app/services/api_entreprise_service.rb index 8eb5f4ed8..5f429d629 100644 --- a/app/services/api_entreprise_service.rb +++ b/app/services/api_entreprise_service.rb @@ -15,6 +15,9 @@ class APIEntrepriseService etablissement_params = APIEntreprise::EtablissementAdapter.new(siret, procedure_id).to_params return nil if etablissement_params.empty? + entreprise_params = APIEntreprise::EntrepriseAdapter.new(siret, procedure_id).to_params + etablissement_params.merge!(entreprise_params) if entreprise_params.any? + etablissement = dossier_or_champ.build_etablissement(etablissement_params) etablissement.save! diff --git a/app/services/api_geo_service.rb b/app/services/api_geo_service.rb index 429e40e81..77194815b 100644 --- a/app/services/api_geo_service.rb +++ b/app/services/api_geo_service.rb @@ -35,7 +35,7 @@ class APIGeoService end def departements - [{ code: '99', name: 'Etranger' }] + get_from_api_geo(:departements).sort_by { _1[:code] } + [{ code: '99', name: 'Etranger' }] + get_from_api_geo('departements?zone=metro,drom,com').sort_by { _1[:code] } end def departement_name(code) @@ -47,6 +47,25 @@ class APIGeoService departements.find { _1[:name] == name }&.dig(:code) end + def communes(departement_code) + get_from_api_geo( + "communes?codeDepartement=#{departement_code}", + additional_keys: { postal_codes: :codesPostaux } + ).sort_by { I18n.transliterate(_1[:name]) } + end + + def commune_name(departement_code, code) + communes(departement_code).find { _1[:code] == code }&.dig(:name) + end + + def commune_code(departement_code, name) + communes(departement_code).find { _1[:name] == name }&.dig(:code) + end + + def commune_postal_codes(departement_code, code) + communes(departement_code).find { _1[:code] == code }&.dig(:postal_codes) + end + def epcis(departement_code) get_from_api_geo("epcis?codeDepartement=#{departement_code}").sort_by { I18n.transliterate(_1[:name]) } end @@ -61,11 +80,16 @@ class APIGeoService private - def get_from_api_geo(scope) + def get_from_api_geo(scope, additional_keys: {}) Rails.cache.fetch("api_geo_#{scope}", expires_in: 1.year) do response = Typhoeus.get("#{API_GEO_URL}/#{scope}") - JSON.parse(response.body).map(&:symbolize_keys) - .map { { name: _1[:nom].tr("'", '’'), code: _1[:code] } } + JSON.parse(response.body) + .map(&:symbolize_keys) + .map do |result| + data = { name: result[:nom].tr("'", '’'), code: result[:code] } + additional_keys.each { |key, value| data = data.merge(key => result[value]) } + data + end end end diff --git a/app/services/geojson_service.rb b/app/services/geojson_service.rb index d2a78d884..154387193 100644 --- a/app/services/geojson_service.rb +++ b/app/services/geojson_service.rb @@ -45,6 +45,66 @@ class GeojsonService radians * EQUATORIAL_RADIUS end + def self.bbox(geojson) + result = [-Float::INFINITY, -Float::INFINITY, Float::INFINITY, Float::INFINITY] + + self.coord_each(geojson) do |coord| + if result[3] > coord[1] + result[3] = coord[1] + end + if result[2] > coord[0] + result[2] = coord[0] + end + if result[1] < coord[1] + result[1] = coord[1] + end + if result[0] < coord[0] + result[0] = coord[0] + end + end + + result + end + + def self.coord_each(geojson) + geometries = if geojson.fetch(:type) == "FeatureCollection" + geojson.fetch(:features).map { _1.fetch(:geometry) } + else + [geojson.fetch(:geometry)] + end.compact + + geometries.each do |geometry| + geometries = if geometry.fetch(:type) == "GeometryCollection" + geometry.fetch(:geometries) + else + [geometry] + end.compact + + geometries.each do |geometry| + case geometry.fetch(:type) + when "Point" + yield geometry.fetch(:coordinates).map(&:to_f) + when "LineString", "MultiPoint" + geometry.fetch(:coordinates).each { yield _1.map(&:to_f) } + when "Polygon", "MultiLineString" + geometry.fetch(:coordinates).each do |shapes| + shapes.each { yield _1.map(&:to_f) } + end + when "MultiPolygon" + geometry.fetch(:coordinates).each do |polygons| + polygons.each do |shapes| + shapes.each { yield _1.map(&:to_f) } + end + end + when "GeometryCollection" + geometry.fetch(:geometries).each do |geometry| + coord_each(geometry) { yield _1 } + end + end + end + end + end + def self.calculate_area(geom) total = 0 case geom[:type] diff --git a/app/services/instructeurs_import_service.rb b/app/services/instructeurs_import_service.rb index fad1e5922..7aa9f4ec3 100644 --- a/app/services/instructeurs_import_service.rb +++ b/app/services/instructeurs_import_service.rb @@ -1,54 +1,55 @@ class InstructeursImportService - def self.import(procedure, groupes_emails) - created_at = Time.zone.now - updated_at = Time.zone.now + def self.import_groupes(procedure, groupes_emails) + groupes_emails, error_groupe_emails = groupes_emails.partition { _1['groupe'].present? } - admins = procedure.administrateurs + groupes_emails = groupes_emails.map do + { + groupe: _1['groupe'].strip, + email: _1['email'].present? ? EmailSanitizableConcern::EmailSanitizer.sanitize(_1['email']) : nil + } + end + errors = error_groupe_emails.map { _1['email'] }.uniq + target_labels = groupes_emails.map { _1[:groupe] }.uniq - groupes_emails, error_groupe_emails = groupes_emails - .map { |groupe_email| { "groupe" => groupe_email["groupe"].present? ? groupe_email["groupe"].strip : nil, "email" => groupe_email["email"].present? ? groupe_email["email"].gsub(/[[:space:]]/, '').downcase : nil } } - .partition { |groupe_email| Devise.email_regexp.match?(groupe_email['email']) && groupe_email['groupe'].present? } - - errors = error_groupe_emails.map { |group_email| group_email['email'] } - - target_labels = groupes_emails.map { |groupe_email| groupe_email['groupe'] }.uniq missing_labels = target_labels - procedure.groupe_instructeurs.pluck(:label) if missing_labels.present? - GroupeInstructeur.insert_all(missing_labels.map { |label| { label: label, procedure_id: procedure.id, created_at: created_at, updated_at: updated_at } }) + created_at = Time.zone.now + GroupeInstructeur.insert_all(missing_labels.map { |label| { procedure_id: procedure.id, label:, created_at:, updated_at: created_at } }) end - target_groupes = procedure.reload.groupe_instructeurs + emails_in_groupe = groupes_emails + .group_by { _1[:groupe] } + .transform_values { |groupes_emails| groupes_emails.map { _1[:email] }.uniq } + emails_in_groupe.default = [] - target_emails = groupes_emails.map { |groupe_email| groupe_email["email"] }.uniq + target_groupes = procedure + .groupe_instructeurs + .where(label: target_labels) + .map { [_1, emails_in_groupe[_1.label]] } + .to_h - existing_emails = Instructeur.where(user: { email: target_emails }).pluck(:email) - missing_emails = target_emails - existing_emails - missing_emails.each { |email| create_instructeur(admins, email) } + added_instructeurs_by_group = [] - target_instructeurs = User.where(email: target_emails).map(&:instructeur) - - groupes_emails.each do |groupe_email| - gi = target_groupes.find { |g| g.label == groupe_email['groupe'] } - instructeur = target_instructeurs.find { |i| i.email == groupe_email['email'] } - - if !gi.instructeurs.include?(instructeur) - gi.instructeurs << instructeur - end + target_groupes.each do |groupe_instructeur, emails| + added_instructeurs, invalid_emails = groupe_instructeur.add_instructeurs(emails:) + added_instructeurs_by_group << [groupe_instructeur, added_instructeurs] + errors << invalid_emails end - errors + [added_instructeurs_by_group, errors.flatten] end - private + def self.import_instructeurs(procedure, emails) + instructeurs_emails = emails + .map { _1["email"] } + .compact + .map { EmailSanitizableConcern::EmailSanitizer.sanitize(_1) } - def self.create_instructeur(administrateurs, email) - user = User.create_or_promote_to_instructeur( - email, - SecureRandom.hex, - administrateurs: administrateurs - ) - user.invite! - user.instructeur + groupe_instructeur = procedure.defaut_groupe_instructeur + + instructeurs, invalid_emails = groupe_instructeur.add_instructeurs(emails: instructeurs_emails) + + [instructeurs, invalid_emails] end end diff --git a/app/validators/tags_validator.rb b/app/validators/tags_validator.rb index b8f3520aa..4b55c5cf9 100644 --- a/app/validators/tags_validator.rb +++ b/app/validators/tags_validator.rb @@ -7,18 +7,18 @@ class TagsValidator < ActiveModel::EachValidator tag if stable_id.nil? end - invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision_id) + invalid_for_draft_revision = invalid_tags_for_revision(record, attribute, tags, procedure.draft_revision) invalid_for_published_revision = if procedure.published_revision_id.present? - invalid_tags_for_revision(record, attribute, tags, procedure.published_revision_id) + invalid_tags_for_revision(record, attribute, tags, procedure.published_revision) else [] end invalid_for_previous_revision = procedure - .revision_ids_with_pending_dossiers - .flat_map do |revision_id| - invalid_tags_for_revision(record, attribute, tags, revision_id) + .revisions_with_pending_dossiers + .flat_map do |revision| + invalid_tags_for_revision(record, attribute, tags, revision) end.uniq # champ is added in draft revision but not yet published @@ -48,12 +48,12 @@ class TagsValidator < ActiveModel::EachValidator end end - def invalid_tags_for_revision(record, attribute, tags, revision_id) - revision_stable_ids = TypeDeChamp - .joins(:revision_types_de_champ) - .where(procedure_revision_types_de_champ: { revision_id: revision_id, parent_id: nil }) - .distinct(:stable_id) - .pluck(:stable_id) + def invalid_tags_for_revision(record, attribute, tags, revision) + revision_stable_ids = revision + .revision_types_de_champ + .filter { !_1.child? } + .map(&:stable_id) + .uniq tags.filter_map do |(tag, stable_id)| if stable_id.present? && !stable_id.in?(revision_stable_ids) diff --git a/app/views/administrateurs/activate/new.html.haml b/app/views/administrateurs/activate/new.html.haml index 5d17e6aa8..b826ddffe 100644 --- a/app/views/administrateurs/activate/new.html.haml +++ b/app/views/administrateurs/activate/new.html.haml @@ -1,21 +1,26 @@ -- content_for(:title, "Choix du mot de passe") +- content_for(:title, t('.title')) - content_for :footer do = render partial: "root/footer" -.container.devise-container - .one-column-centered - = form_for @administrateur, url: { controller: 'administrateurs/activate', action: :create }, html: { class: "form" } do |f| - %br - %h1 - Choix du mot de passe +.fr-container.fr-my-5w + .fr-grid-row.fr-grid-row--center + .fr-col-lg-6 + = form_for @administrateur, url: { controller: 'administrateurs/activate', action: :create } do |f| + = f.hidden_field :reset_password_token, value: @token - = f.hidden_field :reset_password_token, value: @token - = f.label :email, "Email" - = f.text_field :email, disabled: true + %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'edit-password-legend' } } + %legend.fr-fieldset__legend#edit-password-legend + %h1.fr-h2= t('.title') - = f.label :password do - Mot de passe - = render 'password_complexity/field', { form: f, test_complexity: true } + .fr-fieldset__element + = render Dsfr::InputComponent.new(form: f, attribute: :email, opts: { disabled: true }) - = f.submit 'Continuer', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi..." } + .fr-fieldset__element + = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, + opts: { autofocus: 'true', autocomplete: 'new-password', data: { controller: 'turbo-input', turbo_input_url_value: show_password_complexity_path }}) + + #password_complexity + = render PasswordComplexityComponent.new + + = f.submit t('.continue'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') } diff --git a/app/views/administrateurs/attestation_templates/edit.html.haml b/app/views/administrateurs/attestation_templates/edit.html.haml index 5fac6a715..4dbeede52 100644 --- a/app/views/administrateurs/attestation_templates/edit.html.haml +++ b/app/views/administrateurs/attestation_templates/edit.html.haml @@ -7,6 +7,7 @@ .procedure-form#attestation-template-edit .procedure-form__columns.container + = render Attachment::DeleteFormComponent.new = form_for @attestation_template, url: admin_procedure_attestation_template_path(@procedure), html: { multipart: true, class: 'form form-ds-fr-white procedure-form__column--form' } do |f| diff --git a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml index 7a5543d3b..a250482dd 100644 --- a/app/views/administrateurs/groupe_instructeurs/_edit.html.haml +++ b/app/views/administrateurs/groupe_instructeurs/_edit.html.haml @@ -28,12 +28,14 @@ = form_tag import_admin_procedure_groupe_instructeurs_path(procedure), method: :post, multipart: true, class: "mt-4 form" do = label_tag t('.csv_import.title') %p.notice - = t('.csv_import.notice_1') + = procedure.routing_enabled? ? t('.csv_import.routing_enabled.notice_1') : t('.csv_import.routing_disabled.notice_1') %p.notice = t('.csv_import.notice_2', csv_max_size: number_to_human_size(csv_max_size)) - %p.mt-2.mb-2= link_to t('.csv_import.download_exemple'), "/csv/#{I18n.locale}/import-groupe-test.csv" - = file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1" - = submit_tag t('.csv_import.import_file'), class: 'button primary send', data: { disable_with: "Envoi..." } + - sample_file_path = procedure.routing_enabled? ? "/csv/#{I18n.locale}/import-groupe-test.csv" : "/csv/import-instructeurs-test.csv" + %p.mt-2.mb-2= link_to t('.csv_import.download_exemple'), sample_file_path + - csv_params = procedure.routing_enabled? ? :group_csv_file : :instructeurs_csv_file + = file_field_tag csv_params, required: true, accept: 'text/csv', size: "1" + = submit_tag t('.csv_import.import_file'), class: 'button primary send', data: { disable_with: "Envoi...", confirm: t('.csv_import.import_file_alert') } - else %p.mt-4.form.font-weight-bold.mb-2.text-lg = t('.csv_import.title') diff --git a/app/views/administrateurs/procedures/_detail.html.haml b/app/views/administrateurs/procedures/_detail.html.haml index f25a90fd1..2b4e15b4b 100644 --- a/app/views/administrateurs/procedures/_detail.html.haml +++ b/app/views/administrateurs/procedures/_detail.html.haml @@ -8,15 +8,16 @@ %td= procedure.libelle %td= procedure.id + %td= procedure.estimated_dossiers_count %td= procedure.administrateurs.count %td= t procedure.aasm_state, scope: 'activerecord.attributes.procedure.aasm_state' - %td= l(procedure.published_at, format: :message_date_without_time) + %td= l(procedure.published_at, format: :message_date_without_time) if procedure.published_at %td= link_to('Cloner', admin_procedure_clone_path(procedure.id, from_new_from_existing: true), 'data-method' => :put, class: 'fr-btn fr-btn--tertiary fr-btn--sm') - if show_detail %tr.procedure{ id: "procedure_detail_#{procedure.id}" } - %td.fr-highlight--beige-gris-galet{ colspan: '6' } + %td.fr-highlight--beige-gris-galet{ colspan: '8' } .fr-container .fr-grid-row .fr-col-6 diff --git a/app/views/administrateurs/procedures/_main_menu.html.haml b/app/views/administrateurs/procedures/_main_menu.html.haml index 9d3d28ef9..89f3de634 100644 --- a/app/views/administrateurs/procedures/_main_menu.html.haml +++ b/app/views/administrateurs/procedures/_main_menu.html.haml @@ -1,6 +1,6 @@ .fr-container %nav#header-navigation.fr-nav{ role: 'navigation', 'aria-label': 'Menu principal administrateur' } %ul.fr-nav__list - %li.fr-nav__item= link_to 'Mes démarches', admin_procedures_path, class:'fr-nav__link', 'aria-current': current_page?(admin_procedures_path) ? 'page' : nil + %li.fr-nav__item= link_to 'Mes démarches', admin_procedures_path, class:'fr-nav__link', 'aria-current': current_page?(controller: 'procedures', action: :index) ? 'true' : nil - if Rails.application.config.ds_zonage_enabled %li.fr-nav__item= link_to 'Toutes les démarches', all_admin_procedures_path(zone_ids: current_administrateur.zones), class:'fr-nav__link', 'aria-current': current_page?(all_admin_procedures_path) ? 'page' : nil diff --git a/app/views/administrateurs/procedures/administrateurs.html.haml b/app/views/administrateurs/procedures/administrateurs.html.haml index c3051c65a..de7daa525 100644 --- a/app/views/administrateurs/procedures/administrateurs.html.haml +++ b/app/views/administrateurs/procedures/administrateurs.html.haml @@ -2,13 +2,13 @@ .main-filter-header.fr-my-3w = form_with(url: administrateurs_admin_procedures_path, method: :get, data: { turbo_frame: 'procedures' }, html: { role: 'search' }) do |f| - @filter.zone_ids&.each do |zone_id| - = hidden_field_tag 'zone_ids[]', zone_id + = hidden_field_tag 'zone_ids[]', zone_id, id: "zone_#{zone_id}" - @filter.statuses&.each do |status| - = hidden_field_tag 'statuses[]', status + = hidden_field_tag 'statuses[]', status, id: "status_#{status}" = hidden_field_tag 'from_publication_date', @filter.from_publication_date if @filter.from_publication_date.present? = f.label 'email', 'Recercher des administrateurs par email', class: 'fr-label' - = f.search_field 'email', size: 40, class: 'fr-input' + = f.search_field 'email', size: 40, class: 'fr-input', data: { turbo_force: true } .actions= link_to 'Voir la liste des démarches', all_admin_procedures_path(@filter.params), class: 'fr-btn fr-btn--secondary' .fr-table.fr-table--bordered %table#all-admins diff --git a/app/views/administrateurs/procedures/all.html.haml b/app/views/administrateurs/procedures/all.html.haml index 1ac68b3be..5552cf704 100644 --- a/app/views/administrateurs/procedures/all.html.haml +++ b/app/views/administrateurs/procedures/all.html.haml @@ -2,16 +2,16 @@ .main-filter-header.fr-my-3w = form_with(url: all_admin_procedures_path, method: :get, data: { turbo_frame: 'procedures' }, html: { role: 'search', class: 'search' }) do |f| - @filter.zone_ids&.each do |zone_id| - = hidden_field_tag 'zone_ids[]', zone_id + = hidden_field_tag 'zone_ids[]', zone_id, id: "zone_#{zone_id}" - @filter.statuses&.each do |status| - = hidden_field_tag 'statuses[]', status + = hidden_field_tag 'statuses[]', status, id: "status_#{status}" = hidden_field_tag 'from_publication_date', @filter.from_publication_date if @filter.from_publication_date.present? = f.label :libelle, 'Rechercher des démarches par libellé', class: 'fr-label' - = f.search_field 'libelle', size: 30, class: 'fr-input' + = f.search_field 'libelle', size: 30, class: 'fr-input', data: { turbo_force: true } .actions .link.fr-mx-1w= link_to 'Voir les administrateurs', administrateurs_admin_procedures_path(@filter.params), class: 'fr-btn fr-btn--secondary' - .link.fr-mx-1w= link_to 'Exporter les résultats', all_admin_procedures_path(@filter.params.merge(format: :xlsx)), class: 'fr-btn fr-btn--secondary' + .link.fr-mx-1w{ "data-turbo": "false" }= link_to 'Exporter les résultats', all_admin_procedures_path(@filter.params.merge(format: :xlsx)), class: 'fr-btn fr-btn--secondary' .fr-table.fr-table--bordered %table#all-demarches %caption @@ -41,6 +41,7 @@ %th{ scope: 'col' } %th{ scope: 'col' } Démarche %th{ scope: 'col' } № + %th{ scope: 'col' } Dossiers %th{ scope: 'col' } Administrateurs %th{ scope: 'col' } Statut %th{ scope: 'col' } Date diff --git a/app/views/administrateurs/procedures/edit.html.haml b/app/views/administrateurs/procedures/edit.html.haml index a46cdd144..5bbb0508c 100644 --- a/app/views/administrateurs/procedures/edit.html.haml +++ b/app/views/administrateurs/procedures/edit.html.haml @@ -6,6 +6,7 @@ ['Description']] } .procedure-form .procedure-form__columns.container + = render Attachment::DeleteFormComponent.new = form_for @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :update, id: @procedure.id }), html: { class: 'form procedure-form__column--form', diff --git a/app/views/administrateurs/procedures/new.html.haml b/app/views/administrateurs/procedures/new.html.haml index 6fa4ee0c7..43d196bf6 100644 --- a/app/views/administrateurs/procedures/new.html.haml +++ b/app/views/administrateurs/procedures/new.html.haml @@ -6,12 +6,14 @@ .procedure-form .procedure-form__columns.container + = render Attachment::DeleteFormComponent.new = form_for @procedure, url: url_for({ controller: 'administrateurs/procedures', action: :create, id: @procedure.id }), html: { class: 'form procedure-form__column--form', multipart: true } do |f| %h1.page-title Nouvelle démarche + = render partial: 'administrateurs/procedures/informations', locals: { f: f } .procedure-form__actions.sticky--bottom diff --git a/app/views/champs/rna/show.turbo_stream.haml b/app/views/champs/rna/show.turbo_stream.haml index e87d3b7e1..88b4c73aa 100644 --- a/app/views/champs/rna/show.turbo_stream.haml +++ b/app/views/champs/rna/show.turbo_stream.haml @@ -1 +1 @@ -= turbo_stream.update dom_id(@champ, :rna_info), partial: 'shared/champs/rna/association', locals: { champ: @champ, network_error: @network_error, rna: @rna } += turbo_stream.update dom_id(@champ, :rna_info), partial: 'shared/champs/rna/association', locals: { champ: @champ, error: @error } diff --git a/app/views/champs/siret/show.turbo_stream.haml b/app/views/champs/siret/show.turbo_stream.haml index be1c3a1c8..2e56df6aa 100644 --- a/app/views/champs/siret/show.turbo_stream.haml +++ b/app/views/champs/siret/show.turbo_stream.haml @@ -1 +1 @@ -= turbo_stream.update dom_id(@champ, :siret_info), partial: 'shared/champs/siret/etablissement', locals: { siret: @siret, etablissement: @etablissement } += turbo_stream.update dom_id(@champ, :siret_info), partial: 'shared/champs/siret/etablissement', locals: { siret: @siret, etablissement: @champ.etablissement } diff --git a/app/views/devise/_password_rules.html.haml b/app/views/devise/_password_rules.html.haml new file mode 100644 index 000000000..2d4083d2f --- /dev/null +++ b/app/views/devise/_password_rules.html.haml @@ -0,0 +1,3 @@ +.fr-messages-group{ "aria-live" => "off", id: id } + %p.fr-message= t('views.registrations.new.password_message') + %p.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH) diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 46d33dd55..62103f214 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -3,20 +3,31 @@ - content_for :footer do = render partial: 'root/footer' -.container.devise-container - .one-column-centered - = devise_error_messages! +.fr-container.fr-my-5w + .fr-grid-row.fr-grid-row--center + .fr-col-lg-6 + = devise_error_messages! - = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: 'form' }) do |f| + = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :patch, class: '' }) do |f| + = f.hidden_field :reset_password_token - %h1 Changement de mot de passe - = f.hidden_field :reset_password_token + %fieldset.fr-mb-0.fr-fieldset{ aria: { labelledby: 'edit-password-legend' } } + %legend.fr-fieldset__legend#edit-password-legend + %h1.fr-h2= I18n.t('views.users.passwords.edit.subtitle') - = f.label 'Nouveau mot de passe' - = render 'password_complexity/field', { form: f, test_complexity: populated_resource.validate_password_complexity? } + .fr-fieldset__element + = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, + opts: { autofocus: 'true', autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH, data: { controller: populated_resource.validate_password_complexity? ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }}) do |c| + - c.describedby do + - if populated_resource.validate_password_complexity? + %div{ id: c.describedby_id } + #password_complexity + = render PasswordComplexityComponent.new + - else + = render partial: "devise/password_rules", locals: { id: c.describedby_id } - = f.label 'Confirmez le nouveau mot de passe' - = f.password_field :password_confirmation, autocomplete: 'off' + .fr-fieldset__element + = render Dsfr::InputComponent.new(form: f, attribute: :password_confirmation, input_type: :password_field, opts: { autocomplete: 'new-password' }) - = f.submit 'Changer le mot de passe', class: 'button large primary expand', id: "submit-password", data: { disable_with: "Envoi…" } + = f.submit t('views.users.passwords.edit.submit'), id: 'submit-password', class: "fr-btn fr-btn--lg fr-mt-2w", data: { disable_with: t('views.users.passwords.edit.submit_loading') } diff --git a/app/views/dossiers/show.pdf.prawn b/app/views/dossiers/show.pdf.prawn index 741aa31b7..c9a517305 100644 --- a/app/views/dossiers/show.pdf.prawn +++ b/app/views/dossiers/show.pdf.prawn @@ -141,7 +141,13 @@ def add_single_champ(pdf, champ) when 'Champs::PieceJustificativeChamp', 'Champs::TitreIdentiteChamp' return when 'Champs::HeaderSectionChamp' - add_section_title(pdf, tdc.libelle) + libelle = if @dossier.auto_numbering_section_headers_for?(champ) + "#{@dossier.index_for_section_header(champ)}. #{champ.libelle}" + else + champ.libelle + end + + add_section_title(pdf, libelle) when 'Champs::ExplicationChamp' format_in_2_lines(pdf, tdc.libelle, strip_tags(tdc.description)) when 'Champs::CarteChamp' diff --git a/app/views/experts/avis/instruction.html.haml b/app/views/experts/avis/instruction.html.haml index 0075ee54e..0772abd9d 100644 --- a/app/views/experts/avis/instruction.html.haml +++ b/app/views/experts/avis/instruction.html.haml @@ -14,7 +14,7 @@ - if @avis.introduction_file.attached? = render Attachment::ShowComponent.new(attachment: @avis.introduction_file.attachment) %br/ - + = render Attachment::DeleteFormComponent.new = form_for @avis, url: expert_avis_path(@avis.procedure, @avis), html: { class: 'form', data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis) }, multipart: true } do |f| = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true = render Attachment::EditComponent.new(attached_file: @avis.piece_justificative_file, view_as: :download) diff --git a/app/views/experts/shared/avis/_form.html.haml b/app/views/experts/shared/avis/_form.html.haml index 1d6b70ab5..f0bec0090 100644 --- a/app/views/experts/shared/avis/_form.html.haml +++ b/app/views/experts/shared/avis/_form.html.haml @@ -2,7 +2,7 @@ %section.ask-avis %h1.tab-title Inviter des personnes à donner leur avis %p.avis-notice Les invités pourront consulter le dossier, donner un avis et contribuer au fil de messagerie. Ils ne pourront pas modifier le dossier. - + = render Attachment::DeleteFormComponent.new = form_for avis, url: url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@avis.dossier, :avis_by_expert) } } do |f| = hidden_field_tag 'avis[emails]', nil = react_component("ComboMultiple", diff --git a/app/views/experts/shared/avis/_list.html.haml b/app/views/experts/shared/avis/_list.html.haml index 4834a7fe7..a2436d926 100644 --- a/app/views/experts/shared/avis/_list.html.haml +++ b/app/views/experts/shared/avis/_list.html.haml @@ -35,4 +35,4 @@ - if avis.piece_justificative_file.attached? = render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment) .answer-body - = simple_format(avis.answer) + = render SimpleFormatComponent.new(avis.answer, allow_a: false) diff --git a/app/views/groupe_instructeur_mailer/add_instructeurs.html.haml b/app/views/groupe_instructeur_mailer/add_instructeurs.html.haml deleted file mode 100644 index ee899f212..000000000 --- a/app/views/groupe_instructeur_mailer/add_instructeurs.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%p - Bonjour, - -%p - = t('administrateurs.groupe_instructeurs.add_instructeur.email_body', count: @new_instructeur_emails.size, emails: @new_instructeur_emails.join(', '), groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle) - -%p - Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe : - = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group)) - -= render partial: "layouts/mailers/signature" diff --git a/app/views/groupe_instructeur_mailer/notify_added_instructeurs.html.haml b/app/views/groupe_instructeur_mailer/notify_added_instructeurs.html.haml new file mode 100644 index 000000000..7cf65d373 --- /dev/null +++ b/app/views/groupe_instructeur_mailer/notify_added_instructeurs.html.haml @@ -0,0 +1,7 @@ +%p= t(:hello, scope: [:views, :shared, :greetings]) + +%p + - number_of_groups = @group.procedure.groupe_instructeurs.many? ? 'many_groups' : 'one_group' + = t(".email_body.#{number_of_groups}", groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle) + += render partial: "layouts/mailers/signature" diff --git a/app/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed.html.haml b/app/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed.html.haml new file mode 100644 index 000000000..1c3ccf1e1 --- /dev/null +++ b/app/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed.html.haml @@ -0,0 +1,10 @@ +%p= t(:hello, scope: [:views, :shared, :greetings]) + +%p + = t('.email_body', count: @removed_instructeur_emails.size, emails: @removed_instructeur_emails.join(', '), groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle) + +%p + Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe : + = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group)) + += render partial: "layouts/mailers/signature" diff --git a/app/views/groupe_instructeur_mailer/notify_removed_instructeur.html.haml b/app/views/groupe_instructeur_mailer/notify_removed_instructeur.html.haml new file mode 100644 index 000000000..9952a1b55 --- /dev/null +++ b/app/views/groupe_instructeur_mailer/notify_removed_instructeur.html.haml @@ -0,0 +1,7 @@ +%p= t(:hello, scope: [:views, :shared, :greetings]) + +%p + - assignment_state = @still_assigned_to_procedure ? 'assigned' : 'unassigned' + = t(".email_body.#{assignment_state}", groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle) + += render partial: "layouts/mailers/signature" diff --git a/app/views/groupe_instructeur_mailer/remove_instructeur.html.haml b/app/views/groupe_instructeur_mailer/remove_instructeur.html.haml deleted file mode 100644 index 9e7770919..000000000 --- a/app/views/groupe_instructeur_mailer/remove_instructeur.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%p - Bonjour, - -%p - = t('administrateurs.groupe_instructeurs.remove_instructeur.email_body', count: 1, emails: @email, groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle) - -%p - Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe : - = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group)) - -= render partial: "layouts/mailers/signature" diff --git a/app/views/groupe_instructeur_mailer/remove_instructeurs.html.haml b/app/views/groupe_instructeur_mailer/remove_instructeurs.html.haml deleted file mode 100644 index d5a73e165..000000000 --- a/app/views/groupe_instructeur_mailer/remove_instructeurs.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -%p - Bonjour, - -%p - = t('administrateurs.groupe_instructeurs.remove_instructeur.email_body', count: @removed_instructeur_emails.size, emails: @removed_instructeur_emails.join(', '), groupe: @group.label, email: @current_instructeur_email, procedure: @group.procedure.libelle) - -%p - Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe : - = link_to(@group.label, admin_procedure_groupe_instructeur_url(@group.procedure, @group)) - -= render partial: "layouts/mailers/signature" diff --git a/app/views/instructeurs/avis/instruction.html.haml b/app/views/instructeurs/avis/instruction.html.haml index fb9c18335..93deba394 100644 --- a/app/views/instructeurs/avis/instruction.html.haml +++ b/app/views/instructeurs/avis/instruction.html.haml @@ -15,6 +15,7 @@ = render Attachment::ShowComponent.new(attachment: @avis.introduction_file.attachment) %br/ + = render Attachment::DeleteFormComponent.new = form_for @avis, url: instructeur_avis_path(@avis.procedure, @avis), html: { class: 'form' } do |f| = f.text_area :answer, rows: 3, placeholder: 'Votre avis', required: true = render Attachment::EditComponent.new(attached_file: @avis.piece_justificative_file) diff --git a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml index 1ab77bf97..b60bf1c32 100644 --- a/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml +++ b/app/views/instructeurs/dossiers/_envoyer_dossier_block.html.haml @@ -6,14 +6,13 @@ - else %p.tab-paragrah.mb-1 Le destinataire suivra automatiquement le dossier - = form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form' } do |f| - .flex.justify-start.align-start - = hidden_field_tag :recipients, nil - = react_component("ComboMultiple", - options: potential_recipients.map{|r| [r.email, r.id]}, - selected: [], disabled: [], - group: '.recipients-form', - name: 'recipients', - label: 'Emails') + = form_for dossier, url: send_to_instructeurs_instructeur_dossier_path(dossier.procedure, dossier), method: :post, html: { class: 'form recipients-form fr-mb-4w' } do |f| + = hidden_field_tag :recipients, nil + = react_component("ComboMultiple", + options: potential_recipients.map{|r| [r.email, r.id]}, + selected: [], disabled: [], + group: '.recipients-form', + name: 'recipients', + label: 'Emails') - = f.submit "Envoyer", class: "button large send gap-left" + = f.submit "Envoyer", class: "fr-btn fr-mt-2w" diff --git a/app/views/instructeurs/dossiers/show.html.haml b/app/views/instructeurs/dossiers/show.html.haml index 8dd1a8d91..cbb8f7833 100644 --- a/app/views/instructeurs/dossiers/show.html.haml +++ b/app/views/instructeurs/dossiers/show.html.haml @@ -6,12 +6,12 @@ - if @dossier.etablissement&.as_degraded_mode? .container = render Dsfr::CalloutComponent.new(title: "Données de l’entreprise non vérifiées", theme: :warning, icon: "fr-icon-feedback-fill") do |c| - - c.with_body do - Les services de l’INSEE sont indisponibles, nous ne pouvons pas - vérifier les informations liées à l’établissement de ce dossier. - %strong Il n’est pas possible d’accepter ou de refuser un dossier sans cette étape. - %br - %br - Les informations sur l'entreprise arriveront d’ici quelques heures. + - c.with_html_body do + %p + Les services de l’INSEE sont indisponibles, nous ne pouvons pas vérifier les informations liées à l’établissement de ce dossier. + %strong + Il n’est pas possible d’accepter ou de refuser un dossier sans cette étape. + %p + Les informations sur l'entreprise arriveront d’ici quelques heures. = render partial: "shared/dossiers/demande", locals: { dossier: @dossier, demande_seen_at: @demande_seen_at, profile: 'instructeur' } diff --git a/app/views/instructeurs/shared/avis/_form.html.haml b/app/views/instructeurs/shared/avis/_form.html.haml index 90248d0f6..99f99c666 100644 --- a/app/views/instructeurs/shared/avis/_form.html.haml +++ b/app/views/instructeurs/shared/avis/_form.html.haml @@ -8,6 +8,7 @@ %p#avis-emails-description.avis-notice Entrez les adresses email des experts à qui vous souhaitez demander un avis + = render Attachment::DeleteFormComponent.new = form_for avis, url: url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: dom_id(@dossier, :avis_by_instructeur) } } do |f| = hidden_field_tag 'avis[emails]', nil = react_component("ComboMultiple", diff --git a/app/views/instructeurs/shared/avis/_list.html.haml b/app/views/instructeurs/shared/avis/_list.html.haml index 2ed87cfbe..9d66b8d2d 100644 --- a/app/views/instructeurs/shared/avis/_list.html.haml +++ b/app/views/instructeurs/shared/avis/_list.html.haml @@ -51,4 +51,4 @@ - if avis.piece_justificative_file.attached? = render Attachment::ShowComponent.new(attachment: avis.piece_justificative_file.attachment) .answer-body - = simple_format(avis.answer) + = render SimpleFormatComponent.new(avis.answer, allow_a: false) diff --git a/app/views/layouts/_flash_messages.html.haml b/app/views/layouts/_flash_messages.html.haml index 7eb9812f7..74e29b68c 100644 --- a/app/views/layouts/_flash_messages.html.haml +++ b/app/views/layouts/_flash_messages.html.haml @@ -7,8 +7,8 @@ - if value.class == Array .alert{ class: flash_class(key, sticky: sticky, fixed: fixed), role: flash_role(key) } - value.each do |message| - = sanitize(message) + = sanitize_with_link(message) %br - else .alert{ class: flash_class(key, sticky: sticky, fixed: fixed), role: flash_role(key) } - = sanitize(value) + = sanitize_with_link(value) diff --git a/app/views/layouts/_header.haml b/app/views/layouts/_header.haml index e7916f90e..c447fad9e 100644 --- a/app/views/layouts/_header.haml +++ b/app/views/layouts/_header.haml @@ -72,7 +72,7 @@ = render partial: 'layouts/search_dossiers_form', locals: { search_endpoint: recherche_dossiers_path } - has_header = [is_instructeur_context, is_expert_context, is_user_context] - #burger-menu.fr-header__menu.fr-modal{ "aria-label" => t('layouts.header.label_modal') } + #burger-menu.fr-header__menu.fr-modal .fr-container %button#burger_button.fr-btn--close.fr-btn{ "aria-controls" => "burger-menu", :title => t('close_modal', scope: [:layouts, :header]) }= t('close_modal', scope: [:layouts, :header]) .fr-header__menu-links @@ -87,18 +87,18 @@ - if current_instructeur.procedures.any? - current_url = request.path_info %li.fr-nav__item - = active_link_to t('utils.procedure'), instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'fr-nav__link' + = active_link_to t('utils.procedure'), instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'fr-nav__link', aria: { current: current_url == instructeur_procedures_path ? 'page' : true } - if current_instructeur.user.expert && current_expert.avis_summary[:total] > 0 = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } - if is_expert_context - if current_expert.user.instructeur && current_instructeur.procedures.any? - %li.fr-nav__item= active_link_to t('utils.procedure'), instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'fr-nav__link' + %li.fr-nav__item= active_link_to t('utils.procedure'), instructeur_procedures_path, active: ['dossiers','procedures'].include?(controller_name), class: 'fr-nav__link', aria: { current: true } - if current_expert.avis_summary[:total] > 0 = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } - if is_user_context - %li.fr-nav__item= active_link_to t('.files'), dossiers_path, active: :inclusive, class: 'fr-nav__link' + %li.fr-nav__item= active_link_to t('.files'), dossiers_path, active: :inclusive, class: 'fr-nav__link', aria: { current: true } - if current_user.expert && current_expert.avis_summary[:total] > 0 = render partial: 'layouts/header/avis_tab', locals: { current_expert: current_expert } diff --git a/app/views/layouts/_skiplinks.html.haml b/app/views/layouts/_skiplinks.html.haml index b7ad3fb9c..ccd0f3f18 100644 --- a/app/views/layouts/_skiplinks.html.haml +++ b/app/views/layouts/_skiplinks.html.haml @@ -1,5 +1,5 @@ .fr-skiplinks - %nav.fr-container{ role: "navigation", 'aria-label': "Accès rapide" } + %nav.fr-container{ role: "navigation", 'aria-label': t("skiplinks.quick") } %ul.fr-skiplinks__list %li - %a.fr-link{ href: "#contenu" } Contenu + %a.fr-link{ href: "#contenu" }= t('skiplinks.content') diff --git a/app/views/layouts/all.html.haml b/app/views/layouts/all.html.haml index 7a752fa19..1bc899b74 100644 --- a/app/views/layouts/all.html.haml +++ b/app/views/layouts/all.html.haml @@ -10,7 +10,7 @@ %p Ce tableau de bord permet de consulter les informations sur les démarches simplifiées pour toutes les zones. Filtrez par zone et statut. Consultez la liste des démarches et cliquez sur une démarche pour voir la zone et quels sont les administrateurs. .fr-container--fluid{ data: { turbo: 'true' } } - .fr-grid-row.fr-grid-row--gutters + %turbo-frame#procedures.fr-grid-row.fr-grid-row--gutters{ 'data-turbo-action': 'advance' } .fr-col-3 = form_with(url: all_admin_procedures_path, method: :get, data: { controller: 'autosubmit', turbo_frame: 'procedures' }) do |f| @@ -20,7 +20,7 @@ %span.fr-icon-filter-fill.fr-icon--sm.fr-mr-1w{ 'aria-hidden': 'true' } Filtrer .reinit - = link_to all_admin_procedures_path(zone_ids: current_administrateur.zones) do + = link_to all_admin_procedures_path(zone_ids: current_administrateur.zones), { data: { turbo: 'false' } } do %span.fr-icon-arrow-go-back-line Réinitialiser %ul %li.fr-py-2w.fr-pl-2w{ 'data-controller': "expand" } @@ -71,11 +71,11 @@ Tags .fr-ml-1w.hidden{ 'data-expand-target': 'content' } %div - = f.search_field :tag, placeholder: 'Choisissez un tag', list: 'tags_list', class: 'fr-input', data: { no_autosubmit: 'input' } + = f.search_field :tag, placeholder: 'Choisissez un tag', list: 'tags_list', class: 'fr-input', data: { no_autosubmit: 'input', turbo_force: true } %datalist#tags_list - Procedure.tags.each do |tag| %option{ value: tag } - %turbo-frame#procedures.fr-col-9{ 'data-turbo-action': 'advance' } + .fr-col-9 = yield(:results) = render template: 'layouts/application' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 6a49c6418..74b523894 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -59,3 +59,4 @@ - if Rails.env.development? = vite_typescript_tag 'axe-core' = yield :charts_js + = render Attachment::ProgressBarComponent.new diff --git a/app/views/password_complexity/_bar.html.haml b/app/views/password_complexity/_bar.html.haml deleted file mode 100644 index a9b8c8262..000000000 --- a/app/views/password_complexity/_bar.html.haml +++ /dev/null @@ -1 +0,0 @@ -#complexity-bar.password-complexity{ class: "complexity-#{@length < @min_length ? @score/2 : @score}" } diff --git a/app/views/password_complexity/_field.html.haml b/app/views/password_complexity/_field.html.haml deleted file mode 100644 index 2e031f574..000000000 --- a/app/views/password_complexity/_field.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { controller: test_complexity ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path } - -- if test_complexity - #complexity-bar.password-complexity - - .explication - #complexity-label{ style: 'font-weight: bold' } - Inscrivez un mot de passe. - Une courte phrase avec ponctuation peut être un mot de passe très sécurisé. diff --git a/app/views/password_complexity/_label.html.haml b/app/views/password_complexity/_label.html.haml deleted file mode 100644 index 2e9bda1d0..000000000 --- a/app/views/password_complexity/_label.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -#complexity-label{ style: 'font-weight: bold' } - - if @length > 0 - - if @length < @min_length - Le mot de passe doit faire au moins #{@min_length} caractères. - - else - - case @score - - when 0..1 - Mot de passe très vulnérable. - - when 2...@min_complexity - Mot de passe vulnérable. - - when @min_complexity...4 - Mot de passe acceptable. Vous pouvez valider...
ou améliorer votre mot de passe. - - else - Félicitations ! Mot de passe suffisamment fort et sécurisé. - - else - Inscrivez un mot de passe. diff --git a/app/views/password_complexity/show.turbo_stream.haml b/app/views/password_complexity/show.turbo_stream.haml index 3fd3648f6..461ad5b12 100644 --- a/app/views/password_complexity/show.turbo_stream.haml +++ b/app/views/password_complexity/show.turbo_stream.haml @@ -1,5 +1,6 @@ -= turbo_stream.replace 'complexity-label', partial: 'label' -= turbo_stream.replace 'complexity-bar', partial: 'bar' += turbo_stream.update 'password_complexity' do + = render PasswordComplexityComponent.new(length: @length, min_length: @min_length, score: @score, min_complexity: @min_complexity) + - if @score < @min_complexity || @length < @min_length = turbo_stream.disable 'submit-password' - else diff --git a/app/views/prefill_descriptions/_json_description.html.haml b/app/views/prefill_descriptions/_json_description.html.haml index ea759d362..f153e3aaa 100644 --- a/app/views/prefill_descriptions/_json_description.html.haml +++ b/app/views/prefill_descriptions/_json_description.html.haml @@ -1,6 +1,6 @@ = render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.json_description_title"), theme: :success, icon: "fr-icon-layout-grid-fill") do |c| - - c.with_body do - = t("views.prefill_descriptions.edit.json_description_info") + - c.with_html_body do + %p= t("views.prefill_descriptions.edit.json_description_info") %pre %code.code-block = prefill_json_description_url(procedure.path) diff --git a/app/views/prefill_descriptions/_prefill_link.html.haml b/app/views/prefill_descriptions/_prefill_link.html.haml index 07a0f2811..0e7d097fd 100644 --- a/app/views/prefill_descriptions/_prefill_link.html.haml +++ b/app/views/prefill_descriptions/_prefill_link.html.haml @@ -5,8 +5,8 @@ - if prefill_description.prefilled_champs.any? = render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_link_title"), theme: theme, icon: icon) do |c| - - c.with_body do - = body + - c.with_html_body do + %p= body %pre %code.code-block = prefill_description.prefill_link diff --git a/app/views/prefill_descriptions/_prefill_query.html.haml b/app/views/prefill_descriptions/_prefill_query.html.haml index 180ff7e15..a767cb87d 100644 --- a/app/views/prefill_descriptions/_prefill_query.html.haml +++ b/app/views/prefill_descriptions/_prefill_query.html.haml @@ -3,8 +3,8 @@ - if prefill_description.prefilled_champs.any? = render Dsfr::CalloutComponent.new(title: t("views.prefill_descriptions.edit.prefill_query_title"), theme: :success, icon: "fr-icon-code-box-fill") do |c| - - c.with_body do - = t("views.prefill_descriptions.edit.prefill_query_info") + - c.with_html_body do + %p= t("views.prefill_descriptions.edit.prefill_query_info") %pre %code.code-block = prefill_description.prefill_query diff --git a/app/views/prefill_descriptions/_types_de_champs.html.haml b/app/views/prefill_descriptions/_types_de_champs.html.haml index 308da8fa5..49eab0fd0 100644 --- a/app/views/prefill_descriptions/_types_de_champs.html.haml +++ b/app/views/prefill_descriptions/_types_de_champs.html.haml @@ -27,7 +27,7 @@ %th = t("views.prefill_descriptions.edit.champ_id") %td - = type_de_champ.to_typed_id + = type_de_champ.to_typed_id_for_query %tr %th = t("views.prefill_descriptions.edit.champ_type") @@ -37,13 +37,7 @@ %th = t("views.prefill_descriptions.edit.possible_values.title") %td - - if I18n.exists?("views.prefill_descriptions.edit.possible_values.#{type_de_champ.type_champ}_html") - = t("views.prefill_descriptions.edit.possible_values.#{type_de_champ.type_champ}_html") - %br - - if type_de_champ.too_many_possible_values? - = link_to t("views.prefill_descriptions.edit.possible_values.link.text"), prefill_type_de_champ_path(prefill_description.path, type_de_champ), title: new_tab_suffix(t("views.prefill_descriptions.edit.possible_values.link.title")), **external_link_attributes - - else - = type_de_champ.possible_values.to_sentence + = type_de_champ.possible_values %tr{ class: prefillable ? "" : "fr-text-mention--grey" } %th = t("views.prefill_descriptions.edit.examples.title") diff --git a/app/views/prefill_type_de_champs/show.html.haml b/app/views/prefill_type_de_champs/show.html.haml index bad0c9deb..08eb4c6ae 100644 --- a/app/views/prefill_type_de_champs/show.html.haml +++ b/app/views/prefill_type_de_champs/show.html.haml @@ -15,7 +15,7 @@ %th = t("views.prefill_descriptions.edit.champ_id") %td - = @type_de_champ.to_typed_id + = @type_de_champ.to_typed_id_for_query %tr %th = t("views.prefill_descriptions.edit.champ_type") @@ -26,8 +26,8 @@ = t("views.prefill_descriptions.edit.possible_values.title") %td .fr-grid-row.fr-grid-row--gutters.fr-py-5w - - @type_de_champ.possible_values.each do |possible_value| - .fr-col-lg-3.fr-col-md-4.fr-col-sm-6.fr-col-12 + - @type_de_champ.all_possible_values.each do |possible_value| + .fr-col-md-4.fr-col-sm-6.fr-col-12 = possible_value %tr %th diff --git a/app/views/shared/_procedure_description.html.haml b/app/views/shared/_procedure_description.html.haml index e10d4607f..8e2af15c9 100644 --- a/app/views/shared/_procedure_description.html.haml +++ b/app/views/shared/_procedure_description.html.haml @@ -22,6 +22,6 @@ %p Vous pouvez déposer vos dossiers jusqu’au #{procedure_auto_archive_datetime(procedure)}. .procedure-description - .procedure-description-body.read-more-enabled.read-more-collapsed - = h string_to_html(procedure.description, allow_a: true) + .procedure-description-body.read-more-enabled.read-more-collapsed{ tabindex: "0", role: "region", "aria-label": t('views.users.dossiers.identite.description') } + = h render SimpleFormatComponent.new(procedure.description, allow_a: true) = button_tag "Afficher la description complète", class: 'button read-more-button' diff --git a/app/views/shared/champs/rna/_association.html.haml b/app/views/shared/champs/rna/_association.html.haml index bda49c3d9..572a21402 100644 --- a/app/views/shared/champs/rna/_association.html.haml +++ b/app/views/shared/champs/rna/_association.html.haml @@ -1,6 +1,11 @@ -- if network_error - %p.pt-1= t('.network_error') -- elsif @rna.present? && champ.data.blank? +- case error +- when :invalid + %p.pt-1 + Le numéro RNA doit commencer par un W majuscule suivi de 9 chiffres +- when :not_found %p.pt-1= t('.not_found') -- elsif champ.value.present? - %p.pt-1= t('.data_fetched', title: champ.title) +- when :network_error + %p.pt-1= t('.network_error') +- else + - if champ.value.present? + %p.pt-1= t('.data_fetched', title: champ.title) diff --git a/app/views/shared/champs/siret/_etablissement.html.haml b/app/views/shared/champs/siret/_etablissement.html.haml index e93adaa15..71c9ec5f3 100644 --- a/app/views/shared/champs/siret/_etablissement.html.haml +++ b/app/views/shared/champs/siret/_etablissement.html.haml @@ -1,6 +1,9 @@ - case siret -- when :invalid - Le numéro de SIRET doit comporter exactement 14 chiffres. +- when :invalid_length + = t('errors.messages.invalid_siret_length') + +- when :invalid_checksum + = t('errors.messages.invalid_siret_checksum') - when :not_found Nous n’avons pas trouvé d’établissement correspondant à ce numéro de SIRET. diff --git a/app/views/shared/dossiers/_champ_row.html.haml b/app/views/shared/dossiers/_champ_row.html.haml index 5ee4b6547..ea12edae6 100644 --- a/app/views/shared/dossiers/_champ_row.html.haml +++ b/app/views/shared/dossiers/_champ_row.html.haml @@ -10,8 +10,7 @@ - else %tr - if c.type_champ == TypeDeChamp.type_champs.fetch(:header_section) - %th.header-section{ colspan: 3 } - = c.libelle + %th.header-section{ colspan: 3, class: c.dossier.auto_numbering_section_headers_for?(c) ? "header-section-counter" : nil }= c.libelle - else %td.libelle{ class: repetition ? 'padded' : '' } = "#{c.libelle} :" diff --git a/app/views/shared/dossiers/_champs.html.haml b/app/views/shared/dossiers/_champs.html.haml index fb7c6f2a2..4eba2463f 100644 --- a/app/views/shared/dossiers/_champs.html.haml +++ b/app/views/shared/dossiers/_champs.html.haml @@ -1,4 +1,4 @@ -%table.table.vertical.dossier-champs{ role: :presentation } +%table.table.vertical.dossier-champs.counter-start-header-section{ role: :presentation } %tbody - if dossier.show_groupe_instructeur_details? %td.libelle= dossier.procedure.routing_criteria_name diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index ed1ba11c7..0c8991525 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -2,14 +2,14 @@ - content_for(:notice_info) do = render partial: "shared/dossiers/france_connect_informations_notice", locals: { user_information: dossier.france_connect_information } -.dossier-edit.container +.dossier-edit.container.counter-start-header-section = render partial: "shared/dossiers/submit_is_over", locals: { dossier: dossier } - if dossier.brouillon? - form_options = { url: brouillon_dossier_url(dossier), method: :patch, data: { save_on_input: true } } - else - form_options = { url: modifier_dossier_url(dossier), method: :patch } - + = render Attachment::DeleteFormComponent.new = form_for dossier, form_options.merge({ html: { id: 'dossier-edit-form', class: 'form', multipart: true, novalidate: 'novalidate' } }) do |f| %header.mb-6 diff --git a/app/views/shared/dossiers/_edit_annotations.html.haml b/app/views/shared/dossiers/_edit_annotations.html.haml index ebf3339d3..654644e0c 100644 --- a/app/views/shared/dossiers/_edit_annotations.html.haml +++ b/app/views/shared/dossiers/_edit_annotations.html.haml @@ -1,6 +1,7 @@ .container.dossier-edit - if dossier.champs_private.present? - %section + %section.counter-start-header-section + = render Attachment::DeleteFormComponent.new = form_for dossier, url: annotations_instructeur_dossier_path(dossier.procedure, dossier), html: { class: 'form', multipart: true } do |f| - dossier.champs_private.each do |champ| = fields_for champ.input_name, champ do |form| diff --git a/app/views/shared/dossiers/messages/_form.html.haml b/app/views/shared/dossiers/messages/_form.html.haml index 6d04822bd..2d3845451 100644 --- a/app/views/shared/dossiers/messages/_form.html.haml +++ b/app/views/shared/dossiers/messages/_form.html.haml @@ -1,3 +1,4 @@ += render Attachment::DeleteFormComponent.new = form_for(commentaire, url: form_url, html: { class: 'form', multipart: true, data: { controller: 'persisted-form', persisted_form_key_value: @dossier.present? ? dom_id(@dossier) : dom_id(@procedure, :bulk_message) } }) do |f| - dossier = commentaire.dossier - placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder') diff --git a/app/views/shared/dossiers/messages/_messagerie_disabled.html.haml b/app/views/shared/dossiers/messages/_messagerie_disabled.html.haml index d840ecda6..73ec38500 100644 --- a/app/views/shared/dossiers/messages/_messagerie_disabled.html.haml +++ b/app/views/shared/dossiers/messages/_messagerie_disabled.html.haml @@ -6,11 +6,11 @@ Pour poser une question sur ce dossier, contactez : %p = service.nom - %br + %p = service.organisme - %br - - horaires = "Horaires : #{formatted_horaires(service.horaires)}" - = simple_format(horaires) + + - if service.horaires.present? + = render SimpleFormatComponent.new("Horaires : #{formatted_horaires(service.horaires)}") %p = mail_to service.email, service.email, diff --git a/app/views/shared/help/_help_dropdown_dossier.html.haml b/app/views/shared/help/_help_dropdown_dossier.html.haml index 7b58386af..f14542c8a 100644 --- a/app/views/shared/help/_help_dropdown_dossier.html.haml +++ b/app/views/shared/help/_help_dropdown_dossier.html.haml @@ -1,7 +1,8 @@ = render Dropdown::MenuComponent.new(wrapper: :span, wrapper_options: { class: ['help-dropdown']}, menu_options: { id: "help-menu" }) do |menu| - menu.with_button_inner_html do = t('help') - - title = dossier.brouillon? ? "Besoin d’aide pour remplir votre dossier ?" : "Une question sur votre dossier ?" + + - title = dossier.brouillon? ? t("help_dropdown.help_brouillon_title") : t("help_dropdown.help_filled_dossier") - if dossier.messagerie_available? - menu.with_item do diff --git a/app/views/shared/help/_help_dropdown_procedure.html.haml b/app/views/shared/help/_help_dropdown_procedure.html.haml index 80b939161..dc4ddc14e 100644 --- a/app/views/shared/help/_help_dropdown_procedure.html.haml +++ b/app/views/shared/help/_help_dropdown_procedure.html.haml @@ -4,6 +4,6 @@ - if procedure.service.present? - menu.with_item do - = render partial: 'shared/help/dropdown_items/service_item', locals: { service: procedure.service, title: "Une question sur cette démarche ?" } + = render partial: 'shared/help/dropdown_items/service_item', locals: { service: procedure.service, title: t('help_dropdown.procedure_title') } - menu.with_item do = render partial: 'shared/help/dropdown_items/faq_item' diff --git a/app/views/shared/help/dropdown_items/_email_item.html.haml b/app/views/shared/help/dropdown_items/_email_item.html.haml index 3eecc2f5e..dae504d16 100644 --- a/app/views/shared/help/dropdown_items/_email_item.html.haml +++ b/app/views/shared/help/dropdown_items/_email_item.html.haml @@ -1,8 +1,5 @@ -%li{ role: 'none' } - = mail_to CONTACT_EMAIL, role: 'menuitem' do - %span.icon.mail - .dropdown-description - %span.help-dropdown-title - = t('help_dropdown.technical_contact_title') - %p - = t('help_dropdown.technical_contact_description', contact_email: CONTACT_EMAIL) += mail_to CONTACT_EMAIL, role: 'menuitem' do + %span.fr-icon-mail-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" } + .dropdown-description.fr-text--sm + %span.help-dropdown-title= t('help_dropdown.technical_contact_title') + %p.fr-text--sm= t('help_dropdown.technical_contact_description', contact_email: CONTACT_EMAIL) diff --git a/app/views/shared/help/dropdown_items/_faq_item.html.haml b/app/views/shared/help/dropdown_items/_faq_item.html.haml index cea9ceaa4..e4a4e60ef 100644 --- a/app/views/shared/help/dropdown_items/_faq_item.html.haml +++ b/app/views/shared/help/dropdown_items/_faq_item.html.haml @@ -1,7 +1,6 @@ = link_to t("links.common.faq.url"), title: new_tab_suffix(t('help_dropdown.general_title')), **external_link_attributes, role: 'menuitem' do - %span.icon.help - .dropdown-description + %span.fr-icon-question-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" } + .dropdown-description.fr-text--sm %span.help-dropdown-title = t('help_dropdown.problem_title') - %p - = t('help_dropdown.problem_description') + %p.fr-text--sm= t('help_dropdown.problem_description') diff --git a/app/views/shared/help/dropdown_items/_messagerie_item.html.haml b/app/views/shared/help/dropdown_items/_messagerie_item.html.haml index 87244e6af..0316831bc 100644 --- a/app/views/shared/help/dropdown_items/_messagerie_item.html.haml +++ b/app/views/shared/help/dropdown_items/_messagerie_item.html.haml @@ -1,5 +1,5 @@ = link_to messagerie_dossier_path(dossier), role: 'menuitem' do - %span.icon.mail - .dropdown-description + %span.fr-icon-mail-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" } + .dropdown-description.fr-text--sm %span.help-dropdown-title= title - %p Envoyez directement un message à l’instructeur. + %p.fr-text--sm= t('help_dropdown.contact_instructeur') diff --git a/app/views/shared/help/dropdown_items/_service_item.html.haml b/app/views/shared/help/dropdown_items/_service_item.html.haml index 3aae66c04..208601f51 100644 --- a/app/views/shared/help/dropdown_items/_service_item.html.haml +++ b/app/views/shared/help/dropdown_items/_service_item.html.haml @@ -1,14 +1,14 @@ -%span.icon.person -.dropdown-description +%span.fr-icon-user-fill.fr-text-action-high--blue-france{ "aria-hidden": "true" } +.dropdown-description.fr-text--sm %span.help-dropdown-title= title .help-dropdown-service-action - %p Contactez directement l’administration : - %p.help-dropdown-service-item - %span.icon.small.mail + %p.fr-text--sm= t('help_dropdown.contact_administration') + %p.fr-text--sm.help-dropdown-service-item + %span.fr-icon-mail-fill.fr-icon--sm{ "aria-hidden": "true" } = link_to service.email, "mailto:#{service.email}", role: 'menuitem' - %p.help-dropdown-service-item - %span.icon.small.phone + %p.fr-text--sm + %span.fr-icon-phone-fill.fr-icon--sm{ "aria-hidden": "true" } = link_to service.telephone, service.telephone_url, role: 'menuitem' - %p.help-dropdown-service-item - %span.icon.small.clock + %p.fr-text--sm + %span.fr-icon-time-fill.fr-icon--sm{ "aria-hidden": "true" } = service.horaires diff --git a/app/views/users/_procedure_footer.html.haml b/app/views/users/_procedure_footer.html.haml index cda5f0b6d..03c5166f8 100644 --- a/app/views/users/_procedure_footer.html.haml +++ b/app/views/users/_procedure_footer.html.haml @@ -12,13 +12,16 @@ - elsif service.present? %li = link_to I18n.t('users.procedure_footer.contact.email.link', service_email: service.email), "mailto:#{service.email}", class: 'fr-footer__top-link' - %li - - horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}" - = link_to service.telephone_url, class: 'fr-footer__top-link' do - %p - = I18n.t('users.procedure_footer.contact.phone.link', service_telephone: service.telephone) - %p - = horaires + - if service.telephone.present? || service.horaires.present? + %li + - horaires = "#{I18n.t('users.procedure_footer.contact.schedule.prefix')}#{formatted_horaires(service.horaires)}" + - if service.telephone.present? + = link_to service.telephone_url, class: 'fr-footer__top-link' do + %p + = I18n.t('users.procedure_footer.contact.phone.link', service_telephone: service.telephone) + - if service.horaires.present? + %p + = horaires %li = link_to I18n.t('users.procedure_footer.contact.stats.link'), statistiques_path(procedure.path), class: 'fr-footer__top-link', rel: 'noopener' @@ -65,13 +68,13 @@ - if service.present? .fr-footer__content + %p.fr-footer__content-desc = I18n.t('users.procedure_footer.managed_by.header') - %span{ lang: :fr } - = "#{service.nom}," - = "#{service.organisme}," - = string_to_html(service.adresse, 'span') - = render partial: "shared/footer_content_list" + %span{ lang: :fr }= "#{service.nom}, #{service.organisme}," + %div{ lang: :fr } + = render SimpleFormatComponent.new(service.adresse, class_names_map: {paragraph: 'fr-footer__content-desc'}) + = render partial: "shared/footer_content_list" .fr-footer__bottom = render partial: 'users/general_footer_row', locals: { dossier: dossier } diff --git a/app/views/users/registrations/new.html.haml b/app/views/users/registrations/new.html.haml index feb6e0870..e86419ad2 100644 --- a/app/views/users/registrations/new.html.haml +++ b/app/views/users/registrations/new.html.haml @@ -18,10 +18,8 @@ .fr-fieldset__element= render Dsfr::InputComponent.new(form: f, attribute: :email, input_type: :email_field, opts: { autocomplete: 'email', autofocus: true }) .fr-fieldset__element - = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', min_length: PASSWORD_MIN_LENGTH }) do |c| + = render Dsfr::InputComponent.new(form: f, attribute: :password, input_type: :password_field, opts: { autocomplete: 'new-password', minlength: PASSWORD_MIN_LENGTH }) do |c| - c.describedby do - #password-input-messages.fr-messages-group{ "aria-live" => "off" } - %p#password-input-message.fr-message= t('views.registrations.new.password_message') - %p#password-input-message-info.fr-message.fr-message--info= t('views.registrations.new.password_placeholder', min_length: PASSWORD_MIN_LENGTH) + = render partial: "devise/password_rules", locals: { id: c.describedby_id } = f.submit t('views.shared.account.create'), class: "fr-btn fr-btn--lg" diff --git a/config/elastic_apm.yml b/config/elastic_apm.yml new file mode 100644 index 000000000..2c65bac6b --- /dev/null +++ b/config/elastic_apm.yml @@ -0,0 +1,8 @@ +# Set options ELASTIC_APM_SERVER_URL & ELASTIC_APM_SECRET_TOKEN by env vars instead +# See https://www.elastic.co/guide/en/apm/agent/ruby/current/configuration.html +# +# server_url: '' +# secret_token: '' + +# Enable it with ELASTIC_APM_ENABLED="true" +enabled: false diff --git a/config/env.example.optional b/config/env.example.optional index c3924f529..a3f47d43f 100644 --- a/config/env.example.optional +++ b/config/env.example.optional @@ -134,14 +134,16 @@ VITE_LEGACY="" # around july 2022, we changed the duree_conservation_dossiers_dans_ds, allow instances to choose their own duration NEW_MAX_DUREE_CONSERVATION=12 -# -OPENDATA_ENABLED="enabled" -# Publish to datagouv +# Open data +OPENDATA_ENABLED="enabled" # disabled by default if `OPENDATA_ENABLED` not set + +# Open data, publish to data.gouv.fr DATAGOUV_API_KEY="thisisasecret" DATAGOUV_API_URL="https://www.data.gouv.fr/api/1" -DATAGOUV_DESCRIPTIF_DEMARCHES_DATASET="datasetid" -DATAGOUV_DESCRIPTIF_DEMARCHES_RESOURCE="resourceid" +DATAGOUV_STATISTICS_DATASET="dataset-id1" +DATAGOUV_DESCRIPTIF_DEMARCHES_DATASET="dataset-id2" +DATAGOUV_DESCRIPTIF_DEMARCHES_RESOURCE="resource-id-of-dataset-id2" # Zonage ZONAGE_ENABLED='enabled' # zonage disabled by default if `ZONAGE_ENABLED` not set @@ -166,3 +168,10 @@ SIB_WEBHOOK_URL="" # ServicesPublics+ tracking url shown to user when dossier is terminated. SERVICES_PUBLICS_PLUS_URL="" + +# Elasticsearch APM +# send application logs to elasticsearch APM component via elastic-apm gem +# see https://www.elastic.co/guide/en/apm/agent/ruby/current/configuration.html for more configuration options and vars +ELASTIC_APM_ENABLED="" # disabled by default, set to "true" to enable +ELASTIC_APM_SERVER_URL="http://192.168.0.1:8200" # elasticsearch APM endpoint URL + diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 82bd9ca2a..c127bad30 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -110,6 +110,8 @@ ignore_unused: - 'instructeurs.dossiers.filterable_state.*' - 'views.prefill_descriptions.edit.possible_values.*' - 'helpers.page_entries_info.*' +- 'combo_search_component.result_slot_html.*' +- 'combo_search_component.screen_reader_instructions' # - '{devise,kaminari,will_paginate}.*' # - 'simple_form.{yes,no}' # - 'simple_form.{placeholders,hints,labels}.*' diff --git a/config/initializers/acsv.rb b/config/initializers/acsv.rb index 3a24d2032..d099dbe6a 100644 --- a/config/initializers/acsv.rb +++ b/config/initializers/acsv.rb @@ -4,7 +4,7 @@ require 'csv' module ACSV class CSV < ::CSV def self.new_for_ruby3(data, options = {}) - options[:col_sep] ||= ACSV::Detect.separator(data) + options[:col_sep] ||= ACSV::Detect.separator(data) || :auto # because of the Separation of positional and keyword arguments in Ruby 3.0 # (https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/) # instead of diff --git a/config/locales/en.yml b/config/locales/en.yml index ca3bc20f5..b36194b45 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -35,10 +35,15 @@ en: help: 'Help' help_dropdown: general_title: "Online help" + procedure_title: "Do you have a question about this procedure?" problem_title: A problem with the website ? problem_description: Find your answer in the online help. technical_contact_title: Technical contact technical_contact_description: Send us a message to %{contact_email}. + contact_administration: "Contact the administration directly:" + help_brouillon_title: "Need help filling out your file?" + help_filled_dossier: "A question about your file?" + contact_instructeur: Send a message directly to the instructor. utils: 'yes': Yes 'no': No @@ -59,6 +64,15 @@ en: commentaire: send_message_to_instructeur: "Send a message to the instructor" reply_in_mailbox: "Reply in mailbox" + skiplinks: + quick: Quick access + content: Content + combo_search_component: + screen_reader_instructions: "When autocomplete results are available, use the up and down arrows to navigate through the list of results. Press Enter to select a result. Press Escape to close the results list." + result_slot_html: + zero: No result + one: 1 result + other: results layouts: commencer: no_procedure: @@ -68,7 +82,6 @@ en: are_you_new: First time on %{app_name}? my_account: My account header: - label_modal: "Burger menu" close_modal: 'Close' back: "Back" back_title: "Revenir sur le site de mon administration" @@ -128,11 +141,20 @@ en: iban_html: An Iban number yes_no_html: '"true" for Yes, "false" pour No' checkbox_html: '"true" to check, "false" to uncheck' - pays_html: An ISO 3166-2 country code - regions_html: An INSEE region code + pays_html: An ISO 3166-2 country code + regions_html: An INSEE region code + departements_html: A department number + communes_html: An array of the department code and the INSEE commune code. + drop_down_list_html: A choice among those selected when the procedure was created date_html: ISO8601 date datetime_html: ISO8601 datetime drop_down_list_other_html: Any value + siret_html: A SIRET number + rna_html: A RNA number + repetition_html: A array of hashes with possible values for each field of the repetition. + epci_html: An array of the department code and the EPCI one. + annuaire_education_html: An educational institution code, as defined by the Éducation Nationale directory + dossier_link_html: The file ID, as integer examples: title: Example text: Short text @@ -144,10 +166,15 @@ en: iban: FR7611315000011234567890138 yes_no: "true" pays: "FR" + departements: "56" regions: "53" date: "2023-02-01" datetime: "2023-02-01T10:30" checkbox: "true" + annuaire_education: "0561383Z" + dossier_link: 42 + rna: "W503726238" + siret: "13002526500013" prefill_link_title: Prefill link (GET) prefill_link_info: Use the button to copy the link, then remplace the values with your data. prefill_link_too_long: Warning, the prefill link is too long and may not work on all browsers. @@ -262,6 +289,7 @@ en: identity_data: Identity data all_required: All fields are required. civility: Civility + description: Description of the procedure first_name: First Name last_name: Last Name birthdate: Date de naissance @@ -347,6 +375,10 @@ en: connect_with_agent_connect: Visit our dedicated page subtitle: "Sign in with my account" passwords: + edit: + subtitle: Change password + submit: Change password + submit_loading: Sending… reset_link_sent: got_it: Got it! open_your_mailbox: Now open your mailbox. @@ -394,6 +426,19 @@ en: zone: This procedure is run by champs: value: Value + default_mail_attributes: &default_mail_attributes + hints: + subject: The generated subject will be truncated if it exceeds 100 characters. + mails/closed_mail: + << : *default_mail_attributes + mails/initiated_mail: + << : *default_mail_attributes + mails/received_mail: + << : *default_mail_attributes + mails/refused_mail: + << : *default_mail_attributes + mails/without_continuation_mail: + << : *default_mail_attributes errors: messages: @@ -455,19 +500,42 @@ en: attributes: value: not_in_options: "must be in the given options" + "champs/multiple_drop_down_list_champ": + attributes: + value: + not_in_options: "must be in the given options" "champs/region_champ": attributes: value: not_in_region_names: "must be a valid region name" external_id: not_in_region_codes: "must be a valid region code" + "champs/departement_champ": + attributes: + value: + not_in_departement_names: "must be a valid department name" + external_id: + not_in_departement_codes: "must be a valid department code" + "champs/epci_champ": + attributes: + code_departement: + not_in_departement_codes: "must be a valid department code" + external_id: + not_in_departement_epci_codes: "must be a valid EPCI code of the matching department" + value: + not_in_departement_epci_names: "must be a valid EPCI name of the matching department" + "champs/dossier_link_champ": + attributes: + value: + not_integerable: "must be an integer" errors: format: "Field « %{attribute} » %{message}" messages: dossier_not_found: "The file does not exist or you do not have access to it." # # dossier_map_not_activated: "The file does not have access to the map." targeted_user_link_expired: "This invitation link or the file is no longer available." - invalid_siret: "The SIRET is incorrect" + invalid_siret_length: "The SIRET number must contain exactly 14 numbers." + invalid_siret_checksum: "The SIRET number is invalid." procedure_not_found: "The procedure does not exist" siret_unknown: 'Sorry, we did not find any establishment registered under this SIRET number.' siret_network_error: 'Désolé, la récupération des informations SIRET est temporairement indisponible. Veuillez réessayer dans quelques instants.' @@ -541,6 +609,11 @@ en: deleted: one: Deleted other: Deleted + administrateurs: + activate: + new: + title: Pick a password + continue: Continue users: dossiers: test_procedure: "This file is submitted on a test procedure. Any modification of the procedure by the administrator (addition of a field, publication of the procedure, etc.) will result in the removal of the file." diff --git a/config/locales/fr.yml b/config/locales/fr.yml index a69e22f71..02b062b38 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -25,10 +25,15 @@ fr: help: 'Aide' help_dropdown: general_title: "Aide en ligne" + procedure_title: "Une question sur cette démarche ?" problem_title: Un problème avec le site ? problem_description: Trouvez votre réponse dans l’aide en ligne. technical_contact_title: Contact technique technical_contact_description: Envoyez nous un message à %{contact_email}. + contact_administration: "Contactez directement l’administration :" + help_brouillon_title: "Besoin d’aide pour remplir votre dossier ?" + help_filled_dossier: "Une question sur votre dossier ?" + contact_instructeur: Envoyez directement un message à l’instructeur. utils: 'yes': Oui 'no': Non @@ -50,6 +55,15 @@ fr: commentaire: send_message_to_instructeur: "Envoyer un message à l’instructeur" reply_in_mailbox: "Répondre dans la messagerie" + skiplinks: + quick: Accès rapide + content: Contenu + combo_search_component: + screen_reader_instructions: "Quand les résultats de l’autocomplete sont disponibles, utilisez les flèches haut et bas pour naviguer dans la liste des résultats. Appuyez sur Entrée pour sélectionner un résultat. Appuyez sur Echap pour fermer la liste des résultats." + result_slot_html: + zero: Aucun résultat + one: 1 résultat + other: résultats layouts: commencer: no_procedure: @@ -59,7 +73,6 @@ fr: are_you_new: Vous êtes nouveau sur %{app_name} ? my_account: Mon compte header: - label_modal: "Menu en-tête de page" close_modal: 'Fermer' back: "Revenir en arrière" back_title: "Revenir sur le site de mon administration" @@ -119,11 +132,20 @@ fr: iban_html: Un numéro Iban yes_no_html: '"true" pour Oui, "false" pour Non' checkbox_html: '"true" pour coché, "false" pour décoché' - pays_html: Un code pays ISO 3166-2 - regions_html: Un code INSEE de région + pays_html: Un code pays ISO 3166-2 + regions_html: Un code INSEE de région + departements_html: Un numéro de département + communes_html: Un tableau contenant le code de département et le code INSEE de la commune. + drop_down_list_html: Un choix parmi ceux sélectionnés à la création de la procédure datetime_html: Datetime au format ISO8601 date_html: Date au format ISO8601 drop_down_list_other_html: Toute valeur + rna_html: Un numéro RNA + siret_html: Un numéro de SIRET + repetition_html: Un tableau de dictionnaires avec les valeurs possibles pour chaque champ de la répétition. + epci_html: Un tableau contenant le code de département et celui de l'EPCI. + annuaire_education_html: Un code d'établissement scolaire, tel que défini par l'Annuaire de l'Éducation Nationale + dossier_link_html: L'identifiant du dossier, sous forme de nombre entier examples: title: Exemple text: Texte court @@ -136,10 +158,15 @@ fr: yes_no: "true" civilite: "M." pays: "FR" + departements: "56" regions: "53" date: "2023-02-01" datetime: "2023-02-01T10:30" checkbox: "true" + annuaire_education: "0561383Z" + dossier_link: 42 + rna: "W503726238" + siret: "13002526500013" prefill_link_title: Lien de préremplissage (GET) prefill_link_info: Copiez le lien grâce au bouton ci-dessous et remplacez les valeurs par les données dont vous disposez. prefill_link_too_long: Attention, ce lien de préremplissage est trop long et risque de ne pas fonctionner sur certains navigateurs. @@ -258,6 +285,7 @@ fr: identity_data: Données d’identité all_required: Tous les champs sont obligatoires. civility: Civilité + description: Description de la démarche first_name: Prénom last_name: Nom birthdate: Date de naissance @@ -343,6 +371,10 @@ fr: connect_with_agent_connect: Accédez à notre page dédiée subtitle: "Se connecter avec son compte" passwords: + edit: + subtitle: Changement de mot de passe + submit: Changer le mot de passe + submit_loading: Envoi… reset_link_sent: email_sent_html: "Nous vous avons envoyé un email à l’adresse %{email}." click_link_to_reset_password: "Cliquez sur le lien contenu dans l’email pour changer votre mot de passe." @@ -391,6 +423,19 @@ fr: zone: La démarche est mise en œuvre par champs: value: Valeur du champ + default_mail_attributes: &default_mail_attributes + hints: + subject: "L’objet généré sera tronqué s’il dépasse 100 caractères." + mails/closed_mail: + << : *default_mail_attributes + mails/initiated_mail: + << : *default_mail_attributes + mails/received_mail: + << : *default_mail_attributes + mails/refused_mail: + << : *default_mail_attributes + mails/without_continuation_mail: + << : *default_mail_attributes errors: messages: @@ -450,12 +495,34 @@ fr: attributes: value: not_in_options: "doit être dans les options proposées" + "champs/multiple_drop_down_list_champ": + attributes: + value: + not_in_options: "doit être dans les options proposées" "champs/region_champ": attributes: value: not_in_region_names: "doit être un nom de région valide" external_id: not_in_region_codes: "doit être un code de région valide" + "champs/departement_champ": + attributes: + value: + not_in_departement_names: "doit être un nom de département valide" + external_id: + not_in_departement_codes: "doit être un code de département valide" + "champs/epci_champ": + attributes: + code_departement: + not_in_departement_codes: "doit être un code de département valide" + external_id: + not_in_departement_epci_codes: "doit être un code d'EPCI du département correspondant" + value: + not_in_departement_epci_names: "doit être un nom d'EPCI du département correspondant" + "champs/dossier_link_champ": + attributes: + value: + not_integerable: "doit être un entier" errors: format: "Le champ « %{attribute} » %{message}" messages: @@ -463,7 +530,8 @@ fr: dossier_not_found: "Le dossier n’existe pas ou vous n’y avez pas accès." # dossier_map_not_activated: "Le dossier n’a pas accès à la cartographie." targeted_user_link_expired: "Ce lien d’invitation n’est plus valable ou le dossier n’est plus accessible." - invalid_siret: "Le siret est incorrect" + invalid_siret_length: "Le numéro de SIRET doit comporter exactement 14 chiffres." + invalid_siret_checksum: "Le format du numéro de SIRET est invalide." procedure_not_found: "La démarche n’existe pas" siret_unknown: 'Désolé, nous n’avons pas trouvé d’établissement enregistré correspondant à ce numéro SIRET.' siret_network_error: 'Désolé, la récupération des informations SIRET est temporairement indisponible. Veuillez réessayer dans quelques instants.' @@ -590,6 +658,10 @@ fr: to_follow: à suivre total: dossiers administrateurs: + activate: + new: + title: Choix du mot de passe + continue: Continuer index: restored: La démarche %{procedure_id} a été restaurée dropdown_actions: diff --git a/config/locales/views/administrateurs/groupe_instructeurs/en.yml b/config/locales/views/administrateurs/groupe_instructeurs/en.yml index 208cd053f..a232122cc 100644 --- a/config/locales/views/administrateurs/groupe_instructeurs/en.yml +++ b/config/locales/views/administrateurs/groupe_instructeurs/en.yml @@ -16,13 +16,6 @@ en: assignment: one: "The instructor %{emails} was assigned to the group « %{groupe} »." other: "The instructors %{emails} were assigned to the group « %{groupe} »." - email_body: - one: "The instructor %{emails} was assigned to the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." - other: "The instructors %{emails} were assigned to the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." - remove_instructeur: - email_body: - one: "The instructor %{emails} was removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." - other: "The instructors %{emails} were removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." reaffecter_dossiers: existing_groupe: one: "%{count} group exist" @@ -48,11 +41,15 @@ en: notice: This group will be a choice from the list "%{routing_criteria_name}" csv_import: title: CSV Import - notice_1: The csv file must have 2 columns (Group, Email) and be separated by commas. The import does not overwrite existing groups and instructors. + routing_enabled: + notice_1: The csv file must have 2 columns (Group, Email) and be separated by commas. The import does not overwrite existing groups and instructors. + routing_disabled: + notice_1: The csv file must have 1 column with instructors emails. notice_2: The size of the file must be less than %{csv_max_size}. download_exemple: Download sample CSV file import_file: Import file import_file_procedure_not_published: The import of instructors by CSV file is available once the process has been published + import_file_alert: Instructors added to the procedure will receive an email. Are you sure you want to continue ?" set_up: set up button: add_group: Add group diff --git a/config/locales/views/administrateurs/groupe_instructeurs/fr.yml b/config/locales/views/administrateurs/groupe_instructeurs/fr.yml index dc26ca649..4490b862e 100644 --- a/config/locales/views/administrateurs/groupe_instructeurs/fr.yml +++ b/config/locales/views/administrateurs/groupe_instructeurs/fr.yml @@ -22,13 +22,6 @@ fr: assignment: one: "L’instructeur %{emails} a été affecté au groupe « %{groupe} »." other: "Les instructeurs %{emails} ont été affectés au groupe « %{groupe} »." - email_body: - one: "L’instructeur %{emails} a été affecté au groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." - other: "Les instructeurs %{emails} ont été affectés au groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." - remove_instructeur: - email_body: - one: "L’instructeur %{emails} a été retiré du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." - other: "Les instructeurs %{emails} ont été retirés du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." reaffecter_dossiers: existing_groupe: one: "%{count} groupe existe" @@ -54,11 +47,15 @@ fr: notice: Ce groupe sera un choix de la liste "%{routing_criteria_name}" csv_import: title: Importer par CSV - notice_1: Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. Si vous n'avez pas créé de groupe, entrez « défaut » dans la colonne Groupe pour chaque instructeur. L’import n’écrase pas les groupes et les instructeurs existants. La modification du fichier csv ne s’opère que pour l’ajout de nouveaux instructeurs. La suppression d’un instructeur s’opère manuellement en cliquant sur le bouton « retirer ». - notice_2: Le poids du fichier doit être inférieur à %{csv_max_size} + routing_enabled: + notice_1: Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. + routing_disabled: + notice_1: Le fichier csv doit comporter 1 seule colonne (Email) avec une adresse email d'instructeur par ligne. + notice_2: L’import n’écrase pas les groupes et les instructeurs existants. La modification du fichier csv ne s’opère que pour l’ajout de nouveaux instructeurs. La suppression d’un instructeur s’opère manuellement en cliquant sur le bouton « retirer ». Le poids du fichier doit être inférieur à %{csv_max_size}. download_exemple: Télécharger l’exemple de fichier CSV import_file: Importer le fichier import_file_procedure_not_published: L’import d’instructeurs par fichier CSV est disponible une fois la démarche publiée + import_file_alert: Tous les instructeurs ajoutés à la procédure vont être notifiés par email. Voulez-vous continuer ? view: voir set_up: paramétrer button: diff --git a/config/locales/views/groupe_instructeur_mailer/notify_added_instructeurs/en.yml b/config/locales/views/groupe_instructeur_mailer/notify_added_instructeurs/en.yml new file mode 100644 index 000000000..92b83a7dd --- /dev/null +++ b/config/locales/views/groupe_instructeur_mailer/notify_added_instructeurs/en.yml @@ -0,0 +1,6 @@ +en: + groupe_instructeur_mailer: + notify_added_instructeurs: + email_body: + one_group: "You were assigned to the procedure « %{procedure} » by « %{email} »." + many_groups: "You were assigned to the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." diff --git a/config/locales/views/groupe_instructeur_mailer/notify_added_instructeurs/fr.yml b/config/locales/views/groupe_instructeur_mailer/notify_added_instructeurs/fr.yml new file mode 100644 index 000000000..73cb5933e --- /dev/null +++ b/config/locales/views/groupe_instructeur_mailer/notify_added_instructeurs/fr.yml @@ -0,0 +1,6 @@ +fr: + groupe_instructeur_mailer: + notify_added_instructeurs: + email_body: + one_group: "Vous avez été affecté(e) à la démarche « %{procedure} » par « %{email} »." + many_groups: "Vous avez été ajouté(e) au groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." diff --git a/config/locales/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed/en.yml b/config/locales/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed/en.yml new file mode 100644 index 000000000..d5192fdec --- /dev/null +++ b/config/locales/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed/en.yml @@ -0,0 +1,6 @@ +en: + groupe_instructeur_mailer: + notify_group_when_instructeurs_removed: + email_body: + one: "The instructor %{emails} was removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." + other: "The instructors %{emails} were removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." diff --git a/config/locales/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed/fr.yml b/config/locales/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed/fr.yml new file mode 100644 index 000000000..a1e3e00de --- /dev/null +++ b/config/locales/views/groupe_instructeur_mailer/notify_group_when_instructeurs_removed/fr.yml @@ -0,0 +1,6 @@ +fr: + groupe_instructeur_mailer: + notify_group_when_instructeurs_removed: + email_body: + one: "L’instructeur %{emails} a été retiré du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." + other: "Les instructeurs %{emails} ont été retirés du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." diff --git a/config/locales/views/groupe_instructeur_mailer/notify_removed_instructeur/en.yml b/config/locales/views/groupe_instructeur_mailer/notify_removed_instructeur/en.yml new file mode 100644 index 000000000..e786341f3 --- /dev/null +++ b/config/locales/views/groupe_instructeur_mailer/notify_removed_instructeur/en.yml @@ -0,0 +1,6 @@ +en: + groupe_instructeur_mailer: + notify_removed_instructeur: + email_body: + assigned: "You were removed from the group « %{groupe} » by « %{email} », in charge of procedure « %{procedure} »." + unassigned: "You were unassigned from the procedure « %{procedure} » by « %{email} »." diff --git a/config/locales/views/groupe_instructeur_mailer/notify_removed_instructeur/fr.yml b/config/locales/views/groupe_instructeur_mailer/notify_removed_instructeur/fr.yml new file mode 100644 index 000000000..5a877e37c --- /dev/null +++ b/config/locales/views/groupe_instructeur_mailer/notify_removed_instructeur/fr.yml @@ -0,0 +1,6 @@ +fr: + groupe_instructeur_mailer: + notify_removed_instructeur: + email_body: + assigned: "Vous avez été retiré(e) du groupe « %{groupe} » par « %{email} », en charge de la démarche « %{procedure} »." + unassigned: "Vous avez été désaffecté(e) de la démarche « %{procedure} » par « %{email} »." diff --git a/config/routes.rb b/config/routes.rb index e295e8179..ff4055da3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -274,7 +274,7 @@ Rails.application.routes.draw do namespace :v1 do resources :demarches, only: [] do member do - resources :dossiers, only: :create + resources :dossiers, only: [:create, :index] resources :stats, only: :index end end diff --git a/db/migrate/20230216041517_remove_champs_external_id_index.rb b/db/migrate/20230216041517_remove_champs_external_id_index.rb new file mode 100644 index 000000000..1e5863b25 --- /dev/null +++ b/db/migrate/20230216041517_remove_champs_external_id_index.rb @@ -0,0 +1,13 @@ +class RemoveChampsExternalIdIndex < ActiveRecord::Migration[6.1] + include Database::MigrationHelpers + + disable_ddl_transaction! + + def up + remove_index :champs, column: :external_id + end + + def down + add_concurrent_index :champs, :external_id + end +end diff --git a/db/migrate/20230216130722_fix_active_storage_attachment_missing_fk_on_blob_id.rb b/db/migrate/20230216130722_fix_active_storage_attachment_missing_fk_on_blob_id.rb new file mode 100644 index 000000000..51b697797 --- /dev/null +++ b/db/migrate/20230216130722_fix_active_storage_attachment_missing_fk_on_blob_id.rb @@ -0,0 +1,5 @@ +class FixActiveStorageAttachmentMissingFkOnBlobId < ActiveRecord::Migration[6.1] + def change + add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id, validate: false + end +end diff --git a/db/migrate/20230216141558_validate_foreign_key_between_attachments_and_blobs.rb b/db/migrate/20230216141558_validate_foreign_key_between_attachments_and_blobs.rb new file mode 100644 index 000000000..ba383c80a --- /dev/null +++ b/db/migrate/20230216141558_validate_foreign_key_between_attachments_and_blobs.rb @@ -0,0 +1,5 @@ +class ValidateForeignKeyBetweenAttachmentsAndBlobs < ActiveRecord::Migration[6.1] + def up + validate_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id + end +end diff --git a/db/migrate/20230217094119_add_estimated_dossiers_count_and_dossiers_count_computed_at_to_procedures.rb b/db/migrate/20230217094119_add_estimated_dossiers_count_and_dossiers_count_computed_at_to_procedures.rb new file mode 100644 index 000000000..2e33dc484 --- /dev/null +++ b/db/migrate/20230217094119_add_estimated_dossiers_count_and_dossiers_count_computed_at_to_procedures.rb @@ -0,0 +1,6 @@ +class AddEstimatedDossiersCountAndDossiersCountComputedAtToProcedures < ActiveRecord::Migration[6.1] + def change + add_column :procedures, :estimated_dossiers_count, :integer + add_column :procedures, :dossiers_count_computed_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 59abcc837..eda27f5c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_02_07_105539) do +ActiveRecord::Schema.define(version: 2023_02_17_094119) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" @@ -227,13 +227,12 @@ ActiveRecord::Schema.define(version: 2023_02_07_105539) do t.datetime "rebased_at" t.string "row_id" t.string "type" - t.integer "type_de_champ_id", null: false + t.integer "type_de_champ_id" t.datetime "updated_at" t.string "value" t.jsonb "value_json" t.index ["dossier_id"], name: "index_champs_on_dossier_id" t.index ["etablissement_id"], name: "index_champs_on_etablissement_id" - t.index ["external_id"], name: "index_champs_on_external_id" t.index ["parent_id"], name: "index_champs_on_parent_id" t.index ["private"], name: "index_champs_on_private" t.index ["row_id"], name: "index_champs_on_row_id" @@ -704,12 +703,14 @@ ActiveRecord::Schema.define(version: 2023_02_07_105539) do t.string "declarative_with_state" t.string "description" t.string "direction" + t.datetime "dossiers_count_computed_at" t.bigint "draft_revision_id" t.integer "duree_conservation_dossiers_dans_ds" t.boolean "duree_conservation_etendue_par_ds", default: false t.boolean "durees_conservation_required", default: true t.string "encrypted_api_particulier_token" t.boolean "estimated_duration_visible", default: true, null: false + t.integer "estimated_dossiers_count" t.boolean "euro_flag", default: false t.boolean "experts_require_administrateur_invitation", default: false t.boolean "for_individual", default: false diff --git a/lib/tasks/deployment/20230208084036_normalize_departements.rake b/lib/tasks/deployment/20230208084036_normalize_departements.rake new file mode 100644 index 000000000..78c992b30 --- /dev/null +++ b/lib/tasks/deployment/20230208084036_normalize_departements.rake @@ -0,0 +1,32 @@ +namespace :after_party do + desc 'Deployment task: normalize_departements' + task normalize_departements: :environment do + puts "Running deploy task 'normalize_departements'" + + scope_external_id_nil = Champs::DepartementChamp.where(external_id: nil) + scope_external_id_empty = Champs::DepartementChamp.where(external_id: '') + scope_external_id_present = Champs::DepartementChamp.where.not(external_id: [nil, '']) + + progress = ProgressReport.new(scope_external_id_nil.count + scope_external_id_empty.count + scope_external_id_present.count) + + normalize_asynchronously(scope_external_id_nil, progress, Migrations::NormalizeDepartementsWithNilExternalIdJob) + normalize_asynchronously(scope_external_id_empty, progress, Migrations::NormalizeDepartementsWithEmptyExternalIdJob) + normalize_asynchronously(scope_external_id_present, progress, Migrations::NormalizeDepartementsWithPresentExternalIdJob) + + 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 + + private + + def normalize_asynchronously(scope, progress, job) + scope.pluck(:id).in_groups_of(10_000, false) do |champ_ids| + job.perform_later(champ_ids) + progress.inc(champ_ids.count) + end + end +end diff --git a/lib/tasks/deployment/20230215100231_normalize_geometries.rake b/lib/tasks/deployment/20230215100231_normalize_geometries.rake new file mode 100644 index 000000000..275e37169 --- /dev/null +++ b/lib/tasks/deployment/20230215100231_normalize_geometries.rake @@ -0,0 +1,19 @@ +namespace :after_party do + desc 'Deployment task: normalize_geometries' + task normalize_geometries: :environment do + puts "Running deploy task 'normalize_geometries'" + + progress = ProgressReport.new(GeoArea.count) + GeoArea.in_batches(of: 100) do |geo_areas| + ids = geo_areas.ids + Migrations::NormalizeGeoAreaJob.perform_later(ids) + progress.inc(ids.size) + 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 +end diff --git a/lib/tasks/deployment/20230216135218_reclean_attachments.rake b/lib/tasks/deployment/20230216135218_reclean_attachments.rake new file mode 100644 index 000000000..cfc5eb52f --- /dev/null +++ b/lib/tasks/deployment/20230216135218_reclean_attachments.rake @@ -0,0 +1,21 @@ +namespace :after_party do + desc 'Deployment task: reclean_attachments' + task reclean_attachments: :environment do + puts "Running deploy task 'reclean_attachments'" + + invalid_attachments = ActiveStorage::Attachment.where.missing(:blob) + invalid_attachments_count = invalid_attachments.size + + if invalid_attachments.any? + invalid_attachments.destroy_all + puts "#{invalid_attachments_count} with blob that doesn't exist have been destroyed" + else + puts "No attachments with blob that doesn't exist found" + end + + # 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 diff --git a/lib/tasks/deployment/20230221100840_strip_type_de_champ_libelle.rake b/lib/tasks/deployment/20230221100840_strip_type_de_champ_libelle.rake new file mode 100644 index 000000000..e668e3592 --- /dev/null +++ b/lib/tasks/deployment/20230221100840_strip_type_de_champ_libelle.rake @@ -0,0 +1,22 @@ +namespace :after_party do + desc 'Deployment task: strip_type_de_champ_libelle' + task strip_type_de_champ_libelle: :environment do + puts "Running deploy task 'strip_type_de_champ_libelle'" + + # ~ 152K records matched + tdcs = TypeDeChamp.where("libelle LIKE ?", ' %').or(TypeDeChamp.where("libelle LIKE ?", '% ')) + progress = ProgressReport.new(tdcs.count) + + tdcs.find_each do |tdc| + tdc.save! + progress.inc + 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 +end diff --git a/lib/tasks/deployment/20230223103427_update_procedure_dossiers_count.rake b/lib/tasks/deployment/20230223103427_update_procedure_dossiers_count.rake new file mode 100644 index 000000000..e99e096bd --- /dev/null +++ b/lib/tasks/deployment/20230223103427_update_procedure_dossiers_count.rake @@ -0,0 +1,20 @@ +namespace :after_party do + desc 'Deployment task: update_procedure_dossiers_count' + task update_procedure_dossiers_count: :environment do + puts "Running deploy task 'update_procedure_dossiers_count'" + progress = ProgressReport.new(Procedure.count) + + Procedure.find_each do |p| + progress.inc + begin + p.update_columns(estimated_dossiers_count: p.dossiers.visible_by_administration.count, dossiers_count_computed_at: Time.zone.now) + rescue => e + Sentry.capture_exception(e, extra: { procedure_id: p.id }) + end + end + progress.finish + + AfterParty::TaskRecord + .create version: AfterParty::TaskRecorder.new(__FILE__).timestamp + end +end diff --git a/public/csv/import-instructeurs-test.csv b/public/csv/import-instructeurs-test.csv new file mode 100644 index 000000000..df7c8ecff --- /dev/null +++ b/public/csv/import-instructeurs-test.csv @@ -0,0 +1,5 @@ +Email +camilia@gouv.fr +kara@gouv.fr +simon@gouv.fr +pauline@gouv.fr diff --git a/spec/components/attachment/edit_component_spec.rb b/spec/components/attachment/edit_component_spec.rb index 40851b3aa..6158b099d 100644 --- a/spec/components/attachment/edit_component_spec.rb +++ b/spec/components/attachment/edit_component_spec.rb @@ -103,7 +103,7 @@ RSpec.describe Attachment::EditComponent, type: :component do it 'displays the filename, but doesn’t allow to download the file' do expect(attachment.watermark_pending?).to be_truthy expect(subject).to have_text(filename) - expect(subject).to have_link('Supprimer') + expect(subject).to have_button('Supprimer') expect(subject).to have_no_link(text: filename) # don't match "Delete" link which also include filename in title attribute expect(subject).to have_text('Traitement en cours') end @@ -152,8 +152,12 @@ RSpec.describe Attachment::EditComponent, type: :component do end end - context 'when the file is scanned and safe' do + context 'when the file is scanned, watermarked_at, and viewed as download and safe' do + let(:kwargs) { { view_as: :download } } let(:virus_scan_result) { ActiveStorage::VirusScanner::SAFE } + before do + attachment.blob.touch(:watermarked_at) + end it 'allows to download the file' do expect(subject).to have_link(filename) diff --git a/spec/components/attachment/multiple_component_spec.rb b/spec/components/attachment/multiple_component_spec.rb index 54b8782b4..23f585d55 100644 --- a/spec/components/attachment/multiple_component_spec.rb +++ b/spec/components/attachment/multiple_component_spec.rb @@ -47,8 +47,8 @@ RSpec.describe Attachment::MultipleComponent, type: :component do end it 'shows the Delete button by default' do - expect(subject).to have_link(title: "Supprimer le fichier #{attached_file.attachments[0].filename}") - expect(subject).to have_link(title: "Supprimer le fichier #{attached_file.attachments[1].filename}") + expect(subject).to have_button(title: "Supprimer le fichier #{attached_file.attachments[0].filename}") + expect(subject).to have_button(title: "Supprimer le fichier #{attached_file.attachments[1].filename}") end it 'renders a form field for uploading a new file' do diff --git a/spec/components/simple_format_component_spec.rb b/spec/components/simple_format_component_spec.rb new file mode 100644 index 000000000..3cc691718 --- /dev/null +++ b/spec/components/simple_format_component_spec.rb @@ -0,0 +1,73 @@ +describe SimpleFormatComponent, type: :component do + let(:allow_a) { false } + before { render_inline(described_class.new(text, allow_a: allow_a)) } + + context 'one line' do + let(:text) do + "1er paragraphe" + end + it { expect(page).to have_selector("p", count: 1, text: text) } + end + + context 'one with leading spaces' do + let(:text) do + <<-TEXT + 1er paragraphe +TEXT + end + it { expect(page).to have_selector("p", count: 1, text: text.strip) } + end + + context 'two lines' do + let(:text) do + <<~TEXT + 1er paragraphe + 2eme paragraphe + TEXT + end + + it { expect(page).to have_selector("p", count: 2) } + it { text.split("\n").map(&:strip).map { expect(page).to have_text(_1) } } + end + + context 'unordered list items' do + let(:text) do + <<~TEXT + - 1er paragraphe + - paragraphe + TEXT + end + + it { expect(page).to have_selector("ul", count: 1) } + it { expect(page).to have_selector("li", count: 2) } + end + + context 'ordered list items' do + let(:text) do + <<~TEXT + 1. 1er paragraphe + 2. paragraphe + TEXT + end + + it { expect(page).to have_selector("ol", count: 1) } + it { expect(page).to have_selector("li", count: 2) } + end + + context 'auto-link' do + let(:text) do + <<~TEXT + bonjour https://www.demarches-simplifiees.fr + TEXT + end + + context 'enabled' do + let(:allow_a) { true } + it { expect(page).to have_selector("a") } + end + + context 'disabled' do + it { expect(page).not_to have_selector("a") } + end + end +end diff --git a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb index 90e3a193f..d15f59b35 100644 --- a/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/administrateurs/groupe_instructeurs_controller_spec.rb @@ -268,11 +268,22 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do context 'of a news instructeurs' do let(:new_instructeur_emails) { ['new_i1@mail.com', 'new_i2@mail.com'] } - before { do_request } + before do + allow(GroupeInstructeurMailer).to receive(:notify_added_instructeurs) + .and_return(double(deliver_later: true)) + do_request + end it { expect(gi_1_2.instructeurs.pluck(:email)).to include(*new_instructeur_emails) } it { expect(flash.notice).to be_present } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_2)) } it { expect(procedure.routing_enabled?).to be_truthy } + it "calls GroupeInstructeurMailer with the right params" do + expect(GroupeInstructeurMailer).to have_received(:notify_added_instructeurs).with( + gi_1_2, + gi_1_2.instructeurs.last(2), + admin.email + ) + end end context 'of an instructeur already in the group' do @@ -320,11 +331,22 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do end context 'when there are many instructeurs' do - before { remove_instructeur(admin.instructeur) } + before do + allow(GroupeInstructeurMailer).to receive(:notify_removed_instructeur) + .and_return(double(deliver_later: true)) + remove_instructeur(admin.instructeur) + end it { expect(gi_1_1.instructeurs).to include(instructeur) } it { expect(gi_1_1.reload.instructeurs.count).to eq(1) } it { expect(response).to redirect_to(admin_procedure_groupe_instructeur_path(procedure, gi_1_1)) } + it "calls GroupeInstructeurMailer with the right groupe and instructeur" do + expect(GroupeInstructeurMailer).to have_received(:notify_removed_instructeur).with( + gi_1_1, + admin.instructeur, + admin.email + ) + end end context 'when there is only one instructeur' do @@ -383,6 +405,17 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do it { expect(flash.alert).to eq("Import terminé. Cependant les emails suivants ne sont pas pris en compte: kara") } end + context 'when the csv file has only one column' do + let(:csv_file) { fixture_file_upload('spec/fixtures/files/valid-instructeurs-file.csv', 'text/csv') } + + before { subject } + + it { expect { subject }.not_to raise_error } + it { expect(response.status).to eq(302) } + it { expect(flash.alert).to be_present } + it { expect(flash.alert).to eq("Importation impossible, veuillez importer un csv suivant ce modèle") } + end + context 'when the file content type is application/vnd.ms-excel' do let(:csv_file) { fixture_file_upload('spec/fixtures/files/groupe_avec_caracteres_speciaux.csv', "application/vnd.ms-excel") } @@ -395,11 +428,16 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do context 'when the content of csv contains special characters' do let(:csv_file) { fixture_file_upload('spec/fixtures/files/groupe_avec_caracteres_speciaux.csv', 'text/csv') } - before { subject } + before do + allow(GroupeInstructeurMailer).to receive(:notify_added_instructeurs) + .and_return(double(deliver_later: true)) + subject + end it { expect(procedure.groupe_instructeurs.pluck(:label)).to match_array(["Auvergne-Rhône-Alpes", "Vendée", "défaut"]) } it { expect(flash.notice).to be_present } it { expect(flash.notice).to eq("La liste des instructeurs a été importée avec succès") } + it { expect(GroupeInstructeurMailer).to have_received(:notify_added_instructeurs).twice } end context 'when the csv file length is more than 1 mo' do @@ -443,6 +481,76 @@ describe Administrateurs::GroupeInstructeursController, type: :controller do end end + describe '#add_instructeurs_via_csv_file' do + let(:procedure_non_routee) { create(:procedure, :published, :for_individual, administrateurs: [admin]) } + + subject do + post :import, params: { procedure_id: procedure_non_routee.id, instructeurs_csv_file: csv_file } + end + + context 'when the csv file is less than 1 mo and content type text/csv' do + let(:csv_file) { fixture_file_upload('spec/fixtures/files/instructeurs-file.csv', 'text/csv') } + + before do + allow(GroupeInstructeurMailer).to receive(:notify_added_instructeurs) + .and_return(double(deliver_later: true)) + subject + end + + it { expect(response.status).to eq(302) } + it { expect(procedure_non_routee.instructeurs.pluck(:email)).to match_array(["kara@beta-gouv.fr", "philippe@mail.com", "lisa@gouv.fr"]) } + it { expect(flash.alert).to be_present } + it { expect(flash.alert).to eq("Import terminé. Cependant les emails suivants ne sont pas pris en compte: eric") } + it "calls GroupeInstructeurMailer" do + expect(GroupeInstructeurMailer).to have_received(:notify_added_instructeurs).with( + procedure_non_routee.defaut_groupe_instructeur, + any_args, + admin.email + ) + end + end + + context 'when the csv file has more than one column' do + let(:csv_file) { fixture_file_upload('spec/fixtures/files/groupe-instructeur.csv', 'text/csv') } + + before { subject } + + it { expect(response.status).to eq(302) } + it { expect(flash.alert).to be_present } + it { expect(flash.alert).to eq("Importation impossible, veuillez importer un csv suivant ce modèle") } + end + + context 'when the file content type is application/vnd.ms-excel' do + let(:csv_file) { fixture_file_upload('spec/fixtures/files/valid-instructeurs-file.csv', "application/vnd.ms-excel") } + + before { subject } + it { expect(procedure_non_routee.instructeurs.pluck(:email)).to match_array(["kara@beta-gouv.fr", "philippe@mail.com", "lisa@gouv.fr"]) } + it { expect(flash.notice).to be_present } + it { expect(flash.notice).to eq("La liste des instructeurs a été importée avec succès") } + end + + context 'when the csv file length is more than 1 mo' do + let(:csv_file) { fixture_file_upload('spec/fixtures/files/groupe-instructeur.csv', 'text/csv') } + + before do + allow_any_instance_of(ActionDispatch::Http::UploadedFile).to receive(:size).and_return(3.megabytes) + subject + end + + it { expect(flash.alert).to be_present } + it { expect(flash.alert).to eq("Importation impossible : le poids du fichier est supérieur à 1 Mo") } + end + + context 'when the file content type is not accepted' do + let(:csv_file) { fixture_file_upload('spec/fixtures/files/french-flag.gif', 'image/gif') } + + before { subject } + + it { expect(flash.alert).to be_present } + it { expect(flash.alert).to eq("Importation impossible : veuillez importer un fichier CSV") } + end + end + describe '#export_groupe_instructeurs' do let(:procedure) { create(:procedure, :published) } let(:gi_1_2) { procedure.groupe_instructeurs.create(label: 'groupe instructeur 1 2') } diff --git a/spec/controllers/administrateurs/procedures_controller_spec.rb b/spec/controllers/administrateurs/procedures_controller_spec.rb index 23a598d84..b1f09c6da 100644 --- a/spec/controllers/administrateurs/procedures_controller_spec.rb +++ b/spec/controllers/administrateurs/procedures_controller_spec.rb @@ -132,6 +132,16 @@ describe Administrateurs::ProceduresController, type: :controller do expect(assigns(:procedures).any? { |p| p.id == procedure2.id }).to be_truthy expect(assigns(:procedures).any? { |p| p.id == procedure1.id }).to be_falsey end + + context "without zones" do + let!(:procedure) { create(:procedure, :published, zones: []) } + subject { get :all } + + it 'displays procedures without zones' do + subject + expect(assigns(:procedures).any? { |p| p.id == procedure.id }).to be_truthy + end + end end context 'for specific status' do diff --git a/spec/controllers/api/public/v1/dossiers_controller_spec.rb b/spec/controllers/api/public/v1/dossiers_controller_spec.rb index ac434fe44..09f85b55e 100644 --- a/spec/controllers/api/public/v1/dossiers_controller_spec.rb +++ b/spec/controllers/api/public/v1/dossiers_controller_spec.rb @@ -50,8 +50,8 @@ RSpec.describe API::Public::V1::DossiersController, type: :controller do let(:params) { { id: procedure.id, - "champ_#{type_de_champ_1.to_typed_id}" => value_1, - "champ_#{type_de_champ_2.to_typed_id}" => value_2 + "champ_#{type_de_champ_1.to_typed_id_for_query}" => value_1, + "champ_#{type_de_champ_2.to_typed_id_for_query}" => value_2 } } @@ -130,6 +130,74 @@ RSpec.describe API::Public::V1::DossiersController, type: :controller do end end + describe '#index' do + let(:procedure) { dossier.procedure } + let(:dossier) { create(:dossier, prefilled: true, user: nil) } + let(:prefill_token) { dossier.prefill_token } + let(:params) { { id: procedure.id, prefill_token: } } + + subject(:create_request) do + request.headers["Content-Type"] = "application/json" + get :index, params: + end + + let(:body) { JSON.parse(response.body).map(&:deep_symbolize_keys) } + + before { create_request } + + context 'not found' do + let(:prefill_token) { 'invalid_token' } + it 'should respond with and empty array' do + expect(response).to have_http_status(:ok) + expect(body).to eq([]) + end + end + + context 'when dossier prefilled' do + it 'should respond with dossier state' do + expect(response).to have_http_status(:ok) + expect(body.first[:state]).to eq('prefilled') + end + end + + context 'when dossier brouillon' do + let(:dossier) { create(:dossier, prefilled: true) } + it 'should respond with dossier state' do + expect(response).to have_http_status(:ok) + expect(body.first[:state]).to eq('brouillon') + end + end + + context 'when dossier en_construction' do + let(:dossier) { create(:dossier, :en_construction, prefilled: true) } + it 'should respond with dossier state' do + expect(response).to have_http_status(:ok) + expect(body.first[:state]).to eq('en_construction') + expect(body.first[:submitted_at]).to eq(dossier.depose_at.iso8601) + end + end + + context 'with multiple tokens' do + let(:dossier) { create(:dossier, prefilled: true, user: nil) } + let(:other_dossier) { create(:dossier, prefilled: true, user: nil, procedure:) } + let(:prefill_token) { [dossier.prefill_token, other_dossier.prefill_token] } + + it 'should respond with dossiers state' do + expect(response).to have_http_status(:ok) + expect(body.map { _1[:dossier_number] }).to match_array([dossier.id, other_dossier.id]) + end + + context 'comma separated tokens' do + let(:prefill_token) { [dossier.prefill_token, other_dossier.prefill_token].join(',') } + + it 'should respond with dossiers state' do + expect(response).to have_http_status(:ok) + expect(body.map { _1[:dossier_number] }).to match_array([dossier.id, other_dossier.id]) + end + end + end + end + private def find_champ_by_stable_id(dossier, stable_id) diff --git a/spec/controllers/api/v1/dossiers_controller_spec.rb b/spec/controllers/api/v1/dossiers_controller_spec.rb index af705addd..0aefa5a08 100644 --- a/spec/controllers/api/v1/dossiers_controller_spec.rb +++ b/spec/controllers/api/v1/dossiers_controller_spec.rb @@ -4,6 +4,13 @@ describe API::V1::DossiersController do let(:procedure) { create(:procedure, :with_type_de_champ, :with_type_de_champ_private, administrateur: admin) } let(:wrong_procedure) { create(:procedure) } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + end + it { expect(described_class).to be < APIController } describe 'GET index (with bearer token)' do @@ -258,7 +265,7 @@ describe API::V1::DossiersController do end end - describe 'departement' do + describe 'departement', vcr: { cassette_name: 'api_geo_departements' } do let(:procedure) { create(:procedure, :with_departement, administrateur: admin) } let(:dossier) { create(:dossier, :en_construction, :with_populated_champs, procedure: procedure) } @@ -279,9 +286,9 @@ describe API::V1::DossiersController do it 'should have rows' do expect(subject.size).to eq(2) expect(subject[0][:id]).to eq(1) - expect(subject[0][:champs].size).to eq(1) - expect(subject[0][:champs].map { |c| c[:value] }).to eq(['text']) - expect(subject[0][:champs].map { |c| c[:type_de_champ][:type_champ] }).to eq(['text']) + expect(subject[0][:champs].size).to eq(2) + expect(subject[0][:champs].map { |c| c[:value] }).to eq(['text', 42]) + expect(subject[0][:champs].map { |c| c[:type_de_champ][:type_champ] }).to eq(['text', 'integer_number']) end end end diff --git a/spec/controllers/api/v2/graphql_controller_spec.rb b/spec/controllers/api/v2/graphql_controller_spec.rb index acfe61304..6f70743cd 100644 --- a/spec/controllers/api/v2/graphql_controller_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_spec.rb @@ -42,7 +42,6 @@ describe API::V2::GraphqlController do allow(Rails).to receive(:cache).and_return(memory_store) Rails.cache.clear - allow(APIGeoService).to receive(:departement_name).with('01').and_return('Ain') instructeur.assign_to_procedure(procedure) end @@ -397,7 +396,7 @@ describe API::V2::GraphqlController do dossier end - context "for individual", vcr: { cassette_name: 'api_geo_regions' } do + context "for individual", vcr: { cassette_name: 'api_geo_all' } do let(:procedure) { create(:procedure, :published, :for_individual, :with_service, :with_all_champs, :with_all_annotations, administrateurs: [admin]) } let(:query) do "{ diff --git a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb index 3e065f0de..ae6749376 100644 --- a/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb +++ b/spec/controllers/api/v2/graphql_controller_stored_queries_spec.rb @@ -138,6 +138,7 @@ describe API::V2::GraphqlController do it { expect(gql_errors).to be_nil expect(gql_data[:demarcheDescriptor][:id]).to eq(procedure.to_typed_id) + expect(gql_data[:demarcheDescriptor][:demarcheUrl]).to match("commencer/#{procedure.path}") } end diff --git a/spec/controllers/champs/rna_controller_spec.rb b/spec/controllers/champs/rna_controller_spec.rb index 6354769f2..0f845ce4f 100644 --- a/spec/controllers/champs/rna_controller_spec.rb +++ b/spec/controllers/champs/rna_controller_spec.rb @@ -37,10 +37,8 @@ describe Champs::RNAController, type: :controller do subject! { get :show, params: params, format: :turbo_stream } - it 'clears the data and value on the model' do - champ.reload - expect(champ.data).to eq({}) - expect(champ.value).to eq("") + it 'clears the data on the model' do + expect(champ.reload.data).to be_nil end it 'clears any information or error message' do @@ -55,14 +53,12 @@ describe Champs::RNAController, type: :controller do subject! { get :show, params: params, format: :turbo_stream } - it 'clears the data and value on the model' do - champ.reload - expect(champ.data).to be_nil - expect(champ.value).to be_nil + it 'clears the data on the model' do + expect(champ.reload.data).to be_nil end it 'displays a “RNA is invalid” error message' do - expect(response.body).to include("Aucun établissement trouvé") + expect(response.body).to include("Le numéro RNA doit commencer par un W majuscule suivi de 9 chiffres") end end @@ -75,7 +71,7 @@ describe Champs::RNAController, type: :controller do it 'clears the data on the model' do champ.reload - expect(champ.data).to eq({}) + expect(champ.data).to be_nil end it 'displays a “RNA is invalid” error message' do @@ -94,10 +90,8 @@ describe Champs::RNAController, type: :controller do subject! { get :show, params: params, format: :turbo_stream } - it 'clears the data and value on the model' do - champ.reload - expect(champ.data).to be_nil - expect(champ.value).to be_nil + it 'clears the data on the model' do + expect(champ.reload.data).to be_nil end it 'displays a “API is unavailable” error message' do diff --git a/spec/controllers/champs/siret_controller_spec.rb b/spec/controllers/champs/siret_controller_spec.rb index daee4b3da..2b1f5f42a 100644 --- a/spec/controllers/champs/siret_controller_spec.rb +++ b/spec/controllers/champs/siret_controller_spec.rb @@ -31,6 +31,8 @@ describe Champs::SiretController, type: :controller do sign_in user stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/) .to_return(status: api_etablissement_status, body: api_etablissement_body) + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siret[0..8]}/) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/entreprises.json')) allow_any_instance_of(APIEntrepriseToken).to receive(:roles) .and_return(["attestations_fiscales", "attestations_sociales", "bilans_entreprise_bdf"]) allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(token_expired) @@ -39,10 +41,8 @@ describe Champs::SiretController, type: :controller do context 'when the SIRET is empty' do subject! { get :show, params: params, format: :turbo_stream } - it 'clears the etablissement and SIRET on the model' do - champ.reload - expect(champ.etablissement).to be_nil - expect(champ.value).to be_empty + it 'clears the etablissement on the model' do + expect(champ.reload.etablissement).to be_nil end it 'clears any information or error message' do @@ -50,15 +50,13 @@ describe Champs::SiretController, type: :controller do end end - context 'when the SIRET is invalid' do + context "when the SIRET is invalid because of it's length" do let(:siret) { '1234' } subject! { get :show, params: params, format: :turbo_stream } - it 'clears the etablissement and SIRET on the model' do - champ.reload - expect(champ.etablissement).to be_nil - expect(champ.value).to be_empty + it 'clears the etablissement on the model' do + expect(champ.reload.etablissement).to be_nil end it 'displays a “SIRET is invalid” error message' do @@ -66,6 +64,20 @@ describe Champs::SiretController, type: :controller do end end + context "when the SIRET is invalid because of it's checksum" do + let(:siret) { '82812345600023' } + + subject! { get :show, params: params, format: :turbo_stream } + + it 'clears the etablissement on the model' do + expect(champ.reload.etablissement).to be_nil + end + + it 'displays a “SIRET is invalid” error message' do + expect(response.body).to include('Le format du numéro de SIRET est invalide.') + end + end + context 'when the API is unavailable due to network error' do let(:siret) { '82161143100015' } let(:api_etablissement_status) { 503 } @@ -76,10 +88,8 @@ describe Champs::SiretController, type: :controller do subject! { get :show, params: params, format: :turbo_stream } - it 'clears the etablissement and SIRET on the model' do - champ.reload - expect(champ.etablissement).to be_nil - expect(champ.value).to be_empty + it 'clears the etablissement on the model' do + expect(champ.reload.etablissement).to be_nil end it 'displays a “API is unavailable” error message' do @@ -115,10 +125,8 @@ describe Champs::SiretController, type: :controller do subject! { get :show, params: params, format: :turbo_stream } - it 'clears the etablissement and SIRET on the model' do - champ.reload - expect(champ.etablissement).to be_nil - expect(champ.value).to be_empty + it 'clears the etablissement on the model' do + expect(champ.reload.etablissement).to be_nil end it 'displays a “SIRET not found” error message' do diff --git a/spec/controllers/instructeurs/dossiers_controller_spec.rb b/spec/controllers/instructeurs/dossiers_controller_spec.rb index 990b3a29c..b6e1af036 100644 --- a/spec/controllers/instructeurs/dossiers_controller_spec.rb +++ b/spec/controllers/instructeurs/dossiers_controller_spec.rb @@ -792,7 +792,7 @@ describe Instructeurs::DossiersController, type: :controller do champs_private_attributes: { '0': { id: champ_multiple_drop_down_list.id, - value: ['', 'un', 'deux'] + value: ['', 'val1', 'val2'] }, '1': { id: champ_datetime.id, @@ -813,7 +813,7 @@ describe Instructeurs::DossiersController, type: :controller do end it { - expect(champ_multiple_drop_down_list.value).to eq('["un", "deux"]') + expect(champ_multiple_drop_down_list.value).to eq('["val1", "val2"]') expect(champ_linked_drop_down_list.primary_value).to eq('primary') expect(champ_linked_drop_down_list.secondary_value).to eq('secondary') expect(champ_datetime.value).to eq('2019-12-21T13:17:00+01:00') @@ -839,7 +839,7 @@ describe Instructeurs::DossiersController, type: :controller do champs_public_attributes: { '0': { id: champ_multiple_drop_down_list.id, - value: ['', 'un', 'deux'] + value: ['', 'val1', 'val2'] } } } diff --git a/spec/controllers/password_complexity_controller_spec.rb b/spec/controllers/password_complexity_controller_spec.rb index b04985806..6d03cab40 100644 --- a/spec/controllers/password_complexity_controller_spec.rb +++ b/spec/controllers/password_complexity_controller_spec.rb @@ -27,8 +27,7 @@ describe PasswordComplexityController, type: :controller do it 'renders Javascript that updates the password complexity meter' do subject - expect(response.body).to include('complexity-label') - expect(response.body).to include('complexity-bar') + expect(response.body).to include('Mot de passe vulnérable') end end end diff --git a/spec/controllers/prefill_descriptions_controller_spec.rb b/spec/controllers/prefill_descriptions_controller_spec.rb index 23318e43d..5521b3ae3 100644 --- a/spec/controllers/prefill_descriptions_controller_spec.rb +++ b/spec/controllers/prefill_descriptions_controller_spec.rb @@ -67,24 +67,19 @@ describe PrefillDescriptionsController, type: :controller do it { expect(response).to render_template(:update) } it "includes the prefill URL" do + type_de_champ_value = I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}") + type_de_champ_to_add_value = I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ_to_add.type_champ}") expect(response.body).to include(commencer_path(path: procedure.path)) - expect(response.body).to include( - { - "champ_#{type_de_champ.to_typed_id}" => I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}") - }.to_query - ) - expect(response.body).to include({ - "champ_#{type_de_champ_to_add.to_typed_id}" => I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ_to_add.type_champ}") - }.to_query) + expect(response.body).to include("champ_#{type_de_champ.to_typed_id_for_query}=#{type_de_champ_value}") + expect(response.body).to include("champ_#{type_de_champ_to_add.to_typed_id_for_query}=#{type_de_champ_to_add_value}") end it "includes the prefill query" do type_de_champ_value = I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}") type_de_champ_to_add_value = I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ_to_add.type_champ}") - expect(response.body).to include(api_public_v1_dossiers_path(procedure)) expect(response.body).to include( - ""champ_#{type_de_champ.to_typed_id}": "#{type_de_champ_value}", "champ_#{type_de_champ_to_add.to_typed_id}": "#{type_de_champ_to_add_value}"" + ""champ_#{type_de_champ.to_typed_id_for_query}":"#{type_de_champ_value}","champ_#{type_de_champ_to_add.to_typed_id_for_query}":"#{type_de_champ_to_add_value}"" ) end end @@ -96,13 +91,11 @@ describe PrefillDescriptionsController, type: :controller do it { expect(response).to render_template(:update) } it "includes the prefill URL" do + type_de_champ_value = I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}") + type_de_champ_to_remove_value = I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ_to_remove.type_champ}") expect(response.body).to include(commencer_path(path: procedure.path)) - expect(response.body).to include({ - "champ_#{type_de_champ.to_typed_id}" => I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}") - }.to_query) - expect(response.body).not_to include({ - "champ_#{type_de_champ_to_remove.to_typed_id}" => I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ_to_remove.type_champ}") - }.to_query) + expect(response.body).to include("champ_#{type_de_champ.to_typed_id_for_query}=#{type_de_champ_value}") + expect(response.body).not_to include("champ_#{type_de_champ_to_remove.to_typed_id_for_query}=#{type_de_champ_to_remove_value}") end it "includes the prefill query" do @@ -111,10 +104,10 @@ describe PrefillDescriptionsController, type: :controller do expect(response.body).to include(api_public_v1_dossiers_path(procedure)) expect(response.body).to include( - ""champ_#{type_de_champ.to_typed_id}": "#{type_de_champ_value}"" + ""champ_#{type_de_champ.to_typed_id_for_query}":"#{type_de_champ_value}"" ) expect(response.body).not_to include( - ""champ_#{type_de_champ_to_remove.to_typed_id}": "#{type_de_champ_to_remove_value}"" + ""champ_#{type_de_champ_to_remove.to_typed_id_for_query}":"#{type_de_champ_to_remove_value}"" ) end end diff --git a/spec/controllers/prefill_type_de_champs_controller_spec.rb b/spec/controllers/prefill_type_de_champs_controller_spec.rb index a4b61f19c..5e13be629 100644 --- a/spec/controllers/prefill_type_de_champs_controller_spec.rb +++ b/spec/controllers/prefill_type_de_champs_controller_spec.rb @@ -8,6 +8,8 @@ RSpec.describe PrefillTypeDeChampsController, type: :controller do context 'when the procedure is found' do context 'when the procedure is publiee' do context 'when the procedure is opendata' do + render_views + let(:procedure) { create(:procedure, :published, opendata: true) } it { expect(show_request).to render_template(:show) } diff --git a/spec/controllers/users/dossiers_controller_spec.rb b/spec/controllers/users/dossiers_controller_spec.rb index fd100a5fc..d428d6e9f 100644 --- a/spec/controllers/users/dossiers_controller_spec.rb +++ b/spec/controllers/users/dossiers_controller_spec.rb @@ -196,6 +196,8 @@ describe Users::DossiersController, type: :controller do sign_in(user) stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/) .to_return(status: api_etablissement_status, body: api_etablissement_body) + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siren}/) + .to_return(body: File.read('spec/fixtures/files/api_entreprise/entreprises.json'), status: 200) allow_any_instance_of(APIEntrepriseToken).to receive(:roles) .and_return(["attestations_fiscales", "attestations_sociales", "bilans_entreprise_bdf"]) allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(token_expired) @@ -254,7 +256,7 @@ describe Users::DossiersController, type: :controller do context 'When API-Entreprise is globally down' do let(:api_etablissement_status) { 502 } - let(:api_current_status_response) { File.read('spec/fixtures/files/api_entreprise/current_status.json').tr('200', '502') } + let(:api_current_status_response) { File.read('spec/fixtures/files/api_entreprise/current_status.json').gsub('200', '502') } it "create an etablissement only with SIRET as degraded mode" do dossier.reload diff --git a/spec/factories/champ.rb b/spec/factories/champ.rb index 253247a8e..ca13c4325 100644 --- a/spec/factories/champ.rb +++ b/spec/factories/champ.rb @@ -97,7 +97,7 @@ FactoryBot.define do factory :champ_multiple_drop_down_list, class: 'Champs::MultipleDropDownListChamp' do type_de_champ { association :type_de_champ_multiple_drop_down_list, procedure: dossier.procedure } - value { '["choix 1", "choix 2"]' } + value { '["val1", "val2"]' } end factory :champ_linked_drop_down_list, class: 'Champs::LinkedDropDownListChamp' do @@ -122,7 +122,9 @@ FactoryBot.define do factory :champ_communes, class: 'Champs::CommuneChamp' do type_de_champ { association :type_de_champ_communes, procedure: dossier.procedure } - value { 'Paris' } + value { 'Coye-la-Forêt (60580)' } + value_json { { "departement" => "Oise", "code_departement" => "60" } } + external_id { "60172" } end factory :champ_epci, class: 'Champs::EpciChamp' do diff --git a/spec/factories/procedure.rb b/spec/factories/procedure.rb index 2d700d783..263af1076 100644 --- a/spec/factories/procedure.rb +++ b/spec/factories/procedure.rb @@ -206,6 +206,12 @@ FactoryBot.define do end end + trait :with_region do + after(:build) do |procedure, _evaluator| + build(:type_de_champ_regions, procedure: procedure) + end + end + trait :with_piece_justificative do after(:build) do |procedure, _evaluator| build(:type_de_champ_piece_justificative, procedure: procedure) diff --git a/spec/factories/type_de_champ.rb b/spec/factories/type_de_champ.rb index 1b90fa19f..f538ef10f 100644 --- a/spec/factories/type_de_champ.rb +++ b/spec/factories/type_de_champ.rb @@ -198,6 +198,16 @@ FactoryBot.define do parent = revision.revision_types_de_champ.find { |rtdc| rtdc.type_de_champ == type_de_champ_repetition } build(:type_de_champ, procedure: evaluator.procedure, libelle: 'sub type de champ', parent: parent, position: 0) + build(:type_de_champ, type_champ: TypeDeChamp.type_champs.fetch(:integer_number), procedure: evaluator.procedure, libelle: 'sub type de champ2', parent: parent, position: 1) + end + end + + trait :with_region_types_de_champ do + after(:build) do |type_de_champ_repetition, evaluator| + revision = evaluator.procedure.active_revision + parent = revision.revision_types_de_champ.find { |rtdc| rtdc.type_de_champ == type_de_champ_repetition } + + build(:type_de_champ, type_champ: TypeDeChamp.type_champs.fetch(:regions), procedure: evaluator.procedure, libelle: 'region sub_champ', parent: parent, position: 10) end end end diff --git a/spec/fixtures/cassettes/api_geo_all.yml b/spec/fixtures/cassettes/api_geo_all.yml index 6ddbae52e..cd3eca4b0 100644 --- a/spec/fixtures/cassettes/api_geo_all.yml +++ b/spec/fixtures/cassettes/api_geo_all.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://geo.api.gouv.fr/departements + uri: https://geo.api.gouv.fr/departements?zone=metro,drom,com body: encoding: US-ASCII string: '' @@ -19,25 +19,25 @@ http_interactions: Server: - nginx/1.10.3 (Ubuntu) Date: - - Tue, 20 Dec 2022 22:45:29 GMT + - Wed, 22 Feb 2023 14:57:34 GMT Content-Type: - application/json; charset=utf-8 Content-Length: - - '5083' + - '5598' Vary: - Accept-Encoding - Origin X-Powered-By: - Express Etag: - - W/"13db-vyFtsKQWjHylHAtBtjYjxCAjMHE" + - W/"15de-6bFFP56x7MMvD1C1JvzSgcLzQWw" Strict-Transport-Security: - max-age=15552000 body: encoding: ASCII-8BIT string: !binary |- - W3sibm9tIjoiQWluIiwiY29kZSI6IjAxIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBaXNuZSIsImNvZGUiOiIwMiIsImNvZGVSZWdpb24iOiIzMiJ9LHsibm9tIjoiQWxsaWVyIiwiY29kZSI6IjAzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBbHBlcy1kZS1IYXV0ZS1Qcm92ZW5jZSIsImNvZGUiOiIwNCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiSGF1dGVzLUFscGVzIiwiY29kZSI6IjA1IiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJBbHBlcy1NYXJpdGltZXMiLCJjb2RlIjoiMDYiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkFyZMOoY2hlIiwiY29kZSI6IjA3IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBcmRlbm5lcyIsImNvZGUiOiIwOCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXJpw6hnZSIsImNvZGUiOiIwOSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXViZSIsImNvZGUiOiIxMCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXVkZSIsImNvZGUiOiIxMSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXZleXJvbiIsImNvZGUiOiIxMiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQm91Y2hlcy1kdS1SaMO0bmUiLCJjb2RlIjoiMTMiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkNhbHZhZG9zIiwiY29kZSI6IjE0IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJDYW50YWwiLCJjb2RlIjoiMTUiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkNoYXJlbnRlIiwiY29kZSI6IjE2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJDaGFyZW50ZS1NYXJpdGltZSIsImNvZGUiOiIxNyIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiQ2hlciIsImNvZGUiOiIxOCIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiQ29ycsOoemUiLCJjb2RlIjoiMTkiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkPDtHRlLWQnT3IiLCJjb2RlIjoiMjEiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IkPDtHRlcy1kJ0FybW9yIiwiY29kZSI6IjIyIiwiY29kZVJlZ2lvbiI6IjUzIn0seyJub20iOiJDcmV1c2UiLCJjb2RlIjoiMjMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkRvcmRvZ25lIiwiY29kZSI6IjI0IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJEb3VicyIsImNvZGUiOiIyNSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRHLDtG1lIiwiY29kZSI6IjI2IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJFdXJlIiwiY29kZSI6IjI3IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJFdXJlLWV0LUxvaXIiLCJjb2RlIjoiMjgiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkZpbmlzdMOocmUiLCJjb2RlIjoiMjkiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkNvcnNlLWR1LVN1ZCIsImNvZGUiOiIyQSIsImNvZGVSZWdpb24iOiI5NCJ9LHsibm9tIjoiSGF1dGUtQ29yc2UiLCJjb2RlIjoiMkIiLCJjb2RlUmVnaW9uIjoiOTQifSx7Im5vbSI6IkdhcmQiLCJjb2RlIjoiMzAiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkhhdXRlLUdhcm9ubmUiLCJjb2RlIjoiMzEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkdlcnMiLCJjb2RlIjoiMzIiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ikdpcm9uZGUiLCJjb2RlIjoiMzMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkjDqXJhdWx0IiwiY29kZSI6IjM0IiwiY29kZVJlZ2lvbiI6Ijc2In0seyJub20iOiJJbGxlLWV0LVZpbGFpbmUiLCJjb2RlIjoiMzUiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkluZHJlIiwiY29kZSI6IjM2IiwiY29kZVJlZ2lvbiI6IjI0In0seyJub20iOiJJbmRyZS1ldC1Mb2lyZSIsImNvZGUiOiIzNyIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiSXPDqHJlIiwiY29kZSI6IjM4IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJKdXJhIiwiY29kZSI6IjM5IiwiY29kZVJlZ2lvbiI6IjI3In0seyJub20iOiJMYW5kZXMiLCJjb2RlIjoiNDAiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvaXItZXQtQ2hlciIsImNvZGUiOiI0MSIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiTG9pcmUiLCJjb2RlIjoiNDIiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLUxvaXJlIiwiY29kZSI6IjQzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJMb2lyZS1BdGxhbnRpcXVlIiwiY29kZSI6IjQ0IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJMb2lyZXQiLCJjb2RlIjoiNDUiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkxvdCIsImNvZGUiOiI0NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiTG90LWV0LUdhcm9ubmUiLCJjb2RlIjoiNDciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvesOocmUiLCJjb2RlIjoiNDgiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ik1haW5lLWV0LUxvaXJlIiwiY29kZSI6IjQ5IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJNYW5jaGUiLCJjb2RlIjoiNTAiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6Ik1hcm5lIiwiY29kZSI6IjUxIiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJIYXV0ZS1NYXJuZSIsImNvZGUiOiI1MiIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTWF5ZW5uZSIsImNvZGUiOiI1MyIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiTWV1cnRoZS1ldC1Nb3NlbGxlIiwiY29kZSI6IjU0IiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJNZXVzZSIsImNvZGUiOiI1NSIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTW9yYmloYW4iLCJjb2RlIjoiNTYiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6Ik1vc2VsbGUiLCJjb2RlIjoiNTciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6Ik5pw6h2cmUiLCJjb2RlIjoiNTgiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6Ik5vcmQiLCJjb2RlIjoiNTkiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9pc2UiLCJjb2RlIjoiNjAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9ybmUiLCJjb2RlIjoiNjEiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6IlBhcy1kZS1DYWxhaXMiLCJjb2RlIjoiNjIiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlB1eS1kZS1Ew7RtZSIsImNvZGUiOiI2MyIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUHlyw6luw6llcy1BdGxhbnRpcXVlcyIsImNvZGUiOiI2NCIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiSGF1dGVzLVB5csOpbsOpZXMiLCJjb2RlIjoiNjUiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlB5csOpbsOpZXMtT3JpZW50YWxlcyIsImNvZGUiOiI2NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQmFzLVJoaW4iLCJjb2RlIjoiNjciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6IkhhdXQtUmhpbiIsImNvZGUiOiI2OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiUmjDtG5lIiwiY29kZSI6IjY5IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJIYXV0ZS1TYcO0bmUiLCJjb2RlIjoiNzAiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlNhw7RuZS1ldC1Mb2lyZSIsImNvZGUiOiI3MSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiU2FydGhlIiwiY29kZSI6IjcyIiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJTYXZvaWUiLCJjb2RlIjoiNzMiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLVNhdm9pZSIsImNvZGUiOiI3NCIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUGFyaXMiLCJjb2RlIjoiNzUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLU1hcml0aW1lIiwiY29kZSI6Ijc2IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJTZWluZS1ldC1NYXJuZSIsImNvZGUiOiI3NyIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiWXZlbGluZXMiLCJjb2RlIjoiNzgiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IkRldXgtU8OodnJlcyIsImNvZGUiOiI3OSIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiU29tbWUiLCJjb2RlIjoiODAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlRhcm4iLCJjb2RlIjoiODEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlRhcm4tZXQtR2Fyb25uZSIsImNvZGUiOiI4MiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiVmFyIiwiY29kZSI6IjgzIiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJWYXVjbHVzZSIsImNvZGUiOiI4NCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiVmVuZMOpZSIsImNvZGUiOiI4NSIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiVmllbm5lIiwiY29kZSI6Ijg2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJIYXV0ZS1WaWVubmUiLCJjb2RlIjoiODciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IlZvc2dlcyIsImNvZGUiOiI4OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiWW9ubmUiLCJjb2RlIjoiODkiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlRlcnJpdG9pcmUgZGUgQmVsZm9ydCIsImNvZGUiOiI5MCIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRXNzb25uZSIsImNvZGUiOiI5MSIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiSGF1dHMtZGUtU2VpbmUiLCJjb2RlIjoiOTIiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLVNhaW50LURlbmlzIiwiY29kZSI6IjkzIiwiY29kZVJlZ2lvbiI6IjExIn0seyJub20iOiJWYWwtZGUtTWFybmUiLCJjb2RlIjoiOTQiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlZhbC1kJ09pc2UiLCJjb2RlIjoiOTUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6Ikd1YWRlbG91cGUiLCJjb2RlIjoiOTcxIiwiY29kZVJlZ2lvbiI6IjAxIn0seyJub20iOiJNYXJ0aW5pcXVlIiwiY29kZSI6Ijk3MiIsImNvZGVSZWdpb24iOiIwMiJ9LHsibm9tIjoiR3V5YW5lIiwiY29kZSI6Ijk3MyIsImNvZGVSZWdpb24iOiIwMyJ9LHsibm9tIjoiTGEgUsOpdW5pb24iLCJjb2RlIjoiOTc0IiwiY29kZVJlZ2lvbiI6IjA0In0seyJub20iOiJNYXlvdHRlIiwiY29kZSI6Ijk3NiIsImNvZGVSZWdpb24iOiIwNiJ9XQ== - recorded_at: Tue, 20 Dec 2022 22:45:29 GMT + W3sibm9tIjoiQWluIiwiY29kZSI6IjAxIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBaXNuZSIsImNvZGUiOiIwMiIsImNvZGVSZWdpb24iOiIzMiJ9LHsibm9tIjoiQWxsaWVyIiwiY29kZSI6IjAzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBbHBlcy1kZS1IYXV0ZS1Qcm92ZW5jZSIsImNvZGUiOiIwNCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiSGF1dGVzLUFscGVzIiwiY29kZSI6IjA1IiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJBbHBlcy1NYXJpdGltZXMiLCJjb2RlIjoiMDYiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkFyZMOoY2hlIiwiY29kZSI6IjA3IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBcmRlbm5lcyIsImNvZGUiOiIwOCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXJpw6hnZSIsImNvZGUiOiIwOSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXViZSIsImNvZGUiOiIxMCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXVkZSIsImNvZGUiOiIxMSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXZleXJvbiIsImNvZGUiOiIxMiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQm91Y2hlcy1kdS1SaMO0bmUiLCJjb2RlIjoiMTMiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkNhbHZhZG9zIiwiY29kZSI6IjE0IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJDYW50YWwiLCJjb2RlIjoiMTUiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkNoYXJlbnRlIiwiY29kZSI6IjE2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJDaGFyZW50ZS1NYXJpdGltZSIsImNvZGUiOiIxNyIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiQ2hlciIsImNvZGUiOiIxOCIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiQ29ycsOoemUiLCJjb2RlIjoiMTkiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkPDtHRlLWQnT3IiLCJjb2RlIjoiMjEiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IkPDtHRlcy1kJ0FybW9yIiwiY29kZSI6IjIyIiwiY29kZVJlZ2lvbiI6IjUzIn0seyJub20iOiJDcmV1c2UiLCJjb2RlIjoiMjMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkRvcmRvZ25lIiwiY29kZSI6IjI0IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJEb3VicyIsImNvZGUiOiIyNSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRHLDtG1lIiwiY29kZSI6IjI2IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJFdXJlIiwiY29kZSI6IjI3IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJFdXJlLWV0LUxvaXIiLCJjb2RlIjoiMjgiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkZpbmlzdMOocmUiLCJjb2RlIjoiMjkiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkNvcnNlLWR1LVN1ZCIsImNvZGUiOiIyQSIsImNvZGVSZWdpb24iOiI5NCJ9LHsibm9tIjoiSGF1dGUtQ29yc2UiLCJjb2RlIjoiMkIiLCJjb2RlUmVnaW9uIjoiOTQifSx7Im5vbSI6IkdhcmQiLCJjb2RlIjoiMzAiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkhhdXRlLUdhcm9ubmUiLCJjb2RlIjoiMzEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkdlcnMiLCJjb2RlIjoiMzIiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ikdpcm9uZGUiLCJjb2RlIjoiMzMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkjDqXJhdWx0IiwiY29kZSI6IjM0IiwiY29kZVJlZ2lvbiI6Ijc2In0seyJub20iOiJJbGxlLWV0LVZpbGFpbmUiLCJjb2RlIjoiMzUiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkluZHJlIiwiY29kZSI6IjM2IiwiY29kZVJlZ2lvbiI6IjI0In0seyJub20iOiJJbmRyZS1ldC1Mb2lyZSIsImNvZGUiOiIzNyIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiSXPDqHJlIiwiY29kZSI6IjM4IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJKdXJhIiwiY29kZSI6IjM5IiwiY29kZVJlZ2lvbiI6IjI3In0seyJub20iOiJMYW5kZXMiLCJjb2RlIjoiNDAiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvaXItZXQtQ2hlciIsImNvZGUiOiI0MSIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiTG9pcmUiLCJjb2RlIjoiNDIiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLUxvaXJlIiwiY29kZSI6IjQzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJMb2lyZS1BdGxhbnRpcXVlIiwiY29kZSI6IjQ0IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJMb2lyZXQiLCJjb2RlIjoiNDUiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkxvdCIsImNvZGUiOiI0NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiTG90LWV0LUdhcm9ubmUiLCJjb2RlIjoiNDciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvesOocmUiLCJjb2RlIjoiNDgiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ik1haW5lLWV0LUxvaXJlIiwiY29kZSI6IjQ5IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJNYW5jaGUiLCJjb2RlIjoiNTAiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6Ik1hcm5lIiwiY29kZSI6IjUxIiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJIYXV0ZS1NYXJuZSIsImNvZGUiOiI1MiIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTWF5ZW5uZSIsImNvZGUiOiI1MyIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiTWV1cnRoZS1ldC1Nb3NlbGxlIiwiY29kZSI6IjU0IiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJNZXVzZSIsImNvZGUiOiI1NSIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTW9yYmloYW4iLCJjb2RlIjoiNTYiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6Ik1vc2VsbGUiLCJjb2RlIjoiNTciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6Ik5pw6h2cmUiLCJjb2RlIjoiNTgiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6Ik5vcmQiLCJjb2RlIjoiNTkiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9pc2UiLCJjb2RlIjoiNjAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9ybmUiLCJjb2RlIjoiNjEiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6IlBhcy1kZS1DYWxhaXMiLCJjb2RlIjoiNjIiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlB1eS1kZS1Ew7RtZSIsImNvZGUiOiI2MyIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUHlyw6luw6llcy1BdGxhbnRpcXVlcyIsImNvZGUiOiI2NCIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiSGF1dGVzLVB5csOpbsOpZXMiLCJjb2RlIjoiNjUiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlB5csOpbsOpZXMtT3JpZW50YWxlcyIsImNvZGUiOiI2NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQmFzLVJoaW4iLCJjb2RlIjoiNjciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6IkhhdXQtUmhpbiIsImNvZGUiOiI2OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiUmjDtG5lIiwiY29kZSI6IjY5IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJIYXV0ZS1TYcO0bmUiLCJjb2RlIjoiNzAiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlNhw7RuZS1ldC1Mb2lyZSIsImNvZGUiOiI3MSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiU2FydGhlIiwiY29kZSI6IjcyIiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJTYXZvaWUiLCJjb2RlIjoiNzMiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLVNhdm9pZSIsImNvZGUiOiI3NCIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUGFyaXMiLCJjb2RlIjoiNzUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLU1hcml0aW1lIiwiY29kZSI6Ijc2IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJTZWluZS1ldC1NYXJuZSIsImNvZGUiOiI3NyIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiWXZlbGluZXMiLCJjb2RlIjoiNzgiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IkRldXgtU8OodnJlcyIsImNvZGUiOiI3OSIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiU29tbWUiLCJjb2RlIjoiODAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlRhcm4iLCJjb2RlIjoiODEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlRhcm4tZXQtR2Fyb25uZSIsImNvZGUiOiI4MiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiVmFyIiwiY29kZSI6IjgzIiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJWYXVjbHVzZSIsImNvZGUiOiI4NCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiVmVuZMOpZSIsImNvZGUiOiI4NSIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiVmllbm5lIiwiY29kZSI6Ijg2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJIYXV0ZS1WaWVubmUiLCJjb2RlIjoiODciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IlZvc2dlcyIsImNvZGUiOiI4OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiWW9ubmUiLCJjb2RlIjoiODkiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlRlcnJpdG9pcmUgZGUgQmVsZm9ydCIsImNvZGUiOiI5MCIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRXNzb25uZSIsImNvZGUiOiI5MSIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiSGF1dHMtZGUtU2VpbmUiLCJjb2RlIjoiOTIiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLVNhaW50LURlbmlzIiwiY29kZSI6IjkzIiwiY29kZVJlZ2lvbiI6IjExIn0seyJub20iOiJWYWwtZGUtTWFybmUiLCJjb2RlIjoiOTQiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlZhbC1kJ09pc2UiLCJjb2RlIjoiOTUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6Ikd1YWRlbG91cGUiLCJjb2RlIjoiOTcxIiwiY29kZVJlZ2lvbiI6IjAxIn0seyJub20iOiJNYXJ0aW5pcXVlIiwiY29kZSI6Ijk3MiIsImNvZGVSZWdpb24iOiIwMiJ9LHsibm9tIjoiR3V5YW5lIiwiY29kZSI6Ijk3MyIsImNvZGVSZWdpb24iOiIwMyJ9LHsibm9tIjoiTGEgUsOpdW5pb24iLCJjb2RlIjoiOTc0IiwiY29kZVJlZ2lvbiI6IjA0In0seyJub20iOiJNYXlvdHRlIiwiY29kZSI6Ijk3NiIsImNvZGVSZWdpb24iOiIwNiJ9LHsibm9tIjoiU2FpbnQtUGllcnJlLWV0LU1pcXVlbG9uIiwiY29kZSI6Ijk3NSIsImNvZGVSZWdpb24iOiI5NzUifSx7Im5vbSI6IlNhaW50LUJhcnRow6lsZW15IiwiY29kZSI6Ijk3NyIsImNvZGVSZWdpb24iOiI5NzcifSx7Im5vbSI6IlNhaW50LU1hcnRpbiIsImNvZGUiOiI5NzgiLCJjb2RlUmVnaW9uIjoiOTc4In0seyJub20iOiJUZXJyZXMgYXVzdHJhbGVzIGV0IGFudGFyY3RpcXVlcyBmcmFuw6dhaXNlcyIsImNvZGUiOiI5ODQiLCJjb2RlUmVnaW9uIjoiOTg0In0seyJub20iOiJXYWxsaXMgZXQgRnV0dW5hIiwiY29kZSI6Ijk4NiIsImNvZGVSZWdpb24iOiI5ODYifSx7Im5vbSI6IlBvbHluw6lzaWUgZnJhbsOnYWlzZSIsImNvZGUiOiI5ODciLCJjb2RlUmVnaW9uIjoiOTg3In0seyJub20iOiJOb3V2ZWxsZS1DYWzDqWRvbmllIiwiY29kZSI6Ijk4OCIsImNvZGVSZWdpb24iOiI5ODgifSx7Im5vbSI6IsOObGUgZGUgQ2xpcHBlcnRvbiIsImNvZGUiOiI5ODkiLCJjb2RlUmVnaW9uIjoiOTg5In1d + recorded_at: Wed, 22 Feb 2023 14:57:34 GMT - request: method: get uri: https://geo.api.gouv.fr/regions @@ -57,7 +57,7 @@ http_interactions: Server: - nginx/1.10.3 (Ubuntu) Date: - - Tue, 20 Dec 2022 22:45:29 GMT + - Wed, 22 Feb 2023 14:57:34 GMT Content-Type: - application/json; charset=utf-8 Content-Length: @@ -75,5 +75,5 @@ http_interactions: encoding: ASCII-8BIT string: !binary |- W3sibm9tIjoiw45sZS1kZS1GcmFuY2UiLCJjb2RlIjoiMTEifSx7Im5vbSI6IkNlbnRyZS1WYWwgZGUgTG9pcmUiLCJjb2RlIjoiMjQifSx7Im5vbSI6IkJvdXJnb2duZS1GcmFuY2hlLUNvbXTDqSIsImNvZGUiOiIyNyJ9LHsibm9tIjoiTm9ybWFuZGllIiwiY29kZSI6IjI4In0seyJub20iOiJIYXV0cy1kZS1GcmFuY2UiLCJjb2RlIjoiMzIifSx7Im5vbSI6IkdyYW5kIEVzdCIsImNvZGUiOiI0NCJ9LHsibm9tIjoiUGF5cyBkZSBsYSBMb2lyZSIsImNvZGUiOiI1MiJ9LHsibm9tIjoiQnJldGFnbmUiLCJjb2RlIjoiNTMifSx7Im5vbSI6Ik5vdXZlbGxlLUFxdWl0YWluZSIsImNvZGUiOiI3NSJ9LHsibm9tIjoiT2NjaXRhbmllIiwiY29kZSI6Ijc2In0seyJub20iOiJBdXZlcmduZS1SaMO0bmUtQWxwZXMiLCJjb2RlIjoiODQifSx7Im5vbSI6IlByb3ZlbmNlLUFscGVzLUPDtHRlIGQnQXp1ciIsImNvZGUiOiI5MyJ9LHsibm9tIjoiQ29yc2UiLCJjb2RlIjoiOTQifSx7Im5vbSI6Ikd1YWRlbG91cGUiLCJjb2RlIjoiMDEifSx7Im5vbSI6Ik1hcnRpbmlxdWUiLCJjb2RlIjoiMDIifSx7Im5vbSI6Ikd1eWFuZSIsImNvZGUiOiIwMyJ9LHsibm9tIjoiTGEgUsOpdW5pb24iLCJjb2RlIjoiMDQifSx7Im5vbSI6Ik1heW90dGUiLCJjb2RlIjoiMDYifV0= - recorded_at: Tue, 20 Dec 2022 22:45:29 GMT + recorded_at: Wed, 22 Feb 2023 14:57:34 GMT recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/cassettes/api_geo_communes.yml b/spec/fixtures/cassettes/api_geo_communes.yml new file mode 100644 index 000000000..63b76d100 --- /dev/null +++ b/spec/fixtures/cassettes/api_geo_communes.yml @@ -0,0 +1,41 @@ +--- +http_interactions: +- request: + method: get + uri: https://geo.api.gouv.fr/communes?codeDepartement=01 + body: + encoding: US-ASCII + string: '' + headers: + User-Agent: + - demarches-simplifiees.fr + Expect: + - '' + response: + status: + code: 200 + message: '' + headers: + Server: + - nginx/1.10.3 (Ubuntu) + Date: + - Thu, 23 Feb 2023 13:33:37 GMT + Content-Type: + - application/json; charset=utf-8 + Content-Length: + - '64499' + Vary: + - Accept-Encoding + - Origin + X-Powered-By: + - Express + Etag: + - W/"fbf3-p2dPt1CmfK+zLSteTNObEX45Yk8" + Strict-Transport-Security: + - max-age=15552000 + body: + encoding: ASCII-8BIT + string: !binary |- + W3sibm9tIjoiTCdBYmVyZ2VtZW50LUNsw6ltZW5jaWF0IiwiY29kZSI6IjAxMDAxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDAxMiIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDAwIl0sInBvcHVsYXRpb24iOjc3OX0seyJub20iOiJMJ0FiZXJnZW1lbnQtZGUtVmFyZXkiLCJjb2RlIjoiMDEwMDIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMDIwIiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE2NDAiXSwicG9wdWxhdGlvbiI6MjU2fSx7Im5vbSI6IkFtYsOpcmlldS1lbi1CdWdleSIsImNvZGUiOiIwMTAwNCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAwNDYiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTUwMCJdLCJwb3B1bGF0aW9uIjoxNDEzNH0seyJub20iOiJBbWLDqXJpZXV4LWVuLURvbWJlcyIsImNvZGUiOiIwMTAwNSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAwNTMiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMzMCJdLCJwb3B1bGF0aW9uIjoxNzUxfSx7Im5vbSI6IkFtYmzDqW9uIiwiY29kZSI6IjAxMDA2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDA2MSIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjExMn0seyJub20iOiJBbWJyb25heSIsImNvZGUiOiIwMTAwNyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAwNzkiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTUwMCJdLCJwb3B1bGF0aW9uIjoyODAwfSx7Im5vbSI6IkFtYnV0cml4IiwiY29kZSI6IjAxMDA4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDA4NyIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTAwIl0sInBvcHVsYXRpb24iOjc2Mn0seyJub20iOiJBbmRlcnQtZXQtQ29uZG9uIiwiY29kZSI6IjAxMDA5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDA5NSIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjMyNn0seyJub20iOiJBbmdsZWZvcnQiLCJjb2RlIjoiMDEwMTAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMTAzIiwiY29kZUVwY2kiOiIyMDAwNzA4NTIiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNTAiXSwicG9wdWxhdGlvbiI6MTEwNX0seyJub20iOiJBcHJlbW9udCIsImNvZGUiOiIwMTAxMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAxMTEiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEwMCJdLCJwb3B1bGF0aW9uIjozNjh9LHsibm9tIjoiQXJhbmMiLCJjb2RlIjoiMDEwMTIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMTI5IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMTAiXSwicG9wdWxhdGlvbiI6MzI5fSx7Im5vbSI6IkFyYW5kYXMiLCJjb2RlIjoiMDEwMTMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMTM3IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyMzAiXSwicG9wdWxhdGlvbiI6MTQzfSx7Im5vbSI6IkFyYmVudCIsImNvZGUiOiIwMTAxNCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAxNDUiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEwMCJdLCJwb3B1bGF0aW9uIjozMzQ5fSx7Im5vbSI6IkFyYm95cyBlbiBCdWdleSIsImNvZGUiOiIwMTAxNSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwNTM2NjgiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjo2NTB9LHsibm9tIjoiQXJiaWdueSIsImNvZGUiOiIwMTAxNiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAxNjAiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE5MCJdLCJwb3B1bGF0aW9uIjo0NjB9LHsibm9tIjoiQXJnaXMiLCJjb2RlIjoiMDEwMTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMTc4IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyMzAiXSwicG9wdWxhdGlvbiI6NDU3fSx7Im5vbSI6IkFybWl4IiwiY29kZSI6IjAxMDE5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDE5NCIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTEwIl0sInBvcHVsYXRpb24iOjI3fSx7Im5vbSI6IkFycy1zdXItRm9ybWFucyIsImNvZGUiOiIwMTAyMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAyMTAiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ4MCJdLCJwb3B1bGF0aW9uIjoxNDU3fSx7Im5vbSI6IkFydGVtYXJlIiwiY29kZSI6IjAxMDIyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDIyOCIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTEwIl0sInBvcHVsYXRpb24iOjEyNDl9LHsibm9tIjoiQXNuacOocmVzLXN1ci1TYcO0bmUiLCJjb2RlIjoiMDEwMjMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMjM2IiwiY29kZUVwY2kiOiIyMDAwNzEzNzEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1NzAiXSwicG9wdWxhdGlvbiI6NzZ9LHsibm9tIjoiQXR0aWduYXQiLCJjb2RlIjoiMDEwMjQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMjQ0IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNDAiXSwicG9wdWxhdGlvbiI6MzI1MX0seyJub20iOiJCw6Jnw6ktRG9tbWFydGluIiwiY29kZSI6IjAxMDI1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIwMDA3NzIyMCIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzgwIl0sInBvcHVsYXRpb24iOjQwNjV9LHsibm9tIjoiQsOiZ8OpLWxlLUNow6J0ZWwiLCJjb2RlIjoiMDEwMjYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMjY5IiwiY29kZUVwY2kiOiIyMDAwNzEzNzEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzODAiXSwicG9wdWxhdGlvbiI6OTczfSx7Im5vbSI6IkJhbGFuIiwiY29kZSI6IjAxMDI3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDI3NyIsImNvZGVFcGNpIjoiMjQwMTAwNjEwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzYwIl0sInBvcHVsYXRpb24iOjI1NjN9LHsibm9tIjoiQmFuZWlucyIsImNvZGUiOiIwMTAyOCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAyODUiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTk5MCJdLCJwb3B1bGF0aW9uIjo2MTd9LHsibm9tIjoiQmVhdXBvbnQiLCJjb2RlIjoiMDEwMjkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMjkzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNzAiXSwicG9wdWxhdGlvbiI6Njg1fSx7Im5vbSI6IkJlYXVyZWdhcmQiLCJjb2RlIjoiMDEwMzAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMzAxIiwiY29kZUVwY2kiOiIyMDAwNDI0OTciLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0ODAiXSwicG9wdWxhdGlvbiI6ODI5fSx7Im5vbSI6IkJlbGxpZ25hdCIsImNvZGUiOiIwMTAzMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAzMTkiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEwMCJdLCJwb3B1bGF0aW9uIjozNjUyfSx7Im5vbSI6IkLDqWxpZ25ldXgiLCJjb2RlIjoiMDEwMzIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMzI3IiwiY29kZUVwY2kiOiIyNDAxMDA2MTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNjAiXSwicG9wdWxhdGlvbiI6MzM3Nn0seyJub20iOiJWYWxzZXJow7RuZSIsImNvZGUiOiIwMTAzMyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwODM4NjMiLCJjb2RlRXBjaSI6IjI0MDEwMDg5MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIwMCJdLCJwb3B1bGF0aW9uIjoxNjM3OH0seyJub20iOiJCZWxsZXkiLCJjb2RlIjoiMDEwMzQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMzQzIiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6OTE4Mn0seyJub20iOiJCZWxsZXlkb3V4IiwiY29kZSI6IjAxMDM1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDM1MCIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTMwIl0sInBvcHVsYXRpb24iOjMxMH0seyJub20iOiJWYWxyb21leS1zdXItU8OpcmFuIiwiY29kZSI6IjAxMDM2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIwMDA4NjQxMSIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjYwIl0sInBvcHVsYXRpb24iOjEzMjZ9LHsibm9tIjoiQsOpbm9uY2VzIiwiY29kZSI6IjAxMDM3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDM3NiIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDcwIl0sInBvcHVsYXRpb24iOjMwOH0seyJub20iOiJCw6lueSIsImNvZGUiOiIwMTAzOCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDAzODQiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM3MCJdLCJwb3B1bGF0aW9uIjo3NTR9LHsibm9tIjoiQsOpb24iLCJjb2RlIjoiMDEwMzkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwMzkyIiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNTAiXSwicG9wdWxhdGlvbiI6NDQzfSx7Im5vbSI6IkLDqXLDqXppYXQiLCJjb2RlIjoiMDEwNDAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNDAwIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNDAiXSwicG9wdWxhdGlvbiI6NDg2fSx7Im5vbSI6IkJldHRhbnQiLCJjb2RlIjoiMDEwNDEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNDE4IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1MDAiXSwicG9wdWxhdGlvbiI6NzQ4fSx7Im5vbSI6IkJleSIsImNvZGUiOiIwMTA0MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA0MjYiLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI5MCJdLCJwb3B1bGF0aW9uIjoyOTF9LHsibm9tIjoiQmV5bm9zdCIsImNvZGUiOiIwMTA0MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA0MzQiLCJjb2RlRXBjaSI6IjI0MDEwMDgwMCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTcwMCJdLCJwb3B1bGF0aW9uIjo0NzU0fSx7Im5vbSI6IkJpbGxpYXQiLCJjb2RlIjoiMDEwNDQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNDQyIiwiY29kZUVwY2kiOiIyNDAxMDA4OTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyMDAiXSwicG9wdWxhdGlvbiI6NjMyfSx7Im5vbSI6IkJpcmlldXgiLCJjb2RlIjoiMDEwNDUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNDU5IiwiY29kZUVwY2kiOiIyMDAwNjkxOTMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMzAiXSwicG9wdWxhdGlvbiI6MjgzfSx7Im5vbSI6IkJpemlhdCIsImNvZGUiOiIwMTA0NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA0NjciLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI5MCJdLCJwb3B1bGF0aW9uIjo4OTV9LHsibm9tIjoiQmx5ZXMiLCJjb2RlIjoiMDEwNDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNDc1IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNTAiXSwicG9wdWxhdGlvbiI6MTE4Mn0seyJub20iOiJMYSBCb2lzc2UiLCJjb2RlIjoiMDEwNDkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNDkxIiwiY29kZUVwY2kiOiIyNDAxMDA2MTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMjAiXSwicG9wdWxhdGlvbiI6MzI4M30seyJub20iOiJCb2lzc2V5IiwiY29kZSI6IjAxMDUwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDUwOSIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTkwIiwiMDEzODAiXSwicG9wdWxhdGlvbiI6MzY1fSx7Im5vbSI6IkJvbG96b24iLCJjb2RlIjoiMDEwNTEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNTE3IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NTAiXSwicG9wdWxhdGlvbiI6OTF9LHsibm9tIjoiQm91bGlnbmV1eCIsImNvZGUiOiIwMTA1MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA1MjUiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMzMCJdLCJwb3B1bGF0aW9uIjozMjh9LHsibm9tIjoiQm91cmctZW4tQnJlc3NlIiwiY29kZSI6IjAxMDUzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDUzMyIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMDAwIl0sInBvcHVsYXRpb24iOjQxMTExfSx7Im5vbSI6IkJvdXJnLVNhaW50LUNocmlzdG9waGUiLCJjb2RlIjoiMDEwNTQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNTQxIiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE4MDAiXSwicG9wdWxhdGlvbiI6MTQ1N30seyJub20iOiJCb3lldXgtU2FpbnQtSsOpcsO0bWUiLCJjb2RlIjoiMDEwNTYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNTY2IiwiY29kZUVwY2kiOiIyMDAwMjk5OTkiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE2NDAiXSwicG9wdWxhdGlvbiI6MzQ5fSx7Im5vbSI6IkJveiIsImNvZGUiOiIwMTA1NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA1NzQiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE5MCJdLCJwb3B1bGF0aW9uIjo1MTl9LHsibm9tIjoiQnLDqWduaWVyLUNvcmRvbiIsImNvZGUiOiIwMTA1OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA1ODIiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjo4Mjh9LHsibm9tIjoiQnLDqW5vZCIsImNvZGUiOiIwMTA2MCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA2MDgiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTExMCJdLCJwb3B1bGF0aW9uIjo0OTN9LHsibm9tIjoiQnJlbnMiLCJjb2RlIjoiMDEwNjEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNjE2IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6MTE0NH0seyJub20iOiJCcmVzc29sbGVzIiwiY29kZSI6IjAxMDYyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDYyNCIsImNvZGVFcGNpIjoiMjQwMTAwNjEwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzYwIl0sInBvcHVsYXRpb24iOjkzNH0seyJub20iOiJCcmlvbiIsImNvZGUiOiIwMTA2MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA2MzIiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ2MCJdLCJwb3B1bGF0aW9uIjo1MzN9LHsibm9tIjoiQnJpb3JkIiwiY29kZSI6IjAxMDY0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDY0MCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDcwIl0sInBvcHVsYXRpb24iOjEwNzB9LHsibm9tIjoiQnVlbGxhcyIsImNvZGUiOiIwMTA2NSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA2NTciLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMxMCJdLCJwb3B1bGF0aW9uIjoxODEyfSx7Im5vbSI6IkxhIEJ1cmJhbmNoZSIsImNvZGUiOiIwMTA2NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA2NjUiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTUxMCJdLCJwb3B1bGF0aW9uIjo4OX0seyJub20iOiJDZWlnbmVzIiwiY29kZSI6IjAxMDY3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDY3MyIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDMwIl0sInBvcHVsYXRpb24iOjI1NX0seyJub20iOiJDZXJkb24iLCJjb2RlIjoiMDEwNjgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNjgxIiwiY29kZUVwY2kiOiIyMDAwMjk5OTkiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NTAiXSwicG9wdWxhdGlvbiI6Nzc1fSx7Im5vbSI6IkNlcnRpbmVzIiwiY29kZSI6IjAxMDY5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDY5OSIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjQwIl0sInBvcHVsYXRpb24iOjE1MDN9LHsibm9tIjoiQ2Vzc3kiLCJjb2RlIjoiMDEwNzEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwNzE1IiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNzAiXSwicG9wdWxhdGlvbiI6NDgwOH0seyJub20iOiJDZXl6w6lyaWF0IiwiY29kZSI6IjAxMDcyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDcyMyIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjUwIl0sInBvcHVsYXRpb24iOjMxOTR9LHsibm9tIjoiQ2V5esOpcmlldSIsImNvZGUiOiIwMTA3MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA3MzEiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM1MCJdLCJwb3B1bGF0aW9uIjoxMDAwfSx7Im5vbSI6IkNoYWxhbW9udCIsImNvZGUiOiIwMTA3NCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA3NDkiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMyMCJdLCJwb3B1bGF0aW9uIjoyNTg5fSx7Im5vbSI6IkNoYWxlaW5zIiwiY29kZSI6IjAxMDc1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDc1NiIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDgwIl0sInBvcHVsYXRpb24iOjEzODJ9LHsibm9tIjoiQ2hhbGV5IiwiY29kZSI6IjAxMDc2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDc2NCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjMwIl0sInBvcHVsYXRpb24iOjEyNX0seyJub20iOiJDaGFsbGVzLWxhLU1vbnRhZ25lIiwiY29kZSI6IjAxMDc3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDc3MiIsImNvZGVFcGNpIjoiMjAwMDI5OTk5IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDUwIl0sInBvcHVsYXRpb24iOjE5Nn0seyJub20iOiJDaGFsbGV4IiwiY29kZSI6IjAxMDc4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDc4MCIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjMwIl0sInBvcHVsYXRpb24iOjE1NTd9LHsibm9tIjoiQ2hhbXBhZ25lLWVuLVZhbHJvbWV5IiwiY29kZSI6IjAxMDc5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDc5OCIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjYwIl0sInBvcHVsYXRpb24iOjgxMX0seyJub20iOiJDaGFtcGRvci1Db3JjZWxsZXMiLCJjb2RlIjoiMDEwODAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjAwMDU2Njk1IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMTAiXSwicG9wdWxhdGlvbiI6NjQzfSx7Im5vbSI6IkNoYW1wZnJvbWllciIsImNvZGUiOiIwMTA4MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA4MTQiLCJjb2RlRXBjaSI6IjI0MDEwMDg5MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQxMCJdLCJwb3B1bGF0aW9uIjo3MzV9LHsibm9tIjoiQ2hhbmF5IiwiY29kZSI6IjAxMDgyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDgyMiIsImNvZGVFcGNpIjoiMjQwMTAwODkxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDIwIl0sInBvcHVsYXRpb24iOjYyOH0seyJub20iOiJDaGFuZWlucyIsImNvZGUiOiIwMTA4MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA4MzAiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTk5MCJdLCJwb3B1bGF0aW9uIjo5Mjd9LHsibm9tIjoiQ2hhbm96LUNow6J0ZW5heSIsImNvZGUiOiIwMTA4NCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA4NDgiLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQwMCJdLCJwb3B1bGF0aW9uIjo4OTV9LHsibm9tIjoiTGEgQ2hhcGVsbGUtZHUtQ2jDonRlbGFyZCIsImNvZGUiOiIwMTA4NSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA4NTUiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI0MCJdLCJwb3B1bGF0aW9uIjo0MDB9LHsibm9tIjoiQ2hhcml4IiwiY29kZSI6IjAxMDg3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDg3MSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTMwIl0sInBvcHVsYXRpb24iOjI4Mn0seyJub20iOiJDaGFybm96LXN1ci1BaW4iLCJjb2RlIjoiMDEwODgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAwODg5IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE4MDAiXSwicG9wdWxhdGlvbiI6OTAzfSx7Im5vbSI6IkNow6J0ZWF1LUdhaWxsYXJkIiwiY29kZSI6IjAxMDg5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDg5NyIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTAwIl0sInBvcHVsYXRpb24iOjIyNTN9LHsibm9tIjoiQ2jDonRlbmF5IiwiY29kZSI6IjAxMDkwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDkwNSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzIwIl0sInBvcHVsYXRpb24iOjM1Nn0seyJub20iOiJDaMOidGlsbG9uLWxhLVBhbHVkIiwiY29kZSI6IjAxMDkyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDkyMSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzIwIl0sInBvcHVsYXRpb24iOjE2MDR9LHsibm9tIjoiQ2jDonRpbGxvbi1zdXItQ2hhbGFyb25uZSIsImNvZGUiOiIwMTA5MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDA5MzkiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQwMCJdLCJwb3B1bGF0aW9uIjo0OTMxfSx7Im5vbSI6IkNoYXZhbm5lcy1zdXItUmV5c3NvdXplIiwiY29kZSI6IjAxMDk0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDk0NyIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTkwIl0sInBvcHVsYXRpb24iOjc2MX0seyJub20iOiJOaXZpZ25lIGV0IFN1cmFuIiwiY29kZSI6IjAxMDk1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIwMDA2MjUzNyIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjUwIl0sInBvcHVsYXRpb24iOjg1M30seyJub20iOiJDaGF2ZXlyaWF0IiwiY29kZSI6IjAxMDk2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDk2MiIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjYwIl0sInBvcHVsYXRpb24iOjEwMzR9LHsibm9tIjoiQ2hhemV5LUJvbnMiLCJjb2RlIjoiMDEwOTgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjAwMDYzMTIxIiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiLCIwMTUxMCJdLCJwb3B1bGF0aW9uIjoxMTU5fSx7Im5vbSI6IkNoYXpleS1zdXItQWluIiwiY29kZSI6IjAxMDk5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMDk5NiIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTUwIl0sInBvcHVsYXRpb24iOjE1NjZ9LHsibm9tIjoiQ2hlaWduaWV1LWxhLUJhbG1lIiwiY29kZSI6IjAxMTAwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTAwMiIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTEwIl0sInBvcHVsYXRpb24iOjEzMX0seyJub20iOiJDaGV2aWxsYXJkIiwiY29kZSI6IjAxMTAxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTAxMCIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDMwIl0sInBvcHVsYXRpb24iOjE1Mn0seyJub20iOiJDaGV2cm91eCIsImNvZGUiOiIwMTEwMiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDEwMjgiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE5MCJdLCJwb3B1bGF0aW9uIjo5NjF9LHsibm9tIjoiQ2hldnJ5IiwiY29kZSI6IjAxMTAzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTAzNiIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTcwIl0sInBvcHVsYXRpb24iOjIxNDl9LHsibm9tIjoiQ2jDqXplcnktRm9yZW5zIiwiY29kZSI6IjAxMTA0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTA0NCIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjAwIiwiMDE0MTAiXSwicG9wdWxhdGlvbiI6NDYxfSx7Im5vbSI6IkNpdnJpZXV4IiwiY29kZSI6IjAxMTA1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTA1MSIsImNvZGVFcGNpIjoiMjAwMDQyNDk3IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzkwIl0sInBvcHVsYXRpb24iOjE4Mjl9LHsibm9tIjoiQ2l6ZSIsImNvZGUiOiIwMTEwNiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDEwNjkiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjoxNzl9LHsibm9tIjoiQ2xleXppZXUiLCJjb2RlIjoiMDExMDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMDc3IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyMzAiXSwicG9wdWxhdGlvbiI6MTMzfSx7Im5vbSI6IkNvbGlnbnkiLCJjb2RlIjoiMDExMDgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMDg1IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNzAiXSwicG9wdWxhdGlvbiI6MTE3Nn0seyJub20iOiJDb2xsb25nZXMiLCJjb2RlIjoiMDExMDkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMDkzIiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1NTAiXSwicG9wdWxhdGlvbiI6MjIyNn0seyJub20iOiJDb2xvbWlldSIsImNvZGUiOiIwMTExMCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDExMDEiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjoxNTZ9LHsibm9tIjoiQ29uYW5kIiwiY29kZSI6IjAxMTExIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTExOSIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjMwIl0sInBvcHVsYXRpb24iOjEzMn0seyJub20iOiJDb25kYW1pbmUiLCJjb2RlIjoiMDExMTIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMTI3IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MzAiXSwicG9wdWxhdGlvbiI6NDUwfSx7Im5vbSI6IkNvbmRlaXNzaWF0IiwiY29kZSI6IjAxMTEzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTEzNSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDAwIl0sInBvcHVsYXRpb24iOjgxN30seyJub20iOiJDb25mb3J0IiwiY29kZSI6IjAxMTE0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTE0MyIsImNvZGVFcGNpIjoiMjQwMTAwODkxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjAwIl0sInBvcHVsYXRpb24iOjY2N30seyJub20iOiJDb25mcmFuw6dvbiIsImNvZGUiOiIwMTExNSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDExNTAiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMxMCJdLCJwb3B1bGF0aW9uIjoxMzMzfSx7Im5vbSI6IkNvbnRyZXZveiIsImNvZGUiOiIwMTExNiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDExNjgiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjo0OTh9LHsibm9tIjoiQ29uemlldSIsImNvZGUiOiIwMTExNyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDExNzYiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjoxNTF9LHsibm9tIjoiQ29yYm9ub2QiLCJjb2RlIjoiMDExMTgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMTg0IiwiY29kZUVwY2kiOiIyMDAwNzA4NTIiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MjAiXSwicG9wdWxhdGlvbiI6MTI3OH0seyJub20iOiJDb3JsaWVyIiwiY29kZSI6IjAxMTIxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTIxOCIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTEwIl0sInBvcHVsYXRpb24iOjExNn0seyJub20iOiJDb3Jtb3JhbmNoZS1zdXItU2HDtG5lIiwiY29kZSI6IjAxMTIzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTIzNCIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjkwIl0sInBvcHVsYXRpb24iOjExNjJ9LHsibm9tIjoiQ29ybW96IiwiY29kZSI6IjAxMTI0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTI0MiIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTYwIl0sInBvcHVsYXRpb24iOjY4OX0seyJub20iOiJDb3J2ZWlzc2lhdCIsImNvZGUiOiIwMTEyNSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDEyNTkiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjo2MTJ9LHsibm9tIjoiQ291cm1hbmdvdXgiLCJjb2RlIjoiMDExMjciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMjc1IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNzAiXSwicG9wdWxhdGlvbiI6NTA4fSx7Im5vbSI6IkNvdXJ0ZXMiLCJjb2RlIjoiMDExMjgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMjgzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1NjAiXSwicG9wdWxhdGlvbiI6MzA1fSx7Im5vbSI6IkNyYW5zIiwiY29kZSI6IjAxMTI5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTI5MSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzIwIl0sInBvcHVsYXRpb24iOjI5MX0seyJub20iOiJCcmVzc2UgVmFsbG9ucyIsImNvZGUiOiIwMTEzMCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwODY5NjUiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM0MCJdLCJwb3B1bGF0aW9uIjoyMzYzfSx7Im5vbSI6IkNyZXNzaW4tUm9jaGVmb3J0IiwiY29kZSI6IjAxMTMzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTMzMyIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzUwIl0sInBvcHVsYXRpb24iOjM5NH0seyJub20iOiJDcm90dGV0IiwiY29kZSI6IjAxMTM0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTM0MSIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjkwIiwiMDE3NTAiXSwicG9wdWxhdGlvbiI6MTc1OH0seyJub20iOiJDcm96ZXQiLCJjb2RlIjoiMDExMzUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMzU4IiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNzAiXSwicG9wdWxhdGlvbiI6MjE3MX0seyJub20iOiJDcnV6aWxsZXMtbMOocy1Nw6lwaWxsYXQiLCJjb2RlIjoiMDExMzYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxMzY2IiwiY29kZUVwY2kiOiIyMDAwNzA1NTUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyOTAiXSwicG9wdWxhdGlvbiI6ODkzfSx7Im5vbSI6IkN1bG96IiwiY29kZSI6IjAxMTM4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTM4MiIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzUwIl0sInBvcHVsYXRpb24iOjMwMDR9LHsibm9tIjoiQ3VyY2lhdC1Eb25nYWxvbiIsImNvZGUiOiIwMTEzOSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDEzOTAiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU2MCJdLCJwb3B1bGF0aW9uIjo0NDR9LHsibm9tIjoiQ3VydGFmb25kIiwiY29kZSI6IjAxMTQwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTQwOCIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzEwIl0sInBvcHVsYXRpb24iOjc2N30seyJub20iOiJDdXppZXUiLCJjb2RlIjoiMDExNDEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNDE2IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6NDUzfSx7Im5vbSI6IkRhZ25ldXgiLCJjb2RlIjoiMDExNDIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNDI0IiwiY29kZUVwY2kiOiIyNDAxMDA2MTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMjAiXSwicG9wdWxhdGlvbiI6NDc3N30seyJub20iOiJEaXZvbm5lLWxlcy1CYWlucyIsImNvZGUiOiIwMTE0MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE0MzIiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIyMCJdLCJwb3B1bGF0aW9uIjo5OTUxfSx7Im5vbSI6IkRvbXBpZXJyZS1zdXItVmV5bGUiLCJjb2RlIjoiMDExNDUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNDU3IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNDAiXSwicG9wdWxhdGlvbiI6MTE3OX0seyJub20iOiJEb21waWVycmUtc3VyLUNoYWxhcm9ubmUiLCJjb2RlIjoiMDExNDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNDY1IiwiY29kZUVwY2kiOiIyMDAwNjkxOTMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MDAiXSwicG9wdWxhdGlvbiI6NDM2fSx7Im5vbSI6IkRvbXN1cmUiLCJjb2RlIjoiMDExNDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNDczIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNzAiXSwicG9wdWxhdGlvbiI6NDkyfSx7Im5vbSI6IkRvcnRhbiIsImNvZGUiOiIwMTE0OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE0ODEiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU5MCJdLCJwb3B1bGF0aW9uIjoxODc5fSx7Im5vbSI6IkRvdXZyZXMiLCJjb2RlIjoiMDExNDkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNDk5IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1MDAiXSwicG9wdWxhdGlvbiI6MTA3Nn0seyJub20iOiJEcm9tIiwiY29kZSI6IjAxMTUwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTUwNyIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjUwIl0sInBvcHVsYXRpb24iOjIxNH0seyJub20iOiJEcnVpbGxhdCIsImNvZGUiOiIwMTE1MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE1MTUiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE2MCJdLCJwb3B1bGF0aW9uIjoxMTQ2fSx7Im5vbSI6IsOJY2hhbGxvbiIsImNvZGUiOiIwMTE1MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE1MjMiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEzMCJdLCJwb3B1bGF0aW9uIjo3NDh9LHsibm9tIjoiw4ljaGVuZXZleCIsImNvZGUiOiIwMTE1MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE1MzEiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE3MCJdLCJwb3B1bGF0aW9uIjoyMTE1fSx7Im5vbSI6IsOJdm9zZ2VzIiwiY29kZSI6IjAxMTU1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTU1NiIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjMwIl0sInBvcHVsYXRpb24iOjE0M30seyJub20iOiJGYXJhbWFucyIsImNvZGUiOiIwMTE1NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE1NjQiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTgwMCJdLCJwb3B1bGF0aW9uIjo4MTV9LHsibm9tIjoiRmFyZWlucyIsImNvZGUiOiIwMTE1NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE1NzIiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ4MCJdLCJwb3B1bGF0aW9uIjoyMjUwfSx7Im5vbSI6IkZhcmdlcyIsImNvZGUiOiIwMTE1OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE1ODAiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU1MCJdLCJwb3B1bGF0aW9uIjoxMDQ4fSx7Im5vbSI6IkZlaWxsZW5zIiwiY29kZSI6IjAxMTU5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTU5OCIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTcwIl0sInBvcHVsYXRpb24iOjMzNjV9LHsibm9tIjoiRmVybmV5LVZvbHRhaXJlIiwiY29kZSI6IjAxMTYwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTYwNiIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjEwIl0sInBvcHVsYXRpb24iOjEwMDI2fSx7Im5vbSI6IkZsYXhpZXUiLCJjb2RlIjoiMDExNjIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNjIyIiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNTAiXSwicG9wdWxhdGlvbiI6NjV9LHsibm9tIjoiRm9pc3NpYXQiLCJjb2RlIjoiMDExNjMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNjMwIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNDAiXSwicG9wdWxhdGlvbiI6MjAzOH0seyJub20iOiJGcmFuY2hlbGVpbnMiLCJjb2RlIjoiMDExNjUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNjU1IiwiY29kZUVwY2kiOiIyMDAwNzAxMTgiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEwOTAiXSwicG9wdWxhdGlvbiI6MTU5NH0seyJub20iOiJGcmFucyIsImNvZGUiOiIwMTE2NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE2NjMiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ4MCJdLCJwb3B1bGF0aW9uIjoyNDIzfSx7Im5vbSI6Ikdhcm5lcmFucyIsImNvZGUiOiIwMTE2NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE2NzEiLCJjb2RlRXBjaSI6IjIwMDA3MDExOCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE0MCJdLCJwb3B1bGF0aW9uIjo2MzR9LHsibm9tIjoiR2Vub3VpbGxldXgiLCJjb2RlIjoiMDExNjkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNjk3IiwiY29kZUVwY2kiOiIyMDAwNzAxMTgiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEwOTAiXSwicG9wdWxhdGlvbiI6NjQxfSx7Im5vbSI6IkLDqWFyZC1Hw6lvdnJlaXNzaWF0IiwiY29kZSI6IjAxMTcwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTcwNSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDYwIl0sInBvcHVsYXRpb24iOjEwNTN9LHsibm9tIjoiR8Opb3ZyZWlzc2V0IiwiY29kZSI6IjAxMTcxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTcxMyIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTAwIl0sInBvcHVsYXRpb24iOjg0NH0seyJub20iOiJHZXgiLCJjb2RlIjoiMDExNzMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNzM5IiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNzAiXSwicG9wdWxhdGlvbiI6MTMxMjF9LHsibm9tIjoiR2lyb24iLCJjb2RlIjoiMDExNzQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNzQ3IiwiY29kZUVwY2kiOiIyNDAxMDA4OTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMzAiXSwicG9wdWxhdGlvbiI6MTc4fSx7Im5vbSI6IkdvcnJldm9kIiwiY29kZSI6IjAxMTc1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTc1NCIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTkwIl0sInBvcHVsYXRpb24iOjc4NH0seyJub20iOiJHcmFuZC1Db3JlbnQiLCJjb2RlIjoiMDExNzciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxNzcwIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNTAiXSwicG9wdWxhdGlvbiI6MTgzfSx7Im5vbSI6IkdyacOoZ2VzIiwiY29kZSI6IjAxMTc5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTc5NiIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjkwIl0sInBvcHVsYXRpb24iOjE4ODF9LHsibm9tIjoiR3JpbGx5IiwiY29kZSI6IjAxMTgwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTgwNCIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjIwIl0sInBvcHVsYXRpb24iOjgzOX0seyJub20iOiJHcm9pc3NpYXQiLCJjb2RlIjoiMDExODEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxODEyIiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMDAiXSwicG9wdWxhdGlvbiI6MTI0MX0seyJub20iOiJHdcOpcmVpbnMiLCJjb2RlIjoiMDExODMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxODM4IiwiY29kZUVwY2kiOiIyMDAwNzAxMTgiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEwOTAiXSwicG9wdWxhdGlvbiI6MTQ3OX0seyJub20iOiJIYXV0ZWNvdXJ0LVJvbWFuw6hjaGUiLCJjb2RlIjoiMDExODQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxODQ2IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNTAiXSwicG9wdWxhdGlvbiI6NzYzfSx7Im5vbSI6IlBsYXRlYXUgZCdIYXV0ZXZpbGxlIiwiY29kZSI6IjAxMTg1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIwMDA4NjEyMiIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTEwIl0sInBvcHVsYXRpb24iOjQ4NDR9LHsibm9tIjoiSGF1dCBWYWxyb21leSIsImNvZGUiOiIwMTE4NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwNTM2NzYiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI2MCJdLCJwb3B1bGF0aW9uIjo2MTh9LHsibm9tIjoiSWxsaWF0IiwiY29kZSI6IjAxMTg4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTg4NyIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTQwIl0sInBvcHVsYXRpb24iOjY2M30seyJub20iOiJJbmpvdXgtR8Opbmlzc2lhdCIsImNvZGUiOiIwMTE4OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE4OTUiLCJjb2RlRXBjaSI6IjI0MDEwMDg5MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIwMCJdLCJwb3B1bGF0aW9uIjoxMTE1fSx7Im5vbSI6Iklubmltb25kIiwiY29kZSI6IjAxMTkwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTkwMyIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjgwIl0sInBvcHVsYXRpb24iOjg3fSx7Im5vbSI6Ikl6ZW5hdmUiLCJjb2RlIjoiMDExOTEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxOTExIiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MzAiXSwicG9wdWxhdGlvbiI6MTU0fSx7Im5vbSI6Ikl6ZXJub3JlIiwiY29kZSI6IjAxMTkyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTkyOSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTgwIl0sInBvcHVsYXRpb24iOjIyNzd9LHsibm9tIjoiSXppZXUiLCJjb2RlIjoiMDExOTMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxOTM3IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6MjIyfSx7Im5vbSI6Ikphc3NhbnMtUmlvdHRpZXIiLCJjb2RlIjoiMDExOTQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxOTQ1IiwiY29kZUVwY2kiOiIyMDAwNDA1OTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0ODAiXSwicG9wdWxhdGlvbiI6NjM0Nn0seyJub20iOiJKYXNzZXJvbiIsImNvZGUiOiIwMTE5NSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE5NTIiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjoxNzgzfSx7Im5vbSI6IkpheWF0IiwiY29kZSI6IjAxMTk2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTk2MCIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzQwIl0sInBvcHVsYXRpb24iOjEyMjZ9LHsibm9tIjoiSm91cm5hbnMiLCJjb2RlIjoiMDExOTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAxOTc4IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNTAiXSwicG9wdWxhdGlvbiI6MzUxfSx7Im5vbSI6IkpveWV1eCIsImNvZGUiOiIwMTE5OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDE5ODYiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTgwMCJdLCJwb3B1bGF0aW9uIjoyNjh9LHsibm9tIjoiSnVqdXJpZXV4IiwiY29kZSI6IjAxMTk5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMTk5NCIsImNvZGVFcGNpIjoiMjAwMDI5OTk5IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjQwIl0sInBvcHVsYXRpb24iOjIxMzd9LHsibm9tIjoiTGFiYWxtZSIsImNvZGUiOiIwMTIwMCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDIwMDAiLCJjb2RlRXBjaSI6IjIwMDAyOTk5OSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ1MCJdLCJwb3B1bGF0aW9uIjoyMTR9LHsibm9tIjoiTGFnbmlldSIsImNvZGUiOiIwMTIwMiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDIwMjYiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE1MCJdLCJwb3B1bGF0aW9uIjo3MTYxfSx7Im5vbSI6IkxhaXoiLCJjb2RlIjoiMDEyMDMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMDM0IiwiY29kZUVwY2kiOiIyMDAwNzA1NTUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyOTAiXSwicG9wdWxhdGlvbiI6MTMxM30seyJub20iOiJMZSBQb2l6YXQtTGFsbGV5cmlhdCIsImNvZGUiOiIwMTIwNCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwNTMzOTUiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEzMCJdLCJwb3B1bGF0aW9uIjo3MzV9LHsibm9tIjoiTGFudGVuYXkiLCJjb2RlIjoiMDEyMDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMDY3IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MzAiXSwicG9wdWxhdGlvbiI6Mjc3fSx7Im5vbSI6IkxhcGV5cm91c2UiLCJjb2RlIjoiMDEyMDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMDc1IiwiY29kZUVwY2kiOiIyMDAwNjkxOTMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMzAiXSwicG9wdWxhdGlvbiI6MzE3fSx7Im5vbSI6Ikxhdm91cnMiLCJjb2RlIjoiMDEyMDgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMDgzIiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNTAiXSwicG9wdWxhdGlvbiI6MTMxfSx7Im5vbSI6IkzDqWF6IiwiY29kZSI6IjAxMjA5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjA5MSIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjAwIl0sInBvcHVsYXRpb24iOjc3OH0seyJub20iOiJMw6lsZXgiLCJjb2RlIjoiMDEyMTAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMTA5IiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MTAiXSwicG9wdWxhdGlvbiI6MjI4fSx7Im5vbSI6IkxlbnQiLCJjb2RlIjoiMDEyMTEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMTE3IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNDAiXSwicG9wdWxhdGlvbiI6MTM5NX0seyJub20iOiJMZXNjaGVyb3V4IiwiY29kZSI6IjAxMjEyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjEyNSIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTYwIl0sInBvcHVsYXRpb24iOjcxOH0seyJub20iOiJMZXltZW50IiwiY29kZSI6IjAxMjEzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjEzMyIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTUwIl0sInBvcHVsYXRpb24iOjEzNDF9LHsibm9tIjoiTGV5c3NhcmQiLCJjb2RlIjoiMDEyMTQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMTQxIiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NTAiXSwicG9wdWxhdGlvbiI6MTU4fSx7Im5vbSI6IlN1cmpvdXgtTGhvcGl0YWwiLCJjb2RlIjoiMDEyMTUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjAwMDg1MzA2IiwiY29kZUVwY2kiOiIyNDAxMDA4OTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MjAiXSwicG9wdWxhdGlvbiI6MTQwfSx7Im5vbSI6IkxodWlzIiwiY29kZSI6IjAxMjE2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjE2NiIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjgwIl0sInBvcHVsYXRpb24iOjg5NH0seyJub20iOiJMb21wbmFzIiwiY29kZSI6IjAxMjE5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjE5MCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjgwIl0sInBvcHVsYXRpb24iOjE2NH0seyJub20iOiJMb3lldHRlcyIsImNvZGUiOiIwMTIyNCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDIyNDAiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM2MCJdLCJwb3B1bGF0aW9uIjozMjI1fSx7Im5vbSI6Ikx1cmN5IiwiY29kZSI6IjAxMjI1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjI1NyIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMDkwIl0sInBvcHVsYXRpb24iOjM3Mn0seyJub20iOiJNYWduaWV1IiwiY29kZSI6IjAxMjI3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIwMDA4NDk3OCIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjY0M30seyJub20iOiJNYWlsbGF0IiwiY29kZSI6IjAxMjI4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjI4MSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDMwIl0sInBvcHVsYXRpb24iOjY2MH0seyJub20iOiJNYWxhZnJldGF6IiwiY29kZSI6IjAxMjI5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjI5OSIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzQwIl0sInBvcHVsYXRpb24iOjEyMzV9LHsibm9tIjoiTWFudGVuYXktTW9udGxpbiIsImNvZGUiOiIwMTIzMCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDIzMDciLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU2MCJdLCJwb3B1bGF0aW9uIjozMTJ9LHsibm9tIjoiTWFuemlhdCIsImNvZGUiOiIwMTIzMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDIzMTUiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU3MCJdLCJwb3B1bGF0aW9uIjoxOTQ4fSx7Im5vbSI6Ik1hcmJveiIsImNvZGUiOiIwMTIzMiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDIzMjMiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTg1MSJdLCJwb3B1bGF0aW9uIjoyMjc2fSx7Im5vbSI6Ik1hcmNoYW1wIiwiY29kZSI6IjAxMjMzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjMzMSIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjgwIl0sInBvcHVsYXRpb24iOjEzMH0seyJub20iOiJNYXJpZ25pZXUiLCJjb2RlIjoiMDEyMzQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMzQ5IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6MTY5fSx7Im5vbSI6Ik1hcmxpZXV4IiwiY29kZSI6IjAxMjM1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjM1NiIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjQwIl0sInBvcHVsYXRpb24iOjExNTl9LHsibm9tIjoiTWFyc29ubmFzIiwiY29kZSI6IjAxMjM2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjM2NCIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzQwIl0sInBvcHVsYXRpb24iOjEwMTF9LHsibm9tIjoiTWFydGlnbmF0IiwiY29kZSI6IjAxMjM3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjM3MiIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTAwIl0sInBvcHVsYXRpb24iOjE2Njd9LHsibm9tIjoiTWFzc2lldXgiLCJjb2RlIjoiMDEyMzgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyMzgwIiwiY29kZUVwY2kiOiIyMDAwNDI0OTciLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE2MDAiXSwicG9wdWxhdGlvbiI6MjY3MH0seyJub20iOiJNYXNzaWduaWV1LWRlLVJpdmVzIiwiY29kZSI6IjAxMjM5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjM5OCIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjYyMH0seyJub20iOiJNYXRhZmVsb24tR3JhbmdlcyIsImNvZGUiOiIwMTI0MCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI0MDYiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU4MCJdLCJwb3B1bGF0aW9uIjo2Mjd9LHsibm9tIjoiTWVpbGxvbm5hcyIsImNvZGUiOiIwMTI0MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI0MTQiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM3MCJdLCJwb3B1bGF0aW9uIjoxMzU2fSx7Im5vbSI6Ik3DqXJpZ25hdCIsImNvZGUiOiIwMTI0MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI0MjIiLCJjb2RlRXBjaSI6IjIwMDAyOTk5OSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ1MCJdLCJwb3B1bGF0aW9uIjoxMzV9LHsibm9tIjoiTWVzc2lteS1zdXItU2HDtG5lIiwiY29kZSI6IjAxMjQzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjQzMCIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDgwIl0sInBvcHVsYXRpb24iOjEyNTN9LHsibm9tIjoiTWV4aW1pZXV4IiwiY29kZSI6IjAxMjQ0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjQ0OCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxODAwIl0sInBvcHVsYXRpb24iOjc5MjN9LHsibm9tIjoiQm9oYXMtTWV5cmlhdC1SaWduYXQiLCJjb2RlIjoiMDEyNDUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNDU1IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNTAiXSwicG9wdWxhdGlvbiI6OTQzfSx7Im5vbSI6Ik3DqXrDqXJpYXQiLCJjb2RlIjoiMDEyNDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNDYzIiwiY29kZUVwY2kiOiIyMDAwNzA1NTUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE2NjAiXSwicG9wdWxhdGlvbiI6MjIxN30seyJub20iOiJNaWpvdXgiLCJjb2RlIjoiMDEyNDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNDcxIiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNzAiLCIwMTQxMCJdLCJwb3B1bGF0aW9uIjozMTJ9LHsibm9tIjoiTWlvbm5heSIsImNvZGUiOiIwMTI0OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI0ODkiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM5MCJdLCJwb3B1bGF0aW9uIjoyMTk3fSx7Im5vbSI6Ik1pcmliZWwiLCJjb2RlIjoiMDEyNDkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNDk3IiwiY29kZUVwY2kiOiIyNDAxMDA4MDAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE3MDAiXSwicG9wdWxhdGlvbiI6MTAxMjJ9LHsibm9tIjoiTWlzw6lyaWV1eCIsImNvZGUiOiIwMTI1MCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI1MDUiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTYwMCJdLCJwb3B1bGF0aW9uIjoxOTc0fSx7Im5vbSI6Ik1vZ25lbmVpbnMiLCJjb2RlIjoiMDEyNTIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNTIxIiwiY29kZUVwY2kiOiIyMDAwNzAxMTgiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNDAiXSwicG9wdWxhdGlvbiI6Nzg2fSx7Im5vbSI6Ik1vbnRhZ25hdCIsImNvZGUiOiIwMTI1NCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI1NDciLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjoyMTE2fSx7Im5vbSI6Ik1vbnRhZ25pZXUiLCJjb2RlIjoiMDEyNTUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNTU0IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NzAiXSwicG9wdWxhdGlvbiI6NjQ3fSx7Im5vbSI6Ik1vbnRhbmdlcyIsImNvZGUiOiIwMTI1NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI1NzAiLCJjb2RlRXBjaSI6IjI0MDEwMDg5MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIwMCJdLCJwb3B1bGF0aW9uIjozNTF9LHsibm9tIjoiTW9udGNlYXV4IiwiY29kZSI6IjAxMjU4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjU4OCIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMDkwIl0sInBvcHVsYXRpb24iOjExOTB9LHsibm9tIjoiTW9udGNldCIsImNvZGUiOiIwMTI1OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI1OTYiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMxMCJdLCJwb3B1bGF0aW9uIjo3MDJ9LHsibm9tIjoiTGUgTW9udGVsbGllciIsImNvZGUiOiIwMTI2MCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI2MDQiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTgwMCJdLCJwb3B1bGF0aW9uIjozMDJ9LHsibm9tIjoiTW9udGhpZXV4IiwiY29kZSI6IjAxMjYxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjYxMiIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzkwIl0sInBvcHVsYXRpb24iOjY2OH0seyJub20iOiJNb250bHVlbCIsImNvZGUiOiIwMTI2MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI2MjAiLCJjb2RlRXBjaSI6IjI0MDEwMDYxMCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEyMCJdLCJwb3B1bGF0aW9uIjo2ODg2fSx7Im5vbSI6Ik1vbnRtZXJsZS1zdXItU2HDtG5lIiwiY29kZSI6IjAxMjYzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjYzOCIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMDkwIl0sInBvcHVsYXRpb24iOjM3NzJ9LHsibm9tIjoiTW9udHJhY29sIiwiY29kZSI6IjAxMjY0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjY0NiIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzEwIl0sInBvcHVsYXRpb24iOjEwMTZ9LHsibm9tIjoiTW9udHLDqWFsLWxhLUNsdXNlIiwiY29kZSI6IjAxMjY1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjY1MyIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDYwIl0sInBvcHVsYXRpb24iOjM0NTZ9LHsibm9tIjoiTW9udHJldmVsLWVuLUJyZXNzZSIsImNvZGUiOiIwMTI2NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI2NjEiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM0MCJdLCJwb3B1bGF0aW9uIjoyNTc2fSx7Im5vbSI6Ik51cmlldXgtVm9sb2duYXQiLCJjb2RlIjoiMDEyNjciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNjc5IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NjAiXSwicG9wdWxhdGlvbiI6MTAyNX0seyJub20iOiJNdXJzLWV0LUfDqWxpZ25pZXV4IiwiY29kZSI6IjAxMjY4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjY4NyIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjI0Nn0seyJub20iOiJOYW50dWEiLCJjb2RlIjoiMDEyNjkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNjk1IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMzAiLCIwMTQ2MCJdLCJwb3B1bGF0aW9uIjozNDU0fSx7Im5vbSI6Ik5ldXZpbGxlLWxlcy1EYW1lcyIsImNvZGUiOiIwMTI3MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI3MjkiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQwMCJdLCJwb3B1bGF0aW9uIjoxNTAwfSx7Im5vbSI6Ik5ldXZpbGxlLXN1ci1BaW4iLCJjb2RlIjoiMDEyNzMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNzM3IiwiY29kZUVwY2kiOiIyMDAwMjk5OTkiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNjAiXSwicG9wdWxhdGlvbiI6MTgwMH0seyJub20iOiJMZXMgTmV5cm9sbGVzIiwiY29kZSI6IjAxMjc0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjc0NSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTMwIl0sInBvcHVsYXRpb24iOjY0NX0seyJub20iOiJOZXlyb24iLCJjb2RlIjoiMDEyNzUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNzUyIiwiY29kZUVwY2kiOiIyNDAxMDA4MDAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE3MDAiXSwicG9wdWxhdGlvbiI6MjU2OX0seyJub20iOiJOacOpdnJveiIsImNvZGUiOiIwMTI3NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI3NjAiLCJjb2RlRXBjaSI6IjI0MDEwMDYxMCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEyMCJdLCJwb3B1bGF0aW9uIjoxNjI0fSx7Im5vbSI6Ik5pdm9sbGV0LU1vbnRncmlmZm9uIiwiY29kZSI6IjAxMjc3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjc3OCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjMwIl0sInBvcHVsYXRpb24iOjExN30seyJub20iOiJPbmNpZXUiLCJjb2RlIjoiMDEyNzkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyNzk0IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyMzAiXSwicG9wdWxhdGlvbiI6ODB9LHsibm9tIjoiT3Jkb25uYXoiLCJjb2RlIjoiMDEyODAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyODAyIiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1MTAiXSwicG9wdWxhdGlvbiI6MTM4fSx7Im5vbSI6Ik9ybmV4IiwiY29kZSI6IjAxMjgxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjgxMCIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjEwIl0sInBvcHVsYXRpb24iOjQ0ODN9LHsibm9tIjoiT3V0cmlheiIsImNvZGUiOiIwMTI4MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI4MjgiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQzMCJdLCJwb3B1bGF0aW9uIjoyNTh9LHsibm9tIjoiT3lvbm5heCIsImNvZGUiOiIwMTI4MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI4MzYiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEwMCJdLCJwb3B1bGF0aW9uIjoyMjQ1Nn0seyJub20iOiJPemFuIiwiY29kZSI6IjAxMjg0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjg0NCIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTkwIl0sInBvcHVsYXRpb24iOjY5NH0seyJub20iOiJQYXJjaWV1eCIsImNvZGUiOiIwMTI4NSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI4NTEiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTYwMCJdLCJwb3B1bGF0aW9uIjoxMjI5fSx7Im5vbSI6IlBhcnZlcyBldCBOYXR0YWdlcyIsImNvZGUiOiIwMTI4NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwNTk5MTMiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjo5NTF9LHsibm9tIjoiUMOpcm9uIiwiY29kZSI6IjAxMjg4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjg4NSIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjMwIl0sInBvcHVsYXRpb24iOjI3MTd9LHsibm9tIjoiUMOpcm9ubmFzIiwiY29kZSI6IjAxMjg5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjg5MyIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxOTYwIl0sInBvcHVsYXRpb24iOjY1MjB9LHsibm9tIjoiUMOpcm91Z2VzIiwiY29kZSI6IjAxMjkwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjkwMSIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxODAwIl0sInBvcHVsYXRpb24iOjEyNzN9LHsibm9tIjoiUGVycmV4IiwiY29kZSI6IjAxMjkxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjkxOSIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTQwIl0sInBvcHVsYXRpb24iOjg0Mn0seyJub20iOiJQZXlyaWF0IiwiY29kZSI6IjAxMjkzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjkzNSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDMwIl0sInBvcHVsYXRpb24iOjE0OH0seyJub20iOiJQZXlyaWV1IiwiY29kZSI6IjAxMjk0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjk0MyIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjg4Nn0seyJub20iOiJQZXl6aWV1eC1zdXItU2HDtG5lIiwiY29kZSI6IjAxMjk1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMjk1MCIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTQwIl0sInBvcHVsYXRpb24iOjY4Nn0seyJub20iOiJQaXJham91eCIsImNvZGUiOiIwMTI5NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI5NjgiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI3MCJdLCJwb3B1bGF0aW9uIjo0MjB9LHsibm9tIjoiUGl6YXkiLCJjb2RlIjoiMDEyOTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAyOTc2IiwiY29kZUVwY2kiOiIyNDAxMDA2MTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMjAiXSwicG9wdWxhdGlvbiI6ODY3fSx7Im5vbSI6IlBsYWduZSIsImNvZGUiOiIwMTI5OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI5ODQiLCJjb2RlRXBjaSI6IjI0MDEwMDg5MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEzMCJdLCJwb3B1bGF0aW9uIjoxNTB9LHsibm9tIjoiTGUgUGxhbnRheSIsImNvZGUiOiIwMTI5OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDI5OTIiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMzMCJdLCJwb3B1bGF0aW9uIjo1ODR9LHsibm9tIjoiUG9sbGlhdCIsImNvZGUiOiIwMTMwMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMwMTYiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMxMCJdLCJwb3B1bGF0aW9uIjoyNTQ3fSx7Im5vbSI6IlBvbGxpZXUiLCJjb2RlIjoiMDEzMDIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzMDI0IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNTAiXSwicG9wdWxhdGlvbiI6MTYzfSx7Im5vbSI6IlBvbmNpbiIsImNvZGUiOiIwMTMwMyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMwMzIiLCJjb2RlRXBjaSI6IjIwMDAyOTk5OSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ1MCJdLCJwb3B1bGF0aW9uIjoxNjk5fSx7Im5vbSI6IlBvbnQtZCdBaW4iLCJjb2RlIjoiMDEzMDQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzMDQwIiwiY29kZUVwY2kiOiIyMDAwMjk5OTkiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNjAiXSwicG9wdWxhdGlvbiI6MjkyNH0seyJub20iOiJQb250LWRlLVZhdXgiLCJjb2RlIjoiMDEzMDUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzMDU3IiwiY29kZUVwY2kiOiIyMDAwNzEzNzEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExOTAiXSwicG9wdWxhdGlvbiI6MjIxM30seyJub20iOiJQb250LWRlLVZleWxlIiwiY29kZSI6IjAxMzA2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzA2NSIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjkwIl0sInBvcHVsYXRpb24iOjE2NDF9LHsibm9tIjoiUG9ydCIsImNvZGUiOiIwMTMwNyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMwNzMiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ2MCJdLCJwb3B1bGF0aW9uIjo4MzZ9LHsibm9tIjoiUG91Z255IiwiY29kZSI6IjAxMzA4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzA4MSIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTUwIl0sInBvcHVsYXRpb24iOjgwN30seyJub20iOiJQb3VpbGxhdCIsImNvZGUiOiIwMTMwOSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMwOTkiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjo4NX0seyJub20iOiJQcsOpbWV5emVsIiwiY29kZSI6IjAxMzEwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzEwNyIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzAwIl0sInBvcHVsYXRpb24iOjI1MX0seyJub20iOiJQcsOpbWlsbGlldSIsImNvZGUiOiIwMTMxMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMxMTUiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTExMCJdLCJwb3B1bGF0aW9uIjo0NH0seyJub20iOiJQcsOpdmVzc2luLU1vw6tucyIsImNvZGUiOiIwMTMxMyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMxMzEiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI4MCJdLCJwb3B1bGF0aW9uIjo4NzExfSx7Im5vbSI6IlByaWF5IiwiY29kZSI6IjAxMzE0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzE0OSIsImNvZGVFcGNpIjoiMjAwMDI5OTk5IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTYwIl0sInBvcHVsYXRpb24iOjE3Mjl9LHsibm9tIjoiUmFtYXNzZSIsImNvZGUiOiIwMTMxNyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMxNzIiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjozMzB9LHsibm9tIjoiUmFuY8OpIiwiY29kZSI6IjAxMzE4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzE4MCIsImNvZGVFcGNpIjoiMjAwMDQyNDk3IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzkwIl0sInBvcHVsYXRpb24iOjcyNn0seyJub20iOiJSZWxldmFudCIsImNvZGUiOiIwMTMxOSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMxOTgiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTk5MCJdLCJwb3B1bGF0aW9uIjo0NjZ9LHsibm9tIjoiUmVwbG9uZ2VzIiwiY29kZSI6IjAxMzIwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzIwNiIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNzUwIl0sInBvcHVsYXRpb24iOjM3OTF9LHsibm9tIjoiUmV2b25uYXMiLCJjb2RlIjoiMDEzMjEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzMjE0IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNTAiXSwicG9wdWxhdGlvbiI6ODg4fSx7Im5vbSI6IlJleXJpZXV4IiwiY29kZSI6IjAxMzIyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzIyMiIsImNvZGVFcGNpIjoiMjAwMDQyNDk3IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjAwIl0sInBvcHVsYXRpb24iOjQ5ODR9LHsibm9tIjoiUmV5c3NvdXplIiwiY29kZSI6IjAxMzIzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzIzMCIsImNvZGVFcGNpIjoiMjAwMDcxMzcxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTkwIl0sInBvcHVsYXRpb24iOjk4OX0seyJub20iOiJSaWduaWV1eC1sZS1GcmFuYyIsImNvZGUiOiIwMTMyNSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMyNTUiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTgwMCJdLCJwb3B1bGF0aW9uIjoxMDMyfSx7Im5vbSI6IlJvbWFucyIsImNvZGUiOiIwMTMyOCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMyODkiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQwMCJdLCJwb3B1bGF0aW9uIjo1OTd9LHsibm9tIjoiUm9zc2lsbG9uIiwiY29kZSI6IjAxMzI5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzI5NyIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTEwIl0sInBvcHVsYXRpb24iOjE1M30seyJub20iOiJSdWZmaWV1IiwiY29kZSI6IjAxMzMwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzMwNSIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjYwIl0sInBvcHVsYXRpb24iOjE4Mn0seyJub20iOiJTYWludC1BbGJhbiIsImNvZGUiOiIwMTMzMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMzMTMiLCJjb2RlRXBjaSI6IjIwMDAyOTk5OSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ1MCJdLCJwb3B1bGF0aW9uIjoxODd9LHsibm9tIjoiU2FpbnQtQW5kcsOpLWRlLULDomfDqSIsImNvZGUiOiIwMTMzMiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMzMjEiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM4MCJdLCJwb3B1bGF0aW9uIjo3Nzl9LHsibm9tIjoiU2FpbnQtQW5kcsOpLWRlLUNvcmN5IiwiY29kZSI6IjAxMzMzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzMzOSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzkwIl0sInBvcHVsYXRpb24iOjMzNjl9LHsibm9tIjoiU2FpbnQtQW5kcsOpLWQnSHVpcmlhdCIsImNvZGUiOiIwMTMzNCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMzNDciLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI5MCJdLCJwb3B1bGF0aW9uIjo2NTh9LHsibm9tIjoiU2FpbnQtQW5kcsOpLWxlLUJvdWNob3V4IiwiY29kZSI6IjAxMzM1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzM1NCIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjQwIl0sInBvcHVsYXRpb24iOjQwMn0seyJub20iOiJTYWludC1BbmRyw6ktc3VyLVZpZXV4LUpvbmMiLCJjb2RlIjoiMDEzMzYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzMzYyIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE5NjAiXSwicG9wdWxhdGlvbiI6MTE3NH0seyJub20iOiJTYWludC1Cw6luaWduZSIsImNvZGUiOiIwMTMzNyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDMzNzAiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE5MCJdLCJwb3B1bGF0aW9uIjoxMzExfSx7Im5vbSI6Ikdyb3Nsw6llLVNhaW50LUJlbm9pdCIsImNvZGUiOiIwMTMzOCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwNjAxNDMiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMwMCJdLCJwb3B1bGF0aW9uIjoxMjI3fSx7Im5vbSI6IlNhaW50LUJlcm5hcmQiLCJjb2RlIjoiMDEzMzkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzMzk2IiwiY29kZUVwY2kiOiIyMDAwNDI0OTciLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE2MDAiXSwicG9wdWxhdGlvbiI6MTQ2M30seyJub20iOiJTYWludGUtQ3JvaXgiLCJjb2RlIjoiMDEzNDIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNDIwIiwiY29kZUVwY2kiOiIyNDAxMDA2MTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExMjAiXSwicG9wdWxhdGlvbiI6NTU0fSx7Im5vbSI6IlNhaW50LUN5ci1zdXItTWVudGhvbiIsImNvZGUiOiIwMTM0MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM0MzgiLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM4MCJdLCJwb3B1bGF0aW9uIjoxNzkzfSx7Im5vbSI6IlNhaW50LURlbmlzLWzDqHMtQm91cmciLCJjb2RlIjoiMDEzNDQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNDQ2IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEwMDAiXSwicG9wdWxhdGlvbiI6NTk0M30seyJub20iOiJTYWludC1EZW5pcy1lbi1CdWdleSIsImNvZGUiOiIwMTM0NSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM0NTMiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTUwMCJdLCJwb3B1bGF0aW9uIjoyMjY2fSx7Im5vbSI6IlNhaW50LURpZGllci1kJ0F1c3NpYXQiLCJjb2RlIjoiMDEzNDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNDYxIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNDAiXSwicG9wdWxhdGlvbiI6ODU4fSx7Im5vbSI6IlNhaW50LURpZGllci1kZS1Gb3JtYW5zIiwiY29kZSI6IjAxMzQ3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzQ3OSIsImNvZGVFcGNpIjoiMjAwMDQyNDk3IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjAwIl0sInBvcHVsYXRpb24iOjIwMzV9LHsibm9tIjoiU2FpbnQtRGlkaWVyLXN1ci1DaGFsYXJvbm5lIiwiY29kZSI6IjAxMzQ4IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzQ4NyIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTQwIl0sInBvcHVsYXRpb24iOjI5Mzh9LHsibm9tIjoiU2FpbnQtw4lsb2kiLCJjb2RlIjoiMDEzNDkiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNDk1IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE4MDAiXSwicG9wdWxhdGlvbiI6NTAzfSx7Im5vbSI6IlNhaW50LcOJdGllbm5lLWR1LUJvaXMiLCJjb2RlIjoiMDEzNTAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNTAzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNzAiXSwicG9wdWxhdGlvbiI6MjU0Mn0seyJub20iOiJTYWludC3DiXRpZW5uZS1zdXItQ2hhbGFyb25uZSIsImNvZGUiOiIwMTM1MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM1MTEiLCJjb2RlRXBjaSI6IjIwMDA3MDExOCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE0MCJdLCJwb3B1bGF0aW9uIjoxNTcxfSx7Im5vbSI6IlNhaW50LcOJdGllbm5lLXN1ci1SZXlzc291emUiLCJjb2RlIjoiMDEzNTIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNTI5IiwiY29kZUVwY2kiOiIyMDAwNzEzNzEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExOTAiXSwicG9wdWxhdGlvbiI6NTgzfSx7Im5vbSI6IlNhaW50ZS1FdXBow6ltaWUiLCJjb2RlIjoiMDEzNTMiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNTM3IiwiY29kZUVwY2kiOiIyMDAwNDI0OTciLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE2MDAiXSwicG9wdWxhdGlvbiI6MTcwMH0seyJub20iOiJTYWludC1HZW5pcy1Qb3VpbGx5IiwiY29kZSI6IjAxMzU0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzU0NSIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjMwIl0sInBvcHVsYXRpb24iOjEzOTQzfSx7Im5vbSI6IlNhaW50LUdlbmlzLXN1ci1NZW50aG9uIiwiY29kZSI6IjAxMzU1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzU1MiIsImNvZGVFcGNpIjoiMjAwMDcwNTU1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzgwIl0sInBvcHVsYXRpb24iOjQ4Nn0seyJub20iOiJTYWludC1HZW9yZ2VzLXN1ci1SZW5vbiIsImNvZGUiOiIwMTM1NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM1NjAiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQwMCJdLCJwb3B1bGF0aW9uIjoyMDh9LHsibm9tIjoiU2FpbnQtR2VybWFpbi1kZS1Kb3V4IiwiY29kZSI6IjAxMzU3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzU3OCIsImNvZGVFcGNpIjoiMjQwMTAwODkxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTMwIl0sInBvcHVsYXRpb24iOjUwMn0seyJub20iOiJTYWludC1HZXJtYWluLWxlcy1QYXJvaXNzZXMiLCJjb2RlIjoiMDEzNTgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNTg2IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6NDI5fSx7Im5vbSI6IlNhaW50LUdlcm1haW4tc3VyLVJlbm9uIiwiY29kZSI6IjAxMzU5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzU5NCIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjQwIl0sInBvcHVsYXRpb24iOjI0Nn0seyJub20iOiJTYWludC1KZWFuLWRlLUdvbnZpbGxlIiwiY29kZSI6IjAxMzYwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzYwMiIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjMwIl0sInBvcHVsYXRpb24iOjE4OTV9LHsibm9tIjoiU2FpbnQtSmVhbi1kZS1OaW9zdCIsImNvZGUiOiIwMTM2MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2MTAiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTgwMCJdLCJwb3B1bGF0aW9uIjoxNTc3fSx7Im5vbSI6IlNhaW50LUplYW4tZGUtVGh1cmlnbmV1eCIsImNvZGUiOiIwMTM2MiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2MjgiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTM5MCJdLCJwb3B1bGF0aW9uIjo3ODZ9LHsibm9tIjoiU2FpbnQtSmVhbi1sZS1WaWV1eCIsImNvZGUiOiIwMTM2MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2MzYiLCJjb2RlRXBjaSI6IjIwMDAyOTk5OSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTY0MCJdLCJwb3B1bGF0aW9uIjoxNzcyfSx7Im5vbSI6IlNhaW50LUplYW4tc3VyLVJleXNzb3V6ZSIsImNvZGUiOiIwMTM2NCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2NDQiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU2MCJdLCJwb3B1bGF0aW9uIjo3NTZ9LHsibm9tIjoiU2FpbnQtSmVhbi1zdXItVmV5bGUiLCJjb2RlIjoiMDEzNjUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNjUxIiwiY29kZUVwY2kiOiIyMDAwNzA1NTUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyOTAiXSwicG9wdWxhdGlvbiI6MTE2Mn0seyJub20iOiJTYWludGUtSnVsaWUiLCJjb2RlIjoiMDEzNjYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNjY5IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNTAiXSwicG9wdWxhdGlvbiI6MTA4Mn0seyJub20iOiJTYWludC1KdWxpZW4tc3VyLVJleXNzb3V6ZSIsImNvZGUiOiIwMTM2NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2NzciLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU2MCJdLCJwb3B1bGF0aW9uIjo3NDZ9LHsibm9tIjoiU2FpbnQtSnVsaWVuLXN1ci1WZXlsZSIsImNvZGUiOiIwMTM2OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2ODUiLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU0MCJdLCJwb3B1bGF0aW9uIjo4Mzh9LHsibm9tIjoiU2FpbnQtSnVzdCIsImNvZGUiOiIwMTM2OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM2OTMiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjo5NDJ9LHsibm9tIjoiU2FpbnQtTGF1cmVudC1zdXItU2HDtG5lIiwiY29kZSI6IjAxMzcwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzcwMSIsImNvZGVFcGNpIjoiMjAwMDcwMzA4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNzUwIl0sInBvcHVsYXRpb24iOjE2ODh9LHsibm9tIjoiU2FpbnQtTWFyY2VsIiwiY29kZSI6IjAxMzcxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzcxOSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzkwIl0sInBvcHVsYXRpb24iOjEyNDJ9LHsibm9tIjoiU2FpbnQtTWFydGluLWRlLUJhdmVsIiwiY29kZSI6IjAxMzcyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzcyNyIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTEwIl0sInBvcHVsYXRpb24iOjQ0NH0seyJub20iOiJTYWludC1NYXJ0aW4tZHUtRnLDqm5lIiwiY29kZSI6IjAxMzczIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzczNSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDMwIl0sInBvcHVsYXRpb24iOjEwMjN9LHsibm9tIjoiU2FpbnQtTWFydGluLWR1LU1vbnQiLCJjb2RlIjoiMDEzNzQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzNzQzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNjAiXSwicG9wdWxhdGlvbiI6MTg3MX0seyJub20iOiJTYWludC1NYXJ0aW4tbGUtQ2jDonRlbCIsImNvZGUiOiIwMTM3NSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM3NTAiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMxMCJdLCJwb3B1bGF0aW9uIjo3ODJ9LHsibm9tIjoiU2FpbnQtTWF1cmljZS1kZS1CZXlub3N0IiwiY29kZSI6IjAxMzc2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzc2OCIsImNvZGVFcGNpIjoiMjQwMTAwODAwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNzAwIl0sInBvcHVsYXRpb24iOjM5NjJ9LHsibm9tIjoiU2FpbnQtTWF1cmljZS1kZS1Hb3VyZGFucyIsImNvZGUiOiIwMTM3OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM3ODQiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTgwMCJdLCJwb3B1bGF0aW9uIjoyNjQ1fSx7Im5vbSI6IlNhaW50LU1hdXJpY2UtZGUtUsOpbWVucyIsImNvZGUiOiIwMTM3OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM3OTIiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTUwMCJdLCJwb3B1bGF0aW9uIjo3NjB9LHsibm9tIjoiU2FpbnQtTml6aWVyLWxlLUJvdWNob3V4IiwiY29kZSI6IjAxMzgwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzgwMCIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTYwIl0sInBvcHVsYXRpb24iOjYzMn0seyJub20iOiJTYWludC1OaXppZXItbGUtRMOpc2VydCIsImNvZGUiOiIwMTM4MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM4MTgiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMyMCJdLCJwb3B1bGF0aW9uIjo5MjF9LHsibm9tIjoiU2FpbnRlLU9saXZlIiwiY29kZSI6IjAxMzgyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzgyNiIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzMwIl0sInBvcHVsYXRpb24iOjI3OX0seyJub20iOiJTYWludC1QYXVsLWRlLVZhcmF4IiwiY29kZSI6IjAxMzgzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzgzNCIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjQwIl0sInBvcHVsYXRpb24iOjE1NjJ9LHsibm9tIjoiU2FpbnQtUmFtYmVydC1lbi1CdWdleSIsImNvZGUiOiIwMTM4NCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM4NDIiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIzMCJdLCJwb3B1bGF0aW9uIjoyMTg4fSx7Im5vbSI6IlNhaW50LVLDqW15IiwiY29kZSI6IjAxMzg1IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzg1OSIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzEwIl0sInBvcHVsYXRpb24iOjk4NX0seyJub20iOiJTYWludC1Tb3JsaW4tZW4tQnVnZXkiLCJjb2RlIjoiMDEzODYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzODY3IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNTAiXSwicG9wdWxhdGlvbiI6MTEyN30seyJub20iOiJTYWludC1TdWxwaWNlIiwiY29kZSI6IjAxMzg3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzg3NSIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzQwIl0sInBvcHVsYXRpb24iOjI1Nn0seyJub20iOiJTYWludC1Ucml2aWVyLWRlLUNvdXJ0ZXMiLCJjb2RlIjoiMDEzODgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzODgzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1NjAiXSwicG9wdWxhdGlvbiI6MTEwMn0seyJub20iOiJTYWludC1Ucml2aWVyLXN1ci1Nb2lnbmFucyIsImNvZGUiOiIwMTM4OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM4OTEiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTk5MCJdLCJwb3B1bGF0aW9uIjoxODM0fSx7Im5vbSI6IlNhaW50LVZ1bGJhcyIsImNvZGUiOiIwMTM5MCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM5MDkiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE1MCJdLCJwb3B1bGF0aW9uIjoxMjQ2fSx7Im5vbSI6IlNhbGF2cmUiLCJjb2RlIjoiMDEzOTEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzOTE3IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNzAiXSwicG9wdWxhdGlvbiI6MjcxfSx7Im5vbSI6IlNhbW9nbmF0IiwiY29kZSI6IjAxMzkyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwMzkyNSIsImNvZGVFcGNpIjoiMjAwMDQyOTM1IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTgwIl0sInBvcHVsYXRpb24iOjY1Nn0seyJub20iOiJTYW5kcmFucyIsImNvZGUiOiIwMTM5MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM5MzMiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQwMCJdLCJwb3B1bGF0aW9uIjo1NDR9LHsibm9tIjoiU2F1bHQtQnLDqW5heiIsImNvZGUiOiIwMTM5NiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM5NjYiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE1MCJdLCJwb3B1bGF0aW9uIjo5Nzd9LHsibm9tIjoiU2F1dmVybnkiLCJjb2RlIjoiMDEzOTciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTAzOTc0IiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyMjAiXSwicG9wdWxhdGlvbiI6OTc4fSx7Im5vbSI6IlNhdmlnbmV1eCIsImNvZGUiOiIwMTM5OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM5ODIiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQ4MCJdLCJwb3B1bGF0aW9uIjoxNDIxfSx7Im5vbSI6IlPDqWdueSIsImNvZGUiOiIwMTM5OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDM5OTAiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE3MCJdLCJwb3B1bGF0aW9uIjoyMTA3fSx7Im5vbSI6IlNlaWxsb25uYXoiLCJjb2RlIjoiMDE0MDAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MDA2IiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NzAiXSwicG9wdWxhdGlvbiI6MTM5fSx7Im5vbSI6IlNlcmd5IiwiY29kZSI6IjAxNDAxIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDAxNCIsImNvZGVFcGNpIjoiMjQwMTAwNzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjMwIl0sInBvcHVsYXRpb24iOjIxNjd9LHsibm9tIjoiU2VybW95ZXIiLCJjb2RlIjoiMDE0MDIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MDIyIiwiY29kZUVwY2kiOiIyMDAwNzEzNzEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExOTAiXSwicG9wdWxhdGlvbiI6NjUyfSx7Im5vbSI6IlNlcnJpw6hyZXMtZGUtQnJpb3JkIiwiY29kZSI6IjAxNDAzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDAzMCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDcwIl0sInBvcHVsYXRpb24iOjEyOTV9LHsibm9tIjoiU2VycmnDqHJlcy1zdXItQWluIiwiY29kZSI6IjAxNDA0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDA0OCIsImNvZGVFcGNpIjoiMjAwMDI5OTk5IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDUwIl0sInBvcHVsYXRpb24iOjEzNX0seyJub20iOiJTZXJ2YXMiLCJjb2RlIjoiMDE0MDUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MDU1IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE5NjAiXSwicG9wdWxhdGlvbiI6MTI4MH0seyJub20iOiJTZXJ2aWduYXQiLCJjb2RlIjoiMDE0MDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MDYzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1NjAiXSwicG9wdWxhdGlvbiI6MTc0fSx7Im5vbSI6IlNleXNzZWwiLCJjb2RlIjoiMDE0MDciLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MDcxIiwiY29kZUVwY2kiOiIyMDAwNzA4NTIiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0MjAiXSwicG9wdWxhdGlvbiI6MTAwMn0seyJub20iOiJTaW1hbmRyZS1zdXItU3VyYW4iLCJjb2RlIjoiMDE0MDgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MDg5IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNTAiXSwicG9wdWxhdGlvbiI6NjYxfSx7Im5vbSI6IlNvbnRob25uYXgtbGEtTW9udGFnbmUiLCJjb2RlIjoiMDE0MTAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MTA1IiwiY29kZUVwY2kiOiIyMDAwNDI5MzUiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1ODAiXSwicG9wdWxhdGlvbiI6MzAwfSx7Im5vbSI6IlNvdWNsaW4iLCJjb2RlIjoiMDE0MTEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MTEzIiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNTAiXSwicG9wdWxhdGlvbiI6MjczfSx7Im5vbSI6IlN1bGlnbmF0IiwiY29kZSI6IjAxNDEyIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDEyMSIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNDAwIl0sInBvcHVsYXRpb24iOjU5NX0seyJub20iOiJUYWxpc3NpZXUiLCJjb2RlIjoiMDE0MTUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MTU0IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1MTAiXSwicG9wdWxhdGlvbiI6NDk5fSx7Im5vbSI6IlRlbmF5IiwiY29kZSI6IjAxNDE2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDE2MiIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMjMwIl0sInBvcHVsYXRpb24iOjEwMTR9LHsibm9tIjoiVGhpbCIsImNvZGUiOiIwMTQxOCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQxODgiLCJjb2RlRXBjaSI6IjI0MDEwMDgwMCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTEyMCJdLCJwb3B1bGF0aW9uIjoxMDg3fSx7Im5vbSI6IlRob2lyeSIsImNvZGUiOiIwMTQxOSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQxOTYiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTcxMCJdLCJwb3B1bGF0aW9uIjo2MDIxfSx7Im5vbSI6IlRob2lzc2V5IiwiY29kZSI6IjAxNDIwIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDIwNCIsImNvZGVFcGNpIjoiMjAwMDcwMTE4IiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTQwIl0sInBvcHVsYXRpb24iOjE2OTB9LHsibm9tIjoiVG9yY2lldSIsImNvZGUiOiIwMTQyMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQyMTIiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIzMCJdLCJwb3B1bGF0aW9uIjo3Mjl9LHsibm9tIjoiVG9zc2lhdCIsImNvZGUiOiIwMTQyMiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQyMjAiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjoxMzU1fSx7Im5vbSI6IlRvdXNzaWV1eCIsImNvZGUiOiIwMTQyMyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQyMzgiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTYwMCJdLCJwb3B1bGF0aW9uIjoxMTI0fSx7Im5vbSI6IlRyYW1veWVzIiwiY29kZSI6IjAxNDI0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDI0NiIsImNvZGVFcGNpIjoiMjQwMTAwODAwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzkwIl0sInBvcHVsYXRpb24iOjE3NzZ9LHsibm9tIjoiTGEgVHJhbmNsacOocmUiLCJjb2RlIjoiMDE0MjUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MjUzIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNjAiXSwicG9wdWxhdGlvbiI6Mjg4fSx7Im5vbSI6IlZhbC1SZXZlcm1vbnQiLCJjb2RlIjoiMDE0MjYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjAwMDU3MzQ3IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzNzAiXSwicG9wdWxhdGlvbiI6MjUzOH0seyJub20iOiJUcsOpdm91eCIsImNvZGUiOiIwMTQyNyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQyNzkiLCJjb2RlRXBjaSI6IjIwMDA0MjQ5NyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTYwMCJdLCJwb3B1bGF0aW9uIjo2OTIwfSx7Im5vbSI6IlZhbGVpbnMiLCJjb2RlIjoiMDE0MjgiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0Mjg3IiwiY29kZUVwY2kiOiIyMDAwNjkxOTMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNDAiXSwicG9wdWxhdGlvbiI6MTI2fSx7Im5vbSI6IlZhbmRlaW5zIiwiY29kZSI6IjAxNDI5IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDI5NSIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNjYwIl0sInBvcHVsYXRpb24iOjcwN30seyJub20iOiJWYXJhbWJvbiIsImNvZGUiOiIwMTQzMCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQzMDMiLCJjb2RlRXBjaSI6IjIwMDAyOTk5OSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE2MCJdLCJwb3B1bGF0aW9uIjo2MTl9LHsibm9tIjoiVmF1eC1lbi1CdWdleSIsImNvZGUiOiIwMTQzMSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQzMTEiLCJjb2RlRXBjaSI6IjI0MDEwMDg4MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTE1MCJdLCJwb3B1bGF0aW9uIjoxMjI3fSx7Im5vbSI6IlZlcmpvbiIsImNvZGUiOiIwMTQzMiIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQzMjkiLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI3MCJdLCJwb3B1bGF0aW9uIjozMjN9LHsibm9tIjoiVmVybm91eCIsImNvZGUiOiIwMTQzMyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQzMzciLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU2MCJdLCJwb3B1bGF0aW9uIjozMTN9LHsibm9tIjoiVmVyc2FpbGxldXgiLCJjb2RlIjoiMDE0MzQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MzQ1IiwiY29kZUVwY2kiOiIyMDAwNjkxOTMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMzAiXSwicG9wdWxhdGlvbiI6NDcyfSx7Im5vbSI6IlZlcnNvbm5leCIsImNvZGUiOiIwMTQzNSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQzNTIiLCJjb2RlRXBjaSI6IjI0MDEwMDc1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIxMCJdLCJwb3B1bGF0aW9uIjoyMTU3fSx7Im5vbSI6IlZlc2FuY3kiLCJjb2RlIjoiMDE0MzYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0MzYwIiwiY29kZUVwY2kiOiIyNDAxMDA3NTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDExNzAiXSwicG9wdWxhdGlvbiI6NDgxfSx7Im5vbSI6IlZlc2NvdXJzIiwiY29kZSI6IjAxNDM3IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDM3OCIsImNvZGVFcGNpIjoiMjAwMDcxNzUxIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxNTYwIl0sInBvcHVsYXRpb24iOjIyM30seyJub20iOiJWw6lzaW5lcyIsImNvZGUiOiIwMTQzOSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQzOTQiLCJjb2RlRXBjaSI6IjIwMDA3MTM3MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU3MCJdLCJwb3B1bGF0aW9uIjo5OX0seyJub20iOiJWaWV1LWQnSXplbmF2ZSIsImNvZGUiOiIwMTQ0MSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQ0MTAiLCJjb2RlRXBjaSI6IjIwMDA0MjkzNSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTQzMCJdLCJwb3B1bGF0aW9uIjo3MzR9LHsibm9tIjoiVmlsbGFycy1sZXMtRG9tYmVzIiwiY29kZSI6IjAxNDQzIiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDQzNiIsImNvZGVFcGNpIjoiMjAwMDY5MTkzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzMwIl0sInBvcHVsYXRpb24iOjQ5Nzh9LHsibm9tIjoiVmlsbGVib2lzIiwiY29kZSI6IjAxNDQ0IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDQ0NCIsImNvZGVFcGNpIjoiMjQwMTAwODgzIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMTUwIl0sInBvcHVsYXRpb24iOjExOTZ9LHsibm9tIjoiVmlsbGVtb3RpZXIiLCJjb2RlIjoiMDE0NDUiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0NDUxIiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEyNzAiXSwicG9wdWxhdGlvbiI6NjE1fSx7Im5vbSI6IlZpbGxlbmV1dmUiLCJjb2RlIjoiMDE0NDYiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0NDY5IiwiY29kZUVwY2kiOiIyMDAwNDI0OTciLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0ODAiXSwicG9wdWxhdGlvbiI6MTU0OH0seyJub20iOiJWaWxsZXJldmVyc3VyZSIsImNvZGUiOiIwMTQ0NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQ0NzciLCJjb2RlRXBjaSI6IjIwMDA3MTc1MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI1MCJdLCJwb3B1bGF0aW9uIjoxMzQyfSx7Im5vbSI6IlZpbGxlcyIsImNvZGUiOiIwMTQ0OCIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQ0ODUiLCJjb2RlRXBjaSI6IjI0MDEwMDg5MSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTIwMCJdLCJwb3B1bGF0aW9uIjozODl9LHsibm9tIjoiVmlsbGV0dGUtc3VyLUFpbiIsImNvZGUiOiIwMTQ0OSIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQ0OTMiLCJjb2RlRXBjaSI6IjIwMDA2OTE5MyIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTMyMCJdLCJwb3B1bGF0aW9uIjo3NTl9LHsibm9tIjoiVmlsbGlldS1Mb3llcy1Nb2xsb24iLCJjb2RlIjoiMDE0NTAiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0NTAxIiwiY29kZUVwY2kiOiIyNDAxMDA4ODMiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE4MDAiXSwicG9wdWxhdGlvbiI6MzcxN30seyJub20iOiJWaXJpYXQiLCJjb2RlIjoiMDE0NTEiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0NTE5IiwiY29kZUVwY2kiOiIyMDAwNzE3NTEiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE0NDAiXSwicG9wdWxhdGlvbiI6NjU5NX0seyJub20iOiJWaXJpZXUtbGUtR3JhbmQiLCJjb2RlIjoiMDE0NTIiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0NTI3IiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDE1MTAiXSwicG9wdWxhdGlvbiI6MTEzMn0seyJub20iOiJBcnZpw6hyZS1lbi1WYWxyb21leSIsImNvZGUiOiIwMTQ1MyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMDAwODY0MDMiLCJjb2RlRXBjaSI6IjIwMDA0MDM1MCIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTI2MCJdLCJwb3B1bGF0aW9uIjo3MTB9LHsibm9tIjoiVmlyaWduaW4iLCJjb2RlIjoiMDE0NTQiLCJjb2RlRGVwYXJ0ZW1lbnQiOiIwMSIsInNpcmVuIjoiMjEwMTA0NTQzIiwiY29kZUVwY2kiOiIyMDAwNDAzNTAiLCJjb2RlUmVnaW9uIjoiODQiLCJjb2Rlc1Bvc3RhdXgiOlsiMDEzMDAiXSwicG9wdWxhdGlvbiI6MTExMn0seyJub20iOiJWb25nbmVzIiwiY29kZSI6IjAxNDU2IiwiY29kZURlcGFydGVtZW50IjoiMDEiLCJzaXJlbiI6IjIxMDEwNDU2OCIsImNvZGVFcGNpIjoiMjAwMDQwMzUwIiwiY29kZVJlZ2lvbiI6Ijg0IiwiY29kZXNQb3N0YXV4IjpbIjAxMzUwIl0sInBvcHVsYXRpb24iOjcxfSx7Im5vbSI6IlZvbm5hcyIsImNvZGUiOiIwMTQ1NyIsImNvZGVEZXBhcnRlbWVudCI6IjAxIiwic2lyZW4iOiIyMTAxMDQ1NzYiLCJjb2RlRXBjaSI6IjIwMDA3MDU1NSIsImNvZGVSZWdpb24iOiI4NCIsImNvZGVzUG9zdGF1eCI6WyIwMTU0MCJdLCJwb3B1bGF0aW9uIjozMTgxfV0= + recorded_at: Thu, 23 Feb 2023 13:33:37 GMT +recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/cassettes/api_geo_departements.yml b/spec/fixtures/cassettes/api_geo_departements.yml index 9f5f79d6e..79c3de62b 100644 --- a/spec/fixtures/cassettes/api_geo_departements.yml +++ b/spec/fixtures/cassettes/api_geo_departements.yml @@ -2,7 +2,7 @@ http_interactions: - request: method: get - uri: https://geo.api.gouv.fr/departements + uri: https://geo.api.gouv.fr/departements?zone=metro,drom,com body: encoding: US-ASCII string: '' @@ -19,23 +19,23 @@ http_interactions: Server: - nginx/1.10.3 (Ubuntu) Date: - - Tue, 20 Dec 2022 11:55:45 GMT + - Wed, 22 Feb 2023 14:46:29 GMT Content-Type: - application/json; charset=utf-8 Content-Length: - - '5083' + - '5598' Vary: - Accept-Encoding - Origin X-Powered-By: - Express Etag: - - W/"13db-vyFtsKQWjHylHAtBtjYjxCAjMHE" + - W/"15de-6bFFP56x7MMvD1C1JvzSgcLzQWw" Strict-Transport-Security: - max-age=15552000 body: encoding: ASCII-8BIT string: !binary |- - W3sibm9tIjoiQWluIiwiY29kZSI6IjAxIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBaXNuZSIsImNvZGUiOiIwMiIsImNvZGVSZWdpb24iOiIzMiJ9LHsibm9tIjoiQWxsaWVyIiwiY29kZSI6IjAzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBbHBlcy1kZS1IYXV0ZS1Qcm92ZW5jZSIsImNvZGUiOiIwNCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiSGF1dGVzLUFscGVzIiwiY29kZSI6IjA1IiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJBbHBlcy1NYXJpdGltZXMiLCJjb2RlIjoiMDYiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkFyZMOoY2hlIiwiY29kZSI6IjA3IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBcmRlbm5lcyIsImNvZGUiOiIwOCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXJpw6hnZSIsImNvZGUiOiIwOSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXViZSIsImNvZGUiOiIxMCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXVkZSIsImNvZGUiOiIxMSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXZleXJvbiIsImNvZGUiOiIxMiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQm91Y2hlcy1kdS1SaMO0bmUiLCJjb2RlIjoiMTMiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkNhbHZhZG9zIiwiY29kZSI6IjE0IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJDYW50YWwiLCJjb2RlIjoiMTUiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkNoYXJlbnRlIiwiY29kZSI6IjE2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJDaGFyZW50ZS1NYXJpdGltZSIsImNvZGUiOiIxNyIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiQ2hlciIsImNvZGUiOiIxOCIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiQ29ycsOoemUiLCJjb2RlIjoiMTkiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkPDtHRlLWQnT3IiLCJjb2RlIjoiMjEiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IkPDtHRlcy1kJ0FybW9yIiwiY29kZSI6IjIyIiwiY29kZVJlZ2lvbiI6IjUzIn0seyJub20iOiJDcmV1c2UiLCJjb2RlIjoiMjMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkRvcmRvZ25lIiwiY29kZSI6IjI0IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJEb3VicyIsImNvZGUiOiIyNSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRHLDtG1lIiwiY29kZSI6IjI2IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJFdXJlIiwiY29kZSI6IjI3IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJFdXJlLWV0LUxvaXIiLCJjb2RlIjoiMjgiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkZpbmlzdMOocmUiLCJjb2RlIjoiMjkiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkNvcnNlLWR1LVN1ZCIsImNvZGUiOiIyQSIsImNvZGVSZWdpb24iOiI5NCJ9LHsibm9tIjoiSGF1dGUtQ29yc2UiLCJjb2RlIjoiMkIiLCJjb2RlUmVnaW9uIjoiOTQifSx7Im5vbSI6IkdhcmQiLCJjb2RlIjoiMzAiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkhhdXRlLUdhcm9ubmUiLCJjb2RlIjoiMzEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkdlcnMiLCJjb2RlIjoiMzIiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ikdpcm9uZGUiLCJjb2RlIjoiMzMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkjDqXJhdWx0IiwiY29kZSI6IjM0IiwiY29kZVJlZ2lvbiI6Ijc2In0seyJub20iOiJJbGxlLWV0LVZpbGFpbmUiLCJjb2RlIjoiMzUiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkluZHJlIiwiY29kZSI6IjM2IiwiY29kZVJlZ2lvbiI6IjI0In0seyJub20iOiJJbmRyZS1ldC1Mb2lyZSIsImNvZGUiOiIzNyIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiSXPDqHJlIiwiY29kZSI6IjM4IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJKdXJhIiwiY29kZSI6IjM5IiwiY29kZVJlZ2lvbiI6IjI3In0seyJub20iOiJMYW5kZXMiLCJjb2RlIjoiNDAiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvaXItZXQtQ2hlciIsImNvZGUiOiI0MSIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiTG9pcmUiLCJjb2RlIjoiNDIiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLUxvaXJlIiwiY29kZSI6IjQzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJMb2lyZS1BdGxhbnRpcXVlIiwiY29kZSI6IjQ0IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJMb2lyZXQiLCJjb2RlIjoiNDUiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkxvdCIsImNvZGUiOiI0NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiTG90LWV0LUdhcm9ubmUiLCJjb2RlIjoiNDciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvesOocmUiLCJjb2RlIjoiNDgiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ik1haW5lLWV0LUxvaXJlIiwiY29kZSI6IjQ5IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJNYW5jaGUiLCJjb2RlIjoiNTAiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6Ik1hcm5lIiwiY29kZSI6IjUxIiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJIYXV0ZS1NYXJuZSIsImNvZGUiOiI1MiIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTWF5ZW5uZSIsImNvZGUiOiI1MyIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiTWV1cnRoZS1ldC1Nb3NlbGxlIiwiY29kZSI6IjU0IiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJNZXVzZSIsImNvZGUiOiI1NSIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTW9yYmloYW4iLCJjb2RlIjoiNTYiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6Ik1vc2VsbGUiLCJjb2RlIjoiNTciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6Ik5pw6h2cmUiLCJjb2RlIjoiNTgiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6Ik5vcmQiLCJjb2RlIjoiNTkiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9pc2UiLCJjb2RlIjoiNjAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9ybmUiLCJjb2RlIjoiNjEiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6IlBhcy1kZS1DYWxhaXMiLCJjb2RlIjoiNjIiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlB1eS1kZS1Ew7RtZSIsImNvZGUiOiI2MyIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUHlyw6luw6llcy1BdGxhbnRpcXVlcyIsImNvZGUiOiI2NCIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiSGF1dGVzLVB5csOpbsOpZXMiLCJjb2RlIjoiNjUiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlB5csOpbsOpZXMtT3JpZW50YWxlcyIsImNvZGUiOiI2NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQmFzLVJoaW4iLCJjb2RlIjoiNjciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6IkhhdXQtUmhpbiIsImNvZGUiOiI2OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiUmjDtG5lIiwiY29kZSI6IjY5IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJIYXV0ZS1TYcO0bmUiLCJjb2RlIjoiNzAiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlNhw7RuZS1ldC1Mb2lyZSIsImNvZGUiOiI3MSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiU2FydGhlIiwiY29kZSI6IjcyIiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJTYXZvaWUiLCJjb2RlIjoiNzMiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLVNhdm9pZSIsImNvZGUiOiI3NCIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUGFyaXMiLCJjb2RlIjoiNzUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLU1hcml0aW1lIiwiY29kZSI6Ijc2IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJTZWluZS1ldC1NYXJuZSIsImNvZGUiOiI3NyIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiWXZlbGluZXMiLCJjb2RlIjoiNzgiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IkRldXgtU8OodnJlcyIsImNvZGUiOiI3OSIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiU29tbWUiLCJjb2RlIjoiODAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlRhcm4iLCJjb2RlIjoiODEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlRhcm4tZXQtR2Fyb25uZSIsImNvZGUiOiI4MiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiVmFyIiwiY29kZSI6IjgzIiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJWYXVjbHVzZSIsImNvZGUiOiI4NCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiVmVuZMOpZSIsImNvZGUiOiI4NSIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiVmllbm5lIiwiY29kZSI6Ijg2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJIYXV0ZS1WaWVubmUiLCJjb2RlIjoiODciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IlZvc2dlcyIsImNvZGUiOiI4OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiWW9ubmUiLCJjb2RlIjoiODkiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlRlcnJpdG9pcmUgZGUgQmVsZm9ydCIsImNvZGUiOiI5MCIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRXNzb25uZSIsImNvZGUiOiI5MSIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiSGF1dHMtZGUtU2VpbmUiLCJjb2RlIjoiOTIiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLVNhaW50LURlbmlzIiwiY29kZSI6IjkzIiwiY29kZVJlZ2lvbiI6IjExIn0seyJub20iOiJWYWwtZGUtTWFybmUiLCJjb2RlIjoiOTQiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlZhbC1kJ09pc2UiLCJjb2RlIjoiOTUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6Ikd1YWRlbG91cGUiLCJjb2RlIjoiOTcxIiwiY29kZVJlZ2lvbiI6IjAxIn0seyJub20iOiJNYXJ0aW5pcXVlIiwiY29kZSI6Ijk3MiIsImNvZGVSZWdpb24iOiIwMiJ9LHsibm9tIjoiR3V5YW5lIiwiY29kZSI6Ijk3MyIsImNvZGVSZWdpb24iOiIwMyJ9LHsibm9tIjoiTGEgUsOpdW5pb24iLCJjb2RlIjoiOTc0IiwiY29kZVJlZ2lvbiI6IjA0In0seyJub20iOiJNYXlvdHRlIiwiY29kZSI6Ijk3NiIsImNvZGVSZWdpb24iOiIwNiJ9XQ== - recorded_at: Tue, 20 Dec 2022 11:55:45 GMT + W3sibm9tIjoiQWluIiwiY29kZSI6IjAxIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBaXNuZSIsImNvZGUiOiIwMiIsImNvZGVSZWdpb24iOiIzMiJ9LHsibm9tIjoiQWxsaWVyIiwiY29kZSI6IjAzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBbHBlcy1kZS1IYXV0ZS1Qcm92ZW5jZSIsImNvZGUiOiIwNCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiSGF1dGVzLUFscGVzIiwiY29kZSI6IjA1IiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJBbHBlcy1NYXJpdGltZXMiLCJjb2RlIjoiMDYiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkFyZMOoY2hlIiwiY29kZSI6IjA3IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJBcmRlbm5lcyIsImNvZGUiOiIwOCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXJpw6hnZSIsImNvZGUiOiIwOSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXViZSIsImNvZGUiOiIxMCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiQXVkZSIsImNvZGUiOiIxMSIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQXZleXJvbiIsImNvZGUiOiIxMiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQm91Y2hlcy1kdS1SaMO0bmUiLCJjb2RlIjoiMTMiLCJjb2RlUmVnaW9uIjoiOTMifSx7Im5vbSI6IkNhbHZhZG9zIiwiY29kZSI6IjE0IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJDYW50YWwiLCJjb2RlIjoiMTUiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkNoYXJlbnRlIiwiY29kZSI6IjE2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJDaGFyZW50ZS1NYXJpdGltZSIsImNvZGUiOiIxNyIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiQ2hlciIsImNvZGUiOiIxOCIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiQ29ycsOoemUiLCJjb2RlIjoiMTkiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkPDtHRlLWQnT3IiLCJjb2RlIjoiMjEiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IkPDtHRlcy1kJ0FybW9yIiwiY29kZSI6IjIyIiwiY29kZVJlZ2lvbiI6IjUzIn0seyJub20iOiJDcmV1c2UiLCJjb2RlIjoiMjMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkRvcmRvZ25lIiwiY29kZSI6IjI0IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJEb3VicyIsImNvZGUiOiIyNSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRHLDtG1lIiwiY29kZSI6IjI2IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJFdXJlIiwiY29kZSI6IjI3IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJFdXJlLWV0LUxvaXIiLCJjb2RlIjoiMjgiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkZpbmlzdMOocmUiLCJjb2RlIjoiMjkiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkNvcnNlLWR1LVN1ZCIsImNvZGUiOiIyQSIsImNvZGVSZWdpb24iOiI5NCJ9LHsibm9tIjoiSGF1dGUtQ29yc2UiLCJjb2RlIjoiMkIiLCJjb2RlUmVnaW9uIjoiOTQifSx7Im5vbSI6IkdhcmQiLCJjb2RlIjoiMzAiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkhhdXRlLUdhcm9ubmUiLCJjb2RlIjoiMzEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IkdlcnMiLCJjb2RlIjoiMzIiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ikdpcm9uZGUiLCJjb2RlIjoiMzMiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkjDqXJhdWx0IiwiY29kZSI6IjM0IiwiY29kZVJlZ2lvbiI6Ijc2In0seyJub20iOiJJbGxlLWV0LVZpbGFpbmUiLCJjb2RlIjoiMzUiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6IkluZHJlIiwiY29kZSI6IjM2IiwiY29kZVJlZ2lvbiI6IjI0In0seyJub20iOiJJbmRyZS1ldC1Mb2lyZSIsImNvZGUiOiIzNyIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiSXPDqHJlIiwiY29kZSI6IjM4IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJKdXJhIiwiY29kZSI6IjM5IiwiY29kZVJlZ2lvbiI6IjI3In0seyJub20iOiJMYW5kZXMiLCJjb2RlIjoiNDAiLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvaXItZXQtQ2hlciIsImNvZGUiOiI0MSIsImNvZGVSZWdpb24iOiIyNCJ9LHsibm9tIjoiTG9pcmUiLCJjb2RlIjoiNDIiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLUxvaXJlIiwiY29kZSI6IjQzIiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJMb2lyZS1BdGxhbnRpcXVlIiwiY29kZSI6IjQ0IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJMb2lyZXQiLCJjb2RlIjoiNDUiLCJjb2RlUmVnaW9uIjoiMjQifSx7Im5vbSI6IkxvdCIsImNvZGUiOiI0NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiTG90LWV0LUdhcm9ubmUiLCJjb2RlIjoiNDciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IkxvesOocmUiLCJjb2RlIjoiNDgiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6Ik1haW5lLWV0LUxvaXJlIiwiY29kZSI6IjQ5IiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJNYW5jaGUiLCJjb2RlIjoiNTAiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6Ik1hcm5lIiwiY29kZSI6IjUxIiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJIYXV0ZS1NYXJuZSIsImNvZGUiOiI1MiIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTWF5ZW5uZSIsImNvZGUiOiI1MyIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiTWV1cnRoZS1ldC1Nb3NlbGxlIiwiY29kZSI6IjU0IiwiY29kZVJlZ2lvbiI6IjQ0In0seyJub20iOiJNZXVzZSIsImNvZGUiOiI1NSIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiTW9yYmloYW4iLCJjb2RlIjoiNTYiLCJjb2RlUmVnaW9uIjoiNTMifSx7Im5vbSI6Ik1vc2VsbGUiLCJjb2RlIjoiNTciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6Ik5pw6h2cmUiLCJjb2RlIjoiNTgiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6Ik5vcmQiLCJjb2RlIjoiNTkiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9pc2UiLCJjb2RlIjoiNjAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6Ik9ybmUiLCJjb2RlIjoiNjEiLCJjb2RlUmVnaW9uIjoiMjgifSx7Im5vbSI6IlBhcy1kZS1DYWxhaXMiLCJjb2RlIjoiNjIiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlB1eS1kZS1Ew7RtZSIsImNvZGUiOiI2MyIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUHlyw6luw6llcy1BdGxhbnRpcXVlcyIsImNvZGUiOiI2NCIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiSGF1dGVzLVB5csOpbsOpZXMiLCJjb2RlIjoiNjUiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlB5csOpbsOpZXMtT3JpZW50YWxlcyIsImNvZGUiOiI2NiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiQmFzLVJoaW4iLCJjb2RlIjoiNjciLCJjb2RlUmVnaW9uIjoiNDQifSx7Im5vbSI6IkhhdXQtUmhpbiIsImNvZGUiOiI2OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiUmjDtG5lIiwiY29kZSI6IjY5IiwiY29kZVJlZ2lvbiI6Ijg0In0seyJub20iOiJIYXV0ZS1TYcO0bmUiLCJjb2RlIjoiNzAiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlNhw7RuZS1ldC1Mb2lyZSIsImNvZGUiOiI3MSIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiU2FydGhlIiwiY29kZSI6IjcyIiwiY29kZVJlZ2lvbiI6IjUyIn0seyJub20iOiJTYXZvaWUiLCJjb2RlIjoiNzMiLCJjb2RlUmVnaW9uIjoiODQifSx7Im5vbSI6IkhhdXRlLVNhdm9pZSIsImNvZGUiOiI3NCIsImNvZGVSZWdpb24iOiI4NCJ9LHsibm9tIjoiUGFyaXMiLCJjb2RlIjoiNzUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLU1hcml0aW1lIiwiY29kZSI6Ijc2IiwiY29kZVJlZ2lvbiI6IjI4In0seyJub20iOiJTZWluZS1ldC1NYXJuZSIsImNvZGUiOiI3NyIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiWXZlbGluZXMiLCJjb2RlIjoiNzgiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IkRldXgtU8OodnJlcyIsImNvZGUiOiI3OSIsImNvZGVSZWdpb24iOiI3NSJ9LHsibm9tIjoiU29tbWUiLCJjb2RlIjoiODAiLCJjb2RlUmVnaW9uIjoiMzIifSx7Im5vbSI6IlRhcm4iLCJjb2RlIjoiODEiLCJjb2RlUmVnaW9uIjoiNzYifSx7Im5vbSI6IlRhcm4tZXQtR2Fyb25uZSIsImNvZGUiOiI4MiIsImNvZGVSZWdpb24iOiI3NiJ9LHsibm9tIjoiVmFyIiwiY29kZSI6IjgzIiwiY29kZVJlZ2lvbiI6IjkzIn0seyJub20iOiJWYXVjbHVzZSIsImNvZGUiOiI4NCIsImNvZGVSZWdpb24iOiI5MyJ9LHsibm9tIjoiVmVuZMOpZSIsImNvZGUiOiI4NSIsImNvZGVSZWdpb24iOiI1MiJ9LHsibm9tIjoiVmllbm5lIiwiY29kZSI6Ijg2IiwiY29kZVJlZ2lvbiI6Ijc1In0seyJub20iOiJIYXV0ZS1WaWVubmUiLCJjb2RlIjoiODciLCJjb2RlUmVnaW9uIjoiNzUifSx7Im5vbSI6IlZvc2dlcyIsImNvZGUiOiI4OCIsImNvZGVSZWdpb24iOiI0NCJ9LHsibm9tIjoiWW9ubmUiLCJjb2RlIjoiODkiLCJjb2RlUmVnaW9uIjoiMjcifSx7Im5vbSI6IlRlcnJpdG9pcmUgZGUgQmVsZm9ydCIsImNvZGUiOiI5MCIsImNvZGVSZWdpb24iOiIyNyJ9LHsibm9tIjoiRXNzb25uZSIsImNvZGUiOiI5MSIsImNvZGVSZWdpb24iOiIxMSJ9LHsibm9tIjoiSGF1dHMtZGUtU2VpbmUiLCJjb2RlIjoiOTIiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlNlaW5lLVNhaW50LURlbmlzIiwiY29kZSI6IjkzIiwiY29kZVJlZ2lvbiI6IjExIn0seyJub20iOiJWYWwtZGUtTWFybmUiLCJjb2RlIjoiOTQiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6IlZhbC1kJ09pc2UiLCJjb2RlIjoiOTUiLCJjb2RlUmVnaW9uIjoiMTEifSx7Im5vbSI6Ikd1YWRlbG91cGUiLCJjb2RlIjoiOTcxIiwiY29kZVJlZ2lvbiI6IjAxIn0seyJub20iOiJNYXJ0aW5pcXVlIiwiY29kZSI6Ijk3MiIsImNvZGVSZWdpb24iOiIwMiJ9LHsibm9tIjoiR3V5YW5lIiwiY29kZSI6Ijk3MyIsImNvZGVSZWdpb24iOiIwMyJ9LHsibm9tIjoiTGEgUsOpdW5pb24iLCJjb2RlIjoiOTc0IiwiY29kZVJlZ2lvbiI6IjA0In0seyJub20iOiJNYXlvdHRlIiwiY29kZSI6Ijk3NiIsImNvZGVSZWdpb24iOiIwNiJ9LHsibm9tIjoiU2FpbnQtUGllcnJlLWV0LU1pcXVlbG9uIiwiY29kZSI6Ijk3NSIsImNvZGVSZWdpb24iOiI5NzUifSx7Im5vbSI6IlNhaW50LUJhcnRow6lsZW15IiwiY29kZSI6Ijk3NyIsImNvZGVSZWdpb24iOiI5NzcifSx7Im5vbSI6IlNhaW50LU1hcnRpbiIsImNvZGUiOiI5NzgiLCJjb2RlUmVnaW9uIjoiOTc4In0seyJub20iOiJUZXJyZXMgYXVzdHJhbGVzIGV0IGFudGFyY3RpcXVlcyBmcmFuw6dhaXNlcyIsImNvZGUiOiI5ODQiLCJjb2RlUmVnaW9uIjoiOTg0In0seyJub20iOiJXYWxsaXMgZXQgRnV0dW5hIiwiY29kZSI6Ijk4NiIsImNvZGVSZWdpb24iOiI5ODYifSx7Im5vbSI6IlBvbHluw6lzaWUgZnJhbsOnYWlzZSIsImNvZGUiOiI5ODciLCJjb2RlUmVnaW9uIjoiOTg3In0seyJub20iOiJOb3V2ZWxsZS1DYWzDqWRvbmllIiwiY29kZSI6Ijk4OCIsImNvZGVSZWdpb24iOiI5ODgifSx7Im5vbSI6IsOObGUgZGUgQ2xpcHBlcnRvbiIsImNvZGUiOiI5ODkiLCJjb2RlUmVnaW9uIjoiOTg5In1d + recorded_at: Wed, 22 Feb 2023 14:46:29 GMT recorded_with: VCR 6.1.0 diff --git a/spec/fixtures/files/api_education/annuaire_education_empty.json b/spec/fixtures/files/api_education/annuaire_education_empty.json new file mode 100644 index 000000000..34e44e091 --- /dev/null +++ b/spec/fixtures/files/api_education/annuaire_education_empty.json @@ -0,0 +1,14 @@ +{ + "nhits": 0, + "parameters": { + "dataset": "fr-en-annuaire-education", + "rows": 1, + "start": 0, + "refine": { + "identifiant_de_l_etablissement": "0341247" + }, + "format": "json", + "timezone": "UTC" + }, + "records": [] +} diff --git a/spec/fixtures/files/instructeurs-file.csv b/spec/fixtures/files/instructeurs-file.csv new file mode 100644 index 000000000..6e3f06311 --- /dev/null +++ b/spec/fixtures/files/instructeurs-file.csv @@ -0,0 +1,5 @@ +Email +kara@beta-gouv.fr +philippe@mail.com +lisa@gouv.fr +eric diff --git a/spec/fixtures/files/valid-instructeurs-file.csv b/spec/fixtures/files/valid-instructeurs-file.csv new file mode 100644 index 000000000..fb5f0d056 --- /dev/null +++ b/spec/fixtures/files/valid-instructeurs-file.csv @@ -0,0 +1,4 @@ +Email +kara@beta-gouv.fr +philippe@mail.com +lisa@gouv.fr diff --git a/spec/graphql/demarche_spec.rb b/spec/graphql/demarche_spec.rb index 29dc4de85..db60988d0 100644 --- a/spec/graphql/demarche_spec.rb +++ b/spec/graphql/demarche_spec.rb @@ -8,6 +8,24 @@ RSpec.describe Types::DemarcheType, type: :graphql do let(:data) { subject['data'].deep_symbolize_keys } let(:errors) { subject['errors'].deep_symbolize_keys } + describe 'context should correctly preserve demarche authorization state' do + let(:query) { DEMARCHE_QUERY } + let(:admin) { create(:administrateur) } + let(:procedure) { create(:procedure, administrateurs: [admin]) } + + let(:other_admin_procedure) { create(:procedure) } + let(:context) { { administrateur_id: admin.id } } + let(:variables) { { number: procedure.id } } + + it do + result = API::V2::Schema.execute(query, variables: variables, context: context) + graphql_context = result.context + + expect(graphql_context.authorized_demarche?(procedure)).to be_truthy + expect(graphql_context.authorized_demarche?(other_admin_procedure)).to be_falsey + end + end + describe 'demarche with clone' do let(:procedure) { create(:procedure, types_de_champ_public: [{ type: :yes_no }]) } let(:procedure_clone) { procedure.clone(procedure.administrateurs.first, false) } @@ -23,6 +41,13 @@ RSpec.describe Types::DemarcheType, type: :graphql do expect(procedure.draft_revision.types_de_champ_public.first.stable_id).to eq(procedure_clone.draft_revision.types_de_champ_public.first.stable_id) } end + DEMARCHE_QUERY = <<-GRAPHQL + query($number: Int!) { + demarche(number: $number) { + number + } + } + GRAPHQL DEMARCHE_WITH_CHAMP_DESCRIPTORS_QUERY = <<-GRAPHQL query($number: Int!) { diff --git a/spec/helpers/string_to_html_helper_spec.rb b/spec/helpers/string_to_html_helper_spec.rb deleted file mode 100644 index 513fdf75c..000000000 --- a/spec/helpers/string_to_html_helper_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -RSpec.describe StringToHtmlHelper, type: :helper do - describe "#string_to_html" do - let(:allow_a) { false } - subject { string_to_html(description, allow_a:) } - - context "with some simple texte" do - let(:description) { "1er ligne \n 2ieme ligne" } - - it { is_expected.to eq("

1er ligne \n
2ieme ligne

") } - end - - context "with a link" do - context "using an authorized scheme" do - let(:description) { "Cliquez sur https://d-s.fr pour continuer." } - - context 'with a tag authorized' do - let(:allow_a) { true } - it { is_expected.to eq("

Cliquez sur https://d-s.fr pour continuer.

") } - end - - context 'without a tag' do - it { is_expected.to eq("

Cliquez sur https://d-s.fr pour continuer.

") } - end - end - - context "using a non-authorized scheme" do - let(:description) { "Cliquez sur file://etc/password pour continuer." } - it { is_expected.to eq("

Cliquez sur file://etc/password pour continuer.

") } - end - - context "not actually an URL" do - let(:description) { "Pour info: il ne devrait y avoir aucun lien." } - it { is_expected.to eq("

Pour info: il ne devrait y avoir aucun lien.

") } - end - end - - context "with empty decription" do - let(:description) { nil } - - it { is_expected.to eq nil } - end - - context "with a bad script" do - let(:description) { '' } - - it { is_expected.to eq('

bad

') } - end - end -end diff --git a/spec/jobs/champ_fetch_external_data_job_spec.rb b/spec/jobs/champ_fetch_external_data_job_spec.rb new file mode 100644 index 000000000..6f5277727 --- /dev/null +++ b/spec/jobs/champ_fetch_external_data_job_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ChampFetchExternalDataJob, type: :job do + let(:champ) { Struct.new(:external_id, :data).new(champ_external_id, data) } + let(:external_id) { "an ID" } + let(:champ_external_id) { "an ID" } + let(:data) { nil } + let(:fetched_data) { nil } + + subject(:perform_job) { described_class.perform_now(champ, external_id) } + + before do + allow(champ).to receive(:fetch_external_data).and_return(fetched_data) + allow(champ).to receive(:update_with_external_data!) + end + + shared_examples "a champ non-updater" do + it 'does not update the champ' do + perform_job + expect(champ).not_to have_received(:update_with_external_data!) + end + end + + context 'when external_id matches the champ external_id and the champ data is nil' do + it 'fetches external data' do + perform_job + expect(champ).to have_received(:fetch_external_data) + end + + context 'when the fetched data is present' do + let(:fetched_data) { "data" } + + it 'updates the champ' do + perform_job + expect(champ).to have_received(:update_with_external_data!).with(data: fetched_data) + end + end + + context 'when the fetched data is blank' do + it_behaves_like "a champ non-updater" + end + end + + context 'when external_id does not match the champ external_id' do + let(:champ_external_id) { "something else" } + it_behaves_like "a champ non-updater" + end + + context 'when the champ data is present' do + let(:data) { "present" } + it_behaves_like "a champ non-updater" + end +end diff --git a/spec/lib/api_education/annuaire_education_adapter_spec.rb b/spec/lib/api_education/annuaire_education_adapter_spec.rb index 2b29c9b7e..eff7bd789 100644 --- a/spec/lib/api_education/annuaire_education_adapter_spec.rb +++ b/spec/lib/api_education/annuaire_education_adapter_spec.rb @@ -38,4 +38,13 @@ describe APIEducation::AnnuaireEducationAdapter do expect { subject }.to raise_exception(APIEducation::AnnuaireEducationAdapter::InvalidSchemaError) end end + + context "when responds with empty schema" do + let(:body) { File.read('spec/fixtures/files/api_education/annuaire_education_empty.json') } + let(:status) { 200 } + + it '#to_params returns nil' do + expect(subject).to eq(nil) + end + end end diff --git a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb index bd0180fd7..29161bc24 100644 --- a/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb +++ b/spec/lib/tasks/deployment/20220705164551_remove_unused_champs_spec.rb @@ -21,9 +21,9 @@ describe '20220705164551_remove_unused_champs' do describe 'remove_unused_champs', vcr: { cassette_name: 'api_geo_all' } do it "with bad champs" do - expect(Champ.where(dossier: dossier).count).to eq(38) + expect(Champ.where(dossier: dossier).count).to eq(40) run_task - expect(Champ.where(dossier: dossier).count).to eq(37) + expect(Champ.where(dossier: dossier).count).to eq(39) end end end diff --git a/spec/lib/tasks/deployment/20230207144243_normalize_regions_spec.rake b/spec/lib/tasks/deployment/20230207144243_normalize_regions_spec.rake deleted file mode 100644 index 3a049fddc..000000000 --- a/spec/lib/tasks/deployment/20230207144243_normalize_regions_spec.rake +++ /dev/null @@ -1,139 +0,0 @@ -describe '20230207144243_normalize_regions', vcr: { cassette_name: 'api_geo_regions' } do - let(:champ) { create(:champ_regions) } - let(:rake_task) { Rake::Task['after_party:normalize_regions'] } - let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } - - subject(:run_task) { rake_task.invoke } - - before do - allow(Rails).to receive(:cache).and_return(memory_store) - Rails.cache.clear - end - - after { rake_task.reenable } - - shared_examples "a non-changer" do |external_id, value| - before { champ.update_columns(external_id:, value:) } - - it { expect { run_task }.not_to change { champ.reload.external_id } } - - it { expect { run_task }.not_to change { champ.reload.value } } - end - - shared_examples "an external_id nullifier" do |external_id, value| - before { champ.update_columns(external_id:, value:) } - - it { expect { run_task }.to change { champ.reload.external_id }.from(external_id).to(nil) } - - it { expect { run_task }.not_to change { champ.reload.value } } - end - - shared_examples "a value nullifier" do |external_id, value| - before { champ.update_columns(external_id:, value:) } - - it { expect { run_task }.not_to change { champ.reload.external_id } } - - it { expect { run_task }.to change { champ.reload.value }.from(value).to(nil) } - end - - shared_examples "an external_id and value nullifier" do |external_id, value| - before { champ.update_columns(external_id:, value:) } - - it { expect { run_task }.to change { champ.reload.external_id }.from(external_id).to(nil) } - - it { expect { run_task }.to change { champ.reload.value }.from(value).to(nil) } - end - - shared_examples "an external_id updater" do |external_id, value, expected_external_id| - before { champ.update_columns(external_id:, value:) } - - it { expect { run_task }.to change { champ.reload.external_id }.from(external_id).to(expected_external_id) } - - it { expect { run_task }.not_to change { champ.reload.value } } - end - - shared_examples "a result checker" do |external_id, value, expected_external_id, expected_value| - before do - champ.update_columns(external_id:, value:) - run_task - end - - it { expect(champ.reload.external_id).to eq(expected_external_id) } - - it { expect(champ.reload.value).to eq(expected_value) } - end - - shared_examples "a value updater" do |external_id, value, expected_value| - before { champ.update_columns(external_id:, value:) } - - it { expect { run_task }.not_to change { champ.reload.external_id } } - - it { expect { run_task }.to change { champ.reload.value }.from(value).to(expected_value) } - end - - it_behaves_like "a non-changer", nil, nil - it_behaves_like "an external_id nullifier", '', nil - it_behaves_like "a value nullifier", nil, '' - it_behaves_like "an external_id and value nullifier", '', '' - it_behaves_like "an external_id updater", nil, 'Auvergne-Rhône-Alpes', '84' - it_behaves_like "an external_id updater", '', 'Auvergne-Rhône-Alpes', '84' - it_behaves_like "a value updater", '11', nil, 'Île-de-France' - - # Integrity data check: - it_behaves_like "a result checker", "84", "Auvergne-Rhône-Alpes", "84", "Auvergne-Rhône-Alpes" - it_behaves_like "a result checker", nil, "Auvergne-Rhône-Alpes", "84", "Auvergne-Rhône-Alpes" - it_behaves_like "a result checker", '', "Auvergne-Rhône-Alpes", "84", "Auvergne-Rhône-Alpes" - it_behaves_like "a result checker", "27", "Bourgogne-Franche-Comté", "27", "Bourgogne-Franche-Comté" - it_behaves_like "a result checker", nil, "Bourgogne-Franche-Comté", "27", "Bourgogne-Franche-Comté" - it_behaves_like "a result checker", '', "Bourgogne-Franche-Comté", "27", "Bourgogne-Franche-Comté" - it_behaves_like "a result checker", "53", "Bretagne", "53", "Bretagne" - it_behaves_like "a result checker", nil, "Bretagne", "53", "Bretagne" - it_behaves_like "a result checker", '', "Bretagne", "53", "Bretagne" - it_behaves_like "a result checker", "24", "Centre-Val de Loire", "24", "Centre-Val de Loire" - it_behaves_like "a result checker", nil, "Centre-Val de Loire", "24", "Centre-Val de Loire" - it_behaves_like "a result checker", '', "Centre-Val de Loire", "24", "Centre-Val de Loire" - it_behaves_like "a result checker", "94", "Corse", "94", "Corse" - it_behaves_like "a result checker", nil, "Corse", "94", "Corse" - it_behaves_like "a result checker", '', "Corse", "94", "Corse" - it_behaves_like "a result checker", "44", "Grand Est", "44", "Grand Est" - it_behaves_like "a result checker", nil, "Grand Est", "44", "Grand Est" - it_behaves_like "a result checker", '', "Grand Est", "44", "Grand Est" - it_behaves_like "a result checker", "01", "Guadeloupe", "01", "Guadeloupe" - it_behaves_like "a result checker", nil, "Guadeloupe", "01", "Guadeloupe" - it_behaves_like "a result checker", '', "Guadeloupe", "01", "Guadeloupe" - it_behaves_like "a result checker", "03", "Guyane", "03", "Guyane" - it_behaves_like "a result checker", nil, "Guyane", "03", "Guyane" - it_behaves_like "a result checker", '', "Guyane", "03", "Guyane" - it_behaves_like "a result checker", "32", "Hauts-de-France", "32", "Hauts-de-France" - it_behaves_like "a result checker", nil, "Hauts-de-France", "32", "Hauts-de-France" - it_behaves_like "a result checker", '', "Hauts-de-France", "32", "Hauts-de-France" - it_behaves_like "a result checker", "04", "La Réunion", "04", "La Réunion" - it_behaves_like "a result checker", nil, "La Réunion", "04", "La Réunion" - it_behaves_like "a result checker", '', "La Réunion", "04", "La Réunion" - it_behaves_like "a result checker", "02", "Martinique", "02", "Martinique" - it_behaves_like "a result checker", nil, "Martinique", "02", "Martinique" - it_behaves_like "a result checker", '', "Martinique", "02", "Martinique" - it_behaves_like "a result checker", "06", "Mayotte", "06", "Mayotte" - it_behaves_like "a result checker", nil, "Mayotte", "06", "Mayotte" - it_behaves_like "a result checker", '', "Mayotte", "06", "Mayotte" - it_behaves_like "a result checker", "28", "Normandie", "28", "Normandie" - it_behaves_like "a result checker", nil, "Normandie", "28", "Normandie" - it_behaves_like "a result checker", '', "Normandie", "28", "Normandie" - it_behaves_like "a result checker", "75", "Nouvelle-Aquitaine", "75", "Nouvelle-Aquitaine" - it_behaves_like "a result checker", nil, "Nouvelle-Aquitaine", "75", "Nouvelle-Aquitaine" - it_behaves_like "a result checker", '', "Nouvelle-Aquitaine", "75", "Nouvelle-Aquitaine" - it_behaves_like "a result checker", "76", "Occitanie", "76", "Occitanie" - it_behaves_like "a result checker", nil, "Occitanie", "76", "Occitanie" - it_behaves_like "a result checker", '', "Occitanie", "76", "Occitanie" - it_behaves_like "a result checker", "52", "Pays de la Loire", "52", "Pays de la Loire" - it_behaves_like "a result checker", nil, "Pays de la Loire", "52", "Pays de la Loire" - it_behaves_like "a result checker", '', "Pays de la Loire", "52", "Pays de la Loire" - it_behaves_like "a result checker", "93", "Provence-Alpes-Côte d'Azur", "93", "Provence-Alpes-Côte d’Azur" - it_behaves_like "a result checker", nil, "Provence-Alpes-Côte d'Azur", "93", "Provence-Alpes-Côte d’Azur" - it_behaves_like "a result checker", '', "Provence-Alpes-Côte d'Azur", "93", "Provence-Alpes-Côte d’Azur" - it_behaves_like "a result checker", "93", "Provence-Alpes-Côte d’Azur", "93", "Provence-Alpes-Côte d’Azur" - it_behaves_like "a result checker", "11", "Île-de-France", "11", "Île-de-France" - it_behaves_like "a result checker", "11", nil, "11", "Île-de-France" - it_behaves_like "a result checker", nil, "Île-de-France", "11", "Île-de-France" - it_behaves_like "a result checker", '', "Île-de-France", "11", "Île-de-France" -end diff --git a/spec/mailers/groupe_instructeur_mailer_spec.rb b/spec/mailers/groupe_instructeur_mailer_spec.rb index 9157f03ef..73a1abaa9 100644 --- a/spec/mailers/groupe_instructeur_mailer_spec.rb +++ b/spec/mailers/groupe_instructeur_mailer_spec.rb @@ -1,5 +1,5 @@ RSpec.describe GroupeInstructeurMailer, type: :mailer do - describe '#remove_instructeurs' do + describe '#notify_group_when_instructeurs_removed' do let(:groupe_instructeur) do gi = GroupeInstructeur.create(label: 'gi1', procedure: create(:procedure)) gi.instructeurs << create(:instructeur, email: 'int1@g') @@ -7,15 +7,78 @@ RSpec.describe GroupeInstructeurMailer, type: :mailer do gi.instructeurs << instructeurs_to_remove gi end - let(:instructeur_1) { create(:instructeur, email: 'int3@g') } - let(:instructeur_2) { create(:instructeur, email: 'int4@g') } + let(:instructeur_3) { create(:instructeur, email: 'int3@g') } + let(:instructeur_4) { create(:instructeur, email: 'int4@g') } - let(:instructeurs_to_remove) { [instructeur_1, instructeur_2] } + let(:instructeurs_to_remove) { [instructeur_3, instructeur_4] } let(:current_instructeur_email) { 'toto@email.com' } - subject { described_class.remove_instructeurs(groupe_instructeur, instructeurs_to_remove, current_instructeur_email) } + subject { described_class.notify_group_when_instructeurs_removed(groupe_instructeur, instructeurs_to_remove, current_instructeur_email) } + + before { instructeurs_to_remove.each { groupe_instructeur.remove(_1) } } it { expect(subject.body).to include('Les instructeurs int3@g, int4@g ont été retirés du groupe') } - it { expect(subject.bcc).to match_array(['int1@g', 'int2@g', 'int3@g', 'int4@g']) } + it { expect(subject.bcc).to match_array(['int1@g', 'int2@g']) } + end + + describe '#notify_removed_instructeur' do + let(:procedure) { create(:procedure) } + let(:groupe_instructeur) do + gi = GroupeInstructeur.create(label: 'gi1', procedure: procedure) + gi.instructeurs << create(:instructeur, email: 'int1@g') + gi.instructeurs << create(:instructeur, email: 'int2@g') + gi.instructeurs << instructeur_to_remove + gi + end + let(:instructeur_to_remove) { create(:instructeur, email: 'int3@g') } + + let(:current_instructeur_email) { 'toto@email.com' } + + subject { described_class.notify_removed_instructeur(groupe_instructeur, instructeur_to_remove, current_instructeur_email) } + + before { groupe_instructeur.remove(instructeur_to_remove) } + + context 'when instructeur is fully removed form procedure' do + it { expect(subject.body).to include('Vous avez été désaffecté(e) de la démarche') } + it { expect(subject.to).to include('int3@g') } + it { expect(subject.to).not_to include('int1@g', 'int2@g') } + end + + context 'when instructeur is removed from one group but still affected to procedure' do + let!(:groupe_instructeur_2) do + gi2 = GroupeInstructeur.create(label: 'gi2', procedure: procedure) + gi2.instructeurs << instructeur_to_remove + gi2 + end + + it { expect(subject.body).to include('Vous avez été retiré(e) du groupe « gi1 » par « toto@email.com »') } + it { expect(subject.to).to include('int3@g') } + it { expect(subject.to).not_to include('int1@g', 'int2@g') } + end + end + + describe '#notify_added_instructeurs' do + let(:procedure) { create(:procedure) } + + let(:instructeurs_to_add) { [create(:instructeur, email: 'int3@g'), create(:instructeur, email: 'int4@g')] } + + let(:current_instructeur_email) { 'toto@email.com' } + + subject { described_class.notify_added_instructeurs(procedure.defaut_groupe_instructeur, instructeurs_to_add, current_instructeur_email) } + + before { instructeurs_to_add.each { procedure.defaut_groupe_instructeur.add(_1) } } + + context 'when there is only one group on procedure' do + it { expect(subject.body).to include('Vous avez été affecté(e) à la démarche') } + it { expect(subject.bcc).to match_array(['int3@g', 'int4@g']) } + end + + context 'when there are many groups on procedure' do + let!(:groupe_instructeur_2) do + GroupeInstructeur.create(label: 'gi2', procedure: procedure) + end + it { expect(subject.body).to include('Vous avez été ajouté(e) au groupe') } + it { expect(subject.bcc).to match_array(['int3@g', 'int4@g']) } + end end end diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 005db343c..25e77eb4d 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -101,4 +101,27 @@ RSpec.describe NotificationMailer, type: :mailer do end end end + + describe 'subject length' do + let(:procedure) { create(:simple_procedure, libelle: "My super long title " + ("xo " * 100)) } + let(:dossier) { create(:dossier, :en_instruction, :with_individual, :with_service, user: user, procedure: procedure) } + let(:email_template) { create(:closed_mail, subject:, body: 'Your dossier was accepted. Thanks.') } + + before do + dossier.procedure.closed_mail = email_template + end + + subject(:mail) { described_class.send_accepte_notification(dossier) } + + context "subject is too long" do + let(:subject) { 'Un long libellé --libellé démarche--' } + it { expect(mail.subject.length).to be <= 100 } + end + + context "subject should fallback to default" do + let(:subject) { "" } + it { expect(mail.subject).to match(/^Votre dossier .+ a été accepté \(My super long title/) } + it { expect(mail.subject.length).to be <= 100 } + end + end end diff --git a/spec/mailers/previews/groupe_instructeur_mailer_preview.rb b/spec/mailers/previews/groupe_instructeur_mailer_preview.rb index 1b6c006a1..d423b0b25 100644 --- a/spec/mailers/previews/groupe_instructeur_mailer_preview.rb +++ b/spec/mailers/previews/groupe_instructeur_mailer_preview.rb @@ -1,9 +1,25 @@ class GroupeInstructeurMailerPreview < ActionMailer::Preview - def remove_instructeurs + def notify_group_when_instructeurs_removed procedure = Procedure.new(id: 1, libelle: 'une superbe procedure') groupe = GroupeInstructeur.new(id: 1, label: 'Val-De-Marne', procedure:) current_instructeur_email = 'admin@dgfip.com' instructeurs = Instructeur.limit(2) - GroupeInstructeurMailer.remove_instructeurs(groupe, instructeurs, current_instructeur_email) + GroupeInstructeurMailer.notify_group_when_instructeurs_removed(groupe, instructeurs, current_instructeur_email) + end + + def notify_removed_instructeur + procedure = Procedure.new(id: 1, libelle: 'une superbe procedure') + groupe = GroupeInstructeur.new(id: 1, label: 'Val-De-Marne', procedure:) + current_instructeur_email = 'admin@dgfip.com' + instructeur = Instructeur.last + GroupeInstructeurMailer.notify_removed_instructeur(groupe, instructeur, current_instructeur_email) + end + + def notify_added_instructeurs + procedure = Procedure.new(id: 1, libelle: 'une superbe procedure') + groupe = GroupeInstructeur.new(id: 1, label: 'Val-De-Marne', procedure:) + current_instructeur_email = 'admin@dgfip.com' + instructeurs = Instructeur.limit(2) + GroupeInstructeurMailer.notify_added_instructeurs(groupe, instructeurs, current_instructeur_email) end end diff --git a/spec/models/champ_spec.rb b/spec/models/champ_spec.rb index bec9c3097..e20c55417 100644 --- a/spec/models/champ_spec.rb +++ b/spec/models/champ_spec.rb @@ -117,7 +117,7 @@ describe Champ do # when using the old form, and the ChampsService Class # TODO: to remove context 'when the value is already deserialized' do - let(:value) { '["1", "2"]' } + let(:value) { '["val1", "val2"]' } it { expect(champ.value).to eq(value) } @@ -133,9 +133,9 @@ describe Champ do # GOTCHA context 'when the value is not already deserialized' do context 'when a choice is selected' do - let(:value) { '["", "1", "2"]' } + let(:value) { '["", "val1", "val2"]' } - it { expect(champ.value).to eq('["1", "2"]') } + it { expect(champ.value).to eq('["val1", "val2"]') } end context 'when all choices are removed' do @@ -526,7 +526,7 @@ describe Champ do expect(dossier.champs_public.size).to eq(2) expect(champ.rows.size).to eq(2) - second_row = champ.rows.second + second_row = champ.reload.rows.second expect(second_row.size).to eq(1) expect(second_row.first.dossier).to eq(dossier) @@ -602,4 +602,12 @@ describe Champ do end end end + + describe '#update_with_external_data!' do + let(:champ) { create(:champ_siret) } + let(:data) { "data" } + subject { champ.update_with_external_data!(data: data) } + + it { expect { subject }.to change { champ.reload.data }.to(data) } + end end diff --git a/spec/models/champs/annuaire_education_champ_spec.rb b/spec/models/champs/annuaire_education_champ_spec.rb new file mode 100644 index 000000000..8c8f58be5 --- /dev/null +++ b/spec/models/champs/annuaire_education_champ_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Champs::AnnuaireEducationChamp do + describe '#update_with_external_data!' do + let(:champ) { create(:champ_annuaire_education, data: "any data") } + subject { champ.update_with_external_data!(data: data) } + + shared_examples "a data updater (without updating the value)" do |data| + it { expect { subject }.to change { champ.reload.data }.to(data) } + it { expect { subject }.not_to change { champ.reload.value } } + end + + context 'when data is nil' do + let(:data) { nil } + it_behaves_like "a data updater (without updating the value)", nil + end + + context 'when data is empty' do + let(:data) { '' } + it_behaves_like "a data updater (without updating the value)", '' + end + + context 'when data is inconsistent' do + let(:data) { { 'yo' => 'lo' } } + it_behaves_like "a data updater (without updating the value)", { 'yo' => 'lo' } + end + + context 'when data is consistent' do + let(:data) { + { + 'nom_etablissement': "karrigel an ankou", + 'nom_commune' => 'kumun', + 'identifiant_de_l_etablissement' => '666667' + }.with_indifferent_access + } + it { expect { subject }.to change { champ.reload.data }.to(data) } + it { expect { subject }.to change { champ.reload.value }.to('karrigel an ankou, kumun (666667)') } + end + end +end diff --git a/spec/models/champs/carte_champ_spec.rb b/spec/models/champs/carte_champ_spec.rb index b006ad092..c7e4e5c99 100644 --- a/spec/models/champs/carte_champ_spec.rb +++ b/spec/models/champs/carte_champ_spec.rb @@ -1,7 +1,7 @@ describe Champs::CarteChamp do let(:champ) { Champs::CarteChamp.new(geo_areas: geo_areas, type_de_champ: create(:type_de_champ_carte)) } let(:value) { '' } - let(:coordinates) { [[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]] } + let(:coordinates) { [[[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]]] } let(:geo_json) do { "type" => 'Polygon', diff --git a/spec/models/champs/departement_champ_spec.rb b/spec/models/champs/departement_champ_spec.rb index 35353f865..1a70439d5 100644 --- a/spec/models/champs/departement_champ_spec.rb +++ b/spec/models/champs/departement_champ_spec.rb @@ -6,9 +6,69 @@ describe Champs::DepartementChamp, type: :model do Rails.cache.clear end - let(:champ) { described_class.new } + describe 'validations', vcr: { cassette_name: 'api_geo_departements' } do + describe 'external link' do + subject { build(:champ_departements, external_id: external_id) } + + context 'when nil' do + let(:external_id) { nil } + + it { is_expected.to be_valid } + end + + context 'when blank' do + let(:external_id) { '' } + + it { is_expected.not_to be_valid } + end + + context 'when included in the departement codes' do + let(:external_id) { "01" } + + it { is_expected.to be_valid } + end + + context 'when not included in the departement codes' do + let(:external_id) { "totoro" } + + it { is_expected.not_to be_valid } + end + end + + describe 'value' do + subject { create(:champ_departements) } + + before { subject.update_columns(value: value) } + + context 'when nil' do + let(:value) { nil } + + it { is_expected.to be_valid } + end + + context 'when blank' do + let(:value) { '' } + + it { is_expected.not_to be_valid } + end + + context 'when included in the departement names' do + let(:value) { "Ain" } + + it { is_expected.to be_valid } + end + + context 'when not included in the departement names' do + let(:value) { "totoro" } + + it { is_expected.not_to be_valid } + end + end + end describe 'value', vcr: { cassette_name: 'api_geo_departements' } do + let(:champ) { described_class.new } + it 'with code having 2 chars' do champ.value = '01' expect(champ.external_id).to eq('01') diff --git a/spec/models/champs/dossier_link_champ_spec.rb b/spec/models/champs/dossier_link_champ_spec.rb new file mode 100644 index 000000000..ca69d37a9 --- /dev/null +++ b/spec/models/champs/dossier_link_champ_spec.rb @@ -0,0 +1,37 @@ +describe Champs::DossierLinkChamp, type: :model do + describe 'prefilling validations' do + describe 'value' do + subject { build(:champ_dossier_link, value: value).valid?(:prefill) } + + context 'when nil' do + let(:value) { nil } + + it { expect(subject).to eq(true) } + end + + context 'when empty' do + let(:value) { '' } + + it { expect(subject).to eq(true) } + end + + context 'when an integer' do + let(:value) { 42 } + + it { expect(subject).to eq(true) } + end + + context 'when a string representing an integer' do + let(:value) { "42" } + + it { expect(subject).to eq(true) } + end + + context 'when it can be casted as integer' do + let(:value) { 'totoro' } + + it { expect(subject).to eq(false) } + end + end + end +end diff --git a/spec/models/champs/epci_champ_spec.rb b/spec/models/champs/epci_champ_spec.rb index be06ed379..1b5a56dd4 100644 --- a/spec/models/champs/epci_champ_spec.rb +++ b/spec/models/champs/epci_champ_spec.rb @@ -6,9 +6,156 @@ describe Champs::EpciChamp, type: :model do Rails.cache.clear end - let(:champ) { described_class.new } + describe 'validations' do + describe 'code_departement', vcr: { cassette_name: 'api_geo_departements' } do + subject { build(:champ_epci, code_departement: code_departement) } + + context 'when nil' do + let(:code_departement) { nil } + + it { is_expected.to be_valid } + end + + context 'when empty' do + let(:code_departement) { '' } + + it { is_expected.not_to be_valid } + end + + context 'when included in the departement codes' do + let(:code_departement) { "01" } + + it { is_expected.to be_valid } + end + + context 'when not included in the departement codes' do + let(:code_departement) { "totoro" } + + it { is_expected.not_to be_valid } + end + end + + describe 'external_id' do + let(:champ) { build(:champ_epci, code_departement: code_departement, external_id: nil) } + + subject { champ } + + before do + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_epcis') + + champ.save! + champ.update_columns(external_id: external_id) + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_epcis') + end + + context 'when code_departement is nil' do + let(:code_departement) { nil } + let(:external_id) { nil } + + it { is_expected.to be_valid } + end + + context 'when code_departement is not nil and valid' do + let(:code_departement) { "01" } + + context 'when external_id is nil' do + let(:external_id) { nil } + + it { is_expected.to be_valid } + end + + context 'when external_id is empty' do + let(:external_id) { '' } + + it { is_expected.not_to be_valid } + end + + context 'when external_id is included in the epci codes of the departement' do + let(:external_id) { '200042935' } + + it { is_expected.to be_valid } + end + + context 'when external_id is not included in the epci codes of the departement' do + let(:external_id) { 'totoro' } + + it { is_expected.not_to be_valid } + end + end + end + + describe 'value' do + let(:champ) { build(:champ_epci, code_departement: code_departement, external_id: nil, value: nil) } + + subject { champ } + + before do + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_epcis') + + champ.save! + champ.update_columns(external_id: external_id, value: value) + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_epcis') + end + + context 'when code_departement is nil' do + let(:code_departement) { nil } + let(:external_id) { nil } + let(:value) { nil } + + it { is_expected.to be_valid } + end + + context 'when external_id is nil' do + let(:code_departement) { '01' } + let(:external_id) { nil } + let(:value) { nil } + + it { is_expected.to be_valid } + end + + context 'when code_departement and external_id are not nil and valid' do + let(:code_departement) { '01' } + let(:external_id) { '200042935' } + + context 'when value is nil' do + let(:value) { nil } + + it { is_expected.to be_valid } + end + + context 'when value is empty' do + let(:value) { '' } + + it { is_expected.not_to be_valid } + end + + context 'when value is in departement epci names' do + let(:value) { 'CA Haut - Bugey Agglomération' } + + it { is_expected.to be_valid } + end + + context 'when value is not in departement epci names' do + let(:value) { 'totoro' } + + it { is_expected.not_to be_valid } + end + end + end + end describe 'value', vcr: { cassette_name: 'api_geo_epcis' } do + let(:champ) { described_class.new } it 'with departement and code' do champ.code_departement = '01' champ.value = '200042935' diff --git a/spec/models/champs/multiple_drop_down_list_champ_spec.rb b/spec/models/champs/multiple_drop_down_list_champ_spec.rb new file mode 100644 index 000000000..08a78c77c --- /dev/null +++ b/spec/models/champs/multiple_drop_down_list_champ_spec.rb @@ -0,0 +1,38 @@ +describe Champs::MultipleDropDownListChamp do + describe 'validations' do + describe 'inclusion' do + let(:type_de_champ) { build(:type_de_champ_multiple_drop_down_list, drop_down_list_value: "val1\r\nval2\r\nval3") } + subject { build(:champ_multiple_drop_down_list, type_de_champ:, value:) } + + context 'when the value is nil' do + let(:value) { nil } + + it { is_expected.to be_valid } + end + + context 'when the value is an empty string' do + let(:value) { '' } + + it { is_expected.to be_valid } + end + + context 'when the value is an empty array' do + let(:value) { [] } + + it { is_expected.to be_valid } + end + + context 'when the value is included in the option list' do + let(:value) { ["val3", "val1"] } + + it { is_expected.to be_valid } + end + + context 'when the value is not included in the option list' do + let(:value) { ["totoro", "val1"] } + + it { is_expected.not_to be_valid } + end + end + end +end diff --git a/spec/models/concern/dossier_prefillable_concern_spec.rb b/spec/models/concern/dossier_prefillable_concern_spec.rb index 3ab8f73e9..32a52cad8 100644 --- a/spec/models/concern/dossier_prefillable_concern_spec.rb +++ b/spec/models/concern/dossier_prefillable_concern_spec.rb @@ -6,7 +6,10 @@ RSpec.describe DossierPrefillableConcern do let(:dossier) { create(:dossier, :brouillon, procedure: procedure) } let(:types_de_champ_public) { [] } - subject(:fill) { dossier.prefill!(values); dossier.reload } + subject(:fill) do + dossier.prefill!(values) + dossier.reload + end shared_examples 'a dossier marked as prefilled' do it 'marks the dossier as prefilled' do diff --git a/spec/models/concern/dossier_sections_concern_spec.rb b/spec/models/concern/dossier_sections_concern_spec.rb new file mode 100644 index 000000000..c4c329f10 --- /dev/null +++ b/spec/models/concern/dossier_sections_concern_spec.rb @@ -0,0 +1,69 @@ +describe DossierSectionsConcern do + describe '#auto_numbering_section_headers_for?' do + let(:public_libelle) { "Infos" } + let(:private_libelle) { "Infos Private" } + let(:types_de_champ_public) { [{ type: :header_section, libelle: public_libelle }, { type: :header_section, libelle: "Details" }] } + let(:types_de_champ_private) { [{ type: :header_section, libelle: private_libelle }, { type: :header_section, libelle: "Details Private" }] } + + let(:procedure) { create(:procedure, :for_individual, types_de_champ_public:, types_de_champ_private:) } + let(:dossier) { create(:dossier, procedure: procedure) } + + context "with no section having number" do + it { expect(dossier.auto_numbering_section_headers_for?(dossier.champs_public[1])).to eq(true) } + it { expect(dossier.auto_numbering_section_headers_for?(dossier.champs_private[1])).to eq(true) } + end + + context "with public section having number" do + let(:public_libelle) { "1 - infos" } + it { expect(dossier.auto_numbering_section_headers_for?(dossier.champs_public[1])).to eq(false) } + it { expect(dossier.auto_numbering_section_headers_for?(dossier.champs_private[1])).to eq(true) } + end + + context "with private section having number" do + let(:private_libelle) { "1 - infos private" } + it { expect(dossier.auto_numbering_section_headers_for?(dossier.champs_public[1])).to eq(true) } + it { expect(dossier.auto_numbering_section_headers_for?(dossier.champs_private[1])).to eq(false) } + end + end + + describe '#index_for_section_header' do + include Logic + let(:number_stable_id) { 99 } + let(:types_de_champ) { + [ + { type: :header_section, libelle: "Infos" }, { type: :integer_number, stable_id: number_stable_id }, + { type: :header_section, libelle: "Details", condition: ds_eq(champ_value(99), constant(5)) }, { type: :header_section, libelle: "Conclusion" } + ] +} + + let(:procedure) { create(:procedure, :for_individual, types_de_champ_public: types_de_champ) } + let(:dossier) { create(:dossier, procedure: procedure) } + + let(:headers) { dossier.champs_public.filter(&:header_section?) } + + let(:number_value) { nil } + + before do + dossier.champs_public.find { _1.stable_id == number_stable_id }.update(value: number_value) + dossier.reload + end + + context "when there are invisible sections" do + it "index accordingly header sections" do + expect(dossier.index_for_section_header(headers[0])).to eq(1) + expect(headers[1]).not_to be_visible + expect(dossier.index_for_section_header(headers[2])).to eq(2) + end + end + + context "when all headers are visible" do + let(:number_value) { 5 } + it "index accordingly header sections" do + expect(dossier.index_for_section_header(headers[0])).to eq(1) + expect(headers[1]).to be_visible + expect(dossier.index_for_section_header(headers[1])).to eq(2) + expect(dossier.index_for_section_header(headers[2])).to eq(3) + end + end + end +end diff --git a/spec/models/concern/procedure_stats_concern_spec.rb b/spec/models/concern/procedure_stats_concern_spec.rb index 553b1c46c..9ab68e1e8 100644 --- a/spec/models/concern/procedure_stats_concern_spec.rb +++ b/spec/models/concern/procedure_stats_concern_spec.rb @@ -7,15 +7,17 @@ describe ProcedureStatsConcern do before do create_list(:dossier, 2, :brouillon, procedure: procedure) create(:dossier, :en_instruction, procedure: procedure) + create(:dossier, procedure: procedure, for_procedure_preview: true) + create(:dossier, :accepte, procedure: procedure, hidden_by_administration_at: Time.zone.now) end it "returns the funnel stats" do expect(stats_dossiers_funnel).to match( [ - ['Démarrés', procedure.dossiers.count], - ['Déposés', procedure.dossiers.state_not_brouillon.count], - ['Instruction débutée', procedure.dossiers.state_instruction_commencee.count], - ['Traités', procedure.dossiers.state_termine.count] + ['Démarrés', procedure.dossiers.visible_by_user_or_administration.count], + ['Déposés', procedure.dossiers.visible_by_administration.count], + ['Instruction débutée', procedure.dossiers.visible_by_administration.state_instruction_commencee.count], + ['Traités', procedure.dossiers.visible_by_administration.state_termine.count] ] ) end diff --git a/spec/models/concern/rna_champ_association_fetchable_concern_spec.rb b/spec/models/concern/rna_champ_association_fetchable_concern_spec.rb new file mode 100644 index 000000000..75eddd05f --- /dev/null +++ b/spec/models/concern/rna_champ_association_fetchable_concern_spec.rb @@ -0,0 +1,95 @@ +RSpec.describe RNAChampAssociationFetchableConcern do + describe '.fetch_association!' do + let!(:champ) { create(:champ_rna, data: "not nil data", value: 'W173847273') } + + before do + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/associations\//) + .to_return(body: body, status: status) + allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(false) + end + + subject(:fetch_association!) { champ.fetch_association!(rna) } + + shared_examples "an association fetcher" do |expected_result, expected_error, expected_value, expected_data| + it { expect { fetch_association! }.to change { champ.reload.value }.to(expected_value) } + + it { expect { fetch_association! }.to change { champ.reload.data }.to(expected_data) } + + it { expect(fetch_association!).to eq(expected_result) } + + it 'populates the association_fetch_error_key when an error occurs' do + fetch_association! + expect(champ.association_fetch_error_key).to eq(expected_error) + end + end + + context 'when the RNA is empty' do + let(:rna) { '' } + let(:status) { 422 } + let(:body) { '' } + + it_behaves_like "an association fetcher", false, :empty, '', nil + end + + context 'when the RNA is invalid' do + let(:rna) { '1234' } + let(:status) { 422 } + let(:body) { '' } + + it_behaves_like "an association fetcher", false, :invalid, '1234', nil + end + + context 'when the RNA is unknow' do + let(:rna) { 'W111111111' } + let(:status) { 404 } + let(:body) { '' } + + it_behaves_like "an association fetcher", false, :not_found, 'W111111111', nil + end + + context 'when the API is unavailable due to network error' do + let(:rna) { 'W595001988' } + let(:status) { 503 } + let(:body) { File.read('spec/fixtures/files/api_entreprise/associations.json') } + + before { expect(APIEntrepriseService).to receive(:api_up?).and_return(false) } + + it_behaves_like "an association fetcher", false, :network_error, 'W595001988', nil + end + + context 'when the RNA informations are retrieved successfully' do + let(:rna) { 'W595001988' } + let(:status) { 200 } + let(:body) { File.read('spec/fixtures/files/api_entreprise/associations.json') } + + it_behaves_like "an association fetcher", true, nil, 'W595001988', { + "association_id" => "W595001988", + "association_titre" => "UN SUR QUATRE", + "association_objet" => "valoriser, transmettre et partager auprès des publics les plus larges possibles, les bienfaits de l'immigration, la richesse de la diversité et la curiosité de l'autre autrement", + "association_siret" => nil, + "association_date_creation" => "2014-01-23", + "association_date_declaration" => "2014-01-24", + "association_date_publication" => "2014-02-08", + "association_date_dissolution" => "0001-01-01", + "association_adresse_siege" => { + "complement" => "", + "numero_voie" => "61", + "type_voie" => "RUE", + "libelle_voie" => "des Noyers", + "distribution" => "_", + "code_insee" => "93063", + "code_postal" => "93230", + "commune" => "Romainville" + }, + "association_code_civilite_dirigeant" => "PM", + "association_civilite_dirigeant" => "Monsieur le Président", + "association_code_etat" => "A", + "association_etat" => "Active", + "association_code_groupement" => "S", + "association_groupement" => "simple", + "association_mise_a_jour" => 1392295833, + "association_rna" => "W595001988" + } + end + end +end diff --git a/spec/models/concern/siret_champ_etablissement_fetchable_concern_spec.rb b/spec/models/concern/siret_champ_etablissement_fetchable_concern_spec.rb new file mode 100644 index 000000000..6a2c11120 --- /dev/null +++ b/spec/models/concern/siret_champ_etablissement_fetchable_concern_spec.rb @@ -0,0 +1,115 @@ +RSpec.describe SiretChampEtablissementFetchableConcern do + describe '.fetch_etablissement!' do + let(:api_etablissement_status) { 200 } + let(:api_etablissement_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') } + let(:token_expired) { false } + let!(:champ) { create(:champ_siret) } + + before do + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/) + .to_return(status: api_etablissement_status, body: api_etablissement_body) + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siret[0..8]}/) + .to_return(body: File.read('spec/fixtures/files/api_entreprise/entreprises.json'), status: 200) + allow_any_instance_of(APIEntrepriseToken).to receive(:roles) + .and_return(["attestations_fiscales", "attestations_sociales", "bilans_entreprise_bdf"]) + allow_any_instance_of(APIEntrepriseToken).to receive(:expired?).and_return(token_expired) + end + + subject(:fetch_etablissement!) { champ.fetch_etablissement!(siret, build_stubbed(:user)) } + + shared_examples 'an error occured' do |error| + it { expect { fetch_etablissement! }.to change { champ.reload.etablissement }.to(nil) } + + it { expect { fetch_etablissement! }.to change { Etablissement.count }.by(-1) } + + it { expect(fetch_etablissement!).to eq(false) } + + it 'populates the etablissement_fetch_error_key' do + fetch_etablissement! + expect(champ.etablissement_fetch_error_key).to eq(error) + end + end + + context 'when the SIRET is empty' do + let(:siret) { '' } + + it_behaves_like 'an error occured', :empty + end + + context "when the SIRET is invalid because of it's length" do + let(:siret) { '1234' } + + it_behaves_like 'an error occured', :invalid_length + end + + context "when the SIRET is invalid because of it's checksum" do + let(:siret) { '82812345600023' } + + it_behaves_like 'an error occured', :invalid_checksum + end + + context 'when the API is unavailable due to network error' do + let(:siret) { '82161143100015' } + let(:api_etablissement_status) { 503 } + + before { expect(APIEntrepriseService).to receive(:api_up?).and_return(true) } + + it_behaves_like 'an error occured', :network_error + + it 'sends the error to Sentry' do + expect(Sentry).to receive(:capture_exception) + fetch_etablissement! + end + end + + context 'when the API is unavailable due to an api maintenance or pb' do + let(:siret) { '82161143100015' } + let(:api_etablissement_status) { 502 } + + before { expect(APIEntrepriseService).to receive(:api_up?).and_return(false) } + + it { expect { fetch_etablissement! }.to change { champ.reload.value }.to(siret) } + + it { expect { fetch_etablissement! }.to change { champ.reload.etablissement } } + + it { expect { fetch_etablissement! }.to change { champ.reload.etablissement.as_degraded_mode? }.to(true) } + + it { expect { fetch_etablissement! }.to change { Etablissement.count }.by(1) } + + it { expect(fetch_etablissement!).to eq(false) } + + it 'populates the etablissement_fetch_error_key' do + fetch_etablissement! + expect(champ.etablissement_fetch_error_key).to eq(:api_entreprise_down) + end + end + + context 'when the SIRET is valid but unknown' do + let(:siret) { '00000000000000' } + let(:api_etablissement_status) { 404 } + + it_behaves_like 'an error occured', :not_found + end + + context 'when the SIRET informations are retrieved successfully' do + let(:siret) { '41816609600051' } + let(:api_etablissement_status) { 200 } + let(:api_etablissement_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') } + + it { expect { fetch_etablissement! }.to change { champ.reload.value }.to(siret) } + + it { expect { fetch_etablissement! }.to change { champ.reload.etablissement.siret }.to(siret) } + + it { expect { fetch_etablissement! }.to change { champ.reload.etablissement.naf }.to("6202A") } + + it { expect { fetch_etablissement! }.to change { Etablissement.count }.by(1) } + + it { expect(fetch_etablissement!).to eq(true) } + + it "fetches the entreprise raison sociale" do + fetch_etablissement! + expect(champ.reload.etablissement.entreprise_raison_sociale).to eq("OCTO-TECHNOLOGY") + end + end + end +end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index 9a684dcb7..5dc4d8f28 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -1516,8 +1516,8 @@ describe Dossier do { type: 'Feature', geometry: { - 'coordinates' => [[[2.428439855575562, 46.538476837725796], [2.4284291267395024, 46.53842148758162], [2.4282521009445195, 46.53841410755813], [2.42824137210846, 46.53847314771794], [2.428284287452698, 46.53847314771794], [2.428364753723145, 46.538487907747864], [2.4284291267395024, 46.538491597754714], [2.428439855575562, 46.538476837725796]]], - 'type' => 'Polygon' + coordinates: [[[2.428439855575562, 46.538476837725796], [2.4284291267395024, 46.53842148758162], [2.4282521009445195, 46.53841410755813], [2.42824137210846, 46.53847314771794], [2.428284287452698, 46.53847314771794], [2.428364753723145, 46.538487907747864], [2.4284291267395024, 46.538491597754714], [2.428439855575562, 46.538476837725796]]], + type: 'Polygon' }, properties: { area: 103.6, @@ -1861,7 +1861,7 @@ describe Dossier do let(:champ_repetition) { create(:champ_repetition, type_de_champ: type_de_champ_repetition, dossier: dossier) } before { dossier.champs_public << champ_repetition } - it { expect(Champs::RepetitionChamp.where(dossier: new_dossier).first.champs.count).to eq(2) } + it { expect(Champs::RepetitionChamp.where(dossier: new_dossier).first.champs.count).to eq(4) } it { expect(Champs::RepetitionChamp.where(dossier: new_dossier).first.champs.ids).not_to eq(champ_repetition.champs.ids) } end @@ -2080,6 +2080,15 @@ describe Dossier do end end + describe 'update procedure dossiers count' do + let(:dossier) { create(:dossier, :brouillon, :with_individual) } + + it 'update procedure dossiers count when passing to construction' do + expect(dossier.procedure).to receive(:compute_dossiers_count) + dossier.passer_en_construction! + end + end + private def count_for_month(processed_by_month, month) diff --git a/spec/models/geo_area_spec.rb b/spec/models/geo_area_spec.rb index 0a746f363..0cd51a7c6 100644 --- a/spec/models/geo_area_spec.rb +++ b/spec/models/geo_area_spec.rb @@ -23,7 +23,7 @@ RSpec.describe GeoArea, type: :model do it { expect(geo_area.location).to eq("46°32'19\"N 2°25'42\"E") } end - describe '#rgeo_geometry' do + describe '#geometry' do let(:geo_area) { build(:geo_area, :polygon, champ: nil) } let(:polygon) do { @@ -47,9 +47,9 @@ RSpec.describe GeoArea, type: :model do context 'polygon_with_extra_coordinate' do let(:geo_area) { build(:geo_area, :polygon_with_extra_coordinate, champ: nil) } + before { geo_area.valid? } - it { expect(geo_area.geometry).not_to eq(polygon) } - it { expect(geo_area.safe_geometry).to eq(polygon) } + it { expect(geo_area.geometry).to eq(polygon) } end end diff --git a/spec/models/prefill_description_spec.rb b/spec/models/prefill_description_spec.rb index 65c8cd1a5..7a7634873 100644 --- a/spec/models/prefill_description_spec.rb +++ b/spec/models/prefill_description_spec.rb @@ -22,7 +22,7 @@ RSpec.describe PrefillDescription, type: :model do it { expect(types_de_champ.count).to eq(1) } - it { expect(types_de_champ.first).to eql(TypesDeChamp::PrefillTypeDeChamp.build(type_de_champ)) } + it { expect(types_de_champ.first).to eql(TypesDeChamp::PrefillTypeDeChamp.build(type_de_champ, procedure.active_revision)) } shared_examples "filters out non fillable types de champ" do |type_de_champ_name| context "when the procedure has a #{type_de_champ_name} champ" do @@ -86,36 +86,80 @@ RSpec.describe PrefillDescription, type: :model do end end - describe '#prefill_link' do + describe '#prefill_link', vcr: { cassette_name: 'api_geo_regions' } do let(:procedure) { create(:procedure) } - let(:type_de_champ) { create(:type_de_champ_text, procedure: procedure) } + let(:type_de_champ_text) { build(:type_de_champ_text, procedure: procedure) } + let(:type_de_champ_epci) { build(:type_de_champ_epci, procedure: procedure) } + let(:type_de_champ_repetition) { create(:type_de_champ_repetition, :with_types_de_champ, :with_region_types_de_champ, procedure: procedure) } + let(:prefillable_subchamps) { TypesDeChamp::PrefillRepetitionTypeDeChamp.new(type_de_champ_repetition, procedure.active_revision).send(:prefillable_subchamps) } + let(:region_repetition) { prefillable_subchamps.third } let(:prefill_description) { described_class.new(procedure) } - before { prefill_description.update(selected_type_de_champ_ids: [type_de_champ.id]) } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_epcis') + + prefill_description.update(selected_type_de_champ_ids: [type_de_champ_text.id, type_de_champ_epci.id, type_de_champ_repetition.id]) + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_epcis') + end it "builds the URL to create a new prefilled dossier" do expect(prefill_description.prefill_link).to eq( - commencer_url( - path: procedure.path, - "champ_#{type_de_champ.to_typed_id}" => I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}") + CGI.unescape( + commencer_url( + path: procedure.path, + "champ_#{type_de_champ_text.to_typed_id_for_query}" => TypesDeChamp::PrefillTypeDeChamp.build(type_de_champ_text, procedure.active_revision).example_value, + "champ_#{type_de_champ_epci.to_typed_id_for_query}" => TypesDeChamp::PrefillTypeDeChamp.build(type_de_champ_epci, procedure.active_revision).example_value, + "champ_#{type_de_champ_repetition.to_typed_id_for_query}" => TypesDeChamp::PrefillTypeDeChamp.build(type_de_champ_repetition, procedure.active_revision).example_value + ) ) ) end end - describe '#prefill_query' do + describe '#prefill_query', vcr: { cassette_name: 'api_geo_regions' } do let(:procedure) { create(:procedure) } - let(:type_de_champ) { create(:type_de_champ_text, procedure: procedure) } + let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) } + let(:type_de_champ_epci) { TypesDeChamp::PrefillTypeDeChamp.build(create(:type_de_champ_epci, procedure: procedure), procedure.active_revision) } + let(:type_de_champ_repetition) { build(:type_de_champ_repetition, :with_types_de_champ, :with_region_types_de_champ, procedure: procedure) } + let(:prefillable_subchamps) { TypesDeChamp::PrefillRepetitionTypeDeChamp.new(type_de_champ_repetition, procedure.active_revision).send(:prefillable_subchamps) } + let(:text_repetition) { prefillable_subchamps.first } + let(:integer_repetition) { prefillable_subchamps.second } + let(:region_repetition) { prefillable_subchamps.third } let(:prefill_description) { described_class.new(procedure) } let(:expected_query) do <<~TEXT curl --request POST '#{api_public_v1_dossiers_url(procedure)}' \\ --header 'Content-Type: application/json' \\ - --data '{"champ_#{type_de_champ.to_typed_id}": "#{I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}")}"}' + --data '{"champ_#{type_de_champ_text.to_typed_id_for_query}":"Texte court","champ_#{type_de_champ_epci.to_typed_id_for_query}":["01","200042935"],"champ_#{type_de_champ_repetition.to_typed_id_for_query}":[{"champ_#{text_repetition.to_typed_id_for_query}":"Texte court","champ_#{integer_repetition.to_typed_id_for_query}":"42","champ_#{region_repetition.to_typed_id_for_query}":"53"},{"champ_#{text_repetition.to_typed_id_for_query}":"Texte court","champ_#{integer_repetition.to_typed_id_for_query}":"42","champ_#{region_repetition.to_typed_id_for_query}":"53"}]}' TEXT end - before { prefill_description.update(selected_type_de_champ_ids: [type_de_champ.id]) } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_epcis') + + prefill_description.update(selected_type_de_champ_ids: [type_de_champ_text.id, type_de_champ_epci.id, type_de_champ_repetition.id]) + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_epcis') + end it "builds the query to create a new prefilled dossier" do expect(prefill_description.prefill_query).to eq(expected_query) diff --git a/spec/models/prefill_params_spec.rb b/spec/models/prefill_params_spec.rb index 84c99a26b..ccea47657 100644 --- a/spec/models/prefill_params_spec.rb +++ b/spec/models/prefill_params_spec.rb @@ -1,5 +1,5 @@ RSpec.describe PrefillParams do - describe "#to_a", vcr: { cassette_name: 'api_geo_regions' } do + describe "#to_a", vcr: { cassette_name: 'api_geo_all' } do let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } let(:procedure) { create(:procedure, :published, types_de_champ_public:, types_de_champ_private:) } @@ -12,6 +12,18 @@ RSpec.describe PrefillParams do before do allow(Rails).to receive(:cache).and_return(memory_store) Rails.cache.clear + + VCR.insert_cassette('api_geo_regions') + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_communes') + VCR.insert_cassette('api_geo_epcis') + end + + after do + VCR.eject_cassette('api_geo_regions') + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_communes') + VCR.eject_cassette('api_geo_epcis') end context "when the stable ids match the TypeDeChamp of the corresponding procedure" do @@ -26,8 +38,8 @@ RSpec.describe PrefillParams do let(:params) { { - "champ_#{type_de_champ_1.to_typed_id}" => value_1, - "champ_#{type_de_champ_2.to_typed_id}" => value_2 + "champ_#{type_de_champ_1.to_typed_id_for_query}" => value_1, + "champ_#{type_de_champ_2.to_typed_id_for_query}" => value_2 } } @@ -43,7 +55,7 @@ RSpec.describe PrefillParams do let(:type_de_champ) { procedure.published_revision.types_de_champ_public.first } let(:types_de_champ_public) { [{ type: :text }] } - let(:params) { { type_de_champ.to_typed_id => "value" } } + let(:params) { { type_de_champ.to_typed_id_for_query => "value" } } it "filters out the champ" do expect(prefill_params_array).to match([]) @@ -61,7 +73,7 @@ RSpec.describe PrefillParams do context 'when there is no Champ that matches the TypeDeChamp with the given stable id' do let!(:type_de_champ) { create(:type_de_champ_text) } # goes to another procedure - let(:params) { { "champ_#{type_de_champ.to_typed_id}" => "value" } } + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => "value" } } it "filters out the param" do expect(prefill_params_array).to match([]) @@ -72,12 +84,12 @@ RSpec.describe PrefillParams do context "when the type de champ is authorized (#{type_de_champ_type})" do let(:types_de_champ_public) { [{ type: type_de_champ_type }] } let(:type_de_champ) { procedure.published_revision.types_de_champ_public.first } - let(:champ_id) { find_champ_by_stable_id(dossier, type_de_champ.stable_id).id } + let(:champ) { find_champ_by_stable_id(dossier, type_de_champ.stable_id) } - let(:params) { { "champ_#{type_de_champ.to_typed_id}" => value } } + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => value } } - it "builds an array of hash(id, value) matching the given params" do - expect(prefill_params_array).to match([{ id: champ_id, value: value }]) + it "builds an array of hash matching the given params" do + expect(prefill_params_array).to match([{ id: champ.id }.merge(attributes(champ, value))]) end end end @@ -86,12 +98,12 @@ RSpec.describe PrefillParams do context "when the type de champ is authorized (#{type_de_champ_type})" do let(:types_de_champ_private) { [{ type: type_de_champ_type }] } let(:type_de_champ) { procedure.published_revision.types_de_champ_private.first } - let(:champ_id) { find_champ_by_stable_id(dossier, type_de_champ.stable_id).id } + let(:champ) { find_champ_by_stable_id(dossier, type_de_champ.stable_id) } - let(:params) { { "champ_#{type_de_champ.to_typed_id}" => value } } + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => value } } - it "builds an array of hash(id, value) matching the given params" do - expect(prefill_params_array).to match([{ id: champ_id, value: value }]) + it "builds an array of hash matching the given params" do + expect(prefill_params_array).to match([{ id: champ.id }.merge(attributes(champ, value))]) end end end @@ -100,7 +112,7 @@ RSpec.describe PrefillParams do let(:types_de_champ_public) { [{ type: type_de_champ_type }] } let(:type_de_champ) { procedure.published_revision.types_de_champ_public.first } - let(:params) { { "champ_#{type_de_champ.to_typed_id}" => value } } + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => value } } context "when the type de champ is unauthorized (#{type_de_champ_type})" do it "filters out the param" do @@ -118,6 +130,7 @@ RSpec.describe PrefillParams do it_behaves_like "a champ public value that is authorized", :iban, "value" it_behaves_like "a champ public value that is authorized", :civilite, "M." it_behaves_like "a champ public value that is authorized", :pays, "FR" + it_behaves_like "a champ public value that is authorized", :regions, "03" it_behaves_like "a champ public value that is authorized", :date, "2022-12-22" it_behaves_like "a champ public value that is authorized", :datetime, "2022-12-22T10:30" it_behaves_like "a champ public value that is authorized", :yes_no, "true" @@ -125,7 +138,28 @@ RSpec.describe PrefillParams do it_behaves_like "a champ public value that is authorized", :checkbox, "true" it_behaves_like "a champ public value that is authorized", :checkbox, "false" it_behaves_like "a champ public value that is authorized", :drop_down_list, "value" - it_behaves_like "a champ public value that is authorized", :regions, "03" + it_behaves_like "a champ public value that is authorized", :departements, "03" + it_behaves_like "a champ public value that is authorized", :communes, ['01', '01457'] + it_behaves_like "a champ public value that is authorized", :annuaire_education, "0050009H" + it_behaves_like "a champ public value that is authorized", :multiple_drop_down_list, ["val1", "val2"] + it_behaves_like "a champ public value that is authorized", :dossier_link, "1" + it_behaves_like "a champ public value that is authorized", :epci, ['01', '200042935'] + it_behaves_like "a champ public value that is authorized", :siret, "13002526500013" + it_behaves_like "a champ public value that is authorized", :rna, "value" + + context "when the public type de champ is authorized (repetition)" do + let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :text }] }] } + let(:type_de_champ) { procedure.published_revision.types_de_champ_public.first } + let(:type_de_champ_child) { procedure.published_revision.children_of(type_de_champ).first } + let(:type_de_champ_child_value) { "value" } + let(:type_de_champ_child_value2) { "value2" } + + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => [{ "champ_#{type_de_champ_child.to_typed_id_for_query}" => type_de_champ_child_value }, { "champ_#{type_de_champ_child.to_typed_id_for_query}" => type_de_champ_child_value2 }] } } + + it "builds an array of hash(id, value) matching the given params" do + expect(prefill_params_array).to match([{ id: type_de_champ_child.champ.first.id, value: type_de_champ_child_value }, { id: type_de_champ_child.champ.second.id, value: type_de_champ_child_value2 }]) + end + end it_behaves_like "a champ private value that is authorized", :text, "value" it_behaves_like "a champ private value that is authorized", :textarea, "value" @@ -136,6 +170,7 @@ RSpec.describe PrefillParams do it_behaves_like "a champ private value that is authorized", :iban, "value" it_behaves_like "a champ private value that is authorized", :civilite, "M." it_behaves_like "a champ private value that is authorized", :pays, "FR" + it_behaves_like "a champ private value that is authorized", :regions, "93" it_behaves_like "a champ private value that is authorized", :date, "2022-12-22" it_behaves_like "a champ private value that is authorized", :datetime, "2022-12-22T10:30" it_behaves_like "a champ private value that is authorized", :yes_no, "true" @@ -144,23 +179,42 @@ RSpec.describe PrefillParams do it_behaves_like "a champ private value that is authorized", :checkbox, "false" it_behaves_like "a champ private value that is authorized", :drop_down_list, "value" it_behaves_like "a champ private value that is authorized", :regions, "93" + it_behaves_like "a champ private value that is authorized", :rna, "value" + it_behaves_like "a champ private value that is authorized", :siret, "13002526500013" + it_behaves_like "a champ private value that is authorized", :departements, "03" + it_behaves_like "a champ private value that is authorized", :communes, ['01', '01457'] + it_behaves_like "a champ private value that is authorized", :annuaire_education, "0050009H" + it_behaves_like "a champ private value that is authorized", :multiple_drop_down_list, ["val1", "val2"] + it_behaves_like "a champ private value that is authorized", :dossier_link, "1" + it_behaves_like "a champ private value that is authorized", :epci, ['01', '200042935'] + + context "when the private type de champ is authorized (repetition)" do + let(:types_de_champ_private) { [{ type: :repetition, children: [{ type: :text }] }] } + let(:type_de_champ) { procedure.published_revision.types_de_champ_private.first } + let(:type_de_champ_child) { procedure.published_revision.children_of(type_de_champ).first } + let(:type_de_champ_child_value) { "value" } + let(:type_de_champ_child_value2) { "value2" } + + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => [{ "champ_#{type_de_champ_child.to_typed_id_for_query}" => type_de_champ_child_value }, { "champ_#{type_de_champ_child.to_typed_id_for_query}" => type_de_champ_child_value2 }] } } + + it "builds an array of hash(id, value) matching the given params" do + expect(prefill_params_array).to match([{ id: type_de_champ_child.champ.first.id, value: type_de_champ_child_value }, { id: type_de_champ_child.champ.second.id, value: type_de_champ_child_value2 }]) + end + end it_behaves_like "a champ public value that is unauthorized", :decimal_number, "non decimal string" it_behaves_like "a champ public value that is unauthorized", :integer_number, "non integer string" it_behaves_like "a champ public value that is unauthorized", :number, "value" - it_behaves_like "a champ public value that is unauthorized", :communes, "value" it_behaves_like "a champ public value that is unauthorized", :dossier_link, "value" it_behaves_like "a champ public value that is unauthorized", :titre_identite, "value" it_behaves_like "a champ public value that is unauthorized", :civilite, "value" it_behaves_like "a champ public value that is unauthorized", :date, "value" it_behaves_like "a champ public value that is unauthorized", :datetime, "value" it_behaves_like "a champ public value that is unauthorized", :datetime, "12-22-2022T10:30" - it_behaves_like "a champ public value that is unauthorized", :multiple_drop_down_list, "value" it_behaves_like "a champ public value that is unauthorized", :linked_drop_down_list, "value" it_behaves_like "a champ public value that is unauthorized", :header_section, "value" it_behaves_like "a champ public value that is unauthorized", :explication, "value" it_behaves_like "a champ public value that is unauthorized", :piece_justificative, "value" - it_behaves_like "a champ public value that is unauthorized", :repetition, "value" it_behaves_like "a champ public value that is unauthorized", :cnaf, "value" it_behaves_like "a champ public value that is unauthorized", :dgfip, "value" it_behaves_like "a champ public value that is unauthorized", :pole_emploi, "value" @@ -170,9 +224,32 @@ RSpec.describe PrefillParams do it_behaves_like "a champ public value that is unauthorized", :pays, "value" it_behaves_like "a champ public value that is unauthorized", :regions, "value" it_behaves_like "a champ public value that is unauthorized", :departements, "value" - it_behaves_like "a champ public value that is unauthorized", :siret, "value" - it_behaves_like "a champ public value that is unauthorized", :rna, "value" - it_behaves_like "a champ public value that is unauthorized", :annuaire_education, "value" + it_behaves_like "a champ public value that is unauthorized", :communes, "value" + it_behaves_like "a champ public value that is unauthorized", :multiple_drop_down_list, ["value"] + + context "when the public type de champ is unauthorized because of wrong value format (repetition)" do + let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :text }] }] } + let(:type_de_champ) { procedure.published_revision.types_de_champ_public.first } + let(:type_de_champ_child) { procedure.published_revision.children_of(type_de_champ).first } + + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => "value" } } + + it "builds an array of hash(id, value) matching the given params" do + expect(prefill_params_array).to match([]) + end + end + + context "when the public type de champ is unauthorized because of wrong value typed_id (repetition)" do + let(:types_de_champ_public) { [{ type: :repetition, children: [{ type: :text }] }] } + let(:type_de_champ) { procedure.published_revision.types_de_champ_public.first } + let(:type_de_champ_child) { procedure.published_revision.children_of(type_de_champ).first } + + let(:params) { { "champ_#{type_de_champ.to_typed_id_for_query}" => ["{\"wrong\":\"value\"}", "{\"wrong\":\"value2\"}"] } } + + it "builds an array of hash(id, value) matching the given params" do + expect(prefill_params_array).to match([]) + end + end end private @@ -180,4 +257,10 @@ RSpec.describe PrefillParams do def find_champ_by_stable_id(dossier, stable_id) dossier.champs.joins(:type_de_champ).find_by(types_de_champ: { stable_id: stable_id }) end + + def attributes(champ, value) + TypesDeChamp::PrefillTypeDeChamp + .build(champ.type_de_champ, procedure.active_revision) + .to_assignable_attributes(champ, value) + end end diff --git a/spec/models/procedure_revision_spec.rb b/spec/models/procedure_revision_spec.rb index a30030d3c..fde7078fc 100644 --- a/spec/models/procedure_revision_spec.rb +++ b/spec/models/procedure_revision_spec.rb @@ -134,15 +134,15 @@ describe ProcedureRevision do end it 'move down' do - expect(draft.children_of(type_de_champ_repetition).index(second_child)).to eq(1) - - draft.move_type_de_champ(second_child.stable_id, 2) - expect(draft.children_of(type_de_champ_repetition).index(second_child)).to eq(2) + + draft.move_type_de_champ(second_child.stable_id, 3) + + expect(draft.children_of(type_de_champ_repetition).index(second_child)).to eq(3) end it 'move up' do - expect(draft.children_of(type_de_champ_repetition).index(last_child)).to eq(2) + expect(draft.children_of(type_de_champ_repetition).index(last_child)).to eq(3) draft.move_type_de_champ(last_child.stable_id, 0) @@ -205,13 +205,13 @@ describe ProcedureRevision do it 'reorders' do children = draft.children_of(type_de_champ_repetition) - expect(children.pluck(:position)).to eq([0, 1, 2]) + expect(children.pluck(:position)).to eq([0, 1, 2, 3]) draft.remove_type_de_champ(children[1].stable_id) children.reload - expect(children.pluck(:position)).to eq([0, 1]) + expect(children.pluck(:position)).to eq([0, 1, 2]) end end end @@ -242,8 +242,8 @@ describe ProcedureRevision do new_draft.remove_type_de_champ(child.stable_id) expect { child.reload }.not_to raise_error - expect(draft.children_of(type_de_champ_repetition).size).to eq(1) - expect(new_draft.children_of(type_de_champ_repetition)).to be_empty + expect(draft.children_of(type_de_champ_repetition).size).to eq(2) + expect(new_draft.children_of(type_de_champ_repetition).size).to eq(1) end it 'can remove the parent only in the new revision' do @@ -291,7 +291,7 @@ describe ProcedureRevision do let(:procedure) { create(:procedure, :with_repetition) } it 'should have the same tdcs with different links' do - expect(new_draft.types_de_champ.count).to eq(2) + expect(new_draft.types_de_champ.count).to eq(3) expect(new_draft.types_de_champ).to eq(draft.types_de_champ) new_repetition, new_child = new_draft.types_de_champ.partition(&:repetition?).map(&:first) @@ -320,7 +320,7 @@ describe ProcedureRevision do it do expect(procedure.revisions.size).to eq(2) - expect(draft.revision_types_de_champ.where.not(parent_id: nil).size).to eq(1) + expect(draft.revision_types_de_champ.where.not(parent_id: nil).size).to eq(2) end end end @@ -639,9 +639,10 @@ describe ProcedureRevision do context 'with a repetition tdc' do let(:procedure) { create(:procedure, :with_repetition) } let!(:parent) { draft.types_de_champ.find(&:repetition?) } - let!(:child) { draft.types_de_champ.reject(&:repetition?).first } + let!(:first_child) { draft.types_de_champ.reject(&:repetition?).first } + let!(:second_child) { draft.types_de_champ.reject(&:repetition?).second } - it { expect(draft.children_of(parent)).to match([child]) } + it { expect(draft.children_of(parent)).to match([first_child, second_child]) } context 'with multiple child' do let(:child_position_2) { create(:type_de_champ_text) } @@ -654,7 +655,7 @@ describe ProcedureRevision do end it 'returns the children in order' do - expect(draft.children_of(parent)).to eq([child, child_position_1, child_position_2]) + expect(draft.children_of(parent)).to eq([first_child, second_child, child_position_1, child_position_2]) end end @@ -668,13 +669,13 @@ describe ProcedureRevision do before do new_draft .revision_types_de_champ - .where(type_de_champ: child) + .where(type_de_champ: first_child) .update(type_de_champ: new_child) end it 'returns the children regarding the revision' do - expect(draft.children_of(parent)).to match([child]) - expect(new_draft.children_of(parent)).to match([new_child]) + expect(draft.children_of(parent)).to match([first_child, second_child]) + expect(new_draft.children_of(parent)).to match([new_child, second_child]) end end end diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 4a5508d2a..80159b85d 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -9,6 +9,20 @@ describe Procedure do it { expect(subject.without_continuation_mail_template).to be_a(Mails::WithoutContinuationMail) } end + describe 'compute_dossiers_count' do + let(:procedure) { create(:procedure_with_dossiers, dossiers_count: 2, dossiers_count_computed_at: Time.zone.now - Procedure::DOSSIERS_COUNT_EXPIRING) } + + it 'caches estimated_dossiers_count' do + procedure.dossiers.each(&:passer_en_construction!) + expect { procedure.compute_dossiers_count }.to change(procedure, :estimated_dossiers_count).from(nil).to(2) + expect { create(:dossier, procedure: procedure).passer_en_construction! }.not_to change(procedure, :estimated_dossiers_count) + + Timecop.freeze(Time.zone.now + Procedure::DOSSIERS_COUNT_EXPIRING) + expect { procedure.compute_dossiers_count }.to change(procedure, :estimated_dossiers_count).from(2).to(3) + Timecop.return + end + end + describe 'initiated_mail' do let(:procedure) { create(:procedure) } diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index 41138d59a..726d67819 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -4,10 +4,9 @@ describe Stat do describe '.deleted_dossiers_states' do subject { Stat.send(:deleted_dossiers_states) } it 'find counts for columns' do - create(:deleted_dossier, dossier_id: create(:dossier).id, state: :termine) + create(:deleted_dossier, dossier_id: create(:dossier).id, state: :accepte) create(:deleted_dossier, dossier_id: create(:dossier).id, state: :en_construction, deleted_at: 1.month.ago) create(:deleted_dossier, dossier_id: create(:dossier).id, state: :en_construction, deleted_at: 2.months.ago) - create(:deleted_dossier, dossier_id: create(:dossier).id, state: :brouillon, deleted_at: 3.months.ago) create(:deleted_dossier, dossier_id: create(:dossier).id, state: :en_construction, deleted_at: 3.months.ago) create(:deleted_dossier, dossier_id: create(:dossier).id, state: :en_instruction, deleted_at: 3.months.ago) create(:deleted_dossier, dossier_id: create(:dossier).id, state: :accepte, deleted_at: 3.months.ago) @@ -17,10 +16,10 @@ describe Stat do expect(subject["not_brouillon"]).to eq(8) expect(subject["dossiers_depose_avant_30_jours"]).to eq(1) expect(subject["dossiers_deposes_entre_60_et_30_jours"]).to eq(1) - expect(subject["brouillon"]).to eq(1) + expect(subject["brouillon"]).to eq(0) expect(subject["en_construction"]).to eq(3) expect(subject["en_instruction"]).to eq(1) - expect(subject["termines"]).to eq(3) + expect(subject["termines"]).to eq(4) end end diff --git a/spec/models/type_de_champ_spec.rb b/spec/models/type_de_champ_spec.rb index e1ca7bac6..d6d364759 100644 --- a/spec/models/type_de_champ_spec.rb +++ b/spec/models/type_de_champ_spec.rb @@ -95,7 +95,7 @@ describe TypeDeChamp do let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) } it 'removes the children types de champ' do - expect(procedure.draft_revision.children_of(tdc)).to be_empty + expect(procedure.draft_revision.reload.children_of(tdc)).to be_empty end end end @@ -246,30 +246,36 @@ describe TypeDeChamp do it_behaves_like "a prefillable type de champ", :type_de_champ_datetime it_behaves_like "a prefillable type de champ", :type_de_champ_civilite it_behaves_like "a prefillable type de champ", :type_de_champ_pays + it_behaves_like "a prefillable type de champ", :type_de_champ_regions + it_behaves_like "a prefillable type de champ", :type_de_champ_departements + it_behaves_like "a prefillable type de champ", :type_de_champ_communes it_behaves_like "a prefillable type de champ", :type_de_champ_yes_no it_behaves_like "a prefillable type de champ", :type_de_champ_checkbox it_behaves_like "a prefillable type de champ", :type_de_champ_drop_down_list - it_behaves_like "a prefillable type de champ", :type_de_champ_regions + it_behaves_like "a prefillable type de champ", :type_de_champ_repetition + it_behaves_like "a prefillable type de champ", :type_de_champ_annuaire_education + it_behaves_like "a prefillable type de champ", :type_de_champ_multiple_drop_down_list + it_behaves_like "a prefillable type de champ", :type_de_champ_epci + it_behaves_like "a prefillable type de champ", :type_de_champ_dossier_link + it_behaves_like "a prefillable type de champ", :type_de_champ_siret + it_behaves_like "a prefillable type de champ", :type_de_champ_rna it_behaves_like "a non-prefillable type de champ", :type_de_champ_number - it_behaves_like "a non-prefillable type de champ", :type_de_champ_communes - it_behaves_like "a non-prefillable type de champ", :type_de_champ_dossier_link it_behaves_like "a non-prefillable type de champ", :type_de_champ_titre_identite - it_behaves_like "a non-prefillable type de champ", :type_de_champ_multiple_drop_down_list it_behaves_like "a non-prefillable type de champ", :type_de_champ_linked_drop_down_list it_behaves_like "a non-prefillable type de champ", :type_de_champ_header_section it_behaves_like "a non-prefillable type de champ", :type_de_champ_explication it_behaves_like "a non-prefillable type de champ", :type_de_champ_piece_justificative - it_behaves_like "a non-prefillable type de champ", :type_de_champ_repetition it_behaves_like "a non-prefillable type de champ", :type_de_champ_cnaf it_behaves_like "a non-prefillable type de champ", :type_de_champ_dgfip it_behaves_like "a non-prefillable type de champ", :type_de_champ_pole_emploi it_behaves_like "a non-prefillable type de champ", :type_de_champ_mesri it_behaves_like "a non-prefillable type de champ", :type_de_champ_carte it_behaves_like "a non-prefillable type de champ", :type_de_champ_address - it_behaves_like "a non-prefillable type de champ", :type_de_champ_departements - it_behaves_like "a non-prefillable type de champ", :type_de_champ_siret - it_behaves_like "a non-prefillable type de champ", :type_de_champ_rna - it_behaves_like "a non-prefillable type de champ", :type_de_champ_annuaire_education + end + + describe '#normalize_libelle' do + it { expect(create(:type_de_champ, :header_section, libelle: " 2.3 Test").libelle).to eq("2.3 Test") } + it { expect(create(:type_de_champ, libelle: " fix me ").libelle).to eq("fix me") } end end diff --git a/spec/models/types_de_champ/prefill_annuaire_education_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_annuaire_education_type_de_champ_spec.rb new file mode 100644 index 000000000..e79629698 --- /dev/null +++ b/spec/models/types_de_champ/prefill_annuaire_education_type_de_champ_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.describe TypesDeChamp::PrefillAnnuaireEducationTypeDeChamp do + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_annuaire_education, procedure: procedure) } + + describe 'ancestors' do + subject { described_class.new(type_de_champ, procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } + end + + describe '#to_assignable_attributes' do + let(:champ) { create(:champ_annuaire_education, type_de_champ: type_de_champ) } + subject { described_class.build(type_de_champ, procedure.active_revision).to_assignable_attributes(champ, value) } + + context 'when the value is nil' do + let(:value) { nil } + it { is_expected.to eq(nil) } + end + + context 'when the value is empty' do + let(:value) { '' } + it { is_expected.to eq(nil) } + end + + context 'when the value is present' do + let(:value) { '0050009H' } + it { is_expected.to match({ id: champ.id, external_id: '0050009H', value: '0050009H' }) } + end + end +end diff --git a/spec/models/types_de_champ/prefill_commune_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_commune_type_de_champ_spec.rb new file mode 100644 index 000000000..3e058d743 --- /dev/null +++ b/spec/models/types_de_champ/prefill_commune_type_de_champ_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +RSpec.describe TypesDeChamp::PrefillCommuneTypeDeChamp do + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_communes, procedure: procedure) } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + end + + before do + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_communes') + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_communes') + end + + describe 'ancestors' do + subject { described_class.new(type_de_champ, procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } + end + + describe '#all_possible_values' do + let(:expected_values) do + departements.map { |departement| "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/communes?codeDepartement=#{departement[:code]}" } + end + subject(:all_possible_values) { described_class.new(type_de_champ, procedure.active_revision).all_possible_values } + + it { expect(all_possible_values).to match(expected_values) } + end + + describe '#example_value' do + let(:departement_code) { departements.pick(:code) } + let(:commune_code) { APIGeoService.communes(departement_code).pick(:code) } + subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value } + + it { is_expected.to eq([departement_code, commune_code]) } + end + + describe '#to_assignable_attributes' do + let(:champ) { create(:champ_communes, type_de_champ: type_de_champ) } + subject(:to_assignable_attributes) do + described_class.build(type_de_champ, procedure.active_revision).to_assignable_attributes(champ, value) + end + + context 'when the value is nil' do + let(:value) { nil } + it { is_expected.to match(nil) } + end + + context 'when the value is empty' do + let(:value) { '' } + it { is_expected.to match(nil) } + end + + context 'when the value is a string' do + let(:value) { 'hello' } + it { is_expected.to match(nil) } + end + + context 'when the value is an array of one element' do + context 'when the first element is a valid departement code' do + let(:value) { ['01'] } + it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain' }) } + end + + context 'when the first element is not a valid departement code' do + let(:value) { ['totoro'] } + it { is_expected.to match(nil) } + end + end + + context 'when the value is an array of two elements' do + context 'when the first element is a valid departement code' do + context 'when the second element is a valid insee code' do + let(:value) { ['01', '01457'] } + it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain', external_id: '01457', value: 'Vonnas (01540)' }) } + end + + context 'when the second element is not a valid insee code' do + let(:value) { ['01', 'totoro'] } + it { is_expected.to match(nil) } + end + end + + context 'when the first element is not a valid departement code' do + let(:value) { ['totoro', '01457'] } + it { is_expected.to match(nil) } + end + end + + context 'when the value is an array of three or more elements' do + context 'when the first element is a valid departement code' do + context 'when the second element is a valid insee code' do + let(:value) { ['01', '01457', 'hello'] } + it { is_expected.to match({ id: champ.id, code_departement: '01', departement: 'Ain', external_id: '01457', value: 'Vonnas (01540)' }) } + end + + context 'when the second element is not a valid insee code' do + let(:value) { ['01', 'totoro', 'hello'] } + it { is_expected.to match(nil) } + end + end + + context 'when the first element is not a valid departement code' do + let(:value) { ['totoro', '01457', 'hello'] } + it { is_expected.to match(nil) } + end + end + end + + private + + def departements + APIGeoService.departements.sort_by { _1[:code] } + end +end diff --git a/spec/models/types_de_champ/prefill_departement_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_departement_type_de_champ_spec.rb new file mode 100644 index 000000000..65c2dc484 --- /dev/null +++ b/spec/models/types_de_champ/prefill_departement_type_de_champ_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe TypesDeChamp::PrefillDepartementTypeDeChamp, type: :model do + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_departements, procedure: procedure) } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + end + + describe 'ancestors' do + subject { described_class.build(type_de_champ, procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } + end + + describe '#possible_values', vcr: { cassette_name: 'api_geo_departements' } do + let(:expected_values) { + "Un numéro de département
Voir toutes les valeurs possibles" + } + subject(:possible_values) { described_class.new(type_de_champ, procedure.active_revision).possible_values } + + before { type_de_champ.reload } + + it { expect(possible_values).to match(expected_values) } + end +end diff --git a/spec/models/types_de_champ/prefill_drop_down_list_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_drop_down_list_type_de_champ_spec.rb index fc7d4e573..f1a4c65da 100644 --- a/spec/models/types_de_champ/prefill_drop_down_list_type_de_champ_spec.rb +++ b/spec/models/types_de_champ/prefill_drop_down_list_type_de_champ_spec.rb @@ -2,28 +2,32 @@ RSpec.describe TypesDeChamp::PrefillDropDownListTypeDeChamp do describe '#possible_values' do - subject(:possible_values) { described_class.new(type_de_champ).possible_values } + let(:procedure) { create(:procedure) } + subject(:possible_values) { described_class.new(type_de_champ, procedure.active_revision).possible_values } + + before { type_de_champ.reload } context "when the drop down list accepts 'other'" do - let(:type_de_champ) { build(:type_de_champ_drop_down_list, :with_other) } + let(:type_de_champ) { build(:type_de_champ_drop_down_list, :with_other, procedure: procedure) } it { expect(possible_values).to match( - [I18n.t("views.prefill_descriptions.edit.possible_values.drop_down_list_other_html")] + type_de_champ.drop_down_list_enabled_non_empty_options + ([I18n.t("views.prefill_descriptions.edit.possible_values.drop_down_list_other_html")] + type_de_champ.drop_down_list_enabled_non_empty_options).to_sentence ) } end context "when the drop down list does not accept 'other'" do - let(:type_de_champ) { build(:type_de_champ_drop_down_list) } + let(:type_de_champ) { build(:type_de_champ_drop_down_list, procedure:) } - it { expect(possible_values).to match(type_de_champ.drop_down_list_enabled_non_empty_options) } + it { expect(possible_values).to match(type_de_champ.drop_down_list_enabled_non_empty_options.to_sentence) } end end describe '#example_value' do - let(:type_de_champ) { build(:type_de_champ_drop_down_list) } - subject(:example_value) { described_class.new(type_de_champ).example_value } + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_drop_down_list, procedure: procedure) } + subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value } it { expect(example_value).to eq(type_de_champ.drop_down_list_enabled_non_empty_options.first) } end diff --git a/spec/models/types_de_champ/prefill_epci_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_epci_type_de_champ_spec.rb new file mode 100644 index 000000000..20432a36a --- /dev/null +++ b/spec/models/types_de_champ/prefill_epci_type_de_champ_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +RSpec.describe TypesDeChamp::PrefillEpciTypeDeChamp do + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_epci, procedure: procedure) } + let(:champ) { create(:champ_epci, type_de_champ: type_de_champ) } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + end + + describe 'ancestors' do + subject { described_class.new(type_de_champ, procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } + end + + describe '#all_possible_values' do + let(:expected_values) do + departements.map { |departement| "#{departement[:code]} (#{departement[:name]}) : https://geo.api.gouv.fr/epcis?codeDepartement=#{departement[:code]}" } + end + subject(:all_possible_values) { described_class.new(type_de_champ, procedure.active_revision).all_possible_values } + + before do + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_epcis') + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_epcis') + end + + it { expect(all_possible_values).to match(expected_values) } + end + + describe '#example_value' do + let(:departement_code) { departements.pick(:code) } + let(:epci_code) { APIGeoService.epcis(departement_code).pick(:code) } + subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value } + + before do + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_epcis') + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_epcis') + end + + it { is_expected.to eq([departement_code, epci_code]) } + end + + describe '#to_assignable_attributes' do + subject(:to_assignable_attributes) { described_class.build(type_de_champ, procedure.active_revision).to_assignable_attributes(champ, value) } + + shared_examples "a transformation to" do |code_departement, value| + it { is_expected.to match({ code_departement: code_departement, value: value, id: champ.id }) } + end + + context 'when the value is nil' do + let(:value) { nil } + + it_behaves_like "a transformation to", nil, nil + end + + context 'when the value is empty' do + let(:value) { '' } + + it_behaves_like "a transformation to", nil, nil + end + + context 'when the value is a string' do + let(:value) { 'hello' } + + it_behaves_like "a transformation to", nil, nil + end + + context 'when the value is an array of one element' do + let(:value) { ['01'] } + + it_behaves_like "a transformation to", '01', nil + end + + context 'when the value is an array of two elements' do + let(:value) { ['01', '200042935'] } + + it_behaves_like "a transformation to", '01', '200042935' + end + + context 'when the value is an array of three or more elements' do + let(:value) { ['01', '200042935', 'hello'] } + + it_behaves_like "a transformation to", '01', '200042935' + end + end + + private + + def departements + APIGeoService.departements.sort_by { |departement| departement[:code] } + end +end diff --git a/spec/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ_spec.rb new file mode 100644 index 000000000..3a825b41e --- /dev/null +++ b/spec/models/types_de_champ/prefill_multiple_drop_down_list_type_de_champ_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.describe TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp do + let(:procedure) { create(:procedure) } + + describe 'ancestors' do + subject { described_class.new(build(:type_de_champ_multiple_drop_down_list, procedure: procedure), procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillDropDownListTypeDeChamp) } + end + + describe '#example_value' do + let(:type_de_champ) { build(:type_de_champ_multiple_drop_down_list, drop_down_list_value: drop_down_list_value, procedure: procedure) } + subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value } + + context 'when the multiple drop down list has no option' do + let(:drop_down_list_value) { "" } + + it { expect(example_value).to eq(nil) } + end + + context 'when the multiple drop down list only has one option' do + let(:drop_down_list_value) { "value" } + + it { expect(example_value).to eq("value") } + end + + context 'when the multiple drop down list has two options or more' do + let(:drop_down_list_value) { "value1\r\nvalue2\r\nvalue3" } + + it { expect(example_value).to eq(["value1", "value2"]) } + end + end +end diff --git a/spec/models/types_de_champ/prefill_pays_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_pays_type_de_champ_spec.rb index f651d3d60..45014ec22 100644 --- a/spec/models/types_de_champ/prefill_pays_type_de_champ_spec.rb +++ b/spec/models/types_de_champ/prefill_pays_type_de_champ_spec.rb @@ -1,17 +1,22 @@ RSpec.describe TypesDeChamp::PrefillPaysTypeDeChamp, type: :model do - let(:type_de_champ) { build(:type_de_champ_pays) } + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_pays, procedure: procedure) } + + describe 'ancestors' do + subject { described_class.build(type_de_champ, procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } + end describe '#possible_values' do - let(:expected_values) { APIGeoService.countries.sort_by { |country| country[:code] }.map { |country| "#{country[:code]} (#{country[:name]})" } } - subject(:possible_values) { described_class.new(type_de_champ).possible_values } + let(:expected_values) { "Un code pays ISO 3166-2
Voir toutes les valeurs possibles" } + subject(:possible_values) { described_class.new(type_de_champ, procedure.active_revision).possible_values } - it { expect(possible_values).to match(expected_values) } - end + before { type_de_champ.reload } - describe '#example_value' do - subject(:example_value) { described_class.new(type_de_champ).example_value } - - it { expect(example_value).to eq(APIGeoService.countries.sort_by { |country| country[:code] }.first[:code]) } + it { + expect(possible_values).to match(expected_values) + } end end diff --git a/spec/models/types_de_champ/prefill_region_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_region_type_de_champ_spec.rb index 39fd75fb9..1f9ddabb3 100644 --- a/spec/models/types_de_champ/prefill_region_type_de_champ_spec.rb +++ b/spec/models/types_de_champ/prefill_region_type_de_champ_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true RSpec.describe TypesDeChamp::PrefillRegionTypeDeChamp, type: :model do - let(:type_de_champ) { build(:type_de_champ_regions) } + let(:procedure) { create(:procedure) } + let(:type_de_champ) { create(:type_de_champ_regions, procedure: procedure) } let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } before do @@ -10,15 +11,19 @@ RSpec.describe TypesDeChamp::PrefillRegionTypeDeChamp, type: :model do end describe 'ancestors' do - subject { described_class.build(type_de_champ) } + subject { described_class.build(type_de_champ, procedure.active_revision) } it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } end describe '#possible_values', vcr: { cassette_name: 'api_geo_regions' } do - let(:expected_values) { APIGeoService.regions.sort_by { |region| region[:code] }.map { |region| "#{region[:code]} (#{region[:name]})" } } - subject(:possible_values) { described_class.new(type_de_champ).possible_values } + let(:expected_values) { "Un code INSEE de région
Voir toutes les valeurs possibles" } + subject(:possible_values) { described_class.new(type_de_champ, procedure.active_revision).possible_values } - it { expect(possible_values).to match(expected_values) } + before { type_de_champ.reload } + + it { + expect(possible_values).to eq(expected_values) + } end end diff --git a/spec/models/types_de_champ/prefill_repetition_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_repetition_type_de_champ_spec.rb new file mode 100644 index 000000000..731129a48 --- /dev/null +++ b/spec/models/types_de_champ/prefill_repetition_type_de_champ_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +RSpec.describe TypesDeChamp::PrefillRepetitionTypeDeChamp, type: :model, vcr: { cassette_name: 'api_geo_regions' } do + let(:procedure) { create(:procedure) } + let(:type_de_champ) { build(:type_de_champ_repetition, :with_types_de_champ, :with_region_types_de_champ, procedure: procedure) } + let(:champ) { create(:champ_repetition, type_de_champ: type_de_champ) } + let(:prefillable_subchamps) { TypesDeChamp::PrefillRepetitionTypeDeChamp.new(type_de_champ, procedure.active_revision).send(:prefillable_subchamps) } + let(:text_repetition) { prefillable_subchamps.first } + let(:integer_repetition) { prefillable_subchamps.second } + let(:region_repetition) { prefillable_subchamps.third } + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + end + + describe 'ancestors' do + subject { described_class.build(type_de_champ, procedure.active_revision) } + + it { is_expected.to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } + end + + describe '#possible_values' do + subject(:possible_values) { described_class.new(type_de_champ, procedure.active_revision).possible_values } + let(:expected_value) { + "Un tableau de dictionnaires avec les valeurs possibles pour chaque champ de la répétition.
" + } + + it { + expect(possible_values).to eq(expected_value) + } + end + + describe '#example_value' do + subject(:example_value) { described_class.new(type_de_champ, procedure.active_revision).example_value } + let(:expected_value) { [{ "champ_#{text_repetition.to_typed_id_for_query}" => "Texte court", "champ_#{integer_repetition.to_typed_id_for_query}" => "42", "champ_#{region_repetition.to_typed_id_for_query}" => "53" }, { "champ_#{text_repetition.to_typed_id_for_query}" => "Texte court", "champ_#{integer_repetition.to_typed_id_for_query}" => "42", "champ_#{region_repetition.to_typed_id_for_query}" => "53" }] } + + it { expect(example_value).to eq(expected_value) } + end + + describe '#to_assignable_attributes' do + subject(:to_assignable_attributes) { described_class.build(type_de_champ, procedure.active_revision).to_assignable_attributes(champ, value) } + + context 'when the value is nil' do + let(:value) { nil } + it { is_expected.to match([]) } + end + + context 'when the value is empty' do + let(:value) { '' } + it { is_expected.to match([]) } + end + + context 'when the value is a string' do + let(:value) { 'hello' } + it { is_expected.to match([]) } + end + + context 'when the value is an array with wrong keys' do + let(:value) { ["{\"blabla\":\"value\"}", "{\"blabla\":\"value2\"}"] } + + it { is_expected.to match([]) } + end + + context 'when the value is an array with some wrong keys' do + let(:value) { [{ "champ_#{text_repetition.to_typed_id_for_query}" => "value", "blabla" => "value2" }, { "champ_#{integer_repetition.to_typed_id_for_query}" => "value3" }, { "blabla" => "false" }] } + + it { is_expected.to match([[{ id: text_repetition.champ.first.id, value: "value" }], [{ id: integer_repetition.champ.second.id, value: "value3" }]]) } + end + + context 'when the value is an array with right keys' do + let(:value) { [{ "champ_#{text_repetition.to_typed_id_for_query}" => "value" }, { "champ_#{text_repetition.to_typed_id_for_query}" => "value2" }] } + + it { is_expected.to match([[{ id: text_repetition.champ.first.id, value: "value" }], [{ id: text_repetition.champ.second.id, value: "value2" }]]) } + end + end +end diff --git a/spec/models/types_de_champ/prefill_type_de_champ_spec.rb b/spec/models/types_de_champ/prefill_type_de_champ_spec.rb index af0e3cfc9..1d8e135e5 100644 --- a/spec/models/types_de_champ/prefill_type_de_champ_spec.rb +++ b/spec/models/types_de_champ/prefill_type_de_champ_spec.rb @@ -1,36 +1,77 @@ # frozen_string_literal: true RSpec.describe TypesDeChamp::PrefillTypeDeChamp, type: :model do + include ActionView::Helpers::UrlHelper + include ApplicationHelper + + let(:procedure) { create(:procedure) } + describe '.build' do - subject(:built) { described_class.build(type_de_champ) } + subject(:built) { described_class.build(type_de_champ, procedure.active_revision) } context 'when the type de champ is a drop_down_list' do - let(:type_de_champ) { build(:type_de_champ_drop_down_list) } + let(:type_de_champ) { build(:type_de_champ_drop_down_list, procedure: procedure) } it { expect(built).to be_kind_of(TypesDeChamp::PrefillDropDownListTypeDeChamp) } end + context 'when the type de champ is a multiple_drop_down_list' do + let(:type_de_champ) { build(:type_de_champ_multiple_drop_down_list, procedure: procedure) } + + it { expect(built).to be_kind_of(TypesDeChamp::PrefillMultipleDropDownListTypeDeChamp) } + end + context 'when the type de champ is a pays' do - let(:type_de_champ) { build(:type_de_champ_pays) } + let(:type_de_champ) { build(:type_de_champ_pays, procedure: procedure) } it { expect(built).to be_kind_of(TypesDeChamp::PrefillPaysTypeDeChamp) } end context 'when the type de champ is a regions' do - let(:type_de_champ) { build(:type_de_champ_regions) } + let(:type_de_champ) { build(:type_de_champ_regions, procedure: procedure) } it { expect(built).to be_kind_of(TypesDeChamp::PrefillRegionTypeDeChamp) } end + context 'when the type de champ is a repetition' do + let(:type_de_champ) { build(:type_de_champ_repetition, procedure: procedure) } + + it { expect(built).to be_kind_of(TypesDeChamp::PrefillRepetitionTypeDeChamp) } + end + + context 'when the type de champ is a departements' do + let(:type_de_champ) { build(:type_de_champ_departements, procedure: procedure) } + + it { expect(built).to be_kind_of(TypesDeChamp::PrefillDepartementTypeDeChamp) } + end + + context 'when the type de champ is a communes' do + let(:type_de_champ) { build(:type_de_champ_communes) } + + it { expect(built).to be_kind_of(TypesDeChamp::PrefillCommuneTypeDeChamp) } + end + + context 'when the type de champ is a epci' do + let(:type_de_champ) { build(:type_de_champ_epci, procedure: procedure) } + + it { expect(built).to be_kind_of(TypesDeChamp::PrefillEpciTypeDeChamp) } + end + + context 'when the type de champ is an annuaire_education' do + let(:type_de_champ) { build(:type_de_champ_annuaire_education) } + + it { expect(built).to be_kind_of(TypesDeChamp::PrefillAnnuaireEducationTypeDeChamp) } + end + context 'when any other type de champ' do - let(:type_de_champ) { build(:type_de_champ_date) } + let(:type_de_champ) { build(:type_de_champ_date, procedure: procedure) } it { expect(built).to be_kind_of(TypesDeChamp::PrefillTypeDeChamp) } end end describe '.wrap' do - subject(:wrapped) { described_class.wrap([build(:type_de_champ_drop_down_list), build(:type_de_champ_email)]) } + subject(:wrapped) { described_class.wrap([build(:type_de_champ_drop_down_list, procedure: procedure), build(:type_de_champ_email, procedure: procedure)], procedure.active_revision) } it 'wraps the collection' do expect(wrapped.first).to be_kind_of(TypesDeChamp::PrefillDropDownListTypeDeChamp) @@ -39,51 +80,81 @@ RSpec.describe TypesDeChamp::PrefillTypeDeChamp, type: :model do end describe '#possible_values' do - subject(:possible_values) { described_class.build(type_de_champ).possible_values } - - context 'when the type de champ is not prefillable' do - let(:type_de_champ) { build(:type_de_champ_mesri) } - - it { expect(possible_values).to be_empty } - end + let(:built) { described_class.build(type_de_champ, procedure.active_revision) } + subject(:possible_values) { built.possible_values } context 'when the type de champ is prefillable' do - let(:type_de_champ) { build(:type_de_champ_email) } + context 'when the type de champ has a description' do + let(:type_de_champ) { build(:type_de_champ_text) } - it { expect(possible_values).to match([]) } + it { expect(possible_values).to include(I18n.t("views.prefill_descriptions.edit.possible_values.#{type_de_champ.type_champ}_html")) } + end + + context 'when the type de champ does not have a description' do + let(:type_de_champ) { build(:type_de_champ_mesri) } + + it { expect(possible_values).not_to include(I18n.t("views.prefill_descriptions.edit.possible_values.#{type_de_champ.type_champ}_html")) } + end + + describe 'too many possible values or not' do + let!(:procedure) { create(:procedure, :with_drop_down_list) } + let(:type_de_champ) { procedure.draft_types_de_champ_public.first } + let(:link_to_all_possible_values) { + link_to( + I18n.t("views.prefill_descriptions.edit.possible_values.link.text"), + Rails.application.routes.url_helpers.prefill_type_de_champ_path(procedure.path, type_de_champ), + title: new_tab_suffix(I18n.t("views.prefill_descriptions.edit.possible_values.link.title")), + **external_link_attributes + ) + } + + context 'when there is too many possible values' do + before { type_de_champ.drop_down_options = (1..described_class::POSSIBLE_VALUES_THRESHOLD + 1).map(&:to_s) } + + it { expect(possible_values).to include(link_to_all_possible_values) } + + it { expect(possible_values).not_to include(built.all_possible_values.to_sentence) } + end + + context 'when there is not too many possible values' do + before { type_de_champ.drop_down_options = (1..described_class::POSSIBLE_VALUES_THRESHOLD - 1).map(&:to_s) } + + it { expect(possible_values).not_to include(link_to_all_possible_values) } + + it { expect(possible_values).to include(built.all_possible_values.to_sentence) } + end + end + end + + context 'when the type de champ is not prefillable' do + let(:type_de_champ) { build(:type_de_champ_mesri, procedure: procedure) } + + it { expect(possible_values).to be_empty } end end describe '#example_value' do - subject(:example_value) { described_class.build(type_de_champ).example_value } + subject(:example_value) { described_class.build(type_de_champ, procedure.active_revision).example_value } context 'when the type de champ is not prefillable' do - let(:type_de_champ) { build(:type_de_champ_mesri) } + let(:type_de_champ) { build(:type_de_champ_mesri, procedure: procedure) } it { expect(example_value).to be_nil } end context 'when the type de champ is prefillable' do - let(:type_de_champ) { build(:type_de_champ_email) } + let(:type_de_champ) { build(:type_de_champ_email, procedure: procedure) } it { expect(example_value).to eq(I18n.t("views.prefill_descriptions.edit.examples.#{type_de_champ.type_champ}")) } end end - describe '#too_many_possible_values?' do - let(:type_de_champ) { build(:type_de_champ_drop_down_list) } - subject(:too_many_possible_values) { described_class.build(type_de_champ).too_many_possible_values? } + describe '#to_assignable_attributes' do + let(:type_de_champ) { build(:type_de_champ_email, procedure: procedure) } + let(:champ) { build(:champ, type_de_champ: type_de_champ) } + let(:value) { "any@email.org" } + subject(:to_assignable_attributes) { described_class.build(type_de_champ, procedure.active_revision).to_assignable_attributes(champ, value) } - context 'when there are too many possible values' do - before { type_de_champ.drop_down_options = (1..described_class::POSSIBLE_VALUES_THRESHOLD + 1).map(&:to_s) } - - it { expect(too_many_possible_values).to eq(true) } - end - - context 'when there are not too many possible values' do - before { type_de_champ.drop_down_options = (1..described_class::POSSIBLE_VALUES_THRESHOLD).map(&:to_s) } - - it { expect(too_many_possible_values).to eq(false) } - end + it { is_expected.to match({ id: champ.id, value: value }) } end end diff --git a/spec/services/api_entreprise_service_spec.rb b/spec/services/api_entreprise_service_spec.rb index 93ec39539..fd7cc7ada 100644 --- a/spec/services/api_entreprise_service_spec.rb +++ b/spec/services/api_entreprise_service_spec.rb @@ -15,11 +15,16 @@ describe APIEntrepriseService do before do stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/) .to_return(body: etablissements_body, status: etablissements_status) + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siret[0..8]}/) + .to_return(body: entreprises_body, status: entreprises_status) end let(:siret) { '41816609600051' } + let(:raison_sociale) { "OCTO-TECHNOLOGY" } let(:etablissements_status) { 200 } let(:etablissements_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') } + let(:entreprises_status) { 200 } + let(:entreprises_body) { File.read('spec/fixtures/files/api_entreprise/entreprises.json') } let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" } let(:procedure) { create(:procedure, api_entreprise_token: valid_token) } let(:dossier) { create(:dossier, procedure: procedure) } @@ -35,6 +40,10 @@ describe APIEntrepriseService do expect(subject[:siret]).to eq(siret) end + it 'should fetch entreprise params' do + expect(subject[:entreprise_raison_sociale]).to eq(raison_sociale) + end + it_behaves_like 'schedule fetch of all etablissement params' end diff --git a/spec/services/api_geo_service_spec.rb b/spec/services/api_geo_service_spec.rb index 6585b6392..b880e86b8 100644 --- a/spec/services/api_geo_service_spec.rb +++ b/spec/services/api_geo_service_spec.rb @@ -36,13 +36,36 @@ describe APIGeoService do describe 'departements', vcr: { cassette_name: 'api_geo_departements' } do it 'return sorted results' do - expect(APIGeoService.departements.size).to eq(102) + expect(APIGeoService.departements.size).to eq(110) expect(APIGeoService.departements.first).to eq(code: '99', name: 'Etranger') expect(APIGeoService.departements.second).to eq(code: '01', name: 'Ain') - expect(APIGeoService.departements.last).to eq(code: '976', name: 'Mayotte') + expect(APIGeoService.departements.last).to eq(code: '989', name: 'Île de Clipperton') end end + describe 'communes', vcr: { cassette_name: 'api_geo_communes' } do + it 'return sorted results' do + expect(APIGeoService.communes('01').size).to eq(393) + expect(APIGeoService.communes('01').first).to eq(code: '01004', name: 'Ambérieu-en-Bugey', postal_codes: ['01500']) + expect(APIGeoService.communes('01').last).to eq(code: '01457', name: 'Vonnas', postal_codes: ['01540']) + end + end + + describe 'commune_name', vcr: { cassette_name: 'api_geo_communes' } do + subject { APIGeoService.commune_name('01', '01457') } + it { is_expected.to eq('Vonnas') } + end + + describe 'commune_code', vcr: { cassette_name: 'api_geo_communes' } do + subject { APIGeoService.commune_code('01', 'Vonnas') } + it { is_expected.to eq('01457') } + end + + describe 'commune_postal_codes', vcr: { cassette_name: 'api_geo_communes' } do + subject { APIGeoService.commune_postal_codes('01', '01457') } + it { is_expected.to eq(['01540']) } + end + describe 'epcis', vcr: { cassette_name: 'api_geo_epcis' } do it 'return sorted results' do expect(APIGeoService.epcis('01').size).to eq(17) diff --git a/spec/services/demarches_publiques_export_service_spec.rb b/spec/services/demarches_publiques_export_service_spec.rb index 05e96a208..bec59a795 100644 --- a/spec/services/demarches_publiques_export_service_spec.rb +++ b/spec/services/demarches_publiques_export_service_spec.rb @@ -17,7 +17,7 @@ describe DemarchesPubliquesExportService do typeOrganisme: "association" }, cadreJuridiqueUrl: "un cadre juridique important", - demarcheUrl: nil, + demarcheUrl: Rails.application.routes.url_helpers.commencer_url(path: procedure.path), dpoUrl: nil, noticeUrl: nil, siteWebUrl: "https://mon-site.gouv", diff --git a/spec/services/instructeurs_import_service_spec.rb b/spec/services/instructeurs_import_service_spec.rb index 285d3dc98..8e2a26bb4 100644 --- a/spec/services/instructeurs_import_service_spec.rb +++ b/spec/services/instructeurs_import_service_spec.rb @@ -1,5 +1,5 @@ describe InstructeursImportService do - describe '#import' do + describe '#import_groupes' do let(:procedure) { create(:procedure) } let(:procedure_groupes) do @@ -9,7 +9,7 @@ describe InstructeursImportService do .to_h end - subject { described_class.import(procedure, lines) } + subject { described_class.import_groupes(procedure, lines) } context 'nominal case' do let(:lines) do @@ -20,8 +20,8 @@ describe InstructeursImportService do ] end - it 'imports' do - errors = subject + it 'imports groupes' do + _, errors = subject expect(procedure_groupes.keys).to contain_exactly("Auvergne Rhone-Alpes", "Occitanie", "défaut") expect(procedure_groupes["Auvergne Rhone-Alpes"]).to contain_exactly("john@lennon.fr") @@ -63,7 +63,7 @@ describe InstructeursImportService do end it 'ignores or corrects' do - errors = subject + _, errors = subject expect(procedure_groupes.keys).to contain_exactly("Occitanie", "défaut") expect(procedure_groupes["Occitanie"]).to contain_exactly("paul@mccartney.uk", "ringo@starr.uk") @@ -117,7 +117,7 @@ describe InstructeursImportService do end it 'ignores instructeur' do - errors = subject + _, errors = subject expect(procedure_groupes.keys).to contain_exactly("défaut") expect(procedure_groupes["défaut"]).to be_empty @@ -126,4 +126,21 @@ describe InstructeursImportService do end end end + + describe '#import_instructeurs' do + let(:procedure_non_routee) { create(:procedure) } + + subject { described_class.import_instructeurs(procedure_non_routee, emails) } + + context 'nominal case' do + let(:emails) { [{ "email" => "john@lennon.fr" }, { "email" => "paul@mccartney.uk" }, { "email" => "ringo@starr.uk" }] } + + it 'imports instructeurs' do + _, errors = subject + expect(procedure_non_routee.defaut_groupe_instructeur.instructeurs.pluck(:email)).to contain_exactly("john@lennon.fr", "paul@mccartney.uk", "ringo@starr.uk") + + expect(errors).to match_array([]) + end + end + end end diff --git a/spec/support/shared_examples_for_prefilled_dossier.rb b/spec/support/shared_examples_for_prefilled_dossier.rb index 20dd7dbe0..a13538e4c 100644 --- a/spec/support/shared_examples_for_prefilled_dossier.rb +++ b/spec/support/shared_examples_for_prefilled_dossier.rb @@ -1,13 +1,9 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do scenario "the user has got a prefilled dossier, owned by themselves" do - siret = '41816609600051' - stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret}/) - .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/etablissements.json')) - expect(dossier.user).to eq(user) expect(page).to have_current_path siret_dossier_path(procedure.dossiers.last) - fill_in 'Numéro SIRET', with: siret + fill_in 'Numéro SIRET', with: siret_value click_on 'Valider' expect(page).to have_current_path(etablissement_dossier_path(dossier)) @@ -18,6 +14,18 @@ shared_examples "the user has got a prefilled dossier, owned by themselves" do expect(page).to have_field(type_de_champ_text.libelle, with: text_value) expect(page).to have_field(type_de_champ_phone.libelle, with: phone_value) expect(page).to have_css('label', text: type_de_champ_phone.libelle) + expect(page).to have_field(type_de_champ_rna.libelle, with: rna_value) + expect(page).to have_field(type_de_champ_siret.libelle, with: siret_value) + expect(page).to have_css('h3', text: type_de_champ_repetition.libelle) + expect(page).to have_field(text_repetition_libelle, with: text_repetition_value) + expect(page).to have_field(integer_repetition_libelle, with: integer_repetition_value) expect(page).to have_field(type_de_champ_datetime.libelle, with: datetime_value) + expect(page).to have_css('label', text: type_de_champ_multiple_drop_down_list.libelle) + expect(page).to have_content(multiple_drop_down_list_values.first) + expect(page).to have_content(multiple_drop_down_list_values.last) + expect(page).to have_field(type_de_champ_epci.libelle, with: epci_value.last) + expect(page).to have_field(type_de_champ_dossier_link.libelle, with: dossier_link_value) + expect(page).to have_selector("input[value='Vonnas (01540)']") + expect(page).to have_content(annuaire_education_value.last) end end diff --git a/spec/system/administrateurs/procedure_cloning_spec.rb b/spec/system/administrateurs/procedure_cloning_spec.rb index dc6dfafe4..a53545f09 100644 --- a/spec/system/administrateurs/procedure_cloning_spec.rb +++ b/spec/system/administrateurs/procedure_cloning_spec.rb @@ -14,7 +14,22 @@ describe 'As an administrateur I wanna clone a procedure', js: true do published_at: Time.zone.now) login_as administrateur.user, scope: :user end + context 'Visit all admin procedures' do + let(:download_dir) { Rails.root.join('tmp/capybara') } + let(:download_file_pattern) { download_dir.join('*.xlsx') } + scenario do + Dir[download_file_pattern].map { File.delete(_1) } + visit all_admin_procedures_path + + click_on "Exporter les résultats" + Timeout.timeout(Capybara.default_max_wait_time, + Timeout::Error, + "File download timeout! can't download procedure/all.xlsx") do + sleep 0.1 until !Dir[download_file_pattern].empty? + end + end + end context 'Cloning a procedure owned by the current admin' do scenario do visit admin_procedures_path diff --git a/spec/system/instructeurs/expert_spec.rb b/spec/system/instructeurs/expert_spec.rb index 9b4994a44..a2c0f64f8 100644 --- a/spec/system/instructeurs/expert_spec.rb +++ b/spec/system/instructeurs/expert_spec.rb @@ -63,7 +63,7 @@ describe 'Inviting an expert:', js: true do click_on 'Avis externes' expect(page).to have_content(answered_avis.expert.email) - answered_avis.answer.split("\n").each do |answer_line| + answered_avis.answer.split("\n").map { |line| line.gsub("- ", "") }.map do |answer_line| expect(page).to have_content(answer_line) end end diff --git a/spec/system/instructeurs/instructeur_creation_spec.rb b/spec/system/instructeurs/instructeur_creation_spec.rb index b3a78c574..04b2ed243 100644 --- a/spec/system/instructeurs/instructeur_creation_spec.rb +++ b/spec/system/instructeurs/instructeur_creation_spec.rb @@ -16,7 +16,7 @@ describe 'As an instructeur', js: true do end scenario 'I can register' do - confirmation_email = open_email(instructeur_email) + confirmation_email = emails_sent_to(instructeur_email).first token_params = confirmation_email.body.match(/token=[^"]+/) visit "users/activate?#{token_params}" diff --git a/spec/system/instructeurs/procedure_filters_spec.rb b/spec/system/instructeurs/procedure_filters_spec.rb index 86430d082..5bfdc175e 100644 --- a/spec/system/instructeurs/procedure_filters_spec.rb +++ b/spec/system/instructeurs/procedure_filters_spec.rb @@ -1,6 +1,6 @@ describe "procedure filters" do let(:instructeur) { create(:instructeur) } - let(:procedure) { create(:procedure, :published, :with_type_de_champ, instructeurs: [instructeur]) } + let(:procedure) { create(:procedure, :published, :with_type_de_champ, :with_departement, :with_region, instructeurs: [instructeur]) } let!(:type_de_champ) { procedure.active_revision.types_de_champ_public.first } let!(:new_unfollow_dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_instruction)) } let!(:champ) { Champ.find_by(type_de_champ_id: type_de_champ.id, dossier_id: new_unfollow_dossier.id) } @@ -94,6 +94,44 @@ describe "procedure filters" do click_button "Ajouter le filtre" end + describe 'with a vcr cached cassette' do + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + end + + scenario "should be able to find by departements with custom enum lookup", js: true, vcr: { cassette_name: 'api_geo_departements' } do + departement_champ = new_unfollow_dossier.champs.find(&:departement?) + departement_champ.update!(value: 'Oise', external_id: '60') + departement_champ.reload + champ_select_value = "#{departement_champ.external_id} – #{departement_champ.value}" + + click_on 'Sélectionner un filtre' + select departement_champ.libelle, from: "Colonne" + find("select#value", visible: true) + select champ_select_value, from: "Valeur" + click_button "Ajouter le filtre" + find("select#value", visible: false) # w8 for filter to be applied + expect(page).to have_link(new_unfollow_dossier.id.to_s) + end + + scenario "should be able to find by region with custom enum lookup", js: true, vcr: { cassette_name: 'api_geo_regions' } do + region_champ = new_unfollow_dossier.champs.find(&:region?) + region_champ.update!(value: 'Bretagne', external_id: '53') + region_champ.reload + + click_on 'Sélectionner un filtre' + select region_champ.libelle, from: "Colonne" + find("select#value", visible: true) + select region_champ.value, from: "Valeur" + click_button "Ajouter le filtre" + find("select#value", visible: false) # w8 for filter to be applied + expect(page).to have_link(new_unfollow_dossier.id.to_s) + end + end + scenario "should be able to add and remove two filters for the same field", js: true do add_filter(type_de_champ.libelle, champ.value) add_filter(type_de_champ.libelle, champ_2.value) diff --git a/spec/system/integrateurs/procedure_prefilling_spec.rb b/spec/system/integrateurs/procedure_prefilling_spec.rb index 968f98e5b..370cc483a 100644 --- a/spec/system/integrateurs/procedure_prefilling_spec.rb +++ b/spec/system/integrateurs/procedure_prefilling_spec.rb @@ -5,7 +5,7 @@ describe 'As an integrator:', js: true do before { visit "/preremplir/#{procedure.path}" } scenario 'I can read the procedure prefilling (aka public champs)' do - expect(page).to have_content(type_de_champ.to_typed_id) + expect(page).to have_content(type_de_champ.to_typed_id_for_query) expect(page).to have_content(I18n.t("activerecord.attributes.type_de_champ.type_champs.#{type_de_champ.type_champ}")) expect(page).to have_content(type_de_champ.libelle) expect(page).to have_content(type_de_champ.description) diff --git a/spec/system/users/dossier_prefill_get_spec.rb b/spec/system/users/dossier_prefill_get_spec.rb index 72d212b32..e681dfeb0 100644 --- a/spec/system/users/dossier_prefill_get_spec.rb +++ b/spec/system/users/dossier_prefill_get_spec.rb @@ -1,4 +1,6 @@ -describe 'Prefilling a dossier (with a GET request):' do +describe 'Prefilling a dossier (with a GET request):', js: true do + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + let(:password) { 'my-s3cure-p4ssword' } let(:procedure) { create(:procedure, :published, opendata: true) } @@ -6,10 +8,82 @@ describe 'Prefilling a dossier (with a GET request):' do let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) } let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) } + let(:type_de_champ_rna) { create(:type_de_champ_rna, procedure: procedure) } + let(:type_de_champ_siret) { create(:type_de_champ_siret, procedure: procedure) } let(:type_de_champ_datetime) { create(:type_de_champ_datetime, procedure: procedure) } + let(:type_de_champ_multiple_drop_down_list) { create(:type_de_champ_multiple_drop_down_list, procedure: procedure) } + let(:type_de_champ_epci) { create(:type_de_champ_epci, procedure: procedure) } + let(:type_de_champ_annuaire_education) { create(:type_de_champ_annuaire_education, procedure: procedure) } + let(:type_de_champ_dossier_link) { create(:type_de_champ_dossier_link, procedure: procedure) } + let(:type_de_champ_commune) { create(:type_de_champ_communes, procedure: procedure) } + let(:type_de_champ_repetition) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) } + let(:text_value) { "My Neighbor Totoro is the best movie ever" } let(:phone_value) { "invalid phone value" } + let(:rna_value) { 'W595001988' } + let(:siret_value) { '41816609600051' } let(:datetime_value) { "2023-02-01T10:32" } + let(:multiple_drop_down_list_values) { + [ + type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.first, + type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.last + ] + } + let(:epci_value) { ['01', '200029999'] } + let(:dossier_link_value) { '42' } + let(:commune_value) { ['01', '01457'] } # Vonnas (01540) + let(:sub_type_de_champs_repetition) { procedure.active_revision.children_of(type_de_champ_repetition) } + let(:text_repetition_libelle) { sub_type_de_champs_repetition.first.libelle } + let(:integer_repetition_libelle) { sub_type_de_champs_repetition.second.libelle } + let(:text_repetition_value) { "First repetition text" } + let(:integer_repetition_value) { "42" } + let(:annuaire_education_value) { '0050009H' } + + let(:entry_path) { + commencer_path( + path: procedure.path, + "champ_#{type_de_champ_text.to_typed_id_for_query}" => text_value, + "champ_#{type_de_champ_phone.to_typed_id_for_query}" => phone_value, + "champ_#{type_de_champ_datetime.to_typed_id_for_query}" => datetime_value, + "champ_#{type_de_champ_multiple_drop_down_list.to_typed_id_for_query}" => multiple_drop_down_list_values, + "champ_#{type_de_champ_epci.to_typed_id_for_query}" => epci_value, + "champ_#{type_de_champ_dossier_link.to_typed_id_for_query}" => dossier_link_value, + "champ_#{type_de_champ_commune.to_typed_id_for_query}" => commune_value, + "champ_#{type_de_champ_siret.to_typed_id_for_query}" => siret_value, + "champ_#{type_de_champ_rna.to_typed_id_for_query}" => rna_value, + "champ_#{type_de_champ_repetition.to_typed_id_for_query}" => [ + { + "champ_#{sub_type_de_champs_repetition.first.to_typed_id_for_query}": text_repetition_value, + "champ_#{sub_type_de_champs_repetition.second.to_typed_id_for_query}": integer_repetition_value + } + ], + "champ_#{type_de_champ_annuaire_education.to_typed_id_for_query}" => annuaire_education_value + ) + } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\//) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/etablissements.json')) + + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siret_value[0..8]}/) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/entreprises.json')) + + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/associations\//) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/associations.json')) + + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_communes') + VCR.insert_cassette('api_geo_epcis') + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_communes') + VCR.eject_cassette('api_geo_epcis') + end context 'when authenticated' do it_behaves_like "the user has got a prefilled dossier, owned by themselves" do @@ -18,13 +92,7 @@ describe 'Prefilling a dossier (with a GET request):' do before do visit "/users/sign_in" sign_in_with user.email, password - - visit commencer_path( - path: procedure.path, - "champ_#{type_de_champ_text.to_typed_id}" => text_value, - "champ_#{type_de_champ_phone.to_typed_id}" => phone_value, - "champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value - ) + visit entry_path click_on "Poursuivre mon dossier prérempli" end @@ -65,14 +133,7 @@ describe 'Prefilling a dossier (with a GET request):' do end context 'when unauthenticated' do - before do - visit commencer_path( - path: procedure.path, - "champ_#{type_de_champ_text.to_typed_id}" => text_value, - "champ_#{type_de_champ_phone.to_typed_id}" => phone_value, - "champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value - ) - end + before { visit entry_path } context 'when the user signs in with email and password' do it_behaves_like "the user has got a prefilled dossier, owned by themselves" do diff --git a/spec/system/users/dossier_prefill_post_spec.rb b/spec/system/users/dossier_prefill_post_spec.rb index 6e484795f..ee31b2641 100644 --- a/spec/system/users/dossier_prefill_post_spec.rb +++ b/spec/system/users/dossier_prefill_post_spec.rb @@ -1,4 +1,6 @@ -describe 'Prefilling a dossier (with a POST request):' do +describe 'Prefilling a dossier (with a POST request):', js: true do + let(:memory_store) { ActiveSupport::Cache.lookup_store(:memory_store) } + let(:password) { 'my-s3cure-p4ssword' } let(:procedure) { create(:procedure, :published) } @@ -6,10 +8,60 @@ describe 'Prefilling a dossier (with a POST request):' do let(:type_de_champ_text) { create(:type_de_champ_text, procedure: procedure) } let(:type_de_champ_phone) { create(:type_de_champ_phone, procedure: procedure) } + let(:type_de_champ_rna) { create(:type_de_champ_rna, procedure: procedure) } + let(:type_de_champ_siret) { create(:type_de_champ_siret, procedure: procedure) } let(:type_de_champ_datetime) { create(:type_de_champ_datetime, procedure: procedure) } + let(:type_de_champ_multiple_drop_down_list) { create(:type_de_champ_multiple_drop_down_list, procedure: procedure) } + let(:type_de_champ_epci) { create(:type_de_champ_epci, procedure: procedure) } + let(:type_de_champ_annuaire_education) { create(:type_de_champ_annuaire_education, procedure: procedure) } + let(:type_de_champ_dossier_link) { create(:type_de_champ_dossier_link, procedure: procedure) } + let(:type_de_champ_repetition) { create(:type_de_champ_repetition, :with_types_de_champ, procedure: procedure) } + let(:type_de_champ_commune) { create(:type_de_champ_communes, procedure: procedure) } + let(:text_value) { "My Neighbor Totoro is the best movie ever" } let(:phone_value) { "invalid phone value" } + let(:rna_value) { 'W595001988' } + let(:siret_value) { '41816609600051' } let(:datetime_value) { "2023-02-01T10:32" } + let(:multiple_drop_down_list_values) { + [ + type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.first, + type_de_champ_multiple_drop_down_list.drop_down_list_enabled_non_empty_options.last + ] + } + let(:epci_value) { ['01', '200029999'] } + let(:commune_value) { ['01', '01457'] } # Vonnas (01540) + let(:sub_type_de_champs_repetition) { procedure.active_revision.children_of(type_de_champ_repetition) } + let(:text_repetition_libelle) { sub_type_de_champs_repetition.first.libelle } + let(:integer_repetition_libelle) { sub_type_de_champs_repetition.second.libelle } + let(:text_repetition_value) { "First repetition text" } + let(:integer_repetition_value) { "42" } + let(:dossier_link_value) { '42' } + let(:annuaire_education_value) { '0050009H' } + + before do + allow(Rails).to receive(:cache).and_return(memory_store) + Rails.cache.clear + + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/etablissements\/#{siret_value}/) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/etablissements.json')) + + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siret_value[0..8]}/) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/entreprises.json')) + + stub_request(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/associations\//) + .to_return(status: 200, body: File.read('spec/fixtures/files/api_entreprise/associations.json')) + + VCR.insert_cassette('api_geo_departements') + VCR.insert_cassette('api_geo_communes') + VCR.insert_cassette('api_geo_epcis') + end + + after do + VCR.eject_cassette('api_geo_departements') + VCR.eject_cassette('api_geo_communes') + VCR.eject_cassette('api_geo_epcis') + end scenario "the user get the URL of a prefilled orphan brouillon dossier" do dossier_url = create_and_prefill_dossier_with_post_request @@ -96,9 +148,22 @@ describe 'Prefilling a dossier (with a POST request):' do session.post api_public_v1_dossiers_path(procedure), headers: { "Content-Type" => "application/json" }, params: { - "champ_#{type_de_champ_text.to_typed_id}" => text_value, - "champ_#{type_de_champ_phone.to_typed_id}" => phone_value, - "champ_#{type_de_champ_datetime.to_typed_id}" => datetime_value + "champ_#{type_de_champ_text.to_typed_id_for_query}" => text_value, + "champ_#{type_de_champ_phone.to_typed_id_for_query}" => phone_value, + "champ_#{type_de_champ_rna.to_typed_id_for_query}" => rna_value, + "champ_#{type_de_champ_siret.to_typed_id_for_query}" => siret_value, + "champ_#{type_de_champ_repetition.to_typed_id_for_query}" => [ + { + "champ_#{sub_type_de_champs_repetition.first.to_typed_id_for_query}": text_repetition_value, + "champ_#{sub_type_de_champs_repetition.second.to_typed_id_for_query}": integer_repetition_value + } + ], + "champ_#{type_de_champ_datetime.to_typed_id_for_query}" => datetime_value, + "champ_#{type_de_champ_multiple_drop_down_list.to_typed_id_for_query}" => multiple_drop_down_list_values, + "champ_#{type_de_champ_epci.to_typed_id_for_query}" => epci_value, + "champ_#{type_de_champ_dossier_link.to_typed_id_for_query}" => dossier_link_value, + "champ_#{type_de_champ_commune.to_typed_id_for_query}" => commune_value, + "champ_#{type_de_champ_annuaire_education.to_typed_id_for_query}" => annuaire_education_value }.to_json JSON.parse(session.response.body)["dossier_url"].gsub("http://www.example.com", "") end diff --git a/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb b/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb index c094c5f16..60d6f44ef 100644 --- a/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb +++ b/spec/views/instructeur/dossiers/_envoyer_dossier_block.html.haml_spec.rb @@ -14,14 +14,14 @@ describe 'instructeurs/dossiers/envoyer_dossier_block.html.haml', type: :view do let(:potential_recipients) { [instructeur] } it { is_expected.to match(/data-react-props.*#{instructeur.email}/) } - it { is_expected.to have_css(".button.send") } + it { is_expected.to have_css(".fr-btn") } end context "there is no other instructeur for the procedure" do let(:potential_recipients) { [] } it { is_expected.not_to have_css("select") } - it { is_expected.not_to have_css(".button.send") } + it { is_expected.not_to have_css(".fr-btn") } it { is_expected.to have_content("Vous êtes le seul instructeur assigné sur cette démarche") } end end diff --git a/spec/views/instructeur/shared/avis/list.html.haml_spec.rb b/spec/views/instructeur/shared/avis/list.html.haml_spec.rb index 30dbd6cc0..a92696ffa 100644 --- a/spec/views/instructeur/shared/avis/list.html.haml_spec.rb +++ b/spec/views/instructeur/shared/avis/list.html.haml_spec.rb @@ -26,7 +26,9 @@ describe 'instructeurs/shared/avis/_list.html.haml', type: :view do let(:avis) { [create(:avis, :with_answer, claimant: instructeur, experts_procedure: experts_procedure)] } it 'renders the answer formatted with newlines' do - expect(subject).to include(simple_format(avis.first.answer)) + expect(subject).to have_selector(".answer-body p", text: avis.first.answer.split("\n").first) + expect(subject).to have_selector(".answer-body ul", count: 1) # avis.answer has two list item + expect(subject).to have_selector(".answer-body ul li", count: 2) end end