Merge pull request #4438 from betagouv/dev

2019-10-24-01
This commit is contained in:
Paul Chavard 2019-10-24 13:59:06 +02:00 committed by GitHub
commit 6dadc2422f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 812 additions and 137 deletions

View file

@ -4,7 +4,7 @@ gem 'aasm'
gem 'actiontext', git: 'https://github.com/kobaltz/actiontext.git', branch: 'archive', require: 'action_text' # Port of ActionText to Rails 5
gem 'active_link_to' # Automatically set a class on active links
gem 'active_model_serializers'
gem 'activestorage-openstack', git: 'https://github.com/fredZen/activestorage-openstack.git', branch: 'frederic/fix_upload_signature'
gem 'activestorage-openstack'
gem 'administrate'
gem 'after_party'
gem 'anchored'

View file

@ -1,14 +1,3 @@
GIT
remote: https://github.com/fredZen/activestorage-openstack.git
revision: c71d5107a51701eab9d9267dd0000e6c1cf3e39a
branch: frederic/fix_upload_signature
specs:
activestorage-openstack (0.5.0)
fog-openstack (~> 1.0)
marcel
mime-types
rails (~> 5.2.0)
GIT
remote: https://github.com/kobaltz/actiontext.git
revision: ef59c4ba99d1b7614dd47f5a294eef553224db88
@ -75,13 +64,18 @@ GEM
actionpack (= 5.2.2.1)
activerecord (= 5.2.2.1)
marcel (~> 0.3.1)
activestorage-openstack (1.0.0)
fog-openstack (~> 1.0)
marcel
mime-types
rails (<= 6)
activesupport (5.2.2.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
administrate (0.11.0)
actionpack (>= 4.2, < 6.0)
actionview (>= 4.2, < 6.0)
@ -125,18 +119,18 @@ GEM
browser (2.5.3)
builder (3.2.3)
byebug (10.0.2)
capybara (3.12.0)
capybara (3.29.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.2)
regexp_parser (~> 1.5)
xpath (~> 3.2)
capybara-email (3.0.1)
capybara (>= 2.4, < 4.0)
mail
capybara-screenshot (1.0.22)
capybara-screenshot (1.0.23)
capybara (>= 1.0, < 4)
launchy
capybara-selenium (0.0.6)
@ -379,7 +373,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2018.0812)
mimemagic (0.3.3)
mini_mime (1.0.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.11.3)
momentjs-rails (2.20.1)
@ -454,7 +448,7 @@ GEM
pry-byebug (3.6.0)
byebug (~> 10.0)
pry (~> 0.10)
public_suffix (3.0.3)
public_suffix (4.0.1)
puma (3.12.0)
pundit (2.0.1)
activesupport (>= 3.0.0)
@ -518,7 +512,7 @@ GEM
execjs
railties (>= 3.2)
tilt
regexp_parser (1.3.0)
regexp_parser (1.6.0)
request_store (1.4.1)
rack (>= 1.4)
responders (3.0.0)
@ -717,7 +711,7 @@ DEPENDENCIES
actiontext!
active_link_to
active_model_serializers
activestorage-openstack!
activestorage-openstack
administrate
after_party
anchored

View file

@ -0,0 +1,6 @@
.groupe-instructeur {
.actions {
width: 200px;
text-align: center;
}
}

View file

@ -1,4 +1,5 @@
@import "colors";
@import "constants";
.pull-left {
float: left;
@ -48,3 +49,7 @@
background: $orange-bg;
color: $black;
}
.mt-2 {
margin-top: 2 * $default-spacer;
}

View file

@ -10,10 +10,16 @@ module CreateAvisConcern
# the :emails parameter is a 1-element array.
# Hence the call to first
# https://github.com/rails/rails/issues/17225
emails = create_avis_params[:emails].first.split(',').map(&:strip)
expert_emails = create_avis_params[:emails].first.split(',').map(&:strip)
allowed_dossiers = [dossier]
if create_avis_params[:invite_linked_dossiers].present?
allowed_dossiers += dossier.linked_dossiers
end
create_results = Avis.create(
emails.map do |email|
expert_emails.flat_map do |email|
allowed_dossiers.map do |dossier|
{
email: email,
introduction: create_avis_params[:introduction],
@ -22,18 +28,25 @@ module CreateAvisConcern
confidentiel: confidentiel
}
end
end
)
persisted, failed = create_results.partition(&:persisted?)
if persisted.any?
sent_emails_addresses = persisted.map(&:email_to_display).join(", ")
flash.notice = "Une demande d'avis a été envoyée à #{sent_emails_addresses}"
sent_emails_addresses = []
persisted.each do |avis|
dossier.demander_un_avis!(avis)
avis.dossier.demander_un_avis!(avis)
if avis.dossier == dossier
AvisMailer.avis_invitation(avis).deliver_later
sent_emails_addresses << avis.email_to_display
end
end
flash.notice = "Une demande d'avis a été envoyée à #{sent_emails_addresses.uniq.join(", ")}"
end
if failed.any?
flash.now.alert = failed
.filter { |avis| avis.errors.present? }
@ -41,13 +54,13 @@ module CreateAvisConcern
# When an error occurs, return the avis back to the controller
# to give the user a chance to correct and resubmit
Avis.new(create_avis_params.merge(emails: [failed.map(&:email).join(", ")]))
Avis.new(create_avis_params.merge(emails: [failed.map(&:email).uniq.join(", ")]))
else
nil
end
end
def create_avis_params
params.require(:avis).permit(:introduction, :confidentiel, emails: [])
params.require(:avis).permit(:introduction, :confidentiel, :invite_linked_dossiers, emails: [])
end
end

View file

@ -264,7 +264,7 @@ module Instructeurs
end
def ensure_ownership!
if !procedure.defaut_groupe_instructeur.instructeurs.include?(current_instructeur)
if !current_instructeur.procedures.include?(procedure)
flash[:alert] = "Vous n'avez pas accès à cette démarche"
redirect_to root_path
end

View file

@ -0,0 +1,145 @@
module NewAdministrateur
class GroupeInstructeursController < AdministrateurController
ITEMS_PER_PAGE = 25
def index
@procedure = procedure
@groupes_instructeurs = paginated_groupe_instructeurs
end
def show
@procedure = procedure
@groupe_instructeur = groupe_instructeur
@instructeurs = paginated_instructeurs
end
def create
@groupe_instructeur = procedure
.groupe_instructeurs
.new(label: label, instructeurs: [current_administrateur.instructeur])
if @groupe_instructeur.save
redirect_to procedure_groupe_instructeur_path(procedure, @groupe_instructeur),
notice: "Le groupe dinstructeurs « #{label} » a été créé."
else
@procedure = procedure
@groupes_instructeurs = paginated_groupe_instructeurs
flash[:alert] = "le nom « #{label} » est déjà pris par un autre groupe."
render :index
end
end
def update
@groupe_instructeur = groupe_instructeur
if @groupe_instructeur.update(label: label)
redirect_to procedure_groupe_instructeur_path(procedure, groupe_instructeur),
notice: "Le nom est à présent « #{label} »."
else
@procedure = procedure
@instructeurs = paginated_instructeurs
flash[:alert] = "le nom « #{label} » est déjà pris par un autre groupe."
render :show
end
end
def add_instructeur
@instructeur = Instructeur.find_by(email: instructeur_email) ||
create_instructeur(instructeur_email)
if groupe_instructeur.instructeurs.include?(@instructeur)
flash[:alert] = "Linstructeur « #{instructeur_email} » est déjà dans le groupe."
else
groupe_instructeur.instructeurs << @instructeur
flash[:notice] = "Linstructeur « #{instructeur_email} » a été affecté au groupe."
GroupeInstructeurMailer
.add_instructeur(groupe_instructeur, @instructeur, current_user.email)
.deliver_later
end
redirect_to procedure_groupe_instructeur_path(procedure, groupe_instructeur)
end
def remove_instructeur
if groupe_instructeur.instructeurs.one?
flash[:alert] = "Suppression impossible : il doit y avoir au moins un instructeur dans le groupe"
else
@instructeur = Instructeur.find(instructeur_id)
groupe_instructeur.instructeurs.destroy(@instructeur)
flash[:notice] = "Linstructeur « #{@instructeur.email} » a été retiré du groupe."
GroupeInstructeurMailer
.remove_instructeur(groupe_instructeur, @instructeur, current_user.email)
.deliver_later
end
redirect_to procedure_groupe_instructeur_path(procedure, groupe_instructeur)
end
def update_routing_criteria_name
procedure.update!(routing_criteria_name: routing_criteria_name)
redirect_to procedure_groupe_instructeurs_path(procedure),
notice: "Le libellé est maintenant « #{procedure.routing_criteria_name} »."
end
private
def create_instructeur(email)
user = User.create_or_promote_to_instructeur(
email,
SecureRandom.hex,
administrateurs: [current_administrateur]
)
user.invite!
user.instructeur
end
def procedure
current_administrateur
.procedures
.includes(:groupe_instructeurs)
.find(params[:procedure_id])
end
def groupe_instructeur
procedure.groupe_instructeurs.find(params[:id])
end
def instructeur_email
params[:instructeur][:email].strip.downcase
end
def instructeur_id
params[:instructeur][:id]
end
def label
params[:groupe_instructeur][:label]
end
def paginated_groupe_instructeurs
procedure
.groupe_instructeurs
.page(params[:page])
.per(ITEMS_PER_PAGE)
.order(:label)
end
def paginated_instructeurs
groupe_instructeur
.instructeurs
.page(params[:page])
.per(ITEMS_PER_PAGE)
.order(:email)
end
def routing_criteria_name
params[:procedure][:routing_criteria_name]
end
end
end

View file

@ -538,7 +538,7 @@ enum TypeDeChamp {
multiple_drop_down_list
"""
Nombre entier
Nombre
"""
number

View file

@ -1,7 +1,7 @@
module DossierLinkHelper
def dossier_linked_path(user, dossier)
if user.is_a?(Instructeur)
if dossier.procedure.defaut_groupe_instructeur.instructeurs.include?(user)
if user.groupe_instructeurs.include?(dossier.groupe_instructeur)
instructeur_dossier_path(dossier.procedure, dossier)
else
avis = dossier.avis.find_by(instructeur: user)

View file

@ -1,57 +0,0 @@
module ActiveStorage
# Wraps an ActiveStorage::Service to route direct upload and direct download URLs through our proxy,
# thus avoiding exposing the storage providers URL to our end-users.
class Service::DsProxyService < SimpleDelegator
attr_reader :wrapped
def self.build(wrapped:, configurator:, **options)
new(wrapped: configurator.build(wrapped))
end
def initialize(wrapped:)
@wrapped = wrapped
super(wrapped)
end
def url(*args)
url = wrapped.url(*args)
publicize(url)
end
def url_for_direct_upload(*args)
url = wrapped.url_for_direct_upload(*args)
publicize(url)
end
private
def object_for(key, &block)
blob_url = url(key)
if block_given?
request = Typhoeus::Request.new(blob_url)
request.on_headers do |response|
if response.code != 200
raise Fog::OpenStack::Storage::NotFound.new
end
end
request.on_body do |chunk|
yield chunk
end
request.run
else
response = Typhoeus.get(blob_url)
if response.success?
response
else
raise Fog::OpenStack::Storage::NotFound.new
end
end
end
def publicize(url)
search = %r{^https://[^/]+/v1/AUTH_[a-f0-9]{32}}
replace = 'https://static.demarches-simplifiees.fr'
url.gsub(search, replace)
end
end
end

View file

@ -0,0 +1,25 @@
class GroupeInstructeurMailer < ApplicationMailer
layout 'mailers/layout'
def add_instructeur(group, instructeur, current_instructeur_email)
@email = instructeur.email
@group = group
@current_instructeur_email = current_instructeur_email
subject = "Ajout dun instructeur dans le groupe \"#{group.label}\""
emails = @group.instructeurs.pluck(:email)
mail(bcc: emails, subject: subject)
end
def remove_instructeur(group, instructeur, current_instructeur_email)
@email = instructeur.email
@group = group
@current_instructeur_email = current_instructeur_email
subject = "Suppression dun instructeur dans le groupe \"#{group.label}\""
emails = @group.instructeurs.pluck(:email)
mail(bcc: emails, subject: subject)
end
end

View file

@ -12,7 +12,6 @@ class Avis < ApplicationRecord
before_validation -> { sanitize_email(:email) }
before_create :try_to_assign_instructeur
after_create :notify_instructeur
default_scope { joins(:dossier) }
scope :with_answer, -> { where.not(answer: nil) }
@ -24,6 +23,7 @@ class Avis < ApplicationRecord
# The form allows subtmitting avis requests to several emails at once,
# hence this virtual attribute.
attr_accessor :emails
attr_accessor :invite_linked_dossiers
def email_to_display
instructeur&.email || email
@ -49,10 +49,6 @@ class Avis < ApplicationRecord
private
def notify_instructeur
AvisMailer.avis_invitation(self).deliver_later
end
def try_to_assign_instructeur
instructeur = Instructeur.find_by(email: email)
if instructeur

View file

@ -11,7 +11,7 @@ class Champ < ApplicationRecord
belongs_to :etablissement, dependent: :destroy
has_many :champs, -> { ordered }, foreign_key: :parent_id, inverse_of: :parent, dependent: :destroy
delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, :repetition?, to: :type_de_champ
delegate :libelle, :type_champ, :order_place, :mandatory?, :description, :drop_down_list, :exclude_from_export?, :exclude_from_view?, :repetition?, :dossier_link?, to: :type_de_champ
scope :updated_since?, -> (date) { where('champs.updated_at > ?', date) }
scope :public_only, -> { where(private: false) }

View file

@ -517,6 +517,10 @@ class Dossier < ApplicationRecord
self.individual = Individual.create_from_france_connect(fc_information)
end
def linked_dossiers
Dossier.where(id: champs.filter(&:dossier_link?).map(&:value).compact)
end
private
def log_dossier_operation(author, operation, subject = nil)

View file

@ -4,4 +4,9 @@ class GroupeInstructeur < ApplicationRecord
has_many :assign_tos
has_many :instructeurs, through: :assign_tos, dependent: :destroy
has_many :dossiers
validates :label, presence: { message: 'doit être renseigné' }, allow_nil: false
validates :label, uniqueness: { scope: :procedure, message: 'existe déjà' }
before_validation -> { label&.strip! }
end

View file

@ -157,6 +157,10 @@ class TypeDeChamp < ApplicationRecord
type_champ == TypeDeChamp.type_champs.fetch(:repetition)
end
def dossier_link?
type_champ == TypeDeChamp.type_champs.fetch(:dossier_link)
end
def public?
!private?
end

View file

@ -0,0 +1,11 @@
%p
Bonjour,
%p
Linstructeur « #{@email} » a été affecté au groupe « #{@group.label} » par « #{@current_instructeur_email} », en charge de la démarche « #{@group.procedure.libelle} ».
%p
Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
= link_to(@group.label, procedure_groupe_instructeur_url(@group.procedure, @group))
= render partial: "layouts/mailers/signature"

View file

@ -0,0 +1,11 @@
%p
Bonjour,
%p
Linstructeur « #{@email} » a été retiré du groupe « #{@group.label} » par « #{@current_instructeur_email} », en charge de la démarche « #{@group.procedure.libelle} ».
%p
Cliquez sur le lien ci-dessous pour voir la liste des instructeurs de ce groupe :
= link_to(@group.label, procedure_groupe_instructeur_url(@group.procedure, @group))
= render partial: "layouts/mailers/signature"

View file

@ -28,7 +28,7 @@
= f.submit 'Envoyer votre avis', class: 'button send'
- if !@dossier.termine?
= render partial: "instructeurs/shared/avis/form", locals: { url: avis_instructeur_avis_path(@avis), must_be_confidentiel: @avis.confidentiel?, avis: @new_avis }
= render partial: "instructeurs/shared/avis/form", locals: { url: avis_instructeur_avis_path(@avis), linked_dossiers: @dossier.linked_dossiers, must_be_confidentiel: @avis.confidentiel?, avis: @new_avis }
- if @dossier.avis_for(current_instructeur).present?
= render partial: 'instructeurs/shared/avis/list', locals: { avis: @dossier.avis_for(current_instructeur), avis_seen_at: nil }

View file

@ -4,7 +4,7 @@
.container
- if !@dossier.termine?
= render partial: "instructeurs/shared/avis/form", locals: { url: avis_instructeur_dossier_path(@dossier.procedure, @dossier), must_be_confidentiel: false, avis: @avis }
= render partial: "instructeurs/shared/avis/form", locals: { url: avis_instructeur_dossier_path(@dossier.procedure, @dossier), linked_dossiers: @dossier.linked_dossiers, must_be_confidentiel: false, avis: @avis }
- if @dossier.avis.present?
= render partial: 'instructeurs/shared/avis/list', locals: { avis: @dossier.avis, avis_seen_at: @avis_seen_at }

View file

@ -3,7 +3,7 @@
.container
.page-title
Résultat de la recherche :
= pluralize(@dossiers.count, "dossier trouvé", "dossiers trouvés")
= t('pluralize.dossier_trouve', count: @dossiers.count)
- if @dossiers.present?
%table.table.dossiers-table.hoverable

View file

@ -6,6 +6,10 @@
= f.email_field :emails, placeholder: 'Adresses email, séparées par des virgules', required: true, multiple: true, onchange: "javascript:DS.replaceSemicolonByComma(event);"
= f.text_area :introduction, rows: 3, value: avis.introduction || 'Bonjour, merci de me donner votre avis sur ce dossier.', required: true
- if linked_dossiers.present?
= f.check_box :invite_linked_dossiers, value: false
= f.label :invite_linked_dossiers, t('helpers.label.invite_linked_dossiers', count: linked_dossiers.length, ids: linked_dossiers.map(&:id).to_sentence)
.flex.justify-between.align-baseline
- if must_be_confidentiel
%p.confidentiel.flex

View file

@ -27,15 +27,21 @@
%p.missing-steps (à compléter)
%a#onglet-administrateurs{ href: url_for(procedure_administrateurs_path(@procedure)) }
.procedure-list-element{ class: ('active' if active == 'Administrateurs') }
.procedure-list-element
Administrateurs
- if !feature_enabled?(:routage)
%a#onglet-instructeurs{ href: url_for(admin_procedure_assigns_path(@procedure)) }
.procedure-list-element{ class: ('active' if active == 'Instructeurs') }
Instructeurs
- if @procedure.missing_steps.include?(:instructeurs)
%p.missing-steps (à compléter)
- if feature_enabled?(:routage)
%a#onglet-instructeurs{ href: url_for(procedure_groupe_instructeurs_path(@procedure)) }
.procedure-list-element
Groupe d'instructeurs
- if !@procedure.locked?
%a#onglet-champs{ href: champs_procedure_path(@procedure) }
.procedure-list-element{ class: ('active' if active == 'Champs') }

View file

@ -0,0 +1,38 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
'Groupes dinstructeurs'] }
.container.groupe-instructeur
.card
= form_for @procedure,
url: { action: :update_routing_criteria_name },
html: { class: 'form' } do |f|
= f.label :routing_criteria_name do
Libellé du routage
%span.notice Ce texte apparaitra sur le formulaire usager comme le libellé d'une liste
= f.text_field :routing_criteria_name, placeholder: 'Votre ville', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des Groupes
= form_for :groupe_instructeur, html: { class: 'form' } do |f|
= f.label :label do
Ajouter un groupe
%span.notice Ce groupe sera un choix de la liste « #{@procedure.routing_criteria_name} » .
= f.text_field :label, placeholder: 'Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 } Liste des groupes
%tbody
- @groupes_instructeurs.each do |group|
%tr
%td= group.label
%td.actions= link_to "voir", procedure_groupe_instructeur_path(@procedure, group)
= paginate @groupes_instructeurs

View file

@ -0,0 +1,47 @@
= render partial: 'new_administrateur/breadcrumbs',
locals: { steps: [link_to('Démarches', admin_procedures_path),
link_to(@procedure.libelle, admin_procedure_path(@procedure)),
link_to('Groupes dinstructeurs', procedure_groupe_instructeurs_path(@procedure)),
@groupe_instructeur.label] }
.container.groupe-instructeur
.rename_form_block
.flex.baseline-start
%h1 Groupe « #{@groupe_instructeur.label} »
.card.mt-2
= form_for @groupe_instructeur,
url: procedure_groupe_instructeur_path(@procedure, @groupe_instructeur),
html: { class: 'form' } do |f|
= f.label :label, 'Nom du groupe'
= f.text_field :label, placeholder: 'Ville de Bordeaux', required: true
= f.submit 'Renommer', class: 'button primary send'
.card
.card-title Gestion des instructeurs
= form_for :instructeur,
url: { action: :add_instructeur },
html: { class: 'form' } do |f|
= f.label :email do
Affecter un nouvel instructeur
= f.email_field :email, placeholder: 'marie.dupont@exemple.fr', required: true
= f.submit 'Affecter', class: 'button primary send'
%table.table.mt-2
%thead
%tr
%th{ colspan: 2 } Instructeurs affectés
%tbody
- @instructeurs.each do |instructeur|
%tr
%td= instructeur.email
%td.actions= button_to 'retirer',
{ action: :remove_instructeur },
{ method: :delete,
data: { confirm: "Êtes-vous sûr de vouloir retirer linstructeur « #{instructeur.email} » du groupe  « #{@groupe_instructeur.label} » ?" },
params: { instructeur: { id: instructeur.id }},
class: 'button' }
= paginate @instructeurs

View file

@ -93,7 +93,7 @@ Rails.application.configure do
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
config.active_storage.service = :proxied
config.active_storage.service = :openstack
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify

View file

@ -7,3 +7,33 @@ ActiveStorage::Service.url_expires_in = 1.hour
# cleaner (as it allows to enqueue the virus scan on attachment creation, rather
# than on blob creation).
ActiveSupport.on_load(:active_storage_blob) { include BlobVirusScanner }
# When an OpenStack service is initialized it makes a request to fetch
# `publicURL` to use for all operations. We intercept the method that reads
# this url and replace the host with DS_Proxy host. This way all the operation
# are performed through DS_Proxy.
#
# https://github.com/fog/fog-openstack/blob/37621bb1d5ca78d037b3c56bd307f93bba022ae1/lib/fog/openstack/auth/catalog/v2.rb#L16
require 'fog/openstack/auth/catalog/v2'
module Fog::OpenStack::Auth::Catalog
class V2
def endpoint_url(endpoint, interface)
url = endpoint["#{interface}URL"]
if interface == 'public'
publicize(url)
else
url
end
end
private
def publicize(url)
search = %r{^https://[^/]+/}
replace = 'https://static.demarches-simplifiees.fr/'
url.gsub(search, replace)
end
end
end

View file

@ -313,3 +313,7 @@ fr:
zero: archivé
one: archivé
other: archivés
dossier_trouve:
zero: 0 dossier trouvé
one: 1 dossier trouvé
other: "%{count} dossiers trouvés"

View file

@ -5,3 +5,8 @@ fr:
attributes:
avis:
answer: "Réponse"
helpers:
label:
invite_linked_dossiers:
one: Inviter aussi l'expert sur le dossier lié n° %{ids}
other: Inviter aussi l'expert sur les dossiers liés n° %{ids}

View file

@ -9,7 +9,7 @@ fr:
textarea: 'Zone de texte'
date: 'Date'
datetime: 'Date et Heure'
number: 'Nombre entier'
number: 'Nombre'
decimal_number: 'Nombre décimal'
integer_number: 'Nombre entier'
checkbox: 'Case à cocher'

View file

@ -350,6 +350,17 @@ Rails.application.routes.draw do
get 'annotations'
end
resources :groupe_instructeurs, only: [:index, :show, :create, :update] do
member do
post 'add_instructeur'
delete 'remove_instructeur'
end
collection do
patch 'update_routing_criteria_name'
end
end
resources :administrateurs, controller: 'procedure_administrateurs', only: [:index, :create, :destroy]
resources :types_de_champ, only: [:create, :update, :destroy] do

View file

@ -4,9 +4,6 @@ local:
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
proxied:
service: DsProxy
wrapped: openstack
openstack:
service: OpenStack
container: "<%= ENV['FOG_ACTIVESTORAGE_DIRECTORY'] %>"

View file

@ -0,0 +1,5 @@
class AddDefaultValueToRoutingCriteriaName < ActiveRecord::Migration[5.2]
def change
change_column :procedures, :routing_criteria_name, :text, default: "Votre ville"
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: 2019_10_14_160538) do
ActiveRecord::Schema.define(version: 2019_10_23_183120) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -486,7 +486,7 @@ ActiveRecord::Schema.define(version: 2019_10_14_160538) do
t.string "path", null: false
t.string "declarative_with_state"
t.text "monavis_embed"
t.text "routing_criteria_name"
t.text "routing_criteria_name", default: "Votre ville"
t.boolean "csv_export_queued"
t.boolean "xlsx_export_queued"
t.boolean "ods_export_queued"

View file

@ -123,9 +123,10 @@ describe Instructeurs::AvisController, type: :controller do
let(:intro) { 'introduction' }
let(:created_avis) { Avis.last }
let!(:old_avis_count) { Avis.count }
let(:invite_linked_dossiers) { nil }
before do
post :create_avis, params: { id: previous_avis.id, avis: { emails: emails, introduction: intro, confidentiel: asked_confidentiel } }
post :create_avis, params: { id: previous_avis.id, avis: { emails: emails, introduction: intro, confidentiel: asked_confidentiel, invite_linked_dossiers: invite_linked_dossiers } }
end
context 'when an invalid email' do
@ -180,6 +181,34 @@ describe Instructeurs::AvisController, type: :controller do
it { expect(created_avis.confidentiel).to be(true) }
end
end
context 'with linked dossiers' do
let(:asked_confidentiel) { false }
let(:previous_avis_confidentiel) { false }
let(:dossier) { create(:dossier, :en_construction, :with_dossier_link, procedure: procedure) }
it do
expect(flash.notice).to eq("Une demande d'avis a été envoyée à a@b.com")
expect(Avis.count).to eq(old_avis_count + 1)
expect(created_avis.email).to eq("a@b.com")
expect(created_avis.dossier).to eq(dossier)
end
context 'checked' do
let(:invite_linked_dossiers) { true }
let(:created_avis) { Avis.last(2).first }
let(:linked_avis) { Avis.last }
let(:linked_dossier) { dossier.reload.linked_dossiers.first }
it do
expect(flash.notice).to eq("Une demande d'avis a été envoyée à a@b.com")
expect(Avis.count).to eq(old_avis_count + 2)
expect(created_avis.email).to eq("a@b.com")
expect(created_avis.dossier).to eq(dossier)
expect(linked_avis.dossier).to eq(linked_dossier)
end
end
end
end
end

View file

@ -0,0 +1,162 @@
describe NewAdministrateur::GroupeInstructeursController, type: :controller do
render_views
let(:admin) { create(:administrateur) }
let(:procedure) { create(:procedure, :published, administrateurs: [admin]) }
let!(:gi_1_1) { procedure.defaut_groupe_instructeur }
let(:procedure2) { create(:procedure, :published) }
let!(:gi_2_2) { procedure2.groupe_instructeurs.create(label: 'groupe instructeur 2 2') }
before { sign_in(admin.user) }
describe '#index' do
context 'of a procedure I own' do
let!(:gi_1_2) { procedure.groupe_instructeurs.create(label: 'groupe instructeur 2') }
before { get :index, params: { procedure_id: procedure.id } }
context 'when a procedure has multiple groups' do
it { expect(response).to have_http_status(:ok) }
it { expect(response.body).to include(gi_1_1.label) }
it { expect(response.body).to include(gi_1_2.label) }
it { expect(response.body).not_to include(gi_2_2.label) }
end
end
end
describe '#show' do
context 'of a group I belong to' do
before { get :show, params: { procedure_id: procedure.id, id: gi_1_1.id } }
it { expect(response).to have_http_status(:ok) }
end
end
describe '#create' do
before do
post :create,
params: {
procedure_id: procedure.id,
groupe_instructeur: { label: label }
}
end
context 'with a valid name' do
let(:label) { "nouveau_groupe" }
it { expect(flash.notice).to be_present }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, procedure.groupe_instructeurs.last)) }
it { expect(procedure.groupe_instructeurs.count).to eq(2) }
end
context 'with an invalid group name' do
let(:label) { gi_1_1.label }
it { expect(response).to render_template(:index) }
it { expect(procedure.groupe_instructeurs.count).to eq(1) }
it { expect(flash.alert).to be_present }
end
end
describe '#update' do
let(:new_name) { 'nouveau nom du groupe' }
before do
patch :update,
params: {
procedure_id: procedure.id,
id: gi_1_1.id,
groupe_instructeur: { label: new_name }
}
end
it { expect(gi_1_1.reload.label).to eq(new_name) }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, gi_1_1)) }
it { expect(flash.notice).to be_present }
context 'when the name is already taken' do
let!(:gi_1_2) { procedure.groupe_instructeurs.create(label: 'groupe instructeur 2') }
let(:new_name) { gi_1_2.label }
it { expect(gi_1_1.reload.label).not_to eq(new_name) }
it { expect(flash.alert).to be_present }
end
end
describe '#add_instructeur' do
let!(:instructeur) { create(:instructeur) }
before do
gi_1_1.instructeurs << instructeur
post :add_instructeur,
params: {
procedure_id: procedure.id,
id: gi_1_1.id,
instructeur: { email: new_instructeur_email }
}
end
context 'of a new instructeur' do
let(:new_instructeur_email) { 'new_instructeur@mail.com' }
it { expect(gi_1_1.instructeurs.pluck(:email)).to include(new_instructeur_email) }
it { expect(flash.notice).to be_present }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, gi_1_1)) }
end
context 'of an instructeur already in the group' do
let(:new_instructeur_email) { instructeur.email }
it { expect(flash.alert).to be_present }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, procedure.defaut_groupe_instructeur)) }
end
end
describe '#remove_instructeur' do
let!(:instructeur) { create(:instructeur) }
before { gi_1_1.instructeurs << admin.instructeur << instructeur }
def remove_instructeur(email)
delete :remove_instructeur,
params: {
procedure_id: procedure.id,
id: gi_1_1.id,
instructeur: { id: admin.instructeur.id }
}
end
context 'when there are many instructeurs' do
before { remove_instructeur(admin.user.email) }
it { expect(gi_1_1.instructeurs).to include(instructeur) }
it { expect(gi_1_1.reload.instructeurs.count).to eq(1) }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, gi_1_1)) }
end
context 'when there is only one instructeur' do
before do
remove_instructeur(admin.user.email)
remove_instructeur(instructeur.email)
end
it { expect(gi_1_1.instructeurs).to include(instructeur) }
it { expect(gi_1_1.instructeurs.count).to eq(1) }
it { expect(flash.alert).to eq('Suppression impossible : il doit y avoir au moins un instructeur dans le groupe') }
it { expect(response).to redirect_to(procedure_groupe_instructeur_path(procedure, gi_1_1)) }
end
end
describe '#update_routing_criteria_name' do
before do
patch :update_routing_criteria_name,
params: {
procedure_id: procedure.id,
procedure: { routing_criteria_name: 'new name !' }
}
end
it { expect(procedure.reload.routing_criteria_name).to eq('new name !') }
end
end

View file

@ -54,10 +54,30 @@ FactoryBot.define do
trait :with_dossier_link do
after(:create) do |dossier, _evaluator|
# create linked dossier
linked_dossier = create(:dossier)
type_de_champ = dossier.procedure.types_de_champ.find { |t| t.type_champ == TypeDeChamp.type_champs.fetch(:dossier_link) }
champ = dossier.champs.find { |c| c.type_de_champ == type_de_champ }
# find first type de champ dossier_link
type_de_champ = dossier.procedure.types_de_champ.find do |t|
t.type_champ == TypeDeChamp.type_champs.fetch(:dossier_link)
end
# if type de champ does not exist create it
if !type_de_champ
type_de_champ = create(:type_de_champ_dossier_link, procedure: dossier.procedure)
end
# find champ with the type de champ
champ = dossier.reload.champs.find do |c|
c.type_de_champ == type_de_champ
end
# if champ does not exist create it
if !champ
champ = create(:champ_dossier_link, dossier: dossier, type_de_champ: type_de_champ)
end
# set champ value with linked dossier
champ.value = linked_dossier.id
champ.save!
end

View file

@ -8,7 +8,7 @@ feature 'Inviting an expert:' do
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) }
let(:dossier) { create(:dossier, :en_construction, :with_dossier_link, procedure: procedure) }
context 'as an Instructeur' do
scenario 'I can invite an expert' do
@ -20,6 +20,7 @@ feature 'Inviting an expert:' do
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.'
check 'avis_invite_linked_dossiers'
page.select 'confidentiel', from: 'avis_confidentiel'
perform_enqueued_jobs do
@ -34,6 +35,8 @@ feature 'Inviting an expert:' do
expect(page).to have_content('Bonjour, merci de me donner votre avis sur ce dossier.')
end
expect(Avis.count).to eq(4)
expect(all_emails.size).to eq(2)
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)

View file

@ -7,8 +7,7 @@ feature 'Instructing a dossier:' do
let!(:instructeur) { create(:instructeur, password: password) }
let!(:procedure) { create(:procedure, :published, instructeurs: [instructeur]) }
let!(:dossier) { create(:dossier, state: Dossier.states.fetch(:en_construction), procedure: procedure) }
let!(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
context 'the instructeur is also a user' do
scenario 'a instructeur can fill a dossier' do
visit commencer_path(path: procedure.path)

View file

@ -0,0 +1,126 @@
require 'spec_helper'
feature 'The routing' do
let(:procedure) { create(:procedure, :with_service, :for_individual) }
let(:administrateur) { create(:administrateur, procedures: [procedure]) }
let(:scientifique_user) { create(:user) }
let(:litteraire_user) { create(:user) }
before { Flipper.enable_actor(:routage, administrateur.user) }
scenario 'works' do
login_as administrateur.user, scope: :user
visit admin_procedure_path(procedure.id)
click_on "Groupe d'instructeurs"
# rename routing criteria to spécialité
fill_in 'procedure_routing_criteria_name', with: 'spécialité'
click_on 'Renommer'
expect(procedure.reload.routing_criteria_name).to eq('spécialité')
# rename defaut groupe to littéraire
click_on 'voir'
fill_in 'groupe_instructeur_label', with: 'littéraire'
click_on 'Renommer'
# add victor to littéraire groupe
fill_in 'instructeur_email', with: 'victor@inst.com'
perform_enqueued_jobs { click_on 'Affecter' }
victor = User.find_by(email: 'victor@inst.com').instructeur
click_on "Groupes dinstructeurs"
# add scientifique groupe
fill_in 'groupe_instructeur_label', with: 'scientifique'
click_on 'Ajouter le groupe'
# add marie to scientifique groupe
fill_in 'instructeur_email', with: 'marie@inst.com'
perform_enqueued_jobs { click_on 'Affecter' }
marie = User.find_by(email: 'marie@inst.com').instructeur
# publish
publish_procedure(procedure)
log_out
# 2 users fill a dossier in each group
user_send_dossier(scientifique_user, 'scientifique')
user_send_dossier(litteraire_user, 'littéraire')
# the litteraires instructeurs only manage the litteraires dossiers
register_instructeur_and_log_in(victor.email)
click_on procedure.libelle
expect(page).to have_text(litteraire_user.email)
expect(page).not_to have_text(scientifique_user.email)
# the search only show litteraires dossiers
fill_in 'q', with: scientifique_user.email
click_on 'Rechercher'
expect(page).to have_text('0 dossier trouvé')
fill_in 'q', with: litteraire_user.email
click_on 'Rechercher'
expect(page).to have_text('1 dossier trouvé')
## and the result is clickable
click_on litteraire_user.email
expect(page).to have_current_path(instructeur_dossier_path(procedure, litteraire_user.dossiers.first))
log_out
# the scientifiques instructeurs only manage the scientifiques dossiers
register_instructeur_and_log_in(marie.email)
click_on procedure.libelle
expect(page).not_to have_text(litteraire_user.email)
expect(page).to have_text(scientifique_user.email)
log_out
# TODO: notifications tests
end
def publish_procedure(procedure)
click_on procedure.libelle
find('#publish-procedure').click
within '#publish-modal' do
fill_in 'lien_site_web', with: 'http://some.website'
click_on 'publish'
end
expect(page).to have_text('Démarche publiée')
end
def user_send_dossier(user, groupe)
login_as user, scope: :user
visit commencer_path(path: procedure.reload.path)
click_on 'Commencer la démarche'
fill_in 'individual_nom', with: 'Nom'
fill_in 'individual_prenom', with: 'Prenom'
click_button('Continuer')
select(groupe, from: 'dossier_groupe_instructeur_id')
click_on 'Déposer le dossier'
log_out
end
def register_instructeur_and_log_in(email)
confirmation_email = emails_sent_to(email)
.filter { |m| m.subject == 'Activez votre compte instructeur' }
.first
token_params = confirmation_email.body.match(/token=[^"]+/)
visit "users/activate?#{token_params}"
fill_in :user_password, with: 'démarches-simplifiées-pwd'
click_button 'Définir le mot de passe'
expect(page).to have_content 'Mot de passe enregistré'
end
def log_out
click_on 'Se déconnecter'
end
end

View file

@ -15,10 +15,11 @@ describe DossierLinkHelper do
end
context "when access as instructeur" do
let(:dossier) { create(:dossier) }
let(:procedure) { create(:procedure, :routee) }
let(:dossier) { create(:dossier, groupe_instructeur: procedure.groupe_instructeurs.last) }
let(:instructeur) { create(:instructeur) }
before { dossier.procedure.defaut_groupe_instructeur.instructeurs << instructeur }
before { procedure.groupe_instructeurs.last.instructeurs << instructeur }
it { expect(helper.dossier_linked_path(instructeur, dossier)).to eq(instructeur_dossier_path(dossier.procedure, dossier)) }
end

View file

@ -87,18 +87,6 @@ RSpec.describe Avis, type: :model do
end
end
describe '#notify_instructeur' do
context 'when an avis is created' do
before do
avis_invitation_double = double('avis_invitation', deliver_later: true)
allow(AvisMailer).to receive(:avis_invitation).and_return(avis_invitation_double)
Avis.create(claimant: claimant, email: 'email@l.com')
end
it { expect(AvisMailer).to have_received(:avis_invitation) }
end
end
describe '#try_to_assign_instructeur' do
let!(:instructeur) { create(:instructeur) }
let(:avis) { Avis.create(claimant: claimant, email: email, dossier: create(:dossier)) }

View file

@ -0,0 +1,38 @@
require 'spec_helper'
describe GroupeInstructeur, type: :model do
let(:procedure) { create(:procedure) }
subject { GroupeInstructeur.new(label: label, procedure: procedure) }
context 'with no label provided' do
let(:label) { '' }
it { is_expected.to be_invalid }
end
context 'with a valid label' do
let(:label) { 'Préfecture de la Marne' }
it { is_expected.to be_valid }
end
context 'with a label with extra spaces' do
let(:label) { 'Préfecture de la Marne ' }
before do
subject.save
subject.reload
end
it { is_expected.to be_valid }
it { expect(subject.label).to eq("Préfecture de la Marne") }
end
context 'with a label already used for this procedure' do
let(:label) { 'Préfecture de la Marne' }
before do
GroupeInstructeur.create!(label: label, procedure: procedure)
end
it { is_expected.to be_invalid }
end
end