diff --git a/.circleci/config.yml b/.circleci/config.yml index 887b8e06b..baba1cf04 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -90,20 +90,8 @@ jobs: - *yarn_restore_cache - *yarn_install - run: - name: Run eslint - command: yarn lint:js - - run: - name: Run rubocop - command: bundle exec rubocop - - run: - name: Run brakeman - command: bundle exec brakeman - - run: - name: Run haml-lint - command: bundle exec haml-lint app/views/ - - run: - name: Run scss-lint - command: bundle exec scss-lint app/assets/stylesheets/ + name: Run linters + command: bundle exec rake lint deploy: <<: *defaults steps: diff --git a/Gemfile b/Gemfile index 97e2e2e0d..ed7fde5c0 100644 --- a/Gemfile +++ b/Gemfile @@ -70,6 +70,8 @@ gem 'leaflet-draw-rails' gem 'chartkick' gem 'logstasher' +gem 'lograge' +gem 'logstash-event' gem 'font-awesome-rails' diff --git a/Gemfile.lock b/Gemfile.lock index cfd127f09..ac66e576a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -457,6 +457,11 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) + lograge (0.10.0) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) + request_store (~> 1.0) logstash-event (1.2.02) logstasher (1.2.2) activesupport (>= 4.0) @@ -845,6 +850,8 @@ DEPENDENCIES leaflet-draw-rails leaflet-markercluster-rails (~> 0.7.0) leaflet-rails + lograge + logstash-event logstasher mailjet maruku diff --git a/README.md b/README.md index 6277da631..cb8deaf55 100644 --- a/README.md +++ b/README.md @@ -108,11 +108,9 @@ Une fois `overmind` lancé, et un breakpoint `byebug` inséré dans le code, il ## Linting -- Faire tourner RuboCop : `bundle exec rubocop` -- Faire tourner Brakeman : `bundle exec brakeman` -- Linter les fichiers HAML : `bundle exec haml-lint app/views/` -- Linter les fichiers SCSS : `bundle exec scss-lint app/assets/stylesheets/` -- Linter les fichiers JavaScript : `yarn lint:js` (`yarn lint:js --fix`) +Le projet utilise plusieurs linters pour vérifier la lisibilité et la qualité code. + +- Faire tourner tous les linters : `bin/rake lint` - [AccessLint](http://accesslint.com/) tourne automatiquement sur les PRs ## Déploiement diff --git a/Rakefile b/Rakefile index 186e5b1ed..b1ed331ca 100644 --- a/Rakefile +++ b/Rakefile @@ -5,6 +5,14 @@ require File.expand_path('config/application', __dir__) Rails.application.load_tasks +task :lint do + sh "bundle exec rubocop" + sh "bundle exec haml-lint app/views/" + sh "bundle exec scss-lint app/assets/stylesheets/" + sh "bundle exec brakeman --no-pager" + sh "yarn lint:js" +end + task :deploy do domains = %w(37.187.249.111 149.202.72.152 149.202.198.6) domains.each do |domain| diff --git a/app/assets/javascripts/old_design/admin.js b/app/assets/javascripts/old_design/admin.js index 981032ec3..5b7af468e 100644 --- a/app/assets/javascripts/old_design/admin.js +++ b/app/assets/javascripts/old_design/admin.js @@ -1,52 +1,47 @@ -$(document).on('turbolinks:load', init_admin); +$(document).on('click', '.delete', function() { + $(this).hide(); + $(this) + .closest('td') + .find('.confirm') + .show(); +}); -function init_admin(){ - destroy_action(); - on_change_type_de_champ_select(); -} +$(document).on('click', '.cancel', function() { + $(this) + .closest('td') + .find('.delete') + .show(); + $(this) + .closest('td') + .find('.confirm') + .hide(); +}); -function destroy_action(){ - $(".delete").on('click', function(){ - $(this).hide(); - $(this).closest('td').find(".confirm").show(); - }); +$(document).on('change', 'select.form-control.type-champ', function() { + var parent = $(this) + .parent() + .parent(); - $(".cancel").on('click', function(){ - $(this).closest('td').find(".delete").show(); - $(this).closest('td').find(".confirm").hide(); - }); + parent.removeClass('header-section'); + parent.children('.drop-down-list').removeClass('show-inline'); + parent.children('.pj-template').removeClass('show-inline'); - $("#liste-gestionnaire #libelle").on('click', function(){ - setTimeout(destroy_action, 500); - }); -} + $('.mandatory', parent).show(); -function on_change_type_de_champ_select (){ - $("select.form-control.type-champ").on('change', function(e){ - - parent = $(this).parent().parent(); - - parent.removeClass('header-section'); - parent.children(".drop-down-list").removeClass('show-inline'); - parent.children(".pj-template").removeClass('show-inline'); - - $('.mandatory', parent).show(); - - switch(this.value){ - case 'header_section': - parent.addClass('header-section'); - break; - case 'drop_down_list': - case 'multiple_drop_down_list': - case 'linked_drop_down_list': - parent.children(".drop-down-list").addClass('show-inline'); - break; - case 'piece_justificative': - parent.children(".pj-template").addClass('show-inline'); - break; - case 'explication': - $('.mandatory', parent).hide(); - break; - } - }); -} + switch (this.value) { + case 'header_section': + parent.addClass('header-section'); + break; + case 'drop_down_list': + case 'multiple_drop_down_list': + case 'linked_drop_down_list': + parent.children('.drop-down-list').addClass('show-inline'); + break; + case 'piece_justificative': + parent.children('.pj-template').addClass('show-inline'); + break; + case 'explication': + $('.mandatory', parent).hide(); + break; + } +}); diff --git a/app/assets/javascripts/old_design/dossiers_list_link.js b/app/assets/javascripts/old_design/dossiers_list_link.js index 18b167ace..70425251a 100644 --- a/app/assets/javascripts/old_design/dossiers_list_link.js +++ b/app/assets/javascripts/old_design/dossiers_list_link.js @@ -1,10 +1,6 @@ -$(document).on('turbolinks:load', link_init); - -function link_init() { - $('#dossiers-list tr').on('click', function(event) { - var href = $(this).data('href'); - if (href && event.target.tagName !== 'A') { - location.href = href; - } - }); -} +$(document).on('click', '#dossiers-list tr', function(event) { + var href = $(this).data('href'); + if (href && event.target.tagName !== 'A') { + location.href = href; + } +}); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index aa4da2aa3..53796fe13 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -178,7 +178,8 @@ div.pagination { } .alert.alert-success.move-up, -.alert.alert-danger.siret { +.alert.alert-danger.siret, +.alert.sticky { position: fixed; top: 0px; left: 0; diff --git a/app/assets/stylesheets/new_design/dossier-edit.scss b/app/assets/stylesheets/new_design/dossier_edit.scss similarity index 100% rename from app/assets/stylesheets/new_design/dossier-edit.scss rename to app/assets/stylesheets/new_design/dossier_edit.scss diff --git a/app/assets/stylesheets/new_design/dossier_index.scss b/app/assets/stylesheets/new_design/dossier_index.scss index a7b52ca0d..e5a67e1ba 100644 --- a/app/assets/stylesheets/new_design/dossier_index.scss +++ b/app/assets/stylesheets/new_design/dossier_index.scss @@ -7,6 +7,7 @@ .icon { padding: 10px 5px; + margin: 10px 10px; &:hover { cursor: pointer; diff --git a/app/assets/stylesheets/new_design/dossier_show.scss b/app/assets/stylesheets/new_design/dossier_show.scss new file mode 100644 index 000000000..66dc60376 --- /dev/null +++ b/app/assets/stylesheets/new_design/dossier_show.scss @@ -0,0 +1,33 @@ +@import "colors"; +@import "constants"; + +#dossier-show { + .sub-header { + .label { + float: right; + margin-left: $default-spacer; + } + + .title-container { + margin-bottom: $default-padding * 2; + padding-left: 32px; + + .icon.folder { + float: left; + margin-left: -32px; + margin-top: 3px; + } + } + + h1 { + color: $black; + font-size: 22px; + margin-bottom: 0; + } + + h2 { + color: $grey; + font-weight: bold; + } + } +} diff --git a/app/assets/stylesheets/new_design/procedures_show.scss b/app/assets/stylesheets/new_design/procedure_show.scss similarity index 100% rename from app/assets/stylesheets/new_design/procedures_show.scss rename to app/assets/stylesheets/new_design/procedure_show.scss diff --git a/app/assets/stylesheets/new_design/status_progress.scss b/app/assets/stylesheets/new_design/status_progress.scss new file mode 100644 index 000000000..cff35aac6 --- /dev/null +++ b/app/assets/stylesheets/new_design/status_progress.scss @@ -0,0 +1,82 @@ +@import "colors"; +@import "constants"; + +.status-progress { + text-align: center; +} + +.status-timeline { + display: inline-block; + margin-top: $default-padding * 2; + margin-bottom: $default-padding * 2; + border: 1px solid #808080; + border-radius: 3px; + + li { + display: inline-block; + padding-top: $default-spacer; + padding-bottom: $default-spacer; + + &:first-child { + padding-left: 20px; + } + + &:last-child { + padding-right: 20px; + } + + &.active { + font-weight: bold; + } + + &.active ~ li { + color: $grey; + } + + // Arrows + &:not(:last-child)::after { + content: "▸"; + display: inline-block; + margin-left: 10px; + margin-right: 10px; + vertical-align: top; + } + } +} + +.status-explanation { + text-align: left; + + &.brouillon, + &.en-construction, + &.en-instruction { + max-width: 600px; + margin: auto; + } + + h3 { + font-size: 1.1em; + font-weight: bold; + margin-bottom: $default-spacer; + } + + p { + margin-bottom: $default-padding; + } + + blockquote { + quotes: "« " " »" "‘" "’"; + } + + blockquote::before { + content: open-quote; + } + + blockquote::after { + content: close-quote; + } + + .icon { + margin-right: $default-spacer; + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2ee12840f..0997db1f9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -97,15 +97,18 @@ class ApplicationController < ActionController::Base Raven.user_context(context) end - def session_info_payload + def append_info_to_payload(payload) + super user = logged_user - payload = { + payload[:xhr] = !!request.xhr? + + payload.merge!({ user_agent: request.user_agent, - current_user_id: user&.id, - current_user_email: user&.email, - current_user_roles: logged_user_roles - }.compact + user_id: user&.id, + user_email: user&.email, + user_roles: logged_user_roles + }.compact) if browser.known? payload.merge!({ diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 46bae9be5..0cf7d51dd 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -7,13 +7,43 @@ module ApplicationHelper end end - def flash_class(level) + def flash_class(level, sticky = false) case level - when "notice" then "alert-success" - when "alert" then "alert-danger" + when "notice" then "alert-success#{sticky ? ' sticky' : ''}" + when "alert" then "alert-danger#{sticky ? ' sticky' : ''}" end end + def render_to_element(selector, partial:, outer: false, locals: {}) + method = outer ? 'outerHTML' : 'innerHTML' + html = escape_javascript(render partial: partial, locals: locals) + # rubocop:disable Rails/OutputSafety + raw("document.querySelector('#{selector}').#{method} = \"#{html}\";") + # rubocop:enable Rails/OutputSafety + end + + def render_flash(timeout: false, sticky: false) + if flash.any? + html = render_to_element('#flash_messages', partial: 'layouts/flash_messages', locals: { sticky: sticky }, outer: true) + flash.clear + if timeout + html += remove_element('#flash_messages', timeout: timeout, inner: true) + end + html + end + end + + def remove_element(selector, timeout: 0, inner: false) + script = "(function() {"; + script << "var el = document.querySelector('#{selector}');" + method = (inner ? "el.innerHTML = ''" : "el.parentNode.removeChild(el)") + script << "setTimeout(function() { #{method}; }, #{timeout});"; + script << "})();" + # rubocop:disable Rails/OutputSafety + raw(script); + # rubocop:enable Rails/OutputSafety + end + def current_email current_user&.email || current_gestionnaire&.email || @@ -37,4 +67,17 @@ module ApplicationHelper Raven.capture_exception(e) {} end + + def sentry_config + sentry = Rails.application.secrets.sentry + if sentry + { + dsn: sentry[:browser], + id: current_user&.id, + email: current_email + }.to_json + else + {} + end + end end diff --git a/app/javascript/packs/application-old.js b/app/javascript/packs/application-old.js index 89c9a9706..2af95caeb 100644 --- a/app/javascript/packs/application-old.js +++ b/app/javascript/packs/application-old.js @@ -13,6 +13,7 @@ import 'babel-polyfill'; import 'typeahead.js'; +import '../shared/sentry'; import '../shared/rails-ujs-fix'; // Start Rails helpers diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index dce4ef0c5..e94c60e8d 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -13,6 +13,7 @@ import 'babel-polyfill'; import 'select2'; import 'typeahead.js'; +import '../shared/sentry'; import '../shared/rails-ujs-fix'; import '../new_design/buttons'; diff --git a/app/javascript/shared/sentry.js b/app/javascript/shared/sentry.js new file mode 100644 index 000000000..7332390d0 --- /dev/null +++ b/app/javascript/shared/sentry.js @@ -0,0 +1,14 @@ +import { init, configureScope } from '@sentry/browser'; +import { getData } from './data'; + +const { dsn, email, id } = getData('sentry'); + +if (dsn) { + init({ dsn }); + + if (email) { + configureScope(scope => { + scope.setUser({ id, email }); + }); + } +} diff --git a/app/models/champs/checkbox_champ.rb b/app/models/champs/checkbox_champ.rb index 5bd6dea75..9202ca635 100644 --- a/app/models/champs/checkbox_champ.rb +++ b/app/models/champs/checkbox_champ.rb @@ -4,4 +4,8 @@ class Champs::CheckboxChamp < Champ [ libelle ] end end + + def to_s + value == 'on' ? 'oui' : 'non' + end end diff --git a/app/views/admin/pieces_justificatives/show.js.erb b/app/views/admin/pieces_justificatives/show.js.erb index d89882b53..32d4bf4d3 100644 --- a/app/views/admin/pieces_justificatives/show.js.erb +++ b/app/views/admin/pieces_justificatives/show.js.erb @@ -1,4 +1,2 @@ -<% flash.each do |type, message| %> -$("#flash_message").html("
<%= sanitize(message) %>
").children().fadeOut(5000) -<% end %> -$('#piece_justificative_form').html("<%= escape_javascript(render partial: 'form', locals: { procedure: @procedure } ) %>"); +<%= render_flash(timeout: 3000, sticky: true) %> +<%= render_to_element('#piece_justificative_form', partial: 'admin/pieces_justificatives/form', locals: { procedure: @procedure }) %> diff --git a/app/views/admin/procedures/index.js.erb b/app/views/admin/procedures/index.js.erb index 232965d99..b2e0bdc6a 100644 --- a/app/views/admin/procedures/index.js.erb +++ b/app/views/admin/procedures/index.js.erb @@ -1,2 +1 @@ <%= smart_listing_update :procedures %> -link_init(); \ No newline at end of file diff --git a/app/views/admin/procedures/transfer.js.erb b/app/views/admin/procedures/transfer.js.erb index a23b011f9..7dbdff4fe 100644 --- a/app/views/admin/procedures/transfer.js.erb +++ b/app/views/admin/procedures/transfer.js.erb @@ -1,9 +1,7 @@ <%- if response.status == 404 %> transfer_errors_message(true); <%- else %> - $("#main-container").prepend("
"); - $("#flash_message").prepend("
<%= sanitize(flash.notice) %>
"); - <% flash.clear %> + <%= render_flash %> transfer_errors_message(false); $("#email_admin").val(''); diff --git a/app/views/admin/types_de_champ/show.js.erb b/app/views/admin/types_de_champ/show.js.erb index c76ba0c80..5c22f8080 100644 --- a/app/views/admin/types_de_champ/show.js.erb +++ b/app/views/admin/types_de_champ/show.js.erb @@ -1,5 +1,2 @@ -<% flash.each do |type, message| %> -$("#flash_message").html("
<%= sanitize(message) %>
").children().fadeOut(5000) -<% end %> -$('#liste-champ').html("<%= escape_javascript(render partial: 'admin/types_de_champ/form', locals: { procedure: @procedure, types_de_champ: @types_de_champ } ) %>"); -on_change_type_de_champ_select (); +<%= render_flash(timeout: 3000, sticky: true) %> +<%= render_to_element('#liste-champ', partial: 'admin/types_de_champ/form', locals: { procedure: @procedure, types_de_champ: @types_de_champ }) %> diff --git a/app/views/champs/siret/index.js.erb b/app/views/champs/siret/index.js.erb index 4f13c1678..3b74d6a35 100644 --- a/app/views/champs/siret/index.js.erb +++ b/app/views/champs/siret/index.js.erb @@ -1,8 +1,5 @@ -(function() { - <% if @blank || @error %> - var html = "<%= escape_javascript(render partial: 'shared/champs/siret/delete_etablissement', locals: { message: @error, position: @champ.order_place, etablissement: @etablissement }) %>"; - <% else %> - var html = "<%= escape_javascript(render partial: 'shared/champs/siret/etablissement', locals: { position: @champ.order_place, etablissement: @etablissement }) %>"; - <% end %> - document.querySelector("#etablissement-for-<%= @champ.id %>").innerHTML = html; -})(); +<% if @blank || @error %> + <%= render_to_element("#etablissement-for-#{@champ.id}", partial: 'shared/champs/siret/delete_etablissement', locals: { message: @error, position: @champ.order_place, etablissement: @etablissement }) %> +<% else %> + <%= render_to_element("#etablissement-for-#{@champ.id}", partial: 'shared/champs/siret/etablissement', locals: { position: @champ.order_place, etablissement: @etablissement }) %> +<% end %> diff --git a/app/views/invites/create.js.erb b/app/views/invites/create.js.erb index 19c233149..c73db9f67 100644 --- a/app/views/invites/create.js.erb +++ b/app/views/invites/create.js.erb @@ -1,6 +1,2 @@ -var formView = "<%= escape_javascript(render partial: 'invites/form', locals: { dossier: @dossier }) %>"; -document.querySelector("#invites-form").outerHTML = formView; - -var flashMessagesView = "<%= escape_javascript(render partial: 'layouts/flash_messages') %>"; -document.querySelector("#flash_messages").outerHTML = flashMessagesView; -<% flash.clear %> +<%= render_to_element('#invites-form', partial: 'invites/form', locals: { dossier: @dossier }, outer: true) %> +<%= render_flash %> diff --git a/app/views/layouts/_flash_messages.html.haml b/app/views/layouts/_flash_messages.html.haml index 5f4b14ef3..a365b07f6 100644 --- a/app/views/layouts/_flash_messages.html.haml +++ b/app/views/layouts/_flash_messages.html.haml @@ -2,11 +2,12 @@ - if flash.any? #flash_message.center - flash.each do |key, value| + - sticky = defined?(sticky) ? sticky : false - if value.class == Array - .alert{ class: flash_class(key) } + .alert{ class: flash_class(key, sticky) } - value.each do |message| = sanitize(message) %br - else - .alert{ class: flash_class(key) } + .alert{ class: flash_class(key, sticky) } = sanitize(value) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index cc6e06cb7..be9304b91 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -17,7 +17,9 @@ = csrf_meta_tags :javascript - DATA = []; + DATA = [{ + sentry: #{raw(sentry_config)} + }]; %body = render partial: 'layouts/support_navigator_banner' = render partial: 'layouts/pre_maintenance' diff --git a/app/views/layouts/new_application.html.haml b/app/views/layouts/new_application.html.haml index 661a8990c..268c24f61 100644 --- a/app/views/layouts/new_application.html.haml +++ b/app/views/layouts/new_application.html.haml @@ -22,7 +22,9 @@ = stylesheet_link_tag :xray :javascript - DATA = []; + DATA = [{ + sentry: #{raw(sentry_config)} + }]; %body .page-wrapper = render partial: "layouts/support_navigator_banner" diff --git a/app/views/new_gestionnaire/dossiers/_state_button.html.haml b/app/views/new_gestionnaire/dossiers/_state_button.html.haml index 61615d8da..5769317c5 100644 --- a/app/views/new_gestionnaire/dossiers/_state_button.html.haml +++ b/app/views/new_gestionnaire/dossiers/_state_button.html.haml @@ -37,7 +37,7 @@ %span.icon.without-continuation .description %h4 Classer sans suite - L'usager ne recevra aucune notification + L'usager sera notifié que son dossier a été classé sans suite %li{ onclick: "DS.showMotivation('refuse');" } %span.icon.refuse .description diff --git a/app/views/new_gestionnaire/dossiers/_state_button_motivation.html.haml b/app/views/new_gestionnaire/dossiers/_state_button_motivation.html.haml index 1c4a79335..c1bbfa66f 100644 --- a/app/views/new_gestionnaire/dossiers/_state_button_motivation.html.haml +++ b/app/views/new_gestionnaire/dossiers/_state_button_motivation.html.haml @@ -4,8 +4,8 @@ #{popup_title} = form_tag(terminer_gestionnaire_dossier_path(dossier.procedure, dossier), method: :post, class: 'form') do - = text_area :dossier, :motivation, class: 'motivation-text-area', placeholder: 'Rédigez votre motivation ici (facultative)' - if title == 'Accepter' + = text_area :dossier, :motivation, class: 'motivation-text-area', placeholder: 'Rédigez votre motivation ici (facultative)', required: false %p.help L'acceptation du dossier envoie automatiquement une attestation à l'usager. @@ -26,7 +26,8 @@ %ul - unspecified_annotations_privees.each do |unspecified_annotations_privee| %li= unspecified_annotations_privee.libelle - + - else + = text_area :dossier, :motivation, class: 'motivation-text-area', placeholder: 'Rédigez votre motivation ici (obligatoire)', required: true .text-right %span.button{ onclick: 'DS.motivationCancel();' } Annuler = button_tag 'Valider la décision', name: :process_action, value: process_action, class: 'button primary', title: title, data: { confirm: confirm } diff --git a/app/views/new_user/dossiers/index.html.haml b/app/views/new_user/dossiers/index.html.haml index 5124dae82..2613d2a50 100644 --- a/app/views/new_user/dossiers/index.html.haml +++ b/app/views/new_user/dossiers/index.html.haml @@ -15,17 +15,6 @@ = link_to(dossiers_path(current_tab: 'dossiers-invites')) do dossiers invités -- if current_user.feedbacks.empty? - .container#user-satisfaction - %h3 Que pensez-vous de ce service ? - .icons - = link_to feedback_path(mark: 0), data: { remote: true, method: :post } do - %span.icon.frown - = link_to feedback_path(mark: 1), data: { remote: true, method: :post } do - %span.icon.meh - = link_to feedback_path(mark: 2), data: { remote: true, method: :post } do - %span.icon.smile - .container - if @dossiers.present? - if @dossiers.total_pages >= 2 @@ -64,6 +53,17 @@ = dossier.updated_at.localtime.strftime("%d/%m/%Y") = paginate(@dossiers) + - if current_user.feedbacks.empty? + #user-satisfaction + %h3 Que pensez-vous de ce service ? + .icons + = link_to feedback_path(mark: 0), data: { remote: true, method: :post } do + %span.icon.frown + = link_to feedback_path(mark: 1), data: { remote: true, method: :post } do + %span.icon.meh + = link_to feedback_path(mark: 2), data: { remote: true, method: :post } do + %span.icon.smile + - else .dossiers-table-empty %h2.empty-text Aucun dossier. diff --git a/app/views/new_user/dossiers/show.html.haml b/app/views/new_user/dossiers/show.html.haml index 439052790..a9154569c 100644 --- a/app/views/new_user/dossiers/show.html.haml +++ b/app/views/new_user/dossiers/show.html.haml @@ -1,3 +1,4 @@ -%h1 - Dossier - = @dossier.id +#dossier-show + = render partial: 'new_user/dossiers/show/header', locals: { dossier: @dossier } + + = render partial: 'new_user/dossiers/show/resume', locals: { dossier: @dossier } diff --git a/app/views/new_user/dossiers/show/_header.html.haml b/app/views/new_user/dossiers/show/_header.html.haml new file mode 100644 index 000000000..c4d4b63d4 --- /dev/null +++ b/app/views/new_user/dossiers/show/_header.html.haml @@ -0,0 +1,12 @@ +.sub-header + .container + = render partial: 'shared/dossiers/status', locals: { dossier: dossier } + + .title-container + %span.icon.folder + %h1= dossier.procedure.libelle + %h2 Dossier nº #{dossier.id} + + %ul.tabs + %li.active + = link_to "Résumé", dossier_path(dossier) diff --git a/app/views/new_user/dossiers/show/_resume.html.haml b/app/views/new_user/dossiers/show/_resume.html.haml new file mode 100644 index 000000000..d0e584949 --- /dev/null +++ b/app/views/new_user/dossiers/show/_resume.html.haml @@ -0,0 +1,2 @@ +.container + = render partial: 'new_user/dossiers/show/status_progress', locals: { dossier: dossier } diff --git a/app/views/new_user/dossiers/show/_status_progress.html.haml b/app/views/new_user/dossiers/show/_status_progress.html.haml new file mode 100644 index 000000000..f2acfb447 --- /dev/null +++ b/app/views/new_user/dossiers/show/_status_progress.html.haml @@ -0,0 +1,63 @@ +.status-progress + - if !dossier.termine? + %ul.status-timeline + %li.brouillon{ class: dossier.brouillon? ? 'active' : nil } + brouillon + %li.en-construction{ class: dossier.en_construction? ? 'active' : nil } + en construction + %li.en-instruction{ class: dossier.en_instruction? ? 'active' : nil } + en instruction + %li.termine{ class: dossier.termine? ? 'active' : nil } + terminé + + .status-explanation + - if dossier.brouillon? + .brouillon + %p Vous pouvez remplir votre dossier tranquillement : il n’est pas encore visible par l’administration. + %p Quand vous aurez terminé, soumettez votre dossier pour qu’il soit examiné. + + - elsif dossier.en_construction? + .en-construction + %p Un accompagnant de l’administration est en train de vérifier que votre dossier est bien complet. + %p Si des modifications sont nécessaires, vous recevrez un email avec les modifications à effectuer. Et sinon, dès que votre dossier sera complet, il passera automatiquement en instruction. + + - elsif dossier.en_instruction? + .en-instruction + %p Votre dossier est complet. Il est en cours d’examen par les agent·e·s de l’administration. + %p Dès que l’administration aura statué sur votre dossier, vous recevrez un email avec le résultat. + + - elsif dossier.accepte? + .accepte + %p + %span.icon.accept + Votre dossier a été + = succeed '.' do + %strong accepté + + - if dossier.motivation.present? + %h3 Motif de l’acceptation + %blockquote= dossier.motivation + + - elsif dossier.refuse? + .refuse + %p + %span.icon.refuse + Nous sommes désolés, votre dossier a malheureusement été + = succeed '.' do + %strong refusé + + - if dossier.motivation.present? + %h3 Motif du refus + %blockquote= dossier.motivation + + - elsif dossier.sans_suite? + .sans-suite + %p + %span.icon.without-continuation + Votre dossier a été classé + = succeed '.' do + %strong sans suite + + - if dossier.motivation.present? + %h3 Motif du classement sans suite + %blockquote= dossier.motivation diff --git a/app/views/new_user/feedbacks/create.js.erb b/app/views/new_user/feedbacks/create.js.erb index 1e3fea19f..de4c271b3 100644 --- a/app/views/new_user/feedbacks/create.js.erb +++ b/app/views/new_user/feedbacks/create.js.erb @@ -1,4 +1,2 @@ -document.querySelector('#user-satisfaction').innerHTML = ''; -var flashMessagesView = "<%= escape_javascript(render partial: 'layouts/flash_messages') %>"; -document.querySelector("#flash_messages").outerHTML = flashMessagesView; -<% flash.clear %> \ No newline at end of file +<%= remove_element('#user-satisfaction') %> +<%= render_flash %> diff --git a/app/views/notification_mailer/refused_mail.html.haml b/app/views/notification_mailer/refused_mail.html.haml index dc198cead..79f5991c6 100644 --- a/app/views/notification_mailer/refused_mail.html.haml +++ b/app/views/notification_mailer/refused_mail.html.haml @@ -4,6 +4,9 @@ %p Votre dossier nº --numéro du dossier-- a été refusé le --date de décision--. +%p + Le motif de refus est le suivant : --motivation-- + %p Pour en savoir plus sur le motif du refus, vous pouvez consulter votre dossier et les éventuels messages de l'administration à cette adresse : --lien dossier-- diff --git a/app/views/notification_mailer/without_continuation_mail.html.haml b/app/views/notification_mailer/without_continuation_mail.html.haml index ac98bc1a0..c0721aee1 100644 --- a/app/views/notification_mailer/without_continuation_mail.html.haml +++ b/app/views/notification_mailer/without_continuation_mail.html.haml @@ -4,6 +4,9 @@ %p Votre dossier nº --numéro du dossier-- a été classé sans suite le --date de décision--. +%p + Le motif est le suivant : --motivation-- + %p Pour en savoir plus sur les raisons de ce classement sans suite, vous pouvez consulter votre dossier et les éventuels messages de l'administration à cette adresse : --lien dossier-- diff --git a/app/views/shared/dossiers/_status.html.haml b/app/views/shared/dossiers/_status.html.haml index 89ec7d002..6afc81dc6 100644 --- a/app/views/shared/dossiers/_status.html.haml +++ b/app/views/shared/dossiers/_status.html.haml @@ -1,9 +1,9 @@ - if dossier.brouillon? %span.label.brouillon brouillon -- if dossier.en_instruction? - %span.label.instruction en instruction - elsif dossier.en_construction? %span.label.construction en construction +- if dossier.en_instruction? + %span.label.instruction en instruction - elsif dossier.accepte? %span.label.accepted accepté - elsif dossier.refuse? diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb new file mode 100644 index 000000000..40ba1dcdb --- /dev/null +++ b/config/initializers/lograge.rb @@ -0,0 +1,30 @@ +Rails.application.configure do + config.lograge.formatter = Lograge::Formatters::Logstash.new + config.lograge.base_controller_class = ['ActionController::Base', 'Manager::ApplicationController'] + + # This will allow to override custom options from environement file + # injected by ansible. + if !config.lograge.custom_options + config.lograge.custom_options = lambda do |event| + { + type: 'tps', + user_id: event.payload[:user_id], + user_email: event.payload[:user_email], + user_roles: event.payload[:user_roles], + user_agent: event.payload[:user_agent], + browser: event.payload[:browser], + browser_version: event.payload[:browser_version], + platform: event.payload[:platform] + }.compact + end + + config.lograge.custom_payload do |controller| + { + xhr: !!controller.request.xhr? + } + end + end + + config.lograge.keep_original_rails_log = true + config.lograge.logger = ActiveSupport::Logger.new Rails.root.join('log', "logstash_#{Rails.env}.log") +end diff --git a/config/logstasher.yml b/config/logstasher.yml deleted file mode 100644 index 9ffa58258..000000000 --- a/config/logstasher.yml +++ /dev/null @@ -1,11 +0,0 @@ -backtrace: true -suppress_app_log: false -log_controller_parameters: false -development: - enabled: false -test: - enabled: false -staging: - enabled: false -production: - enabled: true diff --git a/config/webpack/production.js b/config/webpack/production.js index be0f53aac..067efd4e4 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -2,4 +2,8 @@ process.env.NODE_ENV = process.env.NODE_ENV || 'production' const environment = require('./environment') +// https://github.com/rails/webpacker/issues/1235 +environment.config.optimization.minimizer[0].options.uglifyOptions.ecma = 5; // for IE 11 support +environment.config.optimization.minimizer[0].options.uglifyOptions.safari10 = true; + module.exports = environment.toWebpackConfig() diff --git a/package.json b/package.json index 75418d893..15fcf1da8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@rails/webpacker": "4.0.0-pre.2", + "@sentry/browser": "^4.0.0-beta.12", "activestorage": "^5.2.0", "bloodhound-js": "^1.2.2", "chartkick": "^2.3.6", diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index c264994bb..09ae4b18f 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -17,7 +17,7 @@ describe ApplicationController, type: :controller do let(:current_gestionnaire) { nil } let(:current_administrateur) { nil } let(:current_administration) { nil } - let(:payload) { @controller.send(:session_info_payload) } + let(:payload) { {} } before do expect(@controller).to receive(:current_user).and_return(current_user) @@ -27,6 +27,7 @@ describe ApplicationController, type: :controller do allow(Raven).to receive(:user_context) @controller.send(:set_raven_context) + @controller.send(:append_info_to_payload, payload) end context 'when no one is logged in' do @@ -35,7 +36,16 @@ describe ApplicationController, type: :controller do .with({ ip_address: '0.0.0.0', roles: 'Guest' }) end - it { expect(payload).to eq({ user_agent: 'Rails Testing', current_user_roles: 'Guest' }) } + it do + [:db_runtime, :view_runtime, :variant, :rendered_format].each do |key| + payload.delete(key) + end + expect(payload).to eq({ + user_agent: 'Rails Testing', + user_roles: 'Guest', + xhr: false + }) + end end context 'when a user is logged in' do @@ -47,11 +57,15 @@ describe ApplicationController, type: :controller do end it do + [:db_runtime, :view_runtime, :variant, :rendered_format].each do |key| + payload.delete(key) + end expect(payload).to eq({ user_agent: 'Rails Testing', - current_user_id: current_user.id, - current_user_email: current_user.email, - current_user_roles: 'User' + user_id: current_user.id, + user_email: current_user.email, + user_roles: 'User', + xhr: false }) end end @@ -68,11 +82,15 @@ describe ApplicationController, type: :controller do end it do + [:db_runtime, :view_runtime, :variant, :rendered_format].each do |key| + payload.delete(key) + end expect(payload).to eq({ user_agent: 'Rails Testing', - current_user_id: current_user.id, - current_user_email: current_user.email, - current_user_roles: 'User, Gestionnaire, Administrateur, Administration' + user_id: current_user.id, + user_email: current_user.email, + user_roles: 'User, Gestionnaire, Administrateur, Administration', + xhr: false }) end end diff --git a/spec/factories/dossier.rb b/spec/factories/dossier.rb index 09b4140fd..fec682138 100644 --- a/spec/factories/dossier.rb +++ b/spec/factories/dossier.rb @@ -87,5 +87,48 @@ FactoryBot.define do dossier.save! end end + + trait :accepte do + after(:create) do |dossier, _evaluator| + dossier.state = 'accepte' + dossier.processed_at = dossier.created_at + 1.minute + dossier.en_construction_at = dossier.created_at + 2.minutes + dossier.created_at = dossier.created_at + 3.minutes + dossier.save! + end + end + + trait :refuse do + after(:create) do |dossier, _evaluator| + dossier.state = 'refuse' + dossier.processed_at = dossier.created_at + 1.minute + dossier.en_construction_at = dossier.created_at + 2.minutes + dossier.created_at = dossier.created_at + 3.minutes + dossier.save! + end + end + + trait :sans_suite do + after(:create) do |dossier, _evaluator| + dossier.state = 'sans_suite' + dossier.processed_at = dossier.created_at + 1.minute + dossier.en_construction_at = dossier.created_at + 2.minutes + dossier.created_at = dossier.created_at + 3.minutes + dossier.save! + end + end + + trait :with_motivation do + after(:create) do |dossier, _evaluator| + dossier.motivation = case dossier.state + when 'refuse' + 'L’entreprise concernée n’est pas agréée.' + when 'sans_suite' + 'Le département n’est pas éligible. Veuillez remplir un nouveau dossier auprès de la DDT du 93.' + else + 'Vous avez validé les conditions.' + end + end + end end end diff --git a/spec/features/new_user/dossier_details_spec.rb b/spec/features/new_user/dossier_details_spec.rb index a82cd881b..3804804ae 100644 --- a/spec/features/new_user/dossier_details_spec.rb +++ b/spec/features/new_user/dossier_details_spec.rb @@ -6,11 +6,12 @@ describe 'Dossier details:' do Flipflop::FeatureSet.current.test!.switch!(:new_dossier_details, true) end - scenario 'the user can see the details of their dossier' do + scenario 'the user can see the summary of the dossier status' do visit_dossier dossier expect(page).to have_current_path(dossier_path(dossier)) expect(page).to have_content(dossier.id) + expect(page).to have_selector('.status-explanation') end private diff --git a/spec/models/champs/checkbox_champ_spec.rb b/spec/models/champs/checkbox_champ_spec.rb new file mode 100644 index 000000000..556a8a69a --- /dev/null +++ b/spec/models/champs/checkbox_champ_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Champs::CheckboxChamp do + let(:checkbox) { Champs::CheckboxChamp.new(value: value) } + + describe '#to_s' do + subject { checkbox.to_s } + + context 'when the value is on' do + let(:value) { 'on' } + + it { is_expected.to eq('oui') } + end + + context 'when the value is off' do + let(:value) { 'off' } + + it { is_expected.to eq('non') } + end + end +end diff --git a/spec/views/new_user/dossiers/show.html.haml_spec.rb b/spec/views/new_user/dossiers/show.html.haml_spec.rb index 278315692..c3feada30 100644 --- a/spec/views/new_user/dossiers/show.html.haml_spec.rb +++ b/spec/views/new_user/dossiers/show.html.haml_spec.rb @@ -11,6 +11,7 @@ describe 'new_user/dossiers/show.html.haml', type: :view do subject! { render } it 'affiche les informations du dossier' do - expect(rendered).to have_text("Dossier #{dossier.id}") + expect(rendered).to have_text(dossier.procedure.libelle) + expect(rendered).to have_text("Dossier nº #{dossier.id}") end end diff --git a/spec/views/new_user/dossiers/show/_status_progress.html.haml_spec.rb b/spec/views/new_user/dossiers/show/_status_progress.html.haml_spec.rb new file mode 100644 index 000000000..3ed819b07 --- /dev/null +++ b/spec/views/new_user/dossiers/show/_status_progress.html.haml_spec.rb @@ -0,0 +1,87 @@ +describe 'new_user/dossiers/show/_status_progress.html.haml', type: :view do + subject! { render 'new_user/dossiers/show/status_progress.html.haml', dossier: dossier } + + matcher :have_timeline_item do |selector| + match do |rendered| + expect(rendered).to have_selector(item_selector(selector)) + end + + chain :active do + @active = true + end + + chain :inactive do + @active = false + end + + def item_selector(selector) + item_selector = ".status-timeline #{selector}" + item_selector += '.active' if @active == true + item_selector += ':not(.active)' if @active == false + item_selector + end + end + + context 'when brouillon' do + let(:dossier) { create :dossier } + + it 'renders the timeline (without the final states)' do + expect(rendered).to have_timeline_item('.brouillon').active + expect(rendered).to have_timeline_item('.en-construction').inactive + expect(rendered).to have_timeline_item('.en-instruction').inactive + expect(rendered).to have_timeline_item('.termine').inactive + end + + it { is_expected.to have_selector('.status-explanation .brouillon') } + end + + context 'when en construction' do + let(:dossier) { create :dossier, :en_construction } + + it 'renders the timeline (without the final states)' do + expect(rendered).to have_timeline_item('.brouillon').inactive + expect(rendered).to have_timeline_item('.en-construction').active + expect(rendered).to have_timeline_item('.en-instruction').inactive + expect(rendered).to have_timeline_item('.termine').inactive + end + + it { is_expected.to have_selector('.status-explanation .en-construction') } + end + + context 'when en instruction' do + let(:dossier) { create :dossier, :en_instruction } + + it 'renders the timeline (without the final states)' do + expect(rendered).to have_timeline_item('.brouillon').inactive + expect(rendered).to have_timeline_item('.en-construction').inactive + expect(rendered).to have_timeline_item('.en-instruction').active + expect(rendered).to have_timeline_item('.termine').inactive + end + + it { is_expected.to have_selector('.status-explanation .en-instruction') } + end + + context 'when accepté' do + let(:dossier) { create :dossier, :accepte, :with_motivation } + + it { is_expected.not_to have_selector('.status-timeline') } + it { is_expected.to have_selector('.status-explanation .accepte') } + it { is_expected.to have_text(dossier.motivation) } + end + + context 'when refusé' do + let(:dossier) { create :dossier, :refuse, :with_motivation } + + it { is_expected.not_to have_selector('.status-timeline') } + it { is_expected.to have_selector('.status-explanation .refuse') } + it { is_expected.to have_text(dossier.motivation) } + end + + context 'when classé sans suite' do + let(:dossier) { create :dossier, :sans_suite, :with_motivation } + + it { is_expected.not_to have_selector('.status-timeline') } + it { is_expected.to have_selector('.status-explanation .sans-suite') } + it { is_expected.to have_text(dossier.motivation) } + end +end diff --git a/yarn.lock b/yarn.lock index 387c088b0..bd59124a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -49,6 +49,49 @@ dependencies: any-observable "^0.3.0" +"@sentry/browser@^4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-4.0.0-beta.12.tgz#dff44c7a3732577057844b1643e0ba38c644138b" + dependencies: + "@sentry/core" "4.0.0-beta.12" + "@sentry/hub" "4.0.0-beta.12" + "@sentry/minimal" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + +"@sentry/core@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-4.0.0-beta.12.tgz#c821e41b02c1d66e48fbef16da744f0173575558" + dependencies: + "@sentry/hub" "4.0.0-beta.12" + "@sentry/minimal" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + +"@sentry/hub@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-4.0.0-beta.12.tgz#85267cec47c0bbf1094a537f7d14d19495c77234" + dependencies: + "@sentry/types" "4.0.0-beta.12" + "@sentry/utils" "4.0.0-beta.12" + +"@sentry/minimal@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-4.0.0-beta.12.tgz#534e8edd065646e0e5f8d71443a63f6ce7187573" + dependencies: + "@sentry/hub" "4.0.0-beta.12" + "@sentry/types" "4.0.0-beta.12" + +"@sentry/types@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-4.0.0-beta.12.tgz#0abd303692e48c0fc11afbfea8cbad87e625357a" + +"@sentry/utils@4.0.0-beta.12": + version "4.0.0-beta.12" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-4.0.0-beta.12.tgz#9d51e88634843232b6c6f0edb6df3d3fba83071e" + dependencies: + "@sentry/types" "4.0.0-beta.12" + "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"