Merge pull request #7245 from betagouv/main

2022-05-04-03
This commit is contained in:
Paul Chavard 2022-05-04 16:28:19 +02:00 committed by GitHub
commit 5a6882ab54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 425 additions and 76 deletions

View file

@ -5,7 +5,7 @@ module Users
layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret] layout 'procedure_context', only: [:identite, :update_identite, :siret, :update_siret]
ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all] ACTIONS_ALLOWED_TO_ANY_USER = [:index, :recherche, :new, :transferer_all]
ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :demande, :messagerie, :brouillon, :update_brouillon, :modifier, :update, :create_commentaire] ACTIONS_ALLOWED_TO_OWNER_OR_INVITE = [:show, :demande, :messagerie, :brouillon, :update_brouillon, :modifier, :update, :create_commentaire, :papertrail]
ACTIONS_ALLOWED_TO_OWNER_OR_INVITE_HIDDEN = [:restore] ACTIONS_ALLOWED_TO_OWNER_OR_INVITE_HIDDEN = [:restore]
before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE_HIDDEN before_action :ensure_ownership!, except: ACTIONS_ALLOWED_TO_ANY_USER + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE + ACTIONS_ALLOWED_TO_OWNER_OR_INVITE_HIDDEN
@ -69,6 +69,11 @@ module Users
end end
end end
def papertrail
raise ActionController::BadRequest if dossier.brouillon?
@dossier = dossier
end
def identite def identite
@dossier = dossier @dossier = dossier
@user = current_user @user = current_user

View file

@ -9,7 +9,11 @@ class API::V2::Schema < GraphQL::Schema
context_class API::V2::Context context_class API::V2::Context
def self.id_from_object(object, type_definition, ctx) def self.id_from_object(object, type_definition, ctx)
object.to_typed_id if object.is_a?(Hash)
object[:id]
else
object.to_typed_id
end
end end
def self.object_from_id(id, query_ctx) def self.object_from_id(id, query_ctx)

View file

@ -69,7 +69,7 @@ module Types
def usager def usager
if object.user_deleted? if object.user_deleted?
{ email: object.user_email_for(:display), id: -1 } { email: object.user_email_for(:display), id: '<deleted>' }
else else
Loaders::Record.for(User).load(object.user_id) Loaders::Record.for(User).load(object.user_id)
end end

View file

@ -0,0 +1,19 @@
module PapertrailHelper
def papertrail_requester_identity(dossier)
if dossier.etablissement.present?
raison_sociale_or_name(dossier.etablissement)
else
[dossier.individual.prenom, dossier.individual.nom.upcase].join(' ')
end
end
def papertrail_dossier_state(dossier)
raise "Dossiers in 'brouillon' state are not supported" if dossier.brouillon?
# i18n-tasks-use t('users.dossiers.papertrail.dossier_state.en_construction')
# i18n-tasks-use t('users.dossiers.papertrail.dossier_state.en_instruction')
# i18n-tasks-use t('users.dossiers.papertrail.dossier_state.accepte')
# i18n-tasks-use t('users.dossiers.papertrail.dossier_state.refuse')
# i18n-tasks-use t('users.dossiers.papertrail.dossier_state.sans_suite')
I18n.t("users.dossiers.papertrail.states.#{dossier.state}")
end
end

View file

@ -31,5 +31,13 @@ module TurboStreamHelper
def focus_all(targets) def focus_all(targets)
dispatch('dom:mutation', { action: :focus, targets: targets }) dispatch('dom:mutation', { action: :focus, targets: targets })
end end
def disable(target)
dispatch('dom:mutation', { action: :disable, target: target })
end
def enable(target)
dispatch('dom:mutation', { action: :enable, target: target })
end
end end
end end

View file

@ -38,7 +38,7 @@ export function useFeatureCollection(
features: callback(features) features: callback(features)
})); }));
httpRequest(url) httpRequest(url)
.js() .turbo()
.catch(() => null); .catch(() => null);
}, },
[url, setFeatureCollection] [url, setFeatureCollection]

View file

@ -22,4 +22,19 @@ export class ApplicationController extends Controller {
target: document.documentElement target: document.documentElement
}); });
} }
protected on<HandlerEvent extends Event = Event>(
eventName: string,
handler: (event: HandlerEvent) => void
): void {
const disconnect = this.disconnect;
const callback = (event: Event): void => {
handler(event as HandlerEvent);
};
this.element.addEventListener(eventName, callback);
this.disconnect = () => {
this.element.removeEventListener(eventName, callback);
disconnect.call(this);
};
}
} }

View file

@ -18,7 +18,7 @@ export class TurboEventController extends ApplicationController {
} }
} }
const MutationAction = z.enum(['show', 'hide', 'focus']); const MutationAction = z.enum(['show', 'hide', 'focus', 'enable', 'disable']);
type MutationAction = z.infer<typeof MutationAction>; type MutationAction = z.infer<typeof MutationAction>;
const Mutation = z.union([ const Mutation = z.union([
z.object({ z.object({
@ -55,6 +55,16 @@ const Mutations: Record<MutationAction, (mutation: Mutation) => void> = {
for (const element of findElements(mutation)) { for (const element of findElements(mutation)) {
element.focus(); element.focus();
} }
},
disable: (mutation) => {
for (const element of findElements<HTMLInputElement>(mutation)) {
element.disabled = true;
}
},
enable: (mutation) => {
for (const element of findElements<HTMLInputElement>(mutation)) {
element.disabled = false;
}
} }
}; };

View file

@ -0,0 +1,22 @@
import { httpRequest } from '@utils';
import { ApplicationController } from './application_controller';
export class TurboInputController extends ApplicationController {
static values = {
url: String
};
declare readonly urlValue: string;
connect(): void {
this.on('input', () => this.debounce(this.load, 200));
}
private load(): void {
const target = this.element as HTMLInputElement;
const url = new URL(this.urlValue, document.baseURI);
url.searchParams.append(target.name, target.value);
httpRequest(url.toString()).turbo();
}
}

View file

@ -8,7 +8,6 @@ import * as Turbo from '@hotwired/turbo';
import '../shared/activestorage/ujs'; import '../shared/activestorage/ujs';
import '../shared/remote-poller'; import '../shared/remote-poller';
import '../shared/safari-11-file-xhr-workaround'; import '../shared/safari-11-file-xhr-workaround';
import '../shared/remote-input';
import '../shared/franceconnect'; import '../shared/franceconnect';
import '../shared/toggle-target'; import '../shared/toggle-target';
import '../shared/ujs-error-handling'; import '../shared/ujs-error-handling';
@ -19,6 +18,7 @@ import {
} from '../controllers/react_controller'; } from '../controllers/react_controller';
import { TurboEventController } from '../controllers/turbo_event_controller'; import { TurboEventController } from '../controllers/turbo_event_controller';
import { GeoAreaController } from '../controllers/geo_area_controller'; import { GeoAreaController } from '../controllers/geo_area_controller';
import { TurboInputController } from '../controllers/turbo_input_controller';
import '../new_design/dropdown'; import '../new_design/dropdown';
import '../new_design/form-validation'; import '../new_design/form-validation';
@ -96,6 +96,7 @@ const Stimulus = Application.start();
Stimulus.register('react', ReactController); Stimulus.register('react', ReactController);
Stimulus.register('turbo-event', TurboEventController); Stimulus.register('turbo-event', TurboEventController);
Stimulus.register('geo-area', GeoAreaController); Stimulus.register('geo-area', GeoAreaController);
Stimulus.register('turbo-input', TurboInputController);
// Expose globals // Expose globals
window.DS = window.DS || DS; window.DS = window.DS || DS;

View file

@ -1,21 +0,0 @@
import { delegate, fire, debounce } from '@utils';
const remote = 'data-remote';
const inputChangeSelector = `input[${remote}], textarea[${remote}]`;
// This is a patch for ujs remote handler. Its purpose is to add
// a debounced input listener.
function handleRemote(event) {
const element = this;
if (isRemote(element)) {
fire(element, 'change', event);
}
}
function isRemote(element) {
const value = element.getAttribute(remote);
return value && value !== 'false';
}
delegate('input', inputChangeSelector, debounce(handleRemote, 200));

View file

@ -148,7 +148,7 @@ class Champ < ApplicationRecord
# predictable input name. # predictable input name.
def input_name def input_name
if parent_id if parent_id
"#{parent.input_name}[#{champs_attributes_accessor}][#{id}]" "#{parent.input_name}[champs_attributes][#{id}]"
else else
"dossier[#{champs_attributes_accessor}][#{id}]" "dossier[#{champs_attributes_accessor}][#{id}]"
end end

View file

@ -12,7 +12,6 @@
# updated_at :datetime not null # updated_at :datetime not null
# bill_signature_id :bigint # bill_signature_id :bigint
# dossier_id :bigint # dossier_id :bigint
# instructeur_id :bigint
# #
class DossierOperationLog < ApplicationRecord class DossierOperationLog < ApplicationRecord
self.ignored_columns = [:instructeur_id] self.ignored_columns = [:instructeur_id]

View file

@ -1,9 +0,0 @@
<%= render_flash(timeout: 5000, fixed: true) %>
<%= render_to_element("##{@champ.input_group_id} .geo-areas",
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, editing: true }) %>
<% if @focus %>
<%= fire_event('map:feature:focus', { bbox: @champ.bounding_box }.to_json) %>
<% end %>

View file

@ -0,0 +1,4 @@
= turbo_stream.update dom_id(@champ, :geo_areas), partial: 'shared/champs/carte/geo_areas', locals: { champ: @champ, editing: true }
- if @focus
= turbo_stream.dispatch 'map:feature:focus', bbox: @champ.bounding_box

View file

@ -1,3 +0,0 @@
<%= render_to_element("##{@champ.input_group_id} .help-block",
partial: 'shared/champs/dossier_link/help_block',
locals: { id: @linked_dossier_id }) %>

View file

@ -0,0 +1 @@
= turbo_stream.update dom_id(@champ, :help_block), partial: 'shared/champs/dossier_link/help_block', locals: { id: @linked_dossier_id }

View file

@ -1,3 +0,0 @@
<%= render_to_element("##{@champ.input_group_id} .siret-info",
partial: 'shared/champs/siret/etablissement',
locals: { siret: @siret, etablissement: @etablissement }) %>

View file

@ -0,0 +1 @@
= turbo_stream.update dom_id(@champ, :siret_info), partial: 'shared/champs/siret/etablissement', locals: { siret: @siret, etablissement: @etablissement }

View file

@ -1,4 +1,4 @@
= form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { remote: test_complexity, url: show_password_complexity_path } = form.password_field :password, autofocus: true, autocomplete: 'off', placeholder: 'Mot de passe', data: { controller: test_complexity ? 'turbo-input' : false, turbo_input_url_value: show_password_complexity_path }
- if test_complexity - if test_complexity
#complexity-bar.password-complexity #complexity-bar.password-complexity

View file

@ -1,3 +0,0 @@
<%= render_to_element('#complexity-label', partial: 'label', outer: true) %>
<%= render_to_element('#complexity-bar', partial: 'bar', outer: true) %>
<%= raw("document.querySelector('#submit-password').disabled = #{@score < @min_complexity || @length < @min_length};") %>

View file

@ -0,0 +1,6 @@
= turbo_stream.replace 'complexity-label', partial: 'label'
= turbo_stream.replace 'complexity-bar', partial: 'bar'
- if @score < @min_complexity || @length < @min_length
= turbo_stream.disable 'submit-password'
- else
= turbo_stream.enable 'submit-password'

View file

@ -1,4 +1,4 @@
= react_component("MapEditor", featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ), options: champ.render_options) = react_component("MapEditor", featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ), options: champ.render_options)
.geo-areas .geo-areas{ id: dom_id(champ, :geo_areas) }
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, editing: true } = render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, editing: true }

View file

@ -5,7 +5,7 @@
placeholder: "Numéro de dossier", placeholder: "Numéro de dossier",
autocomplete: 'off', autocomplete: 'off',
required: champ.mandatory?, required: champ.mandatory?,
data: { remote: true, url: champs_dossier_link_path(champ.id) } data: { controller: 'turbo-input', turbo_input_url_value: champs_dossier_link_path(champ.id) }
.help-block .help-block{ id: dom_id(champ, :help_block) }
= render partial: 'shared/champs/dossier_link/help_block', locals: { id: champ.value } = render partial: 'shared/champs/dossier_link/help_block', locals: { id: champ.value }

View file

@ -2,11 +2,11 @@
id: champ.input_id, id: champ.input_id,
aria: { describedby: champ.describedby_id }, aria: { describedby: champ.describedby_id },
placeholder: champ.libelle, placeholder: champ.libelle,
data: { remote: true, debounce: true, url: champs_siret_path(champ.id), spinner: true }, data: { controller: 'turbo-input', turbo_input_url_value: champs_siret_path(champ.id) },
required: champ.mandatory?, required: champ.mandatory?,
pattern: "[0-9]{14}", pattern: "[0-9]{14}",
title: "Le numéro de SIRET doit comporter exactement 14 chiffres" title: "Le numéro de SIRET doit comporter exactement 14 chiffres"
.spinner.right.hidden .spinner.right.hidden
.siret-info .siret-info{ id: dom_id(champ, :siret_info) }
- if champ.etablissement.present? - if champ.etablissement.present?
= render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: champ.etablissement } = render partial: 'shared/dossiers/editable_champs/etablissement_titre', locals: { etablissement: champ.etablissement }

View file

@ -0,0 +1,99 @@
require 'prawn/measurement_extensions'
#----- A4 page size
page_size = 'A4'
page_width = 595
#----- margins
top_margin = 20
right_margin = 20
bottom_margin = 20
left_margin = 20
header_width = page_width - left_margin - right_margin
body_width = 400
body_left_margin = (page_width - body_width - left_margin - right_margin) / 2
prawn_document(margin: [top_margin, right_margin, bottom_margin, left_margin], page_size: page_size) do |pdf|
pdf.font_families.update('marianne' => {
normal: Rails.root.join('lib/prawn/fonts/marianne/marianne-regular.ttf'),
bold: Rails.root.join('lib/prawn/fonts/marianne/marianne-bold.ttf')
})
pdf.font 'marianne'
grey = '555555'
black = '333333'
pdf.float do
pdf.svg IO.read(DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC), height: 64
end
pdf.bounding_box([110, pdf.cursor - 18], width: header_width - 200) do
pdf.fill_color black
pdf.text APPLICATION_NAME, size: 20, style: :bold
pdf.fill_color grey
pdf.text t('.receipt'), size: 14
end
pdf.bounding_box([body_left_margin, pdf.cursor - 20], width: body_width) do
pdf.fill_color black
pdf.pad_top(40) { pdf.text @dossier.procedure.libelle, size: 14, character_spacing: -0.2, align: :center }
pdf.fill_color grey
description = t('.description', user_name: papertrail_requester_identity(@dossier), procedure: @dossier.procedure.libelle, date: l(@dossier.created_at, format: '%e %B %Y'))
pdf.pad_top(30) { pdf.text description, size: 10, character_spacing: -0.2, align: :left }
pdf.fill_color black
pdf.pad_top(30) { pdf.text t('views.shared.dossiers.demande.requester_identity'), size: 14, character_spacing: -0.2, align: :justify }
if @dossier.individual.present?
pdf.pad_top(7) do
pdf.fill_color grey
pdf.text "#{Individual.human_attribute_name(:prenom)} : #{@dossier.individual.prenom}", size: 10, character_spacing: -0.2, align: :justify
pdf.text "#{Individual.human_attribute_name(:nom)} : #{@dossier.individual.nom.upcase}", size: 10, character_spacing: -0.2, align: :justify
end
end
if @dossier.etablissement.present?
pdf.pad_top(7) do
pdf.fill_color grey
pdf.text "Dénomination : " + raison_sociale_or_name(@dossier.etablissement), size: 10, character_spacing: -0.2, align: :justify
pdf.text "SIRET : " + @dossier.etablissement.siret, size: 10, character_spacing: -0.2, align: :justify
end
end
pdf.fill_color black
pdf.pad_top(30) { pdf.text Dossier.model_name.human, size: 14, character_spacing: -0.2, align: :justify }
pdf.fill_color grey
pdf.pad_top(7) do
pdf.text "#{Dossier.human_attribute_name(:id)} : #{@dossier.id.to_s}", size: 10, character_spacing: -0.2, align: :justify
pdf.text t('.file_submitted_at') + ' : ' + l(@dossier.en_construction_at, format: '%e %B %Y'), size: 10, character_spacing: -0.2, align: :justify
pdf.text t('.dossier_state') + ' : ' + papertrail_dossier_state(@dossier), size: 10, character_spacing: -0.2, align: :justify
end
service = @dossier.procedure.service
if service.present?
pdf.fill_color black
pdf.pad_top(30) { pdf.text t('.administrative_service'), size: 14, character_spacing: -0.2, align: :justify }
pdf.fill_color grey
pdf.pad_top(7) do
pdf.text "#{Service.model_name.human} : " + [service.nom, service.organisme].join(", "), size: 10, character_spacing: -0.2, align: :justify
pdf.text "#{Service.human_attribute_name(:adresse)} : #{service.adresse}", size: 10, character_spacing: -0.2, align: :justify
pdf.text "#{Service.human_attribute_name(:email)} : #{service.email}", size: 10, character_spacing: -0.2, align: :justify
if service.telephone.present?
pdf.text "#{Service.human_attribute_name(:telephone)} : #{service.telephone}", size: 10, character_spacing: -0.2, align: :justify
end
end
end
pdf.fill_color black
pdf.pad_top(100) do
pdf.text t('.generated_at', date: l(Time.zone.now.to_date, format: :long)), size: 10, character_spacing: -0.2, align: :right
pdf.text t('.signature', app_name: APPLICATION_NAME), size: 10, character_spacing: -0.2, align: :right
end
end
end

View file

@ -9,5 +9,8 @@
.container .container
= render partial: 'users/dossiers/show/status_overview', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/status_overview', locals: { dossier: @dossier }
- if @dossier.procedure.feature_enabled?(:procedure_dossier_papertrail)
= render partial: 'users/dossiers/show/papertrail', locals: { dossier: @dossier }
- if !@dossier.termine? - if !@dossier.termine?
= render partial: 'users/dossiers/show/latest_message', locals: { dossier: @dossier } = render partial: 'users/dossiers/show/latest_message', locals: { dossier: @dossier }

View file

@ -0,0 +1,4 @@
.papertrail.mb-2
= link_to papertrail_dossier_url(dossier, format: :pdf), class: "button", download: t('.filename'), target: "_blank" do
%span.icon.justificatif
= t('.get_papertrail')

View file

@ -68,6 +68,9 @@ DS_ENV="staging"
# Instance customization: Procedure default logo ---> to be put in "app/assets/images" # Instance customization: Procedure default logo ---> to be put in "app/assets/images"
# PROCEDURE_DEFAULT_LOGO_SRC="republique-francaise-logo.svg" # PROCEDURE_DEFAULT_LOGO_SRC="republique-francaise-logo.svg"
# Instance customization: Deposit receipt logo ---> to be put in "app/assets/images"
# DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC="app/assets/images/republique-francaise-logo.svg"
# Instance customization: PDF export logo ---> to be put in "app/assets/images" # Instance customization: PDF export logo ---> to be put in "app/assets/images"
# DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg" # DOSSIER_PDF_EXPORT_LOGO_SRC="app/assets/images/header/logo-ds-wide.svg"

View file

@ -32,7 +32,8 @@ features = [
:hide_instructeur_email, :hide_instructeur_email,
:procedure_revisions, :procedure_revisions,
:procedure_routage_api, :procedure_routage_api,
:procedure_process_expired_dossiers_termine :procedure_process_expired_dossiers_termine,
:procedure_dossier_papertrail
] ]
def database_exists? def database_exists?

View file

@ -16,5 +16,8 @@ MAILER_FOOTER_LOGO_SRC = ENV.fetch("MAILER_FOOTER_LOGO_SRC", "mailer/instructeur
# Default logo of a procedure # Default logo of a procedure
PROCEDURE_DEFAULT_LOGO_SRC = ENV.fetch("PROCEDURE_DEFAULT_LOGO_SRC", "republique-francaise-logo.svg") PROCEDURE_DEFAULT_LOGO_SRC = ENV.fetch("PROCEDURE_DEFAULT_LOGO_SRC", "republique-francaise-logo.svg")
# Deposit receipt logo
DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC = ENV.fetch("DOSSIER_DEPOSIT_RECEIPT_LOGO_SRC", "app/assets/images/republique-francaise-logo.svg")
# Logo in PDF export of a "Dossier" # Logo in PDF export of a "Dossier"
DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.svg") DOSSIER_PDF_EXPORT_LOGO_SRC = ENV.fetch("DOSSIER_PDF_EXPORT_LOGO_SRC", "app/assets/images/header/logo-ds-wide.svg")

View file

@ -453,6 +453,24 @@ en:
identity_saved: "Identity data is registred" identity_saved: "Identity data is registred"
attestation: attestation:
no_longer_available: "The certificate is no longer available on this file." no_longer_available: "The certificate is no longer available on this file."
show:
papertrail:
get_papertrail: "Get a deposit receipt"
filename: "deposit-receipt.pdf"
papertrail:
receipt: "Deposit receipt"
description: "This document attests that on the %{date}, %{user_name} submitted a file on the procedure “%{procedure}”."
file_submitted_at: "File submission date"
dossier_state: "File status"
states:
en_construction: "submitted, pending processing"
en_instruction: "processing"
accepte: "accepted"
refuse: "declined"
sans suite: "closed, no further action"
administrative_service: "Administrative department"
generated_at: "Made on %{date},"
signature: "%{app_name}"
instructeurs: instructeurs:
dossiers: dossiers:
deleted_by_instructeur: "The folder has been deleted" deleted_by_instructeur: "The folder has been deleted"

View file

@ -461,6 +461,24 @@ fr:
identity_saved: "Identité enregistrée" identity_saved: "Identité enregistrée"
attestation: attestation:
no_longer_available: "Lattestation n'est plus disponible sur ce dossier." no_longer_available: "Lattestation n'est plus disponible sur ce dossier."
papertrail:
receipt: "Accusé de dépôt"
description: "Ce document atteste que %{user_name} a déposé le %{date} un dossier sur la démarche « %{procedure} »."
file_submitted_at: "Dossier déposé le"
dossier_state: "État du dossier"
states:
en_construction: "déposé, en attente dexamen par ladministration"
en_instruction: "en cours dinstruction par ladministration"
accepte: "accepté"
refuse: "refusé"
sans suite: "classé sans suite"
administrative_service: "Service administratif"
generated_at: "Fait le %{date},"
signature: "La direction de %{app_name}"
show:
papertrail:
get_papertrail: "Obtenir une attestation de dépôt de dossier"
filename: "attestation-de-depot.pdf"
instructeurs: instructeurs:
dossiers: dossiers:
deleted_by_instructeur: "Le dossier a bien été supprimé de votre interface" deleted_by_instructeur: "Le dossier a bien été supprimé de votre interface"

View file

@ -6,6 +6,7 @@ en:
other: "Files" other: "Files"
attributes: attributes:
dossier: dossier:
id: "File number"
state: "State" state: "State"
dossier/state: &state dossier/state: &state
brouillon: "Draft" brouillon: "Draft"

View file

@ -6,6 +6,7 @@ fr:
other: "Dossiers" other: "Dossiers"
attributes: attributes:
dossier: dossier:
id: "Numéro de dossier"
montant_projet: 'Le montant du projet' montant_projet: 'Le montant du projet'
montant_aide_demande: "Le montant daide demandée" montant_aide_demande: "Le montant daide demandée"
date_previsionnelle: "La date de début prévisionnelle" date_previsionnelle: "La date de début prévisionnelle"

View file

@ -0,0 +1,11 @@
en:
activerecord:
models:
service:
one: 'Service'
other: 'Services'
attributes:
service:
adresse: 'Mail address'
email: 'Email'
telephone: 'Phone'

View file

@ -1,4 +1,14 @@
fr: fr:
activerecord:
models:
service:
one: 'Service'
other: 'Services'
attributes:
service:
adresse: 'Adresse postale'
email: 'Email de contact'
telephone: 'Téléphone'
type_organisme: type_organisme:
administration_centrale: 'Administration centrale' administration_centrale: 'Administration centrale'
association: 'Association' association: 'Association'

View file

@ -273,6 +273,7 @@ Rails.application.routes.draw do
patch 'restore', to: 'dossiers#restore' patch 'restore', to: 'dossiers#restore'
get 'attestation' get 'attestation'
get 'transferer', to: 'dossiers#transferer' get 'transferer', to: 'dossiers#transferer'
get 'papertrail', format: :pdf
end end
collection do collection do

View file

@ -0,0 +1,5 @@
class RemoveColumnInstructeurIdFromDossierOperationLog < ActiveRecord::Migration[6.1]
def change
safety_assured { remove_column :dossier_operation_logs, :instructeur_id }
end
end

View file

@ -267,13 +267,11 @@ ActiveRecord::Schema.define(version: 2022_04_26_140107) do
t.text "digest" t.text "digest"
t.bigint "dossier_id" t.bigint "dossier_id"
t.datetime "executed_at" t.datetime "executed_at"
t.bigint "instructeur_id"
t.datetime "keep_until" t.datetime "keep_until"
t.string "operation", null: false t.string "operation", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["bill_signature_id"], name: "index_dossier_operation_logs_on_bill_signature_id" t.index ["bill_signature_id"], name: "index_dossier_operation_logs_on_bill_signature_id"
t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id" t.index ["dossier_id"], name: "index_dossier_operation_logs_on_dossier_id"
t.index ["instructeur_id"], name: "index_dossier_operation_logs_on_instructeur_id"
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until" t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
end end
@ -874,7 +872,6 @@ ActiveRecord::Schema.define(version: 2022_04_26_140107) do
add_foreign_key "commentaires", "dossiers" add_foreign_key "commentaires", "dossiers"
add_foreign_key "commentaires", "experts" add_foreign_key "commentaires", "experts"
add_foreign_key "dossier_operation_logs", "bill_signatures" add_foreign_key "dossier_operation_logs", "bill_signatures"
add_foreign_key "dossier_operation_logs", "instructeurs"
add_foreign_key "dossier_transfer_logs", "dossiers" add_foreign_key "dossier_transfer_logs", "dossiers"
add_foreign_key "dossiers", "dossier_transfers" add_foreign_key "dossiers", "dossier_transfers"
add_foreign_key "dossiers", "groupe_instructeurs" add_foreign_key "dossiers", "groupe_instructeurs"

View file

@ -117,7 +117,7 @@ describe Champs::CarteController, type: :controller do
render_views render_views
before do before do
get :index, params: params, format: :js, xhr: true get :index, params: params, format: :turbo_stream
end end
context 'without focus' do context 'without focus' do
@ -126,7 +126,7 @@ describe Champs::CarteController, type: :controller do
end end
it 'updates the list' do it 'updates the list' do
expect(response.body).not_to include("DS.fire('map:feature:focus'") expect(response.body).not_to include("map:feature:focus")
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
end end
@ -140,7 +140,8 @@ describe Champs::CarteController, type: :controller do
end end
it 'updates the list and focuses the map' do it 'updates the list and focuses the map' do
expect(response.body).to include("DS.fire('map:feature:focus'") expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :geo_areas))
expect(response.body).to include("map:feature:focus")
expect(response.status).to eq 200 expect(response.status).to eq 200
end end
end end

View file

@ -27,33 +27,33 @@ describe Champs::DossierLinkController, type: :controller do
context 'when the dossier exist' do context 'when the dossier exist' do
before do before do
get :show, params: params, format: :js, xhr: true get :show, params: params, format: :turbo_stream
end end
it 'renders the procedure name' do it 'renders the procedure name' do
expect(response.body).to include('Dossier en brouillon') expect(response.body).to include('Dossier en brouillon')
expect(response.body).to include(procedure.libelle) expect(response.body).to include(procedure.libelle)
expect(response.body).to include(procedure.organisation) expect(response.body).to include(procedure.organisation)
expect(response.body).to include("##{champ.input_group_id} .help-block") expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :help_block))
end end
end end
context 'when the dossier does not exist' do context 'when the dossier does not exist' do
let(:dossier_id) { '13' } let(:dossier_id) { '13' }
before do before do
get :show, params: params, format: :js, xhr: true get :show, params: params, format: :turbo_stream
end end
it 'renders error message' do it 'renders error message' do
expect(response.body).to include('Ce dossier est inconnu') expect(response.body).to include('Ce dossier est inconnu')
expect(response.body).to include("##{champ.input_group_id} .help-block") expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :help_block))
end end
end end
end end
context 'when user is not connected' do context 'when user is not connected' do
before do before do
get :show, params: { champ_id: champ.id }, format: :js, xhr: true get :show, params: { champ_id: champ.id }, format: :turbo_stream
end end
it { expect(response.code).to eq('401') } it { expect(response.code).to eq('401') }

View file

@ -37,7 +37,7 @@ describe Champs::SiretController, type: :controller do
end end
context 'when the SIRET is empty' do context 'when the SIRET is empty' do
subject! { get :show, params: params, format: :js, xhr: true } subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do it 'clears the etablissement and SIRET on the model' do
champ.reload champ.reload
@ -46,15 +46,14 @@ describe Champs::SiretController, type: :controller do
end end
it 'clears any information or error message' do it 'clears any information or error message' do
expect(response.body).to include("##{champ.input_group_id} .siret-info") expect(response.body).to include(ActionView::RecordIdentifier.dom_id(champ, :siret_info))
expect(response.body).to include('innerHTML = ""')
end end
end end
context 'when the SIRET is invalid' do context 'when the SIRET is invalid' do
let(:siret) { '1234' } let(:siret) { '1234' }
subject! { get :show, params: params, format: :js, xhr: true } subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do it 'clears the etablissement and SIRET on the model' do
champ.reload champ.reload
@ -71,7 +70,7 @@ describe Champs::SiretController, type: :controller do
let(:siret) { '82161143100015' } let(:siret) { '82161143100015' }
let(:api_etablissement_status) { 503 } let(:api_etablissement_status) { 503 }
subject! { get :show, params: params, format: :js, xhr: true } subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do it 'clears the etablissement and SIRET on the model' do
champ.reload champ.reload
@ -88,7 +87,7 @@ describe Champs::SiretController, type: :controller do
let(:siret) { '00000000000000' } let(:siret) { '00000000000000' }
let(:api_etablissement_status) { 404 } let(:api_etablissement_status) { 404 }
subject! { get :show, params: params, format: :js, xhr: true } subject! { get :show, params: params, format: :turbo_stream }
it 'clears the etablissement and SIRET on the model' do it 'clears the etablissement and SIRET on the model' do
champ.reload champ.reload
@ -106,7 +105,7 @@ describe Champs::SiretController, type: :controller do
let(:api_etablissement_status) { 200 } let(:api_etablissement_status) { 200 }
let(:api_etablissement_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') } let(:api_etablissement_body) { File.read('spec/fixtures/files/api_entreprise/etablissements.json') }
subject! { get :show, params: params, format: :js, xhr: true } subject! { get :show, params: params, format: :turbo_stream }
it 'populates the etablissement and SIRET on the model' do it 'populates the etablissement and SIRET on the model' do
champ.reload champ.reload
@ -119,7 +118,7 @@ describe Champs::SiretController, type: :controller do
end end
context 'when user is not signed in' do context 'when user is not signed in' do
subject! { get :show, params: { champ_id: champ.id }, format: :js, xhr: true } subject! { get :show, params: { champ_id: champ.id }, format: :turbo_stream }
it { expect(response.code).to eq('401') } it { expect(response.code).to eq('401') }
end end

View file

@ -4,7 +4,7 @@ describe PasswordComplexityController, type: :controller do
{ user: { password: 'moderately complex password' } } { user: { password: 'moderately complex password' } }
end end
subject { get :show, format: :js, params: params, xhr: true } subject { get :show, format: :turbo_stream, params: params }
it 'computes a password score' do it 'computes a password score' do
subject subject
@ -27,8 +27,8 @@ describe PasswordComplexityController, type: :controller do
it 'renders Javascript that updates the password complexity meter' do it 'renders Javascript that updates the password complexity meter' do
subject subject
expect(response.body).to include('#complexity-label') expect(response.body).to include('complexity-label')
expect(response.body).to include('#complexity-bar') expect(response.body).to include('complexity-bar')
end end
end end
end end

View file

@ -1011,6 +1011,31 @@ describe Users::DossiersController, type: :controller do
end end
end end
describe "#papertrail" do
before { sign_in(user) }
subject do
get :papertrail, format: :pdf, params: { id: dossier.id }
end
context 'when the dossier has been submitted' do
let(:dossier) { create(:dossier, :en_construction, user: user) }
it 'renders a PDF document' do
subject
expect(response).to render_template(:papertrail)
end
end
context 'when the dossier is still a draft' do
let(:dossier) { create(:dossier, :brouillon, user: user) }
it 'raises an error' do
expect { subject }.to raise_error(ActionController::BadRequest)
end
end
end
describe '#delete_dossier' do describe '#delete_dossier' do
before { sign_in(user) } before { sign_in(user) }

View file

@ -56,6 +56,31 @@ RSpec.describe Types::DossierType, type: :graphql do
it { expect(data[:dossier][:champs][1][:__typename]).to eq "AddressChamp" } it { expect(data[:dossier][:champs][1][:__typename]).to eq "AddressChamp" }
end end
describe 'dossier with user' do
let(:dossier) { create(:dossier, :en_construction) }
let(:query) { DOSSIER_WITH_USAGER_QUERY }
let(:variables) { { number: dossier.id } }
it { expect(data[:dossier][:usager]).not_to be_nil }
end
describe 'dossier with deleted user' do
let(:dossier) { create(:dossier, :en_construction) }
let(:query) { DOSSIER_WITH_USAGER_QUERY }
let(:variables) { { number: dossier.id } }
let(:email) { dossier.user.email }
before do
dossier.update(user_id: nil, deleted_user_email_never_send: email)
end
it {
expect(data[:dossier][:usager]).not_to be_nil
expect(data[:dossier][:usager][:email]).to eq(email)
expect(data[:dossier][:usager][:id]).to eq('<deleted>')
}
end
DOSSIER_QUERY = <<-GRAPHQL DOSSIER_QUERY = <<-GRAPHQL
query($number: Int!) { query($number: Int!) {
dossier(number: $number) { dossier(number: $number) {
@ -65,6 +90,19 @@ RSpec.describe Types::DossierType, type: :graphql do
} }
GRAPHQL GRAPHQL
DOSSIER_WITH_USAGER_QUERY = <<-GRAPHQL
query($number: Int!) {
dossier(number: $number) {
id
number
usager {
id
email
}
}
}
GRAPHQL
DOSSIER_WITH_ATTESTATION_QUERY = <<-GRAPHQL DOSSIER_WITH_ATTESTATION_QUERY = <<-GRAPHQL
query($number: Int!) { query($number: Int!) {
dossier(number: $number) { dossier(number: $number) {

View file

@ -602,5 +602,25 @@ describe Champ do
expect(champ.reload.data).to eq data expect(champ.reload.data).to eq data
end end
end end
context "#input_name" do
let(:champ) { create(:champ_text) }
it { expect(champ.input_name).to eq "dossier[champs_attributes][#{champ.id}]" }
context "when private" do
let(:champ) { create(:champ_text, private: true) }
it { expect(champ.input_name).to eq "dossier[champs_private_attributes][#{champ.id}]" }
end
context "when has parent" do
let(:champ) { create(:champ_text, parent: create(:champ_text)) }
it { expect(champ.input_name).to eq "dossier[champs_attributes][#{champ.parent_id}][champs_attributes][#{champ.id}]" }
end
context "when has private parent" do
let(:champ) { create(:champ_text, private: true, parent: create(:champ_text, private: true)) }
it { expect(champ.input_name).to eq "dossier[champs_private_attributes][#{champ.parent_id}][champs_attributes][#{champ.id}]" }
end
end
end end
end end

View file

@ -16,6 +16,16 @@ describe 'Dossier details:' do
expect(page).to have_text(dossier.commentaires.last.body) expect(page).to have_text(dossier.commentaires.last.body)
end end
context 'when the deposit receipt feature is enabled' do
before { Flipper.enable(:procedure_dossier_papertrail, procedure) }
after { Flipper.disable(:procedure_dossier_papertrail, procedure) }
it 'displays a link to download a deposit receipt' do
visit dossier_path(dossier)
expect(page).to have_link("Obtenir une attestation de dépôt de dossier", href: %r{dossiers/#{dossier.id}/papertrail.pdf})
end
end
describe "the user can see the mean time they are expected to wait" do describe "the user can see the mean time they are expected to wait" do
let(:other_dossier) { create(:dossier, :accepte, :with_individual, procedure: procedure, depose_at: 10.days.ago, en_instruction_at: 9.days.ago, processed_at: Time.zone.now) } let(:other_dossier) { create(:dossier, :accepte, :with_individual, procedure: procedure, depose_at: 10.days.ago, en_instruction_at: 9.days.ago, processed_at: Time.zone.now) }

View file

@ -0,0 +1,25 @@
describe 'users/dossiers/papertrail.pdf.prawn', type: :view do
before do
assign(:dossier, dossier)
end
subject { render }
context 'for a dossier with an individual' do
let(:dossier) { create(:dossier, :en_construction, :with_service, :with_individual) }
it 'renders a PDF document with the dossier state' do
subject
expect(rendered).to be_present
end
end
context 'for a dossier with a SIRET' do
let(:dossier) { create(:dossier, :en_construction, :with_service, :with_entreprise) }
it 'renders a PDF document with the dossier state' do
subject
expect(rendered).to be_present
end
end
end