2019-05-14-01 (#3865)

2019-05-14-01
This commit is contained in:
Pierre de La Morinerie 2019-05-14 16:02:09 +02:00 committed by GitHub
commit 2b922934c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 323 additions and 53 deletions

View file

@ -29,6 +29,9 @@ module CreateAvisConcern
if persisted.any? if persisted.any?
sent_emails_addresses = persisted.map(&:email_to_display).join(", ") sent_emails_addresses = persisted.map(&:email_to_display).join(", ")
flash.notice = "Une demande d'avis a été envoyée à #{sent_emails_addresses}" flash.notice = "Une demande d'avis a été envoyée à #{sent_emails_addresses}"
persisted.each do |avis|
dossier.demander_un_avis!(avis)
end
end end
if failed.any? if failed.any?

View file

@ -136,8 +136,8 @@ module Gestionnaires
def update_annotations def update_annotations
dossier = current_gestionnaire.dossiers.includes(champs_private: :type_de_champ).find(params[:dossier_id]) dossier = current_gestionnaire.dossiers.includes(champs_private: :type_de_champ).find(params[:dossier_id])
# FIXME: add attachements validation, cf. Champ#piece_justificative_file_errors
dossier.update(champs_private_params) dossier.update(champs_private_params)
dossier.modifier_annotations!(current_gestionnaire)
redirect_to annotations_privees_gestionnaire_dossier_path(procedure, dossier) redirect_to annotations_privees_gestionnaire_dossier_path(procedure, dossier)
end end

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { sortableElement, sortableHandle } from 'react-sortable-hoc'; import { sortableElement, sortableHandle } from 'react-sortable-hoc';
import { useInView } from 'react-intersection-observer';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import DescriptionInput from './DescriptionInput'; import DescriptionInput from './DescriptionInput';
@ -29,6 +30,10 @@ const TypeDeChamp = sortableElement(
const canBeMandatory = const canBeMandatory =
!isHeaderSection && !isExplication && !state.isAnnotation; !isHeaderSection && !isExplication && !state.isAnnotation;
const [ref, inView] = useInView({
threshold: [0.6]
});
const updateHandlers = createUpdateHandlers( const updateHandlers = createUpdateHandlers(
dispatch, dispatch,
typeDeChamp, typeDeChamp,
@ -42,8 +47,10 @@ const TypeDeChamp = sortableElement(
return ( return (
<div <div
ref={isLastItem ? state.lastTypeDeChampRef : null} ref={ref}
data-index={index} data-index={index}
data-in-view={inView ? true : undefined}
data-repetition={isRepetition ? true : undefined}
className={`type-de-champ form flex column justify-start ${ className={`type-de-champ form flex column justify-start ${
isHeaderSection ? 'type-header-section' : '' isHeaderSection ? 'type-header-section' : ''
}`} }`}

View file

@ -1,4 +1,4 @@
import React, { useReducer, useRef } from 'react'; import React, { useReducer } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -11,11 +11,7 @@ function TypeDeChampRepetitionOptions({
state: parentState, state: parentState,
typeDeChamp typeDeChamp
}) { }) {
const lastTypeDeChampRef = useRef(null); const [state, dispatch] = useReducer(typeDeChampsReducer, parentState);
const [state, dispatch] = useReducer(typeDeChampsReducer, {
...parentState,
lastTypeDeChampRef
});
if (isVisible) { if (isVisible) {
return ( return (

View file

@ -1,4 +1,4 @@
import React, { useReducer, useRef } from 'react'; import React, { useReducer } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@ -7,10 +7,8 @@ import TypeDeChamp from './TypeDeChamp';
import typeDeChampsReducer from '../typeDeChampsReducer'; import typeDeChampsReducer from '../typeDeChampsReducer';
function TypeDeChamps({ state: rootState, typeDeChamps }) { function TypeDeChamps({ state: rootState, typeDeChamps }) {
const lastTypeDeChampRef = useRef(null);
const [state, dispatch] = useReducer(typeDeChampsReducer, { const [state, dispatch] = useReducer(typeDeChampsReducer, {
...rootState, ...rootState,
lastTypeDeChampRef,
typeDeChamps typeDeChamps
}); });

View file

@ -37,30 +37,52 @@ export default function typeDeChampsReducer(state, { type, params, done }) {
} }
} }
function addNewTypeDeChamp(state, typeDeChamps, done) { function addTypeDeChamp(state, typeDeChamps, insertAfter, done) {
const typeDeChamp = { const typeDeChamp = {
...state.defaultTypeDeChampAttributes, ...state.defaultTypeDeChampAttributes,
order_place: typeDeChamps.length order_place: typeDeChamps.length
}; };
createTypeDeChampOperation(typeDeChamp, state.queue) createTypeDeChampOperation(typeDeChamp, state.queue)
.then(() => { .then(async () => {
if (insertAfter) {
// Move the champ to the correct position server-side
await moveTypeDeChampOperation(
typeDeChamp,
insertAfter.index,
state.queue
);
}
state.flash.success(); state.flash.success();
done(); done();
if (state.lastTypeDeChampRef) { if (insertAfter) {
scrollToComponent(state.lastTypeDeChampRef.current); scrollToComponent(insertAfter.target.nextElementSibling);
} }
}) })
.catch(message => state.flash.error(message)); .catch(message => state.flash.error(message));
let newTypeDeChamps = [...typeDeChamps, typeDeChamp];
if (insertAfter) {
// Move the champ to the correct position client-side
newTypeDeChamps = arrayMove(
newTypeDeChamps,
typeDeChamps.length,
insertAfter.index
);
}
return { return {
...state, ...state,
typeDeChamps: [...typeDeChamps, typeDeChamp] typeDeChamps: newTypeDeChamps
}; };
} }
function addNewTypeDeChamp(state, typeDeChamps, done) {
return addTypeDeChamp(state, typeDeChamps, findItemToInsertAfter(), done);
}
function addNewRepetitionTypeDeChamp(state, typeDeChamps, typeDeChamp, done) { function addNewRepetitionTypeDeChamp(state, typeDeChamps, typeDeChamp, done) {
return addNewTypeDeChamp( return addTypeDeChamp(
{ {
...state, ...state,
defaultTypeDeChampAttributes: { defaultTypeDeChampAttributes: {
@ -69,6 +91,7 @@ function addNewRepetitionTypeDeChamp(state, typeDeChamps, typeDeChamp, done) {
} }
}, },
typeDeChamps, typeDeChamps,
null,
done done
); );
} }
@ -182,3 +205,23 @@ function getUpdateHandler(typeDeChamp, { queue, flash }) {
} }
return handler; return handler;
} }
function findItemToInsertAfter() {
const target = getFirstTarget();
return {
target,
index: parseInt(target.dataset.index) + 1
};
}
function getFirstTarget() {
const [target] = document.querySelectorAll('[data-in-view]');
if (target) {
const parentTarget = target.closest('[data-repetition]');
if (parentTarget) {
return parentTarget;
}
return target;
}
}

View file

@ -5,3 +5,7 @@ import '@babel/polyfill';
import 'dom4'; import 'dom4';
import './polyfills/insertAdjacentElement'; import './polyfills/insertAdjacentElement';
import './polyfills/dataset'; import './polyfills/dataset';
if (typeof window.IntersectionObserver === 'undefined') {
import('intersection-observer');
}

View file

@ -288,7 +288,7 @@ class Dossier < ApplicationRecord
def passer_automatiquement_en_instruction! def passer_automatiquement_en_instruction!
en_instruction! en_instruction!
log_dossier_operation(nil, :passer_en_instruction, automatic_operation: true) log_automatic_dossier_operation(:passer_en_instruction)
end end
def repasser_en_construction!(gestionnaire) def repasser_en_construction!(gestionnaire)
@ -311,7 +311,7 @@ class Dossier < ApplicationRecord
end end
NotificationMailer.send_closed_notification(self).deliver_later NotificationMailer.send_closed_notification(self).deliver_later
log_dossier_operation(gestionnaire, :accepter) log_dossier_operation(gestionnaire, :accepter, self)
end end
def accepter_automatiquement! def accepter_automatiquement!
@ -324,14 +324,14 @@ class Dossier < ApplicationRecord
end end
NotificationMailer.send_closed_notification(self).deliver_later NotificationMailer.send_closed_notification(self).deliver_later
log_dossier_operation(nil, :accepter, automatic_operation: true) log_automatic_dossier_operation(:accepter, self)
end end
def hide!(administration) def hide!(administration)
update(hidden_at: Time.zone.now) update(hidden_at: Time.zone.now)
log_administration_dossier_operation(administration, :supprimer)
DeletedDossier.create_from_dossier(self) DeletedDossier.create_from_dossier(self)
log_dossier_operation(administration, :supprimer, self)
end end
def refuser!(gestionnaire, motivation, justificatif = nil) def refuser!(gestionnaire, motivation, justificatif = nil)
@ -343,7 +343,7 @@ class Dossier < ApplicationRecord
refuse! refuse!
NotificationMailer.send_refused_notification(self).deliver_later NotificationMailer.send_refused_notification(self).deliver_later
log_dossier_operation(gestionnaire, :refuser) log_dossier_operation(gestionnaire, :refuser, self)
end end
def classer_sans_suite!(gestionnaire, motivation, justificatif = nil) def classer_sans_suite!(gestionnaire, motivation, justificatif = nil)
@ -355,7 +355,7 @@ class Dossier < ApplicationRecord
sans_suite! sans_suite!
NotificationMailer.send_without_continuation_notification(self).deliver_later NotificationMailer.send_without_continuation_notification(self).deliver_later
log_dossier_operation(gestionnaire, :classer_sans_suite) log_dossier_operation(gestionnaire, :classer_sans_suite, self)
end end
def check_mandatory_champs def check_mandatory_champs
@ -366,20 +366,33 @@ class Dossier < ApplicationRecord
end end
end end
def modifier_annotations!(gestionnaire)
champs_private.select(&:value_previously_changed?).each do |champ|
log_dossier_operation(gestionnaire, :modifier_annotation, champ)
end
end
def demander_un_avis!(avis)
log_dossier_operation(avis.claimant, :demander_un_avis, avis)
end
private private
def log_dossier_operation(gestionnaire, operation, automatic_operation: false) def log_dossier_operation(author, operation, subject = nil)
dossier_operation_logs.create( DossierOperationLog.create_and_serialize(
gestionnaire: gestionnaire, dossier: self,
operation: DossierOperationLog.operations.fetch(operation), operation: DossierOperationLog.operations.fetch(operation),
automatic_operation: automatic_operation author: author,
subject: subject
) )
end end
def log_administration_dossier_operation(administration, operation) def log_automatic_dossier_operation(operation, subject = nil)
dossier_operation_logs.create( DossierOperationLog.create_and_serialize(
administration: administration, dossier: self,
operation: DossierOperationLog.operations.fetch(operation) operation: DossierOperationLog.operations.fetch(operation),
automatic_operation: true,
subject: subject
) )
end end

View file

@ -5,10 +5,76 @@ class DossierOperationLog < ApplicationRecord
accepter: 'accepter', accepter: 'accepter',
refuser: 'refuser', refuser: 'refuser',
classer_sans_suite: 'classer_sans_suite', classer_sans_suite: 'classer_sans_suite',
supprimer: 'supprimer' supprimer: 'supprimer',
modifier_annotation: 'modifier_annotation',
demander_un_avis: 'demander_un_avis'
} }
belongs_to :dossier belongs_to :dossier
belongs_to :gestionnaire has_one_attached :serialized
belongs_to :administration
def self.create_and_serialize(params)
dossier = params.fetch(:dossier)
duree_conservation_dossiers = dossier.procedure.duree_conservation_dossiers_dans_ds
keep_until = if duree_conservation_dossiers.present?
if dossier.en_instruction_at
dossier.en_instruction_at + duree_conservation_dossiers.months
else
dossier.created_at + duree_conservation_dossiers.months
end
end
operation_log = new(operation: params.fetch(:operation),
dossier_id: dossier.id,
keep_until: keep_until,
executed_at: Time.zone.now,
automatic_operation: !!params[:automatic_operation])
serialized = {
operation: operation_log.operation,
dossier_id: operation_log.dossier_id,
author: self.serialize_author(params[:author]),
subject: self.serialize_subject(params[:subject]),
automatic_operation: operation_log.automatic_operation?,
executed_at: operation_log.executed_at.iso8601
}.compact.to_json
operation_log.digest = Digest::SHA256.hexdigest(serialized)
operation_log.serialized.attach(
io: StringIO.new(serialized),
filename: "operation-#{operation_log.digest}.json",
content_type: 'application/json',
# we don't want to run virus scanner on this file
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
)
operation_log.save!
end
def self.serialize_author(author)
if author.nil?
nil
else
OperationAuthorSerializer.new(author).as_json
end
end
def self.serialize_subject(subject)
if subject.nil?
nil
elsif !Flipflop.operation_log_serialize_subject?
{ id: subject.id }
else
case subject
when Dossier
DossierSerializer.new(subject).as_json
when Champ
ChampSerializer.new(subject).as_json
when Avis
AvisSerializer.new(subject).as_json
end
end
end
end end

View file

@ -0,0 +1,19 @@
class AvisSerializer < ActiveModel::Serializer
attributes :email,
:answer,
:introduction,
:created_at,
:answered_at
def email
object.email_to_display
end
def created_at
object.created_at&.in_time_zone('UTC')
end
def answered_at
object.updated_at&.in_time_zone('UTC')
end
end

View file

@ -0,0 +1,18 @@
class OperationAuthorSerializer < ActiveModel::Serializer
attributes :id, :email
def id
case object
when User
"Usager##{object.id}"
when Gestionnaire
"Instructeur##{object.id}"
when Administrateur
"Administrateur##{object.id}"
when Administration
"Manager##{object.id}"
else
nil
end
end
end

View file

@ -30,7 +30,7 @@
%li Texte de loi (loi, décret, circulaire, arrêté,…) %li Texte de loi (loi, décret, circulaire, arrêté,…)
%li Texte juridique (statuts, délibération, décision du conseil d'administration…) %li Texte juridique (statuts, délibération, décision du conseil d'administration…)
%li %li
= link_to("En savoir plus", CADRE_JURIDIQUE_URL, target: "_blank", rel: "noopener") = link_to("En savoir plus avec cette vidéo de 5 minutes", CADRE_JURIDIQUE_URL, target: "_blank", rel: "noopener")
%p.help-block %p.help-block
%i.fa.fa-info-circle %i.fa.fa-info-circle

View file

@ -31,6 +31,13 @@
%br %br
.alert.alert-info .alert.alert-info
Attention, diffusez toujours le <strong>lien complet</strong> affiché ci-dessus, et non pas un lien générique vers demarches-simplifiees.fr. Ne dites pas non plus aux usagers de se rendre sur le site générique demarches-simplifiees.fr, donnez-leur toujours le lien complet. Attention, diffusez toujours le <strong>lien complet</strong> affiché ci-dessus, et non pas un lien générique vers demarches-simplifiees.fr. Ne dites pas non plus aux usagers de se rendre sur le site générique demarches-simplifiees.fr, donnez-leur toujours le lien complet.
%br
%br
Prenez quelques minutes pour savoir comment établir une bonne relation avec les usagers de votre démarche :
= link_to "Regarder la vidéo de 5 minutes",
"https://vimeo.com/334463514",
target: "_blank"
#path-messages #path-messages
#path_is_mine.text-warning.center.message #path_is_mine.text-warning.center.message
Ce lien est déjà utilisé par une de vos démarche. Ce lien est déjà utilisé par une de vos démarche.

View file

@ -2,9 +2,35 @@
- if current_administrateur.procedures.brouillons.count == 0 - if current_administrateur.procedures.brouillons.count == 0
.card.feedback .card.feedback
.card-title .card-title
Bienvenue, Bienvenue,
%br
vous allez pouvoir créer une première démarche de test. vous allez pouvoir créer une première démarche de test.
Celle-ci sera visible uniquement par vous et ne sera publiée nulle part, alors pas de crainte à avoir. Celle-ci sera visible uniquement par vous et ne sera publiée nulle part, alors pas de crainte à avoir.
%br
%br
Besoin d'aide ?
%br
> Nous proposons des ateliers en ligne pour vous aider à créer votre 1er formulaire et répondre à vos questions :
= link_to "inscrivez-vous ici",
"https://vimeo.com/334463514",
target: "_blank"
%br
> Vous pouvez
= link_to "visionner cette vidéo",
"https://vimeo.com/261478872",
target: "_blank"
%br
> Vous pouvez lire notre
= link_to "documentation en ligne",
"https://doc.demarches-simplifiees.fr/tutoriels/tutoriel-administrateur",
target: "_blank"
%br
> Vous pouvez enfin
= link_to "prendre un rendez-vous téléphonique avec nous",
"https://calendly.com/demarches-simplifiees/accompagnement-administrateur-demarches-simplifiees-fr",
target: "_blank"
.form .form
.send-wrapper .send-wrapper

View file

@ -81,6 +81,11 @@
%p %p
Une fois que vous êtes prêt à publier définitivement votre démarche, cliquez sur le bouton "Publier" pour choisir le lien définitif de votre démarche, les modifications sur la démarches ne seront alors plus possibles. Une fois que vous êtes prêt à publier définitivement votre démarche, cliquez sur le bouton "Publier" pour choisir le lien définitif de votre démarche, les modifications sur la démarches ne seront alors plus possibles.
%br
%h4 Prenez quelques minutes pour savoir comment établir une bonne relation avec les usagers de votre démarche:
%p.center
%br
%iframe{ :src =>"https://player.vimeo.com/video/334463514?color=0069CC",:width =>"640",:height =>"360",:frameborder => "0" }
- else - else
.alert.alert-info .alert.alert-info
Pour pouvoir tester cette démarche, vous devez dabord lui affecter Pour pouvoir tester cette démarche, vous devez dabord lui affecter

View file

@ -13,4 +13,6 @@
\- \-
= link_to 'Documentation', DOC_URL = link_to 'Documentation', DOC_URL
\- \-
= link_to 'Aide', FAQ_URL = link_to 'FAQ', FAQ_ADMIN_URL
\-
= link_to 'Inscription ateliers en ligne', WEBINAIRE_URL

View file

@ -10,7 +10,7 @@
#navbar-body #navbar-body
.row .row
%div{ style: "vertical-align: middle;float:left;position:absolute;line-height: 60px;z-index:2;" } %div{ style: "vertical-align: middle;float:left;position:absolute;line-height: 60px;z-index:2;" }
Besoin d'aide ? <a href="tel:#{CONTACT_PHONE}">#{CONTACT_PHONE}</a> ou <a href="#{contact_admin_path}" target="_blank" rel="noopener">par email</a> Besoin d'aide? <a href="tel:#{CONTACT_PHONE}">#{CONTACT_PHONE}</a> ou <a href="#{contact_admin_path}" target="_blank" rel="noopener">email</a> ou <a target="_blank" rel="noopener" href="https://calendly.com/demarches-simplifiees/accompagnement-administrateur-demarches-simplifiees-fr">prenez rendez-vous avec nous</a>
-# BEST WTF EVER -# BEST WTF EVER
-# this begin rescue hides potentials bugs by displaying another navbar -# this begin rescue hides potentials bugs by displaying another navbar
- begin - begin

View file

@ -11,7 +11,7 @@ by providing a `content_for(:javascript)` block.
<%= javascript_include_tag js_path %> <%= javascript_include_tag js_path %>
<% end %> <% end %>
<%= javascript_pack_tag 'manager' %> <%= javascript_packs_with_chunks_tag 'manager' %>
<%= yield :javascript %> <%= yield :javascript %>

View file

@ -17,6 +17,8 @@ Flipflop.configure do
feature :enable_email_login_token feature :enable_email_login_token
feature :new_champs_editor feature :new_champs_editor
feature :operation_log_serialize_subject
group :production do group :production do
feature :remote_storage, feature :remote_storage,
default: ENV['FOG_ENABLED'] == 'enabled' default: ENV['FOG_ENABLED'] == 'enabled'

View file

@ -15,13 +15,14 @@ FOG_BASE_URL = "https://static.demarches-simplifiees.fr"
DOC_URL = "https://doc.demarches-simplifiees.fr" DOC_URL = "https://doc.demarches-simplifiees.fr"
ADMINISTRATEUR_TUTORIAL_URL = [DOC_URL, "tutoriels", "tutoriel-administrateur"].join("/") ADMINISTRATEUR_TUTORIAL_URL = [DOC_URL, "tutoriels", "tutoriel-administrateur"].join("/")
INSTRUCTEUR_TUTORIAL_URL = [DOC_URL, "tutoriels", "tutoriel-accompagnateur"].join("/") INSTRUCTEUR_TUTORIAL_URL = [DOC_URL, "tutoriels", "tutoriel-accompagnateur"].join("/")
CADRE_JURIDIQUE_URL = [ADMINISTRATEUR_TUTORIAL_URL, "cadre-juridique"].join("#") CADRE_JURIDIQUE_URL = [DOC_URL, "tutoriels/video-le-cadre-juridique"].join("/")
WEBINAIRE_URL = [DOC_URL, "pour-aller-plus-loin", "webinaires"].join("/") WEBINAIRE_URL = "https://app.livestorm.co/demarches-simplifiees"
LISTE_DES_DEMARCHES_URL = [DOC_URL, "listes-des-demarches"].join("/") LISTE_DES_DEMARCHES_URL = [DOC_URL, "listes-des-demarches"].join("/")
CGU_URL = [DOC_URL, "cgu"].join("/") CGU_URL = [DOC_URL, "cgu"].join("/")
MENTIONS_LEGALES_URL = [CGU_URL, "4-mentions-legales"].join("#") MENTIONS_LEGALES_URL = [CGU_URL, "4-mentions-legales"].join("#")
API_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "api"].join("/") API_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "api"].join("/")
WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/") WEBHOOK_DOC_URL = [DOC_URL, "pour-aller-plus-loin", "webhook"].join("/")
FAQ_URL = "https://faq.demarches-simplifiees.fr" FAQ_URL = "https://faq.demarches-simplifiees.fr"
FAQ_ADMIN_URL = "https://faq.demarches-simplifiees.fr/collection/1-administrateur"
STATUS_PAGE_URL = "https://status.demarches-simplifiees.fr" STATUS_PAGE_URL = "https://status.demarches-simplifiees.fr"
MATOMO_IFRAME_URL = "https://stats.data.gouv.fr/index.php?module=CoreAdminHome&action=optOut&language=fr&&fontColor=333333&fontSize=16px&fontFamily=Muli" MATOMO_IFRAME_URL = "https://stats.data.gouv.fr/index.php?module=CoreAdminHome&action=optOut&language=fr&&fontColor=333333&fontSize=16px&fontFamily=Muli"

View file

@ -0,0 +1,8 @@
class AddDigestAndTimestampsToDossierOperationLogs < ActiveRecord::Migration[5.2]
def change
add_column :dossier_operation_logs, :keep_until, :datetime
add_column :dossier_operation_logs, :executed_at, :datetime
add_column :dossier_operation_logs, :digest, :text
add_index :dossier_operation_logs, :keep_until
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_03_27_102357) do ActiveRecord::Schema.define(version: 2019_03_27_102360) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -221,9 +221,13 @@ ActiveRecord::Schema.define(version: 2019_03_27_102357) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "automatic_operation", default: false, null: false t.boolean "automatic_operation", default: false, null: false
t.bigint "administration_id" t.bigint "administration_id"
t.datetime "keep_until"
t.datetime "executed_at"
t.text "digest"
t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_id" t.index ["administration_id"], name: "index_dossier_operation_logs_on_administration_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 ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id" t.index ["gestionnaire_id"], name: "index_dossier_operation_logs_on_gestionnaire_id"
t.index ["keep_until"], name: "index_dossier_operation_logs_on_keep_until"
end end
create_table "dossiers", id: :serial, force: :cascade do |t| create_table "dossiers", id: :serial, force: :cascade do |t|

View file

@ -15,6 +15,7 @@
"debounce": "^1.2.0", "debounce": "^1.2.0",
"dom4": "^2.1.4", "dom4": "^2.1.4",
"highcharts": "^6.1.2", "highcharts": "^6.1.2",
"intersection-observer": "^0.6.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"leaflet": "^1.4.0", "leaflet": "^1.4.0",
"leaflet-freedraw": "^2.10.0", "leaflet-freedraw": "^2.10.0",
@ -23,6 +24,7 @@
"ramda": "=0.24.1", "ramda": "=0.24.1",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-intersection-observer": "^8.23.0",
"react-scroll-to-component": "^1.0.2", "react-scroll-to-component": "^1.0.2",
"react-sortable-hoc": "^1.7.1", "react-sortable-hoc": "^1.7.1",
"react_ujs": "^2.5.0", "react_ujs": "^2.5.0",

View file

@ -27,6 +27,7 @@ RSpec.describe AutoArchiveProcedureJob, type: :job do
let!(:dossier7) { create(:dossier, procedure: procedure_hier, state: Dossier.states.fetch(:refuse), archived: false) } let!(:dossier7) { create(:dossier, procedure: procedure_hier, state: Dossier.states.fetch(:refuse), archived: false) }
let!(:dossier8) { create(:dossier, procedure: procedure_hier, state: Dossier.states.fetch(:sans_suite), archived: false) } let!(:dossier8) { create(:dossier, procedure: procedure_hier, state: Dossier.states.fetch(:sans_suite), archived: false) }
let!(:dossier9) { create(:dossier, procedure: procedure_aujourdhui, state: Dossier.states.fetch(:en_construction), archived: false) } let!(:dossier9) { create(:dossier, procedure: procedure_aujourdhui, state: Dossier.states.fetch(:en_construction), archived: false) }
let(:last_operation) { dossier2.dossier_operation_logs.last }
before do before do
subject subject
@ -40,7 +41,8 @@ RSpec.describe AutoArchiveProcedureJob, type: :job do
it { it {
expect(dossier1.state).to eq Dossier.states.fetch(:brouillon) expect(dossier1.state).to eq Dossier.states.fetch(:brouillon)
expect(dossier2.state).to eq Dossier.states.fetch(:en_instruction) expect(dossier2.state).to eq Dossier.states.fetch(:en_instruction)
expect(dossier2.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) expect(last_operation.operation).to eq('passer_en_instruction')
expect(last_operation.automatic_operation?).to be_truthy
expect(dossier3.state).to eq Dossier.states.fetch(:en_instruction) expect(dossier3.state).to eq Dossier.states.fetch(:en_instruction)
expect(dossier4.state).to eq Dossier.states.fetch(:en_instruction) expect(dossier4.state).to eq Dossier.states.fetch(:en_instruction)
expect(dossier5.state).to eq Dossier.states.fetch(:en_instruction) expect(dossier5.state).to eq Dossier.states.fetch(:en_instruction)

View file

@ -31,11 +31,13 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do
context "with some dossiers" do context "with some dossiers" do
context "en_construction" do context "en_construction" do
let(:state) { Dossier.states.fetch(:en_instruction) } let(:state) { Dossier.states.fetch(:en_instruction) }
let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last }
it { it {
expect(nouveau_dossier1.en_instruction?).to be true expect(nouveau_dossier1.en_instruction?).to be true
expect(nouveau_dossier1.en_instruction_at).to eq(date) expect(nouveau_dossier1.en_instruction_at).to eq(date)
expect(nouveau_dossier1.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) expect(last_operation.operation).to eq('passer_en_instruction')
expect(last_operation.automatic_operation?).to be_truthy
expect(nouveau_dossier2.en_instruction?).to be true expect(nouveau_dossier2.en_instruction?).to be true
expect(nouveau_dossier2.en_instruction_at).to eq(date) expect(nouveau_dossier2.en_instruction_at).to eq(date)
@ -50,13 +52,15 @@ RSpec.describe AutoReceiveDossiersForProcedureJob, type: :job do
context "accepte" do context "accepte" do
let(:state) { Dossier.states.fetch(:accepte) } let(:state) { Dossier.states.fetch(:accepte) }
let(:last_operation) { nouveau_dossier1.dossier_operation_logs.last }
it { it {
expect(nouveau_dossier1.accepte?).to be true expect(nouveau_dossier1.accepte?).to be true
expect(nouveau_dossier1.en_instruction_at).to eq(date) expect(nouveau_dossier1.en_instruction_at).to eq(date)
expect(nouveau_dossier1.processed_at).to eq(date) expect(nouveau_dossier1.processed_at).to eq(date)
expect(nouveau_dossier1.attestation).to be_present expect(nouveau_dossier1.attestation).to be_present
expect(nouveau_dossier1.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'accepter', true]]) expect(last_operation.operation).to eq('accepter')
expect(last_operation.automatic_operation?).to be_truthy
expect(nouveau_dossier2.accepte?).to be true expect(nouveau_dossier2.accepte?).to be true
expect(nouveau_dossier2.en_instruction_at).to eq(date) expect(nouveau_dossier2.en_instruction_at).to eq(date)

View file

@ -764,6 +764,8 @@ describe Dossier do
describe '#accepter!' do describe '#accepter!' do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let!(:gestionnaire) { create(:gestionnaire) } let!(:gestionnaire) { create(:gestionnaire) }
let!(:now) { Time.zone.parse('01/01/2100') } let!(:now) { Time.zone.parse('01/01/2100') }
let(:attestation) { Attestation.new } let(:attestation) { Attestation.new }
@ -783,13 +785,18 @@ describe Dossier do
it { expect(dossier.en_instruction_at).to eq(now) } it { expect(dossier.en_instruction_at).to eq(now) }
it { expect(dossier.processed_at).to eq(now) } it { expect(dossier.processed_at).to eq(now) }
it { expect(dossier.state).to eq('accepte') } it { expect(dossier.state).to eq('accepte') }
it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[gestionnaire.id, 'accepter', false]]) } it { expect(last_operation.operation).to eq('accepter') }
it { expect(last_operation.automatic_operation?).to be_falsey }
it { expect(operation_serialized['operation']).to eq('accepter') }
it { expect(operation_serialized['dossier_id']).to eq(dossier.id) }
it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) }
it { expect(NotificationMailer).to have_received(:send_closed_notification).with(dossier) } it { expect(NotificationMailer).to have_received(:send_closed_notification).with(dossier) }
it { expect(dossier.attestation).to eq(attestation) } it { expect(dossier.attestation).to eq(attestation) }
end end
describe '#accepter_automatiquement!' do describe '#accepter_automatiquement!' do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let!(:now) { Time.zone.parse('01/01/2100') } let!(:now) { Time.zone.parse('01/01/2100') }
let(:attestation) { Attestation.new } let(:attestation) { Attestation.new }
@ -808,30 +815,43 @@ describe Dossier do
it { expect(dossier.en_instruction_at).to eq(now) } it { expect(dossier.en_instruction_at).to eq(now) }
it { expect(dossier.processed_at).to eq(now) } it { expect(dossier.processed_at).to eq(now) }
it { expect(dossier.state).to eq('accepte') } it { expect(dossier.state).to eq('accepte') }
it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'accepter', true]]) } it { expect(last_operation.operation).to eq('accepter') }
it { expect(last_operation.automatic_operation?).to be_truthy }
it { expect(NotificationMailer).to have_received(:send_closed_notification).with(dossier) } it { expect(NotificationMailer).to have_received(:send_closed_notification).with(dossier) }
it { expect(dossier.attestation).to eq(attestation) } it { expect(dossier.attestation).to eq(attestation) }
end end
describe '#passer_en_instruction!' do describe '#passer_en_instruction!' do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let(:gestionnaire) { create(:gestionnaire) } let(:gestionnaire) { create(:gestionnaire) }
before { dossier.passer_en_instruction!(gestionnaire) } before { dossier.passer_en_instruction!(gestionnaire) }
it { expect(dossier.state).to eq('en_instruction') } it { expect(dossier.state).to eq('en_instruction') }
it { expect(dossier.followers_gestionnaires).to include(gestionnaire) } it { expect(dossier.followers_gestionnaires).to include(gestionnaire) }
it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation)).to match([[gestionnaire.id, 'passer_en_instruction']]) } it { expect(last_operation.operation).to eq('passer_en_instruction') }
it { expect(last_operation.automatic_operation?).to be_falsey }
it { expect(operation_serialized['operation']).to eq('passer_en_instruction') }
it { expect(operation_serialized['dossier_id']).to eq(dossier.id) }
it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) }
end end
describe '#passer_automatiquement_en_instruction!' do describe '#passer_automatiquement_en_instruction!' do
let(:dossier) { create(:dossier) } let(:dossier) { create(:dossier) }
let(:last_operation) { dossier.dossier_operation_logs.last }
let(:operation_serialized) { JSON.parse(last_operation.serialized.download) }
let(:gestionnaire) { create(:gestionnaire) } let(:gestionnaire) { create(:gestionnaire) }
before { dossier.passer_automatiquement_en_instruction! } before { dossier.passer_automatiquement_en_instruction! }
it { expect(dossier.followers_gestionnaires).not_to include(gestionnaire) } it { expect(dossier.followers_gestionnaires).not_to include(gestionnaire) }
it { expect(dossier.dossier_operation_logs.pluck(:gestionnaire_id, :operation, :automatic_operation)).to match([[nil, 'passer_en_instruction', true]]) } it { expect(last_operation.operation).to eq('passer_en_instruction') }
it { expect(last_operation.automatic_operation?).to be_truthy }
it { expect(operation_serialized['operation']).to eq('passer_en_instruction') }
it { expect(operation_serialized['dossier_id']).to eq(dossier.id) }
it { expect(operation_serialized['executed_at']).to eq(last_operation.executed_at.iso8601) }
end end
describe "#check_mandatory_champs" do describe "#check_mandatory_champs" do
@ -934,6 +954,6 @@ describe Dossier do
it { expect(dossier.hidden_at).to eq(Time.zone.now) } it { expect(dossier.hidden_at).to eq(Time.zone.now) }
it { expect(last_operation.operation).to eq('supprimer') } it { expect(last_operation.operation).to eq('supprimer') }
it { expect(last_operation.administration).to eq(administration) } it { expect(last_operation.automatic_operation?).to be_falsey }
end end
end end

View file

@ -688,6 +688,13 @@
"@babel/plugin-transform-react-jsx-self" "^7.0.0" "@babel/plugin-transform-react-jsx-self" "^7.0.0"
"@babel/plugin-transform-react-jsx-source" "^7.0.0" "@babel/plugin-transform-react-jsx-source" "^7.0.0"
"@babel/runtime@^7.0.0":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d"
integrity sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2": "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.2":
version "7.4.3" version "7.4.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc"
@ -4166,9 +4173,14 @@ internal-ip@^4.2.0:
ipaddr.js "^1.9.0" ipaddr.js "^1.9.0"
interpret@^1.1.0: interpret@^1.1.0:
version "1.2.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== integrity sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=
intersection-observer@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.6.0.tgz#d64aae04211b4cec051537168f5fa670a4acc770"
integrity sha512-WUVAqGJr08yh73XKe1JhylQ9BiBIytrkt8SH5Knu7Uy44ij5cICi6PbVLIbV/D2eIx9LJVkGBo9WF80R4VXJ+w==
invariant@^2.2.2, invariant@^2.2.4: invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4" version "2.2.4"
@ -6737,6 +6749,14 @@ react-dom@^16.8.6:
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.13.6" scheduler "^0.13.6"
react-intersection-observer@^8.23.0:
version "8.23.0"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.23.0.tgz#1533aaf39cc70300ff8ca37e6551d2d68a589cd0"
integrity sha512-kHXfxhGKvVDNkrvmh9CKCnAWvJBigyB7oSDzMXL54weFDwwI4WfTr58YauZ0RRPkGzoD/hYEuzfS1wipXn23fA==
dependencies:
"@babel/runtime" "^7.0.0"
invariant "^2.2.4"
react-is@^16.8.1: react-is@^16.8.1:
version "16.8.6" version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"