Merge pull request #9014 from demarches-simplifiees/8859-default_zones

Etq admin, lors de la création ou modification d'une démarche, des zones par défaut me sont suggérées
This commit is contained in:
krichtof 2023-05-26 17:25:21 +00:00 committed by GitHub
commit 0051face93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 274 additions and 143 deletions

View file

@ -81,6 +81,12 @@
} }
// Move checkbox to the top-left side of the label // Move checkbox to the top-left side of the label
&.editable-champ-checkbox {
label.admin-default-zone {
font-weight: bold;
}
}
&.editable-champ-checkbox { &.editable-champ-checkbox {
p, p,
label { label {

View file

@ -0,0 +1,6 @@
class AdminUpdateDefaultZonesJob < ApplicationJob
def perform(admin)
tchap_hs = APITchap::HsAdapter.new(admin.email).to_hs
admin.default_zones << Zone.default_for(tchap_hs)
end
end

22
app/lib/api_tchap/api.rb Normal file
View file

@ -0,0 +1,22 @@
class APITchap::API
class ResourceNotFound < StandardError
end
def self.get_hs(email)
call([API_TCHAP_URL, "info?medium=email&address=#{email}"].join('/'))
end
private
def self.call(url)
response = Typhoeus.get(url)
if response.success?
response.body
else
message = response.code == 0 ? response.return_message : response.code.to_s
Rails.logger.error "[APITchap] Error on #{url}: #{message}"
raise ResourceNotFound
end
end
end

View file

@ -0,0 +1,15 @@
class APITchap::HsAdapter
def initialize(email)
@email = email
end
def to_hs
data_source[:hs]
end
private
def data_source
@data_source ||= JSON.parse(APITchap::API.get_hs(@email), symbolize_names: true)
end
end

View file

@ -15,6 +15,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_and_belongs_to_many :default_zones, class_name: 'Zone', join_table: 'default_zones_administrateurs'
belongs_to :user belongs_to :user

View file

@ -139,6 +139,7 @@ class User < ApplicationRecord
if user.valid? && user.administrateur.nil? if user.valid? && user.administrateur.nil?
user.create_administrateur! user.create_administrateur!
user.update(france_connect_information: nil) user.update(france_connect_information: nil)
AdminUpdateDefaultZonesJob.perform_later(user.administrateur)
end end
user user

View file

@ -5,6 +5,7 @@
# id :bigint not null, primary key # id :bigint not null, primary key
# acronym :string not null # acronym :string not null
# label :string # label :string
# tchap_hs :string default([]), is an Array
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# #
@ -26,10 +27,15 @@ class Zone < ApplicationRecord
label_at(date) != 'Non attribué' label_at(date) != 'Non attribué'
end end
def self.available_at(date) def self.available_at(date, without_zones = [])
Zone.all.filter { |zone| zone.available_at?(date) }.sort_by { |zone| zone.label_at(date) } (Zone.all - without_zones).filter { |zone| zone.available_at?(date) }.sort_by { |zone| zone.label_at(date) }
.map do |zone| .map do |zone|
OpenStruct.new(id: zone.id, label: zone.label_at(date)) OpenStruct.new(id: zone.id, label: zone.label_at(date))
end end
end end
def self.default_for(tchap_hs)
sanitized_sql = ActiveRecord::Base.sanitize_sql "'#{tchap_hs}' = ANY (tchap_hs)"
Zone.where(sanitized_sql)
end
end end

View file

@ -1,21 +0,0 @@
class UpdateZoneToProceduresService
def self.call(lines)
errors = []
lines.each do |line|
zone_label = line["POL_PUB_MINISTERE RATTACHEMENT"]
zone = Zone.find_by(acronym: zone_label)
if zone.nil?
errors << "Zone #{zone_label} introuvable"
else
id = line["id"]
procedure = Procedure.find_by(id: id)
if procedure
procedure.update(zone: zone)
else
errors << "Procedure #{id} introuvable"
end
end
end
errors
end
end

View file

@ -14,7 +14,11 @@
= f.label :zone do = f.label :zone do
= t('zone', scope: 'activerecord.attributes.procedure') = t('zone', scope: 'activerecord.attributes.procedure')
- if Rails.application.config.ds_zonage_enabled - if Rails.application.config.ds_zonage_enabled
= f.collection_check_boxes :zone_ids, Zone.available_at(@procedure.published_or_created_at), :id, :label do |b| = f.collection_check_boxes :zone_ids, current_administrateur.default_zones, :id, :current_label do |b|
.editable-champ.editable-champ-checkbox
= b.check_box
= b.label class: "admin-default-zone"
= f.collection_check_boxes :zone_ids, Zone.available_at(@procedure.published_or_created_at, current_administrateur.default_zones), :id, :label do |b|
.editable-champ.editable-champ-checkbox .editable-champ.editable-champ-checkbox
= b.check_box = b.check_box
= b.label = b.label

View file

@ -5,6 +5,7 @@ API_ENTREPRISE_URL = ENV.fetch("API_ENTREPRISE_URL", "https://entreprise.api.gou
API_EDUCATION_URL = ENV.fetch("API_EDUCATION_URL", "https://data.education.gouv.fr/api/records/1.0") API_EDUCATION_URL = ENV.fetch("API_EDUCATION_URL", "https://data.education.gouv.fr/api/records/1.0")
API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr") API_GEO_URL = ENV.fetch("API_GEO_URL", "https://geo.api.gouv.fr")
API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api") API_PARTICULIER_URL = ENV.fetch("API_PARTICULIER_URL", "https://particulier.api.gouv.fr/api")
API_TCHAP_URL = ENV.fetch("API_TCHAP_URL", "https://matrix.agent.tchap.gouv.fr/_matrix/identity/api/v1")
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2") HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1") PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2") SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")

View file

@ -1,58 +1,121 @@
ministeres: ministeres:
- MAA: - MAA:
- '2022-05-20': "Ministère de l'Agriculture et de la Souveraineté alimentaire" labels:
- '2020-07-06': "Ministère de l'Agriculture et de l'Alimentation" - '2022-05-20': "Ministère de l'Agriculture et de la Souveraineté alimentaire"
- '2020-07-06': "Ministère de l'Agriculture et de l'Alimentation"
tchap_hs:
- agent.agriculture.tchap.gouv.fr
- MC: - MC:
- '2020-07-06': "Ministère de la Culture" labels:
- '2020-07-06': "Ministère de la Culture"
tchap_hs:
- agent.culture.tchap.gouv.fr
- MAS: - MAS:
- '2022-05-20': "Ministère de la Santé et de la Prévention" labels:
- '2020-07-06': "Ministère des Solidarités et de la Santé" - '2022-05-20': "Ministère de la Santé et de la Prévention"
- '2020-07-06': "Ministère des Solidarités et de la Santé"
tchap_hs:
- agent.social.tchap.gouv.fr
- MSAPH: - MSAPH:
- '2022-05-20': "Ministère des Solidarités, de l'Autonomie et des personnes handicapées" labels:
- '2020-07-06': "Non attribué" - '2022-05-20': "Ministère des Solidarités, de l'Autonomie et des personnes handicapées"
- '2020-07-06': "Non attribué"
tchap_hs:
- agent.social.tchap.gouv.fr
- MTEI: - MTEI:
- '2022-05-20': "Ministère du Travail, du Plein emploi et de l'Insertion" labels:
- '2020-07-06': "Ministère du Travail" - '2022-05-20': "Ministère du Travail, du Plein emploi et de l'Insertion"
- '2020-07-06': "Ministère du Travail"
tchap_hs:
- agent.social.tchap.gouv.fr
- MEAE: - MEAE:
- '2020-07-06': "Ministère de l'Europe et des Affaires étrangères" labels:
- '2020-07-06': "Ministère de l'Europe et des Affaires étrangères"
tchap_hs:
- agent.diplomatie.tchap.gouv.fr
- MEF: - MEF:
- '2022-05-20': "Ministère de l'Économie, des Finances et de la Souveraineté industrielle et numérique" labels:
- '2020-07-06': "Ministère de l'Économie, des Finances et de la Relance" - '2022-05-20': "Ministère de l'Économie, des Finances et de la Souveraineté industrielle et numérique"
- '2020-07-06': "Ministère de l'Économie, des Finances et de la Relance"
tchap_hs:
- agent.finances.tchap.gouv.fr
- MJS: - MJS:
- '2022-05-20': "Non attribué" labels:
- '2020-07-06': "Ministère de la Jeunesse et des Sports" - '2022-05-20': "Non attribué"
- '2020-07-06': "Ministère de la Jeunesse et des Sports"
tchap_hs:
- agent.social.tchap.gouv.fr
- MSJO: - MSJO:
- '2022-05-20': "Ministère des Sports et des Jeux olympiques et paralympiques" labels:
- '2020-07-06': "Non attribué" - '2022-05-20': "Ministère des Sports et des Jeux olympiques et paralympiques"
- '2020-07-06': "Non attribué"
tchap_hs:
- agent.social.tchap.gouv.fr
- MEN: - MEN:
- '2022-05-20': "Ministère de l'Éducation nationale et de la Jeunesse" labels:
- '2020-07-06': "Ministère de l'Éducation nationale, de la Jeunesse et des Sports" - '2022-05-20': "Ministère de l'Éducation nationale et de la Jeunesse"
- '2020-07-06': "Ministère de l'Éducation nationale, de la Jeunesse et des Sports"
tchap_hs:
- agent.education.tchap.gouv.fr
- ESR: - ESR:
- '2022-05-20': "Ministère de l'Enseignement supérieur et de la Recherche" labels:
- '2020-07-06': "Ministère de l'Enseignement supérieur, de la Recherche et de l'Innovation" - '2022-05-20': "Ministère de l'Enseignement supérieur et de la Recherche"
- '2020-07-06': "Ministère de l'Enseignement supérieur, de la Recherche et de l'Innovation"
tchap_hs:
- agent.education.tchap.gouv.fr
- MI: - MI:
- '2022-05-20': "Ministère de l'Intérieur et des Outre-mer" labels:
- '2020-07-06': "Ministère de l'Intérieur" - '2022-05-20': "Ministère de l'Intérieur et des Outre-mer"
- '2020-07-06': "Ministère de l'Intérieur"
tchap_hs:
- agent.interieur.tchap.gouv.fr
- MINARM: - MINARM:
- '2020-07-06': "Ministère des Armées" labels:
- '2020-07-06': "Ministère des Armées"
tchap_hs:
- agent.intradef.tchap.gouv.fr
- MJ: - MJ:
- '2020-07-06': "Ministère de la Justice" labels:
- '2020-07-06': "Ministère de la Justice"
tchap_hs:
- agent.justice.tchap.gouv.fr
- MTES: - MTES:
- '2022-05-20': "Ministère de la Transition écologique et de la Cohésion des territoires" labels:
- '2020-07-06': "Ministère de la Transition écologique" - '2022-05-20': "Ministère de la Transition écologique et de la Cohésion des territoires"
- '2020-07-06': "Ministère de la Transition écologique"
tchap_hs:
- agent.dev-durable.tchap.gouv.fr
- MTE: - MTE:
- '2022-05-20': "Ministère de la Transition énergétique" labels:
- '2020-07-06': "Non attribué" - '2022-05-20': "Ministère de la Transition énergétique"
- '2020-07-06': "Non attribué"
tchap_hs:
- agent.dev-durable.tchap.gouv.fr
- MCTRCT: - MCTRCT:
- '2022-05-20': "Non attribué" labels:
- '2020-07-06': "Ministère de la Cohésion des territoires et des Relations avec les collectivités territoriales" - '2022-05-20': "Non attribué"
- '2020-07-06': "Ministère de la Cohésion des territoires et des Relations avec les collectivités territoriales"
tchap_hs:
- agent.dev-durable.tchap.gouv.fr
- PM: - PM:
- '2020-07-06': "Premier ministre" labels:
- '2020-07-06': "Premier ministre"
tchap_hs:
- agent.pm.tchap.gouv.fr
- agent.dinum.tchap.gouv.fr
- MER: - MER:
- '2022-05-20': "Non attribué" labels:
- '2020-07-06': "Ministère de la Mer" - '2022-05-20': "Non attribué"
- '2020-07-06': "Ministère de la Mer"
- MTFP: - MTFP:
- '2020-07-06': "Ministère de la Transformation et de la Fonction publiques" labels:
- '2020-07-06': "Ministère de la Transformation et de la Fonction publiques"
tchap_hs:
- agent.pm.tchap.gouv.fr
- agent.dinum.tchap.gouv.fr
- OM: - OM:
- '2022-05-20': "Non attribué" labels:
- '2020-07-06': "Ministère des Outre-mer" - '2022-05-20': "Non attribué"
- '2020-07-06': "Ministère des Outre-mer"

View file

@ -0,0 +1,5 @@
class AddTchapHsToZones < ActiveRecord::Migration[6.1]
def change
add_column :zones, :tchap_hs, :string, array: true, default: []
end
end

View file

@ -0,0 +1,8 @@
class CreateDefaultZonesAdministrateurs < ActiveRecord::Migration[6.1]
def change
create_table :default_zones_administrateurs, id: false do |t|
t.belongs_to :administrateur
t.belongs_to :zone
end
end
end

View file

@ -10,7 +10,8 @@
# #
# 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[7.0].define(version: 2023_05_02_160046) do ActiveRecord::Schema[7.0].define(version: 2023_05_08_160551) 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 "pgcrypto" enable_extension "pgcrypto"
enable_extension "plpgsql" enable_extension "plpgsql"
@ -268,6 +269,13 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_02_160046) do
t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id" t.index ["instructeur_id"], name: "index_commentaires_on_instructeur_id"
end end
create_table "default_zones_administrateurs", id: false, force: :cascade do |t|
t.bigint "administrateur_id"
t.bigint "zone_id"
t.index ["administrateur_id"], name: "index_default_zones_administrateurs_on_administrateur_id"
t.index ["zone_id"], name: "index_default_zones_administrateurs_on_zone_id"
end
create_table "delayed_jobs", id: :serial, force: :cascade do |t| create_table "delayed_jobs", id: :serial, force: :cascade do |t|
t.integer "attempts", default: 0, null: false t.integer "attempts", default: 0, null: false
t.datetime "created_at", precision: 6 t.datetime "created_at", precision: 6
@ -969,6 +977,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_02_160046) do
t.string "acronym", null: false t.string "acronym", null: false
t.datetime "created_at", precision: 6, null: false t.datetime "created_at", precision: 6, null: false
t.string "label" t.string "label"
t.string "tchap_hs", default: [], array: true
t.datetime "updated_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.index ["acronym"], name: "index_zones_on_acronym", unique: true t.index ["acronym"], name: "index_zones_on_acronym", unique: true
end end

View file

@ -0,0 +1,25 @@
namespace :after_party do
desc 'Deployment task: populate_zones_with_tchap_hs'
task populate_zones_with_tchap_hs: :environment do
puts "Running deploy task 'populate_zones_with_tchap_hs'"
collectivite = Zone.find_or_create_by!(acronym: 'COLLECTIVITE')
coll_label = collectivite.labels.find_or_initialize_by(designated_on: Date.parse('1977-07-30'))
coll_label.update(name: 'Collectivité territoriale')
config = Psych.safe_load(Rails.root.join("config", "zones.yml").read)
config["ministeres"].each do |ministere|
acronym = ministere.keys.first
zone = Zone.find_or_create_by!(acronym: acronym)
labels_a = ministere['labels']
labels_a.each do |label_h|
designated_on = label_h.keys.first
label = zone.labels.find_or_initialize_by(designated_on: designated_on)
label.update(name: label_h[designated_on])
end
zone.update(tchap_hs: ministere['tchap_hs']) if ministere['tchap_hs']
end
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -1,23 +0,0 @@
require Rails.root.join("lib", "tasks", "task_helper")
namespace :zones do
desc <<~EOD
Update zone to all procedures
rails zones:update_zone_to_procedures\[csv_path\]
EOD
task :update_zone_to_procedures, [:csv] => :environment do |_t, args|
csv = args[:csv]
lines = CSV.readlines(csv, headers: true)
rake_puts "Mise à jour des procédures en cours..."
errors =
UpdateZoneToProceduresService.call(lines)
if errors.present?
errors.each { |error| rake_puts error }
end
rake_puts "Mise à jour terminée"
end
end

View file

@ -1,6 +1,7 @@
FactoryBot.define do FactoryBot.define do
factory :zone do factory :zone do
sequence(:acronym) { |n| "MA#{n}" } sequence(:acronym) { |n| "MA#{n}" }
tchap_hs { ['agent.educpop.tchap.gouv.fr'] }
transient do transient do
labels { [{ designated_on: '1981-05-08', name: "Ministère de l'Education Populaire" }] } labels { [{ designated_on: '1981-05-08', name: "Ministère de l'Education Populaire" }] }
end end

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe AdminUpdateDefaultZonesJob, type: :job do
let(:email) { 'louise@educ.pop.gouv.fr' }
let(:admin) { create(:administrateur, email: email) }
let(:hs_adapter) { double('hs_adapter', to_hs: 'agent.educpop.tchap.gouv.fr') }
subject(:perform_job) { described_class.perform_now(admin) }
before do
allow(APITchap::HsAdapter).to receive(:new).with(email).and_return(hs_adapter)
create(:zone, acronym: 'EP', tchap_hs: ['agent.educpop.tchap.gouv.fr'])
end
it 'update default zones' do
perform_job
expect(admin.default_zones.map(&:acronym)).to eq ['EP']
end
end

View file

@ -0,0 +1,19 @@
describe APITchap::HsAdapter do
let(:adapter) { described_class.new(email) }
let(:email) { "louise@mjc.gouv.fr" }
subject { adapter.to_hs }
before do
stub_request(:get, /https:\/\/matrix.agent.tchap.gouv.fr\/_matrix\/identity\/api\/v1\/info\?address=#{email}&medium=email/)
.to_return(body: body, status: status)
end
context 'with normal body' do
let(:body) { "{\"hs\": \"agent.educpop.gouv.fr\" }" }
let(:status) { 200 }
it 'returns hs' do
subject
expect(subject).to eq "agent.educpop.gouv.fr"
end
end
end

View file

@ -118,4 +118,18 @@ describe Zone do
expect(Zone.available_at(start_previous_government + 1.day).map(&:label)).to eq ["Ministère de la Culture", "Ministère des Outre-mer"] 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 end
context 'with zones' do
before do
create(:zone, acronym: 'MEN', tchap_hs: ['agent.education.tchap.gouv.fr'])
create(:zone, acronym: 'ESR', tchap_hs: ['agent.education.tchap.gouv.fr'])
end
describe "#self.default_for" do
it 'returns zone related to tchap hs' do
expect(Zone.default_for('agent.education.tchap.gouv.fr').map(&:acronym)).to eq ['MEN', 'ESR']
expect(Zone.default_for('agent.tchap.gouv.fr').map(&:acronym)).to eq []
end
end
end
end end

View file

@ -1,58 +0,0 @@
describe UpdateZoneToProceduresService do
before(:each) do
Flipper.enable :zonage
Rake::Task['after_party:populate_zones'].invoke
end
after(:each) do
Rake::Task['after_party:populate_zones'].reenable
end
describe '#call' do
let(:procedure1) { create(:procedure, zone: nil) }
let(:procedure2) { create(:procedure, zone: nil) }
subject { described_class.call(lines) }
context 'nominal case' do
let(:lines) do
[
{ "id" => procedure1.id, "POL_PUB_MINISTERE RATTACHEMENT" => "PM" },
{ "id" => procedure2.id, "POL_PUB_MINISTERE RATTACHEMENT" => "MI" }
]
end
it 'updates zone to procedures' do
errors = subject
expect(errors).to eq []
expect(procedure1.reload.zone.acronym).to eq("PM")
expect(procedure2.reload.zone.acronym).to eq("MI")
end
end
context 'with unknown procedure' do
let(:lines) do
[
{ "id" => procedure1.id + procedure2.id, "POL_PUB_MINISTERE RATTACHEMENT" => "PM" }
]
end
it 'returns errors' do
errors = subject
expect(errors).to eq ["Procedure #{procedure1.id + procedure2.id} introuvable"]
end
end
context 'with unknown zone' do
let(:lines) do
[
{ "id" => procedure1.id, "POL_PUB_MINISTERE RATTACHEMENT" => "YOUPI" }
]
end
it 'returns errors' do
errors = subject
expect(errors).to eq ["Zone YOUPI introuvable"]
end
end
end
end

View file

@ -6,6 +6,10 @@ describe 'As an administrateur', js: true do
let(:strong_password) { 'a new, long, and complicated password!' } let(:strong_password) { 'a new, long, and complicated password!' }
before do before do
body = "{\"hs\": \"agent.educpop.gouv.fr\" }"
WebMock.stub_request(:get, /https:\/\/matrix.agent.tchap.gouv.fr\/_matrix\/identity\/api\/v1\/info\?address=(.*)&medium=email/)
.to_return(body: body, status: 200)
perform_enqueued_jobs do perform_enqueued_jobs do
super_admin.invite_admin(admin_email) super_admin.invite_admin(admin_email)
end end

View file

@ -1,10 +1,12 @@
describe 'administrateurs/procedures/zones' do describe 'administrateurs/procedures/zones' do
let(:administrateur) { create(:administrateur) }
let(:procedure) { create(:procedure) } let(:procedure) { create(:procedure) }
let(:populate_zones_task) { Rake::Task['after_party:populate_zones'] } let(:populate_zones_task) { Rake::Task['after_party:populate_zones_with_tchap_hs'] }
before do before do
Rails.application.config.ds_zonage_enabled = true Rails.application.config.ds_zonage_enabled = true
populate_zones_task.invoke populate_zones_task.invoke
allow(view).to receive(:current_administrateur).and_return(administrateur)
end end
after do after do