Merge pull request #4423 from betagouv/dev

2019-10-22-01
This commit is contained in:
Keirua 2019-10-22 12:17:51 +02:00 committed by GitHub
commit b4720d9a76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 600 additions and 105 deletions

View file

@ -205,6 +205,23 @@ module Instructeurs
end end
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 def email_notifications
@procedure = procedure @procedure = procedure
@assign_to = assign_to @assign_to = assign_to

View file

@ -282,13 +282,14 @@ module Users
end end
# FIXME: require(:dossier) when all the champs are united # FIXME: require(:dossier) when all the champs are united
def champs_params def champs_and_groupe_instructeurs_params
params.permit(dossier: { params.permit(dossier: [
:groupe_instructeur_id,
champs_attributes: [ champs_attributes: [
:id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [], :id, :value, :primary_value, :secondary_value, :piece_justificative_file, value: [],
champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, value: []] champs_attributes: [:id, :_destroy, :value, :primary_value, :secondary_value, :piece_justificative_file, value: []]
] ]
}) ])
end end
def dossier def dossier
@ -302,7 +303,7 @@ module Users
def update_dossier_and_compute_errors def update_dossier_and_compute_errors
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 errors += @dossier.errors.full_messages
end end

View 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

View file

@ -15,8 +15,8 @@ class Sendinblue::Api
client_key.present? client_key.present?
end end
def identify(email, attributes = {}) def update_contact(email, attributes = {})
req = api_request('identify', email: email, attributes: attributes) req = post_api_request('contacts', email: email, attributes: attributes, updateEnabled: true)
req.on_complete do |response| req.on_complete do |response|
if !response.success? if !response.success?
push_failure("Error while updating identity for administrateur '#{email}' in Sendinblue: #{response.response_code} '#{response.body}'") push_failure("Error while updating identity for administrateur '#{email}' in Sendinblue: #{response.response_code} '#{response.body}'")
@ -34,7 +34,7 @@ class Sendinblue::Api
private private
def hydra def hydra
@hydra ||= Typhoeus::Hydra.new @hydra ||= Typhoeus::Hydra.new(max_concurrency: 50)
end end
def push_failure(failure) def push_failure(failure)
@ -49,8 +49,8 @@ class Sendinblue::Api
end end
end end
def api_request(path, body) def post_api_request(path, body)
url = "#{SENDINBLUE_API_URL}/#{path}" url = "#{SENDINBLUE_API_V3_URL}/#{path}"
Typhoeus::Request.new( Typhoeus::Request.new(
url, url,
@ -62,12 +62,12 @@ class Sendinblue::Api
def headers def headers
{ {
'ma-key': client_key, 'api-key': client_key,
'Content-Type': 'application/json; charset=UTF-8' 'Content-Type': 'application/json; charset=UTF-8'
} }
end end
def client_key def client_key
Rails.application.secrets.sendinblue[:client_key] Rails.application.secrets.sendinblue[:api_v3_key]
end end
end end

View file

@ -42,4 +42,12 @@ class InstructeurMailer < ApplicationMailer
mail(to: instructeur.email, subject: subject) mail(to: instructeur.email, subject: subject)
end 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 end

View file

@ -197,7 +197,7 @@ class Dossier < ApplicationRecord
before_validation :update_state_dates, if: -> { state_changed? } 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 :build_default_individual, if: Proc.new { procedure.for_individual? }
before_save :update_search_terms before_save :update_search_terms

View file

@ -6,6 +6,7 @@ class Procedure < ApplicationRecord
include ProcedureStatsConcern include ProcedureStatsConcern
MAX_DUREE_CONSERVATION = 36 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, -> { 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 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 :refused_mail, class_name: "Mails::RefusedMail", dependent: :destroy
has_one :without_continuation_mail, class_name: "Mails::WithoutContinuationMail", 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 :logo
has_one_attached :notice has_one_attached :notice
has_one_attached :deliberation 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, 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 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
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! def reset!
if locked? if locked?
raise "Can not reset a locked procedure." raise "Can not reset a locked procedure."
else else
groupe_instructeurs.each { |gi| gi.dossiers.destroy_all } groupe_instructeurs.each { |gi| gi.dossiers.destroy_all }
purge_export_files
end end
end end
@ -174,6 +256,14 @@ class Procedure < ApplicationRecord
procedure.blank? || administrateur.owns?(procedure) procedure.blank? || administrateur.owns?(procedure)
end 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? def locked?
publiee_ou_archivee? publiee_ou_archivee?
end end
@ -513,12 +603,14 @@ class Procedure < ApplicationRecord
def after_archive def after_archive
update!(archived_at: Time.zone.now) update!(archived_at: Time.zone.now)
purge_export_files
end end
def after_hide def after_hide
now = Time.zone.now now = Time.zone.now
update!(hidden_at: now) update!(hidden_at: now)
dossiers.update_all(hidden_at: now) dossiers.update_all(hidden_at: now)
purge_export_files
end end
def after_draft def after_draft

View file

@ -10,7 +10,7 @@ class AdministrateurUsageStatisticsService
def update_administrateurs def update_administrateurs
Administrateur.includes(:user).find_each do |administrateur| Administrateur.includes(:user).find_each do |administrateur|
stats = administrateur_stats(administrateur) stats = administrateur_stats(administrateur)
api.identify(administrateur.email, stats) api.update_contact(administrateur.email, stats)
end end
api.run api.run
end end

View file

@ -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"

View file

@ -14,7 +14,7 @@
%h2 Formulaire %h2 Formulaire
- champs = @dossier.champs - 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' } = render partial: "shared/dossiers/champs", locals: { champs: champs, dossier: @dossier, demande_seen_at: nil, profile: 'instructeur' }
%h2 Annotations privées %h2 Annotations privées

View file

@ -4,14 +4,34 @@
Télécharger tous les dossiers Télécharger tous les dossiers
- old_format_limit_date = Date.parse("Oct 31 2019") - old_format_limit_date = Date.parse("Oct 31 2019")
- export_v1_enabled = old_format_limit_date > Time.zone.today - 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' : '' } .dropdown-content.fade-in-down{ style: !export_v1_enabled ? 'width: 330px' : '' }
%ul.dropdown-items %ul.dropdown-items
- if export_v2_enabled
%li %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 %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 %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 - if export_v1_enabled
- old_format_message = "(ancien format, jusquau #{old_format_limit_date.strftime('%d/%m/%Y')})" - old_format_message = "(ancien format, jusquau #{old_format_limit_date.strftime('%d/%m/%Y')})"
%li %li

View file

@ -0,0 +1,2 @@
<%= render_to_element('.procedure-actions', partial: "download_dossiers", locals: { procedure: @procedure }) %>
<%= render_flash %>

View file

@ -1,3 +1,7 @@
%table.table.vertical.dossier-champs %table.table.vertical.dossier-champs
%tbody %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 } = render partial: "shared/dossiers/champ_row", locals: { champs: champs, demande_seen_at: demande_seen_at, profile: profile, repetition: false }

View file

@ -23,6 +23,6 @@
.tab-title Formulaire .tab-title Formulaire
- champs = dossier.champs.includes(:type_de_champ) - champs = dossier.champs.includes(:type_de_champ)
- if champs.any? - if champs.any? || dossier.procedure.routee?
.card .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 }

View file

@ -26,6 +26,13 @@
%hr %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| = f.fields_for :champs, dossier.champs do |champ_form|
- champ = champ_form.object - champ = champ_form.object
= render partial: "shared/dossiers/editable_champs/editable_champ", = render partial: "shared/dossiers/editable_champs/editable_champ",

View file

@ -3,6 +3,11 @@ default: &default
encoding: unicode encoding: unicode
pool: <%= ENV.fetch("DB_POOL") { 5 } %> pool: <%= ENV.fetch("DB_POOL") { 5 } %>
timeout: 5000 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: development:
<<: *default <<: *default

View file

@ -34,6 +34,7 @@ features = [
:mini_profiler, :mini_profiler,
:operation_log_serialize_subject, :operation_log_serialize_subject,
:pre_maintenance_mode, :pre_maintenance_mode,
:procedure_export_v2_enabled,
:xray :xray
] ]

View file

@ -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") 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") 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_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/") UNIVERSIGN_API_URL = ENV.fetch("UNIVERSIGN_API_URL", "https://ws.universign.eu/tsa/post/")
# Internal URLs # Internal URLs

View file

@ -293,6 +293,7 @@ Rails.application.routes.draw do
post 'add_filter' post 'add_filter'
get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter' get 'remove_filter' => 'procedures#remove_filter', as: 'remove_filter'
get 'download_dossiers' get 'download_dossiers'
get 'download_export'
get 'stats' get 'stats'
get 'email_notifications' get 'email_notifications'
patch 'update_email_notifications' patch 'update_email_notifications'

View file

@ -55,6 +55,7 @@ defaults: &defaults
sendinblue: sendinblue:
enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %> enabled: <%= ENV['SENDINBLUE_ENABLED'] == 'enabled' %>
client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %> client_key: <%= ENV['SENDINBLUE_CLIENT_KEY'] %>
api_v3_key: <%= ENV['SENDINBLUE_API_V3_KEY'] %>
matomo: matomo:
enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %> enabled: <%= ENV['MATOMO_ENABLED'] == 'enabled' %>
client_key: <%= ENV['MATOMO_ID'] %> client_key: <%= ENV['MATOMO_ID'] %>

View file

@ -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

View file

@ -487,6 +487,9 @@ ActiveRecord::Schema.define(version: 2019_10_14_160538) do
t.string "declarative_with_state" t.string "declarative_with_state"
t.text "monavis_embed" t.text "monavis_embed"
t.text "routing_criteria_name" 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 ["declarative_with_state"], name: "index_procedures_on_declarative_with_state"
t.index ["hidden_at"], name: "index_procedures_on_hidden_at" t.index ["hidden_at"], name: "index_procedures_on_hidden_at"
t.index ["parent_procedure_id"], name: "index_procedures_on_parent_procedure_id" t.index ["parent_procedure_id"], name: "index_procedures_on_parent_procedure_id"

View file

@ -1,27 +1,16 @@
FactoryBot.define do FactoryBot.define do
sequence(:expert_email) { |n| "expert#{n}@expert.com" }
factory :avis do factory :avis do
email { generate(:expert_email) }
introduction { 'Bonjour, merci de me donner votre avis sur ce dossier' } introduction { 'Bonjour, merci de me donner votre avis sur ce dossier' }
confidentiel { false }
before(:create) do |avis, _evaluator| association :dossier
if !avis.instructeur association :claimant, factory: :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
trait :with_answer do 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 end
end end

View file

@ -74,6 +74,12 @@ FactoryBot.define do
end end
end end
trait :routee do
after(:create) do |procedure, _evaluator|
procedure.groupe_instructeurs.create(label: '2nd groupe')
end
end
trait :for_individual do trait :for_individual do
after(:build) do |procedure, _evaluator| after(:build) do |procedure, _evaluator|
procedure.for_individual = true procedure.for_individual = true
@ -236,5 +242,47 @@ FactoryBot.define do
end end
end 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
end end

View 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 dexpert' }
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 dont 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 dexpert : cest 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 dexpert : cest 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

View file

@ -1,6 +1,6 @@
require 'spec_helper' require 'spec_helper'
feature 'The instructeur part' do feature 'Instructing a dossier:' do
include ActiveJob::TestHelper include ActiveJob::TestHelper
let(:password) { 'démarches-simplifiées-pwd' } let(:password) { 'démarches-simplifiées-pwd' }
@ -93,59 +93,6 @@ feature 'The instructeur part' do
expect(page).to have_text('Aucun dossier') expect(page).to have_text('Aucun dossier')
end 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 scenario 'A instructeur can see the personnes impliquées' do
instructeur2 = FactoryBot.create(:instructeur, password: password) instructeur2 = FactoryBot.create(:instructeur, password: password)

View file

@ -46,4 +46,18 @@ RSpec.describe InstructeurMailer, type: :mailer do
end end
end 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 end

View file

@ -37,6 +37,10 @@ class InstructeurMailerPreview < ActionMailer::Preview
InstructeurMailer.send_notifications(instructeur, data) InstructeurMailer.send_notifications(instructeur, data)
end end
def notify_procedure_export_available
InstructeurMailer.notify_procedure_export_available(instructeur, procedure, 'xlsx')
end
private private
def instructeur def instructeur

View file

@ -954,4 +954,165 @@ describe Procedure do
it { is_expected.to be false } it { is_expected.to be false }
end end
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 end

View file

@ -63,6 +63,13 @@ module FeatureHelpers
expect(page).to have_content(procedure.service.email) expect(page).to have_content(procedure.service.email)
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
def blur def blur
page.find('body').click page.find('body').click
end end
@ -78,13 +85,6 @@ module FeatureHelpers
value value
end end
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 end
RSpec.configure do |config| RSpec.configure do |config|

View file

@ -1,9 +1,11 @@
describe 'instructeurs/avis/instruction.html.haml', type: :view do 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 before do
assign(:avis, avis) 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) allow(view).to receive(:current_instructeur).and_return(avis.instructeur)
end end

View file

@ -8,7 +8,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
allow(view).to receive(:current_instructeur).and_return(instructeur) allow(view).to receive(:current_instructeur).and_return(instructeur)
end 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 context "there are some champs" do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
@ -54,6 +54,21 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
end end
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 context "with a dossier champ, but we are not authorized to acces the dossier" do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:champ) { create(:champ, :dossier_link, value: dossier.id) } let(:champ) { create(:champ, :dossier_link, value: dossier.id) }
@ -65,6 +80,7 @@ describe 'shared/dossiers/champs.html.haml', type: :view do
end end
context "with a dossier_link champ but without value" do context "with a dossier_link champ but without value" do
let(:dossier) { create(:dossier) }
let(:champ) { create(:champ, :dossier_link, value: nil) } let(:champ) { create(:champ, :dossier_link, value: nil) }
let(:champs) { [champ] } let(:champs) { [champ] }