diff --git a/README.md b/README.md index ea03f7c33..6277da631 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ Afin d'initialiser l'environnement de développement, exécutez la commande suiv bundle install yarn install +## Bouchonnage de l’authentification + +Créer les fichiers de configuration avec les valeurs par défaut : + + cp config/france_connect.example.yml config/france_connect.yml + cp config/github_secrets.example.yml config/github_secrets.yml ## Création de la base de données @@ -53,13 +59,6 @@ Afin de générer la BDD de l'application, il est nécessaire d'exécuter les co # Migrate the development database and the test database bin/rails db:migrate -## Bouchonnage de l’authentification - -Créer les fichiers de configuration avec les valeurs par défaut : - - cp config/france_connect.example.yml config/france_connect.yml - cp config/github_secrets.example.yml config/github_secrets.yml - ## Connexion a Pipedrive Dans le fichier `config/intializers/token.rb`, ajouter @@ -70,9 +69,9 @@ Dans le fichier `config/intializers/token.rb`, ajouter ## Lancement de l'application - overmind s + overmind start -Un utilisateur de test est disponible, avec les identifiants `test@exemple.fr`/`testpassword`. +L'application tourne à l'adresse `http://localhost:3000`. Un utilisateur de test est disponible, avec les identifiants `test@exemple.fr`/`testpassword`. ## Programmation des jobs diff --git a/app/assets/images/icons/frown-regular.svg b/app/assets/images/icons/frown-regular.svg new file mode 100644 index 000000000..1ba528365 --- /dev/null +++ b/app/assets/images/icons/frown-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/meh-regular.svg b/app/assets/images/icons/meh-regular.svg new file mode 100644 index 000000000..d5069653e --- /dev/null +++ b/app/assets/images/icons/meh-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/icons/smile-regular.svg b/app/assets/images/icons/smile-regular.svg new file mode 100644 index 000000000..9f33fc403 --- /dev/null +++ b/app/assets/images/icons/smile-regular.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/old_design/dossiers_list_link.js b/app/assets/javascripts/old_design/dossiers_list_link.js index 7193bb433..18b167ace 100644 --- a/app/assets/javascripts/old_design/dossiers_list_link.js +++ b/app/assets/javascripts/old_design/dossiers_list_link.js @@ -1,9 +1,10 @@ $(document).on('turbolinks:load', link_init); function link_init() { - $('#dossiers-list tr').on('click', function (event) { - if (event.target.className !== 'btn-sm btn-danger') { - $(location).attr('href', $(this).data('dossier_url')); + $('#dossiers-list tr').on('click', function(event) { + var href = $(this).data('href'); + if (href && event.target.tagName !== 'A') { + location.href = href; } }); } diff --git a/app/assets/stylesheets/new_design/dossier_index.scss b/app/assets/stylesheets/new_design/dossier_index.scss new file mode 100644 index 000000000..a7b52ca0d --- /dev/null +++ b/app/assets/stylesheets/new_design/dossier_index.scss @@ -0,0 +1,15 @@ +@import "colors"; +@import "constants"; + +#user-satisfaction { + text-align: center; + padding: 20px; + + .icon { + padding: 10px 5px; + + &:hover { + cursor: pointer; + } + } +} diff --git a/app/assets/stylesheets/new_design/icons.scss b/app/assets/stylesheets/new_design/icons.scss index c640357d0..7962cf268 100644 --- a/app/assets/stylesheets/new_design/icons.scss +++ b/app/assets/stylesheets/new_design/icons.scss @@ -89,4 +89,16 @@ background-image: image-url("icons/info-blue.svg"); object-fit: contain; } + + &.smile { + background-image: image-url("icons/smile-regular.svg"); + } + + &.frown { + background-image: image-url("icons/frown-regular.svg"); + } + + &.meh { + background-image: image-url("icons/meh-regular.svg"); + } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5f13b18e2..2ee12840f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -75,38 +75,37 @@ class ApplicationController < ActionController::Base ].compact end + def logged_user + logged_users.first + end + def logged_user_roles roles = logged_users.map { |logged_user| logged_user.class.name } roles.any? ? roles.join(', ') : 'Guest' end - def logged_user_info - logged_user = logged_users.first - - if logged_user - { - id: logged_user.id, - email: logged_user.email - } - end - end - def set_raven_context + user = logged_user + context = { ip_address: request.ip, + id: user&.id, + email: user&.email, roles: logged_user_roles - } - context.merge!(logged_user_info || {}) + }.compact Raven.user_context(context) end - def append_info_to_payload(payload) - payload.merge!({ + def session_info_payload + user = logged_user + + payload = { user_agent: request.user_agent, - current_user: logged_user_info, + current_user_id: user&.id, + current_user_email: user&.email, current_user_roles: logged_user_roles - }.compact) + }.compact if browser.known? payload.merge!({ @@ -115,6 +114,8 @@ class ApplicationController < ActionController::Base platform: browser.platform.name, }) end + + payload end def reject diff --git a/app/controllers/new_gestionnaire/avis_controller.rb b/app/controllers/new_gestionnaire/avis_controller.rb index e20c36812..d926de9da 100644 --- a/app/controllers/new_gestionnaire/avis_controller.rb +++ b/app/controllers/new_gestionnaire/avis_controller.rb @@ -96,7 +96,6 @@ module NewGestionnaire sign_in(gestionnaire, scope: :gestionnaire) Avis.link_avis_to_gestionnaire(gestionnaire) - avis = Avis.find(params[:id]) redirect_to url_for(gestionnaire_avis_index_path) else flash[:alert] = gestionnaire.errors.full_messages diff --git a/app/controllers/new_user/dossiers_controller.rb b/app/controllers/new_user/dossiers_controller.rb index b94747c7e..6f53d7295 100644 --- a/app/controllers/new_user/dossiers_controller.rb +++ b/app/controllers/new_user/dossiers_controller.rb @@ -4,8 +4,8 @@ module NewUser helper_method :new_demarche_url - before_action :ensure_ownership!, except: [:index, :modifier, :update, :recherche] - before_action :ensure_ownership_or_invitation!, only: [:modifier, :update] + before_action :ensure_ownership!, except: [:index, :show, :modifier, :update, :recherche] + before_action :ensure_ownership_or_invitation!, only: [:show, :modifier, :update] before_action :ensure_dossier_can_be_updated, only: [:update_identite, :update] before_action :forbid_invite_submission!, only: [:update] @@ -23,6 +23,17 @@ module NewUser end end + def show + if dossier.brouillon? + redirect_to modifier_dossier_path(dossier) + + elsif !Flipflop.new_dossier_details? + redirect_to users_dossier_recapitulatif_path(dossier) + end + + @dossier = dossier + end + def attestation send_data(dossier.attestation.pdf.read, filename: 'attestation.pdf', type: 'application/pdf') end @@ -209,7 +220,7 @@ module NewUser end def draft? - params[:submit_action] == 'draft' + params[:save_draft] end end end diff --git a/app/controllers/new_user/feedbacks_controller.rb b/app/controllers/new_user/feedbacks_controller.rb new file mode 100644 index 000000000..9e7ca19f5 --- /dev/null +++ b/app/controllers/new_user/feedbacks_controller.rb @@ -0,0 +1,6 @@ +class NewUser::FeedbacksController < ApplicationController + def create + current_user.feedbacks.create!(mark: params[:mark]) + flash.notice = "Merci de votre retour" + end +end diff --git a/app/javascript/packs/application-old.js b/app/javascript/packs/application-old.js index 38a2ae10f..89c9a9706 100644 --- a/app/javascript/packs/application-old.js +++ b/app/javascript/packs/application-old.js @@ -1,6 +1,6 @@ import Turbolinks from 'turbolinks'; import Rails from 'rails-ujs'; -import * as ActiveStorage from 'activestorage'; +import ActiveStorage from '../shared/activestorage/ujs'; import Chartkick from 'chartkick'; import Highcharts from 'highcharts'; import Bloodhound from 'bloodhound-js'; @@ -14,7 +14,6 @@ import 'babel-polyfill'; import 'typeahead.js'; import '../shared/rails-ujs-fix'; -import '../shared/direct-uploads'; // Start Rails helpers Chartkick.addAdapter(Highcharts); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 73f5862e8..dce4ef0c5 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -1,7 +1,6 @@ import Turbolinks from 'turbolinks'; import Rails from 'rails-ujs'; -import * as ActiveStorage from 'activestorage'; - +import ActiveStorage from '../shared/activestorage/ujs'; import Chartkick from 'chartkick'; import Highcharts from 'highcharts'; import jQuery from 'jquery'; @@ -15,7 +14,6 @@ import 'select2'; import 'typeahead.js'; import '../shared/rails-ujs-fix'; -import '../shared/direct-uploads'; import '../new_design/buttons'; import '../new_design/form-validation'; diff --git a/app/javascript/packs/mailjet.js b/app/javascript/packs/mailjet.js index 30b85a446..358f26c29 100644 --- a/app/javascript/packs/mailjet.js +++ b/app/javascript/packs/mailjet.js @@ -1,3 +1,8 @@ +// Include runtime-polyfills for older browsers. +// Due to .babelrc's 'useBuiltIns', only polyfills actually +// required by the browsers we support will be included. +import 'babel-polyfill'; + // This file is copied from mailjet. We serve here a copy of it ourselves // to avoid loading javascript files from other domains on the frontpage. diff --git a/app/javascript/shared/activestorage/direct_upload_controller.js b/app/javascript/shared/activestorage/direct_upload_controller.js new file mode 100644 index 000000000..81cf3e9ad --- /dev/null +++ b/app/javascript/shared/activestorage/direct_upload_controller.js @@ -0,0 +1,69 @@ +import { DirectUpload } from 'activestorage'; +import { dispatchEvent } from './helpers'; + +export class DirectUploadController { + constructor(input, file) { + this.input = input; + this.file = file; + this.directUpload = new DirectUpload(this.file, this.url, this); + this.dispatch('initialize'); + } + + start(callback) { + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'hidden'; + hiddenInput.name = this.input.name; + this.input.insertAdjacentElement('beforebegin', hiddenInput); + + this.dispatch('start'); + + this.directUpload.create((error, attributes) => { + if (error) { + hiddenInput.parentNode.removeChild(hiddenInput); + this.dispatchError(error); + } else { + hiddenInput.value = attributes.signed_id; + } + + this.dispatch('end'); + callback(error); + }); + } + + uploadRequestDidProgress(event) { + const progress = (event.loaded / event.total) * 100; + if (progress) { + this.dispatch('progress', { progress }); + } + } + + get url() { + return this.input.getAttribute('data-direct-upload-url'); + } + + dispatch(name, detail = {}) { + detail.file = this.file; + detail.id = this.directUpload.id; + return dispatchEvent(this.input, `direct-upload:${name}`, { detail }); + } + + dispatchError(error) { + const event = this.dispatch('error', { error }); + if (!event.defaultPrevented) { + alert(error); + } + } + + // DirectUpload delegate + + directUploadWillCreateBlobWithXHR(xhr) { + this.dispatch('before-blob-request', { xhr }); + } + + directUploadWillStoreFileWithXHR(xhr) { + this.dispatch('before-storage-request', { xhr }); + xhr.upload.addEventListener('progress', event => + this.uploadRequestDidProgress(event) + ); + } +} diff --git a/app/javascript/shared/activestorage/direct_uploads_controller.js b/app/javascript/shared/activestorage/direct_uploads_controller.js new file mode 100644 index 000000000..c9b453ca8 --- /dev/null +++ b/app/javascript/shared/activestorage/direct_uploads_controller.js @@ -0,0 +1,53 @@ +import { DirectUploadController } from './direct_upload_controller'; +import { findElements, dispatchEvent, toArray } from './helpers'; + +const inputSelector = + 'input[type=file][data-direct-upload-url]:not([disabled])'; + +export class DirectUploadsController { + constructor(form) { + this.form = form; + this.inputs = findElements(form, inputSelector).filter( + input => input.files.length + ); + } + + start(callback) { + const controllers = this.createDirectUploadControllers(); + + const startNextController = () => { + const controller = controllers.shift(); + if (controller) { + controller.start(error => { + if (error) { + callback(error); + this.dispatch('end'); + } else { + startNextController(); + } + }); + } else { + callback(); + this.dispatch('end'); + } + }; + + this.dispatch('start'); + startNextController(); + } + + createDirectUploadControllers() { + const controllers = []; + this.inputs.forEach(input => { + toArray(input.files).forEach(file => { + const controller = new DirectUploadController(input, file); + controllers.push(controller); + }); + }); + return controllers; + } + + dispatch(name, detail = {}) { + return dispatchEvent(this.form, `direct-uploads:${name}`, { detail }); + } +} diff --git a/app/javascript/shared/activestorage/helpers.js b/app/javascript/shared/activestorage/helpers.js new file mode 100644 index 000000000..96623fcac --- /dev/null +++ b/app/javascript/shared/activestorage/helpers.js @@ -0,0 +1,51 @@ +export function getMetaValue(name) { + const element = findElement(document.head, `meta[name="${name}"]`); + if (element) { + return element.getAttribute('content'); + } +} + +export function findElements(root, selector) { + if (typeof root == 'string') { + selector = root; + root = document; + } + const elements = root.querySelectorAll(selector); + return toArray(elements); +} + +export function findElement(root, selector) { + if (typeof root == 'string') { + selector = root; + root = document; + } + return root.querySelector(selector); +} + +export function dispatchEvent(element, type, eventInit = {}) { + const { disabled } = element; + const { bubbles, cancelable, detail } = eventInit; + const event = document.createEvent('Event'); + + event.initEvent(type, bubbles || true, cancelable || true); + event.detail = detail || {}; + + try { + element.disabled = false; + element.dispatchEvent(event); + } finally { + element.disabled = disabled; + } + + return event; +} + +export function toArray(value) { + if (Array.isArray(value)) { + return value; + } else if (Array.from) { + return Array.from(value); + } else { + return [].slice.call(value); + } +} diff --git a/app/javascript/shared/direct-uploads.js b/app/javascript/shared/activestorage/progress.js similarity index 59% rename from app/javascript/shared/direct-uploads.js rename to app/javascript/shared/activestorage/progress.js index eca7daa45..efaf9349c 100644 --- a/app/javascript/shared/direct-uploads.js +++ b/app/javascript/shared/activestorage/progress.js @@ -47,30 +47,3 @@ addEventListener('direct-upload:end', event => { element.classList.add('direct-upload--complete'); }); - -addEventListener('turbolinks:load', () => { - const submitButtons = document.querySelectorAll( - 'form button[type=submit][data-action]' - ); - const hiddenInput = document.querySelector( - 'form input[type=hidden][name=submit_action]' - ); - - for (let button of submitButtons) { - button.addEventListener('click', () => { - // Active Storage will intercept the form.submit event to upload - // the attached files, and then fire the submit action again – but forgetting - // which button was clicked. So we manually set the type of action that trigerred - // the form submission. - const action = button.getAttribute('data-action'); - hiddenInput.value = action; - // Some form fields are marked as mandatory, but when saving a draft we don't want them - // to be enforced by the browser. - if (action === 'submit') { - button.form.removeAttribute('novalidate'); - } else { - button.form.setAttribute('novalidate', 'novalidate'); - } - }); - } -}); diff --git a/app/javascript/shared/activestorage/ujs.js b/app/javascript/shared/activestorage/ujs.js new file mode 100644 index 000000000..bf70c2310 --- /dev/null +++ b/app/javascript/shared/activestorage/ujs.js @@ -0,0 +1,105 @@ +import { DirectUploadsController } from './direct_uploads_controller'; +import { findElement } from './helpers'; +import './progress'; + +// This is a patched copy of https://github.com/rails/rails/blob/master/activestorage/app/javascript/activestorage/ujs.js +// It fixes support for multiple input/button elements on direct upload forms + +const processingAttribute = 'data-direct-uploads-processing'; +let started = false; + +export function start() { + if (!started) { + started = true; + document.addEventListener('submit', didSubmitForm); + document.addEventListener('click', didSubmitFormElement); + document.addEventListener('ajax:before', didSubmitRemoteElement); + } +} + +export default { start }; + +function didSubmitForm(event) { + handleFormSubmissionEvent(event); +} + +function didSubmitFormElement(event) { + const { target } = event; + if (isSubmitElement(target)) { + handleFormSubmissionEvent(formSubmitEvent(event), target); + } +} + +function didSubmitRemoteElement(event) { + if (event.target.tagName == 'FORM') { + handleFormSubmissionEvent(event); + } +} + +function formSubmitEvent(event) { + return { + target: event.target.form, + preventDefault() { + event.preventDefault(); + } + }; +} + +function isSubmitElement({ tagName, type, form }) { + if (form && (tagName === 'BUTTON' || tagName === 'INPUT')) { + return type === 'submit'; + } + return false; +} + +function handleFormSubmissionEvent(event, button) { + const form = event.target; + + if (form.hasAttribute(processingAttribute)) { + event.preventDefault(); + return; + } + + const controller = new DirectUploadsController(form); + const { inputs } = controller; + + if (inputs.length) { + event.preventDefault(); + form.setAttribute(processingAttribute, ''); + inputs.forEach(disable); + controller.start(error => { + form.removeAttribute(processingAttribute); + if (error) { + inputs.forEach(enable); + } else { + submitForm(form, button); + } + }); + } +} + +function submitForm(form, button) { + button = button || findElement(form, 'input[type=submit]'); + if (button) { + const { disabled } = button; + button.disabled = false; + button.focus(); + button.click(); + button.disabled = disabled; + } else { + button = document.createElement('input'); + button.type = 'submit'; + button.style.display = 'none'; + form.appendChild(button); + button.click(); + form.removeChild(button); + } +} + +function disable(input) { + input.disabled = true; +} + +function enable(input) { + input.disabled = false; +} diff --git a/app/models/feedback.rb b/app/models/feedback.rb new file mode 100644 index 000000000..63affddde --- /dev/null +++ b/app/models/feedback.rb @@ -0,0 +1,3 @@ +class Feedback < ApplicationRecord + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb index 7770f5a22..f1287a153 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -16,6 +16,7 @@ class User < ApplicationRecord has_many :invites, dependent: :destroy has_many :dossiers_invites, through: :invites, source: :dossier has_many :piece_justificative, dependent: :destroy + has_many :feedbacks, dependent: :destroy has_one :france_connect_information, dependent: :destroy delegate :given_name, :family_name, :email_france_connect, :gender, :birthdate, :birthplace, :france_connect_particulier_id, to: :france_connect_information diff --git a/app/views/admin/procedures/_list.html.haml b/app/views/admin/procedures/_list.html.haml index f1948b526..b0157955e 100644 --- a/app/views/admin/procedures/_list.html.haml +++ b/app/views/admin/procedures/_list.html.haml @@ -13,22 +13,20 @@ - @procedures.each do |procedure| - procedure = procedure.decorate - %tr{ id: "tr_dossier_#{procedure.id}", 'data-dossier_url' => admin_procedure_path(id: procedure.id) } - %td= procedure.id - %td.col-xs-6 - = procedure.libelle - - if @active_class - %td.procedure-lien= link_to procedure.lien, procedure.lien, 'data-method' => :get - - if @active_class || @archived_class - %td - = procedure.published_at_fr + - admin_procedure_href = admin_procedure_path(procedure) + %tr{ id: "tr_dossier_#{procedure.id}", data: { href: admin_procedure_href } } + %td= link_to(procedure.id, admin_procedure_href) + %td.col-xs-6= link_to(procedure.libelle, admin_procedure_href) + - if procedure.publiee? + %td.procedure-lien= link_to(procedure.lien, procedure.lien) + - if procedure.publiee_ou_archivee? + %td= link_to(procedure.published_at_fr, admin_procedure_href) - else - %td - = procedure.created_at_fr + %td= link_to(procedure.created_at_fr, admin_procedure_href) %td - = link_to('Cloner', admin_procedure_clone_path(procedure.id), 'data-method' => :put, class: 'btn-sm btn-primary clone-btn') + = link_to('Cloner', admin_procedure_clone_path(procedure.id), data: { method: :put }, class: 'btn-sm btn-primary clone-btn') - if !procedure.publiee_ou_archivee? - = link_to('X', url_for(controller: 'admin/procedures', action: :destroy, id: procedure.id), 'data-method' => :delete, class: 'btn-sm btn-danger') + = link_to('X', url_for(controller: 'admin/procedures', action: :destroy, id: procedure.id), data: { method: :delete }, class: 'btn-sm btn-danger') = smart_listing.paginate = smart_listing.pagination_per_page_links diff --git a/app/views/avis_mailer/avis_invitation.html.haml b/app/views/avis_mailer/avis_invitation.html.haml index 6bb33dba4..401ba1816 100644 --- a/app/views/avis_mailer/avis_invitation.html.haml +++ b/app/views/avis_mailer/avis_invitation.html.haml @@ -12,7 +12,7 @@ - if @avis.gestionnaire.present? %p - = link_to "Connectez-vous pour donner votre avis", gestionnaire_dossier_url(@avis.dossier.procedure, @avis.dossier) + = link_to "Connectez-vous pour donner votre avis", gestionnaire_avis_url(@avis) - else %p = link_to "Inscrivez-vous pour donner votre avis", sign_up_gestionnaire_avis_url(@avis.id, @avis.email) diff --git a/app/views/new_user/dossiers/index.html.haml b/app/views/new_user/dossiers/index.html.haml index f2de5783f..5124dae82 100644 --- a/app/views/new_user/dossiers/index.html.haml +++ b/app/views/new_user/dossiers/index.html.haml @@ -15,6 +15,17 @@ = 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 diff --git a/app/views/new_user/dossiers/show.html.haml b/app/views/new_user/dossiers/show.html.haml new file mode 100644 index 000000000..439052790 --- /dev/null +++ b/app/views/new_user/dossiers/show.html.haml @@ -0,0 +1,3 @@ +%h1 + Dossier + = @dossier.id diff --git a/app/views/new_user/feedbacks/create.js.erb b/app/views/new_user/feedbacks/create.js.erb new file mode 100644 index 000000000..1e3fea19f --- /dev/null +++ b/app/views/new_user/feedbacks/create.js.erb @@ -0,0 +1,4 @@ +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 diff --git a/app/views/notification_mailer/send_draft_notification.html.haml b/app/views/notification_mailer/send_draft_notification.html.haml index e8acfea87..0ac60722c 100644 --- a/app/views/notification_mailer/send_draft_notification.html.haml +++ b/app/views/notification_mailer/send_draft_notification.html.haml @@ -5,7 +5,7 @@ Vous pouvez retrouver et compléter le brouillon que vous avez créé pour la démarche %strong= @dossier.procedure.libelle à l'adresse suivante : - = link_to users_dossiers_url(liste: 'brouillon'), users_dossiers_url(liste: 'brouillon'), target: '_blank' + = link_to dossier_url(@dossier), dossier_url(@dossier), target: '_blank' %p Bonne journée, diff --git a/app/views/root/patron.html.haml b/app/views/root/patron.html.haml index 69d0400af..b0326c258 100644 --- a/app/views/root/patron.html.haml +++ b/app/views/root/patron.html.haml @@ -23,6 +23,9 @@ %span.icon.search %span.icon.sign-out %span.icon.info + %span.icon.frown + %span.icon.meh + %span.icon.smile %h1 Formulaires diff --git a/app/views/shared/dossiers/_edit.html.haml b/app/views/shared/dossiers/_edit.html.haml index 2c83362b3..beae0cdbd 100644 --- a/app/views/shared/dossiers/_edit.html.haml +++ b/app/views/shared/dossiers/_edit.html.haml @@ -4,7 +4,7 @@ - if apercu - form_options = { url: '', method: :get, html: { class: 'form', multipart: true } } - else - - form_options = { url: modifier_dossier_url(dossier), method: :patch, html: { class: 'form', multipart: true, novalidate: dossier.brouillon? } } + - form_options = { url: modifier_dossier_url(dossier), method: :patch, html: { class: 'form', multipart: true } } = form_for dossier, form_options do |f| @@ -59,31 +59,31 @@ - if !apercu .send-wrapper - = hidden_field_tag 'submit_action', 'draft' - - if dossier.brouillon? - if current_user.owns?(dossier) = link_to ask_deletion_dossier_path(dossier), method: :post, class: 'button danger', - data: { confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" } do + data: { disable_with: 'Supprimer le brouillon', confirm: 'En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?' } do Supprimer le brouillon = f.button 'Enregistrer le brouillon', formnovalidate: true, + name: :save_draft, + value: true, class: 'button send secondary', - data: { action: 'draft', disable_with: 'Envoi...' } + data: { disable_with: 'Enregistrer le brouillon' } - if dossier.can_transition_to_en_construction? = f.button 'Soumettre le dossier', class: 'button send primary', disabled: !current_user.owns?(dossier), - data: { action: 'submit', disable_with: 'Envoi...' } + data: { disable_with: 'Soumettre le dossier' } - else = f.button 'Enregistrer les modifications du dossier', class: 'button send primary', - data: { action: 'submit', disable_with: 'Envoi...' } + data: { disable_with: 'Enregistrer les modifications du dossier' } - if dossier.brouillon? && !current_user.owns?(dossier) .send-notice.invite-cannot-submit diff --git a/config/features.rb b/config/features.rb index f5bcb649f..383c95364 100644 --- a/config/features.rb +++ b/config/features.rb @@ -17,6 +17,9 @@ Flipflop.configure do feature :web_hook + feature :new_dossier_details, + title: "Nouvelle page « Dossier »" + group :production do feature :remote_storage, default: Rails.env.production? || Rails.env.staging? diff --git a/config/initializers/logstasher.rb b/config/initializers/logstasher.rb index d85bb8d6a..0b14a8ede 100644 --- a/config/initializers/logstasher.rb +++ b/config/initializers/logstasher.rb @@ -3,17 +3,7 @@ if LogStasher.enabled fields[:type] = "tps" end - LogStasher.watch('process_action.action_controller') do |name, start, finish, id, payload, store| - store[:user_agent] = payload[:user_agent] - store[:browser] = payload[:browser] - store[:browser_version] = payload[:browser_version] - store[:platform] = payload[:platform] - - store[:current_user_roles] = payload[:current_user_roles] - - if payload[:current_user].present? - store[:current_user_id] = payload[:current_user][:id] - store[:current_user_email] = payload[:current_user][:email] - end + LogStasher.add_custom_fields_to_request_context do |fields| + fields.merge!(session_info_payload) end end diff --git a/config/locales/models/type_de_champ/fr.yml b/config/locales/models/type_de_champ/fr.yml index 465fa0745..aa6a33b41 100644 --- a/config/locales/models/type_de_champ/fr.yml +++ b/config/locales/models/type_de_champ/fr.yml @@ -10,7 +10,7 @@ fr: date: 'Date' datetime: 'Date et Heure' number: 'Nombre' - checkbox: 'Checkbox' + checkbox: 'Case à cocher' civilite: 'Civilité' email: 'Email' phone: 'Téléphone' diff --git a/config/logstasher.yml b/config/logstasher.yml new file mode 100644 index 000000000..9ffa58258 --- /dev/null +++ b/config/logstasher.yml @@ -0,0 +1,11 @@ +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/routes.rb b/config/routes.rb index ce2bf183c..e7830425c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -267,7 +267,7 @@ Rails.application.routes.draw do # scope module: 'new_user' do - resources :dossiers, only: [:index, :update] do + resources :dossiers, only: [:index, :show, :update] do member do get 'identite' patch 'update_identite' @@ -282,6 +282,7 @@ Rails.application.routes.draw do post 'recherche' end end + resource :feedback, only: [:create] end # diff --git a/db/migrate/20180808142237_create_feedbacks.rb b/db/migrate/20180808142237_create_feedbacks.rb new file mode 100644 index 000000000..b79ed6dc3 --- /dev/null +++ b/db/migrate/20180808142237_create_feedbacks.rb @@ -0,0 +1,10 @@ +class CreateFeedbacks < ActiveRecord::Migration[5.2] + def change + create_table :feedbacks do |t| + t.references :user, foreign_key: true + t.integer :mark + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8f1d9c2dd..8358eb230 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: 2018_07_24_153247) do +ActiveRecord::Schema.define(version: 2018_08_08_142237) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -328,6 +328,14 @@ ActiveRecord::Schema.define(version: 2018_07_24_153247) do t.datetime "updated_at" end + create_table "feedbacks", force: :cascade do |t| + t.bigint "user_id" + t.integer "mark" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_feedbacks_on_user_id" + end + create_table "flipflop_features", force: :cascade do |t| t.string "key", null: false t.boolean "enabled", default: false, null: false @@ -625,6 +633,7 @@ ActiveRecord::Schema.define(version: 2018_07_24_153247) do add_foreign_key "closed_mails", "procedures" add_foreign_key "commentaires", "dossiers" add_foreign_key "dossiers", "users" + add_foreign_key "feedbacks", "users" add_foreign_key "initiated_mails", "procedures" add_foreign_key "procedure_paths", "administrateurs" add_foreign_key "procedure_paths", "procedures" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index ce2dbc1c1..c264994bb 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) { {} } + let(:payload) { @controller.send(:session_info_payload) } before do expect(@controller).to receive(:current_user).and_return(current_user) @@ -27,7 +27,6 @@ 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 @@ -50,10 +49,8 @@ describe ApplicationController, type: :controller do it do expect(payload).to eq({ user_agent: 'Rails Testing', - current_user: { - id: current_user.id, - email: current_user.email - }, + current_user_id: current_user.id, + current_user_email: current_user.email, current_user_roles: 'User' }) end @@ -73,10 +70,8 @@ describe ApplicationController, type: :controller do it do expect(payload).to eq({ user_agent: 'Rails Testing', - current_user: { - id: current_user.id, - email: current_user.email - }, + current_user_id: current_user.id, + current_user_email: current_user.email, current_user_roles: 'User, Gestionnaire, Administrateur, Administration' }) end diff --git a/spec/controllers/new_user/dossiers_controller_spec.rb b/spec/controllers/new_user/dossiers_controller_spec.rb index 15f75cae1..c62e00de9 100644 --- a/spec/controllers/new_user/dossiers_controller_spec.rb +++ b/spec/controllers/new_user/dossiers_controller_spec.rb @@ -100,38 +100,38 @@ describe NewUser::DossiersController, type: :controller do let(:user) { create(:user) } let(:asked_dossier) { create(:dossier) } let(:ensure_authorized) { :forbid_invite_submission! } - let(:submit_action) { 'submit' } + let(:draft) { false } before do - @controller.params = @controller.params.merge(dossier_id: asked_dossier.id, submit_action: submit_action) + @controller.params = @controller.params.merge(dossier_id: asked_dossier.id, save_draft: draft) allow(@controller).to receive(:current_user).and_return(user) allow(@controller).to receive(:redirect_to) end context 'when a user save their own draft' do let(:asked_dossier) { create(:dossier, user: user) } - let(:submit_action) { 'draft' } + let(:draft) { true } it_behaves_like 'does not redirect nor flash' end context 'when a user submit their own dossier' do let(:asked_dossier) { create(:dossier, user: user) } - let(:submit_action) { 'submit' } + let(:draft) { false } it_behaves_like 'does not redirect nor flash' end context 'when an invite save the draft for a dossier where they where invited' do before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') } - let(:submit_action) { 'draft' } + let(:draft) { true } it_behaves_like 'does not redirect nor flash' end context 'when an invite submit a dossier where they where invited' do before { create(:invite, dossier: asked_dossier, user: user, type: 'InviteUser') } - let(:submit_action) { 'submit' } + let(:draft) { false } it_behaves_like 'redirects and flashes' end @@ -353,7 +353,7 @@ describe NewUser::DossiersController, type: :controller do it { expect(flash.alert).to eq(['Le champ l doit être rempli.', 'pj']) } context 'and the user saves a draft' do - let(:payload) { submit_payload.merge(submit_action: 'draft') } + let(:payload) { submit_payload.merge(save_draft: true) } it { expect(response).to render_template(:modifier) } it { expect(flash.notice).to eq('Votre brouillon a bien été sauvegardé.') } @@ -376,7 +376,7 @@ describe NewUser::DossiersController, type: :controller do let!(:invite) { create(:invite, dossier: dossier, user: user, type: 'InviteUser') } context 'and the invite saves a draft' do - let(:payload) { submit_payload.merge(submit_action: 'draft') } + let(:payload) { submit_payload.merge(save_draft: true) } before do first_champ.type_de_champ.update(mandatory: true, libelle: 'l') @@ -478,6 +478,37 @@ describe NewUser::DossiersController, type: :controller do end end + describe '#show' do + let(:new_dossier_details_enabled) { false } + + before do + Flipflop::FeatureSet.current.test!.switch!(:new_dossier_details, new_dossier_details_enabled) + sign_in(user) + end + + subject! { get(:show, params: { id: dossier.id }) } + + context 'when the dossier is a brouillon' do + let(:dossier) { create(:dossier, user: user) } + it { is_expected.to redirect_to(modifier_dossier_path(dossier)) } + end + + context 'when the dossier has been submitted' do + let(:dossier) { create(:dossier, :en_construction, user: user) } + + context 'and the new dossier details page is disabled' do + let(:new_dossier_details_enabled) { false } + it { is_expected.to redirect_to(users_dossier_recapitulatif_path(dossier)) } + end + + context 'and the new dossier details page is enabled' do + let(:new_dossier_details_enabled) { true } + it { expect(assigns(:dossier)).to eq(dossier) } + it { is_expected.to render_template(:show) } + end + end + end + describe '#ask_deletion' do before { sign_in(user) } diff --git a/spec/factories/feedback.rb b/spec/factories/feedback.rb new file mode 100644 index 000000000..231989f34 --- /dev/null +++ b/spec/factories/feedback.rb @@ -0,0 +1,5 @@ +FactoryBot.define do + factory :feedback do + mark 3 + end +end diff --git a/spec/features/new_user/dossier_details_spec.rb b/spec/features/new_user/dossier_details_spec.rb new file mode 100644 index 000000000..a82cd881b --- /dev/null +++ b/spec/features/new_user/dossier_details_spec.rb @@ -0,0 +1,28 @@ +describe 'Dossier details:' do + let(:user) { create(:user) } + let(:dossier) { create(:dossier, :en_construction, user: user) } + + before do + Flipflop::FeatureSet.current.test!.switch!(:new_dossier_details, true) + end + + scenario 'the user can see the details of their dossier' do + visit_dossier dossier + + expect(page).to have_current_path(dossier_path(dossier)) + expect(page).to have_content(dossier.id) + end + + private + + def visit_dossier(dossier) + visit dossier_path(dossier) + + expect(page).to have_current_path(new_user_session_path) + fill_in 'user_email', with: user.email + fill_in 'user_password', with: user.password + click_on 'Se connecter' + + expect(page).to have_current_path(dossier_path(dossier)) + end +end diff --git a/spec/mailers/avis_mailer_spec.rb b/spec/mailers/avis_mailer_spec.rb index 69e12687f..4d4af26dd 100644 --- a/spec/mailers/avis_mailer_spec.rb +++ b/spec/mailers/avis_mailer_spec.rb @@ -9,5 +9,15 @@ RSpec.describe AvisMailer, type: :mailer do it { expect(subject.subject).to eq("Donnez votre avis sur le dossier nº #{avis.dossier.id} (#{avis.dossier.procedure.libelle})") } it { expect(subject.body).to include("Vous avez été invité par #{avis.claimant.email} à donner votre avis sur le dossier nº #{avis.dossier.id} de la procédure "#{avis.dossier.procedure.libelle}".") } it { expect(subject.body).to include(avis.introduction) } + it { expect(subject.body).to include(gestionnaire_avis_url(avis)) } + + context 'when the recipient is not already registered' do + before do + avis.email = 'accompagnateur@email.com' + avis.gestionnaire = nil + end + + it { expect(subject.body).to include(sign_up_gestionnaire_avis_url(avis.id, avis.email)) } + end end end diff --git a/spec/models/dossier_spec.rb b/spec/models/dossier_spec.rb index e873d2448..de51f60e9 100644 --- a/spec/models/dossier_spec.rb +++ b/spec/models/dossier_spec.rb @@ -556,6 +556,8 @@ describe Dossier do end describe "#send_draft_notification_email" do + include Rails.application.routes.url_helpers + let(:procedure) { create(:procedure) } let(:user) { create(:user) } @@ -564,15 +566,17 @@ describe Dossier do end it "send an email when the dossier is created for the very first time" do + dossier = nil ActiveJob::Base.queue_adapter = :test expect do perform_enqueued_jobs do - Dossier.create(procedure: procedure, state: "brouillon", user: user) + dossier = Dossier.create(procedure: procedure, state: "brouillon", user: user) end end.to change(ActionMailer::Base.deliveries, :size).from(0).to(1) mail = ActionMailer::Base.deliveries.last expect(mail.subject).to eq("Retrouvez votre brouillon pour la démarche \"#{procedure.libelle}\"") + expect(mail.html_part.body).to include(dossier_url(dossier)) end it "does not send an email when the dossier is created with a non brouillon state" do diff --git a/spec/views/new_user/dossiers/index.html.haml_spec.rb b/spec/views/new_user/dossiers/index.html.haml_spec.rb index 3df0520b7..9ee653464 100644 --- a/spec/views/new_user/dossiers/index.html.haml_spec.rb +++ b/spec/views/new_user/dossiers/index.html.haml_spec.rb @@ -10,6 +10,7 @@ describe 'new_user/dossiers/index.html.haml', type: :view do before do allow(view).to receive(:new_demarche_url).and_return('#') + allow(controller).to receive(:current_user) { user } assign(:user_dossiers, Kaminari.paginate_array(user_dossiers).page(1)) assign(:dossiers_invites, Kaminari.paginate_array(dossiers_invites).page(1)) assign(:dossiers, Kaminari.paginate_array(user_dossiers).page(1)) @@ -70,4 +71,17 @@ describe 'new_user/dossiers/index.html.haml', type: :view do expect(rendered).to have_selector('ul.tabs li.active', count: 1) end end + + context "quand le user n'a aucun feedback" do + it "affiche le formulaire de satisfaction" do + expect(rendered).to have_selector('#user-satisfaction', text: 'Que pensez-vous de ce service ?') + end + end + + context "quand le user a un feedback" do + let(:user) { create(:user, feedbacks: [build(:feedback)]) } + it "n'affiche pas le formulaire de satisfaction" do + expect(rendered).to_not have_selector('#user-satisfaction') + 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 new file mode 100644 index 000000000..278315692 --- /dev/null +++ b/spec/views/new_user/dossiers/show.html.haml_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'new_user/dossiers/show.html.haml', type: :view do + let(:dossier) { create(:dossier, :with_service, state: 'brouillon', procedure: create(:procedure)) } + + before do + sign_in dossier.user + assign(:dossier, dossier) + end + + subject! { render } + + it 'affiche les informations du dossier' do + expect(rendered).to have_text("Dossier #{dossier.id}") + end +end