commit
c401a83afb
79 changed files with 1880 additions and 120 deletions
5
Gemfile
5
Gemfile
|
@ -32,6 +32,10 @@ gem 'flipper-ui'
|
||||||
gem 'fog-openstack'
|
gem 'fog-openstack'
|
||||||
gem 'font-awesome-rails'
|
gem 'font-awesome-rails'
|
||||||
gem 'gon'
|
gem 'gon'
|
||||||
|
gem 'graphiql-rails'
|
||||||
|
gem 'graphql'
|
||||||
|
gem 'graphql-batch'
|
||||||
|
gem 'graphql-rails_logger'
|
||||||
gem 'groupdate'
|
gem 'groupdate'
|
||||||
gem 'haml-rails'
|
gem 'haml-rails'
|
||||||
gem 'hashie'
|
gem 'hashie'
|
||||||
|
@ -105,6 +109,7 @@ end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'byebug' # Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
gem 'byebug' # Call 'byebug' anywhere in the code to stop execution and get a debugger console
|
||||||
|
gem 'graphql-schema_comparator'
|
||||||
gem 'mina', git: 'https://github.com/mina-deploy/mina.git', require: false # Deploy
|
gem 'mina', git: 'https://github.com/mina-deploy/mina.git', require: false # Deploy
|
||||||
gem 'pry-byebug'
|
gem 'pry-byebug'
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
|
|
23
Gemfile.lock
23
Gemfile.lock
|
@ -250,6 +250,22 @@ GEM
|
||||||
actionpack (>= 3.0)
|
actionpack (>= 3.0)
|
||||||
multi_json
|
multi_json
|
||||||
request_store (>= 1.0)
|
request_store (>= 1.0)
|
||||||
|
graphiql-rails (1.7.0)
|
||||||
|
railties
|
||||||
|
sprockets-rails
|
||||||
|
graphql (1.9.10)
|
||||||
|
graphql-batch (0.4.1)
|
||||||
|
graphql (>= 1.3, < 2)
|
||||||
|
promise.rb (~> 0.7.2)
|
||||||
|
graphql-rails_logger (1.2.0)
|
||||||
|
actionpack (~> 5.0)
|
||||||
|
activesupport (~> 5.0)
|
||||||
|
railties (~> 5.0)
|
||||||
|
rouge (~> 3.0)
|
||||||
|
graphql-schema_comparator (0.6.1)
|
||||||
|
bundler (>= 1.14)
|
||||||
|
graphql (~> 1.6)
|
||||||
|
thor (>= 0.19, < 2.0)
|
||||||
groupdate (4.1.1)
|
groupdate (4.1.1)
|
||||||
activesupport (>= 4.2)
|
activesupport (>= 4.2)
|
||||||
guard (2.15.0)
|
guard (2.15.0)
|
||||||
|
@ -430,6 +446,7 @@ GEM
|
||||||
premailer-rails (1.10.2)
|
premailer-rails (1.10.2)
|
||||||
actionmailer (>= 3, < 6)
|
actionmailer (>= 3, < 6)
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
|
promise.rb (0.7.4)
|
||||||
pry (0.12.2)
|
pry (0.12.2)
|
||||||
coderay (~> 1.1.0)
|
coderay (~> 1.1.0)
|
||||||
method_source (~> 0.9.0)
|
method_source (~> 0.9.0)
|
||||||
|
@ -517,6 +534,7 @@ GEM
|
||||||
activesupport (>= 3.0)
|
activesupport (>= 3.0)
|
||||||
builder (>= 3.0)
|
builder (>= 3.0)
|
||||||
rubyzip (>= 1.0)
|
rubyzip (>= 1.0)
|
||||||
|
rouge (3.9.0)
|
||||||
rspec (3.8.0)
|
rspec (3.8.0)
|
||||||
rspec-core (~> 3.8.0)
|
rspec-core (~> 3.8.0)
|
||||||
rspec-expectations (~> 3.8.0)
|
rspec-expectations (~> 3.8.0)
|
||||||
|
@ -737,6 +755,11 @@ DEPENDENCIES
|
||||||
fog-openstack
|
fog-openstack
|
||||||
font-awesome-rails
|
font-awesome-rails
|
||||||
gon
|
gon
|
||||||
|
graphiql-rails
|
||||||
|
graphql
|
||||||
|
graphql-batch
|
||||||
|
graphql-rails_logger
|
||||||
|
graphql-schema_comparator
|
||||||
groupdate
|
groupdate
|
||||||
guard
|
guard
|
||||||
guard-livereload
|
guard-livereload
|
||||||
|
|
20
app/controllers/api/v2/base_controller.rb
Normal file
20
app/controllers/api/v2/base_controller.rb
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
class API::V2::BaseController < ApplicationController
|
||||||
|
protect_from_forgery with: :null_session
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def context
|
||||||
|
{
|
||||||
|
administrateur_id: current_administrateur&.id,
|
||||||
|
token: authorization_bearer_token
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorization_bearer_token
|
||||||
|
received_token = nil
|
||||||
|
authenticate_with_http_token do |token, _options|
|
||||||
|
received_token = token
|
||||||
|
end
|
||||||
|
received_token
|
||||||
|
end
|
||||||
|
end
|
45
app/controllers/api/v2/graphql_controller.rb
Normal file
45
app/controllers/api/v2/graphql_controller.rb
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
class API::V2::GraphqlController < API::V2::BaseController
|
||||||
|
def execute
|
||||||
|
variables = ensure_hash(params[:variables])
|
||||||
|
|
||||||
|
result = Api::V2::Schema.execute(params[:query],
|
||||||
|
variables: variables,
|
||||||
|
context: context,
|
||||||
|
operation_name: params[:operationName])
|
||||||
|
|
||||||
|
render json: result
|
||||||
|
rescue => e
|
||||||
|
if Rails.env.development?
|
||||||
|
handle_error_in_development e
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Handle form data, JSON body, or a blank value
|
||||||
|
def ensure_hash(ambiguous_param)
|
||||||
|
case ambiguous_param
|
||||||
|
when String
|
||||||
|
if ambiguous_param.present?
|
||||||
|
ensure_hash(JSON.parse(ambiguous_param))
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
when Hash, ActionController::Parameters
|
||||||
|
ambiguous_param
|
||||||
|
when nil
|
||||||
|
{}
|
||||||
|
else
|
||||||
|
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_error_in_development(e)
|
||||||
|
logger.error e.message
|
||||||
|
logger.error e.backtrace.join("\n")
|
||||||
|
|
||||||
|
render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500
|
||||||
|
end
|
||||||
|
end
|
|
@ -78,7 +78,6 @@ module Instructeurs
|
||||||
|
|
||||||
def archive
|
def archive
|
||||||
dossier.update(archived: true)
|
dossier.update(archived: true)
|
||||||
current_instructeur.unfollow(dossier)
|
|
||||||
redirect_back(fallback_location: instructeur_procedures_url)
|
redirect_back(fallback_location: instructeur_procedures_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -187,17 +187,19 @@ module Instructeurs
|
||||||
def download_dossiers
|
def download_dossiers
|
||||||
options = params.permit(:version, :limit, :since, tables: [])
|
options = params.permit(:version, :limit, :since, tables: [])
|
||||||
|
|
||||||
|
dossiers = current_instructeur.dossiers.for_procedure(procedure)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.csv do
|
format.csv do
|
||||||
send_data(procedure.to_csv(options),
|
send_data(procedure.to_csv(dossiers, options),
|
||||||
filename: procedure.export_filename(:csv))
|
filename: procedure.export_filename(:csv))
|
||||||
end
|
end
|
||||||
format.xlsx do
|
format.xlsx do
|
||||||
send_data(procedure.to_xlsx(options),
|
send_data(procedure.to_xlsx(dossiers, options),
|
||||||
filename: procedure.export_filename(:xlsx))
|
filename: procedure.export_filename(:xlsx))
|
||||||
end
|
end
|
||||||
format.ods do
|
format.ods do
|
||||||
send_data(procedure.to_ods(options),
|
send_data(procedure.to_ods(dossiers, options),
|
||||||
filename: procedure.export_filename(:ods))
|
filename: procedure.export_filename(:ods))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
70
app/graphql/api/v2/schema.rb
Normal file
70
app/graphql/api/v2/schema.rb
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
class Api::V2::Schema < GraphQL::Schema
|
||||||
|
default_max_page_size 100
|
||||||
|
max_complexity 300
|
||||||
|
max_depth 15
|
||||||
|
|
||||||
|
query Types::QueryType
|
||||||
|
mutation Types::MutationType
|
||||||
|
|
||||||
|
def self.id_from_object(object, type_definition, ctx)
|
||||||
|
object.to_typed_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.object_from_id(id, query_ctx)
|
||||||
|
ApplicationRecord.record_from_typed_id(id)
|
||||||
|
rescue => e
|
||||||
|
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.resolve_type(type, obj, ctx)
|
||||||
|
case obj
|
||||||
|
when Procedure
|
||||||
|
Types::DemarcheType
|
||||||
|
when Dossier
|
||||||
|
Types::DossierType
|
||||||
|
when Commentaire
|
||||||
|
Types::MessageType
|
||||||
|
when Instructeur, User
|
||||||
|
Types::ProfileType
|
||||||
|
else
|
||||||
|
raise GraphQL::ExecutionError.new("Unexpected object: #{obj}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
orphan_types Types::Champs::CarteChampType,
|
||||||
|
Types::Champs::CheckboxChampType,
|
||||||
|
Types::Champs::DateChampType,
|
||||||
|
Types::Champs::DecimalNumberChampType,
|
||||||
|
Types::Champs::DossierLinkChampType,
|
||||||
|
Types::Champs::IntegerNumberChampType,
|
||||||
|
Types::Champs::LinkedDropDownListChampType,
|
||||||
|
Types::Champs::MultipleDropDownListChampType,
|
||||||
|
Types::Champs::PieceJustificativeChampType,
|
||||||
|
Types::Champs::RepetitionChampType,
|
||||||
|
Types::Champs::SiretChampType,
|
||||||
|
Types::Champs::TextChampType,
|
||||||
|
Types::GeoAreas::ParcelleCadastraleType,
|
||||||
|
Types::GeoAreas::QuartierPrioritaireType,
|
||||||
|
Types::GeoAreas::SelectionUtilisateurType
|
||||||
|
|
||||||
|
def self.unauthorized_object(error)
|
||||||
|
# Add a top-level error to the response instead of returning nil:
|
||||||
|
raise GraphQL::ExecutionError.new("An object of type #{error.type.graphql_name} was hidden due to permissions", extensions: { code: :unauthorized })
|
||||||
|
end
|
||||||
|
|
||||||
|
middleware(GraphQL::Schema::TimeoutMiddleware.new(max_seconds: 5) do |_, query|
|
||||||
|
Rails.logger.info("GraphQL Timeout: #{query.query_string}")
|
||||||
|
end)
|
||||||
|
|
||||||
|
if Rails.env.development?
|
||||||
|
query_analyzer(GraphQL::Analysis::QueryComplexity.new do |_, complexity|
|
||||||
|
Rails.logger.info("[GraphQL Query Complexity] #{complexity}")
|
||||||
|
end)
|
||||||
|
query_analyzer(GraphQL::Analysis::QueryDepth.new do |_, depth|
|
||||||
|
Rails.logger.info("[GraphQL Query Depth] #{depth}")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
use GraphQL::Batch
|
||||||
|
use GraphQL::Tracing::SkylightTracing
|
||||||
|
end
|
36
app/graphql/extensions/attachment.rb
Normal file
36
app/graphql/extensions/attachment.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# references:
|
||||||
|
# https://evilmartians.com/chronicles/active-storage-meets-graphql-pt-2-exposing-attachment-urls
|
||||||
|
|
||||||
|
module Extensions
|
||||||
|
class Attachment < GraphQL::Schema::FieldExtension
|
||||||
|
attr_reader :attachment_assoc
|
||||||
|
|
||||||
|
def apply
|
||||||
|
# Here we try to define the attachment name:
|
||||||
|
# - it could be set explicitly via extension options
|
||||||
|
# - or we imply that is the same as the field name w/o "_url"
|
||||||
|
# suffix (e.g., "avatar_url" => "avatar")
|
||||||
|
attachment = options&.[](:attachment) || field.original_name.to_s.sub(/_url$/, "")
|
||||||
|
|
||||||
|
# that's the name of the Active Record association
|
||||||
|
@attachment_assoc = "#{attachment}_attachment"
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method resolves (as it states) the field itself
|
||||||
|
# (it's the same as defining a method within a type)
|
||||||
|
def resolve(object:, **_rest)
|
||||||
|
Loaders::Association.for(
|
||||||
|
object.object.class,
|
||||||
|
attachment_assoc => :blob
|
||||||
|
).load(object.object)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is called if the result of the `resolve`
|
||||||
|
# is a lazy value (e.g., a Promise – like in our case)
|
||||||
|
def after_resolve(value:, **_rest)
|
||||||
|
return if value.nil?
|
||||||
|
|
||||||
|
Rails.application.routes.url_helpers.url_for(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
66
app/graphql/loaders/association.rb
Normal file
66
app/graphql/loaders/association.rb
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
# references:
|
||||||
|
# https://github.com/Shopify/graphql-batch/blob/master/examples/association_loader.rb
|
||||||
|
# https://gist.github.com/palkan/03eb5306a1a3e8addbe8df97a298a466
|
||||||
|
# https://evilmartians.com/chronicles/active-storage-meets-graphql-pt-2-exposing-attachment-urls
|
||||||
|
|
||||||
|
module Loaders
|
||||||
|
class Association < GraphQL::Batch::Loader
|
||||||
|
def self.validate(model, association_name)
|
||||||
|
new(model, association_name)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(model, association_schema)
|
||||||
|
@model = model
|
||||||
|
@association_schema = association_schema
|
||||||
|
@association_name = extract_association_id(association_schema)
|
||||||
|
validate
|
||||||
|
end
|
||||||
|
|
||||||
|
def load(record)
|
||||||
|
raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
|
||||||
|
return Promise.resolve(read_association(record)) if association_loaded?(record)
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
# We want to load the associations on all records, even if they have the same id
|
||||||
|
def cache_key(record)
|
||||||
|
record.object_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(records)
|
||||||
|
preload_association(records.uniq)
|
||||||
|
records.each { |record| fulfill(record, read_association(record)) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def validate
|
||||||
|
unless @model.reflect_on_association(@association_name)
|
||||||
|
raise ArgumentError, "No association #{@association_name} on #{@model}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def preload_association(records)
|
||||||
|
::ActiveRecord::Associations::Preloader.new.preload(records, @association_schema)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_association(record)
|
||||||
|
record.public_send(@association_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def association_loaded?(record)
|
||||||
|
record.association(@association_name).loaded?
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_association_id(id_or_hash)
|
||||||
|
return id_or_hash unless id_or_hash.is_a?(Hash)
|
||||||
|
|
||||||
|
if id_or_hash.keys.size != 1
|
||||||
|
raise ArgumentError, "You can only preload exactly one association! You passed: #{id_or_hash}"
|
||||||
|
end
|
||||||
|
|
||||||
|
id_or_hash.keys.first
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
app/graphql/loaders/record.rb
Normal file
30
app/graphql/loaders/record.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# references:
|
||||||
|
# https://github.com/Shopify/graphql-batch/blob/master/examples/record_loader.rb
|
||||||
|
|
||||||
|
module Loaders
|
||||||
|
class Record < GraphQL::Batch::Loader
|
||||||
|
def initialize(model, column: model.primary_key, where: nil)
|
||||||
|
@model = model
|
||||||
|
@column = column.to_s
|
||||||
|
@column_type = model.type_for_attribute(@column)
|
||||||
|
@where = where
|
||||||
|
end
|
||||||
|
|
||||||
|
def load(key)
|
||||||
|
super(@column_type.cast(key))
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(keys)
|
||||||
|
query(keys).each { |record| fulfill(record.public_send(@column), record) }
|
||||||
|
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def query(keys)
|
||||||
|
scope = @model
|
||||||
|
scope = scope.where(@where) if @where
|
||||||
|
scope.where(@column => keys)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
app/graphql/mutations/base_mutation.rb
Normal file
4
app/graphql/mutations/base_mutation.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Mutations
|
||||||
|
class BaseMutation < GraphQL::Schema::Mutation
|
||||||
|
end
|
||||||
|
end
|
585
app/graphql/schema.graphql
Normal file
585
app/graphql/schema.graphql
Normal file
|
@ -0,0 +1,585 @@
|
||||||
|
type Avis {
|
||||||
|
answer: String
|
||||||
|
attachmentUrl: URL
|
||||||
|
createdAt: ISO8601DateTime!
|
||||||
|
email: String!
|
||||||
|
id: ID!
|
||||||
|
question: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CarteChamp implements Champ {
|
||||||
|
geoAreas: [GeoArea!]!
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChampDescriptor {
|
||||||
|
description: String
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
required: Boolean!
|
||||||
|
type: TypeDeChamp!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckboxChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
value: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
GeoJSON coordinates
|
||||||
|
"""
|
||||||
|
scalar Coordinates
|
||||||
|
|
||||||
|
type DateChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
value: ISO8601DateTime
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecimalNumberChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
value: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Une demarche
|
||||||
|
"""
|
||||||
|
type Demarche {
|
||||||
|
annotationDescriptors: [ChampDescriptor!]!
|
||||||
|
archivedAt: ISO8601DateTime
|
||||||
|
champDescriptors: [ChampDescriptor!]!
|
||||||
|
createdAt: ISO8601DateTime!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Déscription de la démarche.
|
||||||
|
"""
|
||||||
|
description: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Liste de tous les dossiers d'une démarche.
|
||||||
|
"""
|
||||||
|
dossiers(
|
||||||
|
"""
|
||||||
|
Returns the elements in the list that come after the specified cursor.
|
||||||
|
"""
|
||||||
|
after: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns the elements in the list that come before the specified cursor.
|
||||||
|
"""
|
||||||
|
before: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns the first _n_ elements from the list.
|
||||||
|
"""
|
||||||
|
first: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filtrer les dossiers par ID.
|
||||||
|
"""
|
||||||
|
ids: [ID!]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns the last _n_ elements from the list.
|
||||||
|
"""
|
||||||
|
last: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Dossiers crées depuis la date.
|
||||||
|
"""
|
||||||
|
since: ISO8601DateTime
|
||||||
|
): DossierConnection!
|
||||||
|
id: ID!
|
||||||
|
instructeurs: [Profile!]!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Le numero de la démarche.
|
||||||
|
"""
|
||||||
|
number: ID!
|
||||||
|
state: DemarcheState!
|
||||||
|
title: String!
|
||||||
|
updatedAt: ISO8601DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DemarcheState {
|
||||||
|
"""
|
||||||
|
Archivée
|
||||||
|
"""
|
||||||
|
archivee
|
||||||
|
|
||||||
|
"""
|
||||||
|
Brouillon
|
||||||
|
"""
|
||||||
|
brouillon
|
||||||
|
|
||||||
|
"""
|
||||||
|
Publiée
|
||||||
|
"""
|
||||||
|
publiee
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Un dossier
|
||||||
|
"""
|
||||||
|
type Dossier {
|
||||||
|
annotations: [Champ!]!
|
||||||
|
archived: Boolean!
|
||||||
|
avis: [Avis!]!
|
||||||
|
champs: [Champ!]!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Date de dépôt.
|
||||||
|
"""
|
||||||
|
datePassageEnConstruction: ISO8601DateTime!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Date de passage en instruction.
|
||||||
|
"""
|
||||||
|
datePassageEnInstruction: ISO8601DateTime
|
||||||
|
|
||||||
|
"""
|
||||||
|
Date de traitement.
|
||||||
|
"""
|
||||||
|
dateTraitement: ISO8601DateTime
|
||||||
|
id: ID!
|
||||||
|
instructeurs: [Profile!]!
|
||||||
|
messages: [Message!]!
|
||||||
|
motivation: String
|
||||||
|
motivationAttachmentUrl: URL
|
||||||
|
|
||||||
|
"""
|
||||||
|
Le numero du dossier.
|
||||||
|
"""
|
||||||
|
number: ID!
|
||||||
|
|
||||||
|
"""
|
||||||
|
L'état du dossier.
|
||||||
|
"""
|
||||||
|
state: DossierState!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Date de dernière mise à jour.
|
||||||
|
"""
|
||||||
|
updatedAt: ISO8601DateTime!
|
||||||
|
usager: Profile!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
The connection type for Dossier.
|
||||||
|
"""
|
||||||
|
type DossierConnection {
|
||||||
|
"""
|
||||||
|
A list of edges.
|
||||||
|
"""
|
||||||
|
edges: [DossierEdge]
|
||||||
|
|
||||||
|
"""
|
||||||
|
A list of nodes.
|
||||||
|
"""
|
||||||
|
nodes: [Dossier]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Information to aid in pagination.
|
||||||
|
"""
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
An edge in a connection.
|
||||||
|
"""
|
||||||
|
type DossierEdge {
|
||||||
|
"""
|
||||||
|
A cursor for use in pagination.
|
||||||
|
"""
|
||||||
|
cursor: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
The item at the end of the edge.
|
||||||
|
"""
|
||||||
|
node: Dossier
|
||||||
|
}
|
||||||
|
|
||||||
|
type DossierLinkChamp implements Champ {
|
||||||
|
dossier: Dossier
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DossierState {
|
||||||
|
"""
|
||||||
|
Accepté
|
||||||
|
"""
|
||||||
|
accepte
|
||||||
|
|
||||||
|
"""
|
||||||
|
En construction
|
||||||
|
"""
|
||||||
|
en_construction
|
||||||
|
|
||||||
|
"""
|
||||||
|
En instruction
|
||||||
|
"""
|
||||||
|
en_instruction
|
||||||
|
|
||||||
|
"""
|
||||||
|
Refusé
|
||||||
|
"""
|
||||||
|
refuse
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sans suite
|
||||||
|
"""
|
||||||
|
sans_suite
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GeoArea {
|
||||||
|
geometry: GeoJSON!
|
||||||
|
id: ID!
|
||||||
|
source: GeoAreaSource!
|
||||||
|
}
|
||||||
|
|
||||||
|
enum GeoAreaSource {
|
||||||
|
"""
|
||||||
|
translation missing: fr.activerecord.attributes.geo_area.source.cadastre
|
||||||
|
"""
|
||||||
|
cadastre
|
||||||
|
|
||||||
|
"""
|
||||||
|
translation missing: fr.activerecord.attributes.geo_area.source.parcelle_agricole
|
||||||
|
"""
|
||||||
|
parcelle_agricole
|
||||||
|
|
||||||
|
"""
|
||||||
|
translation missing: fr.activerecord.attributes.geo_area.source.quartier_prioritaire
|
||||||
|
"""
|
||||||
|
quartier_prioritaire
|
||||||
|
|
||||||
|
"""
|
||||||
|
translation missing: fr.activerecord.attributes.geo_area.source.selection_utilisateur
|
||||||
|
"""
|
||||||
|
selection_utilisateur
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeoJSON {
|
||||||
|
coordinates: Coordinates!
|
||||||
|
type: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
An ISO 8601-encoded datetime
|
||||||
|
"""
|
||||||
|
scalar ISO8601DateTime
|
||||||
|
|
||||||
|
type IntegerNumberChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
value: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedDropDownListChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
primaryValue: String
|
||||||
|
secondaryValue: String
|
||||||
|
stringValue: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message {
|
||||||
|
attachmentUrl: URL
|
||||||
|
body: String!
|
||||||
|
createdAt: ISO8601DateTime!
|
||||||
|
email: String!
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultipleDropDownListChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
values: [String!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Information about pagination in a connection.
|
||||||
|
"""
|
||||||
|
type PageInfo {
|
||||||
|
"""
|
||||||
|
When paginating forwards, the cursor to continue.
|
||||||
|
"""
|
||||||
|
endCursor: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
When paginating forwards, are there more items?
|
||||||
|
"""
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
|
||||||
|
"""
|
||||||
|
When paginating backwards, are there more items?
|
||||||
|
"""
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
|
||||||
|
"""
|
||||||
|
When paginating backwards, the cursor to continue.
|
||||||
|
"""
|
||||||
|
startCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParcelleCadastrale implements GeoArea {
|
||||||
|
codeArr: String!
|
||||||
|
codeCom: String!
|
||||||
|
codeDep: String!
|
||||||
|
feuille: Int!
|
||||||
|
geometry: GeoJSON!
|
||||||
|
id: ID!
|
||||||
|
nomCom: String!
|
||||||
|
numero: String!
|
||||||
|
section: String!
|
||||||
|
source: GeoAreaSource!
|
||||||
|
surfaceIntersection: Float!
|
||||||
|
surfaceParcelle: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonneMorale {
|
||||||
|
adresse: String!
|
||||||
|
codeInseeLocalite: String!
|
||||||
|
codePostal: String!
|
||||||
|
complementAdresse: String!
|
||||||
|
libelleNaf: String!
|
||||||
|
localite: String!
|
||||||
|
naf: String!
|
||||||
|
nomVoie: String!
|
||||||
|
numeroVoie: String!
|
||||||
|
siegeSocial: String!
|
||||||
|
siret: String!
|
||||||
|
typeVoie: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PieceJustificativeChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
url: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile {
|
||||||
|
email: String!
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
|
type QuartierPrioritaire implements GeoArea {
|
||||||
|
code: String!
|
||||||
|
commune: String!
|
||||||
|
geometry: GeoJSON!
|
||||||
|
id: ID!
|
||||||
|
nom: String!
|
||||||
|
source: GeoAreaSource!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
"""
|
||||||
|
Informations concernant une démarche.
|
||||||
|
"""
|
||||||
|
demarche(
|
||||||
|
"""
|
||||||
|
Numéro de la démarche.
|
||||||
|
"""
|
||||||
|
number: ID!
|
||||||
|
): Demarche!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Informations sur un dossier d'une démarche.
|
||||||
|
"""
|
||||||
|
dossier(
|
||||||
|
"""
|
||||||
|
Numéro du dossier.
|
||||||
|
"""
|
||||||
|
number: ID!
|
||||||
|
): Dossier!
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepetitionChamp implements Champ {
|
||||||
|
champs: [Champ!]!
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectionUtilisateur implements GeoArea {
|
||||||
|
geometry: GeoJSON!
|
||||||
|
id: ID!
|
||||||
|
source: GeoAreaSource!
|
||||||
|
}
|
||||||
|
|
||||||
|
type SiretChamp implements Champ {
|
||||||
|
etablissement: PersonneMorale
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextChamp implements Champ {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
stringValue: String
|
||||||
|
value: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TypeDeChamp {
|
||||||
|
"""
|
||||||
|
Adresse
|
||||||
|
"""
|
||||||
|
address
|
||||||
|
|
||||||
|
"""
|
||||||
|
Carte
|
||||||
|
"""
|
||||||
|
carte
|
||||||
|
|
||||||
|
"""
|
||||||
|
Case à cocher
|
||||||
|
"""
|
||||||
|
checkbox
|
||||||
|
|
||||||
|
"""
|
||||||
|
Civilité
|
||||||
|
"""
|
||||||
|
civilite
|
||||||
|
|
||||||
|
"""
|
||||||
|
Date
|
||||||
|
"""
|
||||||
|
date
|
||||||
|
|
||||||
|
"""
|
||||||
|
Date et Heure
|
||||||
|
"""
|
||||||
|
datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
Nombre décimal
|
||||||
|
"""
|
||||||
|
decimal_number
|
||||||
|
|
||||||
|
"""
|
||||||
|
Départements
|
||||||
|
"""
|
||||||
|
departements
|
||||||
|
|
||||||
|
"""
|
||||||
|
Lien vers un autre dossier
|
||||||
|
"""
|
||||||
|
dossier_link
|
||||||
|
|
||||||
|
"""
|
||||||
|
Menu déroulant
|
||||||
|
"""
|
||||||
|
drop_down_list
|
||||||
|
|
||||||
|
"""
|
||||||
|
Email
|
||||||
|
"""
|
||||||
|
email
|
||||||
|
|
||||||
|
"""
|
||||||
|
Engagement
|
||||||
|
"""
|
||||||
|
engagement
|
||||||
|
|
||||||
|
"""
|
||||||
|
Explication
|
||||||
|
"""
|
||||||
|
explication
|
||||||
|
|
||||||
|
"""
|
||||||
|
Titre de section
|
||||||
|
"""
|
||||||
|
header_section
|
||||||
|
|
||||||
|
"""
|
||||||
|
Nombre entier
|
||||||
|
"""
|
||||||
|
integer_number
|
||||||
|
|
||||||
|
"""
|
||||||
|
Deux menus déroulants liés
|
||||||
|
"""
|
||||||
|
linked_drop_down_list
|
||||||
|
|
||||||
|
"""
|
||||||
|
Menu déroulant à choix multiples
|
||||||
|
"""
|
||||||
|
multiple_drop_down_list
|
||||||
|
|
||||||
|
"""
|
||||||
|
Nombre entier
|
||||||
|
"""
|
||||||
|
number
|
||||||
|
|
||||||
|
"""
|
||||||
|
Pays
|
||||||
|
"""
|
||||||
|
pays
|
||||||
|
|
||||||
|
"""
|
||||||
|
Téléphone
|
||||||
|
"""
|
||||||
|
phone
|
||||||
|
|
||||||
|
"""
|
||||||
|
Pièce justificative
|
||||||
|
"""
|
||||||
|
piece_justificative
|
||||||
|
|
||||||
|
"""
|
||||||
|
Régions
|
||||||
|
"""
|
||||||
|
regions
|
||||||
|
|
||||||
|
"""
|
||||||
|
Bloc répétable
|
||||||
|
"""
|
||||||
|
repetition
|
||||||
|
|
||||||
|
"""
|
||||||
|
SIRET
|
||||||
|
"""
|
||||||
|
siret
|
||||||
|
|
||||||
|
"""
|
||||||
|
Texte
|
||||||
|
"""
|
||||||
|
text
|
||||||
|
|
||||||
|
"""
|
||||||
|
Zone de texte
|
||||||
|
"""
|
||||||
|
textarea
|
||||||
|
|
||||||
|
"""
|
||||||
|
Oui/Non
|
||||||
|
"""
|
||||||
|
yes_no
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
A valid URL, transported as a string
|
||||||
|
"""
|
||||||
|
scalar URL
|
12
app/graphql/types/avis_type.rb
Normal file
12
app/graphql/types/avis_type.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module Types
|
||||||
|
class AvisType < Types::BaseObject
|
||||||
|
global_id_field :id
|
||||||
|
field :email, String, null: false
|
||||||
|
field :question, String, null: false, method: :introduction
|
||||||
|
field :answer, String, null: true
|
||||||
|
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||||
|
field :attachment_url, Types::URL, null: true, extensions: [
|
||||||
|
{ Extensions::Attachment => { attachment: :piece_justificative_file } }
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
4
app/graphql/types/base_enum.rb
Normal file
4
app/graphql/types/base_enum.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Types
|
||||||
|
class BaseEnum < GraphQL::Schema::Enum
|
||||||
|
end
|
||||||
|
end
|
4
app/graphql/types/base_input_object.rb
Normal file
4
app/graphql/types/base_input_object.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Types
|
||||||
|
class BaseInputObject < GraphQL::Schema::InputObject
|
||||||
|
end
|
||||||
|
end
|
5
app/graphql/types/base_interface.rb
Normal file
5
app/graphql/types/base_interface.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
module Types
|
||||||
|
module BaseInterface
|
||||||
|
include GraphQL::Schema::Interface
|
||||||
|
end
|
||||||
|
end
|
25
app/graphql/types/base_object.rb
Normal file
25
app/graphql/types/base_object.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
module Types
|
||||||
|
class BaseObject < GraphQL::Schema::Object
|
||||||
|
def self.authorized_demarche?(demarche, context)
|
||||||
|
# We are caching authorization logic because it is called for each node
|
||||||
|
# of the requested graph and can be expensive. Context is reset per request so it is safe.
|
||||||
|
context[:authorized] ||= {}
|
||||||
|
if context[:authorized][demarche.id]
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
administrateur = demarche.administrateurs.find do |administrateur|
|
||||||
|
if context[:token]
|
||||||
|
administrateur.valid_api_token?(context[:token])
|
||||||
|
else
|
||||||
|
administrateur.id == context[:administrateur_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if administrateur && Flipper.enabled?(:administrateur_graphql, administrateur.user)
|
||||||
|
context[:authorized][demarche.id] = true
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
app/graphql/types/base_scalar.rb
Normal file
4
app/graphql/types/base_scalar.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Types
|
||||||
|
class BaseScalar < GraphQL::Schema::Scalar
|
||||||
|
end
|
||||||
|
end
|
4
app/graphql/types/base_union.rb
Normal file
4
app/graphql/types/base_union.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Types
|
||||||
|
class BaseUnion < GraphQL::Schema::Union
|
||||||
|
end
|
||||||
|
end
|
17
app/graphql/types/champ_descriptor_type.rb
Normal file
17
app/graphql/types/champ_descriptor_type.rb
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module Types
|
||||||
|
class ChampDescriptorType < Types::BaseObject
|
||||||
|
class TypeDeChampType < Types::BaseEnum
|
||||||
|
TypeDeChamp.type_champs.each do |symbol_name, string_name|
|
||||||
|
value(string_name,
|
||||||
|
I18n.t(symbol_name, scope: [:activerecord, :attributes, :type_de_champ, :type_champs]),
|
||||||
|
value: symbol_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
global_id_field :id
|
||||||
|
field :type, TypeDeChampType, null: false, method: :type_champ
|
||||||
|
field :label, String, null: false, method: :libelle
|
||||||
|
field :description, String, null: true
|
||||||
|
field :required, Boolean, null: false, method: :mandatory?
|
||||||
|
end
|
||||||
|
end
|
40
app/graphql/types/champ_type.rb
Normal file
40
app/graphql/types/champ_type.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
module Types
|
||||||
|
module ChampType
|
||||||
|
include Types::BaseInterface
|
||||||
|
|
||||||
|
global_id_field :id
|
||||||
|
field :label, String, null: false, method: :libelle
|
||||||
|
field :string_value, String, null: true, method: :for_api_v2
|
||||||
|
|
||||||
|
definition_methods do
|
||||||
|
def resolve_type(object, context)
|
||||||
|
case object
|
||||||
|
when ::Champs::EngagementChamp, ::Champs::YesNoChamp, ::Champs::CheckboxChamp
|
||||||
|
Types::Champs::CheckboxChampType
|
||||||
|
when ::Champs::DateChamp, ::Champs::DatetimeChamp
|
||||||
|
Types::Champs::DateChampType
|
||||||
|
when ::Champs::DossierLinkChamp
|
||||||
|
Types::Champs::DossierLinkChampType
|
||||||
|
when ::Champs::PieceJustificativeChamp
|
||||||
|
Types::Champs::PieceJustificativeChampType
|
||||||
|
when ::Champs::CarteChamp
|
||||||
|
Types::Champs::CarteChampType
|
||||||
|
when ::Champs::NumberChamp, ::Champs::IntegerNumberChamp
|
||||||
|
Types::Champs::IntegerNumberChampType
|
||||||
|
when ::Champs::DecimalNumberChamp
|
||||||
|
Types::Champs::DecimalNumberChampType
|
||||||
|
when ::Champs::SiretChamp
|
||||||
|
Types::Champs::SiretChampType
|
||||||
|
when ::Champs::RepetitionChamp
|
||||||
|
Types::Champs::RepetitionChampType
|
||||||
|
when ::Champs::MultipleDropDownListChamp
|
||||||
|
Types::Champs::MultipleDropDownListChampType
|
||||||
|
when ::Champs::LinkedDropDownListChamp
|
||||||
|
Types::Champs::LinkedDropDownListChampType
|
||||||
|
else
|
||||||
|
Types::Champs::TextChampType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
11
app/graphql/types/champs/carte_champ_type.rb
Normal file
11
app/graphql/types/champs/carte_champ_type.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Types::Champs
|
||||||
|
class CarteChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :geo_areas, [Types::GeoAreaType], null: false
|
||||||
|
|
||||||
|
def geo_areas
|
||||||
|
Loaders::Association.for(Champs::CarteChamp, :geo_areas).load(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
16
app/graphql/types/champs/checkbox_champ_type.rb
Normal file
16
app/graphql/types/champs/checkbox_champ_type.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module Types::Champs
|
||||||
|
class CheckboxChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :value, Boolean, null: false
|
||||||
|
|
||||||
|
def value
|
||||||
|
case object.value
|
||||||
|
when 'true', 'on', '1'
|
||||||
|
true
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/graphql/types/champs/date_champ_type.rb
Normal file
13
app/graphql/types/champs/date_champ_type.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Types::Champs
|
||||||
|
class DateChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :value, GraphQL::Types::ISO8601DateTime, null: true
|
||||||
|
|
||||||
|
def value
|
||||||
|
if object.value.present?
|
||||||
|
Time.zone.parse(object.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/graphql/types/champs/decimal_number_champ_type.rb
Normal file
13
app/graphql/types/champs/decimal_number_champ_type.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Types::Champs
|
||||||
|
class DecimalNumberChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :value, Float, null: true
|
||||||
|
|
||||||
|
def value
|
||||||
|
if object.value.present?
|
||||||
|
object.value.to_f
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/graphql/types/champs/dossier_link_champ_type.rb
Normal file
13
app/graphql/types/champs/dossier_link_champ_type.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Types::Champs
|
||||||
|
class DossierLinkChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :dossier, Types::DossierType, null: true
|
||||||
|
|
||||||
|
def dossier
|
||||||
|
if object.value.present?
|
||||||
|
Loaders::Record.for(Dossier).load(object.value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/graphql/types/champs/integer_number_champ_type.rb
Normal file
13
app/graphql/types/champs/integer_number_champ_type.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Types::Champs
|
||||||
|
class IntegerNumberChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :value, Int, null: true
|
||||||
|
|
||||||
|
def value
|
||||||
|
if object.value.present?
|
||||||
|
object.value.to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
module Types::Champs
|
||||||
|
class LinkedDropDownListChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :primary_value, String, null: true
|
||||||
|
field :secondary_value, String, null: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,7 @@
|
||||||
|
module Types::Champs
|
||||||
|
class MultipleDropDownListChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :values, [String], null: false, method: :selected_options
|
||||||
|
end
|
||||||
|
end
|
10
app/graphql/types/champs/piece_justificative_champ_type.rb
Normal file
10
app/graphql/types/champs/piece_justificative_champ_type.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module Types::Champs
|
||||||
|
class PieceJustificativeChampType < Types::BaseObject
|
||||||
|
include Rails.application.routes.url_helpers
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :url, Types::URL, null: true, extensions: [
|
||||||
|
{ Extensions::Attachment => { attachment: :piece_justificative_file } }
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
11
app/graphql/types/champs/repetition_champ_type.rb
Normal file
11
app/graphql/types/champs/repetition_champ_type.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module Types::Champs
|
||||||
|
class RepetitionChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :champs, [Types::ChampType], null: false
|
||||||
|
|
||||||
|
def champs
|
||||||
|
Loaders::Association.for(object.class, :champs).load(object)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
13
app/graphql/types/champs/siret_champ_type.rb
Normal file
13
app/graphql/types/champs/siret_champ_type.rb
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module Types::Champs
|
||||||
|
class SiretChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :etablissement, Types::PersonneMoraleType, null: true
|
||||||
|
|
||||||
|
def etablissement
|
||||||
|
if object.etablissement_id.present?
|
||||||
|
Loaders::Record.for(Etablissement).load(object.etablissement_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
7
app/graphql/types/champs/text_champ_type.rb
Normal file
7
app/graphql/types/champs/text_champ_type.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module Types::Champs
|
||||||
|
class TextChampType < Types::BaseObject
|
||||||
|
implements Types::ChampType
|
||||||
|
|
||||||
|
field :value, String, null: true
|
||||||
|
end
|
||||||
|
end
|
57
app/graphql/types/demarche_type.rb
Normal file
57
app/graphql/types/demarche_type.rb
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
module Types
|
||||||
|
class DemarcheType < Types::BaseObject
|
||||||
|
class DemarcheState < Types::BaseEnum
|
||||||
|
Procedure.aasm.states.reject { |state| state.name == :hidden }.each do |state|
|
||||||
|
value(state.name.to_s, state.display_name, value: state.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
description "Une demarche"
|
||||||
|
|
||||||
|
global_id_field :id
|
||||||
|
field :number, ID, "Le numero de la démarche.", null: false, method: :id
|
||||||
|
field :title, String, null: false, method: :libelle
|
||||||
|
field :description, String, "Déscription de la démarche.", null: false
|
||||||
|
field :state, DemarcheState, null: false
|
||||||
|
|
||||||
|
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||||
|
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||||
|
field :archived_at, GraphQL::Types::ISO8601DateTime, null: true
|
||||||
|
|
||||||
|
field :instructeurs, [Types::ProfileType], null: false
|
||||||
|
|
||||||
|
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do
|
||||||
|
argument :ids, [ID], required: false, description: "Filtrer les dossiers par ID."
|
||||||
|
argument :since, GraphQL::Types::ISO8601DateTime, required: false, description: "Dossiers crées depuis la date."
|
||||||
|
end
|
||||||
|
|
||||||
|
field :champ_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ
|
||||||
|
field :annotation_descriptors, [Types::ChampDescriptorType], null: false, method: :types_de_champ_private
|
||||||
|
|
||||||
|
def state
|
||||||
|
object.aasm.current_state
|
||||||
|
end
|
||||||
|
|
||||||
|
def instructeurs
|
||||||
|
Loaders::Association.for(Procedure, :instructeurs).load(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dossiers(ids: nil, since: nil)
|
||||||
|
dossiers = object.dossiers.for_api_v2
|
||||||
|
|
||||||
|
if ids.present?
|
||||||
|
dossiers = dossiers.where(id: ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
if since.present?
|
||||||
|
dossiers = dossiers.since(since)
|
||||||
|
end
|
||||||
|
|
||||||
|
dossiers
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.authorized?(object, context)
|
||||||
|
authorized_demarche?(object, context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
68
app/graphql/types/dossier_type.rb
Normal file
68
app/graphql/types/dossier_type.rb
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
module Types
|
||||||
|
class DossierType < Types::BaseObject
|
||||||
|
class DossierState < Types::BaseEnum
|
||||||
|
Dossier.aasm.states.reject { |state| state.name == :brouillon }.each do |state|
|
||||||
|
value(state.name.to_s, state.display_name, value: state.name.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
description "Un dossier"
|
||||||
|
|
||||||
|
global_id_field :id
|
||||||
|
field :number, ID, "Le numero du dossier.", null: false, method: :id
|
||||||
|
field :state, DossierState, "L'état du dossier.", null: false
|
||||||
|
field :updated_at, GraphQL::Types::ISO8601DateTime, "Date de dernière mise à jour.", null: false
|
||||||
|
|
||||||
|
field :date_passage_en_construction, GraphQL::Types::ISO8601DateTime, "Date de dépôt.", null: false, method: :en_construction_at
|
||||||
|
field :date_passage_en_instruction, GraphQL::Types::ISO8601DateTime, "Date de passage en instruction.", null: true, method: :en_instruction_at
|
||||||
|
field :date_traitement, GraphQL::Types::ISO8601DateTime, "Date de traitement.", null: true, method: :processed_at
|
||||||
|
|
||||||
|
field :archived, Boolean, null: false
|
||||||
|
|
||||||
|
field :motivation, String, null: true
|
||||||
|
field :motivation_attachment_url, Types::URL, null: true, extensions: [
|
||||||
|
{ Extensions::Attachment => { attachment: :justificatif_motivation } }
|
||||||
|
]
|
||||||
|
|
||||||
|
field :usager, Types::ProfileType, null: false
|
||||||
|
field :instructeurs, [Types::ProfileType], null: false
|
||||||
|
|
||||||
|
field :champs, [Types::ChampType], null: false
|
||||||
|
field :annotations, [Types::ChampType], null: false
|
||||||
|
|
||||||
|
field :messages, [Types::MessageType], null: false
|
||||||
|
field :avis, [Types::AvisType], null: false
|
||||||
|
|
||||||
|
def state
|
||||||
|
object.state
|
||||||
|
end
|
||||||
|
|
||||||
|
def usager
|
||||||
|
Loaders::Record.for(User).load(object.user_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def instructeurs
|
||||||
|
Loaders::Association.for(object.class, :followers_instructeurs).load(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def messages
|
||||||
|
Loaders::Association.for(object.class, commentaires: [:instructeur, :user]).load(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avis
|
||||||
|
Loaders::Association.for(object.class, avis: [:instructeur, :claimant]).load(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def champs
|
||||||
|
Loaders::Association.for(object.class, :champs).load(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def annotations
|
||||||
|
Loaders::Association.for(object.class, :champs_private).load(object)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.authorized?(object, context)
|
||||||
|
authorized_demarche?(object.procedure, context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
app/graphql/types/geo_area_type.rb
Normal file
30
app/graphql/types/geo_area_type.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
module Types
|
||||||
|
module GeoAreaType
|
||||||
|
include Types::BaseInterface
|
||||||
|
|
||||||
|
class GeoAreaSource < Types::BaseEnum
|
||||||
|
GeoArea.sources.each do |symbol_name, string_name|
|
||||||
|
value(string_name,
|
||||||
|
I18n.t(symbol_name, scope: [:activerecord, :attributes, :geo_area, :source]),
|
||||||
|
value: symbol_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
global_id_field :id
|
||||||
|
field :source, GeoAreaSource, null: false
|
||||||
|
field :geometry, Types::GeoJSON, null: false
|
||||||
|
|
||||||
|
definition_methods do
|
||||||
|
def resolve_type(object, context)
|
||||||
|
case object.source
|
||||||
|
when GeoArea.sources.fetch(:cadastre)
|
||||||
|
Types::GeoAreas::ParcelleCadastraleType
|
||||||
|
when GeoArea.sources.fetch(:quartier_prioritaire)
|
||||||
|
Types::GeoAreas::QuartierPrioritaireType
|
||||||
|
when GeoArea.sources.fetch(:selection_utilisateur)
|
||||||
|
Types::GeoAreas::SelectionUtilisateurType
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
15
app/graphql/types/geo_areas/parcelle_cadastrale_type.rb
Normal file
15
app/graphql/types/geo_areas/parcelle_cadastrale_type.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Types::GeoAreas
|
||||||
|
class ParcelleCadastraleType < Types::BaseObject
|
||||||
|
implements Types::GeoAreaType
|
||||||
|
|
||||||
|
field :surface_intersection, Float, null: false
|
||||||
|
field :surface_parcelle, Float, null: false
|
||||||
|
field :numero, String, null: false
|
||||||
|
field :feuille, Int, null: false
|
||||||
|
field :section, String, null: false
|
||||||
|
field :code_dep, String, null: false
|
||||||
|
field :nom_com, String, null: false
|
||||||
|
field :code_com, String, null: false
|
||||||
|
field :code_arr, String, null: false
|
||||||
|
end
|
||||||
|
end
|
9
app/graphql/types/geo_areas/quartier_prioritaire_type.rb
Normal file
9
app/graphql/types/geo_areas/quartier_prioritaire_type.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
module Types::GeoAreas
|
||||||
|
class QuartierPrioritaireType < Types::BaseObject
|
||||||
|
implements Types::GeoAreaType
|
||||||
|
|
||||||
|
field :code, String, null: false
|
||||||
|
field :nom, String, null: false
|
||||||
|
field :commune, String, null: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
module Types::GeoAreas
|
||||||
|
class SelectionUtilisateurType < Types::BaseObject
|
||||||
|
implements Types::GeoAreaType
|
||||||
|
end
|
||||||
|
end
|
14
app/graphql/types/geo_json.rb
Normal file
14
app/graphql/types/geo_json.rb
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
module Types
|
||||||
|
class GeoJSON < Types::BaseObject
|
||||||
|
class CoordinatesType < Types::BaseScalar
|
||||||
|
description "GeoJSON coordinates"
|
||||||
|
|
||||||
|
def self.coerce_result(ruby_value, context)
|
||||||
|
ruby_value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
field :type, String, null: false
|
||||||
|
field :coordinates, CoordinatesType, null: false
|
||||||
|
end
|
||||||
|
end
|
15
app/graphql/types/message_type.rb
Normal file
15
app/graphql/types/message_type.rb
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
module Types
|
||||||
|
class MessageType < Types::BaseObject
|
||||||
|
global_id_field :id
|
||||||
|
field :email, String, null: false
|
||||||
|
field :body, String, null: false
|
||||||
|
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
|
||||||
|
field :attachment_url, Types::URL, null: true, extensions: [
|
||||||
|
{ Extensions::Attachment => { attachment: :piece_jointe } }
|
||||||
|
]
|
||||||
|
|
||||||
|
def body
|
||||||
|
object.body.nil? ? "" : object.body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
4
app/graphql/types/mutation_type.rb
Normal file
4
app/graphql/types/mutation_type.rb
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module Types
|
||||||
|
class MutationType < Types::BaseObject
|
||||||
|
end
|
||||||
|
end
|
16
app/graphql/types/personne_morale_type.rb
Normal file
16
app/graphql/types/personne_morale_type.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module Types
|
||||||
|
class PersonneMoraleType < Types::BaseObject
|
||||||
|
field :siret, String, null: false
|
||||||
|
field :siege_social, String, null: false
|
||||||
|
field :naf, String, null: false
|
||||||
|
field :libelle_naf, String, null: false
|
||||||
|
field :adresse, String, null: false
|
||||||
|
field :numero_voie, String, null: false
|
||||||
|
field :type_voie, String, null: false
|
||||||
|
field :nom_voie, String, null: false
|
||||||
|
field :complement_adresse, String, null: false
|
||||||
|
field :code_postal, String, null: false
|
||||||
|
field :localite, String, null: false
|
||||||
|
field :code_insee_localite, String, null: false
|
||||||
|
end
|
||||||
|
end
|
6
app/graphql/types/profile_type.rb
Normal file
6
app/graphql/types/profile_type.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module Types
|
||||||
|
class ProfileType < Types::BaseObject
|
||||||
|
global_id_field :id
|
||||||
|
field :email, String, null: false
|
||||||
|
end
|
||||||
|
end
|
27
app/graphql/types/query_type.rb
Normal file
27
app/graphql/types/query_type.rb
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
module Types
|
||||||
|
class QueryType < Types::BaseObject
|
||||||
|
field :demarche, DemarcheType, null: false, description: "Informations concernant une démarche." do
|
||||||
|
argument :number, ID, "Numéro de la démarche.", required: true
|
||||||
|
end
|
||||||
|
|
||||||
|
field :dossier, DossierType, null: false, description: "Informations sur un dossier d'une démarche." do
|
||||||
|
argument :number, ID, "Numéro du dossier.", required: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def demarche(number:)
|
||||||
|
Procedure.for_api_v2.find(number)
|
||||||
|
rescue => e
|
||||||
|
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||||
|
end
|
||||||
|
|
||||||
|
def dossier(number:)
|
||||||
|
Dossier.for_api_v2.find(number)
|
||||||
|
rescue => e
|
||||||
|
raise GraphQL::ExecutionError.new(e.message, extensions: { code: :not_found })
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.accessible?(context)
|
||||||
|
context[:token] || context[:administrateur_id]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
18
app/graphql/types/url.rb
Normal file
18
app/graphql/types/url.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
module Types
|
||||||
|
class URL < Types::BaseScalar
|
||||||
|
description "A valid URL, transported as a string"
|
||||||
|
|
||||||
|
def self.coerce_input(input_value, context)
|
||||||
|
url = URI.parse(input_value)
|
||||||
|
if url.is_a?(URI::HTTP) || url.is_a?(URI::HTTPS)
|
||||||
|
url
|
||||||
|
else
|
||||||
|
raise GraphQL::CoercionError, "#{input_value.inspect} is not a valid URL"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.coerce_result(ruby_value, context)
|
||||||
|
ruby_value.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,3 +1,19 @@
|
||||||
class ApplicationRecord < ActiveRecord::Base
|
class ApplicationRecord < ActiveRecord::Base
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
|
|
||||||
|
def self.record_from_typed_id(id)
|
||||||
|
class_name, record_id = GraphQL::Schema::UniqueWithinType.decode(id)
|
||||||
|
|
||||||
|
if defined?(class_name)
|
||||||
|
Object.const_get(class_name).find(record_id)
|
||||||
|
else
|
||||||
|
raise ActiveRecord::RecordNotFound, "Unexpected object: #{class_name}"
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
raise ActiveRecord::RecordNotFound, e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_typed_id
|
||||||
|
GraphQL::Schema::UniqueWithinType.encode(self.class.name, id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,6 +54,10 @@ class Champ < ApplicationRecord
|
||||||
value
|
value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def for_api_v2
|
||||||
|
to_s
|
||||||
|
end
|
||||||
|
|
||||||
def for_tag
|
def for_tag
|
||||||
value.present? ? value.to_s : ''
|
value.present? ? value.to_s : ''
|
||||||
end
|
end
|
||||||
|
@ -62,6 +66,10 @@ class Champ < ApplicationRecord
|
||||||
:value
|
:value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_typed_id
|
||||||
|
type_de_champ.to_typed_id
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def needs_dossier_id?
|
def needs_dossier_id?
|
||||||
|
|
|
@ -21,6 +21,10 @@ class Champs::YesNoChamp < Champ
|
||||||
value == 'true'
|
value == 'true'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def for_api_v2
|
||||||
|
true? ? 'true' : 'false'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def processed_value
|
def processed_value
|
||||||
|
|
|
@ -126,12 +126,12 @@ class Dossier < ApplicationRecord
|
||||||
champs_private: {
|
champs_private: {
|
||||||
etablissement: :champ,
|
etablissement: :champ,
|
||||||
type_de_champ: :drop_down_list
|
type_de_champ: :drop_down_list
|
||||||
}
|
},
|
||||||
|
procedure: :groupe_instructeurs
|
||||||
).order(en_construction_at: 'asc')
|
).order(en_construction_at: 'asc')
|
||||||
}
|
}
|
||||||
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
|
scope :en_cours, -> { not_archived.state_en_construction_ou_instruction }
|
||||||
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
|
scope :without_followers, -> { left_outer_joins(:follows).where(follows: { id: nil }) }
|
||||||
scope :followed_by, -> (instructeur) { joins(:follows).where(follows: { instructeur: instructeur }) }
|
|
||||||
scope :with_champs, -> { includes(champs: :type_de_champ) }
|
scope :with_champs, -> { includes(champs: :type_de_champ) }
|
||||||
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
|
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
|
||||||
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
|
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
|
||||||
|
@ -162,6 +162,32 @@ class Dossier < ApplicationRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :for_procedure, -> (procedure) { includes(:user, :groupe_instructeur).where(groupe_instructeurs: { procedure: procedure }) }
|
scope :for_procedure, -> (procedure) { includes(:user, :groupe_instructeur).where(groupe_instructeurs: { procedure: procedure }) }
|
||||||
|
scope :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) }
|
||||||
|
|
||||||
|
scope :with_notifications, -> do
|
||||||
|
# This scope is meant to be composed, typically with Instructeur.followed_dossiers, which means that the :follows table is already INNER JOINed;
|
||||||
|
# it will fail otherwise
|
||||||
|
|
||||||
|
# Relations passed to #or must be “structurally compatible”, i.e. query the same tables.
|
||||||
|
joined_dossiers = left_outer_joins(:champs, :champs_private, :avis, :commentaires)
|
||||||
|
|
||||||
|
updated_demandes = joined_dossiers
|
||||||
|
.where('champs.updated_at > follows.demande_seen_at')
|
||||||
|
|
||||||
|
# We join `:champs` twice, the second time with `has_many :champs_privates`. ActiveRecord generates the SQL: 'LEFT OUTER JOIN "champs" "champs_privates_dossiers" ON …'. We can then use this `champs_privates_dossiers` alias to disambiguate the table in this WHERE clause.
|
||||||
|
updated_annotations = joined_dossiers
|
||||||
|
.where('champs_privates_dossiers.updated_at > follows.annotations_privees_seen_at')
|
||||||
|
|
||||||
|
updated_avis = joined_dossiers
|
||||||
|
.where('avis.updated_at > follows.avis_seen_at')
|
||||||
|
|
||||||
|
updated_messagerie = joined_dossiers
|
||||||
|
.where('commentaires.updated_at > follows.messagerie_seen_at')
|
||||||
|
.where.not(commentaires: { email: OLD_CONTACT_EMAIL })
|
||||||
|
.where.not(commentaires: { email: CONTACT_EMAIL })
|
||||||
|
|
||||||
|
updated_demandes.or(updated_annotations).or(updated_avis).or(updated_messagerie).distinct
|
||||||
|
end
|
||||||
|
|
||||||
accepts_nested_attributes_for :individual
|
accepts_nested_attributes_for :individual
|
||||||
|
|
||||||
|
@ -447,7 +473,7 @@ class Dossier < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def spreadsheet_columns
|
def spreadsheet_columns
|
||||||
[
|
columns = [
|
||||||
['ID', id.to_s],
|
['ID', id.to_s],
|
||||||
['Email', user.email],
|
['Email', user.email],
|
||||||
['Civilité', individual&.gender],
|
['Civilité', individual&.gender],
|
||||||
|
@ -462,7 +488,13 @@ class Dossier < ApplicationRecord
|
||||||
['Traité le', :processed_at],
|
['Traité le', :processed_at],
|
||||||
['Motivation de la décision', :motivation],
|
['Motivation de la décision', :motivation],
|
||||||
['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
|
['Instructeurs', followers_instructeurs.map(&:email).join(' ')]
|
||||||
] + champs_for_export + annotations_for_export
|
]
|
||||||
|
|
||||||
|
if procedure.routee?
|
||||||
|
columns << ['Groupe instructeur', groupe_instructeur.label]
|
||||||
|
end
|
||||||
|
|
||||||
|
columns + champs_for_export + annotations_for_export
|
||||||
end
|
end
|
||||||
|
|
||||||
def champs_for_export
|
def champs_for_export
|
||||||
|
|
|
@ -109,66 +109,25 @@ class Instructeur < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications_for_procedure(procedure, state = :en_cours)
|
def notifications_for_procedure(procedure, scope)
|
||||||
dossiers = case state
|
procedure
|
||||||
when :termine
|
.defaut_groupe_instructeur.dossiers
|
||||||
procedure.defaut_groupe_instructeur.dossiers.termine
|
.send(scope) # :en_cours or :termine or :not_archived (or any other Dossier scope)
|
||||||
when :not_archived
|
.merge(followed_dossiers)
|
||||||
procedure.defaut_groupe_instructeur.dossiers.not_archived
|
.with_notifications
|
||||||
when :all
|
|
||||||
procedure.defaut_groupe_instructeur.dossiers
|
|
||||||
else
|
|
||||||
procedure.defaut_groupe_instructeur.dossiers.en_cours
|
|
||||||
end
|
|
||||||
|
|
||||||
dossiers_id_with_notifications(dossiers)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def notifications_per_procedure(state = :en_cours)
|
def procedures_with_notifications(scope)
|
||||||
dossiers = case state
|
dossiers = Dossier
|
||||||
when :termine
|
.send(scope) # :en_cours or :termine (or any other Dossier scope)
|
||||||
Dossier.termine
|
.merge(followed_dossiers)
|
||||||
when :not_archived
|
.with_notifications
|
||||||
Dossier.not_archived
|
|
||||||
else
|
|
||||||
Dossier.en_cours
|
|
||||||
end
|
|
||||||
|
|
||||||
Dossier.joins(:groupe_instructeur).where(id: dossiers_id_with_notifications(dossiers)).group('groupe_instructeurs.procedure_id').count
|
Procedure
|
||||||
end
|
.where(id: dossiers.joins(:groupe_instructeur)
|
||||||
|
.select('groupe_instructeurs.procedure_id')
|
||||||
def create_trusted_device_token
|
.distinct)
|
||||||
trusted_device_token = trusted_device_tokens.create
|
.distinct
|
||||||
trusted_device_token.token
|
|
||||||
end
|
|
||||||
|
|
||||||
def dossiers_id_with_notifications(dossiers)
|
|
||||||
dossiers = dossiers.followed_by(self)
|
|
||||||
|
|
||||||
updated_demandes = dossiers
|
|
||||||
.joins(:champs)
|
|
||||||
.where('champs.updated_at > follows.demande_seen_at')
|
|
||||||
|
|
||||||
updated_annotations = dossiers
|
|
||||||
.joins(:champs_private)
|
|
||||||
.where('champs.updated_at > follows.annotations_privees_seen_at')
|
|
||||||
|
|
||||||
updated_avis = dossiers
|
|
||||||
.joins(:avis)
|
|
||||||
.where('avis.updated_at > follows.avis_seen_at')
|
|
||||||
|
|
||||||
updated_messagerie = dossiers
|
|
||||||
.joins(:commentaires)
|
|
||||||
.where('commentaires.updated_at > follows.messagerie_seen_at')
|
|
||||||
.where.not(commentaires: { email: OLD_CONTACT_EMAIL })
|
|
||||||
.where.not(commentaires: { email: CONTACT_EMAIL })
|
|
||||||
|
|
||||||
[
|
|
||||||
updated_demandes,
|
|
||||||
updated_annotations,
|
|
||||||
updated_avis,
|
|
||||||
updated_messagerie
|
|
||||||
].flat_map { |query| query.distinct.ids }.uniq
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_tab_as_seen(dossier, tab)
|
def mark_tab_as_seen(dossier, tab)
|
||||||
|
@ -177,11 +136,6 @@ class Instructeur < ApplicationRecord
|
||||||
Follow.where(instructeur: self, dossier: dossier).update_all(attributes)
|
Follow.where(instructeur: self, dossier: dossier).update_all(attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
def young_login_token?
|
|
||||||
trusted_device_token = trusted_device_tokens.order(created_at: :desc).first
|
|
||||||
trusted_device_token&.token_young?
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_notification_data
|
def email_notification_data
|
||||||
groupe_instructeur_with_email_notifications
|
groupe_instructeur_with_email_notifications
|
||||||
.reduce([]) do |acc, groupe|
|
.reduce([]) do |acc, groupe|
|
||||||
|
@ -190,7 +144,7 @@ class Instructeur < ApplicationRecord
|
||||||
|
|
||||||
h = {
|
h = {
|
||||||
nb_en_construction: groupe.dossiers.en_construction.count,
|
nb_en_construction: groupe.dossiers.en_construction.count,
|
||||||
nb_notification: notifications_for_procedure(procedure, :all).count
|
nb_notification: notifications_for_procedure(procedure, :not_archived).count
|
||||||
}
|
}
|
||||||
|
|
||||||
if h[:nb_en_construction] > 0 || h[:nb_notification] > 0
|
if h[:nb_en_construction] > 0 || h[:nb_notification] > 0
|
||||||
|
@ -203,6 +157,16 @@ class Instructeur < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_trusted_device_token
|
||||||
|
trusted_device_token = trusted_device_tokens.create
|
||||||
|
trusted_device_token.token
|
||||||
|
end
|
||||||
|
|
||||||
|
def young_login_token?
|
||||||
|
trusted_device_token = trusted_device_tokens.order(created_at: :desc).first
|
||||||
|
trusted_device_token&.token_young?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def annotations_hash(demande, annotations_privees, avis, messagerie)
|
def annotations_hash(demande, annotations_privees, avis, messagerie)
|
||||||
|
|
|
@ -62,6 +62,10 @@ class Procedure < ApplicationRecord
|
||||||
accepte: 'accepte'
|
accepte: 'accepte'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope :for_api_v2, -> {
|
||||||
|
includes(administrateurs: :user)
|
||||||
|
}
|
||||||
|
|
||||||
validates :libelle, presence: true, allow_blank: false, allow_nil: false
|
validates :libelle, presence: true, allow_blank: false, allow_nil: false
|
||||||
validates :description, presence: true, allow_blank: false, allow_nil: false
|
validates :description, presence: true, allow_blank: false, allow_nil: false
|
||||||
validates :administrateurs, presence: true
|
validates :administrateurs, presence: true
|
||||||
|
@ -334,26 +338,26 @@ class Procedure < ApplicationRecord
|
||||||
"dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}"
|
"dossiers_#{procedure_identifier}_#{Time.zone.now.strftime('%Y-%m-%d_%H-%M')}.#{format}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def export(options = {})
|
def export(dossiers, options = {})
|
||||||
version = options.delete(:version)
|
version = options.delete(:version)
|
||||||
if version == 'v2'
|
if version == 'v2'
|
||||||
options.delete(:tables)
|
options.delete(:tables)
|
||||||
ProcedureExportV2Service.new(self, **options.to_h.symbolize_keys)
|
ProcedureExportV2Service.new(self, dossiers, **options.to_h.symbolize_keys)
|
||||||
else
|
else
|
||||||
ProcedureExportService.new(self, **options.to_h.symbolize_keys)
|
ProcedureExportService.new(self, dossiers, **options.to_h.symbolize_keys)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_csv(options = {})
|
def to_csv(dossiers, options = {})
|
||||||
export(options).to_csv
|
export(dossiers, options).to_csv
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_xlsx(options = {})
|
def to_xlsx(dossiers, options = {})
|
||||||
export(options).to_xlsx
|
export(dossiers, options).to_xlsx
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_ods(options = {})
|
def to_ods(dossiers, options = {})
|
||||||
export(options).to_ods
|
export(dossiers, options).to_ods
|
||||||
end
|
end
|
||||||
|
|
||||||
def procedure_overview(start_date)
|
def procedure_overview(start_date)
|
||||||
|
@ -481,6 +485,10 @@ class Procedure < ApplicationRecord
|
||||||
!AssignTo.exists?(groupe_instructeur: groupe_instructeurs)
|
!AssignTo.exists?(groupe_instructeur: groupe_instructeurs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def routee?
|
||||||
|
groupe_instructeurs.count > 1
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)
|
def move_type_de_champ_attributes(types_de_champ, type_de_champ, new_index)
|
||||||
|
|
|
@ -80,7 +80,7 @@ class ProcedurePresentation < ApplicationRecord
|
||||||
|
|
||||||
case table
|
case table
|
||||||
when 'notifications'
|
when 'notifications'
|
||||||
dossiers_id_with_notification = instructeur.dossiers_id_with_notifications(dossiers)
|
dossiers_id_with_notification = dossiers.with_notifications.merge(instructeur.followed_dossiers).ids
|
||||||
if order == 'desc'
|
if order == 'desc'
|
||||||
return dossiers_id_with_notification +
|
return dossiers_id_with_notification +
|
||||||
(dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
|
(dossiers.order('dossiers.updated_at desc').ids - dossiers_id_with_notification)
|
||||||
|
|
|
@ -185,6 +185,10 @@ class TypeDeChamp < ApplicationRecord
|
||||||
self.drop_down_list_attributes = { value: value }
|
self.drop_down_list_attributes = { value: value }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_typed_id
|
||||||
|
GraphQL::Schema::UniqueWithinType.encode('Champ', stable_id)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_default_drop_down_list
|
def set_default_drop_down_list
|
||||||
|
|
|
@ -49,9 +49,16 @@ class ProcedureExportService
|
||||||
:prenom
|
:prenom
|
||||||
]
|
]
|
||||||
|
|
||||||
def initialize(procedure, tables: [], ids: nil, since: nil, limit: nil)
|
def initialize(procedure, dossiers, tables: [], ids: nil, since: nil, limit: nil)
|
||||||
@procedure = procedure
|
@procedure = procedure
|
||||||
@dossiers = procedure.dossiers.downloadable_sorted
|
|
||||||
|
@attributes = ATTRIBUTES.dup
|
||||||
|
|
||||||
|
if procedure.routee?
|
||||||
|
@attributes << :groupe_instructeur_label
|
||||||
|
end
|
||||||
|
|
||||||
|
@dossiers = dossiers.downloadable_sorted
|
||||||
if ids
|
if ids
|
||||||
@dossiers = @dossiers.where(id: ids)
|
@dossiers = @dossiers.where(id: ids)
|
||||||
end
|
end
|
||||||
|
@ -137,7 +144,7 @@ class ProcedureExportService
|
||||||
end
|
end
|
||||||
|
|
||||||
def dossiers_headers
|
def dossiers_headers
|
||||||
headers = ATTRIBUTES.map(&:to_s) +
|
headers = @attributes.map(&:to_s) +
|
||||||
@procedure.types_de_champ.reject(&:exclude_from_export?).map(&:libelle) +
|
@procedure.types_de_champ.reject(&:exclude_from_export?).map(&:libelle) +
|
||||||
@procedure.types_de_champ_private.reject(&:exclude_from_export?).map(&:libelle) +
|
@procedure.types_de_champ_private.reject(&:exclude_from_export?).map(&:libelle) +
|
||||||
ETABLISSEMENT_ATTRIBUTES.map { |key| "etablissement.#{key}" } +
|
ETABLISSEMENT_ATTRIBUTES.map { |key| "etablissement.#{key}" } +
|
||||||
|
@ -148,7 +155,7 @@ class ProcedureExportService
|
||||||
|
|
||||||
def dossiers_data
|
def dossiers_data
|
||||||
@dossiers.map do |dossier|
|
@dossiers.map do |dossier|
|
||||||
values = ATTRIBUTES.map do |key|
|
values = @attributes.map do |key|
|
||||||
case key
|
case key
|
||||||
when :email
|
when :email
|
||||||
dossier.user.email
|
dossier.user.email
|
||||||
|
@ -168,6 +175,8 @@ class ProcedureExportService
|
||||||
dossier.individual&.gender
|
dossier.individual&.gender
|
||||||
when :emails_instructeurs
|
when :emails_instructeurs
|
||||||
dossier.followers_instructeurs.map(&:email).join(' ')
|
dossier.followers_instructeurs.map(&:email).join(' ')
|
||||||
|
when :groupe_instructeur_label
|
||||||
|
dossier.groupe_instructeur.label
|
||||||
else
|
else
|
||||||
dossier.read_attribute(key)
|
dossier.read_attribute(key)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
class ProcedureExportV2Service
|
class ProcedureExportV2Service
|
||||||
attr_reader :dossiers
|
attr_reader :dossiers
|
||||||
|
|
||||||
def initialize(procedure, ids: nil, since: nil, limit: nil)
|
def initialize(procedure, dossiers, ids: nil, since: nil, limit: nil)
|
||||||
@procedure = procedure
|
@procedure = procedure
|
||||||
@dossiers = procedure.dossiers.downloadable_sorted
|
@dossiers = dossiers.downloadable_sorted
|
||||||
if ids
|
if ids
|
||||||
@dossiers = @dossiers.where(id: ids)
|
@dossiers = @dossiers.where(id: ids)
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
%li
|
%li
|
||||||
%object
|
%object
|
||||||
= link_to(instructeur_procedure_path(p, statut: 'suivis')) do
|
= link_to(instructeur_procedure_path(p, statut: 'suivis')) do
|
||||||
- if current_instructeur.notifications_per_procedure[p.id].present?
|
- if current_instructeur.procedures_with_notifications(:en_cours).include?(p)
|
||||||
%span.notifications{ 'aria-label': "notifications" }
|
%span.notifications{ 'aria-label': "notifications" }
|
||||||
- followed_count = @followed_dossiers_count_per_procedure[p.id] || 0
|
- followed_count = @followed_dossiers_count_per_procedure[p.id] || 0
|
||||||
.stats-number
|
.stats-number
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
%li
|
%li
|
||||||
%object
|
%object
|
||||||
= link_to(instructeur_procedure_path(p, statut: 'traites')) do
|
= link_to(instructeur_procedure_path(p, statut: 'traites')) do
|
||||||
- if current_instructeur.notifications_per_procedure(:termine)[p.id].present?
|
- if current_instructeur.procedures_with_notifications(:termine).include?(p)
|
||||||
%span.notifications{ 'aria-label': "notifications" }
|
%span.notifications{ 'aria-label': "notifications" }
|
||||||
- termines_count = @dossiers_termines_count_per_procedure[p.id] || 0
|
- termines_count = @dossiers_termines_count_per_procedure[p.id] || 0
|
||||||
.stats-number
|
.stats-number
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
%h1= procedure_libelle @procedure
|
%h1= procedure_libelle @procedure
|
||||||
= link_to 'gestion des notifications', email_notifications_instructeur_procedure_path(@procedure), class: 'header-link'
|
= link_to 'gestion des notifications', email_notifications_instructeur_procedure_path(@procedure), class: 'header-link'
|
||||||
|
|
|
|
||||||
= link_to 'statistiques', stats_instructeur_procedure_path(@procedure), class: 'header-link'
|
= link_to 'statistiques', stats_instructeur_procedure_path(@procedure), class: 'header-link', data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
|
||||||
|
|
||||||
|
|
||||||
%ul.tabs
|
%ul.tabs
|
||||||
|
@ -24,13 +24,13 @@
|
||||||
instructeur_procedure_path(@procedure, statut: 'suivis'),
|
instructeur_procedure_path(@procedure, statut: 'suivis'),
|
||||||
active: @statut == 'suivis',
|
active: @statut == 'suivis',
|
||||||
badge: @followed_dossiers.count,
|
badge: @followed_dossiers.count,
|
||||||
notification: current_instructeur.notifications_for_procedure(@procedure).present?)
|
notification: current_instructeur.notifications_for_procedure(@procedure, :en_cours).exists?)
|
||||||
|
|
||||||
= tab_item(t('pluralize.processed', count: @termines_dossiers.count),
|
= tab_item(t('pluralize.processed', count: @termines_dossiers.count),
|
||||||
instructeur_procedure_path(@procedure, statut: 'traites'),
|
instructeur_procedure_path(@procedure, statut: 'traites'),
|
||||||
active: @statut == 'traites',
|
active: @statut == 'traites',
|
||||||
badge: @termines_dossiers.count,
|
badge: @termines_dossiers.count,
|
||||||
notification: current_instructeur.notifications_for_procedure(@procedure, :termine).present?)
|
notification: current_instructeur.notifications_for_procedure(@procedure, :termine).exists?)
|
||||||
|
|
||||||
= tab_item('tous les dossiers',
|
= tab_item('tous les dossiers',
|
||||||
instructeur_procedure_path(@procedure, statut: 'tous'),
|
instructeur_procedure_path(@procedure, statut: 'tous'),
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
%table.table.dossiers-table.hoverable
|
%table.table.dossiers-table.hoverable
|
||||||
%thead
|
%thead
|
||||||
%tr
|
%tr
|
||||||
- if @statut == 'suivis' || @statut == 'tous'
|
- if @statut.in? %w(suivis traites tous)
|
||||||
= render partial: "header_field", locals: { field: { "label" => "●", "table" => "notifications", "column" => "notifications" }, classname: "notification-col" }
|
= render partial: "header_field", locals: { field: { "label" => "●", "table" => "notifications", "column" => "notifications" }, classname: "notification-col" }
|
||||||
- else
|
- else
|
||||||
%th.notification-col
|
%th.notification-col
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
%td.folder-col
|
%td.folder-col
|
||||||
= link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do
|
= link_to(instructeur_dossier_path(@procedure, dossier), class: 'cell-link') do
|
||||||
%span.icon.folder
|
%span.icon.folder
|
||||||
- if current_instructeur.notifications_for_procedure(@procedure, :not_archived).include?(dossier.id)
|
- if current_instructeur.notifications_for_procedure(@procedure, :not_archived).include?(dossier)
|
||||||
%span.notifications{ 'aria-label': 'notifications' }
|
%span.notifications{ 'aria-label': 'notifications' }
|
||||||
|
|
||||||
%td.number-col
|
%td.number-col
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
\-
|
\-
|
||||||
= link_to 'Nouveautés', 'https://github.com/betagouv/demarches-simplifiees.fr/releases', target: '_blank'
|
= link_to 'Nouveautés', 'https://github.com/betagouv/demarches-simplifiees.fr/releases', target: '_blank'
|
||||||
\-
|
\-
|
||||||
= link_to 'Statistiques', stats_path
|
= link_to 'Statistiques', stats_path, data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
|
||||||
\-
|
\-
|
||||||
= link_to 'CGU / Mentions légales', CGU_URL
|
= link_to 'CGU / Mentions légales', CGU_URL
|
||||||
\-
|
\-
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
%li.footer-link
|
%li.footer-link
|
||||||
= link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link"
|
= link_to "Nouveautés", "https://github.com/betagouv/demarches-simplifiees.fr/releases", :class => "footer-link"
|
||||||
%li.footer-link
|
%li.footer-link
|
||||||
= link_to "Statistiques", stats_path, :class => "footer-link"
|
= link_to "Statistiques", stats_path, :class => "footer-link", data: { turbolinks: false } # Turbolinks disabled for Chartkick. See Issue #350
|
||||||
%li.footer-link
|
%li.footer-link
|
||||||
= link_to "CGU", CGU_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
|
= link_to "CGU", CGU_URL, :class => "footer-link", :target => "_blank", rel: "noopener noreferrer"
|
||||||
%li.footer-link
|
%li.footer-link
|
||||||
|
|
|
@ -71,3 +71,8 @@
|
||||||
%tr
|
%tr
|
||||||
%th.libelle Date de déclaration :
|
%th.libelle Date de déclaration :
|
||||||
%td= try_format_date(etablissement.association_date_declaration)
|
%td= try_format_date(etablissement.association_date_declaration)
|
||||||
|
|
||||||
|
%p
|
||||||
|
= link_to '➡ Autres informations sur l’organisme sur « entreprise.data.gouv.fr »',
|
||||||
|
"https://entreprise.data.gouv.fr/etablissement/#{etablissement.siret}",
|
||||||
|
target: "_blank"
|
||||||
|
|
|
@ -47,3 +47,8 @@
|
||||||
|
|
||||||
- if etablissement.exercices.present?
|
- if etablissement.exercices.present?
|
||||||
%p.etablissement-exercices Les exercices comptables des trois dernières années seront joints à votre dossier.
|
%p.etablissement-exercices Les exercices comptables des trois dernières années seront joints à votre dossier.
|
||||||
|
|
||||||
|
%p
|
||||||
|
= link_to '➡ Autres informations sur l’organisme sur « entreprise.data.gouv.fr »',
|
||||||
|
"https://entreprise.data.gouv.fr/etablissement/#{etablissement.siret}",
|
||||||
|
target: "_blank"
|
||||||
|
|
|
@ -26,6 +26,7 @@ end
|
||||||
# A list of features to be deployed on first push
|
# A list of features to be deployed on first push
|
||||||
features = [
|
features = [
|
||||||
:administrateur_champ_integer_number,
|
:administrateur_champ_integer_number,
|
||||||
|
:administrateur_graphql,
|
||||||
:administrateur_web_hook,
|
:administrateur_web_hook,
|
||||||
:insee_api_v3,
|
:insee_api_v3,
|
||||||
:instructeur_bypass_email_login_token,
|
:instructeur_bypass_email_login_token,
|
||||||
|
|
5
config/initializers/graphql.rb
Normal file
5
config/initializers/graphql.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
GraphQL::RailsLogger.configure do |config|
|
||||||
|
config.white_list = {
|
||||||
|
'API::V2::GraphqlController' => ['execute']
|
||||||
|
}
|
||||||
|
end
|
|
@ -17,3 +17,9 @@ fr:
|
||||||
refuse: "Refusé"
|
refuse: "Refusé"
|
||||||
sans_suite: "Sans suite"
|
sans_suite: "Sans suite"
|
||||||
autorisation_donnees: Acceptation des CGU
|
autorisation_donnees: Acceptation des CGU
|
||||||
|
state/brouillon: Brouillon
|
||||||
|
state/en_construction: En construction
|
||||||
|
state/en_instruction: En instruction
|
||||||
|
state/accepte: Accepté
|
||||||
|
state/refuse: Refusé
|
||||||
|
state/sans_suite: Sans suite
|
||||||
|
|
|
@ -10,3 +10,7 @@ fr:
|
||||||
organisation: Organisme
|
organisation: Organisme
|
||||||
duree_conservation_dossiers_dans_ds: Durée de conservation des dossiers sur demarches-simplifiees.fr
|
duree_conservation_dossiers_dans_ds: Durée de conservation des dossiers sur demarches-simplifiees.fr
|
||||||
duree_conservation_dossiers_hors_ds: Durée de conservation des dossiers hors demarches-simplifiees.fr
|
duree_conservation_dossiers_hors_ds: Durée de conservation des dossiers hors demarches-simplifiees.fr
|
||||||
|
aasm_state/brouillon: Brouillon
|
||||||
|
aasm_state/publiee: Publiée
|
||||||
|
aasm_state/archivee: Archivée
|
||||||
|
aasm_state/hidden: Suprimée
|
||||||
|
|
|
@ -219,12 +219,20 @@ Rails.application.routes.draw do
|
||||||
# API
|
# API
|
||||||
#
|
#
|
||||||
|
|
||||||
|
authenticated :user, lambda { |user| user.administrateur_id && Flipper.enabled?(:administrateur_graphql, user) } do
|
||||||
|
mount GraphiQL::Rails::Engine, at: "/graphql", graphql_path: "/api/v2/graphql", via: :get
|
||||||
|
end
|
||||||
|
|
||||||
namespace :api do
|
namespace :api do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
resources :procedures, only: [:index, :show] do
|
resources :procedures, only: [:index, :show] do
|
||||||
resources :dossiers, only: [:index, :show]
|
resources :dossiers, only: [:index, :show]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
namespace :v2 do
|
||||||
|
post :graphql, to: "graphql#execute"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
7
db/migrate/20190920122228_add_indexes_to_dossier.rb
Normal file
7
db/migrate/20190920122228_add_indexes_to_dossier.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class AddIndexesToDossier < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_index :dossiers, :state
|
||||||
|
add_index :dossiers, :archived
|
||||||
|
add_index :follows, :unfollowed_at
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# 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.define(version: 2019_09_17_151652) do
|
ActiveRecord::Schema.define(version: 2019_09_20_122228) 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 "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -254,9 +254,11 @@ ActiveRecord::Schema.define(version: 2019_09_17_151652) do
|
||||||
t.bigint "groupe_instructeur_id"
|
t.bigint "groupe_instructeur_id"
|
||||||
t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin
|
t.index "to_tsvector('french'::regconfig, (search_terms || private_search_terms))", name: "index_dossiers_on_search_terms_private_search_terms", using: :gin
|
||||||
t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin
|
t.index "to_tsvector('french'::regconfig, search_terms)", name: "index_dossiers_on_search_terms", using: :gin
|
||||||
|
t.index ["archived"], name: "index_dossiers_on_archived"
|
||||||
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
|
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
|
||||||
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
|
t.index ["hidden_at"], name: "index_dossiers_on_hidden_at"
|
||||||
t.index ["procedure_id"], name: "index_dossiers_on_procedure_id"
|
t.index ["procedure_id"], name: "index_dossiers_on_procedure_id"
|
||||||
|
t.index ["state"], name: "index_dossiers_on_state"
|
||||||
t.index ["user_id"], name: "index_dossiers_on_user_id"
|
t.index ["user_id"], name: "index_dossiers_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -353,6 +355,7 @@ ActiveRecord::Schema.define(version: 2019_09_17_151652) do
|
||||||
t.index ["dossier_id"], name: "index_follows_on_dossier_id"
|
t.index ["dossier_id"], name: "index_follows_on_dossier_id"
|
||||||
t.index ["instructeur_id", "dossier_id", "unfollowed_at"], name: "uniqueness_index", unique: true
|
t.index ["instructeur_id", "dossier_id", "unfollowed_at"], name: "uniqueness_index", unique: true
|
||||||
t.index ["instructeur_id"], name: "index_follows_on_instructeur_id"
|
t.index ["instructeur_id"], name: "index_follows_on_instructeur_id"
|
||||||
|
t.index ["unfollowed_at"], name: "index_follows_on_unfollowed_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "france_connect_informations", id: :serial, force: :cascade do |t|
|
create_table "france_connect_informations", id: :serial, force: :cascade do |t|
|
||||||
|
|
2
lib/tasks/graphql.rake
Normal file
2
lib/tasks/graphql.rake
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
require "graphql/rake_task"
|
||||||
|
GraphQL::RakeTask.new(schema_name: "Api::V2::Schema", directory: 'app/graphql')
|
|
@ -45,7 +45,7 @@
|
||||||
"webpack-dev-server": "^3.7.2"
|
"webpack-dev-server": "^3.7.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint:ec": "eclint check $({ git ls-files ; find vendor -type f ; echo 'db/schema.rb' ; } | sort | uniq -u)",
|
"lint:ec": "eclint check $({ git ls-files | grep -v app/graphql/schema.graphql ; find vendor -type f ; echo 'db/schema.rb' ; } | sort | uniq -u)",
|
||||||
"lint:js": "eslint ./app/javascript ./app/assets/javascripts ./config/webpack"
|
"lint:js": "eslint ./app/javascript ./app/assets/javascripts ./config/webpack"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
179
spec/controllers/api/v2/graphql_controller_spec.rb
Normal file
179
spec/controllers/api/v2/graphql_controller_spec.rb
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe API::V2::GraphqlController do
|
||||||
|
let(:admin) { create(:administrateur) }
|
||||||
|
let(:token) { admin.renew_api_token }
|
||||||
|
let(:procedure) { create(:procedure, :with_all_champs, administrateurs: [admin]) }
|
||||||
|
let(:dossier) do
|
||||||
|
dossier = create(:dossier,
|
||||||
|
:en_construction,
|
||||||
|
:with_all_champs,
|
||||||
|
procedure: procedure)
|
||||||
|
create(:commentaire, dossier: dossier, email: 'test@test.com')
|
||||||
|
dossier
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:query) do
|
||||||
|
"{
|
||||||
|
demarche(number: #{procedure.id}) {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
title
|
||||||
|
description
|
||||||
|
state
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
archivedAt
|
||||||
|
champDescriptors {
|
||||||
|
id
|
||||||
|
type
|
||||||
|
label
|
||||||
|
description
|
||||||
|
required
|
||||||
|
}
|
||||||
|
dossiers {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
end
|
||||||
|
let(:body) { JSON.parse(subject.body, symbolize_names: true) }
|
||||||
|
let(:gql_data) { body[:data] }
|
||||||
|
let(:gql_errors) { body[:errors] }
|
||||||
|
|
||||||
|
subject { post :execute, params: { query: query } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Flipper.enable(:administrateur_graphql, admin.user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when authenticated" do
|
||||||
|
let(:authorization_header) { ActionController::HttpAuthentication::Token.encode_credentials(token) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
request.env['HTTP_AUTHORIZATION'] = authorization_header
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return demarche" do
|
||||||
|
expect(gql_errors).to eq(nil)
|
||||||
|
expect(gql_data).to eq(demarche: {
|
||||||
|
id: procedure.to_typed_id,
|
||||||
|
number: procedure.id.to_s,
|
||||||
|
title: procedure.libelle,
|
||||||
|
description: procedure.description,
|
||||||
|
state: 'brouillon',
|
||||||
|
archivedAt: nil,
|
||||||
|
createdAt: procedure.created_at.iso8601,
|
||||||
|
updatedAt: procedure.updated_at.iso8601,
|
||||||
|
champDescriptors: procedure.types_de_champ.map do |tdc|
|
||||||
|
{
|
||||||
|
id: tdc.to_typed_id,
|
||||||
|
label: tdc.libelle,
|
||||||
|
type: tdc.type_champ,
|
||||||
|
description: tdc.description,
|
||||||
|
required: tdc.mandatory?
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
dossiers: {
|
||||||
|
nodes: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
context "dossier" do
|
||||||
|
let(:query) do
|
||||||
|
"{
|
||||||
|
dossier(number: #{dossier.id}) {
|
||||||
|
id
|
||||||
|
number
|
||||||
|
state
|
||||||
|
updatedAt
|
||||||
|
datePassageEnConstruction
|
||||||
|
datePassageEnInstruction
|
||||||
|
dateTraitement
|
||||||
|
motivation
|
||||||
|
motivationAttachmentUrl
|
||||||
|
usager {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
instructeurs {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
messages {
|
||||||
|
email
|
||||||
|
body
|
||||||
|
attachmentUrl
|
||||||
|
}
|
||||||
|
avis {
|
||||||
|
email
|
||||||
|
question
|
||||||
|
answer
|
||||||
|
attachmentUrl
|
||||||
|
}
|
||||||
|
champs {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
stringValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should return dossier" do
|
||||||
|
expect(gql_errors).to eq(nil)
|
||||||
|
expect(gql_data).to eq(dossier: {
|
||||||
|
id: dossier.to_typed_id,
|
||||||
|
number: dossier.id.to_s,
|
||||||
|
state: 'en_construction',
|
||||||
|
updatedAt: dossier.updated_at.iso8601,
|
||||||
|
datePassageEnConstruction: dossier.en_construction_at.iso8601,
|
||||||
|
datePassageEnInstruction: nil,
|
||||||
|
dateTraitement: nil,
|
||||||
|
motivation: nil,
|
||||||
|
motivationAttachmentUrl: nil,
|
||||||
|
usager: {
|
||||||
|
id: dossier.user.to_typed_id,
|
||||||
|
email: dossier.user.email
|
||||||
|
},
|
||||||
|
instructeurs: [],
|
||||||
|
messages: dossier.commentaires.map do |commentaire|
|
||||||
|
{
|
||||||
|
body: commentaire.body,
|
||||||
|
attachmentUrl: nil,
|
||||||
|
email: commentaire.email
|
||||||
|
}
|
||||||
|
end,
|
||||||
|
avis: [],
|
||||||
|
champs: dossier.champs.map do |champ|
|
||||||
|
{
|
||||||
|
id: champ.to_typed_id,
|
||||||
|
label: champ.libelle,
|
||||||
|
stringValue: champ.for_api_v2
|
||||||
|
}
|
||||||
|
end
|
||||||
|
})
|
||||||
|
expect(gql_data[:dossier][:champs][0][:id]).to eq(dossier.champs[0].type_de_champ.to_typed_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when not authenticated" do
|
||||||
|
it "should return error" do
|
||||||
|
expect(gql_data).to eq(nil)
|
||||||
|
expect(gql_errors).not_to eq(nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "dossier" do
|
||||||
|
let(:query) { "{ dossier(number: #{dossier.id}) { id number usager { email } } }" }
|
||||||
|
|
||||||
|
it "should return error" do
|
||||||
|
expect(gql_data).to eq(nil)
|
||||||
|
expect(gql_errors).not_to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -82,7 +82,6 @@ describe Instructeurs::DossiersController, type: :controller do
|
||||||
|
|
||||||
it { expect(dossier.archived).to be true }
|
it { expect(dossier.archived).to be true }
|
||||||
it { expect(response).to redirect_to(instructeur_procedures_url) }
|
it { expect(response).to redirect_to(instructeur_procedures_url) }
|
||||||
it { expect(instructeur.followed_dossiers).not_to include(dossier) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#unarchive' do
|
describe '#unarchive' do
|
||||||
|
|
|
@ -414,6 +414,9 @@ describe Instructeurs::ProceduresController, type: :controller do
|
||||||
describe "#download_dossiers" do
|
describe "#download_dossiers" do
|
||||||
let(:instructeur) { create(:instructeur) }
|
let(:instructeur) { create(:instructeur) }
|
||||||
let!(:procedure) { create(:procedure, instructeurs: [instructeur]) }
|
let!(:procedure) { create(:procedure, instructeurs: [instructeur]) }
|
||||||
|
let!(:gi_2) { procedure.groupe_instructeurs.create(label: '2') }
|
||||||
|
let!(:dossier_1) { create(:dossier, procedure: procedure) }
|
||||||
|
let!(:dossier_2) { create(:dossier, groupe_instructeur: gi_2) }
|
||||||
|
|
||||||
context "when logged in" do
|
context "when logged in" do
|
||||||
before do
|
before do
|
||||||
|
@ -421,7 +424,12 @@ describe Instructeurs::ProceduresController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "csv" do
|
context "csv" do
|
||||||
before { get :download_dossiers, params: { procedure_id: procedure.id }, format: 'csv' }
|
before do
|
||||||
|
expect_any_instance_of(Procedure).to receive(:to_csv)
|
||||||
|
.with(instructeur.dossiers.for_procedure(procedure), {})
|
||||||
|
|
||||||
|
get :download_dossiers, params: { procedure_id: procedure.id }, format: 'csv'
|
||||||
|
end
|
||||||
|
|
||||||
it { expect(response).to have_http_status(:ok) }
|
it { expect(response).to have_http_status(:ok) }
|
||||||
end
|
end
|
||||||
|
|
11
spec/lib/tasks/graphql_spec.rb
Normal file
11
spec/lib/tasks/graphql_spec.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe 'graphql' do
|
||||||
|
let(:current_defn) { Api::V2::Schema.to_definition }
|
||||||
|
let(:printout_defn) { File.read(Rails.root.join('app', 'graphql', 'schema.graphql')) }
|
||||||
|
|
||||||
|
it "update the printed schema with `bin/rake graphql:schema:idl`" do
|
||||||
|
result = GraphQL::SchemaComparator.compare(current_defn, printout_defn)
|
||||||
|
expect(result.identical?).to be_truthy
|
||||||
|
end
|
||||||
|
end
|
|
@ -53,6 +53,27 @@ describe Dossier do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'with_notifications' do
|
||||||
|
let(:dossier) { create(:dossier) }
|
||||||
|
let(:instructeur) { create(:instructeur) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:follow, dossier: dossier, instructeur: instructeur, messagerie_seen_at: 2.hours.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { instructeur.followed_dossiers.with_notifications }
|
||||||
|
|
||||||
|
context('without changes') do
|
||||||
|
it { is_expected.to eq [] }
|
||||||
|
end
|
||||||
|
|
||||||
|
context('with changes') do
|
||||||
|
before { dossier.commentaires << create(:commentaire, email: 'test@exemple.fr') }
|
||||||
|
|
||||||
|
it { is_expected.to match([dossier]) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'methods' do
|
describe 'methods' do
|
||||||
let(:dossier) { create(:dossier, :with_entreprise, user: user) }
|
let(:dossier) { create(:dossier, :with_entreprise, user: user) }
|
||||||
let(:etablissement) { dossier.etablissement }
|
let(:etablissement) { dossier.etablissement }
|
||||||
|
|
|
@ -248,7 +248,7 @@ describe Instructeur, type: :model do
|
||||||
instructeur_2.followed_dossiers << dossier
|
instructeur_2.followed_dossiers << dossier
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { instructeur.notifications_for_procedure(procedure) }
|
subject { instructeur.notifications_for_procedure(procedure, :en_cours) }
|
||||||
|
|
||||||
context 'when the instructeur has just followed the dossier' do
|
context 'when the instructeur has just followed the dossier' do
|
||||||
it { is_expected.to match([]) }
|
it { is_expected.to match([]) }
|
||||||
|
@ -257,14 +257,14 @@ describe Instructeur, type: :model do
|
||||||
context 'when there is a modification on public champs' do
|
context 'when there is a modification on public champs' do
|
||||||
before { dossier.champs.first.update_attribute('value', 'toto') }
|
before { dossier.champs.first.update_attribute('value', 'toto') }
|
||||||
|
|
||||||
it { is_expected.to match([dossier.id]) }
|
it { is_expected.to match([dossier]) }
|
||||||
it { expect(instructeur_2.notifications_for_procedure(procedure)).to match([dossier.id]) }
|
it { expect(instructeur_2.notifications_for_procedure(procedure, :en_cours)).to match([dossier]) }
|
||||||
it { expect(instructeur_on_procedure_2.notifications_for_procedure(procedure)).to match([]) }
|
it { expect(instructeur_on_procedure_2.notifications_for_procedure(procedure, :en_cours)).to match([]) }
|
||||||
|
|
||||||
context 'and there is a modification on private champs' do
|
context 'and there is a modification on private champs' do
|
||||||
before { dossier.champs_private.first.update_attribute('value', 'toto') }
|
before { dossier.champs_private.first.update_attribute('value', 'toto') }
|
||||||
|
|
||||||
it { is_expected.to match([dossier.id]) }
|
it { is_expected.to match([dossier]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when instructeur update it s public champs last seen' do
|
context 'when instructeur update it s public champs last seen' do
|
||||||
|
@ -273,7 +273,7 @@ describe Instructeur, type: :model do
|
||||||
before { follow.update_attribute('demande_seen_at', Time.zone.now) }
|
before { follow.update_attribute('demande_seen_at', Time.zone.now) }
|
||||||
|
|
||||||
it { is_expected.to match([]) }
|
it { is_expected.to match([]) }
|
||||||
it { expect(instructeur_2.notifications_for_procedure(procedure)).to match([dossier.id]) }
|
it { expect(instructeur_2.notifications_for_procedure(procedure, :en_cours)).to match([dossier]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -286,20 +286,20 @@ describe Instructeur, type: :model do
|
||||||
context 'when there is a modification on private champs' do
|
context 'when there is a modification on private champs' do
|
||||||
before { dossier.champs_private.first.update_attribute('value', 'toto') }
|
before { dossier.champs_private.first.update_attribute('value', 'toto') }
|
||||||
|
|
||||||
it { is_expected.to match([dossier.id]) }
|
it { is_expected.to match([dossier]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there is a modification on avis' do
|
context 'when there is a modification on avis' do
|
||||||
before { create(:avis, dossier: dossier) }
|
before { create(:avis, dossier: dossier) }
|
||||||
|
|
||||||
it { is_expected.to match([dossier.id]) }
|
it { is_expected.to match([dossier]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'the messagerie' do
|
context 'the messagerie' do
|
||||||
context 'when there is a new commentaire' do
|
context 'when there is a new commentaire' do
|
||||||
before { create(:commentaire, dossier: dossier, email: 'a@b.com') }
|
before { create(:commentaire, dossier: dossier, email: 'a@b.com') }
|
||||||
|
|
||||||
it { is_expected.to match([dossier.id]) }
|
it { is_expected.to match([dossier]) }
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there is a new commentaire issued by tps' do
|
context 'when there is a new commentaire issued by tps' do
|
||||||
|
@ -315,12 +315,12 @@ describe Instructeur, type: :model do
|
||||||
let(:instructeur) { dossier.follows.first.instructeur }
|
let(:instructeur) { dossier.follows.first.instructeur }
|
||||||
let(:procedure) { dossier.procedure }
|
let(:procedure) { dossier.procedure }
|
||||||
|
|
||||||
subject { instructeur.notifications_per_procedure }
|
subject { instructeur.procedures_with_notifications(:en_cours) }
|
||||||
|
|
||||||
context 'when there is a modification on public champs' do
|
context 'when there is a modification on public champs' do
|
||||||
before { dossier.champs.first.update_attribute('value', 'toto') }
|
before { dossier.champs.first.update_attribute('value', 'toto') }
|
||||||
|
|
||||||
it { is_expected.to match({ procedure.id => 1 }) }
|
it { is_expected.to match([procedure]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -389,7 +389,7 @@ describe Instructeur, type: :model do
|
||||||
context 'when a notification exists' do
|
context 'when a notification exists' do
|
||||||
before do
|
before do
|
||||||
allow(instructeur).to receive(:notifications_for_procedure)
|
allow(instructeur).to receive(:notifications_for_procedure)
|
||||||
.with(procedure_to_assign, :all)
|
.with(procedure_to_assign, :not_archived)
|
||||||
.and_return([1, 2, 3])
|
.and_return([1, 2, 3])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ describe ProcedureExportService do
|
||||||
describe 'to_data' do
|
describe 'to_data' do
|
||||||
let(:procedure) { create(:procedure, :published, :with_all_champs) }
|
let(:procedure) { create(:procedure, :published, :with_all_champs) }
|
||||||
let(:table) { :dossiers }
|
let(:table) { :dossiers }
|
||||||
subject { ProcedureExportService.new(procedure).to_data(table) }
|
subject { ProcedureExportService.new(procedure, procedure.dossiers).to_data(table) }
|
||||||
|
|
||||||
let(:headers) { subject[:headers] }
|
let(:headers) { subject[:headers] }
|
||||||
let(:data) { subject[:data] }
|
let(:data) { subject[:data] }
|
||||||
|
@ -19,8 +19,8 @@ describe ProcedureExportService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'dossiers' do
|
context 'dossiers' do
|
||||||
it 'should have headers' do
|
let(:nominal_header) do
|
||||||
expect(headers).to eq([
|
[
|
||||||
:id,
|
:id,
|
||||||
:created_at,
|
:created_at,
|
||||||
:updated_at,
|
:updated_at,
|
||||||
|
@ -86,7 +86,19 @@ describe ProcedureExportService do
|
||||||
:entreprise_date_creation,
|
:entreprise_date_creation,
|
||||||
:entreprise_nom,
|
:entreprise_nom,
|
||||||
:entreprise_prenom
|
:entreprise_prenom
|
||||||
])
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should have headers' do
|
||||||
|
expect(headers).to eq(nominal_header)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a procedure routee' do
|
||||||
|
before { procedure.groupe_instructeurs.create(label: '2') }
|
||||||
|
|
||||||
|
let(:routee_header) { nominal_header.insert(nominal_header.index(:textarea), :groupe_instructeur_label) }
|
||||||
|
|
||||||
|
it { expect(headers).to eq(routee_header) }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should have empty values' do
|
it 'should have empty values' do
|
||||||
|
@ -139,6 +151,13 @@ describe ProcedureExportService do
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a procedure routee' do
|
||||||
|
before { procedure.groupe_instructeurs.create(label: '2') }
|
||||||
|
|
||||||
|
it { expect(data.first[15]).to eq('défaut') }
|
||||||
|
it { expect(data.first.count).to eq(dossier_data.count + champs_data.count + etablissement_data.count + 1) }
|
||||||
|
end
|
||||||
|
|
||||||
context 'and etablissement' do
|
context 'and etablissement' do
|
||||||
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :with_entreprise, procedure: procedure) }
|
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :with_entreprise, procedure: procedure) }
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ describe ProcedureExportV2Service do
|
||||||
let(:procedure) { create(:procedure, :published, :with_all_champs) }
|
let(:procedure) { create(:procedure, :published, :with_all_champs) }
|
||||||
subject do
|
subject do
|
||||||
Tempfile.create do |f|
|
Tempfile.create do |f|
|
||||||
f << ProcedureExportV2Service.new(procedure).to_xlsx
|
f << ProcedureExportV2Service.new(procedure, procedure.dossiers).to_xlsx
|
||||||
f.rewind
|
f.rewind
|
||||||
SimpleXlsxReader.open(f.path)
|
SimpleXlsxReader.open(f.path)
|
||||||
end
|
end
|
||||||
|
@ -34,8 +34,8 @@ describe ProcedureExportV2Service do
|
||||||
context 'with dossier' do
|
context 'with dossier' do
|
||||||
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
|
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
|
||||||
|
|
||||||
it 'should have headers' do
|
let(:nominal_headers) do
|
||||||
expect(dossiers_sheet.headers).to eq([
|
[
|
||||||
"ID",
|
"ID",
|
||||||
"Email",
|
"Email",
|
||||||
"Civilité",
|
"Civilité",
|
||||||
|
@ -74,7 +74,11 @@ describe ProcedureExportV2Service do
|
||||||
"siret",
|
"siret",
|
||||||
"carte",
|
"carte",
|
||||||
"text"
|
"text"
|
||||||
])
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should have headers' do
|
||||||
|
expect(dossiers_sheet.headers).to match(nominal_headers)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should have data' do
|
it 'should have data' do
|
||||||
|
@ -88,6 +92,15 @@ describe ProcedureExportV2Service do
|
||||||
expect(en_construction_at).to eq(dossier.en_construction_at.round)
|
expect(en_construction_at).to eq(dossier.en_construction_at.round)
|
||||||
expect(en_instruction_at).to eq(dossier.en_instruction_at.round)
|
expect(en_instruction_at).to eq(dossier.en_instruction_at.round)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a procedure routee' do
|
||||||
|
before { procedure.groupe_instructeurs.create(label: '2') }
|
||||||
|
|
||||||
|
let(:routee_header) { nominal_headers.insert(nominal_headers.index('textarea'), 'Groupe instructeur') }
|
||||||
|
|
||||||
|
it { expect(dossiers_sheet.headers).to match(routee_header) }
|
||||||
|
it { expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut') }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with etablissement' do
|
context 'with etablissement' do
|
||||||
|
|
Loading…
Add table
Reference in a new issue