Merge pull request #5361 from betagouv/dev

2020-07-09-01
This commit is contained in:
Keirua 2020-07-09 14:13:53 +02:00 committed by GitHub
commit 811c8daacd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 366 additions and 157 deletions

View file

@ -689,6 +689,7 @@ Rails/CreateTableWithTimestamps:
- db/migrate/2016*.rb
- db/migrate/2017*.rb
- db/migrate/2018*.rb
- db/migrate/20200630140356_create_traitements.rb
Rails/Date:
Enabled: false

View file

@ -97,7 +97,7 @@ GEM
aes_key_wrap (1.0.1)
after_party (1.11.2)
anchored (1.1.0)
ast (2.4.0)
ast (2.4.1)
attr_required (1.0.1)
autoprefixer-rails (9.7.6)
execjs
@ -342,7 +342,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json-jwt (1.11.0)
json-jwt (1.13.0)
activesupport (>= 4.2)
aes_key_wrap
bindata
@ -407,7 +407,7 @@ GEM
nenv (0.3.0)
netrc (0.11.0)
nio4r (2.5.2)
nokogiri (1.10.9)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
notiffany (0.1.3)
nenv (~> 0.1)
@ -442,9 +442,9 @@ GEM
validate_url
webfinger (>= 1.0.1)
orm_adapter (0.5.0)
parallel (1.19.1)
parser (2.7.1.0)
ast (~> 2.4.0)
parallel (1.19.2)
parser (2.7.1.4)
ast (~> 2.4.1)
pdf-core (0.7.0)
pg (1.2.3)
phonelib (0.6.43)
@ -475,22 +475,22 @@ GEM
byebug (~> 11.0)
pry (~> 0.13.0)
public_suffix (4.0.4)
puma (4.3.3)
puma (4.3.5)
nio4r (~> 2.0)
pundit (2.1.0)
activesupport (>= 3.0.0)
raabro (1.1.6)
rack (2.0.9)
rack (2.2.3)
rack-attack (6.2.2)
rack (>= 1.0, < 3)
rack-mini-profiler (2.0.1)
rack (>= 1.2.0)
rack-oauth2 (1.12.0)
rack-oauth2 (1.15.0)
activesupport
attr_required
httpclient
json-jwt (>= 1.11.0)
rack (< 2.1)
rack (>= 2.1.0)
rack-protection (2.0.8.1)
rack
rack-proxy (0.6.5)

View file

@ -1,2 +1,2 @@
server: bin/rails server -p 3000
jobs: bin/delayed_job run
server: RAILS_QUEUE_ADAPTER=delayed_job bin/rails server -p 3000
jobs: bin/rake jobs:work

View file

@ -22,9 +22,6 @@ Vous souhaitez y apporter des changements ou des améliorations ? Lisez notre [
- rbenv : voir https://github.com/rbenv/rbenv-installer#rbenv-installer--doctor-scripts
- Yarn : voir https://yarnpkg.com/en/docs/install
- Overmind :
* Mac : `brew install overmind`
* Linux : voir https://github.com/DarthSim/overmind#installation
#### Tests
@ -60,9 +57,18 @@ Afin d'initialiser l'environnement de développement, exécutez la commande suiv
### Lancement de l'application
overmind start
On lance le serveur d'application ainsi :
L'application tourne à l'adresse `http://localhost:3000`.
bin/rails server
L'application tourne alors à l'adresse `http://localhost:3000`, et utilise le mécanisme par défaut de rails pour les tâches asynchrones.
C'est ce qu'on veut dans la plupart des cas. Une exception: ça ne joue pas les tâches cron.
Pour être une peu plus proche du comportement de production, et jouer les tâches cron, on peut lancer la message queue
dans un service dédié, et indiquer à rails d'utiliser delayed_job:
bin/rake jobs:work
RAILS_QUEUE_ADAPTER=delayed_job bin/rails server
### Utilisateurs de test
@ -107,12 +113,6 @@ Pour exécuter les tests de l'application, plusieurs possibilités :
rails generate after_party:task task_name
### Debug
Une fois `overmind` lancé, et un breakpoint `byebug` inséré dans le code, il faut se connecter au process `server` dans un nouveau terminal afin d'intéragir avec byebug :
overmind connect server
### Linting
Le projet utilise plusieurs linters pour vérifier la lisibilité et la qualité du code.

View file

@ -249,9 +249,11 @@ class StatsController < ApplicationController
min_date = 11.months.ago
max_date = Time.zone.now.to_date
processed_dossiers = dossiers
processed_dossiers = Traitement.includes(:dossier)
.where(dossier_id: dossiers)
.where('dossiers.state' => Dossier::TERMINE)
.where(:processed_at => min_date..max_date)
.pluck(:groupe_instructeur_id, :en_construction_at, :processed_at)
.pluck('dossiers.groupe_instructeur_id', 'dossiers.en_construction_at', :processed_at)
# Group dossiers by month
processed_dossiers_by_month = processed_dossiers
@ -290,11 +292,13 @@ class StatsController < ApplicationController
min_date = 11.months.ago
max_date = Time.zone.now.to_date
processed_dossiers = dossiers
processed_dossiers = Traitement.includes(:dossier)
.where(dossier: dossiers)
.where('dossiers.state' => Dossier::TERMINE)
.where(:processed_at => min_date..max_date)
.pluck(
:groupe_instructeur_id,
Arel.sql('EXTRACT(EPOCH FROM (en_construction_at - created_at)) / 60 AS processing_time'),
'dossiers.groupe_instructeur_id',
Arel.sql('EXTRACT(EPOCH FROM (dossiers.en_construction_at - dossiers.created_at)) / 60 AS processing_time'),
:processed_at
)

View file

@ -149,7 +149,7 @@ module Users
errors = update_dossier_and_compute_errors
if passage_en_construction? && errors.blank?
@dossier.en_construction!
@dossier.passer_en_construction!
NotificationMailer.send_initiated_notification(@dossier).deliver_later
@dossier.groupe_instructeur.instructeurs.with_instant_email_dossier_notifications.each do |instructeur|
DossierMailer.notify_new_dossier_depose_to_instructeur(@dossier, instructeur.email).deliver_later

View file

@ -9,6 +9,10 @@ class ApiEntreprise::Job < ApplicationJob
error(self, exception)
end
def error(job, exception)
# override ApplicationJob#error to avoid reporting to sentry
end
def max_attempts
ENV.fetch("MAX_ATTEMPTS_API_ENTREPRISE_JOBS", DEFAULT_MAX_ATTEMPTS_API_ENTREPRISE_JOBS).to_i
end

View file

@ -42,6 +42,7 @@ class Dossier < ApplicationRecord
has_many :followers_instructeurs, through: :follows, source: :instructeur
has_many :previous_followers_instructeurs, -> { distinct }, through: :previous_follows, source: :instructeur
has_many :avis, inverse_of: :dossier, dependent: :destroy
has_many :traitements, -> { order(:processed_at) }, inverse_of: :dossier, dependent: :destroy
has_many :dossier_operation_logs, -> { order(:created_at) }, dependent: :nullify, inverse_of: :dossier
@ -128,6 +129,7 @@ class Dossier < ApplicationRecord
:individual,
:followers_instructeurs,
:avis,
:traitements,
etablissement: :champ,
champs: {
etablissement: :champ,
@ -172,6 +174,7 @@ class Dossier < ApplicationRecord
justificatif_motivation_attachment: :blob,
attestation: [],
avis: { piece_justificative_file_attachment: :blob },
traitements: [],
etablissement: [],
individual: [],
user: [])
@ -198,10 +201,9 @@ class Dossier < ApplicationRecord
.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
scope :termine_close_to_expiration, -> do
state_termine
.joins(:procedure)
.where("dossiers.processed_at + (duree_conservation_dossiers_dans_ds * INTERVAL '1 month') - INTERVAL :expires_in < :now", { now: Time.zone.now, expires_in: INTERVAL_BEFORE_EXPIRATION })
def self.termine_close_to_expiration
dossier_ids = Traitement.termine_close_to_expiration.pluck(:dossier_id).uniq
Dossier.where(id: dossier_ids)
end
scope :brouillon_expired, -> do
@ -249,7 +251,7 @@ class Dossier < ApplicationRecord
end
scope :for_procedure, -> (procedure) { includes(:user, :groupe_instructeur).where(groupe_instructeurs: { procedure: procedure }) }
scope :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: []) }
scope :for_api_v2, -> { includes(procedure: [:administrateurs], etablissement: [], individual: [], traitements: []) }
scope :with_notifications, -> do
# This scope is meant to be composed, typically with Instructeur.followed_dossiers, which means that the :follows table is already INNER JOINed;
@ -282,7 +284,6 @@ class Dossier < ApplicationRecord
delegate :types_de_champ, to: :procedure
delegate :france_connect_information, to: :user
before_validation :update_state_dates, if: -> { state_changed? }
before_save :build_default_champs, if: Proc.new { groupe_instructeur_id_was.nil? }
before_save :update_search_terms
@ -294,6 +295,16 @@ class Dossier < ApplicationRecord
validates :individual, presence: true, if: -> { procedure.for_individual? }
validates :groupe_instructeur, presence: true
def motivation
return nil if !termine?
traitements.any? ? traitements.last.motivation : read_attribute(:motivation)
end
def processed_at
return nil if !termine?
traitements.any? ? traitements.last.processed_at : read_attribute(:processed_at)
end
def update_search_terms
self.search_terms = [
user&.email,
@ -508,27 +519,29 @@ class Dossier < ApplicationRecord
end
end
def after_passer_en_construction
update!(en_construction_at: Time.zone.now) if self.en_construction_at.nil?
end
def after_passer_en_instruction(instructeur)
instructeur.follow(self)
update!(en_instruction_at: Time.zone.now) if self.en_instruction_at.nil?
log_dossier_operation(instructeur, :passer_en_instruction)
end
def after_passer_automatiquement_en_instruction
update!(en_instruction_at: Time.zone.now) if self.en_instruction_at.nil?
log_automatic_dossier_operation(:passer_en_instruction)
end
def after_repasser_en_construction(instructeur)
self.en_instruction_at = nil
save!
log_dossier_operation(instructeur, :repasser_en_construction)
end
def after_repasser_en_instruction(instructeur)
self.archived = false
self.processed_at = nil
self.motivation = nil
self.en_instruction_at = Time.zone.now
attestation&.destroy
save!
@ -537,7 +550,7 @@ class Dossier < ApplicationRecord
end
def after_accepter(instructeur, motivation, justificatif = nil)
self.motivation = motivation
self.traitements.build(state: Dossier.states.fetch(:accepte), instructeur_email: instructeur.email, motivation: motivation, processed_at: Time.zone.now)
if justificatif
self.justificatif_motivation.attach(justificatif)
@ -553,6 +566,7 @@ class Dossier < ApplicationRecord
end
def after_accepter_automatiquement
self.traitements.build(state: Dossier.states.fetch(:accepte), instructeur_email: nil, motivation: nil, processed_at: Time.zone.now)
self.en_instruction_at ||= Time.zone.now
if attestation.nil?
@ -565,7 +579,7 @@ class Dossier < ApplicationRecord
end
def after_refuser(instructeur, motivation, justificatif = nil)
self.motivation = motivation
self.traitements.build(state: Dossier.states.fetch(:refuse), instructeur_email: instructeur.email, motivation: motivation, processed_at: Time.zone.now)
if justificatif
self.justificatif_motivation.attach(justificatif)
@ -577,7 +591,7 @@ class Dossier < ApplicationRecord
end
def after_classer_sans_suite(instructeur, motivation, justificatif = nil)
self.motivation = motivation
self.traitements.build(state: Dossier.states.fetch(:sans_suite), instructeur_email: instructeur.email, motivation: motivation, processed_at: Time.zone.now)
if justificatif
self.justificatif_motivation.attach(justificatif)
@ -766,16 +780,6 @@ class Dossier < ApplicationRecord
end
end
def update_state_dates
if en_construction? && !self.en_construction_at
self.en_construction_at = Time.zone.now
elsif en_instruction? && !self.en_instruction_at
self.en_instruction_at = Time.zone.now
elsif TERMINE.include?(state) && !self.processed_at
self.processed_at = Time.zone.now
end
end
def send_dossier_received
if saved_change_to_state? && en_instruction? && !procedure.declarative_accepte?
NotificationMailer.send_dossier_received(self).deliver_later

View file

@ -161,7 +161,7 @@ class Instructeur < ApplicationRecord
h = {
nb_en_construction: groupe.dossiers.en_construction.count,
nb_en_instruction: groupe.dossiers.en_instruction.count,
nb_accepted: groupe.dossiers.accepte.where(processed_at: Time.zone.yesterday.beginning_of_day..Time.zone.yesterday.end_of_day).count,
nb_accepted: Traitement.where(dossier: groupe.dossiers.accepte, processed_at: Time.zone.yesterday.beginning_of_day..Time.zone.yesterday.end_of_day).count,
nb_notification: notifications_for_procedure(procedure, :not_archived).count
}

View file

@ -108,6 +108,8 @@ class Procedure < ApplicationRecord
], size: { less_than: 20.megabytes }
validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: 5.megabytes }
validates :api_entreprise_token, jwt_token: true, allow_blank: true
before_save :update_juridique_required
after_initialize :ensure_path_exists
before_save :ensure_path_exists
@ -438,7 +440,15 @@ class Procedure < ApplicationRecord
end
def usual_traitement_time
percentile_time(:en_construction_at, :processed_at, 90)
times = Traitement.includes(:dossier)
.where(state: Dossier::TERMINE)
.where(processed_at: 1.month.ago..Time.zone.now)
.pluck('dossiers.en_construction_at', :processed_at)
.map { |(en_construction_at, processed_at)| processed_at - en_construction_at }
if times.present?
times.percentile(90).ceil
end
end
def populate_champ_stable_ids
@ -610,18 +620,6 @@ class Procedure < ApplicationRecord
end
end
def percentile_time(start_attribute, end_attribute, p)
times = dossiers
.where.not(start_attribute => nil, end_attribute => nil)
.where(end_attribute => 1.month.ago..Time.zone.now)
.pluck(start_attribute, end_attribute)
.map { |(start_date, end_date)| end_date - start_date }
if times.present?
times.percentile(p).ceil
end
end
def ensure_path_exists
if self.path.blank?
self.path = SecureRandom.uuid

10
app/models/traitement.rb Normal file
View file

@ -0,0 +1,10 @@
class Traitement < ApplicationRecord
belongs_to :dossier
scope :termine_close_to_expiration, -> do
joins(dossier: :procedure)
.where(state: Dossier::TERMINE)
.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
end

View file

@ -82,7 +82,12 @@ class TypeDeChamp < ApplicationRecord
before_validation :check_mandatory
before_save :remove_piece_justificative_template, if: -> { type_champ_changed? }
before_validation :remove_drop_down_list, if: -> { type_champ_changed? }
before_save :remove_drop_down_list, if: -> { type_champ_changed? }
before_save :remove_repetition, if: -> { type_champ_changed? }
after_save if: -> { @remove_piece_justificative_template } do
piece_justificative_template.purge_later
end
def valid?(context = nil)
super
@ -292,7 +297,7 @@ class TypeDeChamp < ApplicationRecord
def remove_piece_justificative_template
if !piece_justificative? && piece_justificative_template.attached?
piece_justificative_template.purge_later
@remove_piece_justificative_template = true
end
end
@ -302,4 +307,10 @@ class TypeDeChamp < ApplicationRecord
self.drop_down_options = nil
end
end
def remove_repetition
if !repetition?
types_de_champ.destroy_all
end
end
end

View file

@ -0,0 +1,9 @@
class JwtTokenValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
begin
JWT.decode value, nil, false
rescue
record.errors[attribute] << (options[:message] || "n'est pas un jeton valide")
end
end
end

View file

@ -156,7 +156,7 @@ def add_etats_dossier(pdf, dossier)
if dossier.en_instruction_at.present?
format_in_2_columns(pdf, "En instruction le", try_format_date(dossier.en_instruction_at))
end
if dossier.processed_at?.present?
if dossier.processed_at.present?
format_in_2_columns(pdf, "Décision le", try_format_date(dossier.processed_at))
end

View file

@ -0,0 +1,19 @@
.tab-title Décisions rendues
- if traitements.any?
%ul.tab-list
- traitements.each do |traitement|
- if traitement.instructeur_email.present?
%li
= "Le #{l(traitement.processed_at, format: '%d %B %Y à %R')}, "
= traitement.instructeur_email
a
%strong= t(traitement.state, scope: 'activerecord.attributes.traitement.state').downcase
ce dossier
- else
%li
= "Le #{l(traitement.processed_at, format: '%d %B %Y à %R')}, "
ce dossier a été
%strong= t(traitement.state, scope: 'activerecord.attributes.traitement.state').downcase
- else
%p.tab-paragraph Aucune décision n'a été rendue

View file

@ -13,3 +13,5 @@
= render partial: 'instructeurs/dossiers/personnes_impliquees_block', locals: { emails_collection: @avis_emails, title: "Personnes à qui un avis a été demandé", blank: "Aucun avis n'a été demandé" }
= render partial: 'instructeurs/dossiers/personnes_impliquees_block', locals: { emails_collection: @invites_emails, title: "Personnes invitées à consulter ce dossier", blank: "Aucune personne n'a été invitée à consulter ce dossier" }
= render partial: 'instructeurs/dossiers/decisions_rendues_block', locals: { traitements: @dossier.traitements }

View file

@ -68,13 +68,20 @@
.cta-panel-wrapper
%div
%h2.cta-panel-title Une question, un problème ?
%p.cta-panel-explanation Notre équipe est disponible pour vous renseigner et vous aider
%p.cta-panel-explanation La réponse est dans laide en ligne
%div
= contact_link "Contactez-nous",
tags: 'landing',
= link_to "Accéder à laide en ligne", FAQ_URL,
class: "cta-panel-button-white",
target: "_blank",
rel: "noopener noreferrer"
-# We temporarily disable the link to the contact page on the homepage
-# %p.cta-panel-explanation Notre équipe est disponible pour vous renseigner et vous aider
-# %div
-# = contact_link "Contactez-nous",
-# tags: 'landing',
-# class: "cta-panel-button-white",
-# target: "_blank",
-# rel: "noopener noreferrer"
.landing-panel
.container

View file

@ -1,6 +1,5 @@
#!/usr/bin/env ruby
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = File.expand_path('..', __dir__)
@ -9,13 +8,15 @@ def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
FileUtils.chdir APP_ROOT do
# This script is a starting point to setup your application.
# Add necessary setup steps to this file.
puts "\n== Installing dependencies =="
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# Install JavaScript dependencies
system! 'bin/yarn install'
puts "\n== Updating webdrivers =="
@ -23,7 +24,7 @@ chdir APP_ROOT do
puts "\n== Copying sample files =="
unless File.exist?('.env')
cp 'config/env.example', '.env'
FileUtils.cp 'config/env.example', '.env'
end
# Create the database, load the schema, and initialize it with the seed data
@ -34,5 +35,5 @@ chdir APP_ROOT do
system! 'bin/rails log:clear tmp:clear'
puts "\n== Done =="
puts "You can now start the application server with `overmind start`."
puts "You can now start the application server with `bin/rails server`."
end

View file

@ -1,6 +1,5 @@
#!/usr/bin/env ruby
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = File.expand_path('..', __dir__)
@ -9,7 +8,7 @@ def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
FileUtils.chdir APP_ROOT do
# This script is a way to update your development environment automatically.
# Add necessary update steps to this file.
@ -31,5 +30,5 @@ chdir APP_ROOT do
system! 'bin/rails log:clear'
puts "\n== Done =="
puts "You can now start (or restart) the application server with `overmind start`."
puts "You can now start (or restart) the application server with `bin/rails server`."
end

View file

@ -36,7 +36,7 @@ Rails.application.configure do
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = false
config.assets.debug = true
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
# yet still be able to expire them through the digest params.
@ -83,10 +83,9 @@ Rails.application.configure do
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
# This is useful to run rails in development with :async queue adapter
if ENV['RAILS_QUEUE_ADAPTER']
config.active_job.queue_adapter = ENV['RAILS_QUEUE_ADAPTER'].to_sym
end
# We use the async adapter by default, but delayed_job can be set using
# RAILS_QUEUE_ADAPTER=delayed_job bin/rails server
config.active_job.queue_adapter = ENV.fetch('RAILS_QUEUE_ADAPTER', 'async').to_sym
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end

View file

@ -9,13 +9,13 @@ fr:
montant_projet: 'Le montant du projet'
montant_aide_demande: "Le montant daide demandée"
date_previsionnelle: "La date de début prévisionnelle"
state:
state: &state
brouillon: "Brouillon"
en_construction: "En construction"
en_instruction: "En instruction"
accepte: "Accepté"
refuse: "Refusé"
sans_suite: "Sans suite"
sans_suite: "Classé sans suite"
autorisation_donnees: Acceptation des CGU
state/brouillon: Brouillon
state/en_construction: En construction
@ -23,3 +23,6 @@ fr:
state/accepte: Accepté
state/refuse: Refusé
state/sans_suite: Sans suite
traitement:
state:
<<: *state

View file

@ -77,7 +77,7 @@ test:
secret_key_base: aa52abc3f3a629d04a61e9899a24c12f52b24c679cbf45f8ec0cdcc64ab9526d673adca84212882dff3911ac98e0c32ec4729ca7b3429ba18ef4dfd1bd18bc7a
signing_key: aef3153a9829fa4ba10acb02927ac855df6b92795b1ad265d654443c4b14a017
api_entreprise:
key: api_entreprise_test_key
key: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik9oIHllYWgiLCJpYXQiOjE1MTYyMzkwMjJ9.f06sBo3q2Yxnw_TYPFUEs0CozBmcV-XniH_DeKNWzKE"
pipedrive:
key: pipedrive_test_key
france_connect_particulier:

View file

@ -0,0 +1,11 @@
class CreateTraitements < ActiveRecord::Migration[5.2]
def change
create_table :traitements do |t|
t.references :dossier, foreign_key: true
t.references :instructeur, foreign_key: true
t.string :motivation
t.string :state
t.timestamp :processed_at
end
end
end

View file

@ -0,0 +1,6 @@
class RemoveInstructeurIdAndAddInstructeurEmailToTraitements < ActiveRecord::Migration[5.2]
def change
add_column :traitements, :instructeur_email, :string
remove_column :traitements, :instructeur_id
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_06_11_122406) do
ActiveRecord::Schema.define(version: 2020_07_07_082256) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -560,6 +560,15 @@ ActiveRecord::Schema.define(version: 2020_06_11_122406) do
t.string "version", null: false
end
create_table "traitements", force: :cascade do |t|
t.bigint "dossier_id"
t.string "motivation"
t.string "state"
t.datetime "processed_at"
t.string "instructeur_email"
t.index ["dossier_id"], name: "index_traitements_on_dossier_id"
end
create_table "trusted_device_tokens", force: :cascade do |t|
t.string "token", null: false
t.bigint "instructeur_id"
@ -660,6 +669,7 @@ ActiveRecord::Schema.define(version: 2020_06_11_122406) do
add_foreign_key "received_mails", "procedures"
add_foreign_key "refused_mails", "procedures"
add_foreign_key "services", "administrateurs"
add_foreign_key "traitements", "dossiers"
add_foreign_key "trusted_device_tokens", "instructeurs"
add_foreign_key "types_de_champ", "types_de_champ", column: "parent_id"
add_foreign_key "users", "administrateurs"

View file

@ -0,0 +1,18 @@
namespace :after_party do
desc 'Deployment task: add_traitements_from_dossiers'
task add_traitements_from_dossiers: :environment do
puts "Running deploy task 'add_traitements_from_dossiers'"
dossiers_termines = Dossier.state_termine
progress = ProgressReport.new(dossiers_termines.count)
dossiers_termines.find_each do |dossier|
dossier.traitements.create!(state: dossier.state, motivation: dossier.motivation, processed_at: dossier.processed_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: '20200630154829'
end
end

View file

@ -596,7 +596,7 @@ describe API::V2::GraphqlController do
it "should fail" do
expect(gql_errors).to eq(nil)
expect(gql_data).to eq(dossierRefuser: {
errors: [{ message: "Le dossier est déjà sans suite" }],
errors: [{ message: "Le dossier est déjà classé sans suite" }],
dossier: nil
})
end

View file

@ -194,7 +194,7 @@ describe Instructeurs::DossiersController, type: :controller do
describe '#terminer' do
context "with refuser" do
before do
dossier.en_instruction!
dossier.passer_en_instruction!(instructeur)
sign_in(instructeur.user)
end
@ -235,7 +235,7 @@ describe Instructeurs::DossiersController, type: :controller do
context "with classer_sans_suite" do
before do
dossier.en_instruction!
dossier.passer_en_instruction!(instructeur)
sign_in(instructeur.user)
end
context 'without attachment' do
@ -277,7 +277,7 @@ describe Instructeurs::DossiersController, type: :controller do
context "with accepter" do
before do
dossier.en_instruction!
dossier.passer_en_instruction!(instructeur)
sign_in(instructeur.user)
expect(NotificationMailer).to receive(:send_closed_notification)

View file

@ -312,10 +312,11 @@ describe NewAdministrateur::ProceduresController, type: :controller do
describe 'PATCH #jeton' do
let(:procedure) { create(:procedure, administrateur: admin) }
let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" }
it "update api_entreprise_token" do
patch :update_jeton, params: { id: procedure.id, procedure: { api_entreprise_token: 'ceci-est-un-jeton' } }
expect(procedure.reload.api_entreprise_token).to eq('ceci-est-un-jeton')
patch :update_jeton, params: { id: procedure.id, procedure: { api_entreprise_token: valid_token } }
expect(procedure.reload.api_entreprise_token).to eq(valid_token)
end
end
end

View file

@ -106,26 +106,23 @@ describe StatsController, type: :controller do
before do
procedure_1 = FactoryBot.create(:procedure)
procedure_2 = FactoryBot.create(:procedure)
dossier_p1_a = FactoryBot.create(:dossier,
dossier_p1_a = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_1,
:en_construction_at => 2.months.ago.beginning_of_month,
:processed_at => 2.months.ago.beginning_of_month + 3.days)
dossier_p1_b = FactoryBot.create(:dossier,
dossier_p1_b = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_1,
:en_construction_at => 2.months.ago.beginning_of_month,
:processed_at => 2.months.ago.beginning_of_month + 1.day)
dossier_p1_c = FactoryBot.create(:dossier,
dossier_p1_c = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_1,
:en_construction_at => 1.month.ago.beginning_of_month,
:processed_at => 1.month.ago.beginning_of_month + 5.days)
dossier_p2_a = FactoryBot.create(:dossier,
dossier_p2_a = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_2,
:en_construction_at => 2.months.ago.beginning_of_month,
:processed_at => 2.months.ago.beginning_of_month + 4.days)
# Write directly in the DB to avoid the before_validation hook
Dossier.update_all(state: Dossier.states.fetch(:accepte))
@expected_hash = {
(2.months.ago.beginning_of_month).to_s => 3.0,
(1.month.ago.beginning_of_month).to_s => 5.0
@ -154,30 +151,27 @@ describe StatsController, type: :controller do
before do
procedure_1 = FactoryBot.create(:procedure, :with_type_de_champ, :types_de_champ_count => 24)
procedure_2 = FactoryBot.create(:procedure, :with_type_de_champ, :types_de_champ_count => 48)
dossier_p1_a = FactoryBot.create(:dossier,
dossier_p1_a = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_1,
:created_at => 2.months.ago.beginning_of_month,
:en_construction_at => 2.months.ago.beginning_of_month + 30.minutes,
:processed_at => 2.months.ago.beginning_of_month + 1.day)
dossier_p1_b = FactoryBot.create(:dossier,
dossier_p1_b = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_1,
:created_at => 2.months.ago.beginning_of_month,
:en_construction_at => 2.months.ago.beginning_of_month + 10.minutes,
:processed_at => 2.months.ago.beginning_of_month + 1.day)
dossier_p1_c = FactoryBot.create(:dossier,
dossier_p1_c = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_1,
:created_at => 1.month.ago.beginning_of_month,
:en_construction_at => 1.month.ago.beginning_of_month + 50.minutes,
:processed_at => 1.month.ago.beginning_of_month + 1.day)
dossier_p2_a = FactoryBot.create(:dossier,
dossier_p2_a = FactoryBot.create(:dossier, :accepte,
:procedure => procedure_2,
:created_at => 2.months.ago.beginning_of_month,
:en_construction_at => 2.months.ago.beginning_of_month + 80.minutes,
:processed_at => 2.months.ago.beginning_of_month + 1.day)
# Write directly in the DB to avoid the before_validation hook
Dossier.update_all(state: Dossier.states.fetch(:accepte))
@expected_hash = {
(2.months.ago.beginning_of_month).to_s => 30.0,
(1.month.ago.beginning_of_month).to_s => 50.0

View file

@ -657,7 +657,7 @@ describe Users::DossiersController, type: :controller do
let!(:invite) { create(:invite, dossier: dossier, user: user) }
before do
dossier.en_construction!
dossier.passer_en_construction!
subject
end

View file

@ -127,11 +127,23 @@ FactoryBot.define do
end
trait :accepte do
after(:create) do |dossier, _evaluator|
transient do
motivation { nil }
processed_at { nil }
end
after(:create) do |dossier, evaluator|
dossier.state = Dossier.states.fetch(:accepte)
dossier.en_construction_at ||= dossier.created_at + 1.minute
dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute
dossier.processed_at ||= dossier.en_instruction_at + 1.minute
processed_at = evaluator.processed_at
if processed_at.present?
dossier.en_construction_at ||= processed_at - 2.minutes
dossier.en_instruction_at ||= processed_at - 1.minute
dossier.traitements.build(state: Dossier.states.fetch(:accepte), processed_at: processed_at, motivation: evaluator.motivation)
else
dossier.en_construction_at ||= dossier.created_at + 1.minute
dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute
dossier.traitements.build(state: Dossier.states.fetch(:accepte), processed_at: dossier.en_instruction_at + 1.minute, motivation: evaluator.motivation)
end
dossier.save!
end
end
@ -141,7 +153,7 @@ FactoryBot.define do
dossier.state = Dossier.states.fetch(:refuse)
dossier.en_construction_at ||= dossier.created_at + 1.minute
dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute
dossier.processed_at ||= dossier.en_instruction_at + 1.minute
dossier.traitements.build(state: Dossier.states.fetch(:refuse), processed_at: dossier.en_instruction_at + 1.minute)
dossier.save!
end
end
@ -151,14 +163,14 @@ FactoryBot.define do
dossier.state = Dossier.states.fetch(:sans_suite)
dossier.en_construction_at ||= dossier.created_at + 1.minute
dossier.en_instruction_at ||= dossier.en_construction_at + 1.minute
dossier.processed_at ||= dossier.en_instruction_at + 1.minute
dossier.traitements.build(state: Dossier.states.fetch(:sans_suite), processed_at: dossier.en_instruction_at + 1.minute)
dossier.save!
end
end
trait :with_motivation do
after(:create) do |dossier, _evaluator|
dossier.motivation = case dossier.state
motivation = case dossier.state
when Dossier.states.fetch(:refuse)
'Lentreprise concernée nest pas agréée.'
when Dossier.states.fetch(:sans_suite)
@ -166,6 +178,7 @@ FactoryBot.define do
else
'Vous avez validé les conditions.'
end
dossier.traitements.last.update!(motivation: motivation)
end
end

View file

@ -108,6 +108,12 @@ FactoryBot.define do
end
factory :type_de_champ_repetition do
type_champ { TypeDeChamp.type_champs.fetch(:repetition) }
trait :with_types_de_champ do
after(:build) do |type_de_champ, _evaluator|
type_de_champ.types_de_champ << create(:type_de_champ, libelle: 'sub type de champ')
end
end
end
trait :private do

View file

@ -156,7 +156,7 @@ RSpec.describe DossierHelper, type: :helper do
it 'sans_suite is traité' do
dossier.sans_suite!
expect(subject).to eq('Sans suite')
expect(subject).to eq('Classé sans suite')
end
it 'refuse is traité' do

View file

@ -62,13 +62,13 @@ describe ApiEntreprise::API do
end
context 'with specific token for procedure' do
let(:token) { 'token-for-demarche' }
let(:token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" }
let(:procedure) { create(:procedure, api_entreprise_token: token) }
let(:procedure_id) { procedure.id }
it 'call api-entreprise with specfic token' do
subject
expect(WebMock).to have_requested(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siren}?.*token=token-for-demarche/)
expect(WebMock).to have_requested(:get, /https:\/\/entreprise.api.gouv.fr\/v2\/entreprises\/#{siren}?.*token=#{token}/)
end
end

View file

@ -35,6 +35,7 @@ describe TagsSubstitutionConcern, type: :model do
let(:individual) { nil }
let(:etablissement) { create(:etablissement) }
let!(:dossier) { create(:dossier, procedure: procedure, individual: individual, etablissement: etablissement) }
let(:instructeur) { create(:instructeur) }
before { Timecop.freeze(Time.zone.now) }
@ -242,7 +243,7 @@ describe TagsSubstitutionConcern, type: :model do
end
context 'when the dossier has a motivation' do
let(:dossier) { create(:dossier, motivation: 'motivation') }
let(:dossier) { create(:dossier, :accepte, motivation: 'motivation') }
context 'and the template has some dossier tags' do
let(:template) { '--motivation-- --numéro du dossier--' }
@ -318,9 +319,13 @@ describe TagsSubstitutionConcern, type: :model do
context "when using a date tag" do
before do
dossier.en_construction_at = Time.zone.local(2001, 2, 3)
dossier.en_instruction_at = Time.zone.local(2004, 5, 6)
dossier.processed_at = Time.zone.local(2007, 8, 9)
Timecop.freeze(Time.zone.local(2001, 2, 3))
dossier.passer_en_construction!
Timecop.freeze(Time.zone.local(2004, 5, 6))
dossier.passer_en_instruction!(instructeur)
Timecop.freeze(Time.zone.local(2007, 8, 9))
dossier.accepter!(instructeur, nil, nil)
Timecop.return
end
context "with date de dépôt" do

View file

@ -209,8 +209,17 @@ describe Dossier do
let(:date1) { 1.day.ago }
let(:date2) { 1.hour.ago }
let(:date3) { 1.minute.ago }
let(:dossier) { create(:dossier, :with_entreprise, user: user, procedure: procedure, en_construction_at: date1, en_instruction_at: date2, processed_at: date3, motivation: "Motivation") }
let!(:follow) { create(:follow, instructeur: instructeur, dossier: dossier) }
let(:dossier) do
d = create(:dossier, :with_entreprise, user: user, procedure: procedure)
Timecop.freeze(date1)
d.passer_en_construction!
Timecop.freeze(date2)
d.passer_en_instruction!(instructeur)
Timecop.freeze(date3)
d.accepter!(instructeur, "Motivation", nil)
Timecop.return
d
end
describe "followers_instructeurs" do
let(:non_following_instructeur) { create(:instructeur) }
@ -346,13 +355,14 @@ describe Dossier do
let(:state) { Dossier.states.fetch(:brouillon) }
let(:dossier) { create(:dossier, state: state) }
let(:beginning_of_day) { Time.zone.now.beginning_of_day }
let(:instructeur) { create(:instructeur) }
before { Timecop.freeze(beginning_of_day) }
after { Timecop.return }
context 'when dossier is en_construction' do
before do
dossier.en_construction!
dossier.passer_en_construction!
dossier.reload
end
@ -361,8 +371,8 @@ describe Dossier do
it 'should keep first en_construction_at date' do
Timecop.return
dossier.en_instruction!
dossier.en_construction!
dossier.passer_en_instruction!(instructeur)
dossier.repasser_en_construction!(instructeur)
expect(dossier.en_construction_at).to eq(beginning_of_day)
end
@ -370,9 +380,10 @@ describe Dossier do
context 'when dossier is en_instruction' do
let(:state) { Dossier.states.fetch(:en_construction) }
let(:instructeur) { create(:instructeur) }
before do
dossier.en_instruction!
dossier.passer_en_instruction!(instructeur)
dossier.reload
end
@ -381,39 +392,48 @@ describe Dossier do
it 'should keep first en_instruction_at date if dossier is set to en_construction again' do
Timecop.return
dossier.en_construction!
dossier.en_instruction!
dossier.repasser_en_construction!(instructeur)
dossier.passer_en_instruction!(instructeur)
expect(dossier.en_instruction_at).to eq(beginning_of_day)
end
end
shared_examples 'dossier is processed' do |new_state|
before do
dossier.update(state: new_state)
dossier.reload
end
it { expect(dossier.state).to eq(new_state) }
it { expect(dossier.processed_at).to eq(beginning_of_day) }
end
context 'when dossier is accepte' do
let(:state) { Dossier.states.fetch(:en_instruction) }
it_behaves_like 'dossier is processed', Dossier.states.fetch(:accepte)
before do
dossier.accepter!(instructeur, nil, nil)
dossier.reload
end
it { expect(dossier.state).to eq(Dossier.states.fetch(:accepte)) }
it { expect(dossier.traitements.last.processed_at).to eq(beginning_of_day) }
it { expect(dossier.processed_at).to eq(beginning_of_day) }
end
context 'when dossier is refuse' do
let(:state) { Dossier.states.fetch(:en_instruction) }
it_behaves_like 'dossier is processed', Dossier.states.fetch(:refuse)
before do
dossier.refuser!(instructeur, nil, nil)
dossier.reload
end
it { expect(dossier.state).to eq(Dossier.states.fetch(:refuse)) }
it { expect(dossier.processed_at).to eq(beginning_of_day) }
end
context 'when dossier is sans_suite' do
let(:state) { Dossier.states.fetch(:en_instruction) }
it_behaves_like 'dossier is processed', Dossier.states.fetch(:sans_suite)
before do
dossier.classer_sans_suite!(instructeur, nil, nil)
dossier.reload
end
it { expect(dossier.state).to eq(Dossier.states.fetch(:sans_suite)) }
it { expect(dossier.processed_at).to eq(beginning_of_day) }
end
end
@ -478,13 +498,14 @@ describe Dossier do
describe "#send_dossier_received" do
let(:procedure) { create(:procedure) }
let(:dossier) { create(:dossier, procedure: procedure, state: Dossier.states.fetch(:en_construction)) }
let(:instructeur) { create(:instructeur) }
before do
allow(NotificationMailer).to receive(:send_dossier_received).and_return(double(deliver_later: nil))
end
it "sends an email when the dossier becomes en_instruction" do
dossier.en_instruction!
dossier.passer_en_instruction!(instructeur)
expect(NotificationMailer).to have_received(:send_dossier_received).with(dossier)
end
@ -777,6 +798,7 @@ describe Dossier do
describe 'webhook' do
let(:dossier) { create(:dossier) }
let(:instructeur) { create(:instructeur) }
it 'should not call webhook' do
expect {
@ -788,19 +810,19 @@ describe Dossier do
dossier.procedure.update_column(:web_hook_url, '/webhook.json')
expect {
dossier.update_column(:motivation, 'bonjour')
dossier.update_column(:search_terms, 'bonjour')
}.to_not have_enqueued_job(WebHookJob)
expect {
dossier.en_construction!
dossier.passer_en_construction!
}.to have_enqueued_job(WebHookJob)
expect {
dossier.update_column(:motivation, 'bonjour2')
dossier.update_column(:search_terms, 'bonjour2')
}.to_not have_enqueued_job(WebHookJob)
expect {
dossier.en_instruction!
dossier.passer_en_instruction!(instructeur)
}.to have_enqueued_job(WebHookJob)
end
end
@ -891,8 +913,11 @@ describe Dossier do
after { Timecop.return }
it { expect(dossier.traitements.last.motivation).to eq('motivation') }
it { expect(dossier.motivation).to eq('motivation') }
it { expect(dossier.traitements.last.instructeur_email).to eq(instructeur.email) }
it { expect(dossier.en_instruction_at).to eq(dossier.en_instruction_at) }
it { expect(dossier.traitements.last.processed_at).to eq(now) }
it { expect(dossier.processed_at).to eq(now) }
it { expect(dossier.state).to eq('accepte') }
it { expect(last_operation.operation).to eq('accepter') }

View file

@ -478,7 +478,7 @@ describe Instructeur, type: :model do
before do
procedure_to_assign.update(declarative_with_state: "accepte")
DeclarativeProceduresJob.new.perform
dossier.update(processed_at: Time.zone.yesterday.beginning_of_day)
dossier.traitements.last.update(processed_at: Time.zone.yesterday.beginning_of_day)
dossier.reload
end

View file

@ -205,6 +205,13 @@ describe Procedure do
it { expect(procedure.valid?).to eq(false) }
end
end
context 'api_entreprise_token' do
let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" }
let(:invalid_token) { 'plouf' }
it { is_expected.to allow_value(valid_token).for(:api_entreprise_token) }
it { is_expected.not_to allow_value(invalid_token).for(:api_entreprise_token) }
end
end
context 'when juridique_required is false' do
@ -335,7 +342,7 @@ describe Procedure do
end
describe 'api_entreprise_token_expired?' do
let(:token) { "mon-token" }
let(:token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" }
let(:procedure) { create(:procedure, api_entreprise_token: token) }
let(:payload) {
[
@ -958,8 +965,7 @@ describe Procedure do
let(:procedure) { create(:procedure) }
def create_dossier(construction_date:, instruction_date:, processed_date:)
dossier = create(:dossier, :accepte, procedure: procedure)
dossier.update!(en_construction_at: construction_date, en_instruction_at: instruction_date, processed_at: processed_date)
dossier = create(:dossier, :accepte, procedure: procedure, en_construction_at: construction_date, en_instruction_at: instruction_date, processed_at: processed_date)
end
before do

View file

@ -48,7 +48,7 @@ shared_examples 'type_de_champ_spec' do
}
end
context 'remove piece_justificative_template' do
context 'changing the type_champ from a piece_justificative' do
context 'when the tdc is piece_justificative' do
let(:template_double) { double('template', attached?: attached, purge_later: true) }
let(:tdc) { create(:type_de_champ_piece_justificative) }
@ -89,6 +89,48 @@ shared_examples 'type_de_champ_spec' do
end
end
describe 'changing the type_champ from a repetition' do
let(:tdc) { create(:type_de_champ_repetition, :with_types_de_champ) }
before do
tdc.update_attribute('type_champ', target_type_champ)
end
context 'when the target type_champ is not repetition' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) }
it 'removes the children types de champ' do
expect(tdc.types_de_champ).to be_empty
end
end
end
describe 'changing the type_champ from a drop_down_list' do
let(:tdc) { create(:type_de_champ_drop_down_list) }
before do
tdc.update_attribute('type_champ', target_type_champ)
end
context 'when the target type_champ is not drop_down_list' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:text) }
it { expect(tdc.drop_down_options).to be_nil }
end
context 'when the target type_champ is linked_drop_down_list' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:linked_drop_down_list) }
it { expect(tdc.drop_down_options).to be_present }
end
context 'when the target type_champ is multiple_drop_down_list' do
let(:target_type_champ) { TypeDeChamp.type_champs.fetch(:multiple_drop_down_list) }
it { expect(tdc.drop_down_options).to be_present }
end
end
context 'delegate validation to dynamic type' do
subject { build(:type_de_champ_text) }
let(:dynamic_type) do

View file

@ -8,7 +8,8 @@ describe ApiEntrepriseService do
let(:siret) { '41816609600051' }
let(:etablissements_status) { 200 }
let(:etablissements_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') }
let(:procedure) { create(:procedure, api_entreprise_token: 'un-jeton') }
let(:valid_token) { "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" }
let(:procedure) { create(:procedure, api_entreprise_token: valid_token) }
let(:dossier) { create(:dossier, procedure: procedure) }
let(:subject) { ApiEntrepriseService.create_etablissement(dossier, siret, procedure.id) }

View file

@ -66,7 +66,7 @@ describe NotificationService do
before do
procedure.update(declarative_with_state: "accepte")
DeclarativeProceduresJob.new.perform
dossier.update(processed_at: Time.zone.yesterday.beginning_of_day)
dossier.traitements.last.update!(processed_at: Time.zone.yesterday.beginning_of_day)
dossier.reload
end