Merge pull request #6699 from betagouv/main

2021-11-30-03
This commit is contained in:
Kara Diaby 2021-11-30 15:35:28 +01:00 committed by GitHub
commit b67301c149
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 253 additions and 96 deletions

View file

@ -23,6 +23,10 @@
color: $blue-france-500;
}
.card-admin-status-error {
color: $light-red;
}
.card-admin-title {
font-weight: bold;
font-size: 18px;

View file

@ -1,5 +1,23 @@
module Manager
class InstructeursController < Manager::ApplicationController
# Temporary code: synchronize Flipper's instructeur_bypass_email_login_token
# when Instructeur.bypass_email_login_token is modified.
#
# This will be removed when the migration of this feature flag out of Flipper will be complete.
def update
super
instructeur = requested_resource
saved_successfully = !requested_resource.changed?
if saved_successfully
if instructeur.bypass_email_login_token
Flipper.enable_actor(:instructeur_bypass_email_login_token, instructeur.user)
else
Flipper.disable_actor(:instructeur_bypass_email_login_token, instructeur.user)
end
end
end
def reinvite
instructeur = Instructeur.find(params[:id])
instructeur.user.invite!

View file

@ -14,7 +14,7 @@ class InstructeurDashboard < Administrate::BaseDashboard
updated_at: Field::DateTime,
dossiers: Field::HasMany,
procedures: Field::HasMany,
features: FeaturesField
bypass_email_login_token: Field::Boolean
}.freeze
# COLLECTION_ATTRIBUTES
@ -35,13 +35,15 @@ class InstructeurDashboard < Administrate::BaseDashboard
:id,
:user,
:created_at,
:features
:bypass_email_login_token
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = [].freeze
FORM_ATTRIBUTES = [
:bypass_email_login_token
].freeze
# Overwrite this method to customize how users are displayed
# across all pages of the admin dashboard.

View file

@ -24,6 +24,12 @@ module ProcedureHelper
t(action, scope: [:modal, :publish, key])
end
# Returns a hash of { attribute: full_message } errors.
def procedure_publication_errors(procedure)
procedure.validate(:publication)
procedure.errors.to_hash(full_messages: true).except(:path)
end
def types_de_champ_data(procedure)
{
isAnnotation: false,

View file

@ -234,6 +234,7 @@ class Procedure < ApplicationRecord
validates :description, presence: true, allow_blank: false, allow_nil: false
validates :administrateurs, presence: true
validates :lien_site_web, presence: true, if: :publiee?
validates :draft_revision, 'revisions/no_empty_repetitions': true, if: :validate_for_publication?
validate :check_juridique
validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,200}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false }
validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }
@ -711,8 +712,12 @@ class Procedure < ApplicationRecord
private
def validate_for_publication?
validation_context == :publication || publiee?
end
def before_publish
update!(closed_at: nil, unpublished_at: nil)
assign_attributes(closed_at: nil, unpublished_at: nil)
end
def after_publish(canonical_procedure = nil)

View file

@ -0,0 +1,22 @@
class Revisions::NoEmptyRepetitionsValidator < ActiveModel::EachValidator
def validate_each(procedure, attribute, revision)
return if revision.nil?
revision_tdcs = revision.types_de_champ + revision.types_de_champ_private
repetitions = revision_tdcs.filter(&:repetition?)
repetitions.each do |repetition|
validate_repetition_not_empty(procedure, attribute, repetition)
end
end
private
def validate_repetition_not_empty(procedure, attribute, repetition)
if repetition.types_de_champ.blank?
procedure.errors.add(
attribute,
procedure.errors.generate_message(attribute, :empty_repetition, { value: repetition.libelle })
)
end
end
end

View file

@ -1,7 +1,7 @@
class SiretFormatValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if !format_is_valid(value)
record.errors.add(attribute, :format)
record.errors.add(attribute, :length)
end
if !luhn_passed(value)

View file

@ -1,6 +1,13 @@
.card.mb-4
%h2.card-title Publiez votre démarche
= form_tag admin_procedure_publish_path(procedure_id: procedure.id), method: :put, class: 'form' do
- publication_errors = procedure_publication_errors(procedure)
- if publication_errors.present?
.card.warning
.card-title Des problèmes empêchent la publication de la démarche
- publication_errors.each do |_attribute, full_messages|
%p= full_messages.to_sentence
- if procedure.draft_changed?
%p.mb-4 Publiez une nouvelle version de votre démarche. Les modifications suivantes seront appliquées :
= render partial: 'revision_changes', locals: { changes: procedure.revision_changes }
@ -31,4 +38,4 @@
placeholder: 'https://exemple.gouv.fr/ma_demarche')
.flex.justify-end
= submit_tag procedure_publish_label(procedure, :submit), { class: "button primary", id: 'publish' }
= submit_tag procedure_publish_label(procedure, :submit), { disabled: publication_errors.present?, class: "button primary", id: 'publish' }

View file

@ -52,15 +52,22 @@
%p.button Modifier
- if !@procedure.locked? || @procedure.feature_enabled?(:procedure_revisions)
= link_to champs_admin_procedure_path(@procedure), class: 'card-admin' do
- if @procedure.draft_types_de_champ.count > 0
%div
%span.icon.accept
%p.card-admin-status-accept Validé
- else
- @procedure.validate(:publication)
- error_messages = @procedure.errors.messages_for(:draft_revision).to_sentence
= link_to champs_admin_procedure_path(@procedure), class: 'card-admin', title: error_messages do
- if @procedure.draft_types_de_champ.count == 0
%div
%span.icon.clock
%p.card-admin-status-todo À faire
- elsif error_messages.present?
%div
%span.icon.refuse
%p.card-admin-status-error À modifier
- else
%div
%span.icon.accept
%p.card-admin-status-accept Validé
%div
%p.card-admin-title
%span.badge.baseline= @procedure.draft_types_de_champ.count

View file

@ -40,6 +40,11 @@
- if avis.revokable_by?(current_instructeur)
|
= link_to(t('revoke', scope: 'helpers.label'), revoquer_instructeur_avis_path(avis.procedure, avis), data: { confirm: t('revoke', scope: 'helpers.confirmation', email: avis.expert.email) }, method: :patch)
- if avis.introduction_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: avis.introduction_file.attachment }
.answer-body.mb-3
%p #{t('views.instructeurs.avis.introduction_file_explaination')} #{avis.claimant.email}
- if avis.piece_justificative_file.attached?
= render partial: 'shared/attachment/show', locals: { attachment: avis.piece_justificative_file.attachment }
.answer-body

View file

@ -26,7 +26,7 @@ as well as a link to its edit page.
<div>
<%= link_to(
t("administrate.actions.edit_resource", name: page.page_title),
'Modifier',
[:edit, namespace, page.resource],
class: "button",
) if valid_action?(:edit) && show_action?(:edit, page.resource) %>

View file

@ -51,6 +51,9 @@ module TPS
# Set the queue name for the mail delivery jobs to 'mailers'
config.action_mailer.deliver_later_queue_name = :mailers
# Allow the error messages format to be customized
config.active_model.i18n_customize_full_message = true
# Set the queue name for the analysis jobs to 'active_storage_analysis'
config.active_storage.queues.analysis = :active_storage_analysis

View file

@ -135,6 +135,8 @@ en:
instructeurs:
dossiers:
deleted_by_user: "File deleted by user"
avis:
introduction_file_explaination: "File attached to the request for advice"
users:
dossiers:
autosave:

View file

@ -131,6 +131,8 @@ fr:
instructeurs:
dossiers:
deleted_by_user: "Dossier supprimé par l'usager"
avis:
introduction_file_explaination: "Fichier joint à la demande davis"
users:
dossiers:
autosave:
@ -308,6 +310,7 @@ fr:
connexion: "Erreur lors de la connexion à France Connect."
forbidden_html: "Seul-e-s les usagers peuvent se connecter via France Connect. En tant quinstructeur ou administrateur, nous vous invitons à <a href='%{reset_link}'>réininitialiser votre mot de passe</a>."
procedure_archived: "Cette démarche en ligne a été close, il nest plus possible de déposer de dossier."
empty_repetition: 'Le bloc répétable « %{value} » doit comporter au moins un champ'
# procedure_not_draft: "Cette démarche nest maintenant plus en brouillon."
cadastres_empty:
one: "Aucune parcelle cadastrale sur la zone sélectionnée"

View file

@ -4,3 +4,10 @@ fr:
instructeur:
one: Instructeur
other: Instructeurs
attributes:
instructeur:
bypass_email_login_token: Autoriser la connexion sans confirmer lemail
helpers:
label:
instructeur:
bypass_email_login_token: Autoriser la connexion sans confirmer lemail

View file

@ -23,4 +23,5 @@ fr:
attributes:
api_particulier_token:
invalid: 'na pas le bon format'
draft_revision:
format: '%{message}'

View file

@ -10,6 +10,6 @@ fr:
siret:
attributes:
siret:
format: 'Le numéro SIRET doit comporter 14 chiffres'
length: 'Le numéro SIRET doit comporter 14 chiffres'
checksum: 'Le numéro SIRET comporte une erreur, vérifiez les chiffres composant le numéro'
invalid: 'Le numéro SIRET ne correspond pas à un établissement existant'

View file

@ -39,7 +39,7 @@ Rails.application.routes.draw do
put 'unblock_email'
end
resources :instructeurs, only: [:index, :show] do
resources :instructeurs, only: [:index, :show, :edit, :update] do
post 'reinvite', on: :member
delete 'delete', on: :member
end

View file

@ -0,0 +1,18 @@
namespace :after_party do
desc 'Deployment task: populate_bypass_email_login'
task populate_bypass_email_login: :environment do
user_ids = Flipper::Adapters::ActiveRecord::Gate
.where(feature_key: 'instructeur_bypass_email_login_token')
.pluck(:value)
.filter { |s| s.start_with?('User:') }
.map { |s| s.gsub('User:', '') }
.map(&:to_i)
Instructeur
.where(user: { id: user_ids })
.update_all(bypass_email_login_token: true)
AfterParty::TaskRecord
.create version: AfterParty::TaskRecorder.new(__FILE__).timestamp
end
end

View file

@ -622,6 +622,19 @@ describe Administrateurs::ProceduresController, type: :controller do
let(:path) { 'Invalid Procedure Path' }
it { expect { put :publish, params: { procedure_id: procedure.id, path: path, lien_site_web: lien_site_web } }.to raise_error(ActiveRecord::RecordInvalid) }
end
context 'procedure revision is invalid' do
let(:path) { 'new_path' }
let(:empty_repetition) { build(:type_de_champ_repetition, types_de_champ: []) }
let(:procedure) do
create(:procedure,
administrateur: admin,
lien_site_web: lien_site_web,
types_de_champ: [empty_repetition])
end
it { expect { put :publish, params: { procedure_id: procedure.id, path: path, lien_site_web: lien_site_web } }.to raise_error(ActiveRecord::RecordInvalid) }
end
end
context 'when admin is not the owner of the procedure' do

View file

@ -278,6 +278,43 @@ describe Procedure do
it_behaves_like 'duree de conservation'
end
describe 'draft_revision' do
let(:repetition) { build(:type_de_champ_repetition, libelle: 'Enfants') }
let(:text_field) { build(:type_de_champ_text) }
let(:procedure) { create(:procedure, types_de_champ: [repetition]) }
let(:invalid_repetition_error_message) { 'Le bloc répétable « Enfants » doit comporter au moins un champ' }
context 'on a draft procedure' do
it 'doesnt validate repetitions' do
procedure.validate
expect(procedure.errors[:draft_revision]).not_to include(invalid_repetition_error_message)
end
end
context 'on a published procedure' do
before { procedure.publish }
it 'validates that no repetition type de champ is empty' do
procedure.validate
expect(procedure.errors.full_messages_for(:draft_revision)).to include(invalid_repetition_error_message)
text_field.revision = repetition.revision
text_field.order_place = repetition.types_de_champ.size
procedure.draft_revision.types_de_champ.find(&:repetition?).types_de_champ << text_field
procedure.validate
expect(procedure.errors.full_messages_for(:draft_revision)).not_to include(invalid_repetition_error_message)
end
end
context 'when validating for publication' do
it 'validates that no repetition type de champ is empty' do
procedure.validate(:publication)
expect(procedure.errors.full_messages_for(:draft_revision)).to include(invalid_repetition_error_message)
end
end
end
end
describe 'active' do

View file

@ -1,71 +1,38 @@
require 'system/administrateurs/procedure_spec_helper'
describe 'As an administrateur I wanna create a new procedure', js: true do
describe 'Creating a new procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur, :with_procedure) }
let(:administrateur) { create(:administrateur) }
before do
login_as administrateur.user, scope: :user
visit root_path
end
context 'Right after sign_in I shall see all procedure states links' do
scenario 'Finding draft procedures' do
page.all('.tabs li a')[1].click
expect(page).to have_current_path(admin_procedures_path(statut: 'brouillons'))
end
scenario 'an admin can create a new procedure from scratch' do
expect(page).to have_selector('#new-procedure')
find('#new-procedure').click
scenario 'Finding active procedures' do
page.all('.tabs li a').first.click
expect(page).to have_current_path(admin_procedures_path(statut: 'publiees'))
end
expect(page).to have_current_path(new_from_existing_admin_procedures_path)
click_on 'Créer une nouvelle démarche de zéro'
expect(find('#procedure_for_individual_true')).to be_checked
expect(find('#procedure_for_individual_false')).not_to be_checked
fill_in 'procedure_duree_conservation_dossiers_dans_ds', with: '3'
click_on 'Créer la démarche'
scenario 'Finding archived procedures' do
page.all('.tabs li a').last.click
expect(page).to have_current_path(admin_procedures_path(statut: 'archivees'))
end
expect(page).to have_text('Libelle doit être rempli')
fill_in_dummy_procedure_details
click_on 'Créer la démarche'
expect(page).to have_current_path(champs_admin_procedure_path(Procedure.last))
end
context 'Creating a new procedure' do
context "when publish_draft enabled" do
scenario 'Finding save button for new procedure, libelle, description and cadre_juridique required' do
expect(page).to have_selector('#new-procedure')
find('#new-procedure').click
context 'with an empty procedure' do
let(:procedure) { create(:procedure, :with_service, administrateur: administrateur) }
expect(page).to have_current_path(new_from_existing_admin_procedures_path)
click_on 'Créer une nouvelle démarche de zéro'
expect(find('#procedure_for_individual_true')).to be_checked
expect(find('#procedure_for_individual_false')).not_to be_checked
fill_in 'procedure_duree_conservation_dossiers_dans_ds', with: '3'
click_on 'Créer la démarche'
expect(page).to have_text('Libelle doit être rempli')
fill_in_dummy_procedure_details
click_on 'Créer la démarche'
expect(page).to have_current_path(champs_admin_procedure_path(Procedure.last))
end
end
end
context 'Editing a new procedure' do
before 'Create procedure' do
expect(page).to have_selector('#new-procedure')
find('#new-procedure').click
expect(page).to have_current_path(new_from_existing_admin_procedures_path)
click_on 'Créer une nouvelle démarche de zéro'
fill_in_dummy_procedure_details
click_on 'Créer la démarche'
procedure = Procedure.last
procedure.update(service: create(:service))
end
scenario 'Add champ, add file, visualize them in procedure preview' do
page.refresh
expect(page).to have_current_path(champs_admin_procedure_path(Procedure.last))
scenario 'an admin can add types de champs' do
visit champs_admin_procedure_path(procedure)
add_champ(remove_flash_message: true)
fill_in 'champ-0-libelle', with: 'libelle de champ'
@ -84,28 +51,22 @@ describe 'As an administrateur I wanna create a new procedure', js: true do
end
end
scenario 'After adding champ and file, make publication' do
page.refresh
scenario 'a warning is displayed when creating an invalid procedure' do
visit champs_admin_procedure_path(procedure)
# Add an empty repetition type de champ
add_champ(remove_flash_message: true)
fill_in 'champ-0-libelle', with: 'libelle de champ'
select('Bloc répétable', from: 'champ-0-type_champ')
fill_in 'champ-0-libelle', with: 'libellé de champ'
blur
expect(page).to have_content('Formulaire enregistré')
click_on Procedure.last.libelle
expect(page).to have_current_path(admin_procedure_path(Procedure.last))
find('#publish-procedure-link').click
expect(page).to have_content('en test')
# Only check the path even though the link is the complete URL
# (Capybara runs the app on an arbitrary host/port.)
expect(page).to have_link(nil, href: /#{commencer_test_path(Procedure.last.path)}/)
click_link procedure.libelle
expect(page).to have_current_path(admin_procedure_path(procedure))
expect(page).to have_selector('#procedure_path', visible: true)
expect(find_field('procedure_path').value).to eq 'libelle-de-la-procedure'
fill_in 'lien_site_web', with: 'http://some.website'
click_on 'publish'
expect(page).to have_text('Démarche publiée')
champs_card = find('.card-admin', text: 'Champs du formulaire')
expect(champs_card).to have_selector('.icon.refuse')
expect(champs_card).to have_content('À modifier')
end
end
end

View file

@ -1,6 +1,6 @@
require 'system/administrateurs/procedure_spec_helper'
describe 'Publication de démarches', js: true do
describe 'Publishing a procedure', js: true do
include ProcedureSpecHelper
let(:administrateur) { create(:administrateur) }
@ -18,23 +18,26 @@ describe 'Publication de démarches', js: true do
login_as administrateur.user, scope: :user
end
context "lorsqu'on essaie d'accéder au backoffice déprécié" do
scenario "on est redirigé pour les démarches brouillon" do
context 'when using a deprecated back-office URL' do
scenario 'the admin is redirected to the draft procedure' do
visit admin_procedures_draft_path
expect(page).to have_current_path(admin_procedures_path(statut: "brouillons"))
end
scenario "on est redirigé pour les démarches archivées" do
scenario 'the admin is redirected to the archived procedures' do
visit admin_procedures_archived_path
expect(page).to have_current_path(admin_procedures_path(statut: "archivees"))
end
end
context 'lorsquune démarche est en test' do
scenario 'un administrateur peut la publier' do
context 'when a procedure isnt published yet' do
before do
visit admin_procedures_path(statut: "brouillons")
click_on procedure.libelle
find('#publish-procedure-link').click
end
scenario 'an admin can publish it' do
expect(find_field('procedure_path').value).to eq procedure.path
fill_in 'lien_site_web', with: 'http://some.website'
click_on 'Publier'
@ -42,9 +45,31 @@ describe 'Publication de démarches', js: true do
expect(page).to have_text('Démarche publiée')
expect(page).to have_selector('#preview-procedure')
end
context 'when the procedure has invalid champs' do
let(:empty_repetition) { build(:type_de_champ_repetition, types_de_champ: []) }
let!(:procedure) do
create(:procedure,
:with_path,
:with_service,
instructeurs: instructeurs,
administrateur: administrateur,
types_de_champ: [empty_repetition])
end
scenario 'an error message prevents the publication' do
expect(page).to have_content('Des problèmes empêchent la publication de la démarche')
expect(page).to have_content("Le bloc répétable « #{empty_repetition.libelle} » doit comporter au moins un champ")
expect(find_field('procedure_path').value).to eq procedure.path
fill_in 'lien_site_web', with: 'http://some.website'
expect(page).to have_button('Publier', disabled: true)
end
end
end
context 'lorsquune démarche est close' do
context 'when a procedure is closed' do
let!(:procedure) do
create(:procedure_with_dossiers,
:closed,
@ -55,7 +80,7 @@ describe 'Publication de démarches', js: true do
administrateur: administrateur)
end
scenario 'un administrateur peut la publier' do
scenario 'an admin can publish it again' do
visit admin_procedures_path(statut: "archivees")
click_on procedure.libelle
find('#publish-procedure-link').click
@ -69,7 +94,7 @@ describe 'Publication de démarches', js: true do
end
end
context 'lorsquune démarche est dépublié' do
context 'when a procedure is de-published' do
let!(:procedure) do
create(:procedure_with_dossiers,
:unpublished,
@ -80,7 +105,7 @@ describe 'Publication de démarches', js: true do
administrateur: administrateur)
end
scenario 'un administrateur peut la publier' do
scenario 'an admin can publish it again' do
visit admin_procedures_path(statut: "archivees")
click_on procedure.libelle
find('#publish-procedure-link').click

View file

@ -4,10 +4,12 @@ describe 'instructeurs/shared/avis/_list.html.haml', type: :view do
subject { render 'instructeurs/shared/avis/list.html.haml', avis: avis, avis_seen_at: seen_at, current_instructeur: instructeur }
let(:instructeur) { create(:instructeur) }
let(:instructeur2) { create(:instructeur) }
let(:introduction_file) { fixture_file_upload('spec/fixtures/files/piece_justificative_0.pdf', 'application/pdf') }
let(:expert) { create(:expert) }
let!(:dossier) { create(:dossier) }
let(:experts_procedure) { create(:experts_procedure, expert: expert, procedure: dossier.procedure) }
let(:avis) { [create(:avis, claimant: instructeur, experts_procedure: experts_procedure)] }
let(:avis) { [create(:avis, claimant: instructeur, experts_procedure: experts_procedure, introduction_file: introduction_file)] }
let(:seen_at) { avis.first.created_at + 1.hour }
it { is_expected.to have_text(avis.first.introduction) }
@ -16,6 +18,7 @@ describe 'instructeurs/shared/avis/_list.html.haml', type: :view do
context 'with a seen_at before avis created_at' do
let(:seen_at) { avis.first.created_at - 1.hour }
it { is_expected.to have_text("Fichier joint à la demande davis") }
it { is_expected.to have_css(".highlighted") }
end
@ -26,4 +29,12 @@ describe 'instructeurs/shared/avis/_list.html.haml', type: :view do
expect(subject).to include(simple_format(avis.first.answer))
end
end
context 'with another instructeur' do
let(:avis) { [create(:avis, :with_answer, claimant: instructeur2, experts_procedure: experts_procedure, introduction_file: introduction_file)] }
it 'shows the files attached to the avis request' do
expect(subject).to have_text("Fichier joint à la demande davis")
end
end
end