Merge pull request #9420 from mfo/US/chorus-tile

amelioration(tuile.chorus): ETQ admin, je peux saisir le cadre budgetaire d'une demarche de subvention pour faciliter le rapprochement d'un export DS a un export Chorus
This commit is contained in:
mfo 2023-10-24 12:57:26 +00:00 committed by GitHub
commit ebea269f79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 2468 additions and 0 deletions

View file

@ -0,0 +1,13 @@
class Procedure::Card::ChorusComponent < ApplicationComponent
def initialize(procedure:)
@procedure = procedure
end
def render?
@procedure.chorusable?
end
def complete?
@procedure.chorus_configuration.complete?
end
end

View file

@ -0,0 +1,16 @@
.fr-col-6.fr-col-md-4.fr-col-lg-3.chorus-component
= link_to edit_admin_procedure_chorus_path(@procedure), class: 'fr-tile fr-enlarge-link', title: 'Configurer le cadre budgetaire Chorus' do
.fr-tile__body.flex.column.align-center.justify-between
- if !@procedure.chorus_configuration.complete?
%div
%span.icon.clock
%p.fr-tile-status-todo À compléter
- else
%div
%span.icon.accept
%p.fr-tile-status-accept Configuré
%div
%h3.fr-h6.fr-mt-10v
Connecteur Chorus
%p.fr-tile-subtitle Vous traitez des données de subvention d'état ?
%p.fr-btn.fr-btn--tertiary Configurer

View file

@ -0,0 +1,15 @@
class Procedure::ChorusFormComponent < ApplicationComponent
attr_reader :procedure
def initialize(procedure:)
@procedure = procedure
end
def map_attribute_to_autocomplete_endpoint
{
centre_de_coup: data_sources_search_centre_couts_path,
domaine_fonctionnel: data_sources_search_domaine_fonct_path,
referentiel_de_programmation: data_sources_search_ref_programmation_path
}
end
end

View file

@ -0,0 +1,9 @@
= form_for([procedure, procedure.chorus_configuration],url: admin_procedure_chorus_path(procedure), method: :put) do |f|
- map_attribute_to_autocomplete_endpoint.map do |chorus_configuration_attribute, datasource_endpoint|
- label_class_name = "#{chorus_configuration_attribute}-label"
.fr-select-group
= f.label chorus_configuration_attribute, class: 'fr-label', id: label_class_name
= render Dsfr::ComboboxComponent.new form: f, name: :chorus_configuration_attribute, url: datasource_endpoint, selected: procedure.chorus_configuration.format_displayed_value(chorus_configuration_attribute), id: chorus_configuration_attribute, class: 'fr-select', describedby: label_class_name do
= f.hidden_field chorus_configuration_attribute, data: { value_slot: 'data' }, value: procedure.chorus_configuration.format_hidden_value(chorus_configuration_attribute)
= f.submit "Enregister", class: 'fr-btn'

View file

@ -0,0 +1,33 @@
module Administrateurs
class ChorusController < AdministrateurController
before_action :retrieve_procedure
def edit
end
def update
@configuration = @procedure.chorus_configuration
@configuration.assign_attributes(configurations_params)
if @configuration.valid?
@procedure.update!(chorus: @configuration.attributes)
flash.notice = "La configuration Chorus a été mise à jour et prend immédiatement effet pour les nouveaux dossiers."
redirect_to admin_procedure_path(@procedure)
else
flash.now.alert = "Des erreurs empêchent la validation du connecteur chorus. Corrigez les erreurs"
render :edit
end
end
private
def search_params
params.permit(:q)
end
def configurations_params
params.require(:chorus_configuration)
.permit(:centre_de_coup, :domaine_fonctionnel, :referentiel_de_programmation)
end
end
end

View file

@ -0,0 +1,33 @@
class DataSources::ChorusController < ApplicationController
before_action :authenticate_administrateur!
def search_domaine_fonct
result_json = APIBretagneService.new.search_domaine_fonct(code_or_label: params[:q])
render json: format_result(result_json:,
label_formatter: ChorusConfiguration.method(:format_domaine_fonctionnel_label))
end
def search_centre_couts
result_json = APIBretagneService.new.search_centre_couts(code_or_label: params[:q])
render json: format_result(result_json:,
label_formatter: ChorusConfiguration.method(:format_centre_de_coup_label))
end
def search_ref_programmation
result_json = APIBretagneService.new.search_ref_programmation(code_or_label: params[:q])
render json: format_result(result_json:,
label_formatter: ChorusConfiguration.method(:format_ref_programmation_label))
end
private
def format_result(result_json:, label_formatter:)
result_json.map do |item|
{
label: label_formatter.call(item),
value: "#{item[:label]} - #{item[:code_programme]}",
data: item
}
end
end
end

View file

@ -0,0 +1,60 @@
class ChorusConfiguration
include ActiveModel::Model
include ActiveModel::Attributes
attribute :centre_de_coup, :simple_json, default: '{}'
attribute :domaine_fonctionnel, :simple_json, default: '{}'
attribute :referentiel_de_programmation, :simple_json, default: '{}'
def format_displayed_value(attribute_name)
case attribute_name
when :centre_de_coup
ChorusConfiguration.format_centre_de_coup_label(centre_de_coup)
when :domaine_fonctionnel
ChorusConfiguration.format_domaine_fonctionnel_label(domaine_fonctionnel)
when :referentiel_de_programmation
ChorusConfiguration.format_ref_programmation_label(referentiel_de_programmation)
else
raise 'unknown attribute_name'
end
end
def format_hidden_value(attribute_name)
case attribute_name
when :centre_de_coup
centre_de_coup.to_json
when :domaine_fonctionnel
domaine_fonctionnel.to_json
when :referentiel_de_programmation
referentiel_de_programmation.to_json
else
raise 'unknown attribute_name'
end
end
def self.format_centre_de_coup_label(api_result)
return "" if api_result.blank?
api_result = api_result.symbolize_keys
"#{api_result[:description]} - #{api_result[:code]}"
end
def self.format_domaine_fonctionnel_label(api_result)
return "" if api_result.blank?
api_result = api_result.symbolize_keys
"#{api_result[:label]} - #{api_result[:code]}"
end
def self.format_ref_programmation_label(api_result)
return "" if api_result.blank?
api_result = api_result.symbolize_keys
"#{api_result[:label]} - #{api_result[:code]}"
end
def complete?
[
centre_de_coup,
domaine_fonctionnel,
referentiel_de_programmation
].all?(&:present?)
end
end

View file

@ -0,0 +1,13 @@
module ProcedureChorusConcern
extend ActiveSupport::Concern
included do
def chorus_configuration
@chorus_configuration ||= ChorusConfiguration.new(chorus)
end
def chorusable?
feature_enabled?(:chorus)
end
end
end

View file

@ -4,6 +4,7 @@ class Procedure < ApplicationRecord
include InitiationProcedureConcern
include ProcedureGroupeInstructeurAPIHackConcern
include ProcedureSVASVRConcern
include ProcedureChorusConcern
include Discard::Model
self.discard_column = :hidden_at

View file

@ -0,0 +1,93 @@
class APIBretagneService
include Dry::Monads[:result]
HOST = 'https://api.databretagne.fr'
ENDPOINTS = {
# see: https://api.databretagne.fr/budget/doc#operations-Auth_Controller-post_login
"login" => "/budget/api/v1/auth/login",
# see: https://api.databretagne.fr/budget/doc#operations-Centre_couts-get_ref_controller_list
"centre-couts" => '/budget/api/v1/centre-couts',
# see: https://api.databretagne.fr/budget/doc#operations-Domaine_Fonctionnel-get_ref_controller_list
"domaine-fonct" => '/budget/api/v1/domaine-fonct',
# see: https://api.databretagne.fr/budget/doc#operations-Referentiel_Programmation-get_ref_controller_list
"ref-programmation" => '/budget/api/v1/ref-programmation'
}
def search_domaine_fonct(code_or_label: "")
request(endpoint: ENDPOINTS.fetch('domaine-fonct'), code_or_label:)
end
def search_centre_couts(code_or_label: "")
request(endpoint: ENDPOINTS.fetch('centre-couts'), code_or_label:)
end
def search_ref_programmation(code_or_label: "")
request(endpoint: ENDPOINTS.fetch('ref-programmation'), code_or_label:)
end
private
def request(endpoint:, code_or_label:)
return [] if (code_or_label || "").strip.size < 3
url = build_url(endpoint)
fetch_page(url:, params: { query: code_or_label, page_number: 1 })[:items] || []
end
def fetch_page(url:, params:, remaining_retry_count: 1)
result = call(url:, params:)
case result
in Failure(code:, reason:) if code.in?(401..403)
if remaining_retry_count > 0
login
fetch_page(url:, params:, remaining_retry_count: 0)
else
fail "APIBretagneService, #{reason} #{code}"
end
in Success(body:)
body
else # no response gives back a 204, so we don't try to JSON.parse(nil) to avoid error
{ items: [] }
end
end
def call(url:, params:)
API::Client.new.(url:, params:, authorization_token:, method:)
end
def method
:get
end
def authorization_token
result = login
case result
in Success(token:)
@token = token
in Failure(reason:, code:)
fail "APIBretagneService, #{reason} #{code}"
end
end
def login
result = API::Client.new.call(url: build_url(ENDPOINTS.fetch("login")),
json: {
email: ENV['API_DATABRETAGE_USERNAME'],
password: ENV['API_DATABRETAGE_PASSWORD']
},
method: :post)
case result
in Success(body:)
Success(token: body.split("Bearer ")[1])
in Failure(code:, reason:) if code.in?(403)
Failure(API::Client::Error[:invalid_credential, code, false, reason])
else
Failure(API::Client::Error[:api_down])
end
end
def build_url(endpoint)
uri = URI(HOST)
uri.path = endpoint
uri
end
end

View file

@ -0,0 +1,11 @@
= render partial: 'administrateurs/breadcrumbs',
locals: { steps: [['Démarches', admin_procedures_path],
[@procedure.libelle.truncate_words(10), admin_procedure_path(@procedure)],
['Connecteur Chorus']] }
.container
%h1.fr-h1
Cadre budgétaire
= render Procedure::ChorusFormComponent.new(procedure: @procedure)

View file

@ -66,3 +66,4 @@
= render Procedure::Card::SVASVRComponent.new(procedure: @procedure) if @procedure.sva_svr_enabled? || @procedure.feature_enabled?(:sva)
= render Procedure::Card::MonAvisComponent.new(procedure: @procedure)
= render Procedure::Card::DossierSubmittedMessageComponent.new(procedure: @procedure)
= render Procedure::Card::ChorusComponent.new(procedure: @procedure)