Merge pull request #3637 from betagouv/dev

2019-03-19-01
This commit is contained in:
Mathieu Magnin 2019-03-19 14:51:23 +01:00 committed by GitHub
commit 5f7aef663f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 361 additions and 26 deletions

View file

@ -68,6 +68,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later FindDubiousProceduresJob.set(cron: "0 0 * * *").perform_later
Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later Administrateurs::ActivateBeforeExpirationJob.set(cron: "0 8 * * *").perform_later
WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later WarnExpiringDossiersJob.set(cron: "0 0 1 * *").perform_later
GestionnaireEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
### Voir les emails envoyés en local ### Voir les emails envoyés en local

View file

@ -6,7 +6,7 @@ $default-padding: 2 * $default-spacer;
// layouts // layouts
$two-columns-padding: 60px; $two-columns-padding: 60px;
$two-columns-breakpoint: $page-width + (2 * $two-columns-padding); $two-columns-breakpoint: 980px;
// z-order // z-order
$alert-z-index: 100; $alert-z-index: 100;

View file

@ -2,7 +2,6 @@ $default-space: 15px;
$contact-padding: $default-space * 2; $contact-padding: $default-space * 2;
#contact-form { #contact-form {
width: 1040px;
margin: 0 auto; margin: 0 auto;
padding-top: $contact-padding; padding-top: $contact-padding;

View file

@ -3,3 +3,7 @@
.mb-1 { .mb-1 {
margin-bottom: $default-spacer; margin-bottom: $default-spacer;
} }
.mr-1 {
margin-right: $default-spacer;
}

View file

@ -13,29 +13,32 @@
@extend .container; @extend .container;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center;
@media (min-width: $two-columns-breakpoint) { @media (min-width: $two-columns-breakpoint) {
flex-direction: row; flex-direction: row;
align-items: stretch; align-items: stretch;
justify-content: center; justify-content: space-between;
} }
} }
.column { .column {
padding: $two-columns-padding 0 0;
width: 100%; width: 100%;
max-width: 500px; max-width: 500px;
margin: 0 auto;
padding: ($default-padding * 2) 0 0 0;
&:last-of-type {
padding-bottom: $default-padding * 2;
}
@media (min-width: $two-columns-breakpoint) { @media (min-width: $two-columns-breakpoint) {
padding: $two-columns-padding; width: 45%;
width: 50%; margin: 0;
padding: $two-columns-padding 0 $two-columns-padding 0;
&:first-child { &:last-of-type {
padding-left: 0; padding-bottom: $two-columns-padding;
}
&:last-child {
padding-right: 0;
} }
} }
} }

View file

@ -26,17 +26,19 @@ $procedure-context-breakpoint: $two-columns-breakpoint;
.procedure-title { .procedure-title {
font-size: 30px; font-size: 30px;
margin: 20px 0 0; margin: 0 0 20px 0;
text-align: center;
@media (min-width: $procedure-context-breakpoint) { @media (min-width: $procedure-context-breakpoint) {
margin: 50px 0 32px; margin: 50px 0 32px;
text-align: left;
} }
} }
.procedure-description { .procedure-description {
font-size: 16px; font-size: 16px;
p { p:not(:last-of-type) {
margin-bottom: 2 * $default-spacer; margin-bottom: 2 * $default-spacer;
} }
@ -52,7 +54,7 @@ $procedure-context-breakpoint: $two-columns-breakpoint;
img { img {
max-height: 50px; max-height: 50px;
max-width: 100%; max-width: 100%;
margin: 0 10px; margin: 0 10px 20px 10px;
@media (min-width: $procedure-context-breakpoint) { @media (min-width: $procedure-context-breakpoint) {
max-height: 130px; max-height: 130px;

View file

@ -6,7 +6,12 @@
h1 { h1 {
color: $black; color: $black;
font-size: 22px; font-size: 22px;
margin-bottom: 2 * $default-padding; margin-bottom: 1 * $default-padding;
}
a.notifications {
display: inline-block;
margin-bottom: 1 * $default-padding;
} }
.dossiers-table { .dossiers-table {

View file

@ -186,6 +186,18 @@ module NewGestionnaire
end end
end end
def email_notifications
@procedure = procedure
@assign_to = assign_to
end
def update_email_notifications
assign_to.update!(email_notifications_enabled: params[:assign_to][:email_notifications_enabled])
flash.notice = 'Vos notifications sont enregistrées.'
redirect_to gestionnaire_procedure_path(procedure)
end
private private
def find_field(table, column) def find_field(table, column)
@ -196,6 +208,10 @@ module NewGestionnaire
field.values_at('table', 'column').join('/') field.values_at('table', 'column').join('/')
end end
def assign_to
current_gestionnaire.assign_to.find_by(procedure: procedure)
end
def statut def statut
@statut ||= (params[:statut].presence || 'a-suivre') @statut ||= (params[:statut].presence || 'a-suivre')
end end

View file

@ -0,0 +1,7 @@
class GestionnaireEmailNotificationJob < ApplicationJob
queue_as :cron
def perform(*args)
NotificationService.send_gestionnaire_email_notification
end
end

View file

@ -43,4 +43,11 @@ class GestionnaireMailer < ApplicationMailer
mail(to: gestionnaire.email, subject: subject) mail(to: gestionnaire.email, subject: subject)
end end
def send_notifications(gestionnaire, data)
@data = data
subject = "Vous avez du nouveau sur vos démarches"
mail(to: gestionnaire.email, subject: subject)
end
end end

View file

@ -3,6 +3,8 @@ class AssignTo < ApplicationRecord
belongs_to :gestionnaire belongs_to :gestionnaire
has_one :procedure_presentation, dependent: :destroy has_one :procedure_presentation, dependent: :destroy
scope :with_email_notifications, -> { where(email_notifications_enabled: true) }
def procedure_presentation_or_default_and_errors def procedure_presentation_or_default_and_errors
errors = reset_procedure_presentation_if_invalid errors = reset_procedure_presentation_if_invalid
[procedure_presentation || build_procedure_presentation, errors] [procedure_presentation || build_procedure_presentation, errors]

View file

@ -11,6 +11,10 @@ class Gestionnaire < ApplicationRecord
has_many :assign_to, dependent: :destroy has_many :assign_to, dependent: :destroy
has_many :procedures, through: :assign_to has_many :procedures, through: :assign_to
has_many :assign_to_with_email_notifications, -> { with_email_notifications }, class_name: 'AssignTo'
has_many :procedures_with_email_notifications, through: :assign_to_with_email_notifications, source: :procedure
has_many :dossiers, -> { state_not_brouillon }, through: :procedures has_many :dossiers, -> { state_not_brouillon }, through: :procedures
has_many :follows has_many :follows
has_many :followed_dossiers, through: :follows, source: :dossier has_many :followed_dossiers, through: :follows, source: :dossier
@ -205,6 +209,25 @@ class Gestionnaire < ApplicationRecord
trusted_device_token&.token_young? trusted_device_token&.token_young?
end end
def email_notification_data
procedures_with_email_notifications
.reduce([]) do |acc, procedure|
h = {
nb_en_construction: procedure.dossiers.en_construction.count,
nb_notification: notifications_per_procedure(procedure).count
}
if h[:nb_en_construction] > 0 || h[:nb_notification] > 0
h[:procedure_id] = procedure.id
h[:procedure_libelle] = procedure.libelle
acc << h
end
acc
end
end
private private
def annotations_hash(demande, annotations_privees, avis, messagerie) def annotations_hash(demande, annotations_privees, avis, messagerie)

View file

@ -0,0 +1,19 @@
class NotificationService
class << self
def send_gestionnaire_email_notification
Gestionnaire
.includes(assign_to: { procedure: :dossiers })
.where(assign_tos: { email_notifications_enabled: true })
.find_in_batches { |gestionnaires| send_batch_of_gestionnaires_email_notification(gestionnaires) }
end
private
def send_batch_of_gestionnaires_email_notification(gestionnaires)
gestionnaires
.map { |gestionnaire| [gestionnaire, gestionnaire.email_notification_data] }
.reject { |(_gestionnaire, data)| data.empty? }
.each { |(gestionnaire, data)| GestionnaireMailer.send_notifications(gestionnaire, data).deliver_later }
end
end
end

View file

@ -107,11 +107,6 @@
qui doivent donc s'identifier en tant que personne physique. qui doivent donc s'identifier en tant que personne physique.
%ul#individual-with-siret %ul#individual-with-siret
%li
.checkbox
%label
= f.check_box :individual_with_siret
Donner la possibilité de renseigner un SIRET au cours de la construction du dossier.
%li %li
.checkbox .checkbox
%label %label

View file

@ -0,0 +1,20 @@
- content_for(:title, 'Du nouveau sur vos démarches')
%p
Bonjour,
%p
Vous avez du nouveau sur demarches-simplifiees.fr.
%ul
- @data.each do |datum|
%li
= link_to(datum[:procedure_libelle], procedure_url(datum[:procedure_id]))
- if datum[:nb_en_construction] > 0
%br
#{datum[:nb_en_construction]} #{'dossier'.pluralize(datum[:nb_en_construction])} en construction
- if datum[:nb_notification] > 0
%br
#{datum[:nb_notification]} #{'notification'.pluralize(datum[:nb_notification])}
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,24 @@
- content_for(:title, "Notifications pour #{@procedure.libelle}")
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to(@procedure.libelle, procedure_path(@procedure)),
'Notifications'] }
.container
%h1 Notifications par email
= form_for @assign_to, url: update_email_notifications_gestionnaire_procedure_path(@procedure), html: { class: 'form' } do |form|
= form.label :email_notification, "Recevoir une fois par jour, du lundi au samedi vers 10 h, un email de notification me signalant le dépôt de nouveaux dossiers ou des changements sur mes dossiers en cours"
.radios
%label
= form.radio_button :email_notifications_enabled, true
Oui
%label
= form.radio_button :email_notifications_enabled, false
Non
.send-wrapper
= link_to "Revenir à la procédure", gestionnaire_procedure_path(@procedure), class: 'button mr-1'
= form.submit "Enregistrer", class: "button primary"

View file

@ -9,6 +9,8 @@
.procedure-header .procedure-header
%h1= procedure_libelle @procedure %h1= procedure_libelle @procedure
= link_to 'configurez vos notifications', email_notifications_gestionnaire_procedure_path(@procedure), class: 'notifications'
%ul.tabs %ul.tabs
= tab_item('à suivre', = tab_item('à suivre',
@ -64,7 +66,7 @@
- if i > 0 - if i > 0
ou ou
%span.filter %span.filter
= link_to remove_filter_gestionnaire_procedure_path(@procedure, statut: @statut, table: filter['table'], column: filter['column'], value: filter['value']) do = link_to remove_filter_gestionnaire_procedure_path(@procedure, { statut: @statut, table: filter['table'], column: filter['column'], value: filter['value'] }) do
%img.close-icon{ src: image_url("close.svg") } %img.close-icon{ src: image_url("close.svg") }
= "#{filter['label'].truncate(50)} : #{filter['value']}" = "#{filter['label'].truncate(50)} : #{filter['value']}"
%table.table.dossiers-table.hoverable %table.table.dossiers-table.hoverable

View file

@ -313,8 +313,10 @@ Rails.application.routes.draw do
patch 'update_displayed_fields' patch 'update_displayed_fields'
get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort' get 'update_sort/:table/:column' => 'procedures#update_sort', as: 'update_sort'
post 'add_filter' post 'add_filter'
get 'remove_filter/:statut/:table/:column/:value' => 'procedures#remove_filter', as: 'remove_filter' get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter'
get 'download_dossiers' get 'download_dossiers'
get 'email_notifications'
patch 'update_email_notifications'
resources :dossiers, only: [:show], param: :dossier_id do resources :dossiers, only: [:show], param: :dossier_id do
member do member do

View file

@ -0,0 +1,5 @@
class AddEmailNotificationsColumnToAssignTo < ActiveRecord::Migration[5.2]
def change
add_column :assign_tos, :email_notifications_enabled, :boolean, default: false, null: false
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_03_11_140926) do ActiveRecord::Schema.define(version: 2019_03_18_154812) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -99,6 +99,7 @@ ActiveRecord::Schema.define(version: 2019_03_11_140926) do
t.integer "procedure_id" t.integer "procedure_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.boolean "email_notifications_enabled", default: false, null: false
t.index ["gestionnaire_id", "procedure_id"], name: "index_assign_tos_on_gestionnaire_id_and_procedure_id", unique: true t.index ["gestionnaire_id", "procedure_id"], name: "index_assign_tos_on_gestionnaire_id_and_procedure_id", unique: true
t.index ["gestionnaire_id"], name: "index_assign_tos_on_gestionnaire_id" t.index ["gestionnaire_id"], name: "index_assign_tos_on_gestionnaire_id"
t.index ["procedure_id"], name: "index_assign_tos_on_procedure_id" t.index ["procedure_id"], name: "index_assign_tos_on_procedure_id"

View file

@ -336,4 +336,25 @@ describe NewGestionnaire::ProceduresController, type: :controller do
end end
end end
end end
describe '#update_email_notifications' do
let(:gestionnaire) { create(:gestionnaire) }
let!(:procedure) { create(:procedure, gestionnaires: [gestionnaire]) }
context "when logged in" do
before { sign_in(gestionnaire) }
it { expect(gestionnaire.procedures_with_email_notifications).to be_empty }
context 'when the gestionnaire update its preferences' do
let(:assign_to) { gestionnaire.assign_to.find_by(procedure: procedure) }
before do
patch :update_email_notifications, params: { procedure_id: procedure.id, assign_to: { id: assign_to.id, email_notifications_enabled: true } }
end
it { expect(gestionnaire.procedures_with_email_notifications).to eq([procedure]) }
end
end
end
end end

View file

@ -115,7 +115,7 @@ feature "procedure filters" do
end end
def remove_filter(filter_value) def remove_filter(filter_value)
find(:xpath, "(//span[contains(@class, 'filter')]/a[contains(@href, '#{URI.encode(filter_value)}')])[1]").click find(:xpath, "(//span[contains(@class, 'filter')]/a[contains(@href, '#{CGI.escape(filter_value)}')])[1]").click
end end
def add_filter(column_name, filter_value) def add_filter(column_name, filter_value)

View file

@ -19,6 +19,24 @@ class GestionnaireMailerPreview < ActionMailer::Preview
GestionnaireMailer.user_to_gestionnaire(gestionnaire.email) GestionnaireMailer.user_to_gestionnaire(gestionnaire.email)
end end
def send_notifications
data = [
{
procedure_libelle: 'une superbe démarche',
procedure_id: 213,
nb_en_construction: 2,
nb_notification: 2
},
{
procedure_libelle: 'une démarche incroyable',
procedure_id: 213,
nb_en_construction: 1,
nb_notification: 1
}
]
GestionnaireMailer.send_notifications(gestionnaire, data)
end
private private
def gestionnaire def gestionnaire
@ -30,6 +48,10 @@ class GestionnaireMailerPreview < ActionMailer::Preview
end end
def procedure def procedure
Procedure.new(id: 15) Procedure.new(id: 15, libelle: 'libelle')
end
def dossier
Dossier.new(id: 15, procedure: procedure)
end end
end end

View file

@ -413,6 +413,53 @@ describe Gestionnaire, type: :model do
end end
end end
describe '#email_notification_data' do
let(:gestionnaire) { create(:gestionnaire) }
let(:procedure_to_assign) { create(:procedure) }
before do
create(:assign_to, gestionnaire: gestionnaire, procedure: procedure_to_assign, email_notifications_enabled: true)
end
context 'when a dossier in construction exists' do
let!(:dossier) { create(:dossier, procedure: procedure_to_assign, state: Dossier.states.fetch(:en_construction)) }
it do
expect(gestionnaire.email_notification_data).to eq([
{
nb_en_construction: 1,
nb_notification: 0,
procedure_id: procedure_to_assign.id,
procedure_libelle: procedure_to_assign.libelle
}
])
end
end
context 'when a notification exists' do
before do
allow(gestionnaire).to receive(:notifications_per_procedure)
.with(procedure_to_assign)
.and_return([1, 2, 3])
end
it do
expect(gestionnaire.email_notification_data).to eq([
{
nb_en_construction: 0,
nb_notification: 3,
procedure_id: procedure_to_assign.id,
procedure_libelle: procedure_to_assign.libelle
}
])
end
end
context 'otherwise' do
it { expect(gestionnaire.email_notification_data).to eq([]) }
end
end
private private
def assign(procedure_to_assign) def assign(procedure_to_assign)

View file

@ -0,0 +1,63 @@
describe NotificationService do
describe '.send_gestionnaire_email_notification' do
let(:procedure) { create(:procedure) }
before do
allow(GestionnaireMailer).to receive(:send_notifications)
.and_return(double(deliver_later: true))
end
subject { NotificationService.send_gestionnaire_email_notification }
context 'when a gestionnaire does not enable its email notification' do
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
let(:gestionnaire) { create(:gestionnaire) }
before { create(:assign_to, gestionnaire: gestionnaire, procedure: procedure) }
it do
subject
expect(GestionnaireMailer).not_to have_received(:send_notifications)
end
end
context 'when a gestionnaire enables its email_notification on one procedure' do
let(:gestionnaire_with_email_notifications) { create(:gestionnaire) }
before do
create(:assign_to,
gestionnaire: gestionnaire_with_email_notifications,
procedure: procedure,
email_notifications_enabled: true)
end
context "when there is no activity on the gestionnaire's procedures" do
it do
subject
expect(GestionnaireMailer).not_to have_received(:send_notifications)
end
end
context 'when a dossier en construction exists on this procedure' do
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
it do
subject
expect(GestionnaireMailer).to have_received(:send_notifications)
end
end
context 'when there is a notification on this procedure' do
before do
allow_any_instance_of(Gestionnaire).to receive(:notifications_per_procedure)
.and_return([12])
end
it do
subject
expect(GestionnaireMailer).to have_received(:send_notifications)
end
end
end
end
end

View file

@ -0,0 +1,45 @@
require 'rails_helper'
describe 'gestionnaire_mailer/send_notifications.html.haml', type: :view do
let(:gestionnaire) { create(:gestionnaire) }
before do
assign(:data, data)
render
end
context 'when there is one dossier in contruction' do
let(:data) do
[
{
procedure_libelle: 'une superbe démarche',
procedure_id: 213,
nb_en_construction: 1,
nb_notification: 0
}
]
end
it { expect(rendered).to have_link('une superbe démarche', href: procedure_url(213)) }
it { expect(rendered).to have_text('une superbe démarche') }
it { expect(rendered).to have_text('1 dossier en construction') }
it { expect(rendered).not_to have_text('notification') }
end
context 'when there is one notification' do
let(:data) do
[
{
procedure_libelle: 'une superbe démarche',
procedure_id: 213,
nb_en_construction: 0,
nb_notification: 1
}
]
end
it { expect(rendered).not_to have_text('en construction') }
it { expect(rendered).to have_text('1 notification') }
end
end