Merge pull request #7815 from betagouv/7692-multizone

Un administrateur peut associer plusieurs zones à une démarche
This commit is contained in:
krichtof 2022-10-07 10:55:39 +02:00 committed by GitHub
commit 23de1bff65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 266 additions and 85 deletions

View file

@ -85,6 +85,7 @@
p,
label {
padding-left: 28px;
font-weight: normal;
}
input[type=checkbox] {

View file

@ -0,0 +1,5 @@
class Procedure::Card::ZonesComponent < ApplicationComponent
def initialize(procedure:)
@procedure = procedure
end
end

View file

@ -0,0 +1,4 @@
---
fr:
title: Zones
subtitle: Ministère(s) ou collectivité qui mettent en oeuvre la démarche

View file

@ -0,0 +1,13 @@
= link_to zones_admin_procedure_path(@procedure), id: 'zones', class: 'card-admin' do
- if @procedure.zones.size >= 1
%div
%span.icon.accept
%p.card-admin-status-accept Validé
- else
%div
%span.icon.clock
%p.card-admin-status-todo À faire
%div
%p.card-admin-title= t('.title')
%p.card-admin-subtitle= t('.subtitle')
%p.button= t('views.shared.actions.edit')

View file

@ -1,6 +1,6 @@
module Administrateurs
class ProceduresController < AdministrateurController
before_action :retrieve_procedure, only: [:champs, :annotations, :modifications, :edit, :monavis, :update_monavis, :jeton, :update_jeton, :publication, :publish, :transfert, :close, :allow_expert_review, :experts_require_administrateur_invitation, :reset_draft]
before_action :retrieve_procedure, only: [:champs, :annotations, :modifications, :edit, :zones, :monavis, :update_monavis, :jeton, :update_jeton, :publication, :publish, :transfert, :close, :allow_expert_review, :experts_require_administrateur_invitation, :reset_draft]
before_action :procedure_revisable?, only: [:champs, :annotations, :modifications, :reset_draft]
before_action :draft_valid?, only: [:apercu]
@ -108,6 +108,9 @@ module Administrateurs
def edit
end
def zones
end
def create
@procedure = Procedure.new(procedure_params.merge(administrateurs: [current_administrateur]))
@procedure.draft_revision = @procedure.revisions.build
@ -128,7 +131,11 @@ module Administrateurs
if !@procedure.update(procedure_params)
flash.now.alert = @procedure.errors.full_messages
render 'edit'
if @procedure.errors[:zones].present?
render 'zones'
else
render 'edit'
end
elsif @procedure.brouillon?
reset_procedure
flash.notice = 'Démarche modifiée. Tous les dossiers de cette démarche ont été supprimés.'
@ -351,7 +358,7 @@ module Administrateurs
:monavis_embed,
:api_entreprise_token,
:duree_conservation_dossiers_dans_ds,
:zone_id,
{ zone_ids: [] },
:lien_dpo,
:opendata,
:procedure_expires_when_termine_enabled

View file

@ -1,13 +0,0 @@
module ZoneHelper
def grouped_options_for_zone(date)
date ||= Time.zone.now
collectivite = Zone.find_by(acronym: "COLLECTIVITE")
{
"--" => [
[I18n.t('i_dont_know', scope: 'utils'), nil],
[collectivite.label, collectivite.id]
],
I18n.t('ministeres', scope: 'zones') => (Zone.available_at(date) - [collectivite]).map { |m| [m.label_at(date), m.id] }
}
end
end

View file

@ -103,6 +103,7 @@ class Procedure < ApplicationRecord
belongs_to :replaced_by_procedure, -> { with_discarded }, inverse_of: :replaced_procedures, class_name: "Procedure", optional: true
belongs_to :service, optional: true
belongs_to :zone, optional: true
has_and_belongs_to_many :zones
def active_dossier_submitted_message
published_dossier_submitted_message || draft_dossier_submitted_message
@ -288,6 +289,7 @@ class Procedure < ApplicationRecord
validates :lien_dpo, email_or_link: true, allow_nil: true
validates_with MonAvisEmbedValidator
validates :zones, presence: true, if: -> record { record.publiee? && Flipper.enabled?(:zonage) }
FILE_MAX_SIZE = 20.megabytes
validates :notice, content_type: [
@ -627,6 +629,10 @@ class Procedure < ApplicationRecord
result << :instructeurs
end
if missing_zones?
result << :zones
end
result
end
@ -659,6 +665,14 @@ class Procedure < ApplicationRecord
!AssignTo.exists?(groupe_instructeur: groupe_instructeurs)
end
def missing_zones?
if Flipper.enabled?(:zonage)
zones.empty?
else
false
end
end
def revised?
feature_enabled?(:procedure_revisions) && revisions.size > 2
end

View file

@ -11,7 +11,7 @@
class Zone < ApplicationRecord
validates :acronym, presence: true, uniqueness: true
has_many :labels, -> { order(designated_on: :desc) }, class_name: 'ZoneLabel', inverse_of: :zone
has_many :procedures, -> { order(published_at: :desc) }, inverse_of: :zone
has_and_belongs_to_many :procedures, -> { order(published_at: :desc) }, inverse_of: :zone
def current_label
labels.first.name
@ -28,5 +28,8 @@ class Zone < ApplicationRecord
def self.available_at(date)
Zone.all.filter { |zone| zone.available_at?(date) }.sort_by { |zone| zone.label_at(date) }
.map do |zone|
OpenStruct.new(id: zone.id, label: zone.label_at(date))
end
end
end

View file

@ -13,12 +13,6 @@
%span.mandatory *
= f.text_area :description, rows: '6', placeholder: 'Description de la démarche, destinataires, etc. ', class: 'form-control'
- if Flipper.enabled? :zonage
= f.label :zone do
= t('zone', scope: 'activerecord.attributes.procedure')
%span.mandatory *
= f.select :zone_id, grouped_options_for_zone(@procedure.published_or_created_at)
%h3.header-subsection Logo de la démarche
= render Attachment::EditComponent.new(form: f, attached_file: @procedure.logo, direct_upload: true, user_can_destroy: true)

View file

@ -74,13 +74,14 @@
- else
.alert.alert-info
Pour pouvoir tester cette démarche, vous devez dabord lui affecter
Pour pouvoir tester cette démarche, vous devez dabord lui affecter :
%ul
- if @procedure.missing_zones?
%li= link_to("une ou plusieurs zones", zones_admin_procedure_path(id: @procedure.id))
- if @procedure.missing_instructeurs?
= link_to("des instructeurs", admin_procedure_groupe_instructeur_path(@procedure, @procedure.defaut_groupe_instructeur))
- if @procedure.missing_instructeurs? && @procedure.service.nil?
et
%li= link_to("des instructeurs", admin_procedure_groupe_instructeur_path(@procedure, @procedure.defaut_groupe_instructeur))
- if @procedure.service.nil?
= link_to("un service", admin_services_path(procedure_id: @procedure))
%li= link_to("un service", admin_services_path(procedure_id: @procedure))
\.
- else
- if @procedure.missing_steps.include?(:service)

View file

@ -58,6 +58,7 @@
%h2.procedure-admin-explanation Indispensable avant publication
.procedure-grid
= render Procedure::Card::PresentationComponent.new(procedure: @procedure)
= render Procedure::Card::ZonesComponent.new(procedure: @procedure) if Flipper.enabled? :zonage
= render Procedure::Card::ChampsComponent.new(procedure: @procedure)
= render Procedure::Card::ServiceComponent.new(procedure: @procedure, administrateur: current_administrateur)
= render Procedure::Card::AdministrateursComponent.new(procedure: @procedure)

View file

@ -0,0 +1,27 @@
- content_for(:root_class, 'scroll-margins-for-sticky-footer')
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Description']] }
.container
= form_for @procedure,
url: url_for({ controller: 'administrateurs/procedures', action: :update, id: @procedure.id }),
multipart: true,
html: { class: 'form' } do |f|
%h1.page-title Zones
= f.label :zone do
= t('zone', scope: 'activerecord.attributes.procedure')
- if Flipper.enabled? :zonage
= f.collection_check_boxes :zone_ids, Zone.available_at(@procedure.published_or_created_at), :id, :label do |b|
.editable-champ.editable-champ-checkbox
= b.check_box
= b.label
.procedure-form__actions.sticky--bottom
.actions-right
= link_to 'Annuler', admin_procedure_path(id: @procedure), class: 'button', data: { confirm: 'Êtes-vous sûr de vouloir annuler les modifications effectuées ?'}
= f.button 'Enregistrer', class: 'button primary send'

View file

@ -431,6 +431,7 @@ Rails.application.routes.draw do
member do
get 'apercu'
get 'champs'
get 'zones'
get 'annotations'
get 'modifications'
get 'monavis'

View file

@ -0,0 +1,10 @@
class CreateProceduresAndZones < ActiveRecord::Migration[6.1]
def change
create_table :procedures_zones, id: false do |t|
t.belongs_to :procedure
t.belongs_to :zone
t.timestamps
end
end
end

View file

@ -0,0 +1,17 @@
class BackfillProceduresZones < ActiveRecord::Migration[6.1]
def up
# rubocop:disable DS/Unscoped
Procedure.unscoped.each do |procedure|
procedure.zones << procedure.zone if procedure.zone
end
# rubocop:enable DS/Unscoped
end
def down
# rubocop:disable DS/Unscoped
Procedure.unscoped.each do |procedure|
procedure.zones.destroy_all
end
# rubocop:enable DS/Unscoped
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: 2022_10_05_145646) do
ActiveRecord::Schema.define(version: 2022_10_06_193737) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
@ -685,6 +685,15 @@ ActiveRecord::Schema.define(version: 2022_10_05_145646) do
t.index ["zone_id"], name: "index_procedures_on_zone_id"
end
create_table "procedures_zones", id: false, force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.bigint "procedure_id"
t.datetime "updated_at", precision: 6, null: false
t.bigint "zone_id"
t.index ["procedure_id"], name: "index_procedures_zones_on_procedure_id"
t.index ["zone_id"], name: "index_procedures_zones_on_zone_id"
end
create_table "received_mails", id: :serial, force: :cascade do |t|
t.text "body"
t.datetime "created_at", null: false

View file

@ -12,6 +12,8 @@ describe Administrateurs::ProceduresController, type: :controller do
let(:duree_conservation_dossiers_dans_ds) { 3 }
let(:monavis_embed) { nil }
let(:lien_site_web) { 'http://mon-site.gouv.fr' }
let(:zone) { create(:zone) }
let(:zone_ids) { [zone.id] }
describe '#apercu' do
render_views
@ -55,6 +57,7 @@ describe Administrateurs::ProceduresController, type: :controller do
cadre_juridique: cadre_juridique,
duree_conservation_dossiers_dans_ds: duree_conservation_dossiers_dans_ds,
monavis_embed: monavis_embed,
zone_ids: zone_ids,
lien_site_web: lien_site_web
}
}
@ -176,6 +179,14 @@ describe Administrateurs::ProceduresController, type: :controller do
end
end
describe 'GET #zones' do
let(:procedure) { create(:procedure, administrateur: admin) }
let(:procedure_id) { procedure.id }
subject { get :zones, params: { id: procedure_id } }
it { is_expected.to have_http_status(:success) }
end
describe 'POST #create' do
context 'when all attributs are filled' do
describe 'new procedure in database' do
@ -323,6 +334,19 @@ describe Administrateurs::ProceduresController, type: :controller do
it { expect(subject.for_individual).not_to eq procedure_params[:for_individual] }
end
context 'when zones are empty' do
before do
Flipper.enable(:zonage)
end
after do
Flipper.disable(:zonage)
end
let(:zone_ids) { [""] }
it { is_expected.to render_template :zones }
end
end
end
end

View file

@ -13,7 +13,6 @@ FactoryBot.define do
ask_birthday { false }
lien_site_web { "https://mon-site.gouv" }
path { SecureRandom.uuid }
association :zone
groupe_instructeurs { [association(:groupe_instructeur, :default, procedure: instance, strategy: :build)] }
administrateurs { administrateur.present? ? [administrateur] : [association(:administrateur)] }
@ -282,6 +281,7 @@ FactoryBot.define do
published_at { Time.zone.now }
unpublished_at { nil }
closed_at { nil }
zones { [association(:zone, strategy: :build)] }
end
trait :closed do

View file

@ -1484,7 +1484,7 @@ describe Dossier do
end
describe "champs_for_export" do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_datetime, :with_yes_no, :with_explication, :with_commune, :with_repetition) }
let(:procedure) { create(:procedure, :with_type_de_champ, :with_datetime, :with_yes_no, :with_explication, :with_commune, :with_repetition, zones: [create(:zone)]) }
let(:text_type_de_champ) { procedure.types_de_champ.find { |type_de_champ| type_de_champ.type_champ == TypeDeChamp.type_champs.fetch(:text) } }
let(:yes_no_type_de_champ) { procedure.types_de_champ.find { |type_de_champ| type_de_champ.type_champ == TypeDeChamp.type_champs.fetch(:yes_no) } }
let(:datetime_type_de_champ) { procedure.types_de_champ.find { |type_de_champ| type_de_champ.type_champ == TypeDeChamp.type_champs.fetch(:datetime) } }

View file

@ -704,7 +704,7 @@ describe Procedure do
end
describe '#publish!' do
let(:procedure) { create(:procedure, path: 'example-path') }
let(:procedure) { create(:procedure, path: 'example-path', zones: [create(:zone)]) }
let(:now) { Time.zone.now.beginning_of_minute }
context 'when publishing a new procedure' do
@ -757,7 +757,7 @@ describe Procedure do
let(:canonical_procedure) { create(:procedure, :published) }
let(:administrateur) { canonical_procedure.administrateurs.first }
let(:procedure) { create(:procedure, administrateurs: [administrateur]) }
let(:procedure) { create(:procedure, administrateurs: [administrateur], zones: [create(:zone)]) }
let(:now) { Time.zone.now.beginning_of_minute }
context 'when publishing over a previous canonical procedure' do
@ -1063,7 +1063,7 @@ describe Procedure do
end
describe 'suggested_path' do
let(:procedure) { create(:procedure, aasm_state: :publiee, libelle: 'Inscription au Collège') }
let(:procedure) { create(:procedure, aasm_state: :publiee, libelle: 'Inscription au Collège', zones: [create(:zone)]) }
subject { procedure.suggested_path(procedure.administrateurs.first) }
@ -1079,7 +1079,7 @@ describe Procedure do
context 'when the suggestion conflicts with one procedure' do
before do
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college')
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college', zones: [create(:zone)])
end
it { is_expected.to eq 'inscription-au-college-2' }
@ -1087,8 +1087,8 @@ describe Procedure do
context 'when the suggestion conflicts with several procedures' do
before do
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college')
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college-2')
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college', zones: [create(:zone)])
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college-2', zones: [create(:zone)])
end
it { is_expected.to eq 'inscription-au-college-3' }
@ -1096,7 +1096,7 @@ describe Procedure do
context 'when the suggestion conflicts with another procedure of the same admin' do
before do
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college', administrateurs: procedure.administrateurs)
create(:procedure, aasm_state: :publiee, path: 'inscription-au-college', administrateurs: procedure.administrateurs, zones: [create(:zone)])
end
it { is_expected.to eq 'inscription-au-college' }
@ -1231,6 +1231,66 @@ describe Procedure do
end
end
describe '.missing_zones?' do
before do
Flipper.enable :zonage
end
after do
Flipper.disable :zonage
end
let(:procedure) { create(:procedure, zones: []) }
subject { procedure.missing_zones? }
it { is_expected.to be true }
context 'when a procedure has zones' do
let(:zone) { create(:zone) }
before { procedure.zones << zone }
it { is_expected.to be false }
end
end
describe '.missing_steps' do
before do
Flipper.enable :zonage
end
after do
Flipper.disable :zonage
end
subject { procedure.missing_steps.include?(step) }
context 'without zone' do
let(:procedure) { create(:procedure, zones: []) }
let(:step) { :zones }
it { is_expected.to be_truthy }
end
context 'with zone' do
let(:procedure) { create(:procedure, zones: [create(:zone)]) }
let(:step) { :zones }
it { is_expected.to be_falsey }
end
context 'without service' do
let(:procedure) { create(:procedure, service: nil) }
let(:step) { :service }
it { is_expected.to be_truthy }
end
context 'with service' do
let(:procedure) { create(:procedure) }
let(:step) { :service }
it { is_expected.to be_truthy }
end
end
describe "#destroy" do
let(:procedure) { create(:procedure, :closed, :with_type_de_champ, :with_bulk_message) }

View file

@ -114,8 +114,8 @@ describe Zone do
end
it 'returns only available zones at specific date' do
expect(Zone.available_at(start_last_government + 1.day)).to eq [culture]
expect(Zone.available_at(start_previous_government + 1.day)).to eq [culture, om]
expect(Zone.available_at(start_last_government + 1.day).map(&:label)).to eq ["Ministère de la Culture"]
expect(Zone.available_at(start_previous_government + 1.day).map(&:label)).to eq ["Ministère de la Culture", "Ministère des Outre-mer"]
end
end
end

View file

@ -1,16 +1,6 @@
describe 'administrateurs/procedures/edit.html.haml' do
let(:logo) { fixture_file_upload('spec/fixtures/files/logo_test_procedure.png', 'image/png') }
let(:procedure) { create(:procedure, logo: logo, lien_site_web: 'http://some.website') }
let(:populate_zones_task) { Rake::Task['after_party:populate_zones'] }
before do
Flipper.enable(:zonage)
populate_zones_task.invoke
end
after do
populate_zones_task.reenable
end
context 'when procedure logo is present' do
it 'display on the page' do
@ -20,39 +10,4 @@ describe 'administrateurs/procedures/edit.html.haml' do
expect(rendered).to have_selector('.procedure-logos')
end
end
context 'when procedure has never been published' do
before { Timecop.freeze(now) }
after { Timecop.return }
let(:procedure) { create(:procedure, zone: Zone.find_by(acronym: 'MTEI')) }
let(:now) { Time.zone.parse('18/05/2022') }
it 'displays zones with label available at the creation date' do
assign(:procedure, procedure)
render
expect(rendered).to have_content("Ministère du Travail")
expect(rendered).not_to have_content("Ministère du Travail, du Plein emploi et de l'Insertion")
end
end
context 'when procedure has been published' do
before { Timecop.freeze(now) }
after { Timecop.return }
let(:procedure) { create(:procedure, zone: Zone.find_by(acronym: 'MTEI')) }
let(:now) { Time.zone.parse('18/05/2022') }
it 'displays zones with label available at the creation date' do
Timecop.freeze(Time.zone.parse('22/05/2022')) do
procedure.publish!
end
assign(:procedure, procedure)
render
expect(rendered).to have_content("Ministère du Travail, du Plein emploi et de l'Insertion")
end
end
end

View file

@ -0,0 +1,48 @@
describe 'administrateurs/procedures/zones.html.haml' do
let(:procedure) { create(:procedure) }
let(:populate_zones_task) { Rake::Task['after_party:populate_zones'] }
before do
Flipper.enable(:zonage)
populate_zones_task.invoke
end
after do
populate_zones_task.reenable
end
context 'when procedure has never been published' do
before { Timecop.freeze(now) }
after { Timecop.return }
let(:procedure) { create(:procedure, zones: [Zone.find_by(acronym: 'MTEI')]) }
let(:now) { Time.zone.parse('18/05/2022') }
it 'displays zones with label available at the creation date' do
assign(:procedure, procedure)
render
expect(rendered).to have_content("Ministère du Travail")
expect(rendered).not_to have_content("Ministère du Travail, du Plein emploi et de l'Insertion")
end
end
context 'when procedure has been published' do
before { Timecop.freeze(now) }
after { Timecop.return }
let(:procedure) { create(:procedure, zones: [Zone.find_by(acronym: 'MTEI')]) }
let(:now) { Time.zone.parse('18/05/2022') }
it 'displays zones with label available at the creation date' do
Timecop.freeze(Time.zone.parse('22/05/2022')) do
procedure.publish!
end
assign(:procedure, procedure)
render
expect(rendered).to have_content("Ministère du Travail, du Plein emploi et de l'Insertion")
end
end
end