commit
0b58bed658
27 changed files with 480 additions and 336 deletions
|
@ -37,12 +37,16 @@ Nous mettons en production au minimum une fois par semaine (et généralement pl
|
||||||
|
|
||||||
demarches-simplifiees.fr est **compliqué à héberger**. Parmi les problématiques que nous rencontrons :
|
demarches-simplifiees.fr est **compliqué à héberger**. Parmi les problématiques que nous rencontrons :
|
||||||
|
|
||||||
- **Sécurité et confidentialité des données** : par nature, demarches-simplifiees.fr traite une quantité importante de données personnelles. La sécurité de l’infrastructure doit être contrôlée et certifiée pour garantir la confidentialité des données. Cela implique par exemple une démarche de mise en conformité avec le [Référentiel Général de Sécurité](https://www.ssi.gouv.fr/entreprise/reglementation/confiance-numerique/le-referentiel-general-de-securite-rgs/), qui est un processus assez lourd.
|
- **Sécurité et confidentialité des données** : par nature, demarches-simplifiees.fr est appelé à traiter des natures de données qui peuvent présenter des caractéristiqus plus ou moins sensibles. La sécurité de l’infrastructure doit être contrôlée et certifiée pour garantir la confidentialité des données. Cela implique par exemple une démarche de mise en conformité avec le [Référentiel Général de Sécurité](https://www.ssi.gouv.fr/entreprise/reglementation/confiance-numerique/le-referentiel-general-de-securite-rgs/), qui est un processus assez lourd.
|
||||||
|
|
||||||
C’est également valable pour le stockage des pièces-jointes, qui sont souvent des documents d’identités dont la confidentialité doit être garantie.
|
C’est également valable pour le stockage des pièces-jointes, qui peuvent la aussi présenter des particularités et des sensibilités dont la confidentialité doit être garantie.
|
||||||
- **Utilisation de services externes** : demarches-simplifiees.fr s’interconnecte à de nombreux services externes : des APIs (API Entreprise, API Carto, la Base Adresse Nationale, etc.) – mais aussi des services pour le stockage externe des pièces-jointes, l’analyse anti-virus ou l’envoi des emails. Le fonctionnement de demarches-simplifiees.fr dépend de la disponibilité de ces services externes.
|
- **Utilisation de services externes** : demarches-simplifiees.fr s’interconnecte à de nombreux services externes : des APIs (API Entreprise, API Carto, la Base Adresse Nationale, etc.) – mais aussi des services pour le stockage externe des pièces-jointes, l’analyse anti-virus ou l’envoi des emails. Le fonctionnement de demarches-simplifiees.fr dépend de la disponibilité de ces services externes.
|
||||||
- **Mises à jour** : le schéma de la base de données change régulièrement. Nous codons également des scripts pour harmoniser les anciennes données. Parfois des modifications ponctuelles sont effectuées sur des démarches anciennes, pour les mettre en conformité avec de nouvelles règles métiers. Nous maintenons également les dépendances logicielles utilisées – notamment en réagissant rapidement lorsqu’une faille de sécurité est signalée. Ces mises à jour fréquentes en production sont indispensables au bon fonctionnement de l’outil.
|
- **Mises à jour** : le schéma de la base de données change régulièrement. Nous codons également des scripts pour harmoniser les anciennes données. Parfois des modifications ponctuelles sont effectuées sur des démarches anciennes, pour les mettre en conformité avec de nouvelles règles métiers. Nous maintenons également les dépendances logicielles utilisées – notamment en réagissant rapidement lorsqu’une faille de sécurité est signalée. Ces mises à jour fréquentes en production sont indispensables au bon fonctionnement de l’outil.
|
||||||
|
|
||||||
Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous recommandons de **proposer vos modifications à la base de code principale** (par exemple en créant une issue) **plutôt que d’héberger une autre instance vous-même**.
|
Si vous souhaitez adapter demarches-simplifiees.fr à votre besoin, nous vous recommandons de **proposer vos modifications à la base de code principale** (par exemple en créant une issue) **plutôt que d’héberger une autre instance vous-même**.
|
||||||
|
|
||||||
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
|
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
|
||||||
|
|
||||||
|
Dans le cas où vous envisagez d’héberger une instance de demarches-simplifiees.fr vous-même, nous n'avons malheureusement pas les moyens de vous accompagner, ni d'assurer de support technique concernant votre installation.
|
||||||
|
|
||||||
|
Totefois, le ministère des armées a déployé une instance au sein de leur intranet. Nous proposons aux acteurs qui sont interessés de les mettre en relation avec eux afin de disposer d'un retour d'expérience, et bénéficier de leur retour.
|
||||||
|
|
|
@ -32,6 +32,10 @@
|
||||||
&.column {
|
&.column {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.row-reverse {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-grow {
|
.flex-grow {
|
||||||
|
|
|
@ -1,49 +1,13 @@
|
||||||
module NewAdministrateur
|
module NewAdministrateur
|
||||||
class ProceduresController < AdministrateurController
|
class ProceduresController < AdministrateurController
|
||||||
before_action :retrieve_procedure, only: [:champs, :annotations, :update]
|
before_action :retrieve_procedure, only: [:champs, :annotations]
|
||||||
before_action :procedure_locked?, only: [:champs, :annotations, :update]
|
before_action :procedure_locked?, only: [:champs, :annotations]
|
||||||
|
|
||||||
TYPE_DE_CHAMP_ATTRIBUTES_BASE = [
|
|
||||||
:_destroy,
|
|
||||||
:libelle,
|
|
||||||
:description,
|
|
||||||
:order_place,
|
|
||||||
:type_champ,
|
|
||||||
:id,
|
|
||||||
:mandatory,
|
|
||||||
:piece_justificative_template,
|
|
||||||
:quartiers_prioritaires,
|
|
||||||
:cadastres,
|
|
||||||
:parcelles_agricoles,
|
|
||||||
drop_down_list_attributes: [:value]
|
|
||||||
]
|
|
||||||
|
|
||||||
TYPE_DE_CHAMP_ATTRIBUTES = TYPE_DE_CHAMP_ATTRIBUTES_BASE.dup
|
|
||||||
TYPE_DE_CHAMP_ATTRIBUTES << {
|
|
||||||
types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES_BASE
|
|
||||||
}
|
|
||||||
|
|
||||||
def apercu
|
def apercu
|
||||||
@dossier = procedure_without_control.new_dossier
|
@dossier = procedure_without_control.new_dossier
|
||||||
@tab = apercu_tab
|
@tab = apercu_tab
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
|
||||||
if @procedure.update(procedure_params)
|
|
||||||
flash.now.notice = if params[:procedure][:types_de_champ_attributes].present?
|
|
||||||
'Formulaire mis à jour.'
|
|
||||||
elsif params[:procedure][:types_de_champ_private_attributes].present?
|
|
||||||
'Annotations privées mises à jour.'
|
|
||||||
else
|
|
||||||
'Démarche enregistrée.'
|
|
||||||
end
|
|
||||||
|
|
||||||
reset_procedure
|
|
||||||
else
|
|
||||||
flash.now.alert = @procedure.errors.full_messages
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def apercu_tab
|
def apercu_tab
|
||||||
|
@ -53,12 +17,5 @@ module NewAdministrateur
|
||||||
def procedure_without_control
|
def procedure_without_control
|
||||||
Procedure.find(params[:id])
|
Procedure.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def procedure_params
|
|
||||||
params.required(:procedure).permit(
|
|
||||||
types_de_champ_attributes: TYPE_DE_CHAMP_ATTRIBUTES,
|
|
||||||
types_de_champ_private_attributes: TYPE_DE_CHAMP_ATTRIBUTES
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
module NewAdministrateur
|
||||||
|
class TypesDeChampController < AdministrateurController
|
||||||
|
before_action :retrieve_procedure, only: [:create, :update, :destroy]
|
||||||
|
before_action :procedure_locked?, only: [:create, :update, :destroy]
|
||||||
|
|
||||||
|
def create
|
||||||
|
type_de_champ = TypeDeChamp.new(type_de_champ_create_params)
|
||||||
|
|
||||||
|
if type_de_champ.save
|
||||||
|
reset_procedure
|
||||||
|
render json: serialize_type_de_champ(type_de_champ), status: :created
|
||||||
|
else
|
||||||
|
render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
|
||||||
|
|
||||||
|
if type_de_champ.update(type_de_champ_update_params)
|
||||||
|
reset_procedure
|
||||||
|
render json: serialize_type_de_champ(type_de_champ)
|
||||||
|
else
|
||||||
|
render json: { errors: type_de_champ.errors.full_messages }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
type_de_champ = TypeDeChamp.where(procedure: @procedure).find(params[:id])
|
||||||
|
|
||||||
|
type_de_champ.destroy!
|
||||||
|
reset_procedure
|
||||||
|
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def serialize_type_de_champ(type_de_champ)
|
||||||
|
{
|
||||||
|
type_de_champ: type_de_champ.as_json(
|
||||||
|
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
|
||||||
|
methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_de_champ_create_params
|
||||||
|
params.required(:type_de_champ).permit(:libelle,
|
||||||
|
:description,
|
||||||
|
:order_place,
|
||||||
|
:type_champ,
|
||||||
|
:private,
|
||||||
|
:parent_id,
|
||||||
|
:mandatory,
|
||||||
|
:piece_justificative_template,
|
||||||
|
:quartiers_prioritaires,
|
||||||
|
:cadastres,
|
||||||
|
:parcelles_agricoles,
|
||||||
|
:drop_down_list_value).merge(procedure: @procedure)
|
||||||
|
end
|
||||||
|
|
||||||
|
def type_de_champ_update_params
|
||||||
|
params.required(:type_de_champ).permit(:libelle,
|
||||||
|
:description,
|
||||||
|
:order_place,
|
||||||
|
:type_champ,
|
||||||
|
:mandatory,
|
||||||
|
:piece_justificative_template,
|
||||||
|
:quartiers_prioritaires,
|
||||||
|
:cadastres,
|
||||||
|
:parcelles_agricoles,
|
||||||
|
:drop_down_list_value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -188,7 +188,7 @@ module NewUser
|
||||||
def ask_deletion
|
def ask_deletion
|
||||||
dossier = current_user.dossiers.includes(:user, procedure: :administrateur).find(params[:id])
|
dossier = current_user.dossiers.includes(:user, procedure: :administrateur).find(params[:id])
|
||||||
|
|
||||||
if !dossier.instruction_commencee?
|
if dossier.can_be_deleted_by_user?
|
||||||
dossier.delete_and_keep_track
|
dossier.delete_and_keep_track
|
||||||
flash.notice = 'Votre dossier a bien été supprimé.'
|
flash.notice = 'Votre dossier a bien été supprimé.'
|
||||||
redirect_to dossiers_path
|
redirect_to dossiers_path
|
||||||
|
@ -257,7 +257,7 @@ module NewUser
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_dossier_can_be_updated
|
def ensure_dossier_can_be_updated
|
||||||
if !dossier.can_be_updated_by_the_user?
|
if !dossier.can_be_updated_by_user?
|
||||||
flash.alert = 'Votre dossier ne peut plus être modifié'
|
flash.alert = 'Votre dossier ne peut plus être modifié'
|
||||||
redirect_to dossiers_path
|
redirect_to dossiers_path
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,7 +39,8 @@ module ProcedureHelper
|
||||||
type: "champ",
|
type: "champ",
|
||||||
types_de_champ_options: types_de_champ_options.to_json,
|
types_de_champ_options: types_de_champ_options.to_json,
|
||||||
types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json,
|
types_de_champ: types_de_champ_as_json(procedure.types_de_champ).to_json,
|
||||||
direct_uploads_url: rails_direct_uploads_url,
|
save_url: procedure_types_de_champ_path(procedure),
|
||||||
|
direct_upload_url: rails_direct_uploads_url,
|
||||||
drag_icon_url: image_url("icons/drag.svg")
|
drag_icon_url: image_url("icons/drag.svg")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -49,18 +50,12 @@ module ProcedureHelper
|
||||||
type: "annotation",
|
type: "annotation",
|
||||||
types_de_champ_options: types_de_champ_options.to_json,
|
types_de_champ_options: types_de_champ_options.to_json,
|
||||||
types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json,
|
types_de_champ: types_de_champ_as_json(procedure.types_de_champ_private).to_json,
|
||||||
direct_uploads_url: rails_direct_uploads_url,
|
save_url: procedure_types_de_champ_path(procedure),
|
||||||
|
direct_upload_url: rails_direct_uploads_url,
|
||||||
drag_icon_url: image_url("icons/drag.svg")
|
drag_icon_url: image_url("icons/drag.svg")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def procedure_data(procedure)
|
|
||||||
{
|
|
||||||
types_de_champ: types_de_champ_as_json(procedure.types_de_champ),
|
|
||||||
types_de_champ_private: types_de_champ_as_json(procedure.types_de_champ_private)
|
|
||||||
}.to_json
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
TOGGLES = {
|
TOGGLES = {
|
||||||
|
@ -79,14 +74,12 @@ module ProcedureHelper
|
||||||
types_de_champ
|
types_de_champ
|
||||||
end
|
end
|
||||||
|
|
||||||
TYPES_DE_CHAMP_INCLUDE = { drop_down_list: { only: :value } }
|
|
||||||
TYPES_DE_CHAMP_BASE = {
|
TYPES_DE_CHAMP_BASE = {
|
||||||
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
|
except: [:created_at, :updated_at, :stable_id, :type, :parent_id, :procedure_id, :private],
|
||||||
methods: [:piece_justificative_template_filename, :piece_justificative_template_url],
|
methods: [:piece_justificative_template_filename, :piece_justificative_template_url, :drop_down_list_value]
|
||||||
include: TYPES_DE_CHAMP_INCLUDE
|
|
||||||
}
|
}
|
||||||
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
|
TYPES_DE_CHAMP = TYPES_DE_CHAMP_BASE
|
||||||
.merge(include: TYPES_DE_CHAMP_INCLUDE.merge(types_de_champ: TYPES_DE_CHAMP_BASE))
|
.merge(include: { types_de_champ: TYPES_DE_CHAMP_BASE })
|
||||||
|
|
||||||
def types_de_champ_as_json(types_de_champ)
|
def types_de_champ_as_json(types_de_champ)
|
||||||
types_de_champ.includes(:drop_down_list,
|
types_de_champ.includes(:drop_down_list,
|
||||||
|
|
|
@ -1,40 +1,23 @@
|
||||||
|
import { getJSON, debounce } from '@utils';
|
||||||
|
import { DirectUpload } from 'activestorage';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['state', 'update', 'index', 'item'],
|
props: ['state', 'index', 'item'],
|
||||||
computed: {
|
computed: {
|
||||||
isDirty() {
|
isValid() {
|
||||||
return (
|
|
||||||
this.state.version &&
|
|
||||||
this.state.unsavedInvalidItems.size > 0 &&
|
|
||||||
this.state.unsavedItems.has(this.itemId)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isInvalid() {
|
|
||||||
if (this.deleted) {
|
if (this.deleted) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.libelle) {
|
if (this.libelle) {
|
||||||
return !this.libelle.trim();
|
return !!this.libelle.trim();
|
||||||
}
|
}
|
||||||
return true;
|
return false;
|
||||||
},
|
|
||||||
itemId() {
|
|
||||||
return this.item.id || this.clientId;
|
|
||||||
},
|
|
||||||
changeLog() {
|
|
||||||
return [this.itemId, !this.isInvalid];
|
|
||||||
},
|
},
|
||||||
itemClassName() {
|
itemClassName() {
|
||||||
const classNames = [`draggable-item-${this.index}`];
|
const classNames = [`draggable-item-${this.index}`];
|
||||||
if (this.isHeaderSection) {
|
if (this.isHeaderSection) {
|
||||||
classNames.push('type-header-section');
|
classNames.push('type-header-section');
|
||||||
}
|
}
|
||||||
if (this.isDirty) {
|
|
||||||
if (this.isInvalid) {
|
|
||||||
classNames.push('invalid');
|
|
||||||
} else {
|
|
||||||
classNames.push('dirty');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return classNames.join(' ');
|
return classNames.join(' ');
|
||||||
},
|
},
|
||||||
isDropDown() {
|
isDropDown() {
|
||||||
|
@ -73,6 +56,47 @@ export default {
|
||||||
return 'types_de_champ_attributes';
|
return 'types_de_champ_attributes';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
payload() {
|
||||||
|
const payload = {
|
||||||
|
libelle: this.libelle,
|
||||||
|
type_champ: this.typeChamp,
|
||||||
|
mandatory: this.mandatory,
|
||||||
|
description: this.description,
|
||||||
|
drop_down_list_value: this.dropDownListValue,
|
||||||
|
order_place: this.index
|
||||||
|
};
|
||||||
|
if (this.pieceJustificativeTemplate) {
|
||||||
|
payload.piece_justificative_template = this.pieceJustificativeTemplate;
|
||||||
|
}
|
||||||
|
if (this.state.parentId) {
|
||||||
|
payload.parent_id = this.state.parentId;
|
||||||
|
}
|
||||||
|
if (!this.id && this.state.isAnnotation) {
|
||||||
|
payload.private = true;
|
||||||
|
}
|
||||||
|
Object.assign(payload, this.options);
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
|
saveUrl() {
|
||||||
|
if (this.id) {
|
||||||
|
return `${this.state.saveUrl}/${this.id}`;
|
||||||
|
}
|
||||||
|
return this.state.saveUrl;
|
||||||
|
},
|
||||||
|
savePayload() {
|
||||||
|
if (this.deleted) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return { type_de_champ: this.payload };
|
||||||
|
},
|
||||||
|
saveMethod() {
|
||||||
|
if (this.deleted) {
|
||||||
|
return 'delete';
|
||||||
|
} else if (this.id) {
|
||||||
|
return 'patch';
|
||||||
|
}
|
||||||
|
return 'post';
|
||||||
|
},
|
||||||
typesDeChamp() {
|
typesDeChamp() {
|
||||||
return this.item.types_de_champ;
|
return this.item.types_de_champ;
|
||||||
},
|
},
|
||||||
|
@ -85,39 +109,46 @@ export default {
|
||||||
return Object.assign({}, this.state, {
|
return Object.assign({}, this.state, {
|
||||||
typesDeChamp: this.typesDeChamp,
|
typesDeChamp: this.typesDeChamp,
|
||||||
typesDeChampOptions: this.typesDeChampOptions,
|
typesDeChampOptions: this.typesDeChampOptions,
|
||||||
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`
|
prefix: `${this.state.prefix}[${this.attribute}][${this.index}]`,
|
||||||
|
parentId: this.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
id: this.item.id,
|
||||||
typeChamp: this.item.type_champ,
|
typeChamp: this.item.type_champ,
|
||||||
libelle: this.item.libelle,
|
libelle: this.item.libelle,
|
||||||
mandatory: this.item.mandatory,
|
mandatory: this.item.mandatory,
|
||||||
description: this.item.description,
|
description: this.item.description,
|
||||||
dropDownList: this.item.drop_down_list && this.item.drop_down_list.value,
|
pieceJustificativeTemplate: null,
|
||||||
|
pieceJustificativeTemplateUrl: this.item.piece_justificative_template_url,
|
||||||
|
pieceJustificativeTemplateFilename: this.item
|
||||||
|
.piece_justificative_template_filename,
|
||||||
|
dropDownListValue: this.item.drop_down_list_value,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
clientId: `id-${clientIds++}`
|
isSaving: false,
|
||||||
|
isUploading: false,
|
||||||
|
hasChanges: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
watch: {
|
||||||
for (let path of PATHS_TO_WATCH) {
|
index() {
|
||||||
this.$watch(path, () => this.update(this.changeLog));
|
this.update();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
created() {
|
||||||
if (this.isInvalid) {
|
this.debouncedSave = debounce(() => this.save(), 500);
|
||||||
this.update(this.changeLog, false);
|
this.debouncedUpload = debounce(evt => this.upload(evt), 500);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeChamp(item) {
|
removeChamp() {
|
||||||
if (item.id) {
|
if (this.id) {
|
||||||
this.deleted = true;
|
this.deleted = true;
|
||||||
|
this.debouncedSave();
|
||||||
} else {
|
} else {
|
||||||
const index = this.state.typesDeChamp.indexOf(item);
|
const index = this.state.typesDeChamp.indexOf(this.item);
|
||||||
this.state.typesDeChamp.splice(index, 1);
|
this.state.typesDeChamp.splice(index, 1);
|
||||||
this.update([this.itemId, true]);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nameFor(name) {
|
nameFor(name) {
|
||||||
|
@ -132,6 +163,76 @@ export default {
|
||||||
type_champ: 'text',
|
type_champ: 'text',
|
||||||
types_de_champ: []
|
types_de_champ: []
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
this.hasChanges = true;
|
||||||
|
if (this.isValid) {
|
||||||
|
if (this.state.inFlight === 0) {
|
||||||
|
this.state.flash.clear();
|
||||||
|
}
|
||||||
|
this.debouncedSave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
upload(evt) {
|
||||||
|
if (this.isUploading) {
|
||||||
|
this.debouncedUpload();
|
||||||
|
} else {
|
||||||
|
const input = evt.target;
|
||||||
|
const file = input.files[0];
|
||||||
|
if (file) {
|
||||||
|
this.isUploading = true;
|
||||||
|
uploadFile(this.state.directUploadUrl, file).then(({ signed_id }) => {
|
||||||
|
this.pieceJustificativeTemplate = signed_id;
|
||||||
|
this.isUploading = false;
|
||||||
|
this.debouncedSave();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
input.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
if (this.isSaving) {
|
||||||
|
this.debouncedSave();
|
||||||
|
} else {
|
||||||
|
this.isSaving = true;
|
||||||
|
this.state.inFlight++;
|
||||||
|
getJSON(this.saveUrl, this.savePayload, this.saveMethod)
|
||||||
|
.then(data => {
|
||||||
|
this.onSuccess(data);
|
||||||
|
})
|
||||||
|
.catch(xhr => {
|
||||||
|
this.onError(xhr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
if (data && data.type_de_champ) {
|
||||||
|
this.id = data.type_de_champ.id;
|
||||||
|
this.pieceJustificativeTemplateUrl =
|
||||||
|
data.type_de_champ.piece_justificative_template_url;
|
||||||
|
this.pieceJustificativeTemplateFilename =
|
||||||
|
data.type_de_champ.piece_justificative_template_filename;
|
||||||
|
this.pieceJustificativeTemplate = null;
|
||||||
|
}
|
||||||
|
this.state.inFlight--;
|
||||||
|
this.isSaving = false;
|
||||||
|
this.hasChanges = false;
|
||||||
|
|
||||||
|
if (this.state.inFlight === 0) {
|
||||||
|
this.state.flash.success();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(xhr) {
|
||||||
|
this.isSaving = false;
|
||||||
|
this.state.inFlight--;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
errors: [message]
|
||||||
|
} = JSON.parse(xhr.responseText);
|
||||||
|
this.state.flash.error(message);
|
||||||
|
} catch (e) {
|
||||||
|
this.state.flash.error(xhr.responseText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -143,21 +244,20 @@ const EXCLUDE_FROM_REPETITION = [
|
||||||
'siret'
|
'siret'
|
||||||
];
|
];
|
||||||
|
|
||||||
const PATHS_TO_WATCH = [
|
|
||||||
'typeChamp',
|
|
||||||
'libelle',
|
|
||||||
'mandatory',
|
|
||||||
'description',
|
|
||||||
'dropDownList',
|
|
||||||
'options.quartiers_prioritaires',
|
|
||||||
'options.cadastres',
|
|
||||||
'options.parcelles_agricoles',
|
|
||||||
'index',
|
|
||||||
'deleted'
|
|
||||||
];
|
|
||||||
|
|
||||||
function castBoolean(value) {
|
function castBoolean(value) {
|
||||||
return value && value != 0;
|
return value && value != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientIds = 0;
|
function uploadFile(directUploadUrl, file) {
|
||||||
|
const upload = new DirectUpload(file, directUploadUrl);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
upload.create((error, blob) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error);
|
||||||
|
} else {
|
||||||
|
resolve(blob);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="deleted" v-if="deleted">
|
<div class="deleted" v-if="deleted">
|
||||||
<input type="hidden" :name="nameFor('id')" :value="item.id">
|
<input type="hidden" :name="nameFor('id')" :value="id">
|
||||||
<input type="hidden" :name="nameFor('_destroy')" value="true">
|
<input type="hidden" :name="nameFor('_destroy')" value="true">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
:id="elementIdFor('type_champ')"
|
:id="elementIdFor('type_champ')"
|
||||||
:name="nameFor('type_champ')"
|
:name="nameFor('type_champ')"
|
||||||
v-model="typeChamp"
|
v-model="typeChamp"
|
||||||
|
@change="update"
|
||||||
class="small-margin small inline">
|
class="small-margin small inline">
|
||||||
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
|
<option v-for="option in state.typesDeChampOptions" :key="option[1]" :value="option[1]">
|
||||||
{{ option[0] }}
|
{{ option[0] }}
|
||||||
|
@ -21,21 +22,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-start delete">
|
<div class="flex justify-start delete">
|
||||||
<div v-if="isDirty" class="error-message">
|
<button class="button danger" @click.prevent="removeChamp">
|
||||||
<span v-if="isInvalid" class="content">
|
|
||||||
Le libellé doit être rempli.
|
|
||||||
</span>
|
|
||||||
<span v-else class="content">
|
|
||||||
<template v-if="state.isAnnotation">
|
|
||||||
Modifications non sauvegardées. Le libellé doit être rempli sur tous les annotations.
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
Modifications non sauvegardées. Le libellé doit être rempli sur tous les champs.
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="button danger" @click.prevent="removeChamp(item)">
|
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,8 +38,9 @@
|
||||||
:id="elementIdFor('libelle')"
|
:id="elementIdFor('libelle')"
|
||||||
:name="nameFor('libelle')"
|
:name="nameFor('libelle')"
|
||||||
v-model="libelle"
|
v-model="libelle"
|
||||||
|
@change="update"
|
||||||
class="small-margin small"
|
class="small-margin small"
|
||||||
:class="{ error: isDirty && isInvalid }">
|
:class="{ error: hasChanges && !isValid }">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
|
<div class="cell" v-show="!isHeaderSection && !isExplication && !state.isAnnotation">
|
||||||
|
@ -65,6 +53,7 @@
|
||||||
:id="elementIdFor('mandatory')"
|
:id="elementIdFor('mandatory')"
|
||||||
:name="nameFor('mandatory')"
|
:name="nameFor('mandatory')"
|
||||||
v-model="mandatory"
|
v-model="mandatory"
|
||||||
|
@change="update"
|
||||||
class="small-margin small"
|
class="small-margin small"
|
||||||
value="1">
|
value="1">
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +67,7 @@
|
||||||
:id="elementIdFor('description')"
|
:id="elementIdFor('description')"
|
||||||
:name="nameFor('description')"
|
:name="nameFor('description')"
|
||||||
v-model="description"
|
v-model="description"
|
||||||
|
@change="update"
|
||||||
rows=3
|
rows=3
|
||||||
cols=40
|
cols=40
|
||||||
class="small-margin small">
|
class="small-margin small">
|
||||||
|
@ -93,7 +83,8 @@
|
||||||
<textarea
|
<textarea
|
||||||
:id="elementIdFor('drop_down_list')"
|
:id="elementIdFor('drop_down_list')"
|
||||||
:name="nameFor('drop_down_list_attributes[value]')"
|
:name="nameFor('drop_down_list_attributes[value]')"
|
||||||
v-model="dropDownList"
|
v-model="dropDownListValue"
|
||||||
|
@change="update"
|
||||||
rows=3
|
rows=3
|
||||||
cols=40
|
cols=40
|
||||||
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
|
placeholder="Ecrire une valeur par ligne et --valeur-- pour un séparateur."
|
||||||
|
@ -104,9 +95,9 @@
|
||||||
<label :for="elementIdFor('piece_justificative_template')">
|
<label :for="elementIdFor('piece_justificative_template')">
|
||||||
Modèle
|
Modèle
|
||||||
</label>
|
</label>
|
||||||
<template v-if="item.piece_justificative_template_url">
|
<template v-if="pieceJustificativeTemplateUrl">
|
||||||
<a :href="item.piece_justificative_template_url" target="_blank">
|
<a :href="pieceJustificativeTemplateUrl" target="_blank">
|
||||||
{{item.piece_justificative_template_filename}}
|
{{pieceJustificativeTemplateFilename}}
|
||||||
</a>
|
</a>
|
||||||
<br> Modifier :
|
<br> Modifier :
|
||||||
</template>
|
</template>
|
||||||
|
@ -114,8 +105,7 @@
|
||||||
type="file"
|
type="file"
|
||||||
:id="elementIdFor('piece_justificative_template')"
|
:id="elementIdFor('piece_justificative_template')"
|
||||||
:name="nameFor('piece_justificative_template')"
|
:name="nameFor('piece_justificative_template')"
|
||||||
:data-direct-upload-url="state.directUploadsUrl"
|
@change="upload"
|
||||||
@change="update(changeLog)"
|
|
||||||
class="small-margin small">
|
class="small-margin small">
|
||||||
</div>
|
</div>
|
||||||
<div class="cell" v-show="isCarte">
|
<div class="cell" v-show="isCarte">
|
||||||
|
@ -130,6 +120,7 @@
|
||||||
:id="elementIdFor('quartiers_prioritaires')"
|
:id="elementIdFor('quartiers_prioritaires')"
|
||||||
:name="nameFor('quartiers_prioritaires')"
|
:name="nameFor('quartiers_prioritaires')"
|
||||||
v-model="options.quartiers_prioritaires"
|
v-model="options.quartiers_prioritaires"
|
||||||
|
@change="update"
|
||||||
class="small-margin small"
|
class="small-margin small"
|
||||||
value="1">
|
value="1">
|
||||||
Quartiers prioritaires
|
Quartiers prioritaires
|
||||||
|
@ -141,6 +132,7 @@
|
||||||
:id="elementIdFor('cadastres')"
|
:id="elementIdFor('cadastres')"
|
||||||
:name="nameFor('cadastres')"
|
:name="nameFor('cadastres')"
|
||||||
v-model="options.cadastres"
|
v-model="options.cadastres"
|
||||||
|
@change="update"
|
||||||
class="small-margin small"
|
class="small-margin small"
|
||||||
value="1">
|
value="1">
|
||||||
Cadastres
|
Cadastres
|
||||||
|
@ -152,6 +144,7 @@
|
||||||
:id="elementIdFor('parcelles_agricoles')"
|
:id="elementIdFor('parcelles_agricoles')"
|
||||||
:name="nameFor('parcelles_agricoles')"
|
:name="nameFor('parcelles_agricoles')"
|
||||||
v-model="options.parcelles_agricoles"
|
v-model="options.parcelles_agricoles"
|
||||||
|
@change="update"
|
||||||
class="small-margin small"
|
class="small-margin small"
|
||||||
value="1">
|
value="1">
|
||||||
Parcelles Agricoles
|
Parcelles Agricoles
|
||||||
|
@ -163,7 +156,6 @@
|
||||||
<DraggableItem
|
<DraggableItem
|
||||||
v-for="(item, index) in typesDeChamp"
|
v-for="(item, index) in typesDeChamp"
|
||||||
:state="stateForRepetition"
|
:state="stateForRepetition"
|
||||||
:update="update"
|
|
||||||
:index="index"
|
:index="index"
|
||||||
:item="item"
|
:item="item"
|
||||||
:key="item.id" />
|
:key="item.id" />
|
||||||
|
@ -181,7 +173,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<input type="hidden" :name="nameFor('order_place')" :value="index">
|
<input type="hidden" :name="nameFor('order_place')" :value="index">
|
||||||
<input type="hidden" :name="nameFor('id')" :value="item.id">
|
<input type="hidden" :name="nameFor('id')" :value="id">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
export default {
|
export default {
|
||||||
props: ['state', 'version', 'update', 'updateAll'],
|
props: ['state', 'version'],
|
||||||
methods: {
|
methods: {
|
||||||
addChamp() {
|
addChamp() {
|
||||||
this.state.typesDeChamp.push({
|
this.state.typesDeChamp.push({
|
||||||
type_champ: 'text',
|
type_champ: 'text',
|
||||||
drop_down_list: {},
|
types_de_champ: []
|
||||||
types_de_champ: [],
|
|
||||||
options: {}
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
save() {
|
||||||
|
this.state.flash.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,14 +10,13 @@
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="button primary" @click.prevent="updateAll">Enregistrer</button>
|
<button class="button primary" @click.prevent="save">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}">
|
<Draggable :list="state.typesDeChamp" :options="{handle:'.handle'}">
|
||||||
<DraggableItem
|
<DraggableItem
|
||||||
v-for="(item, index) in state.typesDeChamp"
|
v-for="(item, index) in state.typesDeChamp"
|
||||||
:state="state"
|
:state="state"
|
||||||
:update="update"
|
|
||||||
:index="index"
|
:index="index"
|
||||||
:item="item"
|
:item="item"
|
||||||
:key="item.id" />
|
:key="item.id" />
|
||||||
|
@ -33,7 +32,7 @@
|
||||||
</template>
|
</template>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="button primary" @click.prevent="updateAll">Enregistrer</button>
|
<button class="button primary" @click.prevent="save">Enregistrer</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Draggable from 'vuedraggable';
|
import Draggable from 'vuedraggable';
|
||||||
import { fire, debounce } from '@utils';
|
|
||||||
|
|
||||||
import DraggableItem from './DraggableItem';
|
import DraggableItem from './DraggableItem';
|
||||||
import DraggableList from './DraggableList';
|
import DraggableList from './DraggableList';
|
||||||
|
@ -16,109 +15,74 @@ addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
function initEditor(el) {
|
function initEditor(el) {
|
||||||
const { directUploadsUrl, dragIconUrl } = el.dataset;
|
const { directUploadUrl, dragIconUrl, saveUrl } = el.dataset;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
|
typesDeChamp: JSON.parse(el.dataset.typesDeChamp),
|
||||||
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
|
typesDeChampOptions: JSON.parse(el.dataset.typesDeChampOptions),
|
||||||
directUploadsUrl,
|
directUploadUrl,
|
||||||
dragIconUrl,
|
dragIconUrl,
|
||||||
|
saveUrl,
|
||||||
isAnnotation: el.dataset.type === 'annotation',
|
isAnnotation: el.dataset.type === 'annotation',
|
||||||
unsavedItems: new Set(),
|
prefix: 'procedure',
|
||||||
unsavedInvalidItems: new Set(),
|
inFlight: 0,
|
||||||
version: 1,
|
flash: new Flash()
|
||||||
prefix: 'procedure'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
new Vue({
|
|
||||||
el,
|
|
||||||
data: {
|
|
||||||
state,
|
|
||||||
update: null
|
|
||||||
},
|
|
||||||
render(h) {
|
|
||||||
return h(DraggableList, {
|
|
||||||
props: {
|
|
||||||
state: this.state,
|
|
||||||
update: this.update,
|
|
||||||
updateAll: this.updateAll
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const [update, updateAll] = createUpdateFunctions(
|
|
||||||
this,
|
|
||||||
state.isAnnotation
|
|
||||||
);
|
|
||||||
|
|
||||||
this.update = update;
|
|
||||||
this.updateAll = updateAll;
|
|
||||||
|
|
||||||
// We add an initial type de champ here if form is empty
|
// We add an initial type de champ here if form is empty
|
||||||
if (this.state.typesDeChamp.length === 0) {
|
if (state.typesDeChamp.length === 0) {
|
||||||
this.state.typesDeChamp.push({
|
state.typesDeChamp.push({
|
||||||
type_champ: 'text',
|
type_champ: 'text',
|
||||||
types_de_champ: []
|
types_de_champ: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el,
|
||||||
|
data: {
|
||||||
|
state
|
||||||
|
},
|
||||||
|
render(h) {
|
||||||
|
return h(DraggableList, {
|
||||||
|
props: {
|
||||||
|
state: this.state
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createUpdateFunctions(app, isAnnotation) {
|
class Flash {
|
||||||
let isSaving = false;
|
constructor(isAnnotation) {
|
||||||
const form = app.$el.closest('form');
|
this.element = document.querySelector('#flash_messages');
|
||||||
|
this.isAnnotation = isAnnotation;
|
||||||
const update = ([id, isValid], refresh = true) => {
|
}
|
||||||
app.state.unsavedItems.add(id);
|
success() {
|
||||||
if (isValid) {
|
if (this.isAnnotation) {
|
||||||
app.state.unsavedInvalidItems.delete(id);
|
this.add('Annotations privées enregistrées.');
|
||||||
} else {
|
} else {
|
||||||
app.state.unsavedInvalidItems.add(id);
|
this.add('Formulaire enregistré.');
|
||||||
}
|
}
|
||||||
if (refresh) {
|
|
||||||
app.state.version += 1;
|
|
||||||
}
|
}
|
||||||
updateAll();
|
error(message) {
|
||||||
};
|
this.add(message, true);
|
||||||
|
|
||||||
const updateAll = debounce(() => {
|
|
||||||
if (isSaving) {
|
|
||||||
updateAll();
|
|
||||||
} else if (
|
|
||||||
app.state.typesDeChamp.length > 0 &&
|
|
||||||
app.state.unsavedInvalidItems.size === 0
|
|
||||||
) {
|
|
||||||
isSaving = true;
|
|
||||||
app.state.unsavedItems.clear();
|
|
||||||
app.state.version += 1;
|
|
||||||
fire(form, 'submit');
|
|
||||||
}
|
}
|
||||||
}, 500);
|
clear() {
|
||||||
|
this.element.innerHTML = '';
|
||||||
|
}
|
||||||
|
add(message, isError) {
|
||||||
|
const html = `<div id="flash_message" class="center">
|
||||||
|
<div class="alert alert-fixed ${
|
||||||
|
isError ? 'alert-danger' : 'alert-success'
|
||||||
|
}">
|
||||||
|
${message}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
addEventListener('ProcedureUpdated', event => {
|
this.element.innerHTML = html;
|
||||||
const { types_de_champ, types_de_champ_private } = event.detail;
|
|
||||||
|
|
||||||
app.state.typesDeChamp = isAnnotation
|
setTimeout(() => {
|
||||||
? types_de_champ_private
|
this.clear();
|
||||||
: types_de_champ;
|
}, 6000);
|
||||||
isSaving = false;
|
|
||||||
updateFileInputs();
|
|
||||||
});
|
|
||||||
|
|
||||||
return [update, updateAll];
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is needed du to the way ActiveStorage javascript integration works.
|
|
||||||
// It is built to be used with traditional forms. Another way would be to not use
|
|
||||||
// high level ActiveStorage abstractions (and maybe this is what we should do in the future).
|
|
||||||
function updateFileInputs() {
|
|
||||||
for (let element of document.querySelectorAll('.direct-upload')) {
|
|
||||||
let hiddenInput = element.nextElementSibling;
|
|
||||||
let fileInput = hiddenInput.nextElementSibling;
|
|
||||||
element.remove();
|
|
||||||
hiddenInput.remove();
|
|
||||||
fileInput.value = '';
|
|
||||||
fileInput.removeAttribute('disabled');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,13 +38,26 @@ function source(url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener('turbolinks:load', function() {
|
addEventListener('turbolinks:load', function() {
|
||||||
|
autocompleteSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
addEventListener('ajax:success', function() {
|
||||||
|
autocompleteSetup();
|
||||||
|
});
|
||||||
|
|
||||||
|
function autocompleteSetup() {
|
||||||
for (let { type, url } of sources) {
|
for (let { type, url } of sources) {
|
||||||
for (let target of document.querySelectorAll(selector(type))) {
|
for (let element of document.querySelectorAll(selector(type))) {
|
||||||
let select = autocomplete(target, options, [source(url)]);
|
element.removeAttribute('data-autocomplete');
|
||||||
|
autocompleteInitializeElement(element, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autocompleteInitializeElement(element, url) {
|
||||||
|
const select = autocomplete(element, options, [source(url)]);
|
||||||
select.on('autocomplete:selected', ({ target }, suggestion) => {
|
select.on('autocomplete:selected', ({ target }, suggestion) => {
|
||||||
fire(target, 'autocomplete:select', suggestion);
|
fire(target, 'autocomplete:select', suggestion);
|
||||||
select.autocomplete.setVal(suggestion.label);
|
select.autocomplete.setVal(suggestion.label);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ class ApiCarto::API
|
||||||
params = geojson.to_s
|
params = geojson.to_s
|
||||||
RestClient.post(url, params, content_type: 'application/json')
|
RestClient.post(url, params, content_type: 'application/json')
|
||||||
|
|
||||||
rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout => e
|
rescue RestClient::InternalServerError, RestClient::BadGateway, RestClient::GatewayTimeout, RestClient::ServiceUnavailable => e
|
||||||
Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
|
Rails.logger.error "[ApiCarto] Error on #{url}: #{e}"
|
||||||
raise RestClient::ResourceNotFound
|
raise RestClient::ResourceNotFound
|
||||||
end
|
end
|
||||||
|
|
|
@ -120,17 +120,11 @@ class Dossier < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_default_champs
|
def build_default_champs
|
||||||
procedure.types_de_champ.each do |type_de_champ|
|
procedure.build_champs.each do |champ|
|
||||||
champ = type_de_champ.champ.build
|
|
||||||
|
|
||||||
if type_de_champ.repetition?
|
|
||||||
champ.add_row
|
|
||||||
end
|
|
||||||
|
|
||||||
champs << champ
|
champs << champ
|
||||||
end
|
end
|
||||||
procedure.types_de_champ_private.each do |type_de_champ|
|
procedure.build_champs_private.each do |champ|
|
||||||
champs_private << type_de_champ.champ.build
|
champs_private << champ
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -166,7 +160,11 @@ class Dossier < ApplicationRecord
|
||||||
!procedure.archivee? && brouillon?
|
!procedure.archivee? && brouillon?
|
||||||
end
|
end
|
||||||
|
|
||||||
def can_be_updated_by_the_user?
|
def can_be_updated_by_user?
|
||||||
|
brouillon? || en_construction?
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_be_deleted_by_user?
|
||||||
brouillon? || en_construction?
|
brouillon? || en_construction?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -138,10 +138,15 @@ class Procedure < ApplicationRecord
|
||||||
# Warning: dossier after_save build_default_champs must be removed
|
# Warning: dossier after_save build_default_champs must be removed
|
||||||
# to save a dossier created from this method
|
# to save a dossier created from this method
|
||||||
def new_dossier
|
def new_dossier
|
||||||
champs = types_de_champ.map { |tdc| tdc.champ.build }
|
Dossier.new(procedure: self, champs: build_champs, champs_private: build_champs_private)
|
||||||
champs_private = types_de_champ_private.map { |tdc| tdc.champ.build }
|
end
|
||||||
|
|
||||||
Dossier.new(procedure: self, champs: champs, champs_private: champs_private)
|
def build_champs
|
||||||
|
types_de_champ.map(&:build_champ)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_champs_private
|
||||||
|
types_de_champ_private.map(&:build_champ)
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_path
|
def default_path
|
||||||
|
|
|
@ -120,6 +120,10 @@ class TypeDeChamp < ApplicationRecord
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_champ
|
||||||
|
dynamic_type.build_champ
|
||||||
|
end
|
||||||
|
|
||||||
def self.type_de_champs_list_fr
|
def self.type_de_champs_list_fr
|
||||||
type_champs.map { |champ| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{champ.last}"), champ.first] }
|
type_champs.map { |champ| [I18n.t("activerecord.attributes.type_de_champ.type_champs.#{champ.last}"), champ.first] }
|
||||||
end
|
end
|
||||||
|
@ -175,6 +179,14 @@ class TypeDeChamp < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def drop_down_list_value
|
||||||
|
drop_down_list&.value
|
||||||
|
end
|
||||||
|
|
||||||
|
def drop_down_list_value=(value)
|
||||||
|
self.drop_down_list_attributes = { value: value }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def setup_procedure
|
def setup_procedure
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
class TypesDeChamp::RepetitionTypeDeChamp < TypesDeChamp::TypeDeChampBase
|
||||||
|
def build_champ
|
||||||
|
champ = super
|
||||||
|
champ.add_row
|
||||||
|
champ
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,4 +19,8 @@ class TypesDeChamp::TypeDeChampBase
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_champ
|
||||||
|
@type_de_champ.champ.build
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<%= render_flash timeout: 6000, fixed: true %>
|
|
||||||
|
|
||||||
<%= fire_event(:ProcedureUpdated, procedure_data(@procedure.reload)) %>
|
|
|
@ -51,7 +51,7 @@
|
||||||
= link_to(url_for_dossier(dossier), class: 'cell-link') do
|
= link_to(url_for_dossier(dossier), class: 'cell-link') do
|
||||||
= dossier.updated_at.strftime("%d/%m/%Y")
|
= dossier.updated_at.strftime("%d/%m/%Y")
|
||||||
%td.action-col.delete-col
|
%td.action-col.delete-col
|
||||||
- if dossier.brouillon?
|
- if dossier.can_be_deleted_by_user?
|
||||||
= link_to(ask_deletion_dossier_path(dossier), method: :post, class: 'button danger', data: { disable: true, confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" }) do
|
= link_to(ask_deletion_dossier_path(dossier), method: :post, class: 'button danger', data: { disable: true, confirm: "En continuant, vous allez supprimer ce dossier ainsi que les informations qu’il contient. Toute suppression entraine l’annulation de la démarche en cours.\n\nConfirmer la suppression ?" }) do
|
||||||
%span.icon.delete
|
%span.icon.delete
|
||||||
Supprimer
|
Supprimer
|
||||||
|
|
|
@ -5,11 +5,16 @@
|
||||||
= form.fields_for :champs, champ do |form|
|
= form.fields_for :champs, champ do |form|
|
||||||
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }
|
= render partial: 'shared/dossiers/editable_champs/editable_champ', locals: { champ: form.object, form: form }
|
||||||
= form.hidden_field :_destroy, disabled: true
|
= form.hidden_field :_destroy, disabled: true
|
||||||
|
.flex.row-reverse
|
||||||
|
- if champ.persisted?
|
||||||
%button.button.danger.remove-row
|
%button.button.danger.remove-row
|
||||||
Supprimer
|
Supprimer
|
||||||
|
- else
|
||||||
|
%button.button.danger{ type: :button }
|
||||||
|
Supprimer
|
||||||
|
|
||||||
- if champ.persisted?
|
- if champ.persisted?
|
||||||
= link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query }
|
= link_to "Ajouter une ligne pour « #{champ.libelle} »", champs_repetition_path(form.index), class: 'button add-row', data: { remote: true, method: 'POST', params: { champ_id: champ&.id }.to_query }
|
||||||
- else
|
- else
|
||||||
%button.button.add-row{ disabled: true }
|
%button.button{ type: :button }
|
||||||
= "Ajouter une ligne pour « #{champ.libelle} »"
|
= "Ajouter une ligne pour « #{champ.libelle} »"
|
||||||
|
|
|
@ -367,6 +367,8 @@ Rails.application.routes.draw do
|
||||||
get 'champs'
|
get 'champs'
|
||||||
get 'annotations'
|
get 'annotations'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :types_de_champ, only: [:create, :update, :destroy]
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :services, except: [:show] do
|
resources :services, except: [:show] do
|
||||||
|
|
|
@ -798,13 +798,13 @@ describe NewUser::DossiersController, type: :controller do
|
||||||
subject { post :ask_deletion, params: { id: dossier.id } }
|
subject { post :ask_deletion, params: { id: dossier.id } }
|
||||||
|
|
||||||
shared_examples_for "the dossier can not be deleted" do
|
shared_examples_for "the dossier can not be deleted" do
|
||||||
it do
|
it "doesn’t notify the deletion" do
|
||||||
expect(DossierMailer).not_to receive(:notify_deletion_to_administration)
|
expect(DossierMailer).not_to receive(:notify_deletion_to_administration)
|
||||||
expect(DossierMailer).not_to receive(:notify_deletion_to_user)
|
expect(DossierMailer).not_to receive(:notify_deletion_to_user)
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it "doesn’t delete the dossier" do
|
||||||
subject
|
subject
|
||||||
expect(Dossier.find_by(id: dossier.id)).not_to eq(nil)
|
expect(Dossier.find_by(id: dossier.id)).not_to eq(nil)
|
||||||
expect(dossier.procedure.deleted_dossiers.count).to eq(0)
|
expect(dossier.procedure.deleted_dossiers.count).to eq(0)
|
||||||
|
@ -814,13 +814,13 @@ describe NewUser::DossiersController, type: :controller do
|
||||||
context 'when dossier is owned by signed in user' do
|
context 'when dossier is owned by signed in user' do
|
||||||
let(:dossier) { create(:dossier, :en_construction, user: user, autorisation_donnees: true) }
|
let(:dossier) { create(:dossier, :en_construction, user: user, autorisation_donnees: true) }
|
||||||
|
|
||||||
it do
|
it "notifies the user and the admin of the deletion" do
|
||||||
expect(DossierMailer).to receive(:notify_deletion_to_administration).with(kind_of(DeletedDossier), dossier.procedure.administrateur.email).and_return(double(deliver_later: nil))
|
expect(DossierMailer).to receive(:notify_deletion_to_administration).with(kind_of(DeletedDossier), dossier.procedure.administrateur.email).and_return(double(deliver_later: nil))
|
||||||
expect(DossierMailer).to receive(:notify_deletion_to_user).with(kind_of(DeletedDossier), dossier.user.email).and_return(double(deliver_later: nil))
|
expect(DossierMailer).to receive(:notify_deletion_to_user).with(kind_of(DeletedDossier), dossier.user.email).and_return(double(deliver_later: nil))
|
||||||
subject
|
subject
|
||||||
end
|
end
|
||||||
|
|
||||||
it do
|
it "deletes the dossier" do
|
||||||
procedure = dossier.procedure
|
procedure = dossier.procedure
|
||||||
dossier_id = dossier.id
|
dossier_id = dossier.id
|
||||||
subject
|
subject
|
||||||
|
|
|
@ -102,7 +102,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
||||||
|
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
blur
|
||||||
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
within '.footer' do
|
within '.footer' do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
|
@ -129,7 +130,8 @@ feature 'As an administrateur I wanna create a new procedure', js: true do
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libelle de champ'
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
blur
|
||||||
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
click_on Procedure.last.libelle
|
click_on Procedure.last.libelle
|
||||||
click_on 'onglet-pieces'
|
click_on 'onglet-pieces'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
feature 'As an administrateur I edit procedure', js: true do
|
feature 'As an administrateur I can edit types de champ', js: true do
|
||||||
let(:administrateur) { procedure.administrateur }
|
let(:administrateur) { procedure.administrateur }
|
||||||
let(:procedure) { create(:procedure) }
|
let(:procedure) { create(:procedure) }
|
||||||
|
|
||||||
|
@ -18,14 +18,15 @@ feature 'As an administrateur I edit procedure', js: true do
|
||||||
end
|
end
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
blur
|
||||||
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
page.refresh
|
page.refresh
|
||||||
within '.footer' do
|
within '.footer' do
|
||||||
click_on 'Enregistrer'
|
click_on 'Enregistrer'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Add multiple champs" do
|
it "Add multiple champs" do
|
||||||
|
@ -34,33 +35,29 @@ feature 'As an administrateur I edit procedure', js: true do
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
click_on 'Ajouter un champ'
|
click_on 'Ajouter un champ'
|
||||||
end
|
end
|
||||||
expect(page).not_to have_content('Le libellé doit être rempli.')
|
expect(page).not_to have_content('Formulaire enregistré')
|
||||||
expect(page).not_to have_content('Modifications non sauvegardées.')
|
|
||||||
expect(page).not_to have_content('Formulaire mis à jour')
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ 0'
|
||||||
|
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1'
|
||||||
|
blur
|
||||||
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_1_libelle')
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_2_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_2_libelle')
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_3_libelle')
|
||||||
|
|
||||||
expect(page).to have_content('Le libellé doit être rempli.')
|
within '.draggable-item-2' do
|
||||||
expect(page).to have_content('Modifications non sauvegardées.')
|
|
||||||
expect(page).not_to have_content('Formulaire mis à jour')
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
|
|
||||||
|
|
||||||
within '.draggable-item-3' do
|
|
||||||
click_on 'Supprimer'
|
click_on 'Supprimer'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_content('Le libellé doit être rempli.')
|
expect(page).not_to have_selector('#procedure_types_de_champ_attributes_3_libelle')
|
||||||
expect(page).to have_content('Modifications non sauvegardées.')
|
fill_in 'procedure_types_de_champ_attributes_2_libelle', with: 'libellé de champ 2'
|
||||||
expect(page).not_to have_content('Formulaire mis à jour')
|
blur
|
||||||
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 1'
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
|
expect(page).to have_content('Supprimer', count: 3)
|
||||||
|
|
||||||
expect(page).not_to have_content('Le libellé doit être rempli.')
|
|
||||||
expect(page).not_to have_content('Modifications non sauvegardées.')
|
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
expect(page).to have_content('Supprimer', count: 3)
|
expect(page).to have_content('Supprimer', count: 3)
|
||||||
|
@ -68,11 +65,12 @@ feature 'As an administrateur I edit procedure', js: true do
|
||||||
|
|
||||||
it "Remove champs" do
|
it "Remove champs" do
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
blur
|
||||||
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
click_on 'Supprimer'
|
click_on 'Supprimer'
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
expect(page).not_to have_content('Supprimer')
|
expect(page).not_to have_content('Supprimer')
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
|
@ -82,19 +80,21 @@ feature 'As an administrateur I edit procedure', js: true do
|
||||||
it "Only add valid champs" do
|
it "Only add valid champs" do
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_description')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ'
|
fill_in 'procedure_types_de_champ_attributes_0_description', with: 'déscription du champ'
|
||||||
expect(page).to have_content('Le libellé doit être rempli.')
|
blur
|
||||||
expect(page).not_to have_content('Formulaire mis à jour')
|
expect(page).not_to have_content('Formulaire enregistré')
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
blur
|
||||||
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "Add repetition champ" do
|
it "Add repetition champ" do
|
||||||
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
expect(page).to have_selector('#procedure_types_de_champ_attributes_0_libelle')
|
||||||
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ')
|
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_0_type_champ')
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
fill_in 'procedure_types_de_champ_attributes_0_libelle', with: 'libellé de champ'
|
||||||
|
blur
|
||||||
|
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
page.refresh
|
page.refresh
|
||||||
|
|
||||||
within '.flex-grow' do
|
within '.flex-grow' do
|
||||||
|
@ -102,8 +102,9 @@ feature 'As an administrateur I edit procedure', js: true do
|
||||||
end
|
end
|
||||||
|
|
||||||
fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1'
|
fill_in 'procedure_types_de_champ_attributes_0_types_de_champ_attributes_0_libelle', with: 'libellé de champ 1'
|
||||||
|
blur
|
||||||
|
|
||||||
expect(page).to have_content('Formulaire mis à jour')
|
expect(page).to have_content('Formulaire enregistré')
|
||||||
expect(page).to have_content('Supprimer', count: 2)
|
expect(page).to have_content('Supprimer', count: 2)
|
||||||
|
|
||||||
within '.footer' do
|
within '.footer' do
|
||||||
|
@ -112,6 +113,7 @@ feature 'As an administrateur I edit procedure', js: true do
|
||||||
|
|
||||||
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ')
|
select('Bloc répétable', from: 'procedure_types_de_champ_attributes_1_type_champ')
|
||||||
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
|
fill_in 'procedure_types_de_champ_attributes_1_libelle', with: 'libellé de champ 2'
|
||||||
|
blur
|
||||||
|
|
||||||
expect(page).to have_content('Supprimer', count: 3)
|
expect(page).to have_content('Supprimer', count: 3)
|
||||||
end
|
end
|
|
@ -1,13 +1,13 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe 'user access to the list of his dossier' do
|
describe 'user access to the list of their dossiers' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let!(:last_updated_dossier) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
|
let!(:dossier_brouillon) { create(:dossier, user: user) }
|
||||||
let!(:dossier1) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
|
let!(:dossier_en_construction) { create(:dossier, :en_construction, user: user) }
|
||||||
let!(:dossier2) { create(:dossier, :with_entreprise) }
|
let!(:dossier_en_instruction) { create(:dossier, :en_instruction, user: user) }
|
||||||
let!(:dossier_brouillon) { create(:dossier, :with_entreprise, user: user) }
|
let!(:dossier_archived) { create(:dossier, :en_instruction, :archived, user: user) }
|
||||||
let!(:dossier_archived) { create(:dossier, :with_entreprise, user: user, state: Dossier.states.fetch(:en_construction)) }
|
|
||||||
let(:dossiers_per_page) { 25 }
|
let(:dossiers_per_page) { 25 }
|
||||||
|
let(:last_updated_dossier) { dossier_en_construction }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
@default_per_page = Dossier.default_per_page
|
@default_per_page = Dossier.default_per_page
|
||||||
|
@ -15,12 +15,8 @@ describe 'user access to the list of his dossier' do
|
||||||
|
|
||||||
last_updated_dossier.update_column(:updated_at, "19/07/2052 15:35".to_time)
|
last_updated_dossier.update_column(:updated_at, "19/07/2052 15:35".to_time)
|
||||||
|
|
||||||
visit new_user_session_path
|
login_as user, scope: :user
|
||||||
within('#new_user') do
|
visit dossiers_path
|
||||||
page.find_by_id('user_email').set user.email
|
|
||||||
page.find_by_id('user_password').set user.password
|
|
||||||
page.click_on 'Se connecter'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
after do
|
after do
|
||||||
|
@ -28,52 +24,59 @@ describe 'user access to the list of his dossier' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'the list of dossier is displayed' do
|
it 'the list of dossier is displayed' do
|
||||||
expect(page).to have_content(dossier1.procedure.libelle)
|
expect(page).to have_content(dossier_brouillon.procedure.libelle)
|
||||||
expect(page).to have_content('en construction')
|
expect(page).to have_content(dossier_en_construction.procedure.libelle)
|
||||||
end
|
expect(page).to have_content(dossier_en_instruction.procedure.libelle)
|
||||||
|
|
||||||
it 'dossiers belonging to other users are not displayed' do
|
|
||||||
expect(page).not_to have_content(dossier2.procedure.libelle)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'the list must be ordered by last updated' do
|
|
||||||
expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier1.procedure.libelle}/m)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'should list archived dossiers' do
|
|
||||||
expect(page).to have_content(dossier_archived.procedure.libelle)
|
expect(page).to have_content(dossier_archived.procedure.libelle)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should have link to only delete brouillon' do
|
it 'the list must be ordered by last updated' do
|
||||||
expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon))
|
expect(page.body).to match(/#{last_updated_dossier.procedure.libelle}.*#{dossier_en_instruction.procedure.libelle}/m)
|
||||||
expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier1))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user clicks on delete brouillon', js: true do
|
context 'when there are dossiers from other users' do
|
||||||
scenario 'dossier is deleted' do
|
let!(:dossier_other_user) { create(:dossier) }
|
||||||
page.accept_alert('Confirmer la suppression ?') do
|
|
||||||
find(:xpath, "//a[@href='#{ask_deletion_dossier_path(dossier_brouillon)}']").click
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page).to have_content('Votre dossier a bien été supprimé.')
|
it 'doesn’t display dossiers belonging to other users' do
|
||||||
end
|
expect(page).not_to have_content(dossier_other_user.procedure.libelle)
|
||||||
end
|
|
||||||
|
|
||||||
context 'when user clicks on a projet in list', js: true do
|
|
||||||
before do
|
|
||||||
page.click_on(dossier1.procedure.libelle)
|
|
||||||
end
|
|
||||||
scenario 'user is redirected to dossier page' do
|
|
||||||
expect(page).to have_current_path(dossier_path(dossier1))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when there is more than one page' do
|
context 'when there is more than one page' do
|
||||||
let(:dossiers_per_page) { 2 }
|
let(:dossiers_per_page) { 2 }
|
||||||
|
|
||||||
scenario 'the user can navigate through the other pages', js: true do
|
scenario 'the user can navigate through the other pages' do
|
||||||
|
expect(page).not_to have_content(dossier_en_instruction.procedure.libelle)
|
||||||
page.click_link("Suivant")
|
page.click_link("Suivant")
|
||||||
expect(page).to have_content(dossier_archived.procedure.libelle)
|
expect(page).to have_content(dossier_en_instruction.procedure.libelle)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user clicks on a projet in list' do
|
||||||
|
before do
|
||||||
|
page.click_on(dossier_en_construction.procedure.libelle)
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'user is redirected to dossier page' do
|
||||||
|
expect(page).to have_current_path(dossier_path(dossier_en_construction))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'deletion' do
|
||||||
|
it 'should have links to delete dossiers' do
|
||||||
|
expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_brouillon))
|
||||||
|
expect(page).to have_link(nil, href: ask_deletion_dossier_path(dossier_en_construction))
|
||||||
|
expect(page).not_to have_link(nil, href: ask_deletion_dossier_path(dossier_en_instruction))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user clicks on delete button', js: true do
|
||||||
|
scenario 'the dossier is deleted' do
|
||||||
|
page.accept_alert('Confirmer la suppression ?') do
|
||||||
|
find(:xpath, "//a[@href='#{ask_deletion_dossier_path(dossier_brouillon)}']").click
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(page).to have_content('Votre dossier a bien été supprimé.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -91,25 +94,27 @@ describe 'user access to the list of his dossier' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the dossier does not belong to the user" do
|
context "when the dossier does not belong to the user" do
|
||||||
|
let!(:dossier_other_user) { create(:dossier) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
page.find_by_id('dossier_id').set(dossier2.id)
|
page.find_by_id('dossier_id').set(dossier_other_user.id)
|
||||||
click_button("Rechercher")
|
click_button("Rechercher")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "shows an error message on the dossiers page" do
|
it "shows an error message on the dossiers page" do
|
||||||
expect(current_path).to eq(dossiers_path)
|
expect(current_path).to eq(dossiers_path)
|
||||||
expect(page).to have_content("Vous n’avez pas de dossier avec le nº #{dossier2.id}.")
|
expect(page).to have_content("Vous n’avez pas de dossier avec le nº #{dossier_other_user.id}.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when the dossier belongs to the user" do
|
context "when the dossier belongs to the user" do
|
||||||
before do
|
before do
|
||||||
page.find_by_id('dossier_id').set(dossier1.id)
|
page.find_by_id('dossier_id').set(dossier_en_construction.id)
|
||||||
click_button("Rechercher")
|
click_button("Rechercher")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "redirects to the dossier page" do
|
it "redirects to the dossier page" do
|
||||||
expect(current_path).to eq(dossier_path(dossier1))
|
expect(current_path).to eq(dossier_path(dossier_en_construction))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,6 +58,10 @@ module FeatureHelpers
|
||||||
# Procedure contact infos in the footer
|
# Procedure contact infos in the footer
|
||||||
expect(page).to have_content(procedure.service.email)
|
expect(page).to have_content(procedure.service.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def blur
|
||||||
|
page.find('body').click
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
|
|
Loading…
Reference in a new issue