Merge pull request #6678 from betagouv/main

2021-11-25-01
This commit is contained in:
Paul Chavard 2021-11-25 14:08:54 +03:00 committed by GitHub
commit faeb75ffb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
87 changed files with 1192 additions and 307 deletions

View file

@ -67,8 +67,11 @@ GEM
activemodel (>= 4.1, < 6.2) activemodel (>= 4.1, < 6.2)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
active_storage_validations (0.9.2) active_storage_validations (0.9.6)
rails (>= 5.2.0) activejob (>= 5.2.0)
activemodel (>= 5.2.0)
activestorage (>= 5.2.0)
activesupport (>= 5.2.0)
activejob (6.1.4.1) activejob (6.1.4.1)
activesupport (= 6.1.4.1) activesupport (= 6.1.4.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
@ -376,7 +379,7 @@ GEM
regexp_parser (~> 2.0) regexp_parser (~> 2.0)
uri_template (~> 0.7) uri_template (~> 0.7)
jsonapi-renderer (0.2.2) jsonapi-renderer (0.2.2)
jwt (2.2.2) jwt (2.3.0)
kaminari (1.2.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1) kaminari-actionview (= 1.2.1)
@ -421,7 +424,7 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2021.0212) mime-types-data (3.2021.0212)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.1) mini_mime (1.1.2)
mini_portile2 (2.6.1) mini_portile2 (2.6.1)
minitest (5.14.4) minitest (5.14.4)
momentjs-rails (2.20.1) momentjs-rails (2.20.1)
@ -437,7 +440,7 @@ GEM
mini_portile2 (~> 2.6.1) mini_portile2 (~> 2.6.1)
racc (~> 1.4) racc (~> 1.4)
open4 (1.3.4) open4 (1.3.4)
openid_connect (1.2.0) openid_connect (1.3.0)
activemodel activemodel
attr_required (>= 1.0.0) attr_required (>= 1.0.0)
json-jwt (>= 1.5.0) json-jwt (>= 1.5.0)
@ -492,7 +495,7 @@ GEM
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-mini-profiler (2.3.1) rack-mini-profiler (2.3.1)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-oauth2 (1.16.0) rack-oauth2 (1.19.0)
activesupport activesupport
attr_required attr_required
httpclient httpclient
@ -700,7 +703,7 @@ GEM
actionpack (>= 4.0) actionpack (>= 4.0)
activesupport (>= 4.0) activesupport (>= 4.0)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
swd (1.2.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
attr_required (>= 0.0.5) attr_required (>= 0.0.5)
httpclient (>= 2.4) httpclient (>= 2.4)
@ -747,7 +750,7 @@ GEM
nokogiri (~> 1.6) nokogiri (~> 1.6)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
selenium-webdriver (>= 3.0, < 4.0) selenium-webdriver (>= 3.0, < 4.0)
webfinger (1.1.0) webfinger (1.2.0)
activesupport activesupport
httpclient (>= 2.4) httpclient (>= 2.4)
webmock (3.11.2) webmock (3.11.2)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -1,9 +1,7 @@
@import "colors"; @import "colors";
@import "constants"; @import "constants";
#dossier-show { .dossier-container {
margin-bottom: 30px;
.sub-header { .sub-header {
.label { .label {
float: right; float: right;

View file

@ -0,0 +1,11 @@
@import "colors";
@import "constants";
.france-connect-agent-login-button {
background-image: image-url("logo-agent-connect.png");
display: block;
height: 60px;
width: 230px;
margin: 20px auto;
font-size: 0;
}

View file

@ -50,6 +50,8 @@
color: $black; color: $black;
} }
.mt-1 { .mt-1 {
margin-top: $default-spacer; margin-top: $default-spacer;
} }
@ -58,6 +60,10 @@
margin-top: 2 * $default-spacer; margin-top: 2 * $default-spacer;
} }
.mt-3 {
margin-top: 3 * $default-spacer;
}
.mt-4 { .mt-4 {
margin-top: 4 * $default-spacer; margin-top: 4 * $default-spacer;
} }
@ -66,10 +72,62 @@
margin-top: 8 * $default-spacer; margin-top: 8 * $default-spacer;
} }
.mb-1 {
margin-bottom: $default-spacer;
}
.mb-2 { .mb-2 {
margin-bottom: 2 * $default-spacer; margin-bottom: 2 * $default-spacer;
} }
.mb-1 { .mb-3 {
margin-bottom: $default-spacer; margin-bottom: 3 * $default-spacer;
}
.mb-4 {
margin-bottom: 4 * $default-spacer;
}
.mb-8 {
margin-bottom: 8 * $default-spacer;
}
.pt-1 {
padding-top: $default-spacer;
}
.pt-2 {
padding-top: 2 * $default-spacer;
}
.pt-3 {
padding-top: 3 * $default-spacer;
}
.pt-4 {
padding-top: 4 * $default-spacer;
}
.pt-8 {
padding-top: 8 * $default-spacer;
}
.pb-1 {
padding-bottom: $default-spacer;
}
.pb-2 {
padding-bottom: 2 * $default-spacer;
}
.pb-3 {
padding-bottom: 3 * $default-spacer;
}
.pb-4 {
padding-bottom: 4 * $default-spacer;
}
.pb-8 {
padding-bottom: 8 * $default-spacer;
} }

View file

@ -0,0 +1,53 @@
# doc: https://github.com/france-connect/Documentation-AgentConnect
class AgentConnect::AgentController < ApplicationController
before_action :redirect_to_login_if_fc_aborted, only: [:callback]
def index
end
def login
redirect_to AgentConnectService.authorization_uri
end
def callback
user_info = AgentConnectService.user_info(params[:code])
instructeur = Instructeur.find_by(agent_connect_id: user_info['sub'])
if instructeur.nil?
instructeur = Instructeur.find_by(users: { email: santized_email(user_info) })
instructeur&.update(agent_connect_id: user_info['sub'])
end
if instructeur.nil?
user = User.create_or_promote_to_instructeur(santized_email(user_info), Devise.friendly_token[0, 20])
instructeur = user.instructeur
instructeur.update(agent_connect_id: user_info['sub'])
end
sign_in(:user, instructeur.user)
redirect_to instructeur_procedures_path
rescue Rack::OAuth2::Client::Error => e
Rails.logger.error e.message
redirect_france_connect_error_connection
end
private
def santized_email(user_info)
user_info['email'].strip.downcase
end
def redirect_to_login_if_fc_aborted
if params[:code].blank?
redirect_to new_user_session_path
end
end
def redirect_france_connect_error_connection
flash.alert = t('errors.messages.france_connect.connexion')
redirect_to(new_user_session_path)
end
end

View file

@ -1,4 +1,6 @@
class API::V2::GraphqlController < API::V2::BaseController class API::V2::GraphqlController < API::V2::BaseController
include GraphqlOperationLogConcern
def execute def execute
variables = ensure_hash(params[:variables]) variables = ensure_hash(params[:variables])
@ -8,6 +10,8 @@ class API::V2::GraphqlController < API::V2::BaseController
operation_name: params[:operationName]) operation_name: params[:operationName])
render json: result render json: result
rescue GraphQL::ParseError => exception
handle_parse_error(exception)
rescue => exception rescue => exception
if Rails.env.production? if Rails.env.production?
handle_error_in_production(exception) handle_error_in_production(exception)
@ -22,43 +26,10 @@ class API::V2::GraphqlController < API::V2::BaseController
super super
payload.merge!({ payload.merge!({
graphql_operation: operation_log(params[:query], params[:operationName], params[:variables]) graphql_operation: operation_log(params[:query], params[:operationName], params[:variables]&.to_unsafe_h)
}) })
end end
def operation_log(query, operation_name, variables)
return "NoQuery" if query.nil?
operation = GraphQL.parse(query).children.find do |node|
if node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
node.name == operation_name
end
end
return "InvalidQuery" if operation.nil?
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
message = operation.operation_type
if operation.name
message += ": #{operation.name} { "
end
message += operation.selections.map(&:name).join(', ')
message += " }"
if variables.present?
message += " "
message += variables.to_unsafe_h.flat_map do |(name, value)|
if name == "input"
value.map do |(name, value)|
"#{name}: \"#{value.to_s.truncate(10)}\""
end
else
"#{name}: \"#{value.to_s.truncate(10)}\""
end
end.join(', ')
end
message
end
def process_action(*args) def process_action(*args)
super super
rescue ActionDispatch::Http::Parameters::ParseError => exception rescue ActionDispatch::Http::Parameters::ParseError => exception
@ -88,6 +59,15 @@ class API::V2::GraphqlController < API::V2::BaseController
end end
end end
def handle_parse_error(exception)
render json: {
errors: [
{ message: exception.message }
],
data: nil
}, status: 400
end
def handle_error_in_development(exception) def handle_error_in_development(exception)
logger.error exception.message logger.error exception.message
logger.error exception.backtrace.join("\n") logger.error exception.backtrace.join("\n")

View file

@ -0,0 +1,62 @@
module GraphqlOperationLogConcern
extend ActiveSupport::Concern
# This method parses GraphQL query and creates a short description of the query. It is useful for logging.
def operation_log(query, operation_name, variables)
return "NoQuery" if query.nil?
operation = parse_graphql_query(query, operation_name)
return "InvalidQuery" if operation.nil?
return "IntrospectionQuery" if operation.name == "IntrospectionQuery"
message = "#{operation.operation_type}: "
message += if operation.name.present?
"#{operation.name} { "
else
"{ "
end
message += operation.selections.map(&:name).join(', ')
message += " } "
message += if variables.present?
variables.flat_map do |(name, value)|
format_graphql_variable(name, value)
end
else
operation.selections.flat_map(&:arguments).flat_map do |argument|
format_graphql_variable(argument.name, argument.value)
end
end.join(', ')
message.strip
end
private
def parse_graphql_query(query, operation_name)
operations = GraphQL.parse(query).children.filter do |node|
node.is_a?(GraphQL::Language::Nodes::OperationDefinition)
end
if operations.size == 1
operations.first
else
operations.find { |node| node.name == operation_name }
end
rescue
nil
end
def format_graphql_variable(name, value)
if value.is_a?(Hash)
value.map do |(name, value)|
format_graphql_variable(name, value)
end
elsif value.is_a?(GraphQL::Language::Nodes::InputObject)
value.arguments.map do |argument|
format_graphql_variable(argument.name, argument.value)
end
else
"#{name}: \"#{value.to_s.truncate(10)}\""
end
end
end

View file

@ -24,7 +24,8 @@ module Users
.with_dossiers .with_dossiers
.where(email: current_user.email) .where(email: current_user.email)
.page(page) .page(page)
@statut = statut(@user_dossiers, @dossiers_invites, @dossiers_supprimes, @dossier_transfers, params[:statut]) @dossiers_close_to_expiration = current_user.dossiers.close_to_expiration.page(page)
@statut = statut(@user_dossiers, @dossiers_invites, @dossiers_supprimes, @dossier_transfers, @dossiers_close_to_expiration, params[:statut])
end end
def show def show
@ -291,12 +292,13 @@ module Users
# if the status tab is filled, then this tab # if the status tab is filled, then this tab
# else first filled tab # else first filled tab
# else mes-dossiers # else mes-dossiers
def statut(mes_dossiers, dossiers_invites, dossiers_supprimes, dossier_transfers, params_statut) def statut(mes_dossiers, dossiers_invites, dossiers_supprimes, dossier_transfers, dossiers_close_to_expiration, params_statut)
tabs = { tabs = {
'mes-dossiers' => mes_dossiers.present?, 'mes-dossiers' => mes_dossiers.present?,
'dossiers-invites' => dossiers_invites.present?, 'dossiers-invites' => dossiers_invites.present?,
'dossiers-supprimes' => dossiers_supprimes.present?, 'dossiers-supprimes' => dossiers_supprimes.present?,
'dossiers-transferes' => dossier_transfers.present? 'dossiers-transferes' => dossier_transfers.present?,
'dossiers-expirant' => dossiers_close_to_expiration.present?
} }
if tabs[params_statut] if tabs[params_statut]
params_statut params_statut

View file

@ -98,6 +98,11 @@ module DossierHelper
end end
end end
def safe_expiration_date(dossier)
date = dossier.expiration_date.presence || dossier.approximative_expiration_date
l(date, format: '%d/%m/%Y')
end
def annuaire_link(siren) def annuaire_link(siren)
base_url = "https://annuaire-entreprises.data.gouv.fr" base_url = "https://annuaire-entreprises.data.gouv.fr"
return base_url if siren.blank? return base_url if siren.blank?

View file

@ -105,7 +105,8 @@ function ComboCommunesSearch(params) {
<div> <div>
<div className="notice" id={communeDescribedBy}> <div className="notice" id={communeDescribedBy}>
<p> <p>
Choisissez la commune. Vous pouver entre le nom ou le code postal. Choisissez la commune. Vous pouvez entrer le nom ou le code
postal.
</p> </p>
</div> </div>
<ComboSearch <ComboSearch

View file

@ -0,0 +1,9 @@
class AgentConnectClient < OpenIDConnect::Client
def initialize(code = nil)
super(AGENT_CONNECT)
if code.present?
self.authorization_code = code
end
end
end

View file

@ -14,7 +14,6 @@
class AttestationTemplate < ApplicationRecord class AttestationTemplate < ApplicationRecord
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
include TagsSubstitutionConcern include TagsSubstitutionConcern
include FileValidationConcern
belongs_to :procedure, optional: false belongs_to :procedure, optional: false
@ -24,8 +23,8 @@ class AttestationTemplate < ApplicationRecord
validates :footer, length: { maximum: 190 } validates :footer, length: { maximum: 190 }
FILE_MAX_SIZE = 1.megabytes FILE_MAX_SIZE = 1.megabytes
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: file_size_validation(FILE_MAX_SIZE) validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: FILE_MAX_SIZE }
validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: file_size_validation(FILE_MAX_SIZE) validates :signature, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: FILE_MAX_SIZE }
DOSSIER_STATE = Dossier.states.fetch(:accepte) DOSSIER_STATE = Dossier.states.fetch(:accepte)

View file

@ -17,7 +17,6 @@
# #
class Avis < ApplicationRecord class Avis < ApplicationRecord
include EmailSanitizableConcern include EmailSanitizableConcern
include FileValidationConcern
belongs_to :dossier, inverse_of: :avis, touch: true, optional: false belongs_to :dossier, inverse_of: :avis, touch: true, optional: false
belongs_to :experts_procedure, optional: false belongs_to :experts_procedure, optional: false
@ -31,16 +30,16 @@ class Avis < ApplicationRecord
FILE_MAX_SIZE = 20.megabytes FILE_MAX_SIZE = 20.megabytes
validates :piece_justificative_file, validates :piece_justificative_file,
content_type: AUTHORIZED_CONTENT_TYPES, content_type: AUTHORIZED_CONTENT_TYPES,
size: file_size_validation(FILE_MAX_SIZE) size: { less_than: FILE_MAX_SIZE }
validates :introduction_file, validates :introduction_file,
content_type: AUTHORIZED_CONTENT_TYPES, content_type: AUTHORIZED_CONTENT_TYPES,
size: file_size_validation(FILE_MAX_SIZE) size: { less_than: FILE_MAX_SIZE }
validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, allow_nil: true validates :email, format: { with: Devise.email_regexp, message: "n'est pas valide" }, allow_nil: true
validates :claimant, presence: true validates :claimant, presence: true
validates :piece_justificative_file, size: file_size_validation(FILE_MAX_SIZE) validates :piece_justificative_file, size: { less_than: FILE_MAX_SIZE }
validates :introduction_file, size: file_size_validation(FILE_MAX_SIZE) validates :introduction_file, size: { less_than: FILE_MAX_SIZE }
before_validation -> { sanitize_email(:email) } before_validation -> { sanitize_email(:email) }
default_scope { joins(:dossier) } default_scope { joins(:dossier) }

View file

@ -20,11 +20,10 @@
# type_de_champ_id :integer # type_de_champ_id :integer
# #
class Champs::PieceJustificativeChamp < Champ class Champs::PieceJustificativeChamp < Champ
include FileValidationConcern
FILE_MAX_SIZE = 200.megabytes FILE_MAX_SIZE = 200.megabytes
validates :piece_justificative_file, validates :piece_justificative_file,
size: file_size_validation(FILE_MAX_SIZE), size: { less_than: FILE_MAX_SIZE },
if: -> { !type_de_champ.skip_pj_validation } if: -> { !type_de_champ.skip_pj_validation }
validates :piece_justificative_file, validates :piece_justificative_file,

View file

@ -20,20 +20,9 @@
# type_de_champ_id :integer # type_de_champ_id :integer
# #
class Champs::TitreIdentiteChamp < Champ class Champs::TitreIdentiteChamp < Champ
include FileValidationConcern
FILE_MAX_SIZE = 20.megabytes FILE_MAX_SIZE = 20.megabytes
ACCEPTED_FORMATS = ['image/png', 'image/jpeg']
ACCEPTED_FORMATS = [ validates :piece_justificative_file, content_type: ACCEPTED_FORMATS, size: { less_than: FILE_MAX_SIZE }
"image/png",
"image/jpeg"
]
# TODO: once we're running on Rails 6, re-enable this validation.
# See https://github.com/betagouv/demarches-simplifiees.fr/issues/4926
#
validates :piece_justificative_file,
content_type: ACCEPTED_FORMATS,
size: file_size_validation(FILE_MAX_SIZE)
def main_value_name def main_value_name
:piece_justificative_file :piece_justificative_file

View file

@ -13,7 +13,6 @@
# instructeur_id :bigint # instructeur_id :bigint
# #
class Commentaire < ApplicationRecord class Commentaire < ApplicationRecord
include FileValidationConcern
include Discard::Model include Discard::Model
self.ignored_columns = [:user_id] self.ignored_columns = [:user_id]
@ -31,7 +30,7 @@ class Commentaire < ApplicationRecord
FILE_MAX_SIZE = 20.megabytes FILE_MAX_SIZE = 20.megabytes
validates :piece_jointe, validates :piece_jointe,
content_type: AUTHORIZED_CONTENT_TYPES, content_type: AUTHORIZED_CONTENT_TYPES,
size: file_size_validation(FILE_MAX_SIZE) size: { less_than: FILE_MAX_SIZE }
default_scope { order(created_at: :asc) } default_scope { order(created_at: :asc) }
scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) } scope :updated_since?, -> (date) { where('commentaires.updated_at > ?', date) }

View file

@ -1,12 +0,0 @@
module FileValidationConcern
extend ActiveSupport::Concern
class_methods do
# This method works around missing `%{min_size}` and `%{max_size}` variables in active_record_validation
# default error message.
#
# Hopefully this will be fixed upstream in https://github.com/igorkasyanchuk/active_storage_validations/pull/134
def file_size_validation(file_max_size = 200.megabytes)
{ less_than: file_max_size, message: I18n.t('errors.messages.file_size_out_of_range', file_size_limit: ActiveSupport::NumberHelper.number_to_human_size(file_max_size)) }
end
end
end

View file

@ -10,6 +10,7 @@
# conservation_extension :interval default(0 seconds) # conservation_extension :interval default(0 seconds)
# declarative_triggered_at :datetime # declarative_triggered_at :datetime
# deleted_user_email_never_send :string # deleted_user_email_never_send :string
# depose_at :datetime
# en_construction_at :datetime # en_construction_at :datetime
# en_construction_close_to_expiration_notice_sent_at :datetime # en_construction_close_to_expiration_notice_sent_at :datetime
# en_instruction_at :datetime # en_instruction_at :datetime
@ -60,8 +61,11 @@ class Dossier < ApplicationRecord
REMAINING_DAYS_BEFORE_CLOSING = 2 REMAINING_DAYS_BEFORE_CLOSING = 2
INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days" INTERVAL_BEFORE_CLOSING = "#{REMAINING_DAYS_BEFORE_CLOSING} days"
INTERVAL_BEFORE_EXPIRATION = '2 weeks' REMAINING_WEEKS_BEFORE_EXPIRATION = 2
INTERVAL_EXPIRATION = '1 month 5 days' INTERVAL_BEFORE_EXPIRATION = "#{REMAINING_WEEKS_BEFORE_EXPIRATION} weeks"
MONTHS_AFTER_EXPIRATION = 1
DAYS_AFTER_EXPIRATION = 5
INTERVAL_EXPIRATION = "#{MONTHS_AFTER_EXPIRATION} month #{DAYS_AFTER_EXPIRATION} days"
has_one :etablissement, dependent: :destroy has_one :etablissement, dependent: :destroy
has_one :individual, validate: false, dependent: :destroy has_one :individual, validate: false, dependent: :destroy
@ -286,25 +290,39 @@ class Dossier < ApplicationRecord
.where.not(user_id: nil) .where.not(user_id: nil)
end end
scope :interval_brouillon_close_to_expiration, -> do
state_brouillon.where("dossiers.created_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :interval_en_construction_close_to_expiration, -> do
state_en_construction.where("dossiers.en_construction_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :interval_en_instruction_close_to_expiration, -> do
state_en_instruction.where("dossiers.en_instruction_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end
scope :interval_termine_close_to_expiration, -> do
state_termine.where(id: Traitement.termine_close_to_expiration.select(:dossier_id).distinct)
end
scope :brouillon_close_to_expiration, -> do scope :brouillon_close_to_expiration, -> do
state_brouillon joins(:procedure).interval_brouillon_close_to_expiration
.joins(:procedure)
.where("dossiers.created_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end end
scope :en_construction_close_to_expiration, -> do scope :en_construction_close_to_expiration, -> do
state_en_construction joins(:procedure).interval_en_construction_close_to_expiration
.joins(:procedure)
.where("dossiers.en_construction_at + dossiers.conservation_extension + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end end
scope :en_instruction_close_to_expiration, -> do scope :en_instruction_close_to_expiration, -> do
state_en_instruction joins(:procedure).interval_en_instruction_close_to_expiration
.joins(:procedure)
.where("dossiers.en_instruction_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
end end
scope :termine_close_to_expiration, -> do scope :termine_close_to_expiration, -> do
state_termine joins(:procedure).interval_termine_close_to_expiration
.joins(:procedure) end
.where(id: Traitement.termine_close_to_expiration.select(:dossier_id).distinct)
scope :close_to_expiration, -> do
joins(:procedure).scoping do
interval_brouillon_close_to_expiration
.or(interval_en_construction_close_to_expiration)
.or(interval_en_instruction_close_to_expiration)
.or(interval_termine_close_to_expiration)
end
end end
scope :brouillon_expired, -> do scope :brouillon_expired, -> do
@ -522,16 +540,50 @@ class Dossier < ApplicationRecord
!brouillon? && !user_deleted? && !archived !brouillon? && !user_deleted? && !archived
end end
def en_construction_close_to_expiration? def expirable?
self.class.en_construction_close_to_expiration.exists?(id: self) [brouillon?, en_construction?, termine?].any?
end end
def brouillon_close_to_expiration? def approximative_expiration_date_reference
self.class.brouillon_close_to_expiration.exists?(id: self) if brouillon?
created_at
elsif en_construction?
en_construction_at
elsif termine?
processed_at
else
fail "approximative_expiration_date_reference should not be called in state #{self.state}"
end
end
def approximative_expiration_date
[
approximative_expiration_date_reference,
conservation_extension,
procedure.duree_conservation_dossiers_dans_ds.months
].sum - REMAINING_WEEKS_BEFORE_EXPIRATION.weeks
end end
def close_to_expiration? def close_to_expiration?
en_construction_close_to_expiration? || brouillon_close_to_expiration? approximative_expiration_date < Time.zone.now
end
def expiration_date
if brouillon? && brouillon_close_to_expiration_notice_sent_at.present?
brouillon_close_to_expiration_notice_sent_at + duration_after_notice
elsif en_construction? && en_construction_close_to_expiration_notice_sent_at.present?
en_construction_close_to_expiration_notice_sent_at + duration_after_notice
elsif termine? && termine_close_to_expiration_notice_sent_at.present?
termine_close_to_expiration_notice_sent_at + duration_after_notice
end
end
def duration_after_notice
MONTHS_AFTER_EXPIRATION.month + DAYS_AFTER_EXPIRATION.days
end
def expiration_can_be_extended?
brouillon? || en_construction?
end end
def show_groupe_instructeur_details? def show_groupe_instructeur_details?
@ -725,7 +777,7 @@ class Dossier < ApplicationRecord
def after_passer_en_construction def after_passer_en_construction
self.conservation_extension = 0.days self.conservation_extension = 0.days
self.en_construction_at = self.traitements self.depose_at = self.en_construction_at = self.traitements
.passer_en_construction .passer_en_construction
.processed_at .processed_at
save! save!
@ -758,6 +810,8 @@ class Dossier < ApplicationRecord
end end
def after_repasser_en_construction(instructeur) def after_repasser_en_construction(instructeur)
create_missing_traitemets
self.en_construction_close_to_expiration_notice_sent_at = nil self.en_construction_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days self.conservation_extension = 0.days
self.en_construction_at = self.traitements self.en_construction_at = self.traitements
@ -768,6 +822,8 @@ class Dossier < ApplicationRecord
end end
def after_repasser_en_instruction(instructeur, disable_notification: false) def after_repasser_en_instruction(instructeur, disable_notification: false)
create_missing_traitemets
self.archived = false self.archived = false
self.termine_close_to_expiration_notice_sent_at = nil self.termine_close_to_expiration_notice_sent_at = nil
self.conservation_extension = 0.days self.conservation_extension = 0.days
@ -1037,6 +1093,16 @@ class Dossier < ApplicationRecord
private private
def create_missing_traitemets
if en_construction_at.present? && traitements.en_construction.empty?
self.traitements.passer_en_construction(processed_at: en_construction_at)
self.depose_at ||= en_construction_at
end
if en_instruction_at.present? && traitements.en_instruction.empty?
self.traitements.passer_en_instruction(processed_at: en_instruction_at)
end
end
def deleted_dossier def deleted_dossier
@deleted_dossier ||= DeletedDossier.find_by(dossier_id: id) @deleted_dossier ||= DeletedDossier.find_by(dossier_id: id)
end end

View file

@ -8,6 +8,7 @@
# login_token_created_at :datetime # login_token_created_at :datetime
# created_at :datetime # created_at :datetime
# updated_at :datetime # updated_at :datetime
# agent_connect_id :string
# #
class Instructeur < ApplicationRecord class Instructeur < ApplicationRecord
has_many :administrateurs_instructeurs has_many :administrateurs_instructeurs

View file

@ -54,7 +54,6 @@ class Procedure < ApplicationRecord
self.ignored_columns = [:duree_conservation_dossiers_hors_ds] self.ignored_columns = [:duree_conservation_dossiers_hors_ds]
include ProcedureStatsConcern include ProcedureStatsConcern
include EncryptableConcern include EncryptableConcern
include FileValidationConcern
include Discard::Model include Discard::Model
self.discard_column = :hidden_at self.discard_column = :hidden_at
@ -235,7 +234,6 @@ class Procedure < ApplicationRecord
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
validates :lien_site_web, presence: true, if: :publiee? validates :lien_site_web, presence: true, if: :publiee?
validate :validate_for_publication, on: :publication
validate :check_juridique validate :check_juridique
validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false } validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false }
validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION } validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }
@ -254,7 +252,7 @@ class Procedure < ApplicationRecord
"image/jpg", "image/jpg",
"image/png", "image/png",
"text/plain" "text/plain"
], size: file_size_validation(FILE_MAX_SIZE), if: -> { new_record? || created_at > Date.new(2020, 2, 28) } ], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 2, 28) }
validates :deliberation, content_type: [ validates :deliberation, content_type: [
"application/msword", "application/msword",
@ -265,11 +263,11 @@ class Procedure < ApplicationRecord
"image/jpg", "image/jpg",
"image/png", "image/png",
"text/plain" "text/plain"
], size: file_size_validation(FILE_MAX_SIZE), if: -> { new_record? || created_at > Date.new(2020, 4, 29) } ], size: { less_than: FILE_MAX_SIZE }, if: -> { new_record? || created_at > Date.new(2020, 4, 29) }
LOGO_MAX_SIZE = 5.megabytes LOGO_MAX_SIZE = 5.megabytes
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'],
size: file_size_validation(LOGO_MAX_SIZE), size: { less_than: LOGO_MAX_SIZE },
if: -> { new_record? || created_at > Date.new(2020, 11, 13) } if: -> { new_record? || created_at > Date.new(2020, 11, 13) }
validates :api_entreprise_token, jwt_token: true, allow_blank: true validates :api_entreprise_token, jwt_token: true, allow_blank: true
@ -325,18 +323,6 @@ class Procedure < ApplicationRecord
end end
end end
def validate_for_publication
old_attributes = self.slice(:aasm_state, :closed_at)
self.aasm_state = :publiee
self.closed_at = nil
is_valid = validate
self.attributes = old_attributes
is_valid
end
def suggested_path(administrateur) def suggested_path(administrateur)
if path_customized? if path_customized?
return path return path
@ -692,7 +678,9 @@ class Procedure < ApplicationRecord
end end
def create_new_revision def create_new_revision
draft_revision.deep_clone(include: [:revision_types_de_champ, :revision_types_de_champ_private]) draft_revision
.deep_clone(include: [:revision_types_de_champ, :revision_types_de_champ_private])
.tap(&:save!)
end end
def average_dossier_weight def average_dossier_weight

View file

@ -17,14 +17,6 @@ class Traitement < ApplicationRecord
scope :en_instruction, -> { where(state: Dossier.states.fetch(:en_instruction)) } scope :en_instruction, -> { where(state: Dossier.states.fetch(:en_instruction)) }
scope :termine, -> { where(state: Dossier::TERMINE) } scope :termine, -> { where(state: Dossier::TERMINE) }
scope :termine_close_to_expiration, -> do
joins(dossier: :procedure)
.termine
.where(process_expired: true)
.where('dossiers.state' => Dossier::TERMINE)
.where("traitements.processed_at + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: Dossier::INTERVAL_BEFORE_EXPIRATION })
end
scope :for_traitement_time_stats, -> (procedure) do scope :for_traitement_time_stats, -> (procedure) do
includes(:dossier) includes(:dossier)
.termine .termine
@ -33,6 +25,14 @@ class Traitement < ApplicationRecord
.order(:processed_at) .order(:processed_at)
end end
scope :termine_close_to_expiration, -> do
joins(dossier: :procedure)
.termine
.where(process_expired: true)
.where('dossiers.state' => Dossier::TERMINE)
.where("traitements.processed_at + (procedures.duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: Dossier::INTERVAL_BEFORE_EXPIRATION })
end
def self.count_dossiers_termines_by_month(groupe_instructeurs) def self.count_dossiers_termines_by_month(groupe_instructeurs)
last_traitements_per_dossier = Traitement last_traitements_per_dossier = Traitement
.select('max(traitements.processed_at) as processed_at') .select('max(traitements.processed_at) as processed_at')

View file

@ -0,0 +1,24 @@
class AgentConnectService
def self.enabled?
ENV.fetch("AGENT_CONNECT_ENABLED", "enabled") == "enabled"
end
def self.authorization_uri
client = AgentConnectClient.new
client.authorization_uri(
scope: [:openid, :email],
state: SecureRandom.hex(16),
nonce: SecureRandom.hex(16),
acr_values: 'eidas1'
)
end
def self.user_info(code)
client = AgentConnectClient.new(code)
client.access_token!(client_auth_method: :secret)
.userinfo!
.raw_attributes
end
end

View file

@ -0,0 +1,8 @@
- content_for(:title, t('.cta'))
.container
%h1.mt-2.mb-2= t('.connect')
%p= t('.intro_html', app_name: APPLICATION_NAME)
= link_to t('.cta'), agent_connect_login_path, class: "france-connect-agent-login-button"

View file

@ -30,14 +30,5 @@
autocomplete: 'off', autocomplete: 'off',
placeholder: 'https://exemple.gouv.fr/ma_demarche') placeholder: 'https://exemple.gouv.fr/ma_demarche')
- procedure.validate(:publication)
- errors = procedure.errors
-# Ignore the :taken error if the path can be claimed
- if errors.details[:path]&.pluck(:error)&.include?(:taken) && procedure.path_available?(administrateur, procedure.path)
- errors.delete(:path)
- options = { class: "button primary", id: 'publish' }
- if errors.details[:path].present?
- options[:disabled] = :disabled
.flex.justify-end .flex.justify-end
= submit_tag procedure_publish_label(procedure, :submit), options = submit_tag procedure_publish_label(procedure, :submit), { class: "button primary", id: 'publish' }

View file

@ -0,0 +1,14 @@
- if dossier.expirable? && dossier.close_to_expiration?
.card.warning.mt-2.mb-3
.card-title= t('shared.dossiers.header.banner.title')
%p
- if dossier.brouillon?
= t('shared.dossiers.header.banner.states.brouillon')
- elsif dossier.en_construction?
= t('shared.dossiers.header.banner.states.en_construction')
- elsif dossier.termine?
= t('shared.dossiers.header.banner.states.termine')
- if dossier.expiration_can_be_extended?
%br
= button_to t('shared.dossiers.header.banner.button_delay_expiration'), users_dossier_repousser_expiration_path(dossier), class: 'button secondary mt-2'

View file

@ -1,6 +1,14 @@
%h1 = status_badge(dossier.state)
= procedure_libelle(dossier.procedure) .title-container
%span.icon.folder
%h1= procedure_libelle(dossier.procedure)
%h2
= t('views.users.dossiers.show.header.dossier_number', dossier_id: dossier.id)
= t('views.users.dossiers.show.header.created_date', date_du_dossier: I18n.l(dossier.created_at))
= render(partial: 'shared/dossiers/short_expires_message', locals: {dossier: dossier})
.header-actions
- if current_user.owns?(dossier)
= render partial: 'invites/dropdown', locals: { dossier: dossier }
.dossier-form-actions
- if current_user.owns?(dossier)
= render partial: 'invites/dropdown', locals: { dossier: dossier }

View file

@ -0,0 +1,9 @@
- if dossier.expirable?
%p.expires_at
%small= t("shared.dossiers.header.expires_at.#{dossier.state}", date: safe_expiration_date(dossier))
- else
%p.expires_at_en_instruction
%small= t("shared.dossiers.header.expires_at.en_instruction")
= render(partial: 'shared/dossiers/expiration_banner', locals: {dossier: dossier})

View file

@ -4,7 +4,7 @@
- content_for :footer do - content_for :footer do
= render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier } = render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier }
#dossier-draft .dossier-container
.dossier-header.sub-header .dossier-header.sub-header
.container .container
= render partial: "shared/dossiers/header", locals: { dossier: @dossier, apercu: false } = render partial: "shared/dossiers/header", locals: { dossier: @dossier, apercu: false }

View file

@ -3,7 +3,7 @@
- content_for :footer do - content_for :footer do
= render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier } = render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier }
#dossier-show .dossier-container.mb-4
= render partial: 'users/dossiers/show/header', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/header', locals: { dossier: @dossier }
= render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' } = render partial: 'shared/dossiers/demande', locals: { dossier: @dossier, demande_seen_at: nil, profile: 'usager' }

View file

@ -39,6 +39,12 @@
active: @statut == 'dossiers-transferes', active: @statut == 'dossiers-transferes',
badge: number_with_html_delimiter(@dossier_transfers.count)) badge: number_with_html_delimiter(@dossier_transfers.count))
- if @dossiers_close_to_expiration.count > 0
= tab_item(t('pluralize.dossiers_close_to_expiration', count: @dossiers_close_to_expiration.count),
dossiers_path(statut: 'dossiers-expirant'),
active: @statut == 'dossiers-expirant',
badge: number_with_html_delimiter(@dossiers_close_to_expiration.count))
.container .container
- if @statut == "mes-dossiers" - if @statut == "mes-dossiers"
= render partial: "dossiers_list", locals: { dossiers: @user_dossiers } = render partial: "dossiers_list", locals: { dossiers: @user_dossiers }
@ -48,6 +54,8 @@
- if @statut == "dossiers-supprimes" - if @statut == "dossiers-supprimes"
= render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes } = render partial: "deleted_dossiers_list", locals: { deleted_dossiers: @dossiers_supprimes }
- if @statut == "dossiers-transferes" - if @statut == "dossiers-transferes"
= render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossier_transfers } = render partial: "transfered_dossiers_list", locals: { dossier_transfers: @dossier_transfers }
- if @statut == "dossiers-expirant"
= render partial: "dossiers_list", locals: { dossiers: @dossiers_close_to_expiration }

View file

@ -3,7 +3,7 @@
- content_for :footer do - content_for :footer do
= render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier } = render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier }
#dossier-show .dossier-container.mb-4
= render partial: 'users/dossiers/show/header', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/header', locals: { dossier: @dossier }
.container .container

View file

@ -3,7 +3,7 @@
- content_for :footer do - content_for :footer do
= render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier } = render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier }
#dossier-show .dossier-container.mb-4
= render partial: 'users/dossiers/show/header', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/header', locals: { dossier: @dossier }
.container .container

View file

@ -3,7 +3,7 @@
- content_for :footer do - content_for :footer do
= render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier } = render partial: "users/procedure_footer", locals: { procedure: @dossier.procedure, dossier: @dossier }
#dossier-show .dossier-container.mb-4
= render partial: 'users/dossiers/show/header', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/header', locals: { dossier: @dossier }
.container .container

View file

@ -10,6 +10,9 @@
- if dossier.en_construction_at.present? - if dossier.en_construction_at.present?
= t('views.users.dossiers.show.header.submit_date', date_du_dossier: I18n.l(dossier.en_construction_at)) = t('views.users.dossiers.show.header.submit_date', date_du_dossier: I18n.l(dossier.en_construction_at))
= render(partial: 'shared/dossiers/short_expires_message', locals: {dossier: dossier})
- if current_user.owns?(dossier) - if current_user.owns?(dossier)
.header-actions .header-actions
= render partial: 'invites/dropdown', locals: { dossier: dossier } = render partial: 'invites/dropdown', locals: { dossier: dossier }
@ -22,21 +25,6 @@
%li %li
= link_to t('views.users.dossiers.show.header.print_dossier'), dossier_path(dossier, format: :pdf), target: "_blank", rel: "noopener", class: "menu-item menu-link" = link_to t('views.users.dossiers.show.header.print_dossier'), dossier_path(dossier, format: :pdf), target: "_blank", rel: "noopener", class: "menu-item menu-link"
- if dossier.close_to_expiration?
.card.warning
.card-title Votre dossier va expirer
- if dossier.brouillon?
%p
Votre dossier est en brouillon, mais va bientôt expirer. Cela signifie quil va bientôt être supprimé sans avoir été déposé.
Si vous souhaitez le conserver afin de poursuivre la démarche, vous pouvez le conserver
un mois de plus en cliquant sur le bouton ci-dessous.
- else
%p
Votre dossier a été déposé, mais va bientôt expirer. Cela signifie quil va bientôt être supprimé sans avoir été traité par ladministration.
Si vous souhaitez le conserver afin de poursuivre la démarche, vous pouvez le conserver
un mois de plus en cliquant sur le bouton ci-dessous.
%br
= button_to 'Repousser sa suppression', users_dossier_repousser_expiration_path(dossier), class: 'button secondary'
%ul.tabs %ul.tabs
= dynamic_tab_item(t('views.users.dossiers.show.header.summary'), dossier_path(dossier)) = dynamic_tab_item(t('views.users.dossiers.show.header.summary'), dossier_path(dossier))

View file

@ -23,7 +23,13 @@
= f.submit t('views.users.sessions.new.connection'), class: "button large primary expand" = f.submit t('views.users.sessions.new.connection'), class: "button large primary expand"
%hr .france-connect-login-separator
= t('views.shared.france_connect_login.separator')
- if AgentConnectService.enabled?
.center
%p.mb-2= t('views.users.sessions.new.instructor_or_admin')
= link_to t('views.users.sessions.new.connect_with_agent_connect'), agent_connect_path
%hr
%p.center %p.center
%span= t('views.users.sessions.new.are_you_new', app_name: APPLICATION_NAME.gsub("-","&#8209;")).html_safe %span= t('views.users.sessions.new.are_you_new', app_name: APPLICATION_NAME.gsub("-","&#8209;")).html_safe
%br %br

View file

@ -45,6 +45,13 @@ FC_PARTICULIER_ID=""
FC_PARTICULIER_SECRET="" FC_PARTICULIER_SECRET=""
FC_PARTICULIER_BASE_URL="" FC_PARTICULIER_BASE_URL=""
# Service externe: authentification Agent Connect
AGENT_CONNECT_ID=""
AGENT_CONNECT_SECRET=""
AGENT_CONNECT_BASE_URL=""
AGENT_CONNECT_JWKS=""
AGENT_CONNECT_REDIRECT=""
# Service externe: Support Utilisateur HelpScout | Spécifique démarches-simplifiées.fr # Service externe: Support Utilisateur HelpScout | Spécifique démarches-simplifiées.fr
HELPSCOUT_MAILBOX_ID="" HELPSCOUT_MAILBOX_ID=""
HELPSCOUT_CLIENT_ID="" HELPSCOUT_CLIENT_ID=""

View file

@ -11,6 +11,9 @@ DS_ENV="staging"
# Utilisation de France Connect # Utilisation de France Connect
# FRANCE_CONNECT_ENABLED="disabled" # "enabled" par défaut # FRANCE_CONNECT_ENABLED="disabled" # "enabled" par défaut
# Utilisation de Agent Connect
# AGENT_CONNECT_ENABLED="disabled" # "enabled" par défaut
# Personnalisation d'instance - URLs des CGU et des mentions légales # Personnalisation d'instance - URLs des CGU et des mentions légales
# CGU_URL="" # CGU_URL=""
# MENTIONS_LEGALES_URL="" # MENTIONS_LEGALES_URL=""

View file

@ -101,6 +101,7 @@ ignore_unused:
- 'activerecord.errors.*' - 'activerecord.errors.*'
- 'errors.messages.blank' - 'errors.messages.blank'
- 'errors.messages.content_type_invalid' - 'errors.messages.content_type_invalid'
- 'errors.messages.file_size_out_of_range'
- 'pluralize.*' - 'pluralize.*'
- 'views.pagination.*' - 'views.pagination.*'
- 'time.formats.default' - 'time.formats.default'

View file

@ -0,0 +1 @@
AGENT_CONNECT = Rails.application.secrets.agent_connect

View file

@ -3,3 +3,59 @@ OpenIDConnect.logger = Rails.logger
Rack::OAuth2.logger = Rails.logger Rack::OAuth2.logger = Rails.logger
# Webfinger.logger = Rails.logger # Webfinger.logger = Rails.logger
SWD.logger = Rails.logger SWD.logger = Rails.logger
# the openid_connect gem does not support
# jwt format in the userinfo call.
# A PR is open to improve the situation
# https://github.com/nov/openid_connect/pull/54
module OpenIDConnect
class AccessToken < Rack::OAuth2::AccessToken::Bearer
private
def jwk_loader
JSON.parse(URI.parse(ENV['AGENT_CONNECT_JWKS']).read).deep_symbolize_keys
end
def decode_jwt(requested_host, jwt)
agent_connect_host = URI.parse(ENV['AGENT_CONNECT_BASE_URL']).host
if requested_host == agent_connect_host
# rubocop:disable Lint/UselessAssignment
JWT.decode(jwt, key = nil, verify = true, { algorithms: ['ES256'], jwks: jwk_loader })[0]
# rubocop:enable Lint/UselessAssignment
else
raise "unknwon host : #{requested_host}"
end
end
def resource_request
res = yield
case res.status
when 200
hash = case parse_type_and_subtype(res.content_type)
when 'application/jwt'
requested_host = URI.parse(client.userinfo_endpoint).host
decode_jwt(requested_host, res.body)
when 'application/json'
JSON.parse(res.body)
end
hash&.with_indifferent_access
when 400
raise BadRequest.new('API Access Faild', res)
when 401
raise Unauthorized.new('Access Token Invalid or Expired', res)
when 403
raise Forbidden.new('Insufficient Scope', res)
else
raise HttpError.new(res.status, 'Unknown HttpError', res)
end
end
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
# - type and subtype are the first member
# they are case insensitive
def parse_type_and_subtype(content_type)
content_type.split(';')[0].strip.downcase
end
end
end

View file

@ -1,5 +1,5 @@
en: en:
errors: errors:
messages: messages:
content_type_invalid: "is not of an accepted type" content_type_invalid: is not of an accepted type
file_size_out_of_range: "is too big. The file must be at most %{file_size_limit}." file_size_out_of_range: is too big. The file must be at most %{max_size}.

View file

@ -1,5 +1,5 @@
fr: fr:
errors: errors:
messages: messages:
content_type_invalid: "nest pas dun type accepté" content_type_invalid: nest pas dun type accepté
file_size_out_of_range: "est trop lourd(e). Le fichier doit faire au plus %{file_size_limit}." file_size_out_of_range: est trop lourd(e). Le fichier doit faire au plus %{max_size}.

View file

@ -160,6 +160,7 @@ en:
request: "Request" request: "Request"
mailbox: "Mailbox" mailbox: "Mailbox"
dossier_number: "File n. %{dossier_id}" dossier_number: "File n. %{dossier_id}"
created_date: "- Draft on %{date_du_dossier}"
submit_date: "- Submit on %{date_du_dossier}" submit_date: "- Submit on %{date_du_dossier}"
print: "print" print: "print"
print_dossier: "All the file" print_dossier: "All the file"
@ -203,6 +204,8 @@ en:
connection: Sign in connection: Sign in
are_you_new: First time on %{app_name}? are_you_new: First time on %{app_name}?
find_procedure: Find your procedure find_procedure: Find your procedure
instructor_or_admin: Instructor or Administrator ?
connect_with_agent_connect: Connect with AgentConnect
passwords: passwords:
reset_link_sent: reset_link_sent:
got_it: Got it! got_it: Got it!
@ -339,6 +342,10 @@ en:
zero: transfer request zero: transfer request
one: transfer request one: transfer request
other: transfer requests other: transfer requests
dossiers_close_to_expiration:
zero: expiring file
one: expiring file
other: expiring files
dossier_trouve: dossier_trouve:
zero: 0 file found zero: 0 file found
one: 1 file found one: 1 file found

View file

@ -156,6 +156,7 @@ fr:
request: "Demande" request: "Demande"
mailbox: "Messagerie" mailbox: "Messagerie"
dossier_number: "Dossier nº %{dossier_id}" dossier_number: "Dossier nº %{dossier_id}"
created_date: "- En brouillon depuis le %{date_du_dossier}"
submit_date: "- Déposé le %{date_du_dossier}" submit_date: "- Déposé le %{date_du_dossier}"
print: "imprimer" print: "imprimer"
print_dossier: "Tout le dossier" print_dossier: "Tout le dossier"
@ -199,6 +200,8 @@ fr:
connection: Se connecter connection: Se connecter
are_you_new: Vous êtes nouveau sur %{app_name} ? are_you_new: Vous êtes nouveau sur %{app_name} ?
find_procedure: Trouvez votre démarche find_procedure: Trouvez votre démarche
instructor_or_admin: Vous êtes instructeur ou administrateur ?
connect_with_agent_connect: Se connecter avec AgentConnect
passwords: passwords:
reset_link_sent: reset_link_sent:
email_sent_html: "Nous vous avons envoyé un email à ladresse <strong>%{email}</strong>." email_sent_html: "Nous vous avons envoyé un email à ladresse <strong>%{email}</strong>."
@ -347,6 +350,10 @@ fr:
zero: demande de transfert zero: demande de transfert
one: demande de transfert one: demande de transfert
other: demandes de transfert other: demandes de transfert
dossiers_close_to_expiration:
zero: dossier expirant
one: dossier expirant
other: dossiers expirant
dossier_trouve: dossier_trouve:
zero: 0 dossier trouvé zero: 0 dossier trouvé
one: 1 dossier trouvé one: 1 dossier trouvé

View file

@ -7,6 +7,21 @@ en:
numero_allocataire_notice: It is usually composed of 7 digits. numero_allocataire_notice: It is usually composed of 7 digits.
code_postal_label: postal code code_postal_label: postal code
code_postal_notice: It is usually composed of 5 digits. code_postal_notice: It is usually composed of 5 digits.
header:
expires_at:
brouillon: "Expires at %{date}"
en_construction: "Expires at %{date}"
en_instruction: "This file is being instructed, the administration will answer as soon as possible"
accepte: "Expires at %{date}"
refuse: "Expires at %{date}"
sans_suite: "Expires at %{date}"
banner:
title: Your file will expire
states:
brouillon: Your file is still in draft and will soon expire. So it will be deleted soon without being instructed. If you want to pursue your procedure you can submit it now. Otherwise you are able to delay its expiration by clicking on the underneath button.
en_construction: Your file is pending for instruction. The maximum delay is 6 months, but your can extend the duration by a month by clicking on the underneath button.
termine: Your file had been processed and will soon expire.So it will be deleted soon. If you want to keep it, your can dowload a PDF file of it.
button_delay_expiration: "Delay deletion"
champs: champs:
cnaf: cnaf:
show: show:

View file

@ -7,6 +7,22 @@ fr:
numero_allocataire_notice: Il est généralement composé de 7 chiffres. numero_allocataire_notice: Il est généralement composé de 7 chiffres.
code_postal_label: Le code postal code_postal_label: Le code postal
code_postal_notice: Il est généralement composé de 5 chiffres. code_postal_notice: Il est généralement composé de 5 chiffres.
header:
expires_at:
brouillon: "Expirera le %{date}"
en_construction: "Expirera le %{date}"
en_instruction: "Ce dossier est en instruction, il n'expirera pas"
accepte: "Expirera le %{date}"
refuse: "Expirera le %{date}"
sans_suite: "Expirera le %{date}"
banner:
title: Votre dossier va expirer
states:
brouillon: Votre dossier est en brouillon, mais va bientôt expirer. Cela signifie quil va bientôt être supprimé sans avoir été déposé. Si vous souhaitez le conserver afin de poursuivre la démarche, vous pouvez le conserver un mois de plus en cliquant sur le bouton ci-dessous.
en_construction: Votre dossier est en attente de prise en charge par l'administration. Le delais de prise en charge maximale est de 6 mois. Vous pouvez toutefois entendre cette durée d'un mois en cliquant sur le bouton suivant.
termine: Le traitement de votre dossier est terminé, mais il va bientôt expirer. Cela signifie quil va bientôt être supprimé. Si vous souhaitez conserver une trace, vous pouvez le télécharger au format PDF.
button_delay_expiration: "Repousser sa suppression"
champs: champs:
cnaf: cnaf:
show: show:

View file

@ -0,0 +1,11 @@
en:
agent_connect:
agent:
index:
connect: Connect with AgentConnect
intro_html: |
AgentConnect allows <b class='bold'>instructors et administrators</b> to use their usual login credentials to connect to %{app_name}.
<br />
<br />
Only agents of <b class='bold'>the Ministry of Ecological Transition</b> can currently benefit from it.
cta: Connect with AgentConnect

View file

@ -0,0 +1,11 @@
fr:
agent_connect:
agent:
index:
connect: Connectez-vous avec AgentConnect
intro_html: |
AgentConnect permet aux <b class='bold'>instructeurs et administrateurs</b> dutiliser leurs identifiants habituels pour se connecter à %{app_name}.
<br />
<br />
Seul les agents du <b class='bold'>ministère de la Transition écologique</b> peuvent actuellement en bénéficier.
cta: Sidentifier avec AgentConnect

View file

@ -129,6 +129,12 @@ Rails.application.routes.draw do
post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account' post 'particulier/merge_with_new_account' => 'particulier#merge_with_new_account'
end end
namespace :agent_connect do
get '' => 'agent#index'
get 'login' => 'agent#login'
get 'callback' => 'agent#callback'
end
namespace :champs do namespace :champs do
get ':position/siret', to: 'siret#show', as: :siret get ':position/siret', to: 'siret#show', as: :siret
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link

View file

@ -25,6 +25,14 @@ defaults: &defaults
token_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/token token_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/token
userinfo_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/userinfo userinfo_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/userinfo
logout_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/logout logout_endpoint: <%= ENV['FC_PARTICULIER_BASE_URL'] %>/api/v1/logout
agent_connect:
identifier: <%= ENV['AGENT_CONNECT_ID'] %>
secret: <%= ENV['AGENT_CONNECT_SECRET'] %>
redirect_uri: <%= ENV['AGENT_CONNECT_REDIRECT'] %>
authorization_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/authorize
token_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/token
userinfo_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/userinfo
logout_endpoint: <%= ENV['AGENT_CONNECT_BASE_URL'] %>/api/v2/session/end
mailjet: mailjet:
api_key: <%= ENV['MAILJET_API_KEY'] %> api_key: <%= ENV['MAILJET_API_KEY'] %>
secret_key: <%= ENV['MAILJET_SECRET_KEY'] %> secret_key: <%= ENV['MAILJET_SECRET_KEY'] %>

View file

@ -0,0 +1,6 @@
class AddAgentConnectSubColumnToInstructeursTable < ActiveRecord::Migration[6.1]
def change
add_column :instructeurs, :agent_connect_id, :string
add_index :instructeurs, :agent_connect_id, unique: true
end
end

View file

@ -0,0 +1,5 @@
class AddDeposeAtToDossiers < ActiveRecord::Migration[6.1]
def change
add_column :dossiers, :depose_at, :datetime
end
end

View file

@ -10,7 +10,8 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_11_15_112933) do ActiveRecord::Schema.define(version: 2021_11_24_111429) 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"
enable_extension "unaccent" enable_extension "unaccent"
@ -322,6 +323,7 @@ ActiveRecord::Schema.define(version: 2021_11_15_112933) do
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.bigint "dossier_transfer_id" t.bigint "dossier_transfer_id"
t.datetime "identity_updated_at" t.datetime "identity_updated_at"
t.datetime "depose_at"
t.index ["archived"], name: "index_dossiers_on_archived" t.index ["archived"], name: "index_dossiers_on_archived"
t.index ["dossier_transfer_id"], name: "index_dossiers_on_dossier_transfer_id" t.index ["dossier_transfer_id"], name: "index_dossiers_on_dossier_transfer_id"
t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id" t.index ["groupe_instructeur_id"], name: "index_dossiers_on_groupe_instructeur_id"
@ -534,6 +536,8 @@ ActiveRecord::Schema.define(version: 2021_11_15_112933) do
t.text "encrypted_login_token" t.text "encrypted_login_token"
t.datetime "login_token_created_at" t.datetime "login_token_created_at"
t.boolean "bypass_email_login_token", default: false, null: false t.boolean "bypass_email_login_token", default: false, null: false
t.string "agent_connect_id"
t.index ["agent_connect_id"], name: "index_instructeurs_on_agent_connect_id", unique: true
end end
create_table "invites", id: :serial, force: :cascade do |t| create_table "invites", id: :serial, force: :cascade do |t|

View file

@ -0,0 +1,22 @@
namespace :after_party do
desc 'Deployment task: add_depose_at_to_dossiers'
task add_depose_at_to_dossiers: :environment do
puts "Running deploy task 'add_depose_at_to_dossiers'"
dossiers = Dossier.includes(:traitements).where(depose_at: nil).where.not(en_construction_at: nil)
progress = ProgressReport.new(dossiers.count)
dossiers.find_each do |dossier|
traitement = dossier.traitements.find { |traitement| traitement.state == :en_construction }
depose_at = traitement&.processed_at || dossier.en_construction_at
dossier.update_column(:depose_at, depose_at)
progress.inc
end
progress.finish
# Update task as completed. If you remove the line below, the task will
# run with every deploy (or every time you call after_party:run).
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -0,0 +1,90 @@
describe AgentConnect::AgentController, type: :controller do
describe '#callback' do
let(:email) { 'i@email.com' }
subject { get :callback, params: { code: code } }
context 'when the callback code is correct' do
let(:code) { 'correct' }
let(:user_info) { { 'sub' => 'sub', 'email' => ' I@email.com' } }
context 'and user_info returns some info' do
before do
expect(AgentConnectService).to receive(:user_info).and_return(user_info)
end
context 'and the instructeur does not have an account yet' do
before do
expect(controller).to receive(:sign_in)
end
it 'creates the user, signs in and redirects to procedure_path' do
expect { subject }.to change { User.count }.by(1).and change { Instructeur.count }.by(1)
last_user = User.last
expect(last_user.email).to eq(email)
expect(last_user.confirmed_at).to be_present
expect(last_user.instructeur.agent_connect_id).to eq('sub')
expect(response).to redirect_to(instructeur_procedures_path)
end
end
context 'and the instructeur already has an account' do
let!(:instructeur) { create(:instructeur, email: email) }
before do
expect(controller).to receive(:sign_in)
end
it 'reuses the account, signs in and redirects to procedure_path' do
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(0)
instructeur.reload
expect(instructeur.agent_connect_id).to eq('sub')
expect(response).to redirect_to(instructeur_procedures_path)
end
end
context 'and the instructeur already has an account as a user' do
let!(:user) { create(:user, email: email) }
before do
expect(controller).to receive(:sign_in)
end
it 'reuses the account, signs in and redirects to procedure_path' do
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(1)
instructeur = user.reload.instructeur
expect(instructeur.agent_connect_id).to eq('sub')
expect(response).to redirect_to(instructeur_procedures_path)
end
end
end
context 'but user_info raises and error' do
before do
expect(AgentConnectService).to receive(:user_info).and_raise(Rack::OAuth2::Client::Error.new(500, error: 'Unknown'))
end
it 'aborts the processus' do
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(0)
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'when the callback code is blank' do
let(:code) { '' }
it 'aborts the processus' do
expect { subject }.to change { User.count }.by(0).and change { Instructeur.count }.by(0)
expect(response).to redirect_to(new_user_session_path)
end
end
end
end

View file

@ -0,0 +1,59 @@
RSpec.describe GraphqlOperationLogConcern, type: :controller do
class TestController < ActionController::Base
include GraphqlOperationLogConcern
end
controller TestController do
end
describe '#operation_log' do
let(:query) { nil }
let(:variables) { nil }
let(:operation_name) { nil }
subject { controller.operation_log(query, operation_name, variables) }
context 'with no query' do
it { expect(subject).to eq('NoQuery') }
end
context 'with invalid query' do
let(:query) { 'query { demarche {} }' }
it { expect(subject).to eq('InvalidQuery') }
end
context 'with two queries' do
let(:query) { 'query demarche { demarche } query dossier { dossier }' }
let(:operation_name) { 'dossier' }
it { expect(subject).to eq('query: dossier { dossier }') }
end
context 'with arguments' do
let(:query) { 'query demarche { demarche(number: 123) { id } }' }
it { expect(subject).to eq('query: demarche { demarche } number: "123"') }
end
context 'with variables' do
let(:query) { 'query { demarche(number: 123) { id } }' }
let(:variables) { { number: 124 } }
it { expect(subject).to eq('query: { demarche } number: "124"') }
end
context 'with mutation and arguments' do
let(:query) { 'mutation { passerDossierEnInstruction(input: { number: 123 }) { id } }' }
it { expect(subject).to eq('mutation: { passerDossierEnInstruction } number: "123"') }
end
context 'with mutation and variables' do
let(:query) { 'mutation { passerDossierEnInstruction(input: { number: 123 }) { id } }' }
let(:variables) { { input: { number: 124 } } }
it { expect(subject).to eq('mutation: { passerDossierEnInstruction } number: "124"') }
end
end
end

View file

@ -131,7 +131,7 @@ describe NewAdministrateur::ServicesController, type: :controller do
end end
it { expect(service.reload).not_to be_nil } it { expect(service.reload).not_to be_nil }
it { expect(flash.alert).to eq("la démarche #{procedure.libelle} utilise encore le service service. Veuillez l'affecter à un autre service avant de pouvoir le supprimer") } it { expect(flash.alert).to eq("la démarche #{procedure.libelle} utilise encore le service #{service.nom}. Veuillez l'affecter à un autre service avant de pouvoir le supprimer") }
it { expect(flash.notice).to be_nil } it { expect(flash.notice).to be_nil }
it { expect(response).to redirect_to(admin_services_path(procedure_id: 12)) } it { expect(response).to redirect_to(admin_services_path(procedure_id: 12)) }
end end

View file

@ -1,15 +1,35 @@
describe RechercheController, type: :controller do describe RechercheController, type: :controller do
let(:dossier) { create(:dossier, :en_construction, :with_populated_annotations) } let(:procedure) {
let(:dossier2) { create(:dossier, :en_construction, procedure: dossier.procedure) } create(:procedure,
:published,
:for_individual,
:with_type_de_champ,
:with_type_de_champ_private,
types_de_champ_count: 2,
types_de_champ_private_count: 2)
}
let(:dossier) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
let(:instructeur) { create(:instructeur) } let(:instructeur) { create(:instructeur) }
let(:dossier_with_expert) { avis.dossier } let(:dossier_with_expert) { create(:dossier, :en_construction, :with_individual, procedure: procedure) }
let(:avis) { create(:avis, dossier: create(:dossier, :en_construction, :with_populated_annotations)) } let(:avis) { create(:avis, dossier: dossier_with_expert) }
let(:user) { instructeur.user } let(:user) { instructeur.user }
before do before do
instructeur.assign_to_procedure(dossier.procedure) instructeur.assign_to_procedure(dossier.procedure)
dossier.champs[0].value = "Name of district A"
dossier.champs[1].value = "Name of city A"
dossier.champs_private[0].value = "Dossier A is complete"
dossier.champs_private[1].value = "Dossier A is valid"
dossier.save!
dossier_with_expert.champs[0].value = "Name of district B"
dossier_with_expert.champs[1].value = "name of city B"
dossier_with_expert.champs_private[0].value = "Dossier B is incomplete"
dossier_with_expert.champs_private[1].value = "Dossier B is invalid"
dossier_with_expert.save!
end end
describe 'GET #index' do describe 'GET #index' do
@ -46,8 +66,8 @@ describe RechercheController, type: :controller do
end end
context 'when instructeur do not own the dossier' do context 'when instructeur do not own the dossier' do
let(:dossier3) { create(:dossier, :en_construction) } let(:dossier2) { create(:dossier, :en_construction) }
let(:query) { dossier3.id } let(:query) { dossier2.id }
it { is_expected.to have_http_status(200) } it { is_expected.to have_http_status(200) }
@ -69,29 +89,49 @@ describe RechercheController, type: :controller do
end end
end end
describe 'by private annotations' do describe 'by champs' do
context 'when instructeur search by private annotations' do let(:query) { 'district A' }
let(:query) { dossier.private_search_terms }
before { subject } before { subject }
it { is_expected.to have_http_status(200) } it { is_expected.to have_http_status(200) }
it 'returns the expected dossier' do it 'returns the expected dossier' do
expect(assigns(:projected_dossiers).count).to eq(1) expect(assigns(:projected_dossiers).count).to eq(1)
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier.id) expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier.id)
end
end end
context 'when expert search by private annotations' do context 'as an expert' do
let(:user) { avis.experts_procedure.expert.user } let(:user) { avis.experts_procedure.expert.user }
let(:query) { dossier_with_expert.private_search_terms } let(:query) { 'district' }
before { subject }
it { is_expected.to have_http_status(200) } it { is_expected.to have_http_status(200) }
it 'returns 0 dossiers' do it 'returns only the dossier available to the expert' do
expect(assigns(:projected_dossiers).count).to eq(1)
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier_with_expert.id)
end
end
end
describe 'by private annotations' do
let(:query) { 'invalid' }
before { subject }
it { is_expected.to have_http_status(200) }
it 'returns the expected dossier' do
expect(assigns(:projected_dossiers).count).to eq(1)
expect(assigns(:projected_dossiers).first.dossier_id).to eq(dossier_with_expert.id)
end
context 'as an expert' do
let(:user) { avis.experts_procedure.expert.user }
it { is_expected.to have_http_status(200) }
it 'does not allow experts to search in private annotations' do
expect(assigns(:projected_dossiers).count).to eq(0) expect(assigns(:projected_dossiers).count).to eq(0)
end end
end end

View file

@ -1125,4 +1125,14 @@ describe Users::DossiersController, type: :controller do
it { is_expected.to be_falsy } it { is_expected.to be_falsy }
end end
end end
describe '#index' do
before do
sign_in(user)
end
it 'works' do
get :index
expect(response).to have_http_status(:ok)
end
end
end end

View file

@ -190,7 +190,7 @@ FactoryBot.define do
end end
factory :champ_siret, class: 'Champs::SiretChamp' do factory :champ_siret, class: 'Champs::SiretChamp' do
association :type_de_champ, factory: [:type_de_champ_siret] type_de_champ { association :type_de_champ_siret, procedure: dossier.procedure }
association :etablissement, factory: [:etablissement] association :etablissement, factory: [:etablissement]
value { '44011762001530' } value { '44011762001530' }
end end

View file

@ -2,27 +2,19 @@ FactoryBot.define do
factory :dossier do factory :dossier do
autorisation_donnees { true } autorisation_donnees { true }
state { Dossier.states.fetch(:brouillon) } state { Dossier.states.fetch(:brouillon) }
association :user
user { association :user }
groupe_instructeur { procedure.routee? ? nil : procedure.defaut_groupe_instructeur }
revision { procedure.active_revision }
individual { association(:individual, :empty, dossier: instance, strategy: :build) if procedure.for_individual? }
transient do transient do
procedure { nil } for_individual? { false }
end # For now a dossier must use a `create`d procedure, even if the dossier is only built (and not created).
# This is because saving the dossier fails when the procedure has not been saved beforehand
after(:build) do |dossier, evaluator| # (due to some internal ActiveRecord error).
if evaluator.procedure.present? # TODO: find a way to find the issue and just `build` the procedure.
procedure = evaluator.procedure procedure { create(:procedure, :published, :with_type_de_champ, :with_type_de_champ_private, for_individual: for_individual?) }
else
procedure = create(:procedure, :published, :with_type_de_champ, :with_type_de_champ_private)
end
dossier.revision = procedure.active_revision
# Assign the procedure to the dossier through the groupe_instructeur
if dossier.groupe_instructeur.nil?
dossier.groupe_instructeur = procedure.routee? ? nil : procedure.defaut_groupe_instructeur
end
dossier.build_default_individual
end end
trait :with_entreprise do trait :with_entreprise do
@ -42,12 +34,11 @@ FactoryBot.define do
end end
trait :with_individual do trait :with_individual do
after(:build) do |dossier, evaluator| transient do
# If the procedure was implicitely created by the factory, for_individual? { true }
# mark it automatically as for_individual. end
if evaluator.procedure.nil?
dossier.procedure.update(for_individual: true) after(:build) do |dossier, _evaluator|
end
if !dossier.procedure.for_individual? if !dossier.procedure.for_individual?
raise 'Inconsistent factory: attempting to create a dossier :with_individual on a procedure that is not `for_individual?`' raise 'Inconsistent factory: attempting to create a dossier :with_individual on a procedure that is not `for_individual?`'
end end
@ -99,9 +90,7 @@ FactoryBot.define do
end end
trait :with_commentaires do trait :with_commentaires do
after(:create) do |dossier, _evaluator| commentaires { [build(:commentaire), build(:commentaire)] }
dossier.commentaires += create_list(:commentaire, 2)
end
end end
trait :followed do trait :followed do

View file

@ -4,5 +4,9 @@ FactoryBot.define do
factory :groupe_instructeur do factory :groupe_instructeur do
label { generate(:groupe_label) } label { generate(:groupe_label) }
association :procedure association :procedure
trait :default do
label { GroupeInstructeur::DEFAUT_LABEL }
end
end end
end end

View file

@ -5,5 +5,12 @@ FactoryBot.define do
prenom { 'Xavier' } prenom { 'Xavier' }
birthdate { Date.new(1991, 11, 01) } birthdate { Date.new(1991, 11, 01) }
association :dossier association :dossier
trait :empty do
gender { nil }
nom { nil }
prenom { nil }
birthdate { nil }
end
end end
end end

View file

@ -1,5 +1,6 @@
FactoryBot.define do FactoryBot.define do
sequence(:published_path) { |n| "fake_path#{n}" } sequence(:published_path) { |n| "fake_path#{n}" }
factory :procedure do factory :procedure do
sequence(:libelle) { |n| "Procedure #{n}" } sequence(:libelle) { |n| "Procedure #{n}" }
description { "Demande de subvention à l'intention des associations" } description { "Demande de subvention à l'intention des associations" }
@ -12,6 +13,9 @@ FactoryBot.define do
lien_site_web { "https://mon-site.gouv" } lien_site_web { "https://mon-site.gouv" }
path { SecureRandom.uuid } path { SecureRandom.uuid }
groupe_instructeurs { [association(:groupe_instructeur, :default, procedure: instance, strategy: :build)] }
administrateurs { administrateur.present? ? [administrateur] : [association(:administrateur)] }
transient do transient do
administrateur { } administrateur { }
instructeurs { [] } instructeurs { [] }
@ -21,29 +25,16 @@ FactoryBot.define do
end end
after(:build) do |procedure, evaluator| after(:build) do |procedure, evaluator|
if evaluator.administrateur initial_revision = build(:procedure_revision, procedure: procedure)
procedure.administrateurs = [evaluator.administrateur] add_types_de_champs(evaluator.types_de_champ, to: initial_revision, scope: :public)
elsif procedure.administrateurs.empty? add_types_de_champs(evaluator.types_de_champ_private, to: initial_revision, scope: :private)
procedure.administrateurs = [build(:administrateur)]
end
procedure.draft_revision = build(:procedure_revision, procedure: procedure)
evaluator.types_de_champ.each do |type_de_champ| if procedure.brouillon?
type_de_champ.revision = procedure.draft_revision procedure.draft_revision = initial_revision
type_de_champ.private = false else
type_de_champ.revision.revision_types_de_champ << build(:procedure_revision_type_de_champ, procedure.published_revision = initial_revision
revision: procedure.draft_revision, procedure.published_revision.published_at = Time.zone.now
position: type_de_champ.order_place, procedure.draft_revision = build(:procedure_revision, from_original: initial_revision)
type_de_champ: type_de_champ)
end
evaluator.types_de_champ_private.each do |type_de_champ|
type_de_champ.revision = procedure.draft_revision
type_de_champ.private = true
type_de_champ.revision.revision_types_de_champ_private << build(:procedure_revision_type_de_champ,
revision: procedure.draft_revision,
position: type_de_champ.order_place,
type_de_champ: type_de_champ)
end end
end end
@ -71,11 +62,12 @@ FactoryBot.define do
end end
factory :simple_procedure do factory :simple_procedure do
published
for_individual { true }
after(:build) do |procedure, _evaluator| after(:build) do |procedure, _evaluator|
procedure.for_individual = true
build(:type_de_champ, libelle: 'Texte obligatoire', mandatory: true, procedure: procedure) build(:type_de_champ, libelle: 'Texte obligatoire', mandatory: true, procedure: procedure)
procedure.path = generate(:published_path)
procedure.publish!
end end
end end
@ -88,9 +80,7 @@ FactoryBot.define do
end end
trait :with_service do trait :with_service do
after(:build) do |procedure, _evaluator| service { association :service, administrateur: administrateurs.first }
procedure.service = create(:service)
end
end end
trait :with_instructeur do trait :with_instructeur do
@ -106,9 +96,7 @@ FactoryBot.define do
end end
trait :for_individual do trait :for_individual do
after(:build) do |procedure, _evaluator| for_individual { true }
procedure.for_individual = true
end
end end
trait :with_auto_archive do trait :with_auto_archive do
@ -218,26 +206,27 @@ FactoryBot.define do
end end
trait :published do trait :published do
after(:build) do |procedure, _evaluator| aasm_state { :publiee }
procedure.path = generate(:published_path) path { generate(:published_path) }
procedure.publish! published_at { Time.zone.now }
end unpublished_at { nil }
closed_at { nil }
end end
trait :closed do trait :closed do
after(:build) do |procedure, _evaluator| published
procedure.path = generate(:published_path)
procedure.publish! aasm_state { :close }
procedure.close! published_at { Time.zone.now - 1.second }
end closed_at { Time.zone.now }
end end
trait :unpublished do trait :unpublished do
after(:build) do |procedure, _evaluator| published
procedure.path = generate(:published_path)
procedure.publish! aasm_state { :depubliee }
procedure.unpublish! published_at { Time.zone.now - 1.second }
end unpublished_at { Time.zone.now }
end end
trait :discarded do trait :discarded do
@ -308,3 +297,17 @@ FactoryBot.define do
end end
end end
end end
def add_types_de_champs(types_de_champ, to: nil, scope: :public)
revision = to
association_name = scope == :private ? :revision_types_de_champ_private : :revision_types_de_champ
types_de_champ.each do |type_de_champ|
type_de_champ.revision = revision
type_de_champ.private = (scope == :private)
type_de_champ.revision.public_send(association_name) << build(:procedure_revision_type_de_champ,
revision: revision,
position: type_de_champ.order_place,
type_de_champ: type_de_champ)
end
end

View file

@ -1,4 +1,21 @@
FactoryBot.define do FactoryBot.define do
factory :procedure_revision do factory :procedure_revision do
transient do
from_original { nil }
end
after(:build) do |revision, evaluator|
if evaluator.from_original
original = evaluator.from_original
revision.procedure = original.procedure
original.revision_types_de_champ.each do |r_tdc|
revision.revision_types_de_champ << build(:procedure_revision_type_de_champ, from_original: r_tdc)
end
original.revision_types_de_champ_private.each do |r_tdc|
revision.revision_types_de_champ_private << build(:procedure_revision_type_de_champ, from_original: r_tdc)
end
end
end
end end
end end

View file

@ -1,4 +1,16 @@
FactoryBot.define do FactoryBot.define do
factory :procedure_revision_type_de_champ do factory :procedure_revision_type_de_champ do
transient do
from_original { nil }
end
after(:build) do |revision_type_de_champ, evaluator|
if evaluator.from_original
original = evaluator.from_original
revision_type_de_champ.type_de_champ = original.type_de_champ
revision_type_de_champ.position = original.position
end
end
end end
end end

View file

@ -1,6 +1,6 @@
FactoryBot.define do FactoryBot.define do
factory :service do factory :service do
nom { 'service' } sequence(:nom) { |n| "Service #{n}" }
organisme { 'organisme' } organisme { 'organisme' }
type_organisme { Service.type_organismes.fetch(:association) } type_organisme { Service.type_organismes.fetch(:association) }
email { 'email@toto.com' } email { 'email@toto.com' }

View file

@ -0,0 +1,8 @@
FactoryBot.define do
factory :traitement do
trait :accepte do
process_expired { true }
state { :accepte }
end
end
end

View file

@ -49,7 +49,7 @@ RSpec.describe DossierHelper, type: :helper do
let(:procedure) { create(:simple_procedure, :for_individual) } let(:procedure) { create(:simple_procedure, :for_individual) }
context "when the individual is not provided" do context "when the individual is not provided" do
let(:individual) { nil } let(:individual) { build(:individual, :empty) }
it { is_expected.to be_blank } it { is_expected.to be_blank }
end end

View file

@ -6,7 +6,7 @@ RSpec.describe APIEntreprise::Job, type: :job do
describe '#perform' do describe '#perform' do
let(:dossier) { create(:dossier, :with_entreprise) } let(:dossier) { create(:dossier, :with_entreprise) }
context 'when a un retryable error is raised' do context 'when an un-retriable error is raised' do
let(:errors) { [:standard_error] } let(:errors) { [:standard_error] }
it 'does not retry' do it 'does not retry' do
@ -14,7 +14,7 @@ RSpec.describe APIEntreprise::Job, type: :job do
end end
end end
context 'when a retryable error is raised' do context 'when a retriable error is raised' do
let(:errors) { [:service_unavaible, :bad_gateway, :timed_out] } let(:errors) { [:service_unavaible, :bad_gateway, :timed_out] }
it 'retries 5 times' do it 'retries 5 times' do

View file

@ -1,6 +1,6 @@
RSpec.describe ApplicationMailer, type: :mailer do RSpec.describe ApplicationMailer, type: :mailer do
describe 'dealing with invalid emails' do describe 'dealing with invalid emails' do
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) } let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
subject { DossierMailer.notify_new_draft(dossier) } subject { DossierMailer.notify_new_draft(dossier) }
describe 'invalid emails are not sent' do describe 'invalid emails are not sent' do

View file

@ -12,7 +12,7 @@ RSpec.describe DossierMailer, type: :mailer do
end end
describe '.notify_new_draft' do describe '.notify_new_draft' do
let(:dossier) { create(:dossier, procedure: build(:simple_procedure, :with_auto_archive)) } let(:dossier) { create(:dossier, procedure: create(:simple_procedure, :with_auto_archive)) }
subject { described_class.notify_new_draft(dossier) } subject { described_class.notify_new_draft(dossier) }
@ -27,7 +27,7 @@ RSpec.describe DossierMailer, type: :mailer do
end end
describe '.notify_new_answer with dossier brouillon' do describe '.notify_new_answer with dossier brouillon' do
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) } let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
let(:commentaire) { create(:commentaire, dossier: dossier) } let(:commentaire) { create(:commentaire, dossier: dossier) }
subject { described_class.with(commentaire: commentaire).notify_new_answer } subject { described_class.with(commentaire: commentaire).notify_new_answer }
@ -39,8 +39,9 @@ RSpec.describe DossierMailer, type: :mailer do
end end
describe '.notify_new_answer with dossier en construction' do describe '.notify_new_answer with dossier en construction' do
let(:dossier) { create(:dossier, state: "en_construction", procedure: build(:simple_procedure)) } let(:dossier) { create(:dossier, :en_construction, procedure: create(:simple_procedure)) }
let(:commentaire) { create(:commentaire, dossier: dossier) } let(:commentaire) { create(:commentaire, dossier: dossier) }
subject { described_class.with(commentaire: commentaire).notify_new_answer } subject { described_class.with(commentaire: commentaire).notify_new_answer }
it { expect(subject.subject).to include("Nouveau message") } it { expect(subject.subject).to include("Nouveau message") }
@ -51,7 +52,7 @@ RSpec.describe DossierMailer, type: :mailer do
end end
describe '.notify_new_answer with commentaire discarded' do describe '.notify_new_answer with commentaire discarded' do
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) } let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
let(:commentaire) { create(:commentaire, dossier: dossier, discarded_at: 2.minutes.ago) } let(:commentaire) { create(:commentaire, dossier: dossier, discarded_at: 2.minutes.ago) }
subject { described_class.with(commentaire: commentaire).notify_new_answer } subject { described_class.with(commentaire: commentaire).notify_new_answer }
@ -83,7 +84,7 @@ RSpec.describe DossierMailer, type: :mailer do
end end
describe '.notify_revert_to_instruction' do describe '.notify_revert_to_instruction' do
let(:dossier) { create(:dossier, procedure: build(:simple_procedure)) } let(:dossier) { create(:dossier, procedure: create(:simple_procedure)) }
subject { described_class.notify_revert_to_instruction(dossier) } subject { described_class.notify_revert_to_instruction(dossier) }

View file

@ -1,4 +1,3 @@
describe Champs::IbanChamp do describe Champs::IbanChamp do
describe '#valid?' do describe '#valid?' do
it do it do

View file

@ -61,6 +61,16 @@ describe Dossier do
it { is_expected.not_to include(expiring_dossier) } it { is_expected.not_to include(expiring_dossier) }
end end
context 'when .close_to_expiration' do
subject { Dossier.close_to_expiration }
it do
is_expected.not_to include(young_dossier)
is_expected.to include(expiring_dossier)
is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier)
end
end
end end
describe 'en_construction_close_to_expiration' do describe 'en_construction_close_to_expiration' do
@ -87,6 +97,16 @@ describe Dossier do
it { is_expected.not_to include(expiring_dossier) } it { is_expected.not_to include(expiring_dossier) }
end end
context 'when .close_to_expiration' do
subject { Dossier.close_to_expiration }
it do
is_expected.not_to include(young_dossier)
is_expected.to include(expiring_dossier)
is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier)
end
end
end end
describe 'en_instruction_close_to_expiration' do describe 'en_instruction_close_to_expiration' do
@ -104,6 +124,43 @@ describe Dossier do
is_expected.to include(just_expired_dossier) is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier) is_expected.to include(long_expired_dossier)
end end
context 'when .close_to_expiration' do
subject { Dossier.close_to_expiration }
it do
is_expected.not_to include(young_dossier)
is_expected.to include(expiring_dossier)
is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier)
end
end
end
describe 'termine_close_to_expiration' do
let(:procedure) { create(:procedure, :published, duree_conservation_dossiers_dans_ds: 6) }
let!(:young_dossier) { create(:dossier, :accepte, procedure: procedure, traitements: [build(:traitement, :accepte)]) }
let!(:expiring_dossier) { create(:dossier, :accepte, procedure: procedure, traitements: [build(:traitement, :accepte, processed_at: 175.days.ago)]) }
let!(:just_expired_dossier) { create(:dossier, :accepte, procedure: procedure, traitements: [build(:traitement, :accepte, processed_at: (6.months + 1.hour + 10.seconds).ago)]) }
let!(:long_expired_dossier) { create(:dossier, :accepte, procedure: procedure, traitements: [build(:traitement, :accepte, processed_at: 1.year.ago)]) }
subject { Dossier.termine_close_to_expiration }
it do
is_expected.not_to include(young_dossier)
is_expected.to include(expiring_dossier)
is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier)
end
context 'when .close_to_expiration' do
subject { Dossier.close_to_expiration }
it do
is_expected.not_to include(young_dossier)
is_expected.to include(expiring_dossier)
is_expected.to include(just_expired_dossier)
is_expected.to include(long_expired_dossier)
end
end
end end
describe 'with_notifications' do describe 'with_notifications' do
@ -193,11 +250,21 @@ describe Dossier do
expect(dossier.champs.count).to eq(1) expect(dossier.champs.count).to eq(1)
expect(dossier.champs_private.count).to eq(1) expect(dossier.champs_private.count).to eq(1)
end end
end
describe '#build_default_individual' do
let(:dossier) { build(:dossier, procedure: procedure, user: user) }
subject do
dossier.individual = nil
dossier.build_default_individual
end
context 'when the dossier belongs to a procedure for individuals' do context 'when the dossier belongs to a procedure for individuals' do
let(:procedure) { create(:procedure, :with_type_de_champ, for_individual: true) } let(:procedure) { create(:procedure, for_individual: true) }
it 'creates a default individual' do it 'creates a default individual' do
subject
expect(dossier.individual).to be_present expect(dossier.individual).to be_present
expect(dossier.individual.nom).to be_nil expect(dossier.individual.nom).to be_nil
expect(dossier.individual.prenom).to be_nil expect(dossier.individual.prenom).to be_nil
@ -209,6 +276,7 @@ describe Dossier do
let(:user) { build(:user, france_connect_information: france_connect_information) } let(:user) { build(:user, france_connect_information: france_connect_information) }
it 'fills the individual with the informations from France Connect' do it 'fills the individual with the informations from France Connect' do
subject
expect(dossier.individual.nom).to eq('DUBOIS') expect(dossier.individual.nom).to eq('DUBOIS')
expect(dossier.individual.prenom).to eq('Angela Claire Louise') expect(dossier.individual.prenom).to eq('Angela Claire Louise')
expect(dossier.individual.gender).to eq(Individual::GENDER_FEMALE) expect(dossier.individual.gender).to eq(Individual::GENDER_FEMALE)
@ -217,9 +285,10 @@ describe Dossier do
end end
context 'when the dossier belongs to a procedure for moral personas' do context 'when the dossier belongs to a procedure for moral personas' do
let(:procedure) { create(:procedure, :with_type_de_champ, for_individual: false) } let(:procedure) { create(:procedure, for_individual: false) }
it 'doesnt create a individual' do it 'doesnt create a individual' do
subject
expect(dossier.individual).to be_nil expect(dossier.individual).to be_nil
end end
end end
@ -387,6 +456,7 @@ describe Dossier do
it { expect(dossier.state).to eq(Dossier.states.fetch(:en_construction)) } it { expect(dossier.state).to eq(Dossier.states.fetch(:en_construction)) }
it { expect(dossier.en_construction_at).to eq(beginning_of_day) } it { expect(dossier.en_construction_at).to eq(beginning_of_day) }
it { expect(dossier.depose_at).to eq(beginning_of_day) }
it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:en_construction)) } it { expect(dossier.traitement.state).to eq(Dossier.states.fetch(:en_construction)) }
it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) } it { expect(dossier.traitement.processed_at).to eq(beginning_of_day) }
@ -398,6 +468,7 @@ describe Dossier do
expect(dossier.traitements.size).to eq(3) expect(dossier.traitements.size).to eq(3)
expect(dossier.traitements.first.processed_at).to eq(beginning_of_day) expect(dossier.traitements.first.processed_at).to eq(beginning_of_day)
expect(dossier.traitement.processed_at.round).to eq(dossier.en_construction_at.round) expect(dossier.traitement.processed_at.round).to eq(dossier.en_construction_at.round)
expect(dossier.depose_at).to eq(beginning_of_day)
expect(dossier.en_construction_at).to be > beginning_of_day expect(dossier.en_construction_at).to be > beginning_of_day
end end
end end
@ -421,8 +492,9 @@ describe Dossier do
dossier.repasser_en_construction!(instructeur) dossier.repasser_en_construction!(instructeur)
dossier.passer_en_instruction!(instructeur) dossier.passer_en_instruction!(instructeur)
expect(dossier.traitements.size).to eq(3) expect(dossier.traitements.size).to eq(4)
expect(dossier.traitements.first.processed_at).to eq(beginning_of_day) expect(dossier.traitements.en_construction.first.processed_at).to eq(dossier.depose_at)
expect(dossier.traitements.en_instruction.first.processed_at).to eq(beginning_of_day)
expect(dossier.traitement.processed_at.round).to eq(dossier.en_instruction_at.round) expect(dossier.traitement.processed_at.round).to eq(dossier.en_instruction_at.round)
expect(dossier.en_instruction_at).to be > beginning_of_day expect(dossier.en_instruction_at).to be > beginning_of_day
end end

View file

@ -262,14 +262,14 @@ describe Instructeur, type: :model do
end end
describe '#notifications_for_groupe_instructeurs' do describe '#notifications_for_groupe_instructeurs' do
# one procedure, one group, 2 instructeurs # a procedure, one group, 2 instructeurs
let(:procedure) { create(:simple_procedure, :routee, :with_type_de_champ_private, :for_individual) } let(:procedure) { create(:simple_procedure, :routee, :with_type_de_champ_private, :for_individual) }
let(:gi_p1) { procedure.groupe_instructeurs.last } let(:gi_p1) { procedure.groupe_instructeurs.last }
let!(:dossier) { create(:dossier, :with_individual, :followed, groupe_instructeur: gi_p1, state: Dossier.states.fetch(:en_construction)) } let!(:dossier) { create(:dossier, :with_individual, :followed, procedure: procedure, groupe_instructeur: gi_p1, state: Dossier.states.fetch(:en_construction)) }
let(:instructeur) { dossier.follows.first.instructeur } let(:instructeur) { dossier.follows.first.instructeur }
let!(:instructeur_2) { create(:instructeur, groupe_instructeurs: [gi_p1]) } let!(:instructeur_2) { create(:instructeur, groupe_instructeurs: [gi_p1]) }
# one other procedure, dossier followed by a third instructeur # another procedure, dossier followed by a third instructeur
let!(:dossier_on_procedure_2) { create(:dossier, :followed, state: Dossier.states.fetch(:en_construction)) } let!(:dossier_on_procedure_2) { create(:dossier, :followed, state: Dossier.states.fetch(:en_construction)) }
let!(:instructeur_on_procedure_2) { dossier_on_procedure_2.follows.first.instructeur } let!(:instructeur_on_procedure_2) { dossier_on_procedure_2.follows.first.instructeur }
let(:gi_p2) { dossier.groupe_instructeur } let(:gi_p2) { dossier.groupe_instructeur }

View file

@ -16,7 +16,7 @@ describe ProcedurePresentation do
context 'for a published procedure' do context 'for a published procedure' do
let(:procedure) { create(:procedure, :published) } let(:procedure) { create(:procedure, :published) }
let!(:tdc) { { type_champ: :number, libelle: 'libelle 1' } } let(:tdc) { { type_champ: :number, libelle: 'libelle 1' } }
before do before do
procedure.draft_revision.add_type_de_champ(tdc) procedure.draft_revision.add_type_de_champ(tdc)
@ -26,7 +26,7 @@ describe ProcedurePresentation do
it { is_expected.to match(['libelle 1']) } it { is_expected.to match(['libelle 1']) }
context 'when there is another published revision with an added tdc' do context 'when there is another published revision with an added tdc' do
let!(:added_tdc) { { type_champ: :number, libelle: 'libelle 2' } } let(:added_tdc) { { type_champ: :number, libelle: 'libelle 2' } }
before do before do
procedure.draft_revision.add_type_de_champ(added_tdc) procedure.draft_revision.add_type_de_champ(added_tdc)
@ -37,7 +37,7 @@ describe ProcedurePresentation do
end end
context 'add one tdc above the first one' do context 'add one tdc above the first one' do
let!(:tdc2) { { type_champ: :number, libelle: 'libelle 2' } } let(:tdc2) { { type_champ: :number, libelle: 'libelle 2' } }
before do before do
created_tdc2 = procedure.draft_revision.add_type_de_champ(tdc2) created_tdc2 = procedure.draft_revision.add_type_de_champ(tdc2)
@ -47,7 +47,7 @@ describe ProcedurePresentation do
it { is_expected.to match(['libelle 2', 'libelle 1']) } it { is_expected.to match(['libelle 2', 'libelle 1']) }
context 'and finaly, when this tdc is removed' do context 'and finally, when this tdc is removed' do
let!(:previous_tdc2) { procedure.published_revision.types_de_champ.find_by(libelle: 'libelle 2') } let!(:previous_tdc2) { procedure.published_revision.types_de_champ.find_by(libelle: 'libelle 2') }
before do before do

View file

@ -320,8 +320,8 @@ describe ProcedurePresentation do
let(:procedure) { create(:procedure, :for_individual) } let(:procedure) { create(:procedure, :for_individual) }
let!(:first_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'M', prenom: 'Alain', nom: 'Antonelli')) } let!(:first_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Alain', nom: 'Antonelli')) }
let!(:last_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'Mme', prenom: 'Zora', nom: 'Zemmour')) } let!(:last_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'Mme', prenom: 'Zora', nom: 'Zemmour')) }
context 'for gender column' do context 'for gender column' do
let(:column) { 'gender' } let(:column) { 'gender' }
@ -617,8 +617,8 @@ describe ProcedurePresentation do
context 'for individual table' do context 'for individual table' do
let(:procedure) { create(:procedure, :for_individual) } let(:procedure) { create(:procedure, :for_individual) }
let!(:kept_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'Mme', prenom: 'Josephine', nom: 'Baker')) } let!(:kept_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'Mme', prenom: 'Josephine', nom: 'Baker')) }
let!(:discarded_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'M', prenom: 'Jean', nom: 'Tremblay')) } let!(:discarded_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Jean', nom: 'Tremblay')) }
context 'for gender column' do context 'for gender column' do
let(:filter) { [{ 'table' => 'individual', 'column' => 'gender', 'value' => 'Mme' }] } let(:filter) { [{ 'table' => 'individual', 'column' => 'gender', 'value' => 'Mme' }] }
@ -646,7 +646,7 @@ describe ProcedurePresentation do
] ]
end end
let!(:other_kept_dossier) { create(:dossier, procedure: procedure, individual: create(:individual, gender: 'M', prenom: 'Romuald', nom: 'Pistis')) } let!(:other_kept_dossier) { create(:dossier, procedure: procedure, individual: build(:individual, gender: 'M', prenom: 'Romuald', nom: 'Pistis')) }
it 'returns every dossier that matches any of the search criteria for a given column' do it 'returns every dossier that matches any of the search criteria for a given column' do
is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id) is_expected.to contain_exactly(kept_dossier.id, other_kept_dossier.id)

View file

@ -695,6 +695,50 @@ describe Procedure do
end end
end end
describe "#publish_revision!" do
let(:procedure) { create(:procedure, :published) }
let(:tdc_attributes) { { type_champ: :number, libelle: 'libelle 1' } }
let(:publication_date) { Time.zone.local(2021, 1, 1, 12, 00, 00) }
before do
procedure.draft_revision.add_type_de_champ(tdc_attributes)
end
subject do
Timecop.freeze(publication_date) do
procedure.publish_revision!
end
end
it 'publishes the new revision' do
subject
expect(procedure.published_revision).to be_present
expect(procedure.published_revision.published_at).to eq(publication_date)
expect(procedure.published_revision.types_de_champ.first.libelle).to eq('libelle 1')
end
it 'creates a new draft revision' do
expect { subject }.to change(ProcedureRevision, :count).by(1)
expect(procedure.draft_revision).to be_present
expect(procedure.draft_revision.revision_types_de_champ).to be_present
expect(procedure.draft_revision.types_de_champ).to be_present
expect(procedure.draft_revision.types_de_champ.first.libelle).to eq('libelle 1')
end
context 'when the procedure has dossiers' do
let(:dossier_draft) { create(:dossier, :brouillon, procedure: procedure) }
let(:dossier_submitted) { create(:dossier, :en_construction, procedure: procedure) }
before { [dossier_draft, dossier_submitted] }
it 'enqueues rebase jobs for draft dossiers' do
subject
expect(DossierRebaseJob).to have_been_enqueued.with(dossier_draft)
expect(DossierRebaseJob).not_to have_been_enqueued.with(dossier_submitted)
end
end
end
describe "#unpublish!" do describe "#unpublish!" do
let(:procedure) { create(:procedure, :published) } let(:procedure) { create(:procedure, :published) }
let(:now) { Time.zone.now.beginning_of_minute } let(:now) { Time.zone.now.beginning_of_minute }

View file

@ -21,11 +21,11 @@ describe DossierSerializer do
let(:dossier) { create(:dossier, :en_construction, procedure: create(:procedure, :published, :with_type_de_champ)) } let(:dossier) { create(:dossier, :en_construction, procedure: create(:procedure, :published, :with_type_de_champ)) }
before do before do
dossier.champs << build(:champ_carte) dossier.champs << build(:champ_carte, dossier: dossier)
dossier.champs << build(:champ_siret) dossier.champs << build(:champ_siret, dossier: dossier)
dossier.champs << build(:champ_integer_number) dossier.champs << build(:champ_integer_number, dossier: dossier)
dossier.champs << build(:champ_decimal_number) dossier.champs << build(:champ_decimal_number, dossier: dossier)
dossier.champs << build(:champ_linked_drop_down_list) dossier.champs << build(:champ_linked_drop_down_list, dossier: dossier)
end end
it { it {

View file

@ -92,7 +92,7 @@ describe DossierProjectionService do
context 'for individual table' do context 'for individual table' do
let(:table) { 'individual' } let(:table) { 'individual' }
let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) } let(:procedure) { create(:procedure, :for_individual, :with_type_de_champ, :with_type_de_champ_private) }
let(:dossier) { create(:dossier, procedure: procedure, individual: create(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) } let(:dossier) { create(:dossier, procedure: procedure, individual: build(:individual, nom: 'Martin', prenom: 'Jacques', gender: 'M.')) }
context 'for prenom column' do context 'for prenom column' do
let(:column) { 'prenom' } let(:column) { 'prenom' }

View file

@ -9,7 +9,7 @@ end
Capybara.register_driver :headless_chrome do |app| Capybara.register_driver :headless_chrome do |app|
options = Selenium::WebDriver::Chrome::Options.new options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless') options.add_argument('--headless') unless ENV['NO_HEADLESS']
options.add_argument('--window-size=1440,900') options.add_argument('--window-size=1440,900')
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(

View file

@ -1,5 +1,5 @@
describe 'wcag rules for usager', js: true do describe 'wcag rules for usager', js: true do
let(:procedure) { create(:procedure, :with_type_de_champ, :with_all_champs, :with_service, :for_individual, :published) } let(:procedure) { create(:procedure, :published, :with_all_champs, :with_service, :for_individual) }
let(:password) { 'a very complicated password' } let(:password) { 'a very complicated password' }
let(:litteraire_user) { create(:user, password: password) } let(:litteraire_user) { create(:user, password: password) }

View file

@ -165,6 +165,27 @@ describe 'The user' do
expect(page).to have_current_path(merci_dossier_path(user_dossier)) expect(page).to have_current_path(merci_dossier_path(user_dossier))
end end
scenario 'extends dossier experation date more than one time, ', js: true do
user_old_dossier = create(:dossier,
procedure: simple_procedure,
created_at: simple_procedure.duree_conservation_dossiers_dans_ds.month.ago,
user: user)
login_as(user, scope: :user)
visit brouillon_dossier_path(user_old_dossier)
expect(page).to have_css('.card-title', text: 'Votre dossier va expirer', visible: true)
click_on "Repousser sa suppression"
expect(page).not_to have_button("Repousser sa suppression")
Timecop.freeze(1.month.from_now) do
visit brouillon_dossier_path(user_old_dossier)
expect(page).to have_css('.card-title', text: 'Votre dossier va expirer', visible: true)
click_on "Repousser sa suppression"
expect(page).not_to have_button("Repousser sa suppression")
end
end
let(:procedure_with_pj) do let(:procedure_with_pj) do
tdcs = [build(:type_de_champ_piece_justificative, mandatory: true, libelle: 'Pièce justificative')] tdcs = [build(:type_de_champ_piece_justificative, mandatory: true, libelle: 'Pièce justificative')]
create(:procedure, :published, :for_individual, types_de_champ: tdcs) create(:procedure, :published, :for_individual, types_de_champ: tdcs)

View file

@ -32,7 +32,7 @@ describe 'shared/dossiers/demande.html.haml', type: :view do
end end
context 'when dossier was created by an individual' do context 'when dossier was created by an individual' do
let(:individual) { create(:individual) } let(:individual) { build(:individual) }
it 'renders the individual identity infos' do it 'renders the individual identity infos' do
expect(subject).to include(individual.gender) expect(subject).to include(individual.gender)

View file

@ -0,0 +1,56 @@
describe 'shared/dossiers/short_expires_message.html.haml', type: :view do
include DossierHelper
let(:dossier) do
build(:dossier, state, attributes.merge(id: 1, state: state))
end
let(:i18n_key_state) { state }
subject do
render('shared/dossiers/short_expires_message.html.haml',
dossier: dossier,
current_user: build(:user))
end
context 'with dossier.brouillon?' do
let(:attributes) { { created_at: 6.months.ago } }
let(:state) { :brouillon }
it 'render estimated expiration date' do
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
context 'with dossier.en_construction?' do
let(:attributes) { { en_construction_at: 6.months.ago } }
let(:state) { :en_construction }
it 'render estimated expiration date' do
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
context 'with dossier.en_instruction?' do
let(:state) { :en_instruction }
let(:attributes) { {} }
it 'render estimated expiration date' do
expect(subject).to have_selector('p.expires_at_en_instruction',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}"))
end
end
context 'with dossier.en_processed_at?' do
let(:state) { :accepte }
let(:attributes) { {} }
it 'render estimated expiration date' do
allow(dossier).to receive(:processed_at).and_return(6.months.ago)
expect(subject).to have_selector('.expires_at',
text: I18n.t("shared.dossiers.header.expires_at.#{i18n_key_state}",
date: safe_expiration_date(dossier)))
end
end
end

View file

@ -13,6 +13,7 @@ describe 'users/dossiers/index.html.haml', type: :view do
assign(:dossiers_invites, Kaminari.paginate_array(dossiers_invites).page(1)) assign(:dossiers_invites, Kaminari.paginate_array(dossiers_invites).page(1))
assign(:dossiers_supprimes, Kaminari.paginate_array(user_dossiers).page(1)) assign(:dossiers_supprimes, Kaminari.paginate_array(user_dossiers).page(1))
assign(:dossier_transfers, Kaminari.paginate_array([]).page(1)) assign(:dossier_transfers, Kaminari.paginate_array([]).page(1))
assign(:dossiers_close_to_expiration, Kaminari.paginate_array([]).page(1))
assign(:statut, statut) assign(:statut, statut)
render render
end end