Merge pull request #7489 from betagouv/opendata-export-demarches-desc

Permet la génération json du descriptif des démarches publiques

Une prochaine PR permettra la publication de ce fichier json dans data.gouv.fr
This commit is contained in:
krichtof 2022-07-06 18:07:57 +02:00 committed by GitHub
commit 8788ee70b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 194 additions and 5 deletions

View file

@ -72,7 +72,7 @@ class API::V2::Schema < GraphQL::Schema
use GraphQL::Execution::Interpreter use GraphQL::Execution::Interpreter
use GraphQL::Analysis::AST use GraphQL::Analysis::AST
use GraphQL::Schema::Timeout, max_seconds: 10 use GraphQL::Schema::Timeout, max_seconds: 30
use GraphQL::Batch use GraphQL::Batch
use GraphQL::Backtrace use GraphQL::Backtrace

View file

@ -581,7 +581,7 @@ type Demarche {
number: Int! number: Int!
publishedRevision: Revision publishedRevision: Revision
revisions: [Revision!]! revisions: [Revision!]!
service: Service! service: Service
""" """
État de la démarche. État de la démarche.
@ -600,6 +600,8 @@ Ceci est une version abrégée du type `Demarche`, qui nexpose que les métad
Cela évite laccès récursif aux dossiers. Cela évite laccès récursif aux dossiers.
""" """
type DemarcheDescriptor { type DemarcheDescriptor {
cadreJuridique: String
""" """
Date de la création. Date de la création.
""" """
@ -629,6 +631,7 @@ type DemarcheDescriptor {
Pour une démarche déclarative, état cible des dossiers à valider automatiquement Pour une démarche déclarative, état cible des dossiers à valider automatiquement
""" """
declarative: DossierDeclarativeState declarative: DossierDeclarativeState
deliberation: String
""" """
Description de la démarche. Description de la démarche.
@ -641,7 +644,7 @@ type DemarcheDescriptor {
""" """
number: Int! number: Int!
revision: Revision! revision: Revision!
service: Service! service: Service
""" """
État de la démarche. État de la démarche.

View file

@ -0,0 +1,12 @@
module Types
class BaseField < GraphQL::Schema::Field
def initialize(*args, internal: false, **kwargs, &block)
@internal = internal
super(*args, **kwargs, &block)
end
def visible?(ctx)
super && (@internal ? ctx[:internal_use] : true)
end
end
end

View file

@ -1,5 +1,6 @@
module Types module Types
class DemarcheDescriptorType < Types::BaseObject class DemarcheDescriptorType < Types::BaseObject
field_class BaseField
description "Une démarche (métadonnées) description "Une démarche (métadonnées)
Ceci est une version abrégée du type `Demarche`, qui nexpose que les métadonnées. Ceci est une version abrégée du type `Demarche`, qui nexpose que les métadonnées.
Cela évite laccès récursif aux dossiers." Cela évite laccès récursif aux dossiers."
@ -18,7 +19,12 @@ Cela évite laccès récursif aux dossiers."
field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true
field :revision, Types::RevisionType, null: false field :revision, Types::RevisionType, null: false
field :service, Types::ServiceType, null: false field :service, Types::ServiceType, null: true
field :cadre_juridique, String, null: true
field :deliberation, String, null: true
field :dossiers_count, Int, null: false, internal: true
def service def service
Loaders::Record.for(Service).load(procedure.service_id) Loaders::Record.for(Service).load(procedure.service_id)
@ -28,10 +34,22 @@ Cela évite laccès récursif aux dossiers."
object.is_a?(ProcedureRevision) ? object : object.active_revision object.is_a?(ProcedureRevision) ? object : object.active_revision
end end
def dossiers_count
object.dossiers.count
end
def deliberation
Rails.application.routes.url_helpers.url_for(procedure.deliberation) if procedure.deliberation.attached?
end
def state def state
procedure.aasm.current_state procedure.aasm.current_state
end end
def cadre_juridique
procedure.cadre_juridique
end
def number def number
procedure.id procedure.id
end end

View file

@ -30,7 +30,7 @@ module Types
field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true, method: :closed_at field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true, method: :closed_at
field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false
field :service, Types::ServiceType, null: false field :service, Types::ServiceType, null: true
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers dune démarche.", null: false do field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers dune démarche.", null: false do
argument :order, Types::Order, default_value: :asc, required: false, description: "Lordre des dossiers." argument :order, Types::Order, default_value: :asc, required: false, description: "Lordre des dossiers."

View file

@ -1,5 +1,7 @@
module Types module Types
class QueryType < Types::BaseObject class QueryType < Types::BaseObject
field_class BaseField
field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do
argument :number, Int, "Numéro de la démarche.", required: true argument :number, Int, "Numéro de la démarche.", required: true
end end
@ -12,6 +14,12 @@ module Types
argument :number, Int, "Numéro du groupe instructeur.", required: true argument :number, Int, "Numéro du groupe instructeur.", required: true
end end
field :demarches_publiques, DemarcheDescriptorType.connection_type, null: false, internal: true, max_page_size: 30
def demarches_publiques
Procedure.opendata
end
def demarche(number:) def demarche(number:)
Procedure.for_api_v2.find(number) Procedure.for_api_v2.find(number)
rescue => e rescue => e

View file

@ -219,6 +219,7 @@ class Procedure < ApplicationRecord
scope :brouillons, -> { where(aasm_state: :brouillon) } scope :brouillons, -> { where(aasm_state: :brouillon) }
scope :publiees, -> { where(aasm_state: :publiee) } scope :publiees, -> { where(aasm_state: :publiee) }
scope :closes, -> { where(aasm_state: [:close, :depubliee]) } scope :closes, -> { where(aasm_state: [:close, :depubliee]) }
scope :opendata, -> { where(opendata: true) }
scope :publiees_ou_closes, -> { where(aasm_state: [:publiee, :close, :depubliee]) } scope :publiees_ou_closes, -> { where(aasm_state: [:publiee, :close, :depubliee]) }
scope :by_libelle, -> { order(libelle: :asc) } scope :by_libelle, -> { order(libelle: :asc) }
scope :created_during, -> (range) { where(created_at: range) } scope :created_during, -> (range) { where(created_at: range) }

View file

@ -0,0 +1,100 @@
class DemarchesPubliquesExportService
attr_reader :io
def initialize(io)
@io = io
end
def call
end_cursor = nil
first = true
write_array_opening
loop do
write_demarches_separator if !first
execute_query(cursor: end_cursor)
end_cursor = last_cursor
io.write(jsonify(demarches))
first = false
break if !has_next_page?
end
write_array_closing
io.close
end
private
def execute_query(cursor: nil)
result = API::V2::Schema.execute(query, variables: { cursor: cursor }, context: { internal_use: true })
raise DemarchesPubliquesExportService::Error.new(result["errors"]) if result["errors"]
@graphql_data = result["data"]
end
def query
"query($cursor: String) {
demarchesPubliques(after: $cursor) {
pageInfo {
endCursor
hasNextPage
}
edges {
node {
number
title
description
datePublication
service { nom organisme typeOrganisme }
cadreJuridique
deliberation
dossiersCount
revision {
champDescriptors {
type
label
description
required
options
champDescriptors {
type
label
description
required
options
}
}
}
}
}
}
}"
end
def last_cursor
@graphql_data["demarchesPubliques"]["pageInfo"]["endCursor"]
end
def has_next_page?
@graphql_data["demarchesPubliques"]["pageInfo"]["hasNextPage"]
end
def demarches
@graphql_data["demarchesPubliques"]["edges"].map { |edge| edge["node"] }
end
def jsonify(demarches)
demarches.map(&:to_json).join(',')
end
def write_array_opening
io.write('[')
end
def write_array_closing
io.write(']')
end
def write_demarches_separator
io.write(',')
end
end
class DemarchesPubliquesExportService::Error < StandardError
end

View file

@ -0,0 +1,47 @@
describe DemarchesPubliquesExportService do
let(:procedure) { create(:procedure, :published, :with_service, :with_type_de_champ) }
let!(:dossier) { create(:dossier, procedure: procedure) }
let(:io) { StringIO.new }
describe 'call' do
it 'generate json for all closed procedures' do
expected_result = {
number: procedure.id,
title: procedure.libelle,
description: "Demande de subvention à l'intention des associations",
service: {
nom: procedure.service.nom,
organisme: "organisme",
typeOrganisme: "association"
},
cadreJuridique: "un cadre juridique important",
deliberation: nil,
datePublication: procedure.published_at.iso8601,
dossiersCount: 1,
revision: {
champDescriptors: [
{
description: procedure.types_de_champ.first.description,
label: procedure.types_de_champ.first.libelle,
options: nil,
required: false,
type: "text",
champDescriptors: nil
}
]
}
}
DemarchesPubliquesExportService.new(io).call
expect(JSON.parse(io.string)[0]
.deep_symbolize_keys)
.to eq(expected_result)
end
it 'raises exception when procedure with bad data' do
procedure.libelle = nil
procedure.save(validate: false)
expect { DemarchesPubliquesExportService.new(io).call }.to raise_error(DemarchesPubliquesExportService::Error)
end
end
end