commit
b4720d9a76
32 changed files with 600 additions and 105 deletions
|
@ -205,6 +205,23 @@ module Instructeurs
|
|||
end
|
||||
end
|
||||
|
||||
def download_export
|
||||
export_format = params[:export_format]
|
||||
|
||||
if procedure.should_generate_export?(export_format)
|
||||
procedure.queue_export(current_instructeur, export_format)
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
flash.notice = "Nous générons cet export. Lorsque celui-ci sera disponible, vous recevrez une notification par email accompagnée d'un lien de téléchargement."
|
||||
@procedure = procedure
|
||||
end
|
||||
end
|
||||
else
|
||||
redirect_to url_for(procedure.export_file(export_format))
|
||||
end
|
||||
end
|
||||
|
||||
def email_notifications
|
||||
@procedure = procedure
|
||||
@assign_to = assign_to
|
||||
|
|
|
@ -282,13 +282,14 @@ module Users
|
|||
end
|
||||
|
||||
# FIXME: require(:dossier) when all the champs are united
|
||||
def champs_params
|
||||
params.permit(dossier: {
|
||||
def champs_and_groupe_instructeurs_params
|
||||
params.permit(dossier: [
|
||||
:groupe_instructeur_id,
|
||||
champs_attributes: [
|
||||
:id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [],
|
||||
champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, value: []]
|
||||
]
|
||||
})
|
||||
])
|
||||
end
|
||||
|
||||
def dossier
|
||||
|
@ -302,7 +303,7 @@ module Users
|
|||
def update_dossier_and_compute_errors
|
||||
errors = []
|
||||
|
||||
if champs_params[:dossier] && !@dossier.update(champs_params[:dossier])
|
||||
if champs_and_groupe_instructeurs_params[:dossier] && !@dossier.update(champs_and_groupe_instructeurs_params[:dossier])
|
||||
errors += @dossier.errors.full_messages
|
||||
end
|
||||
|
||||
|
|
6
app/jobs/export_procedure_job.rb
Normal file
6
app/jobs/export_procedure_job.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class ExportProcedureJob < ApplicationJob
|
||||
def perform(procedure, instructeur, export_format)
|
||||
procedure.prepare_export_download(export_format)
|
||||
InstructeurMailer.notify_procedure_export_available(instructeur, procedure, export_format).deliver_later
|
||||
end
|
||||
end
|
|
@ -15,8 +15,8 @@ class Sendinblue::Api
|
|||
client_key.present?
|
||||
end
|
||||
|
||||
def identify(email, attributes = {})
|
||||
req = api_request('identify', email: email, attributes: attributes)
|
||||
def update_contact(email, attributes = {})
|
||||
req = post_api_request('contacts', email: email, attributes: attributes, updateEnabled: true)
|
||||
req.on_complete do |response|
|
||||
if !response.success?
|
||||
push_failure("Error while updating identity for administrateur '#{email}' in Sendinblue: #{response.response_code} '#{response.body}'")
|
||||
|
@ -34,7 +34,7 @@ class Sendinblue::Api
|
|||
private
|
||||
|
||||
def hydra
|
||||
@hydra ||= Typhoeus::Hydra.new
|
||||
@hydra ||= Typhoeus::Hydra.new(max_concurrency: 50)
|
||||
end
|
||||
|
||||
def push_failure(failure)
|
||||
|
@ -49,8 +49,8 @@ class Sendinblue::Api
|
|||
end
|
||||
end
|
||||
|
||||
def api_request(path, body)
|
||||
url = "#{SENDINBLUE_API_URL}/#{path}"
|
||||
def post_api_request(path, body)
|
||||
url = "#{SENDINBLUE_API_V3_URL}/#{path}"
|
||||
|
||||
Typhoeus::Request.new(
|
||||
url,
|
||||
|
@ -62,12 +62,12 @@ class Sendinblue::Api
|
|||
|
||||
def headers
|
||||
{
|
||||
'ma-key': client_key,
|
||||
'api-key': client_key,
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
}
|
||||
end
|
||||
|
||||
def client_key
|
||||
Rails.application.secrets.sendinblue[:client_key]
|
||||
Rails.application.secrets.sendinblue[:api_v3_key]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -42,4 +42,12 @@ class InstructeurMailer < ApplicationMailer
|
|||
|
||||
mail(to: instructeur.email, subject: subject)
|
||||
end
|
||||
|
||||
def notify_procedure_export_available(instructeur, procedure, export_format)
|
||||
@procedure = procedure
|
||||
@export_format = export_format
|
||||
subject = "Votre export de la démarche nº #{procedure.id} est disponible"
|
||||
|
||||
mail(to: instructeur.email, subject: subject)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -197,7 +197,7 @@ class Dossier < ApplicationRecord
|
|||
|
||||
before_validation :update_state_dates, if: -> { state_changed? }
|
||||
|
||||
before_save :build_default_champs, if: Proc.new { groupe_instructeur_id_changed? }
|
||||
before_save :build_default_champs, if: Proc.new { groupe_instructeur_id_was.nil? }
|
||||
before_save :build_default_individual, if: Proc.new { procedure.for_individual? }
|
||||
before_save :update_search_terms
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ class Procedure < ApplicationRecord
|
|||
include ProcedureStatsConcern
|
||||
|
||||
MAX_DUREE_CONSERVATION = 36
|
||||
MAX_DUREE_CONSERVATION_EXPORT = 3.hours
|
||||
|
||||
has_many :types_de_champ, -> { root.public_only.ordered }, inverse_of: :procedure, dependent: :destroy
|
||||
has_many :types_de_champ_private, -> { root.private_only.ordered }, class_name: 'TypeDeChamp', inverse_of: :procedure, dependent: :destroy
|
||||
|
@ -29,12 +30,16 @@ class Procedure < ApplicationRecord
|
|||
has_one :refused_mail, class_name: "Mails::RefusedMail", dependent: :destroy
|
||||
has_one :without_continuation_mail, class_name: "Mails::WithoutContinuationMail", dependent: :destroy
|
||||
|
||||
has_one :defaut_groupe_instructeur, -> { where(label: GroupeInstructeur::DEFAULT_LABEL) }, class_name: 'GroupeInstructeur', inverse_of: :procedure
|
||||
has_one :defaut_groupe_instructeur, -> { order(:id) }, class_name: 'GroupeInstructeur', inverse_of: :procedure
|
||||
|
||||
has_one_attached :logo
|
||||
has_one_attached :notice
|
||||
has_one_attached :deliberation
|
||||
|
||||
has_one_attached :csv_export_file
|
||||
has_one_attached :xlsx_export_file
|
||||
has_one_attached :ods_export_file
|
||||
|
||||
accepts_nested_attributes_for :types_de_champ, reject_if: proc { |attributes| attributes['libelle'].blank? }, allow_destroy: true
|
||||
accepts_nested_attributes_for :types_de_champ_private, reject_if: proc { |attributes| attributes['libelle'].blank? }, allow_destroy: true
|
||||
|
||||
|
@ -128,11 +133,88 @@ class Procedure < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def csv_export_stale?
|
||||
!csv_export_file.attached? || csv_export_file.created_at < MAX_DUREE_CONSERVATION_EXPORT.ago
|
||||
end
|
||||
|
||||
def xlsx_export_stale?
|
||||
!xlsx_export_file.attached? || xlsx_export_file.created_at < MAX_DUREE_CONSERVATION_EXPORT.ago
|
||||
end
|
||||
|
||||
def ods_export_stale?
|
||||
!ods_export_file.attached? || ods_export_file.created_at < MAX_DUREE_CONSERVATION_EXPORT.ago
|
||||
end
|
||||
|
||||
def should_generate_export?(format)
|
||||
case format.to_sym
|
||||
when :csv
|
||||
return csv_export_stale? && !csv_export_queued?
|
||||
when :xlsx
|
||||
return xlsx_export_stale? && !xlsx_export_queued?
|
||||
when :ods
|
||||
return ods_export_stale? && !ods_export_queued?
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def export_file(export_format)
|
||||
case export_format.to_sym
|
||||
when :csv
|
||||
csv_export_file
|
||||
when :xlsx
|
||||
xlsx_export_file
|
||||
when :ods
|
||||
ods_export_file
|
||||
end
|
||||
end
|
||||
|
||||
def queue_export(instructeur, export_format)
|
||||
ExportProcedureJob.perform_now(self, instructeur, export_format)
|
||||
case export_format.to_sym
|
||||
when :csv
|
||||
update(csv_export_queued: true)
|
||||
when :xlsx
|
||||
update(xlsx_export_queued: true)
|
||||
when :ods
|
||||
update(ods_export_queued: true)
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_export_download(format)
|
||||
service = ProcedureExportV2Service.new(self, self.dossiers)
|
||||
filename = export_filename(format)
|
||||
|
||||
case format.to_sym
|
||||
when :csv
|
||||
csv_export_file.attach(
|
||||
io: StringIO.new(service.to_csv),
|
||||
filename: filename,
|
||||
content_type: 'text/csv'
|
||||
)
|
||||
update(csv_export_queued: false)
|
||||
when :xlsx
|
||||
xlsx_export_file.attach(
|
||||
io: StringIO.new(service.to_xlsx),
|
||||
filename: filename,
|
||||
content_type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
)
|
||||
update(xlsx_export_queued: false)
|
||||
when :ods
|
||||
ods_export_file.attach(
|
||||
io: StringIO.new(service.to_ods),
|
||||
filename: filename,
|
||||
content_type: 'application/vnd.oasis.opendocument.spreadsheet'
|
||||
)
|
||||
update(ods_export_queued: false)
|
||||
end
|
||||
end
|
||||
|
||||
def reset!
|
||||
if locked?
|
||||
raise "Can not reset a locked procedure."
|
||||
else
|
||||
groupe_instructeurs.each { |gi| gi.dossiers.destroy_all }
|
||||
purge_export_files
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -174,6 +256,14 @@ class Procedure < ApplicationRecord
|
|||
procedure.blank? || administrateur.owns?(procedure)
|
||||
end
|
||||
|
||||
def purge_export_files
|
||||
xlsx_export_file.purge_later
|
||||
ods_export_file.purge_later
|
||||
csv_export_file.purge_later
|
||||
|
||||
update(csv_export_queued: false, xlsx_export_queued: false, ods_export_queued: false)
|
||||
end
|
||||
|
||||
def locked?
|
||||
publiee_ou_archivee?
|
||||
end
|
||||
|
@ -513,12 +603,14 @@ class Procedure < ApplicationRecord
|
|||
|
||||
def after_archive
|
||||
update!(archived_at: Time.zone.now)
|
||||
purge_export_files
|
||||
end
|
||||
|
||||
def after_hide
|
||||
now = Time.zone.now
|
||||
update!(hidden_at: now)
|
||||
dossiers.update_all(hidden_at: now)
|
||||
purge_export_files
|
||||
end
|
||||
|
||||
def after_draft
|
||||
|
|
|
@ -10,7 +10,7 @@ class AdministrateurUsageStatisticsService
|
|||
def update_administrateurs
|
||||
Administrateur.includes(:user).find_each do |administrateur|
|
||||
stats = administrateur_stats(administrateur)
|
||||
api.identify(administrateur.email, stats)
|
||||
api.update_contact(administrateur.email, stats)
|
||||
end
|
||||
api.run
|
||||
end
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
%p
|
||||
Bonjour,
|
||||
|
||||
%p
|
||||
Votre export des dossiers de la démarche nº #{@procedure.id} « #{@procedure.libelle} » au format #{@export_format} est prêt.
|
||||
|
||||
%p
|
||||
Cliquez sur le lien ci-dessous pour le télécharger :
|
||||
= link_to('Télécharger l\'export des dossiers', download_export_instructeur_procedure_url(@procedure, :export_format => @export_format))
|
||||
|
||||
= render partial: "layouts/mailers/signature"
|
|
@ -14,7 +14,7 @@
|
|||
%h2 Formulaire
|
||||
|
||||
- champs = @dossier.champs
|
||||
- if champs.any?
|
||||
- if champs.any? || @dossier.procedure.routee?
|
||||
= render partial: "shared/dossiers/champs", locals: { champs: champs, dossier: @dossier, demande_seen_at: nil, profile: 'instructeur' }
|
||||
|
||||
%h2 Annotations privées
|
||||
|
|
|
@ -4,14 +4,34 @@
|
|||
Télécharger tous les dossiers
|
||||
- old_format_limit_date = Date.parse("Oct 31 2019")
|
||||
- export_v1_enabled = old_format_limit_date > Time.zone.today
|
||||
- export_v2_enabled = Flipper[:procedure_export_v2_enabled] || !export_v1_enabled
|
||||
.dropdown-content.fade-in-down{ style: !export_v1_enabled ? 'width: 330px' : '' }
|
||||
%ul.dropdown-items
|
||||
- if export_v2_enabled
|
||||
%li
|
||||
= link_to "Au format .xlsx", procedure_dossiers_download_path(procedure, format: :xlsx, version: 'v2'), target: "_blank", rel: "noopener"
|
||||
- if procedure.xlsx_export_stale?
|
||||
- if procedure.xlsx_export_queued?
|
||||
L'export au format .xlsx est en cours de préparation, vous recevrez un email lorsqu'il sera disponible.
|
||||
- else
|
||||
= link_to "Exporter au format .xlsx", download_export_instructeur_procedure_path(procedure, export_format: :xlsx), remote: true
|
||||
- else
|
||||
= link_to "Au format .xlsx", url_for(procedure.xlsx_export_file), target: "_blank", rel: "noopener"
|
||||
%li
|
||||
= link_to "Au format .ods", procedure_dossiers_download_path(procedure, format: :ods, version: 'v2'), target: "_blank", rel: "noopener"
|
||||
- if procedure.ods_export_stale?
|
||||
- if procedure.ods_export_queued?
|
||||
L'export au format .ods est en cours de préparation, vous recevrez un email lorsqu'il sera disponible.
|
||||
- else
|
||||
= link_to "Exporter au format .ods", download_export_instructeur_procedure_path(procedure, export_format: :ods), remote: true
|
||||
- else
|
||||
= link_to "Au format .ods", url_for(procedure.ods_export_file), target: "_blank", rel: "noopener"
|
||||
%li
|
||||
= link_to "Au format .csv", procedure_dossiers_download_path(procedure, format: :csv, version: 'v2'), target: "_blank", rel: "noopener"
|
||||
- if procedure.csv_export_stale?
|
||||
- if procedure.csv_export_queued?
|
||||
L'export au format .csv est en cours de préparation, vous recevrez un email lorsqu'il sera disponible.
|
||||
- else
|
||||
= link_to "Exporter au format .csv", download_export_instructeur_procedure_path(procedure, export_format: :csv), remote: true
|
||||
- else
|
||||
= link_to "Au format .csv", url_for(procedure.csv_export_file), target: "_blank", rel: "noopener"
|
||||
- if export_v1_enabled
|
||||
- old_format_message = "(ancien format, jusqu’au #{old_format_limit_date.strftime('%d/%m/%Y')})"
|
||||
%li
|
||||
|
|
2
app/views/instructeurs/procedures/download_export.js.erb
Normal file
2
app/views/instructeurs/procedures/download_export.js.erb
Normal file
|
@ -0,0 +1,2 @@
|
|||
<%= render_to_element('.procedure-actions', partial: "download_dossiers", locals: { procedure: @procedure }) %>
|
||||
<%= render_flash %>
|
|
@ -1,3 +1,7 @@
|
|||
%table.table.vertical.dossier-champs
|
||||
%tbody
|
||||
- if dossier.procedure.routee?
|
||||
%th= dossier.procedure.routing_criteria_name
|
||||
%td= dossier.groupe_instructeur.label
|
||||
%td
|
||||
= render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: false }
|
||||
|
|
|
@ -23,6 +23,6 @@
|
|||
|
||||
.tab-title Formulaire
|
||||
- champs = dossier.champs.includes(:type_de_champ)
|
||||
- if champs.any?
|
||||
- if champs.any? || dossier.procedure.routee?
|
||||
.card
|
||||
= render partial: "shared/dossiers/champs", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile }
|
||||
= render partial: "shared/dossiers/champs", locals: { champs: champs, dossier: dossier, demande_seen_at: demande_seen_at, profile: profile }
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
|
||||
%hr
|
||||
|
||||
- if dossier.procedure.routee?
|
||||
= f.label :groupe_instructeur_id, dossier.procedure.routing_criteria_name
|
||||
= f.select :groupe_instructeur_id,
|
||||
dossier.procedure.groupe_instructeurs.order(:label).map { |gi| [gi.label, gi.id] },
|
||||
{},
|
||||
required: true
|
||||
|
||||
= f.fields_for :champs, dossier.champs do |champ_form|
|
||||
- champ = champ_form.object
|
||||
= render partial: "shared/dossiers/editable_champs/editable_champ",
|
||||
|
|
|
@ -3,6 +3,11 @@ default: &default
|
|||
encoding: unicode
|
||||
pool: <%= ENV.fetch("DB_POOL") { 5 } %>
|
||||
timeout: 5000
|
||||
# sql queries will be killed after 60s
|
||||
# we should reduce this number
|
||||
# A bigger timeout can be set for jobs
|
||||
variables:
|
||||
statement_timeout: <%= ENV['PG_STATEMENT_TIMEOUT'] || 60000 %>
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
|
|
@ -34,6 +34,7 @@ features = [
|
|||
:mini_profiler,
|
||||
:operation_log_serialize_subject,
|
||||
:pre_maintenance_mode,
|
||||
:procedure_export_v2_enabled,
|
||||
:xray
|
||||
]
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ API_GEO_SANDBOX_URL = ENV.fetch("API_GEO_SANDBOX_URL", "https://sandbox.geo.api.
|
|||
HELPSCOUT_API_URL = ENV.fetch("HELPSCOUT_API_URL", "https://api.helpscout.net/v2")
|
||||
PIPEDRIVE_API_URL = ENV.fetch("PIPEDRIVE_API_URL", "https://api.pipedrive.com/v1")
|
||||
SENDINBLUE_API_URL = ENV.fetch("SENDINBLUE_API_URL", "https://in-automate.sendinblue.com/api/v2")
|
||||
SENDINBLUE_API_V3_URL = ENV.fetch("SENDINBLUE_API_V3_URL", "https://api.sendinblue.com/v3")
|
||||
UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/")
|
||||
|
||||
# Internal URLs
|
||||
|
|
|
@ -293,6 +293,7 @@ Rails.application.routes.draw do
|
|||
post 'add_filter'
|
||||
get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter'
|
||||
get 'download_dossiers'
|
||||
get 'download_export'
|
||||
get 'stats'
|
||||
get 'email_notifications'
|
||||
patch 'update_email_notifications'
|
||||
|
|
|
@ -55,6 +55,7 @@ defaults: &defaults
|
|||
sendinblue:
|
||||
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
|
||||
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
|
||||
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
|
||||
matomo:
|
||||
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
|
||||
client_key: <%= ENV['MATOMO_ID'] %>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
class AddExportQueuedToProcedures < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
add_column :procedures, :csv_export_queued, :boolean
|
||||
add_column :procedures, :xlsx_export_queued, :boolean
|
||||
add_column :procedures, :ods_export_queued, :boolean
|
||||
end
|
||||
end
|
|
@ -487,6 +487,9 @@ ActiveRecord::Schema.define(version: 2019_10_14_160538) do
|
|||
t.string "declarative_with_state"
|
||||
t.text "monavis_embed"
|
||||
t.text "routing_criteria_name"
|
||||
t.boolean "csv_export_queued"
|
||||
t.boolean "xlsx_export_queued"
|
||||
t.boolean "ods_export_queued"
|
||||
t.index ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
|
||||
t.index ["hidden_at"], name: "index_procedures_on_hidden_at"
|
||||
t.index ["parent_procedure_id"], name: "index_procedures_on_parent_procedure_id"
|
||||
|
|
|
@ -1,27 +1,16 @@
|
|||
FactoryBot.define do
|
||||
sequence(:expert_email) { |n| "expert#{n}@expert.com" }
|
||||
|
||||
factory :avis do
|
||||
email { generate(:expert_email) }
|
||||
introduction { 'Bonjour, merci de me donner votre avis sur ce dossier' }
|
||||
confidentiel { false }
|
||||
|
||||
before(:create) do |avis, _evaluator|
|
||||
if !avis.instructeur
|
||||
avis.instructeur = create :instructeur
|
||||
end
|
||||
end
|
||||
|
||||
before(:create) do |avis, _evaluator|
|
||||
if !avis.dossier
|
||||
avis.dossier = create :dossier
|
||||
end
|
||||
end
|
||||
|
||||
before(:create) do |avis, _evaluator|
|
||||
if !avis.claimant
|
||||
avis.claimant = create :instructeur
|
||||
end
|
||||
end
|
||||
association :dossier
|
||||
association :claimant, factory: :instructeur
|
||||
|
||||
trait :with_answer do
|
||||
answer { "Mon avis se décompose en deux points :\n- La demande semble pertinente\n- Le demandeur remplit les conditions." }
|
||||
answer { "Mon avis se décompose en deux points :\n- La demande semble pertinente\n- Le demandeur remplit les conditions." }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -74,6 +74,12 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
|
||||
trait :routee do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.groupe_instructeurs.create(label: '2nd groupe')
|
||||
end
|
||||
end
|
||||
|
||||
trait :for_individual do
|
||||
after(:build) do |procedure, _evaluator|
|
||||
procedure.for_individual = true
|
||||
|
@ -236,5 +242,47 @@ FactoryBot.define do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_csv_export_file do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.csv_export_file.attach(io: StringIO.new("some csv data"), filename: "export.csv", content_type: "text/plain")
|
||||
procedure.csv_export_file.update(created_at: 5.minutes.ago)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_stale_csv_export_file do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.csv_export_file.attach(io: StringIO.new("some csv data"), filename: "export.csv", content_type: "text/plain")
|
||||
procedure.csv_export_file.update(created_at: 4.hours.ago)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_ods_export_file do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.ods_export_file.attach(io: StringIO.new("some ods data"), filename: "export.ods", content_type: "text/plain")
|
||||
procedure.ods_export_file.update(created_at: 5.minutes.ago)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_stale_ods_export_file do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.ods_export_file.attach(io: StringIO.new("some ods data"), filename: "export.ods", content_type: "text/plain")
|
||||
procedure.ods_export_file.update(created_at: 4.hours.ago)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_xlsx_export_file do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.xlsx_export_file.attach(io: StringIO.new("some xlsx data"), filename: "export.xlsx", content_type: "text/plain")
|
||||
procedure.xlsx_export_file.update(created_at: 5.minutes.ago)
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_stale_xlsx_export_file do
|
||||
after(:create) do |procedure, _evaluator|
|
||||
procedure.xlsx_export_file.attach(io: StringIO.new("some xlsx data"), filename: "export.xlsx", content_type: "text/plain")
|
||||
procedure.xlsx_export_file.update(created_at: 4.hours.ago)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
127
spec/features/instructeurs/expert_spec.rb
Normal file
127
spec/features/instructeurs/expert_spec.rb
Normal file
|
@ -0,0 +1,127 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Inviting an expert:' do
|
||||
include ActiveJob::TestHelper
|
||||
include ActionView::Helpers
|
||||
|
||||
let(:instructeur) { create(:instructeur, password: 'démarches-simplifiées-pwd') }
|
||||
let(:expert) { create(:instructeur, password: expert_password) }
|
||||
let(:expert_password) { 'mot de passe d’expert' }
|
||||
let(:procedure) { create(:procedure, :published, instructeurs: [instructeur]) }
|
||||
let(:dossier) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure) }
|
||||
|
||||
context 'as an Instructeur' do
|
||||
scenario 'I can invite an expert' do
|
||||
login_as instructeur.user, scope: :user
|
||||
visit instructeur_dossier_path(procedure, dossier)
|
||||
|
||||
click_on 'Avis externes'
|
||||
expect(page).to have_current_path(avis_instructeur_dossier_path(procedure, dossier))
|
||||
|
||||
fill_in 'avis_emails', with: 'expert1@exemple.fr, expert2@exemple.fr'
|
||||
fill_in 'avis_introduction', with: 'Bonjour, merci de me donner votre avis sur ce dossier.'
|
||||
page.select 'confidentiel', from: 'avis_confidentiel'
|
||||
|
||||
perform_enqueued_jobs do
|
||||
click_on 'Demander un avis'
|
||||
end
|
||||
|
||||
expect(page).to have_content('Une demande d\'avis a été envoyée')
|
||||
expect(page).to have_content('Avis des invités')
|
||||
within('.list-avis') do
|
||||
expect(page).to have_content('expert1@exemple.fr')
|
||||
expect(page).to have_content('expert2@exemple.fr')
|
||||
expect(page).to have_content('Bonjour, merci de me donner votre avis sur ce dossier.')
|
||||
end
|
||||
|
||||
invitation_email = open_email('expert2@exemple.fr')
|
||||
avis = Avis.find_by(email: 'expert2@exemple.fr')
|
||||
sign_up_link = sign_up_instructeur_avis_path(avis.id, avis.email)
|
||||
expect(invitation_email.body).to include(sign_up_link)
|
||||
end
|
||||
|
||||
context 'when experts submitted their answer' do
|
||||
let!(:answered_avis) { create(:avis, :with_answer, dossier: dossier, claimant: instructeur, email: expert.email) }
|
||||
|
||||
scenario 'I can read the expert answer' do
|
||||
login_as instructeur.user, scope: :user
|
||||
visit instructeur_dossier_path(procedure, dossier)
|
||||
|
||||
click_on 'Avis externes'
|
||||
|
||||
expect(page).to have_content(expert.email)
|
||||
answered_avis.answer.split("\n").each do |answer_line|
|
||||
expect(page).to have_content(answer_line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an invited Expert' do
|
||||
let(:avis_email) { expert.email }
|
||||
let(:avis) { create(:avis, dossier: dossier, claimant: instructeur, email: avis_email, confidentiel: true) }
|
||||
|
||||
context 'when I don’t already have an account' do
|
||||
let(:avis_email) { 'not-signed-up-expert@exemple.fr' }
|
||||
|
||||
scenario 'I can sign up' do
|
||||
visit sign_up_instructeur_avis_path(avis.id, avis_email)
|
||||
|
||||
expect(page).to have_field('Email', with: avis_email, disabled: true)
|
||||
fill_in 'Mot de passe', with: 'This is a very complicated password !'
|
||||
click_on 'Créer un compte'
|
||||
|
||||
expect(page).to have_current_path(instructeur_avis_index_path)
|
||||
expect(page).to have_text('avis à donner 1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when I already have an existing account' do
|
||||
let(:avis_email) { expert.email }
|
||||
|
||||
scenario 'I can sign in' do
|
||||
visit sign_up_instructeur_avis_path(avis.id, avis_email)
|
||||
|
||||
expect(page).to have_current_path(new_user_session_path)
|
||||
sign_in_with(expert.email, expert_password)
|
||||
|
||||
expect(page).to have_current_path(instructeur_avis_index_path)
|
||||
expect(page).to have_text('avis à donner 1')
|
||||
end
|
||||
end
|
||||
|
||||
scenario 'I can give an answer' do
|
||||
avis # create avis
|
||||
login_as expert.user, scope: :user
|
||||
|
||||
visit instructeur_avis_index_path
|
||||
expect(page).to have_text('avis à donner 1')
|
||||
expect(page).to have_text('avis donnés 0')
|
||||
|
||||
click_on avis.dossier.user.email
|
||||
|
||||
within('.tabs') { click_on 'Avis' }
|
||||
expect(page).to have_text("Demandeur : #{instructeur.email}")
|
||||
expect(page).to have_text('Cet avis est confidentiel')
|
||||
|
||||
fill_in 'avis_answer', with: 'Ma réponse d’expert : c’est un oui.'
|
||||
find('.piece-justificative input[type=file]').attach_file(Rails.root + 'spec/fixtures/files/RIB.pdf')
|
||||
click_on 'Envoyer votre avis'
|
||||
|
||||
expect(page).to have_content('Votre réponse est enregistrée')
|
||||
expect(page).to have_content('Ma réponse d’expert : c’est un oui.')
|
||||
expect(page).to have_content('RIB.pdf')
|
||||
|
||||
within('.new-header') { click_on 'Avis' }
|
||||
expect(page).to have_text('avis à donner 0')
|
||||
expect(page).to have_text('avis donné 1')
|
||||
end
|
||||
|
||||
# TODO
|
||||
# scenario 'I can read other experts advices' do
|
||||
# end
|
||||
|
||||
# scenario 'I can invite other experts' do
|
||||
# end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'The instructeur part' do
|
||||
feature 'Instructing a dossier:' do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let(:password) { 'démarches-simplifiées-pwd' }
|
||||
|
@ -93,59 +93,6 @@ feature 'The instructeur part' do
|
|||
expect(page).to have_text('Aucun dossier')
|
||||
end
|
||||
|
||||
scenario 'A instructeur can use avis' do
|
||||
log_in(instructeur.email, password)
|
||||
|
||||
click_on procedure.libelle
|
||||
click_on dossier.user.email
|
||||
|
||||
click_on 'Avis externes'
|
||||
expect(page).to have_current_path(avis_instructeur_dossier_path(procedure, dossier))
|
||||
|
||||
expert_email = 'expert@tps.com'
|
||||
|
||||
perform_enqueued_jobs do
|
||||
ask_confidential_avis(expert_email, 'a good introduction')
|
||||
end
|
||||
|
||||
log_out
|
||||
|
||||
avis = dossier.avis.first
|
||||
test_mail(expert_email, sign_up_instructeur_avis_path(avis, expert_email))
|
||||
|
||||
avis_sign_up(avis, expert_email)
|
||||
|
||||
expect(page).to have_current_path(instructeur_avis_index_path)
|
||||
expect(page).to have_text('avis à donner 1')
|
||||
expect(page).to have_text('avis donnés 0')
|
||||
|
||||
click_on dossier.user.email
|
||||
expect(page).to have_current_path(instructeur_avis_path(dossier.avis.first))
|
||||
|
||||
within(:css, '.tabs') do
|
||||
click_on 'Avis'
|
||||
end
|
||||
expect(page).to have_current_path(instruction_instructeur_avis_path(dossier.avis.first))
|
||||
|
||||
within(:css, '.give-avis') do
|
||||
expect(page).to have_text("Demandeur : #{instructeur.email}")
|
||||
expect(page).to have_text('a good introduction')
|
||||
expect(page).to have_text('Cet avis est confidentiel')
|
||||
fill_in 'avis_answer', with: 'a great answer'
|
||||
click_on 'Envoyer votre avis'
|
||||
end
|
||||
|
||||
log_out
|
||||
|
||||
log_in(instructeur.email, password, check_email: false)
|
||||
|
||||
click_on procedure.libelle
|
||||
click_on dossier.user.email
|
||||
click_on 'Avis externes'
|
||||
|
||||
expect(page).to have_text('a great answer')
|
||||
end
|
||||
|
||||
scenario 'A instructeur can see the personnes impliquées' do
|
||||
instructeur2 = FactoryBot.create(:instructeur, password: password)
|
||||
|
|
@ -46,4 +46,18 @@ RSpec.describe InstructeurMailer, type: :mailer do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#notify_procedure_export_available' do
|
||||
let(:instructeur) { create(:instructeur) }
|
||||
let(:procedure) { create(:procedure, :published, instructeurs: [instructeur]) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:format) { 'xlsx' }
|
||||
|
||||
context 'when the mail is sent' do
|
||||
subject { described_class.notify_procedure_export_available(instructeur, procedure, format) }
|
||||
it 'contains a download link' do
|
||||
expect(subject.body).to include download_export_instructeur_procedure_url(procedure, :export_format => format)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,6 +37,10 @@ class InstructeurMailerPreview < ActionMailer::Preview
|
|||
InstructeurMailer.send_notifications(instructeur, data)
|
||||
end
|
||||
|
||||
def notify_procedure_export_available
|
||||
InstructeurMailer.notify_procedure_export_available(instructeur, procedure, 'xlsx')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def instructeur
|
||||
|
|
|
@ -954,4 +954,165 @@ describe Procedure do
|
|||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.ods_export_stale?' do
|
||||
subject { procedure.ods_export_stale? }
|
||||
|
||||
context 'with no ods export' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'with a recent ods export' do
|
||||
let(:procedure) { create(:procedure, :with_ods_export_file) }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'with an old ods export' do
|
||||
let(:procedure) { create(:procedure, :with_stale_ods_export_file) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.csv_export_stale?' do
|
||||
subject { procedure.csv_export_stale? }
|
||||
|
||||
context 'with no csv export' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'with a recent csv export' do
|
||||
let(:procedure) { create(:procedure, :with_csv_export_file) }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'with an old csv export' do
|
||||
let(:procedure) { create(:procedure, :with_stale_csv_export_file) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.xlsx_export_stale?' do
|
||||
subject { procedure.xlsx_export_stale? }
|
||||
|
||||
context 'with no xlsx export' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'with a recent xlsx export' do
|
||||
let(:procedure) { create(:procedure, :with_xlsx_export_file) }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'with an old xlsx export' do
|
||||
let(:procedure) { create(:procedure, :with_stale_xlsx_export_file) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.should_generate_export?' do
|
||||
context 'xlsx' do
|
||||
subject { procedure.should_generate_export?('xlsx') }
|
||||
context 'with no export' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'with a recent export' do
|
||||
context 'when its not queued' do
|
||||
let(:procedure) { create(:procedure, :with_xlsx_export_file, xlsx_export_queued: false) }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when its already queued' do
|
||||
let(:procedure) { create(:procedure, :with_xlsx_export_file, xlsx_export_queued: true) }
|
||||
it { expect(procedure.xlsx_export_queued).to be true }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old export' do
|
||||
context 'when its not queued' do
|
||||
let(:procedure) { create(:procedure, :with_stale_xlsx_export_file, xlsx_export_queued: false) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when its already queued' do
|
||||
let(:procedure) { create(:procedure, :with_stale_xlsx_export_file, xlsx_export_queued: true) }
|
||||
it { expect(procedure.xlsx_export_queued).to be true }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'csv' do
|
||||
subject { procedure.should_generate_export?('csv') }
|
||||
context 'with no export' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'with a recent export' do
|
||||
context 'when its not queued' do
|
||||
let(:procedure) { create(:procedure, :with_csv_export_file, csv_export_queued: false) }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when its already queued' do
|
||||
let(:procedure) { create(:procedure, :with_csv_export_file, csv_export_queued: true) }
|
||||
it { expect(procedure.csv_export_queued).to be true }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old export' do
|
||||
context 'when its not queued' do
|
||||
let(:procedure) { create(:procedure, :with_stale_csv_export_file, csv_export_queued: false) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when its already queued' do
|
||||
let(:procedure) { create(:procedure, :with_stale_csv_export_file, csv_export_queued: true) }
|
||||
it { expect(procedure.csv_export_queued).to be true }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'ods' do
|
||||
subject { procedure.should_generate_export?('ods') }
|
||||
context 'with no export' do
|
||||
let(:procedure) { create(:procedure) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'with a recent export' do
|
||||
context 'when its not queued' do
|
||||
let(:procedure) { create(:procedure, :with_ods_export_file, ods_export_queued: false) }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
|
||||
context 'when its already queued' do
|
||||
let(:procedure) { create(:procedure, :with_ods_export_file, ods_export_queued: true) }
|
||||
it { expect(procedure.ods_export_queued).to be true }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old export' do
|
||||
context 'when its not queued' do
|
||||
let(:procedure) { create(:procedure, :with_stale_ods_export_file, ods_export_queued: false) }
|
||||
it { is_expected.to be true }
|
||||
end
|
||||
|
||||
context 'when its already queued' do
|
||||
let(:procedure) { create(:procedure, :with_stale_ods_export_file, ods_export_queued: true) }
|
||||
it { expect(procedure.ods_export_queued).to be true }
|
||||
it { is_expected.to be false }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -63,6 +63,13 @@ module FeatureHelpers
|
|||
expect(page).to have_content(procedure.service.email)
|
||||
end
|
||||
|
||||
def click_reset_password_link_for(email)
|
||||
reset_password_email = open_email(email)
|
||||
token_params = reset_password_email.body.match(/reset_password_token=[^"]+/)
|
||||
|
||||
visit "/users/password/edit?#{token_params}"
|
||||
end
|
||||
|
||||
def blur
|
||||
page.find('body').click
|
||||
end
|
||||
|
@ -78,13 +85,6 @@ module FeatureHelpers
|
|||
value
|
||||
end
|
||||
end
|
||||
|
||||
def click_reset_password_link_for(email)
|
||||
reset_password_email = open_email(email)
|
||||
token_params = reset_password_email.body.match(/reset_password_token=[^"]+/)
|
||||
|
||||
visit "/users/password/edit?#{token_params}"
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
describe 'instructeurs/avis/instruction.html.haml', type: :view do
|
||||
let(:avis) { create(:avis, confidentiel: confidentiel) }
|
||||
let(:expert) { create(:instructeur) }
|
||||
let(:avis) { create(:avis, confidentiel: confidentiel, email: expert.email) }
|
||||
|
||||
before do
|
||||
assign(:avis, avis)
|
||||
@dossier = create(:dossier, :accepte)
|
||||
assign(:new_avis, Avis.new)
|
||||
assign(:dossier, avis.dossier)
|
||||
allow(view).to receive(:current_instructeur).and_return(avis.instructeur)
|
||||
end
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
|
|||
allow(view).to receive(:current_instructeur).and_return(instructeur)
|
||||
end
|
||||
|
||||
subject { render 'shared/dossiers/champs.html.haml', champs: champs, demande_seen_at: demande_seen_at, profile: nil }
|
||||
subject { render 'shared/dossiers/champs.html.haml', champs: champs, dossier: dossier, demande_seen_at: demande_seen_at, profile: nil }
|
||||
|
||||
context "there are some champs" do
|
||||
let(:dossier) { create(:dossier) }
|
||||
|
@ -54,6 +54,21 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
|
|||
end
|
||||
end
|
||||
|
||||
context "with a routed procedure" do
|
||||
let(:procedure) do
|
||||
create(:procedure,
|
||||
:routee,
|
||||
routing_criteria_name: 'departement')
|
||||
end
|
||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
||||
let(:champs) { [] }
|
||||
|
||||
it "renders the routing criteria name and its value" do
|
||||
expect(subject).to include(procedure.routing_criteria_name)
|
||||
expect(subject).to include(dossier.groupe_instructeur.label)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a dossier champ, but we are not authorized to acces the dossier" do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:champ) { create(:champ, :dossier_link, value: dossier.id) }
|
||||
|
@ -65,6 +80,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
|
|||
end
|
||||
|
||||
context "with a dossier_link champ but without value" do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:champ) { create(:champ, :dossier_link, value: nil) }
|
||||
let(:champs) { [champ] }
|
||||
|
||||
|
|
Loading…
Reference in a new issue