Merge pull request #6138 from betagouv/main

2021-04-27-01
This commit is contained in:
Pierre de La Morinerie 2021-04-27 16:41:26 +02:00 committed by GitHub
commit 9488112005
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 156 additions and 282 deletions

View file

@ -1,16 +0,0 @@
@import "colors";
@import "constants";
#user-satisfaction {
text-align: center;
padding: 20px;
.icon {
padding: 10px 5px;
margin: 10px 10px;
&:hover {
cursor: pointer;
}
}
}

View file

@ -233,7 +233,10 @@ module Instructeurs
private
def dossier
@dossier ||= current_instructeur.dossiers.find(params[:dossier_id])
@dossier ||= current_instructeur
.dossiers
.includes(champs: :type_de_champ)
.find(params[:dossier_id])
end
def generate_pdf_for_instructeur_export

View file

@ -17,8 +17,6 @@ class StatsController < ApplicationController
stat.dossiers_deposes_entre_60_et_30_jours
)
@satisfaction_usagers = satisfaction_usagers
@contact_percentage = contact_percentage
@dossiers_states_for_pie = {
@ -123,42 +121,6 @@ class StatsController < ApplicationController
}
end
def satisfaction_usagers
legend = {
Feedback.ratings.fetch(:unhappy) => "Mécontents",
Feedback.ratings.fetch(:neutral) => "Neutres",
Feedback.ratings.fetch(:happy) => "Satisfaits"
}
number_of_weeks = 12
totals = Feedback
.group_by_week(:created_at, last: number_of_weeks, current: false)
.count
legend.keys.map do |rating|
data = Feedback
.where(rating: rating)
.group_by_week(:created_at, last: number_of_weeks, current: false)
.count
.map do |week, count|
total = totals[week]
# By default a week is displayed by the first day of the week but we'd rather display the last day
label = week.next_week
if total > 0
[label, (count.to_f / total * 100).round(2)]
else
[label, 0]
end
end.to_h
{
name: legend[rating],
data: data
}
end
end
def contact_percentage
number_of_months = 13

View file

@ -1,8 +0,0 @@
module Users
class FeedbacksController < UserController
def create
current_user.feedbacks.create!(rating: params[:rating])
flash.notice = "Merci de votre retour, si vous souhaitez nous en dire plus, n'hésitez pas à #{helpers.contact_link('nous contacter', type: Helpscout::FormAdapter::TYPE_AMELIORATION)}."
end
end
end

View file

@ -77,4 +77,22 @@ class Users::SessionsController < Devise::SessionsController
redirect_to link_sent_path(email: instructeur.email)
end
end
private
def handle_unverified_request
log_invalid_authenticity_token_error
super
end
def log_invalid_authenticity_token_error
Sentry.with_scope do |temp_scope|
tags = {
request_tokens: request_authenticity_tokens.compact.map { |t| t.gsub(/.....$/, '*****') }.join(', '),
session_token: session[:_csrf_token]&.gsub(/.....$/, '*****')
}
temp_scope.set_tags(tags)
Sentry.capture_message("ActionController::InvalidAuthenticityToken in Users::SessionsController")
end
end
end

View file

@ -80,10 +80,10 @@ module Types
def messages(id: nil)
if id.present?
Loaders::Record
.for(Commentaire, where: { dossier: object }, includes: [:instructeur, :user], array: true)
.for(Commentaire, where: { dossier: object }, includes: [:instructeur, :expert], array: true)
.load(ApplicationRecord.id_from_typed_id(id))
else
Loaders::Association.for(object.class, commentaires: [:instructeur, :user]).load(object)
Loaders::Association.for(object.class, commentaires: [:instructeur, :expert]).load(object)
end
end

View file

@ -21,7 +21,6 @@ class Champ < ApplicationRecord
belongs_to :dossier, -> { with_discarded }, inverse_of: false, touch: true, optional: false
belongs_to :type_de_champ, inverse_of: :champ, optional: false
belongs_to :parent, class_name: 'Champ', optional: true
has_many :commentaires
has_one_attached :piece_justificative_file
# We declare champ specific relationships (Champs::CarteChamp, Champs::SiretChamp and Champs::RepetitionChamp)

View file

@ -8,14 +8,15 @@
# created_at :datetime not null
# updated_at :datetime not null
# dossier_id :integer
# expert_id :bigint
# instructeur_id :bigint
# user_id :bigint
#
class Commentaire < ApplicationRecord
self.ignored_columns = [:user_id]
belongs_to :dossier, inverse_of: :commentaires, touch: true, optional: false
belongs_to :user, optional: true
belongs_to :instructeur, optional: true
belongs_to :expert, optional: true
validate :messagerie_available?, on: :create
@ -33,10 +34,10 @@ class Commentaire < ApplicationRecord
after_create :notify
def email
if user
user.email
elsif instructeur
if sent_by_instructeur?
instructeur.email
elsif sent_by_expert?
expert.email
else
read_attribute(:email)
end
@ -47,7 +48,7 @@ class Commentaire < ApplicationRecord
end
def redacted_email
if instructeur.present?
if sent_by_instructeur?
if Flipper.enabled?(:hide_instructeur_email, dossier.procedure)
"Instructeur n° #{instructeur.id}"
else
@ -59,8 +60,15 @@ class Commentaire < ApplicationRecord
end
def sent_by_system?
[CONTACT_EMAIL, OLD_CONTACT_EMAIL].include?(email) &&
user.nil? && instructeur.nil?
[CONTACT_EMAIL, OLD_CONTACT_EMAIL].include?(email)
end
def sent_by_instructeur?
instructeur_id.present?
end
def sent_by_expert?
expert_id.present?
end
def sent_by?(someone)
@ -76,15 +84,12 @@ class Commentaire < ApplicationRecord
private
def notify
dossier_user_email = dossier.user.email
invited_users_emails = dossier.invites.pluck(:email).to_a
# - If the email is the contact email, the commentaire is a copy
# of an automated notification email we sent to a user, so do nothing.
# - If a user or an invited user posted a commentaire, do nothing,
# the notification system will properly
# - Otherwise, a instructeur posted a commentaire, we need to notify the user
if !email.in?([CONTACT_EMAIL, dossier_user_email, *invited_users_emails])
if sent_by_instructeur? || sent_by_expert?
notify_user
end
end

View file

@ -1,21 +0,0 @@
# == Schema Information
#
# Table name: feedbacks
#
# id :bigint not null, primary key
# rating :string not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint
#
class Feedback < ApplicationRecord
belongs_to :user, optional: false
enum rating: {
happy: 'happy',
neutral: 'neutral',
unhappy: 'unhappy'
}
validates :rating, presence: true
end

View file

@ -2,47 +2,47 @@
#
# Table name: procedures
#
# id :integer not null, primary key
# aasm_state :string default("brouillon")
# allow_expert_review :boolean default(TRUE), not null
# api_entreprise_token :string
# ask_birthday :boolean default(FALSE), not null
# auto_archive_on :date
# cadre_juridique :string
# cerfa_flag :boolean default(FALSE)
# cloned_from_library :boolean default(FALSE)
# closed_at :datetime
# declarative_with_state :string
# description :string
# direction :string
# duree_conservation_dossiers_dans_ds :integer
# duree_conservation_dossiers_hors_ds :integer
# durees_conservation_required :boolean default(TRUE)
# euro_flag :boolean default(FALSE)
# for_individual :boolean default(FALSE)
# id :integer not null, primary key
# aasm_state :string default("brouillon")
# allow_expert_review :boolean default(TRUE), not null
# api_entreprise_token :string
# ask_birthday :boolean default(FALSE), not null
# auto_archive_on :date
# cadre_juridique :string
# cerfa_flag :boolean default(FALSE)
# cloned_from_library :boolean default(FALSE)
# closed_at :datetime
# declarative_with_state :string
# description :string
# direction :string
# duree_conservation_dossiers_dans_ds :integer
# duree_conservation_dossiers_hors_ds :integer
# durees_conservation_required :boolean default(TRUE)
# euro_flag :boolean default(FALSE)
# experts_require_administrateur_invitation :boolean default(FALSE)
# hidden_at :datetime
# juridique_required :boolean default(TRUE)
# libelle :string
# lien_demarche :string
# lien_notice :string
# lien_site_web :string
# monavis_embed :text
# organisation :string
# path :string not null
# published_at :datetime
# routing_criteria_name :text default("Votre ville")
# test_started_at :datetime
# unpublished_at :datetime
# web_hook_url :string
# whitelisted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# canonical_procedure_id :bigint
# draft_revision_id :bigint
# parent_procedure_id :bigint
# published_revision_id :bigint
# service_id :bigint
# for_individual :boolean default(FALSE)
# hidden_at :datetime
# juridique_required :boolean default(TRUE)
# libelle :string
# lien_demarche :string
# lien_notice :string
# lien_site_web :string
# monavis_embed :text
# organisation :string
# path :string not null
# published_at :datetime
# routing_criteria_name :text default("Votre ville")
# test_started_at :datetime
# unpublished_at :datetime
# web_hook_url :string
# whitelisted_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# canonical_procedure_id :bigint
# draft_revision_id :bigint
# parent_procedure_id :bigint
# published_revision_id :bigint
# service_id :bigint
#
class Procedure < ApplicationRecord

View file

@ -44,7 +44,6 @@ class User < ApplicationRecord
has_many :dossiers, dependent: :destroy
has_many :invites, dependent: :destroy
has_many :dossiers_invites, through: :invites, source: :dossier
has_many :feedbacks, dependent: :destroy
has_many :deleted_dossiers
has_one :france_connect_information, dependent: :destroy
belongs_to :instructeur, optional: true

View file

@ -2,10 +2,10 @@ class CommentaireService
class << self
def build(sender, dossier, params)
case sender
when User
params[:user] = sender
when Instructeur
params[:instructeur] = sender
when Expert
params[:expert] = sender
end
build_with_email(sender.email, dossier, params)

View file

@ -24,19 +24,6 @@
%span.big-number-card-detail
#{number_with_delimiter(@dossiers_numbers[:last_30_days_count])} (#{@dossiers_numbers[:evolution]} %) sur les 30 derniers jours
.stat-card.stat-card-half.pull-left
%span.stat-card-title
Satisfaction usager
.chart-container
.chart
= area_chart @satisfaction_usagers,
stacked: true,
suffix: ' %',
max: 100,
library: { plotOptions: { series: { marker: { enabled: true }}}},
colors: ["#C31C25", "#F5962A", "#25B177"]
.stat-card.stat-card-half.pull-left
%span.stat-card-title
Pourcentage de contact utilisateur

View file

@ -23,17 +23,6 @@
= paginate(deleted_dossiers)
- if current_user.feedbacks.empty? || current_user.feedbacks.last.created_at < 1.month.ago
#user-satisfaction
%h3 Que pensez-vous de la facilité d'utilisation de ce service ?
.icons
= link_to feedback_path(rating: Feedback.ratings.fetch(:unhappy)), data: { remote: true, method: :post } do
%span.icon.frown
= link_to feedback_path(rating: Feedback.ratings.fetch(:neutral)), data: { remote: true, method: :post } do
%span.icon.meh
= link_to feedback_path(rating: Feedback.ratings.fetch(:happy)), data: { remote: true, method: :post } do
%span.icon.smile
- else
.blank-tab
%h2.empty-text Aucun dossier.

View file

@ -31,17 +31,6 @@
= paginate(dossiers)
- if current_user.feedbacks.empty? || current_user.feedbacks.last.created_at < 1.month.ago
#user-satisfaction
%h3 Que pensez-vous de la facilité d'utilisation de ce service ?
.icons
= link_to feedback_path(rating: Feedback.ratings.fetch(:unhappy)), data: { remote: true, method: :post } do
%span.icon.frown
= link_to feedback_path(rating: Feedback.ratings.fetch(:neutral)), data: { remote: true, method: :post } do
%span.icon.meh
= link_to feedback_path(rating: Feedback.ratings.fetch(:happy)), data: { remote: true, method: :post } do
%span.icon.smile
- else
.blank-tab
%h2.empty-text Aucun dossier.

View file

@ -1,7 +0,0 @@
try {
window.scroll({ top: 0, left: 0, behavior: 'smooth' });
} catch(e) {
window.scroll(0, 0);
}
<%= remove_element('#user-satisfaction') %>
<%= render_flash %>

View file

@ -11,7 +11,7 @@
%p
Ouvrez votre boite email <strong>#{@email}</strong> puis cliquez sur le lien dactivation du message <strong>Connexion sécurisée à #{APPLICATION_NAME}</strong>.
%p
= t('views.users.shared.email_can_take_a_while')
= t('views.users.shared.email_can_take_a_while_html')
%section.link-sent-help
%p

View file

@ -58,10 +58,10 @@ fr:
reset_link_sent:
email_sent_html: "Nous vous avons envoyé un email à ladresse <strong>%{email}</strong>."
click_link_to_reset_password: "Cliquez sur le lien contenu dans lemail pour changer votre mot de passe."
no_mail: "Vous navez pas reçu lemail ?"
no_mail: "Vous navez pas reçu lemail ?"
check_spams: "Vérifiez la boite Indésirables ou Spam de votre boite email."
check_account: "Avez-vous bien créé un compte %{application_name} avec ladresse %{email} ? Si aucun compte nexiste avec cette adresse, vous ne recevrez pas de message."
check_france_connect_html: "Vous êtes-vous connecté avec France Connect par le passé ? Dans ce cas <a href=\"%{href}\">essayez à nouveau avec France Connect</a>."
check_account: "Avez-vous bien créé un compte %{application_name} avec ladresse %{email} ? Si aucun compte nexiste avec cette adresse, vous ne recevrez pas de message."
check_france_connect_html: "Vous êtes-vous connecté avec France Connect par le passé ? Dans ce cas <a href=\"%{href}\">essayez à nouveau avec France Connect</a>."
got_it: "Bien reçu !"
open_your_mailbox: "Maintenant ouvrez votre boite email."
title: "Lien de réinitialisation du mot de passe envoyé"

View file

@ -0,0 +1,5 @@
class AddExpertIdToCommentaires < ActiveRecord::Migration[6.1]
def change
add_belongs_to :commentaires, :expert, type: :bigint, foreign_key: true
end
end

View file

@ -0,0 +1,14 @@
class AddUniqueIndexToInvites < ActiveRecord::Migration[6.1]
include Database::MigrationHelpers
disable_ddl_transaction!
def up
delete_duplicates :invites, [:email, :dossier_id]
add_concurrent_index :invites, [:email, :dossier_id], unique: true
end
def down
remove_index :invites, column: [:email, :dossier_id]
end
end

View file

@ -0,0 +1,14 @@
class AddUniqueIndexToProcedures < ActiveRecord::Migration[6.1]
include Database::MigrationHelpers
disable_ddl_transaction!
def up
delete_duplicates :procedures, [:path, :closed_at, :hidden_at, :unpublished_at]
add_concurrent_index :procedures, [:path, :closed_at, :hidden_at, :unpublished_at], name: 'procedure_path_uniqueness', unique: true
end
def down
remove_index :procedures, [:path, :closed_at, :hidden_at, :unpublished_at], name: 'procedure_path_uniqueness'
end
end

View file

@ -0,0 +1,14 @@
class AddUniqueIndexToIndividuals < ActiveRecord::Migration[6.1]
include Database::MigrationHelpers
disable_ddl_transaction!
def up
delete_duplicates :individuals, [:dossier_id]
add_concurrent_index :individuals, [:dossier_id], unique: true
end
def down
remove_index :individuals, [:dossier_id]
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_04_16_160721) do
ActiveRecord::Schema.define(version: 2021_04_27_120002) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -175,7 +175,9 @@ ActiveRecord::Schema.define(version: 2021_04_16_160721) do
t.datetime "updated_at", null: false
t.bigint "user_id"
t.bigint "instructeur_id"
t.bigint "expert_id"
t.index ["dossier_id"], name: "index_commentaires_on_dossier_id"
t.index ["expert_id"], name: "index_commentaires_on_expert_id"
t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id"
t.index ["user_id"], name: "index_commentaires_on_user_id"
end
@ -444,7 +446,7 @@ ActiveRecord::Schema.define(version: 2021_04_16_160721) do
t.datetime "created_at"
t.datetime "updated_at"
t.date "birthdate"
t.index ["dossier_id"], name: "index_individuals_on_dossier_id"
t.index ["dossier_id"], name: "index_individuals_on_dossier_id", unique: true
end
create_table "initiated_mails", id: :serial, force: :cascade do |t|
@ -472,6 +474,7 @@ ActiveRecord::Schema.define(version: 2021_04_16_160721) do
t.datetime "created_at"
t.datetime "updated_at"
t.text "message"
t.index ["email", "dossier_id"], name: "index_invites_on_email_and_dossier_id", unique: true
end
create_table "module_api_cartos", id: :serial, force: :cascade do |t|
@ -557,6 +560,7 @@ ActiveRecord::Schema.define(version: 2021_04_16_160721) do
t.index ["draft_revision_id"], name: "index_procedures_on_draft_revision_id"
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"
t.index ["parent_procedure_id"], name: "index_procedures_on_parent_procedure_id"
t.index ["path", "closed_at", "hidden_at", "unpublished_at"], name: "procedure_path_uniqueness", unique: true
t.index ["path", "closed_at", "hidden_at"], name: "index_procedures_on_path_and_closed_at_and_hidden_at", unique: true
t.index ["published_revision_id"], name: "index_procedures_on_published_revision_id"
t.index ["service_id"], name: "index_procedures_on_service_id"
@ -738,6 +742,7 @@ ActiveRecord::Schema.define(version: 2021_04_16_160721) do
add_foreign_key "champs", "champs", column: "parent_id"
add_foreign_key "closed_mails", "procedures"
add_foreign_key "commentaires", "dossiers"
add_foreign_key "commentaires", "experts"
add_foreign_key "dossier_operation_logs", "bill_signatures"
add_foreign_key "dossier_operation_logs", "instructeurs"
add_foreign_key "dossiers", "groupe_instructeurs"

View file

@ -185,60 +185,6 @@ describe StatsController, type: :controller do
it { expect(subject).to eq(@expected_hash) }
end
describe "#satisfaction_usagers" do
before do
# Test the stats on October 2018 where the 1st, 8th, 15th, 22th and 29th are conveniently Mondays
# Current week: 1 negative feedback
Timecop.freeze(Time.zone.local(2018, 10, 22, 12, 00)) { create(:feedback, :unhappy) }
# Last week: 3 positive, 1 negative
Timecop.freeze(Time.zone.local(2018, 10, 21, 12, 00)) { create(:feedback, :unhappy) }
Timecop.freeze(Time.zone.local(2018, 10, 19, 12, 00)) { create(:feedback, :happy) }
Timecop.freeze(Time.zone.local(2018, 10, 17, 12, 00)) { create(:feedback, :happy) }
Timecop.freeze(Time.zone.local(2018, 10, 15, 12, 00)) { create(:feedback, :happy) }
# N-2 week: 2 positive, 2 negative
Timecop.freeze(Time.zone.local(2018, 10, 14, 12, 00)) { create(:feedback, :unhappy) }
Timecop.freeze(Time.zone.local(2018, 10, 12, 12, 00)) { create(:feedback, :happy) }
Timecop.freeze(Time.zone.local(2018, 10, 10, 12, 00)) { create(:feedback, :unhappy) }
Timecop.freeze(Time.zone.local(2018, 10, 8, 12, 00)) { create(:feedback, :happy) }
# N-3 week: 1 positive, 3 negative
Timecop.freeze(Time.zone.local(2018, 10, 1, 12, 00)) { create(:feedback, :unhappy) }
Timecop.freeze(Time.zone.local(2018, 10, 3, 12, 00)) { create(:feedback, :happy) }
Timecop.freeze(Time.zone.local(2018, 10, 5, 12, 00)) { create(:feedback, :unhappy) }
Timecop.freeze(Time.zone.local(2018, 10, 7, 12, 00)) { create(:feedback, :unhappy) }
end
subject(:stats) do
Timecop.freeze(Time.zone.local(2018, 10, 28, 12, 00)) {
StatsController.new.send(:satisfaction_usagers)
}
end
it 'returns one set of values for each kind of feedback' do
expect(stats.count).to eq 3
expect(stats.map { |g| g[:name] }).to contain_exactly('Satisfaits', 'Neutres', 'Mécontents')
end
it 'returns weekly ratios between a given feedback and all feedback' do
happy_data = stats.find { |g| g[:name] == 'Satisfaits' }[:data]
expect(happy_data.values[-4]).to eq 0
expect(happy_data.values[-3]).to eq 25.0
expect(happy_data.values[-2]).to eq 50.0
expect(happy_data.values[-1]).to eq 75.0
unhappy_data = stats.find { |g| g[:name] == 'Mécontents' }[:data]
expect(unhappy_data.values[-4]).to eq 0
expect(unhappy_data.values[-3]).to eq 75.0
expect(unhappy_data.values[-2]).to eq 50.0
expect(unhappy_data.values[-1]).to eq 25.0
end
it 'excludes values still in the current week' do
unhappy_data = stats.find { |g| g[:name] == 'Mécontents' }[:data]
expect(unhappy_data.values).not_to include(100.0)
end
end
describe '#avis_usage' do
let!(:dossier) { create(:dossier) }
let!(:avis_with_dossier) { create(:avis) }

View file

@ -1,18 +0,0 @@
FactoryBot.define do
factory :feedback do
rating { Feedback.ratings.fetch(:happy) }
association :user
trait :happy do
rating { Feedback.ratings.fetch(:happy) }
end
trait :neutral do
rating { Feedback.ratings.fetch(:neutral) }
end
trait :unhappy do
rating { Feedback.ratings.fetch(:unhappy) }
end
end
end

View file

@ -63,7 +63,7 @@ describe Commentaire do
end
context 'with a commentaire created by a user' do
let(:commentaire) { build :commentaire, user: user }
let(:commentaire) { build :commentaire, email: user.email }
let(:user) { build :user, email: 'some_user@exemple.fr' }
it { is_expected.to eq 'some_user@exemple.fr' }
@ -73,26 +73,34 @@ describe Commentaire do
describe "#notify" do
let(:procedure) { create(:procedure) }
let(:instructeur) { create(:instructeur) }
let(:expert) { create(:expert) }
let(:assign_to) { create(:assign_to, instructeur: instructeur, procedure: procedure) }
let(:user) { create(:user) }
let(:dossier) { create(:dossier, :en_construction, procedure: procedure, user: user) }
let(:commentaire) { Commentaire.new(dossier: dossier, body: "Mon commentaire") }
context "with a commentaire created by a instructeur" do
let(:commentaire) { CommentaireService.build(instructeur, dossier, body: "Mon commentaire") }
it "calls notify_user" do
expect(commentaire).to receive(:notify_user)
commentaire.save
end
end
commentaire.email = instructeur.email
context "with a commentaire created by an expert" do
let(:commentaire) { CommentaireService.build(expert, dossier, body: "Mon commentaire") }
it "calls notify_user" do
expect(commentaire).to receive(:notify_user)
commentaire.save
end
end
context "with a commentaire automatically created (notification)" do
it "does not call notify_user or notify_instructeurs" do
expect(commentaire).not_to receive(:notify_user)
expect(commentaire).not_to receive(:notify_instructeurs)
let(:commentaire) { CommentaireService.build_with_email(CONTACT_EMAIL, dossier, body: "Mon commentaire") }
commentaire.email = CONTACT_EMAIL
it "does not call notify_user" do
expect(commentaire).not_to receive(:notify_user)
commentaire.save
end
end

View file

@ -69,17 +69,4 @@ describe 'users/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 la facilité d\'utilisation 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