Merge pull request #9566 from adullact/feature-ouidou/admin_creation_delegation_gestionnaire_page_children_management

Feature ouidou/admin creation delegation gestionnaire page children management
This commit is contained in:
krichtof 2023-11-08 15:13:10 +00:00 committed by GitHub
commit a6ea607e7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 212 additions and 31 deletions

View file

@ -12,6 +12,7 @@ gem 'addressable'
gem 'administrate' gem 'administrate'
gem 'administrate-field-enum' # Allow using Field::Enum in administrate gem 'administrate-field-enum' # Allow using Field::Enum in administrate
gem 'after_party' gem 'after_party'
gem 'ancestry'
gem 'anchored' gem 'anchored'
gem 'bcrypt' gem 'bcrypt'
gem 'bootsnap', '>= 1.4.4', require: false # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.4.4', require: false # Reduces boot times through caching; required in config/boot.rb

View file

@ -100,6 +100,8 @@ GEM
administrate (~> 0.12) administrate (~> 0.12)
aes_key_wrap (1.1.0) aes_key_wrap (1.1.0)
after_party (1.11.2) after_party (1.11.2)
ancestry (4.3.3)
activerecord (>= 5.2.6)
anchored (1.1.0) anchored (1.1.0)
ast (2.4.2) ast (2.4.2)
attr_required (1.0.1) attr_required (1.0.1)
@ -815,6 +817,7 @@ DEPENDENCIES
administrate administrate
administrate-field-enum administrate-field-enum
after_party after_party
ancestry
anchored anchored
axe-core-rspec axe-core-rspec
bcrypt bcrypt

View file

@ -0,0 +1,5 @@
class GroupeGestionnaire::Card::ChildrenComponent < ApplicationComponent
def initialize(groupe_gestionnaire:)
@groupe_gestionnaire = groupe_gestionnaire
end
end

View file

@ -0,0 +1,5 @@
---
fr:
title:
one: Groupe enfants
other: Groupes enfants

View file

@ -0,0 +1,12 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3
= link_to gestionnaire_groupe_gestionnaire_children_path(@groupe_gestionnaire), id: 'gestionnaires', class: 'fr-tile fr-enlarge-link' do
.fr-tile__body.flex.column.align-center.justify-between
%div
%span.icon.accept
%p.fr-tile-status-accept Validé
%div
.line-count.fr-my-1w
%p.fr-tag= @groupe_gestionnaire.children.size
%h3.fr-h6
= t('.title', count: @groupe_gestionnaire.children.size)
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')

View file

@ -9,5 +9,4 @@
%p.fr-tag= @groupe_gestionnaire.gestionnaires.size %p.fr-tag= @groupe_gestionnaire.gestionnaires.size
%h3.fr-h6 %h3.fr-h6
= t('.title', count: @groupe_gestionnaire.gestionnaires.size) = t('.title', count: @groupe_gestionnaire.gestionnaires.size)
%p.fr-tile-subtitle Gestion de la démarche
%p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit') %p.fr-btn.fr-btn--tertiary= t('views.shared.actions.edit')

View file

@ -0,0 +1,16 @@
class GroupeGestionnaire::GroupeGestionnaireChildren::ChildComponent < ApplicationComponent
include ApplicationHelper
def initialize(groupe_gestionnaire:, child:)
@groupe_gestionnaire = groupe_gestionnaire
@child = child
end
def name
@child.name
end
def created_at
try_format_datetime(@child.created_at)
end
end

View file

@ -0,0 +1,4 @@
%tr{ id: dom_id(@child) }
%td
= link_to name, gestionnaire_groupe_gestionnaire_path(@child)
%td= created_at

View file

@ -8,8 +8,10 @@ module Gestionnaires
def retrieve_groupe_gestionnaire def retrieve_groupe_gestionnaire
id = params[:groupe_gestionnaire_id] || params[:id] id = params[:groupe_gestionnaire_id] || params[:id]
@groupe_gestionnaire = GroupeGestionnaire.find(id)
@groupe_gestionnaire = current_gestionnaire.groupe_gestionnaires.find(id) if ((@groupe_gestionnaire.ancestor_ids + [@groupe_gestionnaire.id]) & current_gestionnaire.groupe_gestionnaire_ids).empty?
raise(ActiveRecord::RecordNotFound)
end
Sentry.configure_scope do |scope| Sentry.configure_scope do |scope|
scope.set_tags(groupe_gestionnaire: @groupe_gestionnaire.id) scope.set_tags(groupe_gestionnaire: @groupe_gestionnaire.id)

View file

@ -0,0 +1,16 @@
module Gestionnaires
class GroupeGestionnaireChildrenController < GestionnaireController
before_action :retrieve_groupe_gestionnaire, except: [:new]
def index
end
def create
if (@child = @groupe_gestionnaire.children.create!(name: params.require(:groupe_gestionnaire)[:name]))
flash[:notice] = "Le groupe enfants a bien été créé"
else
flash[:alert] = @child.errors.full_messages
end
end
end
end

View file

@ -1,4 +1,17 @@
module Manager module Manager
class GestionnairesController < Manager::ApplicationController class GestionnairesController < Manager::ApplicationController
def delete
gestionnaire = Gestionnaire.find(params[:id])
if !gestionnaire.can_be_deleted?
flash[:alert] = "Impossible de supprimer ce gestionnaire car il est gestionnaire d'un groupe racine"
else
gestionnaire.destroy!
logger.info("Le gestionnaire #{gestionnaire.id} est supprimé par #{current_super_admin.id}")
flash[:notice] = "Le gestionnaire #{gestionnaire.id} est supprimé"
end
redirect_to manager_gestionnaires_path
end
end end
end end

View file

@ -4,7 +4,7 @@ class GroupeGestionnaireMailer < ApplicationMailer
def notify_removed_gestionnaire(groupe_gestionnaire, removed_gestionnaire, current_super_admin_email) def notify_removed_gestionnaire(groupe_gestionnaire, removed_gestionnaire, current_super_admin_email)
@groupe_gestionnaire = groupe_gestionnaire @groupe_gestionnaire = groupe_gestionnaire
@current_super_admin_email = current_super_admin_email @current_super_admin_email = current_super_admin_email
subject = "Vous avez été retiré(e) du groupe d'administrateur \"#{groupe_gestionnaire.name}\"" subject = "Vous avez été retiré(e) du groupe gestionnaire \"#{groupe_gestionnaire.name}\""
mail(to: removed_gestionnaire.email, subject: subject) mail(to: removed_gestionnaire.email, subject: subject)
end end

View file

@ -34,7 +34,10 @@ class Gestionnaire < ApplicationRecord
end end
def can_be_deleted? def can_be_deleted?
!(root_groupe_gestionnaire = groupe_gestionnaires.where(groupe_gestionnaire: nil).first) || root_groupe_gestionnaire.gestionnaires.size > 1 groupe_gestionnaires.roots.each do |rt|
return false unless rt.gestionnaires.size > 1
end
true
end end
def registration_state def registration_state

View file

@ -1,12 +1,8 @@
class GroupeGestionnaire < ApplicationRecord class GroupeGestionnaire < ApplicationRecord
belongs_to :groupe_gestionnaire, optional: true # parent
has_many :children, class_name: "GroupeGestionnaire", inverse_of: :groupe_gestionnaire
has_many :administrateurs has_many :administrateurs
has_and_belongs_to_many :gestionnaires has_and_belongs_to_many :gestionnaires
def root_groupe_gestionnaire? has_ancestry
groupe_gestionnaire.nil?
end
def add(gestionnaire) def add(gestionnaire)
return if gestionnaire.nil? return if gestionnaire.nil?
@ -16,7 +12,7 @@ class GroupeGestionnaire < ApplicationRecord
end end
def remove(gestionnaire_id, current_user) def remove(gestionnaire_id, current_user)
if !self.root_groupe_gestionnaire? || self.gestionnaires.one? if !self.is_root? || self.gestionnaires.one?
alert = "Suppression impossible : il doit y avoir au moins un gestionnaire dans le groupe racine" alert = "Suppression impossible : il doit y avoir au moins un gestionnaire dans le groupe racine"
else else
gestionnaire = Gestionnaire.find(gestionnaire_id) gestionnaire = Gestionnaire.find(gestionnaire_id)
@ -69,7 +65,7 @@ class GroupeGestionnaire < ApplicationRecord
end end
if gestionnaires_to_add.present? if gestionnaires_to_add.present?
notice = "Les gestionnaires ont bien été affectés au groupe d'administrateurs" notice = "Les gestionnaires ont bien été affectés au groupe gestionnaire"
GroupeGestionnaireMailer GroupeGestionnaireMailer
.notify_added_gestionnaires(self, gestionnaires_to_add, current_user.email) .notify_added_gestionnaires(self, gestionnaires_to_add, current_user.email)

View file

@ -0,0 +1,13 @@
= form_for groupe_gestionnaire.children.new,
url: { controller: 'groupe_gestionnaire_children' },
html: { id: "new_child" },
data: { turbo: true, turbo_force: :server } do |f|
.fr-input-group
= f.label :email, class: "fr-label" do
Ajouter un groupe enfants
%span.fr-hint-text
= "Renseignez le nom du nouveau groupe enfants de « #{groupe_gestionnaire.name} »."
= f.text_field :name, required: true, class: "fr-input", autofocus: true
= f.submit 'Ajouter un nouveau groupe', class: 'fr-btn'

View file

@ -0,0 +1,5 @@
- if @child.present?
= turbo_stream.update 'children' do
= render GroupeGestionnaire::GroupeGestionnaireChildren::ChildComponent.with_collection(@groupe_gestionnaire.children, groupe_gestionnaire: @groupe_gestionnaire)
= turbo_stream.replace "new_child", partial: 'add_admin_form', locals: { groupe_gestionnaire: @groupe_gestionnaire }
= turbo_stream.focus 'groupe_gestionnaire_name'

View file

@ -0,0 +1,18 @@
= render partial: 'gestionnaires/breadcrumbs',
locals: { steps: [['Groupes gestionnaire', gestionnaire_groupe_gestionnaires_path],
["#{@groupe_gestionnaire.name.truncate_words(10)}", gestionnaire_groupe_gestionnaire_path(@groupe_gestionnaire)],
['Groupes enfants']], preview: false }
.container
%h1 Gérer les groupes enfants de « #{@groupe_gestionnaire.name} »
%table.table
%thead
%tr
%th= 'Nom'
%th= 'Enregistré le'
%tbody#children
= render(GroupeGestionnaire::GroupeGestionnaireChildren::ChildComponent.with_collection(@groupe_gestionnaire.children, groupe_gestionnaire: @groupe_gestionnaire))
.fr-mt-4w
= render 'add_admin_form', groupe_gestionnaire: @groupe_gestionnaire

View file

@ -8,9 +8,11 @@
%table.table %table.table
%thead %thead
%th= 'Adresse email' %tr
%th= 'Enregistré le' %th= 'Adresse email'
%th= 'État' %th= 'Enregistré le'
%th= 'État'
%th
%tbody#gestionnaires %tbody#gestionnaires
= render(GroupeGestionnaire::GroupeGestionnaireGestionnaires::GestionnaireComponent.with_collection(@groupe_gestionnaire.gestionnaires.order('users.email'), groupe_gestionnaire: @groupe_gestionnaire)) = render(GroupeGestionnaire::GroupeGestionnaireGestionnaires::GestionnaireComponent.with_collection(@groupe_gestionnaire.gestionnaires.order('users.email'), groupe_gestionnaire: @groupe_gestionnaire))

View file

@ -18,3 +18,4 @@
%a{ href: gestionnaire_groupe_gestionnaire_path(@groupe_gestionnaire.groupe_gestionnaire) }= @groupe_gestionnaire.groupe_gestionnaire.name %a{ href: gestionnaire_groupe_gestionnaire_path(@groupe_gestionnaire.groupe_gestionnaire) }= @groupe_gestionnaire.groupe_gestionnaire.name
.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) = render GroupeGestionnaire::Card::GestionnairesComponent.new(groupe_gestionnaire: @groupe_gestionnaire)
= render GroupeGestionnaire::Card::ChildrenComponent.new(groupe_gestionnaire: @groupe_gestionnaire)

View file

@ -30,13 +30,9 @@ as well as a link to its edit page.
class: "button", class: "button",
) if accessible_action?(page.resource, :edit) %> ) if accessible_action?(page.resource, :edit) %>
<%= link_to( </div>
t("administrate.actions.destroy"), <div>
[namespace, page.resource], <%= button_to "Supprimer", delete_manager_gestionnaire_path(page.resource), method: :delete, disabled: !page.resource.can_be_deleted?, class: "button", data: { confirm: "Confirmez-vous la suppression du gestionnaire ?" }, title: page.resource.can_be_deleted? ? "Supprimer" : "Ce gestionnaire ne peut etre supprimé car il est le seul gestionnaire d'un groupe racine" %>
class: "button button--danger",
method: :delete,
data: { confirm: t("administrate.actions.confirm") }
) if accessible_action?(page.resource, :destroy) %>
</div> </div>
</header> </header>

View file

@ -233,7 +233,7 @@ REDIS_CACHE_SSL_VERIFY_NONE="enabled"
# can be debug, info, warn, error, fatal, and unknown # can be debug, info, warn, error, fatal, and unknown
DS_LOG_LEVEL='info' DS_LOG_LEVEL='info'
# Admins group usage (gestionnaire de groupes d'administrateurs) # GroupeGestionnaire
# can be removed if needed when EVERY PARTS of the feature will be merged / only used in routes.rb # can be removed if needed when EVERY PARTS of the feature will be merged / only used in routes.rb
ADMINS_GROUP_ENABLED="disabled" ADMINS_GROUP_ENABLED="disabled"

View file

@ -0,0 +1,3 @@
# use the newer format
Ancestry.default_ancestry_format = :materialized_path2

View file

@ -57,7 +57,9 @@ Rails.application.routes.draw do
end end
if ENV['ADMINS_GROUP_ENABLED'] == 'enabled' || Rails.env.test? # can be removed if needed when EVERY PARTS of the feature will be merged / from env.example.optional if ENV['ADMINS_GROUP_ENABLED'] == 'enabled' || Rails.env.test? # can be removed if needed when EVERY PARTS of the feature will be merged / from env.example.optional
resources :gestionnaires, only: [:index, :show, :edit, :update, :destroy] resources :gestionnaires, only: [:index, :show, :edit, :update] do
delete 'delete', on: :member
end
resources :groupe_gestionnaires, path: 'groupe_administrateurs', only: [:index, :show, :new, :create, :edit, :update] do resources :groupe_gestionnaires, path: 'groupe_administrateurs', only: [:index, :show, :new, :create, :edit, :update] do
post 'add_gestionnaire', on: :member post 'add_gestionnaire', on: :member
@ -493,6 +495,7 @@ Rails.application.routes.draw do
scope module: 'gestionnaires', as: 'gestionnaire' do scope module: 'gestionnaires', as: 'gestionnaire' do
resources :groupe_gestionnaires, path: 'groupes', only: [:index, :show, :create, :edit, :update, :destroy] do resources :groupe_gestionnaires, path: 'groupes', only: [:index, :show, :create, :edit, :update, :destroy] do
resources :gestionnaires, controller: 'groupe_gestionnaire_gestionnaires', only: [:index, :create, :destroy] resources :gestionnaires, controller: 'groupe_gestionnaire_gestionnaires', only: [:index, :create, :destroy]
resources :children, controller: 'groupe_gestionnaire_children', only: [:index, :create, :destroy]
end end
end end
end end

View file

@ -0,0 +1,8 @@
class AddAncestryToGroupeGestionnaires < ActiveRecord::Migration[6.1]
disable_ddl_transaction!
def change
add_column :groupe_gestionnaires, :ancestry, :string, collation: 'C', null: false, default: '/'
add_index :groupe_gestionnaires, :ancestry, algorithm: :concurrently
end
end

View file

@ -645,10 +645,12 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_26_161609) do
end end
create_table "groupe_gestionnaires", force: :cascade do |t| create_table "groupe_gestionnaires", force: :cascade do |t|
t.string "ancestry", default: "/", null: false, collation: "C"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.bigint "groupe_gestionnaire_id" t.bigint "groupe_gestionnaire_id"
t.string "name", null: false t.string "name", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["ancestry"], name: "index_groupe_gestionnaires_on_ancestry"
t.index ["groupe_gestionnaire_id"], name: "index_groupe_gestionnaires_on_groupe_gestionnaire_id" t.index ["groupe_gestionnaire_id"], name: "index_groupe_gestionnaires_on_groupe_gestionnaire_id"
t.index ["name"], name: "index_groupe_gestionnaires_on_name" t.index ["name"], name: "index_groupe_gestionnaires_on_name"
end end

View file

@ -0,0 +1,45 @@
describe Gestionnaires::GroupeGestionnaireChildrenController, type: :controller do
let(:gestionnaire) { create(:gestionnaire).tap { _1.user.update(last_sign_in_at: Time.zone.now) } }
let(:groupe_gestionnaire) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) }
describe "#index" do
render_views
subject { get :index, params: { groupe_gestionnaire_id: groupe_gestionnaire.id } }
context "when not logged" do
before { subject }
it { expect(response).to redirect_to(new_user_session_path) }
end
context "when logged in" do
let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, ancestry: "/#{groupe_gestionnaire.id}/", gestionnaires: [gestionnaire]) }
before do
sign_in(gestionnaire.user)
subject
end
it { expect(response).to have_http_status(:ok) }
it { expect(assigns(:groupe_gestionnaire).children).to include(child_groupe_gestionnaire) }
it { expect(response.body).to include(child_groupe_gestionnaire.name) }
end
end
describe '#create' do
before do
sign_in gestionnaire.user
post :create,
params: {
groupe_gestionnaire_id: groupe_gestionnaire.id,
groupe_gestionnaire: { name: new_child_group_name }
},
format: :turbo_stream
end
context 'of a child group' do
let(:new_child_group_name) { 'child group' }
it { expect(groupe_gestionnaire.reload.children.map(&:name)).to include(new_child_group_name) }
it { expect(flash.notice).to eq("Le groupe enfants a bien été créé") }
end
end
end

View file

@ -18,7 +18,7 @@ describe Gestionnaires::GroupeGestionnaireGestionnairesController, type: :contro
let(:new_gestionnaire_email) { 'new_gestionnaire@mail.com' } let(:new_gestionnaire_email) { 'new_gestionnaire@mail.com' }
it { expect(groupe_gestionnaire.reload.gestionnaires.map(&:email)).to include(new_gestionnaire_email) } it { expect(groupe_gestionnaire.reload.gestionnaires.map(&:email)).to include(new_gestionnaire_email) }
it { expect(flash.notice).to eq("Les gestionnaires ont bien été affectés au groupe d'administrateurs") } it { expect(flash.notice).to eq("Les gestionnaires ont bien été affectés au groupe gestionnaire") }
end end
end end

View file

@ -28,7 +28,7 @@ describe Gestionnaires::GroupeGestionnairesController, type: :controller do
describe "#show" do describe "#show" do
subject { get :show, params: { id: child_groupe_gestionnaire.id } } subject { get :show, params: { id: child_groupe_gestionnaire.id } }
let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) } let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) }
let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, groupe_gestionnaire: groupe_gestionnaire_root, gestionnaires: [gestionnaire]) } let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, ancestry: "/#{groupe_gestionnaire_root.id}/", gestionnaires: [gestionnaire]) }
context "when not logged" do context "when not logged" do
before { subject } before { subject }
@ -49,7 +49,7 @@ describe Gestionnaires::GroupeGestionnairesController, type: :controller do
describe "#edit" do describe "#edit" do
subject { get :edit, params: { id: child_groupe_gestionnaire.id } } subject { get :edit, params: { id: child_groupe_gestionnaire.id } }
let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) } let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) }
let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, groupe_gestionnaire: groupe_gestionnaire_root, gestionnaires: [gestionnaire]) } let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, ancestry: "/#{groupe_gestionnaire_root.id}/", gestionnaires: [gestionnaire]) }
context "when not logged" do context "when not logged" do
before { subject } before { subject }
@ -70,7 +70,7 @@ describe Gestionnaires::GroupeGestionnairesController, type: :controller do
describe "#update" do describe "#update" do
subject { post :update, params: { id: child_groupe_gestionnaire.id, groupe_gestionnaire: { name: 'new child name' } } } subject { post :update, params: { id: child_groupe_gestionnaire.id, groupe_gestionnaire: { name: 'new child name' } } }
let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) } let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) }
let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, groupe_gestionnaire: groupe_gestionnaire_root, gestionnaires: [gestionnaire]) } let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, ancestry: "/#{groupe_gestionnaire_root.id}/", gestionnaires: [gestionnaire]) }
context "when not logged" do context "when not logged" do
before { subject } before { subject }
@ -91,7 +91,7 @@ describe Gestionnaires::GroupeGestionnairesController, type: :controller do
describe "#destroy" do describe "#destroy" do
subject { post :destroy, params: { id: child_groupe_gestionnaire.id } } subject { post :destroy, params: { id: child_groupe_gestionnaire.id } }
let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) } let!(:groupe_gestionnaire_root) { create(:groupe_gestionnaire, gestionnaires: [gestionnaire]) }
let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, groupe_gestionnaire: groupe_gestionnaire_root, gestionnaires: [gestionnaire]) } let!(:child_groupe_gestionnaire) { create(:groupe_gestionnaire, ancestry: "/#{groupe_gestionnaire_root.id}/", gestionnaires: [gestionnaire]) }
context "when not logged" do context "when not logged" do
before { subject } before { subject }

View file

@ -22,4 +22,16 @@ describe Manager::GestionnairesController, type: :controller do
it { expect(response.body).to include(gestionnaire.email) } it { expect(response.body).to include(gestionnaire.email) }
end end
describe '#delete' do
before { sign_in super_admin }
subject { delete :delete, params: { id: gestionnaire.id } }
it 'deletes the gestionnaire' do
subject
expect(Gestionnaire.find_by(id: gestionnaire.id)).to be_nil
end
end
end end

View file

@ -1,7 +1,5 @@
describe GroupeGestionnaire, type: :model do describe GroupeGestionnaire, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:groupe_gestionnaire).optional }
it { is_expected.to have_many(:children) }
it { is_expected.to have_many(:administrateurs) } it { is_expected.to have_many(:administrateurs) }
it { is_expected.to have_and_belong_to_many(:gestionnaires) } it { is_expected.to have_and_belong_to_many(:gestionnaires) }
end end