Merge pull request #9637 from adullact/feature-ouidou/admin_creation_delegation_administrateur_contact_gestionnaire

Delégation de compte admin: ETQ admin je peux contacter mon/mes gestionnaires de groupe
This commit is contained in:
Colin Darie 2024-01-11 09:04:34 +00:00 committed by GitHub
commit 0dd3b16d10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 382 additions and 11 deletions

View file

@ -0,0 +1,7 @@
class GroupeGestionnaire::Card::CommentairesComponent < ApplicationComponent
def initialize(groupe_gestionnaire:, administrateur:, path:)
@groupe_gestionnaire = groupe_gestionnaire
@administrateur = administrateur
@path = path
end
end

View file

@ -0,0 +1,5 @@
---
fr:
title:
one: Message
other: Messages

View file

@ -0,0 +1,9 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to @path, id: 'administrateurs', class: 'fr-tile fr-enlarge-link' do
.fr-tile__body.flex.column.align-center.justify-between
%div
.line-count.fr-my-1w
%p.fr-tag= @administrateur.commentaire_groupe_gestionnaires.size
%h3.fr-h6
= t('.title', count: @administrateur.commentaire_groupe_gestionnaires.size)
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.see')

View file

@ -38,12 +38,4 @@ class GroupeGestionnaire::GroupeGestionnaireAdministrateurs::AdministrateurCompo
class: 'fr-btn fr-btn--sm fr-btn--tertiary', class: 'fr-btn fr-btn--sm fr-btn--tertiary',
form: { data: { turbo: true, turbo_confirm: "Supprimer « #{@administrateur.email} » en tant qu'administrateurs ?" } } form: { data: { turbo: true, turbo_confirm: "Supprimer « #{@administrateur.email} » en tant qu'administrateurs ?" } }
end end
def is_there_at_least_another_active_admin?
if @administrateur.active?
@groupe_gestionnaire.administrateurs.count(&:active?) > 1
else
@groupe_gestionnaire.administrateurs.count(&:active?) >= 1
end
end
end end

View file

@ -0,0 +1,24 @@
class GroupeGestionnaire::GroupeGestionnaireCommentaires::CommentaireComponent < ApplicationComponent
include ApplicationHelper
def initialize(commentaire:, connected_user:, is_gestionnaire: true)
@commentaire = commentaire
@connected_user = connected_user
@is_gestionnaire = is_gestionnaire
end
private
def commentaire_issuer
if @commentaire.sent_by?(@connected_user)
t('.you')
else
(@commentaire.gestionnaire || @commentaire.sender).email
end
end
def commentaire_date
is_current_year = (@commentaire.created_at.year == Time.zone.today.year)
l(@commentaire.created_at, format: is_current_year ? :message_date : :message_date_with_year)
end
end

View file

@ -0,0 +1,7 @@
---
en:
reply: Reply
delete_button: Delete this message
confirm: Are you sure you want to delete this message ?
you: You
deleted_body: Message deleted

View file

@ -0,0 +1,7 @@
---
fr:
reply: Répondre
delete_button: Supprimer le message
confirm: Êtes-vous sûr de vouloir supprimer ce message ?
you: Vous
deleted_body: Message supprimé

View file

@ -0,0 +1,12 @@
.width-100
%h2.fr-h6
%span.mail
= commentaire_issuer
%span.date{ class: ["fr-text--xs", "fr-text-mention--grey", "font-weight-normal"] }
= commentaire_date
.rich-text
- if @commentaire.discarded?
%p= t('.deleted_body')
- else
= render SimpleFormatComponent.new(@commentaire.body, allow_a: false)

View file

@ -1,6 +1,6 @@
module Administrateurs module Administrateurs
class GroupeGestionnaireController < AdministrateurController class GroupeGestionnaireController < AdministrateurController
before_action :retrieve_groupe_gestionnaire, only: [:show, :administrateurs, :gestionnaires] before_action :retrieve_groupe_gestionnaire, only: [:show, :administrateurs, :gestionnaires, :commentaires, :create_commentaire]
def show def show
end end
@ -11,6 +11,22 @@ module Administrateurs
def gestionnaires def gestionnaires
end end
def commentaires
@commentaire = Commentaire.new
end
def create_commentaire
@commentaire = @groupe_gestionnaire.commentaire_groupe_gestionnaires.create(commentaire_params.merge(sender: current_administrateur))
if @commentaire.errors.empty?
flash.notice = "Message envoyé"
redirect_to admin_groupe_gestionnaire_commentaires_path
else
flash.alert = @commentaire.errors.full_messages
render :commentaires
end
end
private private
def retrieve_groupe_gestionnaire def retrieve_groupe_gestionnaire
@ -24,5 +40,9 @@ module Administrateurs
flash.alert = 'Groupe inexistant' flash.alert = 'Groupe inexistant'
redirect_to admin_procedures_path, status: 404 redirect_to admin_procedures_path, status: 404
end end
def commentaire_params
params.require(:commentaire).permit(:body)
end
end end
end end

View file

@ -7,6 +7,7 @@ class Administrateur < ApplicationRecord
has_many :procedures, through: :administrateurs_procedures has_many :procedures, through: :administrateurs_procedures
has_many :services has_many :services
has_many :api_tokens, inverse_of: :administrateur, dependent: :destroy has_many :api_tokens, inverse_of: :administrateur, dependent: :destroy
has_many :commentaire_groupe_gestionnaires, as: :sender
has_and_belongs_to_many :default_zones, class_name: 'Zone', join_table: 'default_zones_administrateurs' has_and_belongs_to_many :default_zones, class_name: 'Zone', join_table: 'default_zones_administrateurs'
belongs_to :user belongs_to :user

View file

@ -0,0 +1,28 @@
class CommentaireGroupeGestionnaire < ApplicationRecord
include Discard::Model
belongs_to :groupe_gestionnaire
belongs_to :gestionnaire, optional: true
belongs_to :sender, polymorphic: true
validates :body, presence: { message: "ne peut être vide" }
def soft_deletable?(connected_user)
sent_by?(connected_user) && sent_by_gestionnaire? && !discarded?
end
def soft_delete!
discard!
end
def sent_by_gestionnaire?
gestionnaire_id.present?
end
def sent_by?(someone)
if gestionnaire
someone == gestionnaire
else
someone == sender
end
end
end

View file

@ -1,6 +1,7 @@
class Gestionnaire < ApplicationRecord class Gestionnaire < ApplicationRecord
include UserFindByConcern include UserFindByConcern
has_and_belongs_to_many :groupe_gestionnaires has_and_belongs_to_many :groupe_gestionnaires
has_many :commentaire_groupe_gestionnaires
belongs_to :user belongs_to :user

View file

@ -1,5 +1,6 @@
class GroupeGestionnaire < ApplicationRecord class GroupeGestionnaire < ApplicationRecord
has_many :administrateurs has_many :administrateurs
has_many :commentaire_groupe_gestionnaires
has_and_belongs_to_many :gestionnaires has_and_belongs_to_many :gestionnaires
has_ancestry has_ancestry

View file

@ -0,0 +1,13 @@
= render partial: 'gestionnaires/breadcrumbs',
locals: { steps: [['Mon groupe gestionnaire', admin_groupe_gestionnaire_path],
['Messagerie']], preview: false }
.container
%h1 Messagerie de « #{@groupe_gestionnaire.name} »
.messagerie.container
- if current_administrateur.commentaire_groupe_gestionnaires.where(groupe_gestionnaire: @groupe_gestionnaire).present?
%ul.messages-list{ data: { controller: 'scroll-to' } }
- current_administrateur.commentaire_groupe_gestionnaires.where(groupe_gestionnaire: @groupe_gestionnaire).each do |commentaire|
%li.message{ class: commentaire_is_from_me_class(commentaire, current_administrateur), id: dom_id(commentaire) }
= render(GroupeGestionnaire::GroupeGestionnaireCommentaires::CommentaireComponent.new(commentaire: commentaire, connected_user: current_administrateur, is_gestionnaire: false))
= render partial: "shared/groupe_gestionnaires/commentaires/form", locals: { commentaire: @commentaire, form_url: admin_groupe_gestionnaire_create_commentaire_path }

View file

@ -7,3 +7,4 @@
.fr-grid-row.fr-grid-row--gutters.fr-mb-5w .fr-grid-row.fr-grid-row--gutters.fr-mb-5w
= render GroupeGestionnaire::Card::GestionnairesComponent.new(groupe_gestionnaire: @groupe_gestionnaire, path: admin_groupe_gestionnaire_gestionnaires_path) = render GroupeGestionnaire::Card::GestionnairesComponent.new(groupe_gestionnaire: @groupe_gestionnaire, path: admin_groupe_gestionnaire_gestionnaires_path)
= render GroupeGestionnaire::Card::AdministrateursComponent.new(groupe_gestionnaire: @groupe_gestionnaire, path: admin_groupe_gestionnaire_administrateurs_path) = render GroupeGestionnaire::Card::AdministrateursComponent.new(groupe_gestionnaire: @groupe_gestionnaire, path: admin_groupe_gestionnaire_administrateurs_path)
= render GroupeGestionnaire::Card::CommentairesComponent.new(groupe_gestionnaire: @groupe_gestionnaire, administrateur: current_administrateur, path: admin_groupe_gestionnaire_commentaires_path)

View file

@ -0,0 +1,12 @@
= render NestedForms::FormOwnerComponent.new
= form_for(commentaire, url: form_url) do |f|
- dossier = commentaire.dossier
- placeholder = t('views.shared.dossiers.messages.form.write_message_to_administration_placeholder')
- if instructeur_signed_in? || administrateur_signed_in? || expert_signed_in?
- placeholder = t('views.shared.dossiers.messages.form.write_message_placeholder')
%p.mandatory-explanation= t('asterisk_html', scope: [:utils])
= render Dsfr::InputComponent.new(form: f, attribute: :body, input_type: :text_area, opts: { rows: 5, placeholder: placeholder, title: placeholder, class: 'fr-input message-textarea'})
.fr-mt-3w
= f.submit t('views.shared.dossiers.messages.form.send_message'), class: 'fr-btn', data: { disable: true }

View file

@ -324,6 +324,7 @@ fr:
actions: actions:
save: Enregistrer save: Enregistrer
edit: Modifier edit: Modifier
see: Voir
greetings: greetings:
hello: Bonjour, hello: Bonjour,
best_regards: Cordialement, best_regards: Cordialement,

View file

@ -12,8 +12,8 @@ fr:
one: "%{emails} est déjà gestionnaire de ce groupe" one: "%{emails} est déjà gestionnaire de ce groupe"
other: "%{emails} sont déjà gestionnaires de ce groupe" other: "%{emails} sont déjà gestionnaires de ce groupe"
administrateurs_already_in_groupe_gestionnaire: administrateurs_already_in_groupe_gestionnaire:
one: Cet administrateur est déjà dans un groupe gestionnaire dont vous n'avez pas la gestion. one: Ajout impossible. Cet administrateur fait déjà partie d'un groupe.
other: Ces administrateurs sont déjà dans un groupe gestionnaire dont vous n'avez pas la gestion. other: Ajout impossible. Ces administrateurs font déjà partie d'un groupe.
wrong_address: wrong_address:
one: "%{emails} nest pas une adresse email valide" one: "%{emails} nest pas une adresse email valide"
other: "%{emails} ne sont pas des adresses emails valides" other: "%{emails} ne sont pas des adresses emails valides"

View file

@ -653,6 +653,8 @@ Rails.application.routes.draw do
get 'mon-groupe' => 'groupe_gestionnaire#show', as: :groupe_gestionnaire get 'mon-groupe' => 'groupe_gestionnaire#show', as: :groupe_gestionnaire
get 'mon-groupe/administrateurs' => 'groupe_gestionnaire#administrateurs', as: :groupe_gestionnaire_administrateurs get 'mon-groupe/administrateurs' => 'groupe_gestionnaire#administrateurs', as: :groupe_gestionnaire_administrateurs
get 'mon-groupe/gestionnaires' => 'groupe_gestionnaire#gestionnaires', as: :groupe_gestionnaire_gestionnaires get 'mon-groupe/gestionnaires' => 'groupe_gestionnaire#gestionnaires', as: :groupe_gestionnaire_gestionnaires
get 'mon-groupe/commentaires' => 'groupe_gestionnaire#commentaires', as: :groupe_gestionnaire_commentaires
post 'mon-groupe/create_commentaire' => 'groupe_gestionnaire#create_commentaire', as: :groupe_gestionnaire_create_commentaire
resources :services, except: [:show] do resources :services, except: [:show] do
collection do collection do

View file

@ -0,0 +1,15 @@
class CreateCommentaireGroupeGestionnaires < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
create_table "commentaire_groupe_gestionnaires" do |t|
t.references :groupe_gestionnaire, index: { name: :index_commentaire_groupe_gestionnaires_on_groupe_gestionnaire }
t.references "gestionnaire", null: true
t.string "sender_type", null: false
t.bigint "sender_id", null: false
t.string "body"
t.datetime "discarded_at"
t.timestamps
end
end
end

View file

@ -261,6 +261,19 @@ ActiveRecord::Schema[7.0].define(version: 2023_12_21_142727) do
t.index ["procedure_id"], name: "index_closed_mails_on_procedure_id" t.index ["procedure_id"], name: "index_closed_mails_on_procedure_id"
end end
create_table "commentaire_groupe_gestionnaires", force: :cascade do |t|
t.string "body"
t.datetime "created_at", precision: 6, null: false
t.datetime "discarded_at", precision: 6
t.bigint "gestionnaire_id"
t.bigint "groupe_gestionnaire_id"
t.bigint "sender_id", null: false
t.string "sender_type", null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["gestionnaire_id"], name: "index_commentaire_groupe_gestionnaires_on_gestionnaire_id"
t.index ["groupe_gestionnaire_id"], name: "index_commentaire_groupe_gestionnaires_on_groupe_gestionnaire"
end
create_table "commentaires", id: :serial, force: :cascade do |t| create_table "commentaires", id: :serial, force: :cascade do |t|
t.string "body" t.string "body"
t.datetime "created_at", precision: nil, null: false t.datetime "created_at", precision: nil, null: false

View file

@ -0,0 +1,60 @@
RSpec.describe GroupeGestionnaire::GroupeGestionnaireCommentaires::CommentaireComponent, type: :component do
let!(:component) do
described_class.new(
commentaire: commentaire,
connected_user: connected_user
)
end
let(:connected_user) { create(:administrateur) }
let(:commentaire) { create(:commentaire_groupe_gestionnaire, sender: connected_user) }
subject { render_inline(component).to_html }
it do
is_expected.to include("plop")
is_expected.to include("Vous")
end
describe '#commentaire_date' do
let(:present_date) { Time.zone.local(2018, 9, 2, 10, 5, 0) }
let(:creation_date) { present_date }
let(:commentaire) do
Timecop.freeze(creation_date) { create(:commentaire_groupe_gestionnaire, sender: connected_user) }
end
subject do
Timecop.freeze(present_date) { component.send(:commentaire_date) }
end
it 'doesnt include the creation year' do
expect(subject).to eq 'le 2 septembre à 10 h 05'
end
context 'when displaying a commentaire created on a previous year' do
let(:creation_date) { present_date.prev_year }
it 'includes the creation year' do
expect(subject).to eq 'le 2 septembre 2017 à 10 h 05'
end
end
context 'when formatting the first day of the month' do
let(:present_date) { Time.zone.local(2018, 9, 1, 10, 5, 0) }
it 'includes the ordinal' do
expect(subject).to eq 'le 1er septembre à 10 h 05'
end
end
end
describe '#commentaire_issuer' do
let(:gestionnaire) { create(:gestionnaire) }
let(:commentaire) { create(:commentaire_groupe_gestionnaire, sender: connected_user, gestionnaire: gestionnaire) }
subject { component.send(:commentaire_issuer) }
context 'issuer is connected_user' do
it 'returns gestionnaire s email' do
expect(subject).to eq gestionnaire.email
end
end
end
end

View file

@ -63,4 +63,55 @@ describe Administrateurs::GroupeGestionnaireController, type: :controller do
it { expect(assigns(:groupe_gestionnaire)).to eq(groupe_gestionnaire) } it { expect(assigns(:groupe_gestionnaire)).to eq(groupe_gestionnaire) }
end end
end end
describe '#commentaires' do
let(:gestionnaire) { create(:gestionnaire) }
let!(:groupe_gestionnaire) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire], administrateurs: [admin]) }
subject { get :commentaires }
context "when not logged" do
before { subject }
it { expect(response).to redirect_to(new_user_session_path) }
end
context "when logged in" do
before do
sign_in(admin.user)
end
it { expect(subject).to have_http_status(:ok) }
end
end
describe "#create_commentaire" do
let(:gestionnaire) { create(:gestionnaire) }
let!(:groupe_gestionnaire) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire], administrateurs: [admin]) }
let(:body) { "avant\napres" }
subject {
post :create_commentaire, params: {
commentaire: {
body: body
}
}
}
context "when not logged" do
before { subject }
it { expect(response).to redirect_to(new_user_session_path) }
end
context "when logged in" do
before do
sign_in(admin.user)
end
it "creates a commentaire" do
expect { subject }.to change(CommentaireGroupeGestionnaire, :count).by(1)
expect(response).to redirect_to(admin_groupe_gestionnaire_commentaires_path)
expect(flash.notice).to be_present
end
end
end
end end

View file

@ -0,0 +1,7 @@
FactoryBot.define do
factory :commentaire_groupe_gestionnaire do
association :groupe_gestionnaire
body { 'plop' }
end
end

View file

@ -2,6 +2,7 @@ describe Administrateur, type: :model do
let(:administration) { create(:administration) } let(:administration) { create(:administration) }
describe 'associations' do describe 'associations' do
it { is_expected.to have_many(:commentaire_groupe_gestionnaires) }
it { is_expected.to have_and_belong_to_many(:instructeurs) } it { is_expected.to have_and_belong_to_many(:instructeurs) }
it { is_expected.to belong_to(:groupe_gestionnaire).optional } it { is_expected.to belong_to(:groupe_gestionnaire).optional }
end end

View file

@ -0,0 +1,79 @@
describe CommentaireGroupeGestionnaire, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:groupe_gestionnaire) }
it { is_expected.to belong_to(:gestionnaire).optional }
it { is_expected.to belong_to(:sender) }
end
describe "#soft_deletable?" do
subject { commentaire_groupe_gestionnaire.soft_deletable?(user) }
let(:commentaire_groupe_gestionnaire) { build :commentaire_groupe_gestionnaire, sender: sender, gestionnaire: gestionnaire }
context 'with a commentaire_groupe_gestionnaire created by an administrateur deleted by administrateur' do
let(:sender) { create(:administrateur) }
let(:user) { sender }
let(:gestionnaire) { nil }
it { is_expected.to be_falsy }
end
context 'with a commentaire_groupe_gestionnaire created by an administrateur deleted by gestionnaire' do
let(:sender) { create(:administrateur) }
let(:user) { create(:gestionnaire) }
let(:gestionnaire) { nil }
it { is_expected.to be_falsy }
end
context 'with a commentaire_groupe_gestionnaire created by an gestionnaire deleted by gestionnaire' do
let(:sender) { create(:gestionnaire) }
let(:user) { sender }
let(:gestionnaire) { sender }
it { is_expected.to be_truthy }
end
context 'with a commentaire_groupe_gestionnaire created by an gestionnaire deleted by administrateur' do
let(:sender) { create(:gestionnaire) }
let(:user) { create(:administrateur) }
let(:gestionnaire) { sender }
it { is_expected.to be_falsy }
end
end
describe "#sent_by?" do
subject { commentaire_groupe_gestionnaire.sent_by?(user) }
let(:commentaire_groupe_gestionnaire) { build :commentaire_groupe_gestionnaire, sender: sender }
context 'with a commentaire_groupe_gestionnaire created by an administrateur so sent by administrateur' do
let(:sender) { create(:administrateur) }
let(:user) { sender }
it { is_expected.to be_truthy }
end
context 'with a commentaire_groupe_gestionnaire created by an administrateur so not sent by gestionnaire' do
let(:sender) { create(:administrateur) }
let(:user) { create(:gestionnaire) }
it { is_expected.to be_falsy }
end
context 'with a commentaire_groupe_gestionnaire created by an gestionnaire so sent by gestionnaire' do
let(:sender) { create(:gestionnaire) }
let(:user) { sender }
it { is_expected.to be_truthy }
end
context 'with a commentaire_groupe_gestionnaire created by an gestionnaire so not sent by administrateur' do
let(:sender) { create(:gestionnaire) }
let(:user) { create(:administrateur) }
it { is_expected.to be_falsy }
end
end
end

View file

@ -1,5 +1,6 @@
describe Gestionnaire, type: :model do describe Gestionnaire, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to have_many(:commentaire_groupe_gestionnaires) }
it { is_expected.to have_and_belong_to_many(:groupe_gestionnaires) } it { is_expected.to have_and_belong_to_many(:groupe_gestionnaires) }
end end

View file

@ -1,6 +1,7 @@
describe GroupeGestionnaire, type: :model do describe GroupeGestionnaire, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to have_many(:administrateurs) } it { is_expected.to have_many(:administrateurs) }
it { is_expected.to have_many(:commentaire_groupe_gestionnaires) }
it { is_expected.to have_and_belong_to_many(:gestionnaires) } it { is_expected.to have_and_belong_to_many(:gestionnaires) }
end end