Merge pull request #4612 from betagouv/dev

2019-12-04-01
This commit is contained in:
Paul Chavard 2019-12-04 14:52:44 +01:00 committed by GitHub
commit 75df51a783
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 704 additions and 67 deletions

View file

@ -47,6 +47,7 @@ gem 'omniauth-rails_csrf_protection', '~> 0.1'
gem 'openid_connect' gem 'openid_connect'
gem 'openstack' gem 'openstack'
gem 'pg' gem 'pg'
gem 'phonelib'
gem 'prawn' # PDF Generation gem 'prawn' # PDF Generation
gem 'prawn_rails' gem 'prawn_rails'
gem 'premailer-rails' gem 'premailer-rails'

View file

@ -241,7 +241,7 @@ GEM
graphiql-rails (1.7.0) graphiql-rails (1.7.0)
railties railties
sprockets-rails sprockets-rails
graphql (1.9.10) graphql (1.9.15)
graphql-batch (0.4.1) graphql-batch (0.4.1)
graphql (>= 1.3, < 2) graphql (>= 1.3, < 2)
promise.rb (~> 0.7.2) promise.rb (~> 0.7.2)
@ -420,6 +420,7 @@ GEM
ast (~> 2.4.0) ast (~> 2.4.0)
pdf-core (0.7.0) pdf-core (0.7.0)
pg (1.1.3) pg (1.1.3)
phonelib (0.6.39)
powerpack (0.1.2) powerpack (0.1.2)
prawn (2.2.2) prawn (2.2.2)
pdf-core (~> 0.7.0) pdf-core (~> 0.7.0)
@ -767,6 +768,7 @@ DEPENDENCIES
openid_connect openid_connect
openstack openstack
pg pg
phonelib
prawn prawn
prawn_rails prawn_rails
premailer-rails premailer-rails

View file

@ -77,6 +77,7 @@ En local, un utilisateur de test est créé automatiquement, avec les identifian
InstructeurEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later InstructeurEmailNotificationJob.set(cron: "0 10 * * 1,2,3,4,5,6").perform_later
PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later PurgeUnattachedBlobsJob.set(cron: "0 0 * * *").perform_later
OperationsSignatureJob.set(cron: "0 6 * * *").perform_later OperationsSignatureJob.set(cron: "0 6 * * *").perform_later
SeekAndDestroyExpiredDossiersJob.set(cron: "0 7 * * *").perform_later
### Voir les emails envoyés en local ### Voir les emails envoyés en local

View file

@ -68,7 +68,7 @@ module NewAdministrateur
end end
def procedure_params def procedure_params
editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :euro_flag, :logo, :auto_archive_on, :monavis_embed] editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :declarative_with_state, :euro_flag, :logo, :auto_archive_on, :monavis_embed]
permited_params = if @procedure&.locked? permited_params = if @procedure&.locked?
params.require(:procedure).permit(*editable_params) params.require(:procedure).permit(*editable_params)
else else

View file

@ -26,6 +26,10 @@ class Api::V2::Schema < GraphQL::Schema
Types::MessageType Types::MessageType
when Instructeur, User when Instructeur, User
Types::ProfileType Types::ProfileType
when Individual
Types::PersonnePhysiqueType
when Etablissement
Types::PersonneMoraleType
else else
raise GraphQL::ExecutionError.new("Unexpected object: #{obj}") raise GraphQL::ExecutionError.new("Unexpected object: #{obj}")
end end
@ -33,6 +37,7 @@ class Api::V2::Schema < GraphQL::Schema
orphan_types Types::Champs::CarteChampType, orphan_types Types::Champs::CarteChampType,
Types::Champs::CheckboxChampType, Types::Champs::CheckboxChampType,
Types::Champs::CiviliteChampType,
Types::Champs::DateChampType, Types::Champs::DateChampType,
Types::Champs::DecimalNumberChampType, Types::Champs::DecimalNumberChampType,
Types::Champs::DossierLinkChampType, Types::Champs::DossierLinkChampType,
@ -45,7 +50,9 @@ class Api::V2::Schema < GraphQL::Schema
Types::Champs::TextChampType, Types::Champs::TextChampType,
Types::GeoAreas::ParcelleCadastraleType, Types::GeoAreas::ParcelleCadastraleType,
Types::GeoAreas::QuartierPrioritaireType, Types::GeoAreas::QuartierPrioritaireType,
Types::GeoAreas::SelectionUtilisateurType Types::GeoAreas::SelectionUtilisateurType,
Types::PersonneMoraleType,
Types::PersonnePhysiqueType
def self.unauthorized_object(error) def self.unauthorized_object(error)
# Add a top-level error to the response instead of returning nil: # Add a top-level error to the response instead of returning nil:

View file

@ -1,3 +1,12 @@
type Association {
dateCreation: ISO8601Date!
dateDeclaration: ISO8601Date!
datePublication: ISO8601Date!
objet: String!
rna: String!
titre: String!
}
type Avis { type Avis {
attachmentUrl: URL attachmentUrl: URL
dateQuestion: ISO8601DateTime! dateQuestion: ISO8601DateTime!
@ -76,6 +85,33 @@ type CheckboxChamp implements Champ {
value: Boolean! value: Boolean!
} }
enum Civilite {
"""
Monsieur
"""
M
"""
Madame
"""
Mme
}
type CiviliteChamp implements Champ {
id: ID!
"""
Libellé du champ.
"""
label: String!
"""
La valeur du champ sous forme texte.
"""
stringValue: String
value: Civilite
}
""" """
GeoJSON coordinates GeoJSON coordinates
""" """
@ -157,6 +193,10 @@ type DecimalNumberChamp implements Champ {
value: Float value: Float
} }
interface Demandeur {
id: ID!
}
""" """
Une demarche Une demarche
""" """
@ -184,6 +224,11 @@ type Demarche {
""" """
datePublication: ISO8601DateTime! datePublication: ISO8601DateTime!
"""
L'état de dossier pour une démarche déclarative
"""
declarative: DossierDeclarativeState
""" """
Description de la démarche. Description de la démarche.
""" """
@ -240,6 +285,7 @@ type Demarche {
Le numero de la démarche. Le numero de la démarche.
""" """
number: Int! number: Int!
service: Service!
""" """
L'état de la démarche. L'état de la démarche.
@ -322,6 +368,7 @@ type Dossier {
Date de traitement. Date de traitement.
""" """
dateTraitement: ISO8601DateTime dateTraitement: ISO8601DateTime
demandeur: Demandeur!
id: ID! id: ID!
instructeurs: [Profile!]! instructeurs: [Profile!]!
messages: [Message!]! messages: [Message!]!
@ -428,6 +475,18 @@ type DossierConnection {
pageInfo: PageInfo! pageInfo: PageInfo!
} }
enum DossierDeclarativeState {
"""
Accepté
"""
accepte
"""
En instruction
"""
en_instruction
}
""" """
An edge in a connection. An edge in a connection.
""" """
@ -578,6 +637,22 @@ enum DossierState {
sans_suite sans_suite
} }
type Entreprise {
capitalSocial: Int!
codeEffectifEntreprise: String!
dateCreation: ISO8601Date!
formeJuridique: String!
formeJuridiqueCode: String!
inlineAdresse: String!
nom: String!
nomCommercial: String!
numeroTvaIntracommunautaire: String!
prenom: String!
raisonSociale: String!
siren: String!
siretSiegeSocial: String!
}
interface GeoArea { interface GeoArea {
geometry: GeoJSON! geometry: GeoJSON!
id: ID! id: ID!
@ -615,6 +690,11 @@ type GroupeInstructeur {
label: String! label: String!
} }
"""
An ISO 8601-encoded date
"""
scalar ISO8601Date
""" """
An ISO 8601-encoded datetime An ISO 8601-encoded datetime
""" """
@ -758,21 +838,32 @@ type ParcelleCadastrale implements GeoArea {
surfaceParcelle: Float! surfaceParcelle: Float!
} }
type PersonneMorale { type PersonneMorale implements Demandeur {
adresse: String! adresse: String!
association: Association
codeInseeLocalite: String! codeInseeLocalite: String!
codePostal: String! codePostal: String!
complementAdresse: String! complementAdresse: String!
entreprise: Entreprise
id: ID!
libelleNaf: String! libelleNaf: String!
localite: String! localite: String!
naf: String! naf: String!
nomVoie: String! nomVoie: String!
numeroVoie: String! numeroVoie: String!
siegeSocial: String! siegeSocial: Boolean!
siret: String! siret: String!
typeVoie: String! typeVoie: String!
} }
type PersonnePhysique implements Demandeur {
civilite: Civilite
dateDeNaissance: ISO8601Date
id: ID!
nom: String!
prenom: String!
}
type PieceJustificativeChamp implements Champ { type PieceJustificativeChamp implements Champ {
id: ID! id: ID!
@ -845,6 +936,13 @@ type SelectionUtilisateur implements GeoArea {
source: GeoAreaSource! source: GeoAreaSource!
} }
type Service {
id: ID!
nom: String!
organisme: String!
typeOrganisme: TypeOrganisme!
}
type SiretChamp implements Champ { type SiretChamp implements Champ {
etablissement: PersonneMorale etablissement: PersonneMorale
id: ID! id: ID!
@ -1012,6 +1110,43 @@ enum TypeDeChamp {
yes_no yes_no
} }
enum TypeOrganisme {
"""
Administration centrale
"""
administration_centrale
"""
Association
"""
association
"""
Autre
"""
autre
"""
Collectivité territoriale
"""
collectivite_territoriale
"""
Établissement denseignement
"""
etablissement_enseignement
"""
Opérateur d'État
"""
operateur_d_etat
"""
Service déconcentré de l'État
"""
service_deconcentre_de_l_etat
}
""" """
A valid URL, transported as a string A valid URL, transported as a string
""" """

View file

@ -31,6 +31,8 @@ module Types
Types::Champs::MultipleDropDownListChampType Types::Champs::MultipleDropDownListChampType
when ::Champs::LinkedDropDownListChamp when ::Champs::LinkedDropDownListChamp
Types::Champs::LinkedDropDownListChampType Types::Champs::LinkedDropDownListChampType
when ::Champs::CiviliteChamp
Types::Champs::CiviliteChampType
else else
Types::Champs::TextChampType Types::Champs::TextChampType
end end

View file

@ -0,0 +1,7 @@
module Types::Champs
class CiviliteChampType < Types::BaseObject
implements Types::ChampType
field :value, Types::Civilite, null: true
end
end

View file

@ -0,0 +1,6 @@
module Types
class Civilite < Types::BaseEnum
value("M", "Monsieur", value: Individual::GENDER_MALE)
value("Mme", "Madame", value: Individual::GENDER_FEMALE)
end
end

View file

@ -0,0 +1,18 @@
module Types
module DemandeurType
include Types::BaseInterface
global_id_field :id
definition_methods do
def resolve_type(object, context)
case object
when Individual
Types::PersonnePhysiqueType
when Etablissement
Types::PersonneMoraleType
end
end
end
end
end

View file

@ -6,6 +6,14 @@ module Types
end end
end end
class DossierDeclarativeState < Types::BaseEnum
Procedure.declarative_with_states.each do |symbol_name, string_name|
value(string_name,
I18n.t("declarative_with_state/#{string_name}", scope: [:activerecord, :attributes, :procedure]),
value: symbol_name)
end
end
description "Une demarche" description "Une demarche"
global_id_field :id global_id_field :id
@ -13,6 +21,7 @@ module Types
field :title, String, "Le titre de la démarche.", null: false, method: :libelle field :title, String, "Le titre de la démarche.", null: false, method: :libelle
field :description, String, "Description de la démarche.", null: false field :description, String, "Description de la démarche.", null: false
field :state, DemarcheState, "L'état de la démarche.", null: false field :state, DemarcheState, "L'état de la démarche.", null: false
field :declarative, DossierDeclarativeState, "L'état de dossier pour une démarche déclarative", null: true, method: :declarative_with_state
field :date_creation, GraphQL::Types::ISO8601DateTime, "Date de la création.", null: false, method: :created_at field :date_creation, GraphQL::Types::ISO8601DateTime, "Date de la création.", null: false, method: :created_at
field :date_publication, GraphQL::Types::ISO8601DateTime, "Date de la publication.", null: false, method: :published_at field :date_publication, GraphQL::Types::ISO8601DateTime, "Date de la publication.", null: false, method: :published_at
@ -20,6 +29,7 @@ module Types
field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true, method: :closed_at field :date_fermeture, GraphQL::Types::ISO8601DateTime, "Date de la fermeture.", null: true, method: :closed_at
field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false field :groupe_instructeurs, [Types::GroupeInstructeurType], null: false
field :service, Types::ServiceType, null: false
field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do field :dossiers, Types::DossierType.connection_type, "Liste de tous les dossiers d'une démarche.", null: false do
argument :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers." argument :order, Types::Order, default_value: :asc, required: false, description: "L'ordre des dossiers."
@ -39,6 +49,10 @@ module Types
Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object) Loaders::Association.for(object.class, groupe_instructeurs: { procedure: [:administrateurs] }).load(object)
end end
def service
Loaders::Record.for(Service).load(object.service_id)
end
def dossiers(updated_since: nil, created_since: nil, state: nil, order:) def dossiers(updated_since: nil, created_since: nil, state: nil, order:)
dossiers = object.dossiers.state_not_brouillon.for_api_v2 dossiers = object.dossiers.state_not_brouillon.for_api_v2

View file

@ -24,6 +24,8 @@ module Types
{ Extensions::Attachment => { attachment: :justificatif_motivation } } { Extensions::Attachment => { attachment: :justificatif_motivation } }
] ]
field :demandeur, Types::DemandeurType, null: false
field :usager, Types::ProfileType, null: false field :usager, Types::ProfileType, null: false
field :instructeurs, [Types::ProfileType], null: false field :instructeurs, [Types::ProfileType], null: false
@ -61,6 +63,14 @@ module Types
Loaders::Association.for(object.class, :champs_private).load(object) Loaders::Association.for(object.class, :champs_private).load(object)
end end
def demandeur
if object.procedure.for_individual
Loaders::Association.for(object.class, :individual).load(object)
else
Loaders::Association.for(object.class, :etablissement).load(object)
end
end
def self.authorized?(object, context) def self.authorized?(object, context)
authorized_demarche?(object.procedure, context) authorized_demarche?(object.procedure, context)
end end

View file

@ -1,7 +1,34 @@
module Types module Types
class PersonneMoraleType < Types::BaseObject class PersonneMoraleType < Types::BaseObject
class EntrepriseType < Types::BaseObject
field :siren, String, null: false
field :capital_social, Int, null: false
field :numero_tva_intracommunautaire, String, null: false
field :forme_juridique, String, null: false
field :forme_juridique_code, String, null: false
field :nom_commercial, String, null: false
field :raison_sociale, String, null: false
field :siret_siege_social, String, null: false
field :code_effectif_entreprise, String, null: false
field :date_creation, GraphQL::Types::ISO8601Date, null: false
field :nom, String, null: false
field :prenom, String, null: false
field :inline_adresse, String, null: false
end
class AssociationType < Types::BaseObject
field :rna, String, null: false
field :titre, String, null: false
field :objet, String, null: false
field :date_creation, GraphQL::Types::ISO8601Date, null: false
field :date_declaration, GraphQL::Types::ISO8601Date, null: false
field :date_publication, GraphQL::Types::ISO8601Date, null: false
end
implements Types::DemandeurType
field :siret, String, null: false field :siret, String, null: false
field :siege_social, String, null: false field :siege_social, Boolean, null: false
field :naf, String, null: false field :naf, String, null: false
field :libelle_naf, String, null: false field :libelle_naf, String, null: false
field :adresse, String, null: false field :adresse, String, null: false
@ -12,5 +39,26 @@ module Types
field :code_postal, String, null: false field :code_postal, String, null: false
field :localite, String, null: false field :localite, String, null: false
field :code_insee_localite, String, null: false field :code_insee_localite, String, null: false
field :entreprise, EntrepriseType, null: true
field :association, AssociationType, null: true
def entreprise
if object.entreprise_siren.present?
object.entreprise
end
end
def association
if object.association?
{
rna: object.association_rna,
titre: object.association_titre,
objet: object.association_objet,
date_creation: object.association_date_creation,
date_declaration: object.association_date_declaration,
date_publication: object.association_date_publication
}
end
end
end end
end end

View file

@ -0,0 +1,10 @@
module Types
class PersonnePhysiqueType < Types::BaseObject
implements Types::DemandeurType
field :nom, String, null: false
field :prenom, String, null: false
field :civilite, Types::Civilite, null: true, method: :gender
field :date_de_naissance, GraphQL::Types::ISO8601Date, null: true, method: :birthdate
end
end

View file

@ -0,0 +1,15 @@
module Types
class ServiceType < Types::BaseObject
class TypeOrganisme < Types::BaseEnum
Service.type_organismes.each do |symbol_name, string_name|
value(string_name, I18n.t(symbol_name, scope: [:type_organisme]), value: symbol_name)
end
end
global_id_field :id
field :nom, String, null: false
field :type_organisme, TypeOrganisme, null: false
field :organisme, String, null: false
end
end

View file

@ -0,0 +1,8 @@
class SeekAndDestroyExpiredDossiersJob < ApplicationJob
queue_as :cron
def perform(*args)
Dossier.send_brouillon_expiration_notices
Dossier.destroy_brouillons_and_notify
end
end

View file

@ -69,4 +69,18 @@ class DossierMailer < ApplicationMailer
format.html { render layout: 'mailers/notifications_layout' } format.html { render layout: 'mailers/notifications_layout' }
end end
end end
def notify_brouillon_near_deletion(user, dossiers)
@subject = default_i18n_subject(count: dossiers.count)
@dossiers = dossiers
mail(to: user.email, subject: @subject)
end
def notify_brouillon_deletion(user, dossier_hashes)
@subject = default_i18n_subject(count: dossier_hashes.count)
@dossier_hashes = dossier_hashes
mail(to: user.email, subject: @subject)
end
end end

View file

@ -18,6 +18,8 @@ class Dossier < ApplicationRecord
TAILLE_MAX_ZIP = 50.megabytes TAILLE_MAX_ZIP = 50.megabytes
DRAFT_EXPIRATION = 1.month + 5.days
has_one :etablissement, dependent: :destroy has_one :etablissement, dependent: :destroy
has_one :individual, dependent: :destroy has_one :individual, dependent: :destroy
has_one :attestation, dependent: :destroy has_one :attestation, dependent: :destroy
@ -162,6 +164,14 @@ class Dossier < ApplicationRecord
user: []) user: [])
} }
scope :brouillon_close_to_expiration, -> do
brouillon
.joins(:procedure)
.where("dossiers.created_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - (1 * interval '1 month') <= now()")
end
scope :expired_brouillon, -> { brouillon.where("brouillon_close_to_expiration_notice_sent_at < ?", (Time.zone.now - (DRAFT_EXPIRATION))) }
scope :without_notice_sent, -> { where(brouillon_close_to_expiration_notice_sent_at: nil) }
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 :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) }
@ -577,6 +587,10 @@ class Dossier < ApplicationRecord
Dossier.where(id: champs.filter(&:dossier_link?).map(&:value).compact) Dossier.where(id: champs.filter(&:dossier_link?).map(&:value).compact)
end end
def hash_for_deletion_mail
{ id: self.id, procedure_libelle: self.procedure.libelle }
end
private private
def log_dossier_operation(author, operation, subject = nil) def log_dossier_operation(author, operation, subject = nil)
@ -627,4 +641,37 @@ class Dossier < ApplicationRecord
) )
end end
end end
def self.send_brouillon_expiration_notices
brouillons = Dossier
.brouillon_close_to_expiration
.without_notice_sent
brouillons
.includes(:user)
.group_by(&:user)
.each do |(user, dossiers)|
DossierMailer.notify_brouillon_near_deletion(user, dossiers).deliver_later
end
brouillons.update_all(brouillon_close_to_expiration_notice_sent_at: Time.zone.now)
end
def self.destroy_brouillons_and_notify
expired_brouillons = Dossier.expired_brouillon
expired_brouillons
.includes(:procedure, :user)
.group_by(&:user)
.each do |(user, dossiers)|
dossier_hashes = dossiers.map(&:hash_for_deletion_mail)
DossierMailer.notify_brouillon_deletion(user, dossier_hashes).deliver_later
dossiers.each do |dossier|
DeletedDossier.create_from_dossier(dossier)
dossier.destroy
end
end
end
end end

View file

@ -305,6 +305,12 @@ class Procedure < ApplicationRecord
declarative_with_state == Procedure.declarative_with_states.fetch(:accepte) declarative_with_state == Procedure.declarative_with_states.fetch(:accepte)
end end
def self.declarative_attributes_for_select
declarative_with_states.map do |state, _|
[I18n.t("activerecord.attributes.#{model_name.i18n_key}.declarative_with_state/#{state}"), state]
end
end
# Warning: dossier after_save build_default_champs must be removed # Warning: dossier after_save build_default_champs must be removed
# to save a dossier created from this method # to save a dossier created from this method
def new_dossier def new_dossier

View file

@ -20,7 +20,7 @@ class Service < ApplicationRecord
validates :organisme, presence: { message: 'doit être renseigné' }, allow_nil: false validates :organisme, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :type_organisme, presence: { message: 'doit être renseigné' }, allow_nil: false validates :type_organisme, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :email, presence: { message: 'doit être renseigné' }, allow_nil: false validates :email, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :telephone, presence: { message: 'doit être renseigné' }, allow_nil: false validates :telephone, phone: { possible: true, allow_blank: true }
validates :horaires, presence: { message: 'doivent être renseignés' }, allow_nil: false validates :horaires, presence: { message: 'doivent être renseignés' }, allow_nil: false
validates :adresse, presence: { message: 'doit être renseignée' }, allow_nil: false validates :adresse, presence: { message: 'doit être renseignée' }, allow_nil: false
validates :administrateur, presence: { message: 'doit être renseigné' }, allow_nil: false validates :administrateur, presence: { message: 'doit être renseigné' }, allow_nil: false

View file

@ -0,0 +1,12 @@
- content_for(:title, "#{@subject}")
%p
Bonjour,
%p= t('.automatic_dossier_deletion', count: @dossier_hashes.count)
%ul
- @dossier_hashes.each do |d|
%li n° #{d[:id]} (#{d[:procedure_libelle]})
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,16 @@
- content_for(:title, "#{@subject}")
%p
Bonjour,
%p
Afin de limiter la conservation de vos données personnelles,
= t('.automatic_dossier_deletion', count: @dossiers.count)
%ul
- @dossiers.each do |d|
%li= link_to("n° #{d.id} (#{d.procedure.libelle})", dossier_url(d))
%p
#{sanitize(t('.send_your_draft', count: @dossiers.count))}. Et sinon, vous n'avez rien à faire.
= render partial: "layouts/mailers/signature"

View file

@ -113,3 +113,13 @@
%p.explication %p.explication
La clôture automatique suspend la publication de la démarche et entraîne le passage de tous les dossiers "en construction" La clôture automatique suspend la publication de la démarche et entraîne le passage de tous les dossiers "en construction"
(c'est à dire ceux qui ont été déposés), au statut "en instruction", ce qui ne permet plus aux usagers de les modifier. (c'est à dire ceux qui ont été déposés), au statut "en instruction", ce qui ne permet plus aux usagers de les modifier.
= f.label :declarative_with_state do
Démarche déclarative
= f.select :declarative_with_state, Procedure.declarative_attributes_for_select, { include_blank: true }, class: 'form-control'
%p.explication
Par défaut, une démarche n'est pas déclarative; à son dépot, un dossier est «en construction». Vous pouvez choisir de la rendre déclarative, afin que tous les dossiers déposés soient immédiatement au statut "en instruction" en "accepté".
%br
%br
Dans le cadre d'une démarche déclarative, au dépot, seul l'email associé à l'état choisi est envoyé. (ex: démarche déclarative «accepté»: envoi uniquement de l'email d'acceptation)

View file

@ -18,16 +18,23 @@
%h2.header-section Informations de contact %h2.header-section Informations de contact
%p.explication Ces informations seront visibles par les utilisateurs du formulaire. %p.explication
Votre démarche sera hébergée par demarche-simplifiees.fr mais nous ne pouvons pas assurer le support des démarches. Et malgré la dématérialisation, les usagers se poseront parfois des questions légitimes sur le processus administratif.
%br
Il est donc important que les usagers puissent vous contacter s'ils ont des questions sur votre démarche.
%br
Ces informations seront visibles par les utilisateurs de la démarche, affichées dans le menu "Aide".
%p.explication Indiquez une adresse mail en capacité de répondre à l'usager.
= f.label :email do = f.label :email do
Adresse email Adresse email
%span.mandatory * %span.mandatory *
= f.email_field :email, placeholder: 'contact@mon-service.fr', required: true = f.email_field :email, placeholder: 'contact@mon-service.fr', required: true
%p.explication Indiquez le numéro de téléphone du service le plus à même de fournir des réponses pertinentes à vos usagers.
= f.label :telephone do = f.label :telephone do
Numéro de téléphone
%span.mandatory * %span.mandatory *
Numéro de téléphone
= f.telephone_field :telephone, placeholder: '04 12 24 42 37', required: true = f.telephone_field :telephone, placeholder: '04 12 24 42 37', required: true
= f.label :horaires do = f.label :horaires do

View file

@ -1,6 +1,9 @@
.editable-champ{ class: "editable-champ-#{champ.type_champ}" } .editable-champ{ class: "editable-champ-#{champ.type_champ}" }
- if champ.repetition? - if champ.repetition?
= render partial: 'shared/dossiers/editable_champs/header_section', locals: { champ: champ } = render partial: 'shared/dossiers/editable_champs/header_section', locals: { champ: champ }
- if champ.description.present?
%p.notice= string_to_html(champ.description, false)
- elsif has_label?(champ) - elsif has_label?(champ)
= render partial: 'shared/dossiers/editable_champs/champ_label', locals: { form: form, champ: champ, seen_at: defined?(seen_at) ? seen_at : nil } = render partial: 'shared/dossiers/editable_champs/champ_label', locals: { form: form, champ: champ, seen_at: defined?(seen_at) ? seen_at : nil }

View file

@ -24,13 +24,14 @@
Par email : Par email :
= link_to service.email, "mailto:#{service.email}" = link_to service.email, "mailto:#{service.email}"
%li - if service.telephone.present?
Par téléphone : %li
= link_to service.telephone, service.telephone_url Par téléphone :
= link_to service.telephone, service.telephone_url
%li %li
- horaires = "Horaires : #{formatted_horaires(service.horaires)}" - horaires = "Horaires : #{formatted_horaires(service.horaires)}"
= simple_format(horaires, {}, wrapper_tag: 'span') = simple_format(horaires, {}, wrapper_tag: 'span')
- politiques = politiques_conservation_de_donnees(procedure) - politiques = politiques_conservation_de_donnees(procedure)

View file

@ -0,0 +1,2 @@
Phonelib.default_country = "FR"
Phonelib.parse_special = true

View file

@ -14,3 +14,5 @@ fr:
aasm_state/publiee: Publiée aasm_state/publiee: Publiée
aasm_state/close: Close aasm_state/close: Close
aasm_state/hidden: Suprimée aasm_state/hidden: Suprimée
declarative_with_state/en_instruction: En instruction
declarative_with_state/accepte: Accepté

View file

@ -0,0 +1,9 @@
fr:
dossier_mailer:
notify_brouillon_deletion:
subject:
one: "Un dossier en brouillon a été supprimé automatiquement"
other: "Des dossiers en brouillon ont été supprimés automatiquement"
automatic_dossier_deletion:
one: "Le délai maximum de conservation du dossier en brouillon suivant a été atteint, il a donc été supprimé :"
other: "Le délai maximum de conservation des dossiers en brouillon suivants a été atteint, ils ont donc été supprimés :"

View file

@ -0,0 +1,12 @@
fr:
dossier_mailer:
notify_brouillon_near_deletion:
subject:
one: Un dossier en brouillon va bientôt être supprimé
other: Des dossiers en brouillon vont bientôt être supprimés
automatic_dossier_deletion:
one: "le dossier en brouillon suivant sera bientôt automatiquement supprimé :"
other: "les dossiers en brouillon suivant seront bientôt automatiquement supprimés :"
send_your_draft:
one: "Si vous souhaitez toujours déposer ce dossier, vous pouvez retrouver votre brouillon pendant encore <b>un mois</b>"
other: "Si vous souhaitez toujours déposer ces dossiers, vous pouvez retrouver vos brouillons pendant encore <b>un mois</b>"

View file

@ -0,0 +1,5 @@
class AddNearDeletionNoticeSendToDossier < ActiveRecord::Migration[5.2]
def change
add_column :dossiers, :brouillon_close_to_expiration_notice_sent_at, :datetime
end
end

View file

@ -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_11_14_113700) do ActiveRecord::Schema.define(version: 2019_11_28_081324) 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"
@ -252,6 +252,7 @@ ActiveRecord::Schema.define(version: 2019_11_14_113700) do
t.text "search_terms" t.text "search_terms"
t.text "private_search_terms" t.text "private_search_terms"
t.bigint "groupe_instructeur_id" t.bigint "groupe_instructeur_id"
t.datetime "brouillon_close_to_expiration_notice_sent_at"
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 ["archived"], name: "index_dossiers_on_archived"

View file

@ -0,0 +1,7 @@
namespace :after_party do
desc 'Deployment task: enable_seek_and_destroy_job'
task enable_seek_and_destroy_job: :environment do
SeekAndDestroyExpiredDossiersJob.set(cron: "0 7 * * *").perform_later
AfterParty::TaskRecord.create version: '20191203142402'
end
end

View file

@ -3,18 +3,19 @@ require 'spec_helper'
describe API::V2::GraphqlController do describe API::V2::GraphqlController do
let(:admin) { create(:administrateur) } let(:admin) { create(:administrateur) }
let(:token) { admin.renew_api_token } let(:token) { admin.renew_api_token }
let(:procedure) { create(:procedure, :with_all_champs, administrateurs: [admin]) } let(:procedure) { create(:procedure, :published, :for_individual, :with_service, :with_all_champs, administrateurs: [admin]) }
let(:dossier) do let(:dossier) do
dossier = create(:dossier, dossier = create(:dossier,
:en_construction, :en_construction,
:with_all_champs, :with_all_champs,
:for_individual,
procedure: procedure) procedure: procedure)
create(:commentaire, dossier: dossier, email: 'test@test.com') create(:commentaire, dossier: dossier, email: 'test@test.com')
dossier dossier
end end
let(:dossier1) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 1.day.ago) } let(:dossier1) { create(:dossier, :en_construction, :for_individual, procedure: procedure, en_construction_at: 1.day.ago) }
let(:dossier2) { create(:dossier, :en_construction, procedure: procedure, en_construction_at: 3.days.ago) } let(:dossier2) { create(:dossier, :en_construction, :for_individual, procedure: procedure, en_construction_at: 3.days.ago) }
let!(:dossier_brouillon) { create(:dossier, procedure: procedure) } let!(:dossier_brouillon) { create(:dossier, :for_individual, procedure: procedure) }
let(:dossiers) { [dossier2, dossier1, dossier] } let(:dossiers) { [dossier2, dossier1, dossier] }
let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) } let(:instructeur) { create(:instructeur, followed_dossiers: dossiers) }
@ -39,6 +40,11 @@ describe API::V2::GraphqlController do
email email
} }
} }
service {
nom
typeOrganisme
organisme
}
champDescriptors { champDescriptors {
id id
type type
@ -79,7 +85,7 @@ describe API::V2::GraphqlController do
number: procedure.id, number: procedure.id,
title: procedure.libelle, title: procedure.libelle,
description: procedure.description, description: procedure.description,
state: 'brouillon', state: 'publiee',
dateFermeture: nil, dateFermeture: nil,
dateCreation: procedure.created_at.iso8601, dateCreation: procedure.created_at.iso8601,
dateDerniereModification: procedure.updated_at.iso8601, dateDerniereModification: procedure.updated_at.iso8601,
@ -89,6 +95,11 @@ describe API::V2::GraphqlController do
label: "défaut" label: "défaut"
} }
], ],
service: {
nom: procedure.service.nom,
typeOrganisme: procedure.service.type_organisme,
organisme: procedure.service.organisme
},
champDescriptors: procedure.types_de_champ.map do |tdc| champDescriptors: procedure.types_de_champ.map do |tdc|
{ {
id: tdc.to_typed_id, id: tdc.to_typed_id,
@ -149,6 +160,15 @@ describe API::V2::GraphqlController do
id id
email email
} }
demandeur {
id
... on PersonnePhysique {
nom
prenom
civilite
dateDeNaissance
}
}
instructeurs { instructeurs {
id id
email email
@ -177,45 +197,104 @@ describe API::V2::GraphqlController do
}" }"
end end
it "should be returned" do context "with individual" do
expect(gql_errors).to eq(nil) it "should be returned" do
expect(gql_data).to eq(dossier: { expect(gql_errors).to eq(nil)
id: dossier.to_typed_id, expect(gql_data).to eq(dossier: {
number: dossier.id, id: dossier.to_typed_id,
state: 'en_construction', number: dossier.id,
dateDerniereModification: dossier.updated_at.iso8601, state: 'en_construction',
datePassageEnConstruction: dossier.en_construction_at.iso8601, dateDerniereModification: dossier.updated_at.iso8601,
datePassageEnInstruction: nil, datePassageEnConstruction: dossier.en_construction_at.iso8601,
dateTraitement: nil, datePassageEnInstruction: nil,
motivation: nil, dateTraitement: nil,
motivationAttachmentUrl: nil, motivation: nil,
usager: { motivationAttachmentUrl: nil,
id: dossier.user.to_typed_id, usager: {
email: dossier.user.email id: dossier.user.to_typed_id,
}, email: dossier.user.email
instructeurs: [ },
{ instructeurs: [
id: instructeur.to_typed_id, {
email: instructeur.email id: instructeur.to_typed_id,
email: instructeur.email
}
],
demandeur: {
id: dossier.individual.to_typed_id,
nom: dossier.individual.nom,
prenom: dossier.individual.prenom,
civilite: 'M',
dateDeNaissance: '1991-11-01'
},
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
context "with entreprise" do
let(:procedure) { create(:procedure, :published, administrateurs: [admin]) }
let(:dossier) { create(:dossier, :en_construction, :with_entreprise, procedure: procedure) }
let(:query) do
"{
dossier(number: #{dossier.id}) {
id
number
usager {
id
email
}
demandeur {
id
... on PersonneMorale {
siret
siegeSocial
entreprise {
siren
dateCreation
}
}
}
} }
], }"
messages: dossier.commentaires.map do |commentaire| end
{
body: commentaire.body, it "should be returned" do
attachmentUrl: nil, expect(gql_errors).to eq(nil)
email: commentaire.email expect(gql_data).to eq(dossier: {
id: dossier.to_typed_id,
number: dossier.id,
usager: {
id: dossier.user.to_typed_id,
email: dossier.user.email
},
demandeur: {
id: dossier.etablissement.to_typed_id,
siret: dossier.etablissement.siret,
siegeSocial: dossier.etablissement.siege_social,
entreprise: {
siren: dossier.etablissement.entreprise_siren,
dateCreation: dossier.etablissement.entreprise_date_creation.iso8601
}
} }
end, })
avis: [], end
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 end
@ -296,7 +375,7 @@ describe API::V2::GraphqlController do
end end
describe 'dossierPasserEnInstruction' do describe 'dossierPasserEnInstruction' do
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) } let(:dossier) { create(:dossier, :en_construction, :for_individual, procedure: procedure) }
let(:query) do let(:query) do
"mutation { "mutation {
dossierPasserEnInstruction(input: { dossierPasserEnInstruction(input: {
@ -331,7 +410,7 @@ describe API::V2::GraphqlController do
end end
context 'validation error' do context 'validation error' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) }
it "should fail" do it "should fail" do
expect(gql_errors).to eq(nil) expect(gql_errors).to eq(nil)
@ -344,7 +423,7 @@ describe API::V2::GraphqlController do
end end
describe 'dossierClasserSansSuite' do describe 'dossierClasserSansSuite' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) }
let(:query) do let(:query) do
"mutation { "mutation {
dossierClasserSansSuite(input: { dossierClasserSansSuite(input: {
@ -380,7 +459,7 @@ describe API::V2::GraphqlController do
end end
context 'validation error' do context 'validation error' do
let(:dossier) { create(:dossier, :accepte, procedure: procedure) } let(:dossier) { create(:dossier, :accepte, :for_individual, procedure: procedure) }
it "should fail" do it "should fail" do
expect(gql_errors).to eq(nil) expect(gql_errors).to eq(nil)
@ -393,7 +472,7 @@ describe API::V2::GraphqlController do
end end
describe 'dossierRefuser' do describe 'dossierRefuser' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) }
let(:query) do let(:query) do
"mutation { "mutation {
dossierRefuser(input: { dossierRefuser(input: {
@ -429,7 +508,7 @@ describe API::V2::GraphqlController do
end end
context 'validation error' do context 'validation error' do
let(:dossier) { create(:dossier, :sans_suite, procedure: procedure) } let(:dossier) { create(:dossier, :sans_suite, :for_individual, procedure: procedure) }
it "should fail" do it "should fail" do
expect(gql_errors).to eq(nil) expect(gql_errors).to eq(nil)
@ -442,7 +521,7 @@ describe API::V2::GraphqlController do
end end
describe 'dossierAccepter' do describe 'dossierAccepter' do
let(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } let(:dossier) { create(:dossier, :en_instruction, :for_individual, procedure: procedure) }
let(:query) do let(:query) do
"mutation { "mutation {
dossierAccepter(input: { dossierAccepter(input: {
@ -511,7 +590,7 @@ describe API::V2::GraphqlController do
end end
context 'validation error' do context 'validation error' do
let(:dossier) { create(:dossier, :refuse, procedure: procedure) } let(:dossier) { create(:dossier, :refuse, :for_individual, procedure: procedure) }
it "should fail" do it "should fail" do
expect(gql_errors).to eq(nil) expect(gql_errors).to eq(nil)

View file

@ -83,4 +83,27 @@ RSpec.describe DossierMailer, type: :mailer do
it_behaves_like 'a dossier notification' it_behaves_like 'a dossier notification'
end end
describe '.notify_brouillon_near_deletion' do
let(:dossier) { create(:dossier) }
before do
duree = dossier.procedure.duree_conservation_dossiers_dans_ds
@date_suppression = dossier.created_at + duree.months
end
subject { described_class.notify_brouillon_near_deletion(dossier.user, [dossier]) }
it { expect(subject.body).to include(" #{dossier.id} ") }
it { expect(subject.body).to include(dossier.procedure.libelle) }
end
describe '.notify_brouillon_deletion' do
let(:dossier) { create(:dossier) }
subject { described_class.notify_brouillon_deletion(dossier.user, [dossier.hash_for_deletion_mail]) }
it { expect(subject.subject).to eq("Un dossier en brouillon a été supprimé automatiquement") }
it { expect(subject.body).to include(" #{dossier.id} (#{dossier.procedure.libelle})") }
end
end end

View file

@ -20,6 +20,23 @@ class DossierMailerPreview < ActionMailer::Preview
DossierMailer.notify_revert_to_instruction(dossier) DossierMailer.notify_revert_to_instruction(dossier)
end end
def notify_brouillon_near_deletion
DossierMailer.notify_brouillon_near_deletion(User.new(email: "usager@example.com"), [dossier])
end
def notify_brouillons_near_deletion
DossierMailer.notify_brouillon_near_deletion(User.new(email: "usager@example.com"), [dossier, dossier])
end
def notify_brouillon_deletion
DossierMailer.notify_brouillon_deletion(User.new(email: "usager@example.com"), [dossier.hash_for_deletion_mail])
end
def notify_brouillons_deletion
dossier_hashes = [dossier, dossier].map(&:hash_for_deletion_mail)
DossierMailer.notify_brouillon_deletion(User.new(email: "usager@example.com"), dossier_hashes)
end
private private
def deleted_dossier def deleted_dossier

View file

@ -1047,4 +1047,57 @@ describe Dossier do
it { expect(Dossier.for_procedure(procedure_1)).to contain_exactly(dossier_1_1, dossier_1_2) } it { expect(Dossier.for_procedure(procedure_1)).to contain_exactly(dossier_1_1, dossier_1_2) }
it { expect(Dossier.for_procedure(procedure_2)).to contain_exactly(dossier_2_1) } it { expect(Dossier.for_procedure(procedure_2)).to contain_exactly(dossier_2_1) }
end end
describe '#send_brouillon_expiration_notices' do
let!(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) }
let!(:date_close_to_expiration) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 1.month }
let!(:date_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months - 6.days }
let!(:date_not_expired) { Date.today - procedure.duree_conservation_dossiers_dans_ds.months + 2.months }
before { Timecop.freeze(Time.zone.parse('12/12/2012').beginning_of_day) }
after { Timecop.return }
context "Envoi de message pour les dossiers expirant dans - d'un mois" do
let!(:expired_brouillon) { create(:dossier, procedure: procedure, created_at: date_expired) }
let!(:brouillon_close_to_expiration) { create(:dossier, procedure: procedure, created_at: date_close_to_expiration) }
let!(:brouillon_close_but_with_notice_sent) { create(:dossier, procedure: procedure, created_at: date_close_to_expiration, brouillon_close_to_expiration_notice_sent_at: Time.zone.now) }
let!(:valid_brouillon) { create(:dossier, procedure: procedure, created_at: date_not_expired) }
before do
allow(DossierMailer).to receive(:notify_brouillon_near_deletion).and_return(double(deliver_later: nil))
Dossier.send_brouillon_expiration_notices
end
it 'verification de la creation de mail' do
expect(DossierMailer).to have_received(:notify_brouillon_near_deletion).with(brouillon_close_to_expiration.user, [brouillon_close_to_expiration])
expect(DossierMailer).to have_received(:notify_brouillon_near_deletion).with(expired_brouillon.user, [expired_brouillon])
end
it 'Verification du changement d etat du champ' do
expect(brouillon_close_to_expiration.reload.brouillon_close_to_expiration_notice_sent_at).not_to be_nil
end
end
end
describe '#destroy_brouillons_and_notify' do
let!(:today) { Time.zone.now.at_midnight }
let!(:expired_brouillon) { create(:dossier, brouillon_close_to_expiration_notice_sent_at: today - (Dossier::DRAFT_EXPIRATION + 1.day)) }
let!(:other_brouillon) { create(:dossier, brouillon_close_to_expiration_notice_sent_at: today - (Dossier::DRAFT_EXPIRATION - 1.day)) }
before do
allow(DossierMailer).to receive(:notify_brouillon_deletion).and_return(double(deliver_later: nil))
Dossier.destroy_brouillons_and_notify
end
it 'notifies deletion' do
expect(DossierMailer).to have_received(:notify_brouillon_deletion).once
expect(DossierMailer).to have_received(:notify_brouillon_deletion).with(expired_brouillon.user, [expired_brouillon.hash_for_deletion_mail])
end
it 'deletes the expired brouillon' do
expect(DeletedDossier.find_by(dossier_id: expired_brouillon.id)).to be_present
expect { expired_brouillon.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end end

View file

@ -7,7 +7,7 @@ describe Service, type: :model do
organisme: 'mairie des iles', organisme: 'mairie des iles',
type_organisme: Service.type_organismes.fetch(:association), type_organisme: Service.type_organismes.fetch(:association),
email: 'super@email.com', email: 'super@email.com',
telephone: '1212202', telephone: '012345678',
horaires: 'du lundi au vendredi', horaires: 'du lundi au vendredi',
adresse: '12 rue des schtroumpfs', adresse: '12 rue des schtroumpfs',
administrateur_id: administrateur.id administrateur_id: administrateur.id
@ -16,6 +16,33 @@ describe Service, type: :model do
it { expect(Service.new(params).valid?).to be_truthy } it { expect(Service.new(params).valid?).to be_truthy }
it 'should forbid invalid phone numbers' do
service = Service.create(params)
invalid_phone_numbers = ["1", "Néant", "01 60 50 40 30 20"]
invalid_phone_numbers.each do |tel|
service.telephone = tel
expect(service.valid?).to be_falsey
end
end
it 'should accept no phone numbers' do
service = Service.create(params)
service.telephone = nil
expect(service.valid?).to be_truthy
end
it 'should accept valid phone numbers' do
service = Service.create(params)
valid_phone_numbers = ["3646", "273115", "0160376983", "01 60 50 40 30 ", "+33160504030"]
valid_phone_numbers.each do |tel|
service.telephone = tel
expect(service.valid?).to be_truthy
end
end
context 'when a first service exists' do context 'when a first service exists' do
before { Service.create(params) } before { Service.create(params) }