commit
048c167a25
41 changed files with 935 additions and 227 deletions
|
@ -8,4 +8,8 @@ $contact-padding: $default-space * 2;
|
||||||
.description {
|
.description {
|
||||||
padding-bottom: $contact-padding;
|
padding-bottom: $contact-padding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
.table.vertical.procedure-library-list th {
|
||||||
|
padding-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table.vertical.procedure-library-list td {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.procedure-library-list .button {
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
|
@ -1,6 +1,16 @@
|
||||||
#switch-menu {
|
#switch-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 10px;
|
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
z-index: 300;
|
color: #FFFFFF;
|
||||||
|
list-style: none;
|
||||||
|
text-decoration: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#switch-menu a,
|
||||||
|
#switch-menu a:link,
|
||||||
|
#switch-menu a:visited,
|
||||||
|
#switch-menu a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,6 +199,7 @@ class Admin::ProceduresController < AdminController
|
||||||
.where(id: significant_procedure_ids)
|
.where(id: significant_procedure_ids)
|
||||||
.group_by(&:organisation_name)
|
.group_by(&:organisation_name)
|
||||||
.sort_by { |_, procedures| procedures.first.created_at }
|
.sort_by { |_, procedures| procedures.first.created_at }
|
||||||
|
render layout: 'application'
|
||||||
end
|
end
|
||||||
|
|
||||||
def active_class
|
def active_class
|
||||||
|
|
|
@ -34,11 +34,15 @@ module Users
|
||||||
redirect_to new_user_registration_path
|
redirect_to new_user_registration_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def procedure_for_help
|
||||||
|
Procedure.publiees.find_by(path: params[:path]) || Procedure.brouillons.find_by(path: params[:path])
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def store_user_location!
|
def store_user_location!
|
||||||
procedure = Procedure.find_by(path: params[:path])
|
procedure = Procedure.find_by(path: params[:path])
|
||||||
store_location_for(:user, commencer_path(path: procedure.path))
|
store_location_for(:user, helpers.procedure_lien(procedure))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -119,4 +119,20 @@ module ApplicationHelper
|
||||||
{}
|
{}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def try_format_date(date)
|
||||||
|
begin
|
||||||
|
date.is_a?(String) ? Date.parse(date).strftime("%d %B %Y") : date.strftime("%d %B %Y")
|
||||||
|
rescue
|
||||||
|
date
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_format_datetime(datetime)
|
||||||
|
begin
|
||||||
|
datetime.is_a?(String) ? Time.zone.parse(datetime).strftime("%d %B %Y %R") : datetime.strftime("%d %B %Y %R")
|
||||||
|
rescue
|
||||||
|
datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
import { show, hide, delegate } from '@utils';
|
import { show, hide, delegate } from '@utils';
|
||||||
|
|
||||||
delegate('change', '#contact-form #type', event => {
|
function updateContactElementsVisibility() {
|
||||||
const type = event.target.value;
|
const contactSelect = document.querySelector('#contact-form #type');
|
||||||
const answer = document.querySelector(`[data-answer="${type}"]`);
|
if (contactSelect) {
|
||||||
const card = document.querySelector('.support.card');
|
const type = contactSelect.value;
|
||||||
|
const visibleElements = `[data-contact-type-only="${type}"]`;
|
||||||
|
const hiddenElements = `[data-contact-type-only]:not([data-contact-type-only="${type}"])`;
|
||||||
|
|
||||||
for (let element of document.querySelectorAll('.card-content')) {
|
document.querySelectorAll(visibleElements).forEach(show);
|
||||||
hide(element);
|
document.querySelectorAll(hiddenElements).forEach(hide);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (answer) {
|
addEventListener('turbolinks:load', updateContactElementsVisibility);
|
||||||
show(card);
|
delegate('change', '#contact-form #type', updateContactElementsVisibility);
|
||||||
show(answer);
|
|
||||||
} else {
|
|
||||||
hide(card);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -454,15 +454,12 @@ class Procedure < ApplicationRecord
|
||||||
def clone_attachment(cloned_procedure, attachment_symbol)
|
def clone_attachment(cloned_procedure, attachment_symbol)
|
||||||
attachment = send(attachment_symbol)
|
attachment = send(attachment_symbol)
|
||||||
if attachment.attached?
|
if attachment.attached?
|
||||||
response = Typhoeus.get(attachment.service_url, timeout: 5)
|
|
||||||
if response.success?
|
|
||||||
cloned_procedure.send(attachment_symbol).attach(
|
cloned_procedure.send(attachment_symbol).attach(
|
||||||
io: StringIO.new(response.body),
|
io: StringIO.new(attachment.download),
|
||||||
filename: attachment.filename
|
filename: attachment.filename
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def check_juridique
|
def check_juridique
|
||||||
if juridique_required? && (cadre_juridique.blank? && !deliberation.attached?)
|
if juridique_required? && (cadre_juridique.blank? && !deliberation.attached?)
|
||||||
|
|
143
app/services/carrierwave_active_storage_migration_service.rb
Normal file
143
app/services/carrierwave_active_storage_migration_service.rb
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
class CarrierwaveActiveStorageMigrationService
|
||||||
|
def ensure_openstack_copy_possible!(uploader)
|
||||||
|
ensure_active_storage_openstack!
|
||||||
|
ensure_carrierwave_openstack!(uploader)
|
||||||
|
ensure_active_storage_and_carrierwave_credetials_match(uploader)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_active_storage_openstack!
|
||||||
|
# If we manage to get the client, it means that ActiveStorage is on OpenStack
|
||||||
|
openstack_client!
|
||||||
|
end
|
||||||
|
|
||||||
|
def openstack_client!
|
||||||
|
@openstack_client ||= active_storage_openstack_client!
|
||||||
|
end
|
||||||
|
|
||||||
|
def active_storage_openstack_client!
|
||||||
|
service = ActiveStorage::Blob.service
|
||||||
|
|
||||||
|
if !defined?(ActiveStorage::Service::OpenStackService) ||
|
||||||
|
!service.is_a?(ActiveStorage::Service::OpenStackService)
|
||||||
|
raise StandardError, 'ActiveStorage must be backed by OpenStack'
|
||||||
|
end
|
||||||
|
|
||||||
|
service.client
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_carrierwave_openstack!(uploader)
|
||||||
|
storage = fog_client!(uploader)
|
||||||
|
|
||||||
|
if !defined?(Fog::OpenStack::Storage::Real) ||
|
||||||
|
!storage.is_a?(Fog::OpenStack::Storage::Real)
|
||||||
|
raise StandardError, 'Carrierwave must be backed by OpenStack'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fog_client!(uploader)
|
||||||
|
storage = uploader.new.send(:storage)
|
||||||
|
|
||||||
|
if !defined?(CarrierWave::Storage::Fog) ||
|
||||||
|
!storage.is_a?(CarrierWave::Storage::Fog)
|
||||||
|
raise StandardError, 'Carrierwave must be backed by a Fog provider'
|
||||||
|
end
|
||||||
|
|
||||||
|
storage.connection
|
||||||
|
end
|
||||||
|
|
||||||
|
# OpenStack Swift's COPY object command works across different buckets, but they still need
|
||||||
|
# to be on the same object store. This method tries to ensure that Carrierwave and ActiveStorage
|
||||||
|
# are indeed pointing to the same Swift store.
|
||||||
|
def ensure_active_storage_and_carrierwave_credetials_match(uploader)
|
||||||
|
auth_keys = [
|
||||||
|
:openstack_tenant,
|
||||||
|
:openstack_api_key,
|
||||||
|
:openstack_username,
|
||||||
|
:openstack_region,
|
||||||
|
:openstack_management_url
|
||||||
|
]
|
||||||
|
|
||||||
|
active_storage_creds = openstack_client!.credentials.slice(*auth_keys)
|
||||||
|
carrierwave_creds = fog_client!(uploader).credentials.slice(*auth_keys)
|
||||||
|
|
||||||
|
if active_storage_creds != carrierwave_creds
|
||||||
|
raise StandardError, "Active Storage and Carrierwave credentials must match"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# If identify is true, force ActiveStorage to examine the beginning of the file
|
||||||
|
# to determine its MIME type. This identification does not happen immediately,
|
||||||
|
# but when the first attachment that references this blob is created.
|
||||||
|
def make_blob(uploader, created_at, filename: nil, identify: false)
|
||||||
|
content_type = uploader.content_type
|
||||||
|
|
||||||
|
ActiveStorage::Blob.create(
|
||||||
|
filename: filename || uploader.filename,
|
||||||
|
content_type: uploader.content_type,
|
||||||
|
identified: content_type.present? && !identify,
|
||||||
|
byte_size: uploader.size,
|
||||||
|
checksum: checksum(uploader),
|
||||||
|
created_at: created_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def checksum(uploader)
|
||||||
|
hex_to_base64(uploader.file.send(:file).etag)
|
||||||
|
end
|
||||||
|
|
||||||
|
def hex_to_base64(hexdigest)
|
||||||
|
[[hexdigest].pack("H*")].pack("m0")
|
||||||
|
end
|
||||||
|
|
||||||
|
def copy_from_carrierwave_to_active_storage!(source_name, blob)
|
||||||
|
openstack_client!.copy_object(
|
||||||
|
carrierwave_container_name,
|
||||||
|
source_name,
|
||||||
|
active_storage_container_name,
|
||||||
|
blob.key
|
||||||
|
)
|
||||||
|
|
||||||
|
fix_content_type(blob)
|
||||||
|
end
|
||||||
|
|
||||||
|
def carrierwave_container_name
|
||||||
|
Rails.application.secrets.fog[:directory]
|
||||||
|
end
|
||||||
|
|
||||||
|
def active_storage_container_name
|
||||||
|
ENV['FOG_ACTIVESTORAGE_DIRECTORY']
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_from_active_storage!(blob)
|
||||||
|
openstack_client!.delete_object(
|
||||||
|
active_storage_container_name,
|
||||||
|
blob.key
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Before calling this method, you must make sure the file has been uploaded for the blob.
|
||||||
|
# Otherwise, this method might fail if it needs to read the beginning of the file to
|
||||||
|
# update the blob’s MIME type.
|
||||||
|
def make_attachment(model, attachment_name, blob)
|
||||||
|
attachment = ActiveStorage::Attachment.create(
|
||||||
|
name: attachment_name,
|
||||||
|
record_type: model.class.base_class.name,
|
||||||
|
record_id: model.id,
|
||||||
|
blob: blob,
|
||||||
|
created_at: model.updated_at.iso8601
|
||||||
|
)
|
||||||
|
|
||||||
|
# Making the attachment may have triggerred MIME type auto detection on the blob,
|
||||||
|
# so we make sure to sync that potentially new MIME type to the object in OpenStack
|
||||||
|
fix_content_type(blob)
|
||||||
|
|
||||||
|
attachment
|
||||||
|
end
|
||||||
|
|
||||||
|
def fix_content_type(blob)
|
||||||
|
# In OpenStack, ActiveStorage cannot inject the MIME type on the fly during direct
|
||||||
|
# download. Instead, the MIME type needs to be stored statically on the file object
|
||||||
|
# in OpenStack. This is what this call does.
|
||||||
|
blob.service.change_content_type(blob.key, blob.content_type)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,104 @@
|
||||||
|
class PieceJustificativeToChampPieceJointeMigrationService
|
||||||
|
def initialize(**params)
|
||||||
|
params.each do |key, value|
|
||||||
|
instance_variable_set("@#{key}", value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_correct_storage_configuration!
|
||||||
|
storage_service.ensure_openstack_copy_possible!(PieceJustificativeUploader)
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_procedure_pjs_to_champ_pjs(procedure)
|
||||||
|
types_de_champ_pj = PiecesJustificativesService.types_pj_as_types_de_champ(procedure)
|
||||||
|
populate_champs_pjs!(procedure, types_de_champ_pj)
|
||||||
|
|
||||||
|
# Only destroy the old types PJ once everything has been safely migrated to
|
||||||
|
# champs PJs. Destroying the types PJ will cascade and destroy the PJs,
|
||||||
|
# and delete the linked objects from remote storage. This means that no other
|
||||||
|
# cleanup action is required.
|
||||||
|
procedure.types_de_piece_justificative.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def storage_service
|
||||||
|
@storage_service ||= CarrierwaveActiveStorageMigrationService.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_champs_pjs!(procedure, types_de_champ_pj)
|
||||||
|
procedure.types_de_champ += types_de_champ_pj
|
||||||
|
|
||||||
|
# Unscope to make sure all dossiers are migrated, even the soft-deleted ones
|
||||||
|
procedure.dossiers.unscope(where: :hidden_at).find_each do |dossier|
|
||||||
|
champs_pj = types_de_champ_pj.map(&:build_champ)
|
||||||
|
dossier.champs += champs_pj
|
||||||
|
|
||||||
|
champs_pj.each do |champ|
|
||||||
|
type_pj_id = champ.type_de_champ.old_pj&.fetch('stable_id', nil)
|
||||||
|
pj = dossier.retrieve_last_piece_justificative_by_type(type_pj_id)
|
||||||
|
|
||||||
|
if pj.present?
|
||||||
|
champ.update(
|
||||||
|
updated_at: pj.updated_at,
|
||||||
|
created_at: pj.created_at
|
||||||
|
)
|
||||||
|
|
||||||
|
convert_pj_to_champ!(pj, champ)
|
||||||
|
else
|
||||||
|
champ.update(
|
||||||
|
updated_at: dossier.updated_at,
|
||||||
|
created_at: dossier.created_at
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
# If anything goes wrong, we roll back the migration by destroying the newly created
|
||||||
|
# types de champ, champs blobs and attachments.
|
||||||
|
types_de_champ_pj.each do |type_champ|
|
||||||
|
type_champ.champ.each { |c| c.piece_justificative_file.purge }
|
||||||
|
type_champ.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
# Reraise the exception to abort the migration.
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
def convert_pj_to_champ!(pj, champ)
|
||||||
|
blob = make_blob(pj)
|
||||||
|
|
||||||
|
# Upload the file before creating the attachment to make sure MIME type
|
||||||
|
# identification doesn’t fail.
|
||||||
|
storage_service.copy_from_carrierwave_to_active_storage!(pj.content.path, blob)
|
||||||
|
attachment = storage_service.make_attachment(champ, 'piece_justificative_file', blob)
|
||||||
|
|
||||||
|
# By reloading, we force ActiveStorage to look at the attachment again, and see
|
||||||
|
# that one exists now. We do this so that, if we need to roll back and destroy the champ,
|
||||||
|
# the blob, the attachment and the actual file on OpenStack also get deleted.
|
||||||
|
champ.reload
|
||||||
|
rescue
|
||||||
|
# Destroy partially attached object that the more general rescue in `populate_champs_pjs!`
|
||||||
|
# might not be able to handle.
|
||||||
|
|
||||||
|
if blob&.key.present?
|
||||||
|
begin
|
||||||
|
storage_service.delete_from_active_storage!(blob)
|
||||||
|
rescue => e
|
||||||
|
# The cleanup attempt failed, perhaps because the object had not been
|
||||||
|
# successfully copied to the Active Storage bucket yet.
|
||||||
|
# Continue trying to clean up the rest anyway.
|
||||||
|
pp e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
blob&.destroy
|
||||||
|
attachment&.destroy
|
||||||
|
champ.reload
|
||||||
|
|
||||||
|
# Reraise the exception to abort the migration.
|
||||||
|
raise
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_blob(pj)
|
||||||
|
storage_service.make_blob(pj.content, pj.updated_at.iso8601, filename: pj.original_filename)
|
||||||
|
end
|
||||||
|
end
|
|
@ -33,7 +33,13 @@ class PiecesJustificativesService
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.types_pj_as_types_de_champ(procedure)
|
def self.types_pj_as_types_de_champ(procedure)
|
||||||
order_place = procedure.types_de_champ.last&.order_place || 0
|
last_champ = procedure.types_de_champ.last
|
||||||
|
if last_champ.present?
|
||||||
|
order_place = last_champ.order_place + 1
|
||||||
|
else
|
||||||
|
order_place = 0
|
||||||
|
end
|
||||||
|
|
||||||
types_de_champ = [
|
types_de_champ = [
|
||||||
TypeDeChamp.new(
|
TypeDeChamp.new(
|
||||||
libelle: "Pièces jointes",
|
libelle: "Pièces jointes",
|
||||||
|
|
|
@ -1,30 +1,34 @@
|
||||||
|
.container
|
||||||
- if current_administrateur.procedures.brouillons.count == 0
|
- if current_administrateur.procedures.brouillons.count == 0
|
||||||
%h4{ style: 'padding: 20px; margin: 20px !important;' }
|
.card.feedback
|
||||||
Bienvenue, 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.
|
.card-title
|
||||||
|
Bienvenue,
|
||||||
|
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.
|
||||||
|
|
||||||
.row{ style: 'padding: 20px; margin: 20px !important;' }
|
.form
|
||||||
%a#from-scratch{ href: new_admin_procedure_path, class: 'btn-lg btn-primary' }
|
.send-wrapper
|
||||||
|
%a#from-scratch.button.primary{ href: new_admin_procedure_path }
|
||||||
Créer une nouvelle démarche de zéro
|
Créer une nouvelle démarche de zéro
|
||||||
|
|
||||||
.row.white-back
|
.card
|
||||||
%h3
|
%h2.header-section
|
||||||
Créer une nouvelle démarche à partir d'une démarche existante
|
Créer une nouvelle démarche à partir d'une démarche existante
|
||||||
|
|
||||||
.section.section-label
|
%label
|
||||||
|
.notice
|
||||||
Pour rechercher dans cette liste, utilisez la fonction "Recherche" de votre navigateur (CTRL+F ou command+F)
|
Pour rechercher dans cette liste, utilisez la fonction "Recherche" de votre navigateur (CTRL+F ou command+F)
|
||||||
%br
|
|
||||||
%br
|
%table.table.vertical.procedure-library-list
|
||||||
- @grouped_procedures.each do |_, procedures|
|
- @grouped_procedures.each do |_, procedures|
|
||||||
%b
|
%tr
|
||||||
|
%th
|
||||||
= procedures.first.organisation_name
|
= procedures.first.organisation_name
|
||||||
%table{ style: 'margin-bottom: 40px;' }
|
|
||||||
- procedures.sort_by(&:id).each do |procedure|
|
- procedures.sort_by(&:id).each do |procedure|
|
||||||
%tr{ style: 'height: 36px;' }
|
%tr
|
||||||
%td{ style: 'width: 750px;' }
|
|
||||||
= procedure.libelle
|
|
||||||
%td{ style: 'padding-right: 10px; padding-left: 10px; width: 60px;' }
|
|
||||||
= link_to('Consulter', apercu_procedure_path(id: procedure.id), target: "_blank", rel: "noopener")
|
|
||||||
%td
|
%td
|
||||||
= link_to('Cloner', admin_procedure_clone_path(procedure.id, from_new_from_existing: true), 'data-method' => :put, class: 'btn-sm btn-primary clone-btn')
|
= procedure.libelle
|
||||||
%td{ style: 'padding-left: 10px;' }
|
%td.flex
|
||||||
= link_to('Contacter', "mailto:#{procedure.administrateurs.pluck(:email) * ","}")
|
= link_to('Consulter', apercu_procedure_path(id: procedure.id), target: "_blank", rel: "noopener", class: 'button small')
|
||||||
|
= link_to('Cloner', admin_procedure_clone_path(procedure.id, from_new_from_existing: true), 'data-method' => :put, class: 'button small primary')
|
||||||
|
= link_to('Contacter', "mailto:#{procedure.administrateurs.pluck(:email) * ","}", class: 'button small')
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
.dropdown.help-dropdown
|
|
||||||
.button.primary.dropdown-button Aide
|
|
||||||
.dropdown-content.fade-in-down
|
|
||||||
%ul.dropdown-items
|
|
||||||
|
|
||||||
-# Use the help website
|
|
||||||
%li
|
|
||||||
= link_to FAQ_URL, target: "_blank", rel: "noopener" do
|
|
||||||
%span.icon.help
|
|
||||||
.dropdown-description
|
|
||||||
%h4.help-dropdown-title Un problème avec le site ?
|
|
||||||
%p Trouvez votre réponse dans l’aide en ligne.
|
|
||||||
|
|
||||||
-# Technical contact
|
|
||||||
%li
|
|
||||||
= mail_to CONTACT_EMAIL do
|
|
||||||
%span.icon.mail
|
|
||||||
.dropdown-description
|
|
||||||
%h4.help-dropdown-title Contact technique
|
|
||||||
%p Envoyez nous un message à #{CONTACT_EMAIL}.
|
|
33
app/views/layouts/_account_dropdown.haml
Normal file
33
app/views/layouts/_account_dropdown.haml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
%span.dropdown.header-menu-opener
|
||||||
|
%button.button.dropdown-button.header-menu-button
|
||||||
|
= image_tag "icons/account-circle.svg", title: "Mon compte"
|
||||||
|
%ul.header-menu.dropdown-content
|
||||||
|
%li
|
||||||
|
.menu-item{ title: current_email }
|
||||||
|
= current_email
|
||||||
|
- if administration_signed_in?
|
||||||
|
%li
|
||||||
|
= link_to manager_root_path, class: "menu-item menu-link" do
|
||||||
|
= image_tag "icons/super-admin.svg"
|
||||||
|
Passer en super-admin
|
||||||
|
- if SwitchDeviseProfileService.new(warden).multiple_devise_profile_connect?
|
||||||
|
- if user_signed_in? && nav_bar_profile != :user
|
||||||
|
%li
|
||||||
|
= link_to dossiers_path, class: "menu-item menu-link" do
|
||||||
|
= image_tag "icons/switch-profile.svg"
|
||||||
|
Passer en usager
|
||||||
|
- if gestionnaire_signed_in? && nav_bar_profile != :gestionnaire
|
||||||
|
%li
|
||||||
|
= link_to gestionnaire_procedures_path, class: "menu-item menu-link" do
|
||||||
|
= image_tag "icons/switch-profile.svg"
|
||||||
|
Passer en instructeur
|
||||||
|
- if administrateur_signed_in? && nav_bar_profile != :administrateur
|
||||||
|
%li
|
||||||
|
= link_to admin_procedures_path, class: "menu-item menu-link" do
|
||||||
|
= image_tag "icons/switch-profile.svg"
|
||||||
|
Passer en administrateur
|
||||||
|
|
||||||
|
%li
|
||||||
|
= link_to destroy_user_session_path, method: :delete, class: "menu-item menu-link" do
|
||||||
|
= image_tag "icons/sign-out.svg"
|
||||||
|
Se déconnecter
|
|
@ -1,6 +1,7 @@
|
||||||
-# We can't use &. because the controller may not implement #nav_bar_profile
|
-# We can't use &. because the controller may not implement #nav_bar_profile
|
||||||
- nav_bar_profile = controller.try(:nav_bar_profile)
|
- nav_bar_profile = controller.try(:nav_bar_profile) || :guest
|
||||||
- dossier = controller.try(:dossier_for_help)
|
- dossier = controller.try(:dossier_for_help)
|
||||||
|
- procedure = controller.try(:procedure_for_help)
|
||||||
|
|
||||||
.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
|
.new-header{ class: current_page?(root_path) ? nil : "new-header-with-border" }
|
||||||
.header-inner-content
|
.header-inner-content
|
||||||
|
@ -46,39 +47,8 @@
|
||||||
|
|
||||||
- if gestionnaire_signed_in? || user_signed_in?
|
- if gestionnaire_signed_in? || user_signed_in?
|
||||||
%li
|
%li
|
||||||
%span.dropdown.header-menu-opener
|
= render partial: 'layouts/account_dropdown', locals: { nav_bar_profile: nav_bar_profile }
|
||||||
%button.button.dropdown-button.header-menu-button
|
|
||||||
= image_tag "icons/account-circle.svg", title: "Mon compte"
|
|
||||||
%ul.header-menu.dropdown-content
|
|
||||||
%li
|
|
||||||
.menu-item{ title: current_email }
|
|
||||||
= current_email
|
|
||||||
- if administration_signed_in?
|
|
||||||
%li
|
|
||||||
= link_to manager_root_path, class: "menu-item menu-link" do
|
|
||||||
= image_tag "icons/super-admin.svg"
|
|
||||||
Passer en super-admin
|
|
||||||
- if SwitchDeviseProfileService.new(warden).multiple_devise_profile_connect?
|
|
||||||
- if user_signed_in? && nav_bar_profile != :user
|
|
||||||
%li
|
|
||||||
= link_to dossiers_path, class: "menu-item menu-link" do
|
|
||||||
= image_tag "icons/switch-profile.svg"
|
|
||||||
Passer en usager
|
|
||||||
- if gestionnaire_signed_in? && nav_bar_profile != :gestionnaire
|
|
||||||
%li
|
|
||||||
= link_to gestionnaire_procedures_path, class: "menu-item menu-link" do
|
|
||||||
= image_tag "icons/switch-profile.svg"
|
|
||||||
Passer en instructeur
|
|
||||||
- if administrateur_signed_in? && nav_bar_profile != :administrateur
|
|
||||||
%li
|
|
||||||
= link_to admin_procedures_path, class: "menu-item menu-link" do
|
|
||||||
= image_tag "icons/switch-profile.svg"
|
|
||||||
Passer en administrateur
|
|
||||||
|
|
||||||
%li
|
|
||||||
= link_to destroy_user_session_path, method: :delete, class: "menu-item menu-link" do
|
|
||||||
= image_tag "icons/sign-out.svg"
|
|
||||||
Se déconnecter
|
|
||||||
- elsif request.path != new_user_session_path
|
- elsif request.path != new_user_session_path
|
||||||
- if request.path == new_user_registration_path
|
- if request.path == new_user_registration_path
|
||||||
%li
|
%li
|
||||||
|
@ -88,9 +58,14 @@
|
||||||
|
|
||||||
%li
|
%li
|
||||||
.header-help
|
.header-help
|
||||||
- if nav_bar_profile == :user && dossier.present?
|
- if dossier.present? && nav_bar_profile == :user
|
||||||
= render partial: 'users/dossier_help_dropdown', locals: { dossier: dossier }
|
= render partial: 'shared/help/help_dropdown_dossier', locals: { dossier: dossier }
|
||||||
|
|
||||||
|
- elsif procedure.present? && (nav_bar_profile == :user || nav_bar_profile == :guest)
|
||||||
|
= render partial: 'shared/help/help_dropdown_procedure', locals: { procedure: procedure }
|
||||||
|
|
||||||
- elsif nav_bar_profile == :gestionnaire
|
- elsif nav_bar_profile == :gestionnaire
|
||||||
= render partial: 'gestionnaires/help_dropdown'
|
= render partial: 'shared/help/help_dropdown_gestionnaire'
|
||||||
|
|
||||||
- else
|
- else
|
||||||
= link_to 'Aide', FAQ_URL, class: "button primary"
|
= render partial: 'shared/help/help_button'
|
||||||
|
|
|
@ -1,25 +1,14 @@
|
||||||
- if SwitchDeviseProfileService.new(warden).multiple_devise_profile_connect?
|
- if SwitchDeviseProfileService.new(warden).multiple_devise_profile_connect?
|
||||||
#switch-menu.dropdown.dropup
|
%ul#switch-menu
|
||||||
%button.btn.btn-default.dropdown-toggle{ type: :button, 'data-toggle' => 'dropdown', 'aria-haspopup' => true, 'aria-expanded' => false }
|
%li
|
||||||
%i.fa.fa-toggle-on
|
Changer de rôle
|
||||||
%span.caret
|
|
||||||
%ul.dropdown-menu.dropdown-menu-left
|
|
||||||
- if user_signed_in?
|
- if user_signed_in?
|
||||||
%li
|
%li
|
||||||
= link_to(dossiers_path, id: :menu_item_procedure) do
|
= link_to(dossiers_path, id: :menu_item_procedure, title: 'Aller dans votre espace usager. Vous pourrez revenir ici ensuite') do
|
||||||
%i.fa.fa-user
|
%i.fa.fa-users
|
||||||
|
Usager
|
||||||
Usager
|
|
||||||
- if gestionnaire_signed_in?
|
- if gestionnaire_signed_in?
|
||||||
%li
|
%li
|
||||||
= link_to(gestionnaire_procedures_path) do
|
= link_to(gestionnaire_procedures_path, title: 'Aller dans votre espace instructeur. Vous pourrez revenir ici ensuite.') do
|
||||||
%i.fa.fa-user
|
%i.fa.fa-user
|
||||||
|
Instructeur
|
||||||
Instructeur
|
|
||||||
|
|
||||||
- if administrateur_signed_in?
|
|
||||||
%li
|
|
||||||
= link_to(admin_procedures_path) do
|
|
||||||
%i.fa.fa-user
|
|
||||||
|
|
||||||
Administrateur
|
|
||||||
|
|
|
@ -48,6 +48,4 @@
|
||||||
%h1
|
%h1
|
||||||
%i.fa.fa-times{ style: 'position: fixed; top: 10; right: 30; color: white;' }
|
%i.fa.fa-times{ style: 'position: fixed; top: 10; right: 30; color: white;' }
|
||||||
|
|
||||||
= render partial: 'layouts/switch_devise_profile_module'
|
|
||||||
|
|
||||||
= render partial: 'layouts/footer', locals: { main_container_size: main_container_size }
|
= render partial: 'layouts/footer', locals: { main_container_size: main_container_size }
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
= current_administrateur.procedures.archivees.count
|
= current_administrateur.procedures.archivees.count
|
||||||
|
|
||||||
.split-hr-left
|
.split-hr-left
|
||||||
|
= render partial: 'layouts/switch_devise_profile_module'
|
||||||
|
|
||||||
|
|
||||||
#infos-block
|
#infos-block
|
||||||
|
|
|
@ -30,6 +30,10 @@
|
||||||
= render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile }
|
= render partial: "shared/champs/siret/show", locals: { champ: c, profile: profile }
|
||||||
- when TypeDeChamp.type_champs.fetch(:textarea)
|
- when TypeDeChamp.type_champs.fetch(:textarea)
|
||||||
= render partial: "shared/champs/textarea/show", locals: { champ: c }
|
= render partial: "shared/champs/textarea/show", locals: { champ: c }
|
||||||
|
- when TypeDeChamp.type_champs.fetch(:date)
|
||||||
|
= try_format_date(c.to_s)
|
||||||
|
- when TypeDeChamp.type_champs.fetch(:datetime)
|
||||||
|
= try_format_datetime(c.to_s)
|
||||||
- else
|
- else
|
||||||
= sanitize(c.to_s)
|
= sanitize(c.to_s)
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
%td= etablissement.naf
|
%td= etablissement.naf
|
||||||
%tr
|
%tr
|
||||||
%th.libelle Date de création :
|
%th.libelle Date de création :
|
||||||
%td= etablissement.entreprise.date_creation&.strftime("%d/%m/%Y")
|
%td= try_format_date(etablissement.entreprise.date_creation)
|
||||||
%tr
|
%tr
|
||||||
%th.libelle Effectif de l'organisation :
|
%th.libelle Effectif de l'organisation :
|
||||||
%td= effectif(etablissement)
|
%td= effectif(etablissement)
|
||||||
|
@ -64,10 +64,10 @@
|
||||||
%td= etablissement.association_objet
|
%td= etablissement.association_objet
|
||||||
%tr
|
%tr
|
||||||
%th.libelle Date de création :
|
%th.libelle Date de création :
|
||||||
%td= etablissement.association_date_creation&.strftime("%d/%m/%Y")
|
%td= try_format_date(etablissement.association_date_creation)
|
||||||
%tr
|
%tr
|
||||||
%th.libelle Date de publication :
|
%th.libelle Date de publication :
|
||||||
%td= etablissement.association_date_publication&.strftime("%d/%m/%Y")
|
%td= try_format_date(etablissement.association_date_publication)
|
||||||
%tr
|
%tr
|
||||||
%th.libelle Date de déclaration :
|
%th.libelle Date de déclaration :
|
||||||
%td= etablissement.association_date_declaration&.strftime("%d/%m/%Y")
|
%td= try_format_date(etablissement.association_date_declaration)
|
||||||
|
|
1
app/views/shared/help/_help_button.html.haml
Normal file
1
app/views/shared/help/_help_button.html.haml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
= link_to 'Aide', FAQ_URL, class: 'button primary'
|
14
app/views/shared/help/_help_dropdown_dossier.html.haml
Normal file
14
app/views/shared/help/_help_dropdown_dossier.html.haml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.dropdown.help-dropdown
|
||||||
|
.button.primary.dropdown-button Aide
|
||||||
|
.dropdown-content.fade-in-down
|
||||||
|
%ul.dropdown-items
|
||||||
|
- title = dossier.brouillon? ? "Besoin d’aide pour remplir votre dossier ?" : "Une question sur votre dossier ?"
|
||||||
|
|
||||||
|
- if dossier.messagerie_available?
|
||||||
|
= render partial: 'shared/help/dropdown_items/messagerie_item',
|
||||||
|
locals: { dossier: dossier, title: title }
|
||||||
|
- elsif dossier.procedure.service.present?
|
||||||
|
= render partial: 'shared/help/dropdown_items/service_item',
|
||||||
|
locals: { service: dossier.procedure.service, title: title }
|
||||||
|
|
||||||
|
= render partial: 'shared/help/dropdown_items/faq_item'
|
|
@ -0,0 +1,6 @@
|
||||||
|
.dropdown.help-dropdown
|
||||||
|
.button.primary.dropdown-button Aide
|
||||||
|
.dropdown-content.fade-in-down
|
||||||
|
%ul.dropdown-items
|
||||||
|
= render partial: 'shared/help/dropdown_items/faq_item'
|
||||||
|
= render partial: 'shared/help/dropdown_items/email_item'
|
9
app/views/shared/help/_help_dropdown_procedure.html.haml
Normal file
9
app/views/shared/help/_help_dropdown_procedure.html.haml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.dropdown.help-dropdown
|
||||||
|
.button.primary.dropdown-button Aide
|
||||||
|
.dropdown-content.fade-in-down
|
||||||
|
%ul.dropdown-items
|
||||||
|
- if procedure.service.present?
|
||||||
|
= render partial: 'shared/help/dropdown_items/service_item',
|
||||||
|
locals: { service: procedure.service, title: "Une question sur cette démarche ?" }
|
||||||
|
|
||||||
|
= render partial: 'shared/help/dropdown_items/faq_item'
|
|
@ -0,0 +1,6 @@
|
||||||
|
%li
|
||||||
|
= mail_to CONTACT_EMAIL do
|
||||||
|
%span.icon.mail
|
||||||
|
.dropdown-description
|
||||||
|
%h4.help-dropdown-title Contact technique
|
||||||
|
%p Envoyez nous un message à #{CONTACT_EMAIL}.
|
6
app/views/shared/help/dropdown_items/_faq_item.html.haml
Normal file
6
app/views/shared/help/dropdown_items/_faq_item.html.haml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
%li
|
||||||
|
= link_to FAQ_URL, target: "_blank", rel: "noopener" do
|
||||||
|
%span.icon.help
|
||||||
|
.dropdown-description
|
||||||
|
%h4.help-dropdown-title Un problème avec le site ?
|
||||||
|
%p Trouvez votre réponse dans l’aide en ligne.
|
|
@ -0,0 +1,6 @@
|
||||||
|
%li
|
||||||
|
= link_to messagerie_dossier_path(dossier) do
|
||||||
|
%span.icon.mail
|
||||||
|
.dropdown-description
|
||||||
|
%h4.help-dropdown-title= title
|
||||||
|
%p Envoyez directement un message à l’instructeur.
|
15
app/views/shared/help/dropdown_items/_service_item.html.haml
Normal file
15
app/views/shared/help/dropdown_items/_service_item.html.haml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
%li.help-dropdown-service
|
||||||
|
%span.icon.person
|
||||||
|
.dropdown-description
|
||||||
|
%h4.help-dropdown-title= title
|
||||||
|
.help-dropdown-service-action
|
||||||
|
%p Contactez directement l’administration :
|
||||||
|
%p.help-dropdown-service-item
|
||||||
|
%span.icon.small.mail
|
||||||
|
= link_to service.email, "mailto:#{service.email}"
|
||||||
|
%p.help-dropdown-service-item
|
||||||
|
%span.icon.small.phone
|
||||||
|
= link_to service.telephone, "tel:#{service.telephone}"
|
||||||
|
%p.help-dropdown-service-item
|
||||||
|
%span.icon.small.clock
|
||||||
|
= service.horaires
|
|
@ -22,17 +22,20 @@
|
||||||
%span.mandatory *
|
%span.mandatory *
|
||||||
= select_tag :type, options_for_select(@options, params[:type]), include_blank: "Choisir un problème", required: true
|
= select_tag :type, options_for_select(@options, params[:type]), include_blank: "Choisir un problème", required: true
|
||||||
|
|
||||||
.support.card.featured.hidden
|
.support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_INFO } }
|
||||||
.card-title
|
.card-title
|
||||||
👉 Notre réponse
|
👉 Notre réponse
|
||||||
.card-content.hidden{ data: { answer: "info demarche" } }
|
.card-content
|
||||||
%p Avez-vous bien vérifié que tous les champs obligatoires (*) sont bien remplis ?
|
%p Avez-vous bien vérifié que tous les champs obligatoires (*) sont bien remplis ?
|
||||||
%p Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.
|
%p Si vous avez des questions sur les informations à saisir, contactez les services en charge de la démarche.
|
||||||
%p
|
%p
|
||||||
%a{ href: 'https://faq.demarches-simplifiees.fr/article/12-contacter-le-service-en-charge-de-ma-demarche' }
|
%a{ href: 'https://faq.demarches-simplifiees.fr/article/12-contacter-le-service-en-charge-de-ma-demarche' }
|
||||||
En savoir plus
|
En savoir plus
|
||||||
|
|
||||||
.card-content.hidden{ data: { answer: "usager perdu" } }
|
.support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_PERDU } }
|
||||||
|
.card-title
|
||||||
|
👉 Notre réponse
|
||||||
|
.card-content
|
||||||
%p Nous vous invitons à contacter l’administration en charge de votre démarche pour qu’elle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : https://www.demarches-simplifiees.fr/commencer/NOM_DE_LA_DEMARCHE .
|
%p Nous vous invitons à contacter l’administration en charge de votre démarche pour qu’elle vous indique le lien à suivre. Celui-ci devrait ressembler à cela : https://www.demarches-simplifiees.fr/commencer/NOM_DE_LA_DEMARCHE .
|
||||||
%br
|
%br
|
||||||
%p Vous pouvez aussi consulter ici la liste de nos démarches les plus frequentes (permis, detr etc) :
|
%p Vous pouvez aussi consulter ici la liste de nos démarches les plus frequentes (permis, detr etc) :
|
||||||
|
@ -40,8 +43,10 @@
|
||||||
%a{ href: 'https://doc.demarches-simplifiees.fr/listes-des-demarches' }
|
%a{ href: 'https://doc.demarches-simplifiees.fr/listes-des-demarches' }
|
||||||
https://doc.demarches-simplifiees.fr/listes-des-demarches
|
https://doc.demarches-simplifiees.fr/listes-des-demarches
|
||||||
|
|
||||||
.card-content.hidden{ data: { answer: "info instruction" } }
|
.support.card.featured.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_INSTRUCTION } }
|
||||||
%p Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie
|
.card-title
|
||||||
|
👉 Notre réponse
|
||||||
|
%p Si vous avez des questions sur l’instruction de votre dossier (par exemple sur les délais), nous vous invitons à contacter directement les services qui instruisent votre dossier par votre messagerie.
|
||||||
%p
|
%p
|
||||||
%a{ href: 'https://faq.demarches-simplifiees.fr/article/11-je-veux-savoir-ou-en-est-linstruction-de-ma-demarche' }
|
%a{ href: 'https://faq.demarches-simplifiees.fr/article/11-je-veux-savoir-ou-en-est-linstruction-de-ma-demarche' }
|
||||||
En savoir plus
|
En savoir plus
|
||||||
|
@ -65,7 +70,12 @@
|
||||||
= text_area_tag :text, params[:text], rows: 6, required: true
|
= text_area_tag :text, params[:text], rows: 6, required: true
|
||||||
|
|
||||||
.contact-champ
|
.contact-champ
|
||||||
= label_tag :text, 'Pièce jointe'
|
= label_tag :text do
|
||||||
|
Pièce jointe
|
||||||
|
.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AMELIORATION } }
|
||||||
|
Une capture d’écran peut nous aider à identifier plus facilement l’endroit à améliorer.
|
||||||
|
.notice.hidden{ data: { 'contact-type-only': Helpscout::FormAdapter::TYPE_AUTRE } }
|
||||||
|
Une capture d’écran peut nous aider à identifier plus facilement le problème.
|
||||||
= file_field_tag :file
|
= file_field_tag :file
|
||||||
|
|
||||||
= hidden_field_tag :tags, @tags&.join(',')
|
= hidden_field_tag :tags, @tags&.join(',')
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
.dropdown.help-dropdown
|
|
||||||
.button.primary.dropdown-button Aide
|
|
||||||
.dropdown-content.fade-in-down
|
|
||||||
%ul.dropdown-items
|
|
||||||
|
|
||||||
- title = dossier.brouillon? ? "Besoin d’aide pour remplir votre dossier ?" : "Une question sur votre dossier ?"
|
|
||||||
|
|
||||||
- if dossier.messagerie_available?
|
|
||||||
-# Contact the administration using the messagerie
|
|
||||||
%li
|
|
||||||
= link_to messagerie_dossier_path(dossier) do
|
|
||||||
%span.icon.mail
|
|
||||||
.dropdown-description
|
|
||||||
%h4.help-dropdown-title= title
|
|
||||||
%p Envoyez directement un message à l’instructeur.
|
|
||||||
|
|
||||||
- elsif dossier.procedure.service.present?
|
|
||||||
- service = dossier.procedure.service
|
|
||||||
-# Contact the administration using email or phone
|
|
||||||
%li.help-dropdown-service
|
|
||||||
%span.icon.person
|
|
||||||
.dropdown-description
|
|
||||||
%h4.help-dropdown-title= title
|
|
||||||
.help-dropdown-service-action
|
|
||||||
%p Contactez directement l’administration :
|
|
||||||
%p.help-dropdown-service-item
|
|
||||||
%span.icon.small.mail
|
|
||||||
= link_to service.email, "mailto:#{service.email}"
|
|
||||||
%p.help-dropdown-service-item
|
|
||||||
%span.icon.small.phone
|
|
||||||
= link_to service.telephone, "tel:#{service.telephone}"
|
|
||||||
%p.help-dropdown-service-item
|
|
||||||
%span.icon.small.clock
|
|
||||||
= service.horaires
|
|
||||||
|
|
||||||
-# Use the help website
|
|
||||||
%li
|
|
||||||
= link_to FAQ_URL, target: "_blank", rel: "noopener" do
|
|
||||||
%span.icon.help
|
|
||||||
.dropdown-description
|
|
||||||
%h4.help-dropdown-title Un problème avec le site ?
|
|
||||||
%p Trouvez votre réponse dans l’aide en ligne.
|
|
|
@ -13,12 +13,12 @@
|
||||||
|
|
||||||
%li
|
%li
|
||||||
Date de création :
|
Date de création :
|
||||||
= etablissement.association_date_creation&.strftime('%d/%m/%Y')
|
= try_format_date(etablissement.association_date_creation)
|
||||||
|
|
||||||
%li
|
%li
|
||||||
Date de déclaration :
|
Date de déclaration :
|
||||||
= etablissement.association_date_declaration&.strftime('%d/%m/%Y')
|
= try_format_date(etablissement.association_date_declaration)
|
||||||
|
|
||||||
%li
|
%li
|
||||||
Date de publication :
|
Date de publication :
|
||||||
= etablissement.association_date_publication&.strftime('%d/%m/%Y')
|
= try_format_date(etablissement.association_date_publication)
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
%li
|
%li
|
||||||
Date de création :
|
Date de création :
|
||||||
= etablissement.entreprise.date_creation&.strftime('%d/%m/%Y')
|
= try_format_date(etablissement.entreprise.date_creation)
|
||||||
|
|
||||||
%li
|
%li
|
||||||
Effectif organisation :
|
Effectif organisation :
|
||||||
|
|
|
@ -21,6 +21,9 @@ chdir APP_ROOT do
|
||||||
puts "\n== Updating database =="
|
puts "\n== Updating database =="
|
||||||
system! 'bin/rails db:migrate'
|
system! 'bin/rails db:migrate'
|
||||||
|
|
||||||
|
puts "\n== Running after_party tasks =="
|
||||||
|
system! 'bin/rails after_party:run'
|
||||||
|
|
||||||
puts "\n== Removing old logs =="
|
puts "\n== Removing old logs =="
|
||||||
system! 'bin/rails log:clear'
|
system! 'bin/rails log:clear'
|
||||||
|
|
||||||
|
|
8
lib/tasks/2019_03_13_migrate_pjs_to_champs.rake
Normal file
8
lib/tasks/2019_03_13_migrate_pjs_to_champs.rake
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace :'2019_03_13_migrate_pjs_to_champs' do
|
||||||
|
task run: :environment do
|
||||||
|
procedure_id = ENV['PROCEDURE_ID']
|
||||||
|
service = PieceJustificativeToChampPieceJointeMigrationService.new
|
||||||
|
service.ensure_correct_storage_configuration!
|
||||||
|
service.convert_procedure_pjs_to_champ_pjs(Procedure.find(procedure_id))
|
||||||
|
end
|
||||||
|
end
|
|
@ -18,7 +18,7 @@ describe Users::CommencerController, type: :controller do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when the path is for a non-published procedure' do
|
context 'when the path is for a draft procedure' do
|
||||||
let(:path) { draft_procedure.path }
|
let(:path) { draft_procedure.path }
|
||||||
|
|
||||||
it 'redirects with an error message' do
|
it 'redirects with an error message' do
|
||||||
|
@ -66,9 +66,10 @@ describe Users::CommencerController, type: :controller do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#sign_in' do
|
describe '#sign_in' do
|
||||||
|
context 'for a published procedure' do
|
||||||
subject { get :sign_in, params: { path: published_procedure.path } }
|
subject { get :sign_in, params: { path: published_procedure.path } }
|
||||||
|
|
||||||
it 'set the path to return after sign-in to the dossier creation path' do
|
it 'set the path to return after sign-in to the procedure start page' do
|
||||||
subject
|
subject
|
||||||
expect(controller.stored_location_for(:user)).to eq(commencer_path(path: published_procedure.path))
|
expect(controller.stored_location_for(:user)).to eq(commencer_path(path: published_procedure.path))
|
||||||
end
|
end
|
||||||
|
@ -76,14 +77,39 @@ describe Users::CommencerController, type: :controller do
|
||||||
it { expect(subject).to redirect_to(new_user_session_path) }
|
it { expect(subject).to redirect_to(new_user_session_path) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for a draft procedure' do
|
||||||
|
subject { get :sign_in, params: { path: draft_procedure.path } }
|
||||||
|
|
||||||
|
it 'set the path to return after sign-in to the draft procedure start page' do
|
||||||
|
subject
|
||||||
|
expect(controller.stored_location_for(:user)).to eq(commencer_test_path(path: draft_procedure.path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(subject).to redirect_to(new_user_session_path) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#sign_up' do
|
describe '#sign_up' do
|
||||||
|
context 'for a published procedure' do
|
||||||
subject { get :sign_up, params: { path: published_procedure.path } }
|
subject { get :sign_up, params: { path: published_procedure.path } }
|
||||||
|
|
||||||
it 'set the path to return after sign-up to the dossier creation path' do
|
it 'set the path to return after sign-up to the procedure start page' do
|
||||||
subject
|
subject
|
||||||
expect(controller.stored_location_for(:user)).to eq(commencer_path(path: published_procedure.path))
|
expect(controller.stored_location_for(:user)).to eq(commencer_path(path: published_procedure.path))
|
||||||
end
|
end
|
||||||
|
|
||||||
it { expect(subject).to redirect_to(new_user_registration_path) }
|
it { expect(subject).to redirect_to(new_user_registration_path) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'for a draft procedure' do
|
||||||
|
subject { get :sign_up, params: { path: draft_procedure.path } }
|
||||||
|
|
||||||
|
it 'set the path to return after sign-up to the draft procedure start page' do
|
||||||
|
subject
|
||||||
|
expect(controller.stored_location_for(:user)).to eq(commencer_test_path(path: draft_procedure.path))
|
||||||
|
end
|
||||||
|
|
||||||
|
it { expect(subject).to redirect_to(new_user_registration_path) }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,24 @@ feature 'Getting help:' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'on pages related to a procedure' do
|
||||||
|
let(:procedure) { create(:procedure, :published, :with_service) }
|
||||||
|
|
||||||
|
scenario 'a Help menu provides administration contacts and a link to the FAQ' do
|
||||||
|
visit commencer_path(path: procedure.path)
|
||||||
|
|
||||||
|
within('.new-header') do
|
||||||
|
expect(page).to have_help_menu
|
||||||
|
end
|
||||||
|
|
||||||
|
within('.help-dropdown') do
|
||||||
|
expect(page).to have_content(procedure.service.email)
|
||||||
|
expect(page).to have_content(procedure.service.telephone)
|
||||||
|
expect(page).to have_link(nil, href: FAQ_URL)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'as a signed-in user' do
|
context 'as a signed-in user' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:procedure) { create(:procedure, :with_service) }
|
let(:procedure) { create(:procedure, :with_service) }
|
||||||
|
|
|
@ -17,4 +17,62 @@ describe ApplicationHelper do
|
||||||
it { is_expected.to be_nil }
|
it { is_expected.to be_nil }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#try_format_date" do
|
||||||
|
subject { try_format_date(date) }
|
||||||
|
|
||||||
|
describe 'try formatting 2019-01-24' do
|
||||||
|
let(:date) { "2019-01-24" }
|
||||||
|
it { is_expected.to eq("24 January 2019") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting 24/01/2019' do
|
||||||
|
let(:date) { "24/01/2019" }
|
||||||
|
it { is_expected.to eq("24 January 2019") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting 2019-01-32' do
|
||||||
|
let(:date) { "2019-01-32" }
|
||||||
|
it { is_expected.to eq("2019-01-32") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting a blank string' do
|
||||||
|
let(:date) { "" }
|
||||||
|
it { is_expected.to eq("") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting a nil string' do
|
||||||
|
let(:date) { nil }
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#try_format_datetime" do
|
||||||
|
subject { try_format_datetime(datetime) }
|
||||||
|
|
||||||
|
describe 'try formatting 31/01/2019 11:25' do
|
||||||
|
let(:datetime) { "31/01/2019 11:25" }
|
||||||
|
it { is_expected.to eq("31 January 2019 11:25") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting 2019-01-31 11:25' do
|
||||||
|
let(:datetime) { "2019-01-31 11:25" }
|
||||||
|
it { is_expected.to eq("31 January 2019 11:25") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting 2019-01-32 11:25' do
|
||||||
|
let(:datetime) { "2019-01-32 11:25" }
|
||||||
|
it { is_expected.to eq("2019-01-32 11:25") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting a blank string' do
|
||||||
|
let(:datetime) { "" }
|
||||||
|
it { is_expected.to eq("") }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'try formatting a nil string' do
|
||||||
|
let(:datetime) { nil }
|
||||||
|
it { is_expected.to be_nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe CarrierwaveActiveStorageMigrationService do
|
||||||
|
describe '#hex_to_base64' do
|
||||||
|
let(:service) { CarrierwaveActiveStorageMigrationService.new }
|
||||||
|
|
||||||
|
it { expect(service.hex_to_base64('deadbeef')).to eq('3q2+7w==') }
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,222 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe PieceJustificativeToChampPieceJointeMigrationService do
|
||||||
|
let(:service) { PieceJustificativeToChampPieceJointeMigrationService.new(storage_service: storage_service) }
|
||||||
|
let(:storage_service) { CarrierwaveActiveStorageMigrationService.new }
|
||||||
|
let(:pj_uploader) { class_double(PieceJustificativeUploader) }
|
||||||
|
let(:pj_service) { class_double(PiecesJustificativesService) }
|
||||||
|
|
||||||
|
let(:procedure) { create(:procedure, types_de_piece_justificative: types_pj) }
|
||||||
|
let(:types_pj) { [create(:type_de_piece_justificative)] }
|
||||||
|
|
||||||
|
let!(:dossier) do
|
||||||
|
create(
|
||||||
|
:dossier,
|
||||||
|
procedure: procedure,
|
||||||
|
pieces_justificatives: pjs
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:pjs) { [] }
|
||||||
|
|
||||||
|
def make_pjs
|
||||||
|
types_pj.map do |tpj|
|
||||||
|
create(:piece_justificative, :contrat, type_de_piece_justificative: tpj)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def expect_storage_service_to_convert_object
|
||||||
|
expect(storage_service).to receive(:make_blob)
|
||||||
|
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||||
|
expect(storage_service).to receive(:make_attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when conversion succeeds' do
|
||||||
|
context 'for the procedure' do
|
||||||
|
it 'types de champ are created for the "pièces jointes" header and for each PJ' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to change { procedure.types_de_champ.count }
|
||||||
|
.by(types_pj.count + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the old types de pj are removed' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to change { procedure.types_de_piece_justificative.count }
|
||||||
|
.to(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'no notifications are sent to instructeurs' do
|
||||||
|
context 'when there is a PJ' do
|
||||||
|
let(:pjs) { make_pjs }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Reload PJ because the resolution of in-database timestamps is
|
||||||
|
# different from the resolution of in-memory timestamps, causing the
|
||||||
|
# tests to fail on fractional time differences.
|
||||||
|
pjs.last.reload
|
||||||
|
|
||||||
|
expect_storage_service_to_convert_object
|
||||||
|
Timecop.travel(1.hour) { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
|
||||||
|
# Reload the dossier to see the newly created champs
|
||||||
|
dossier.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the champ has the same created_at as the PJ' do
|
||||||
|
expect(dossier.champs.last.created_at).to eq(pjs.last.created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the champ has the same updated_at as the PJ' do
|
||||||
|
expect(dossier.champs.last.updated_at).to eq(pjs.last.updated_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is no PJ' do
|
||||||
|
let!(:expected_updated_at) do
|
||||||
|
# Reload dossier because the resolution of in-database timestamps is
|
||||||
|
# different from the resolution of in-memory timestamps, causing the
|
||||||
|
# tests to fail on fractional time differences.
|
||||||
|
dossier.reload
|
||||||
|
dossier.updated_at
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
Timecop.travel(1.hour) { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
|
||||||
|
# Reload the dossier to see the newly created champs
|
||||||
|
dossier.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the champ has the same created_at as the dossier' do
|
||||||
|
expect(dossier.champs.last.created_at).to eq(dossier.created_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the champ has the same updated_at as the dossier' do
|
||||||
|
expect(dossier.champs.last.updated_at).to eq(expected_updated_at)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'for the dossier' do
|
||||||
|
let(:pjs) { make_pjs }
|
||||||
|
|
||||||
|
before { expect_storage_service_to_convert_object }
|
||||||
|
|
||||||
|
it 'champs are created for the "pièces jointes" header and for each PJ' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to change { dossier.champs.count }
|
||||||
|
.by(types_pj.count + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the old pjs are removed' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to change { dossier.pieces_justificatives.count }
|
||||||
|
.to(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the dossier is soft-deleted it still gets converted' do
|
||||||
|
let(:pjs) { make_pjs }
|
||||||
|
|
||||||
|
let!(:dossier) do
|
||||||
|
create(
|
||||||
|
:dossier,
|
||||||
|
procedure: procedure,
|
||||||
|
pieces_justificatives: pjs,
|
||||||
|
hidden_at: Time.zone.now
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before { expect_storage_service_to_convert_object }
|
||||||
|
|
||||||
|
it 'champs are created for the "pièces jointes" header and for each PJ' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to change { dossier.champs.count }
|
||||||
|
.by(types_pj.count + 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'the old pjs are removed' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to change { dossier.pieces_justificatives.count }
|
||||||
|
.to(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there are several pjs for one type' do
|
||||||
|
let(:pjs) { make_pjs + make_pjs }
|
||||||
|
|
||||||
|
it 'only converts the most recent PJ for each type PJ' do
|
||||||
|
expect(storage_service).to receive(:make_blob).exactly(types_pj.count)
|
||||||
|
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!).exactly(types_pj.count)
|
||||||
|
expect(storage_service).to receive(:make_attachment).exactly(types_pj.count)
|
||||||
|
|
||||||
|
service.convert_procedure_pjs_to_champ_pjs(procedure)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'cleanup when conversion fails' do
|
||||||
|
let(:pjs) { make_pjs }
|
||||||
|
|
||||||
|
let!(:failing_dossier) do
|
||||||
|
create(
|
||||||
|
:dossier,
|
||||||
|
procedure: procedure,
|
||||||
|
pieces_justificatives: make_pjs
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(storage_service).to receive(:checksum).and_return('cafe')
|
||||||
|
allow(storage_service).to receive(:fix_content_type)
|
||||||
|
|
||||||
|
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||||
|
expect(storage_service).to receive(:copy_from_carrierwave_to_active_storage!)
|
||||||
|
.and_raise('LOL no!')
|
||||||
|
|
||||||
|
expect(storage_service).to receive(:delete_from_active_storage!)
|
||||||
|
end
|
||||||
|
|
||||||
|
def try_convert(procedure)
|
||||||
|
service.convert_procedure_pjs_to_champ_pjs(procedure)
|
||||||
|
rescue => e
|
||||||
|
e
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'passes on the exception' do
|
||||||
|
expect { service.convert_procedure_pjs_to_champ_pjs(procedure) }
|
||||||
|
.to raise_error('LOL no!')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create champs' do
|
||||||
|
expect { try_convert(procedure) }
|
||||||
|
.not_to change { dossier.champs.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not remove any old pjs' do
|
||||||
|
expect { try_convert(procedure) }
|
||||||
|
.not_to change { dossier.pieces_justificatives.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not creates types de champ' do
|
||||||
|
expect { try_convert(procedure) }
|
||||||
|
.not_to change { procedure.types_de_champ.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not remove old types de pj' do
|
||||||
|
expect { try_convert(procedure) }
|
||||||
|
.not_to change { procedure.types_de_piece_justificative.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not leave stale blobs behind' do
|
||||||
|
expect { try_convert(procedure) }
|
||||||
|
.not_to change { ActiveStorage::Blob.count }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not leave stale attachments behind' do
|
||||||
|
expect { try_convert(procedure) }
|
||||||
|
.not_to change { ActiveStorage::Attachment.count }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -89,4 +89,36 @@ describe PiecesJustificativesService do
|
||||||
it { expect(errors).to match([]) }
|
it { expect(errors).to match([]) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'types_pj_as_types_de_champ' do
|
||||||
|
subject { PiecesJustificativesService.types_pj_as_types_de_champ(procedure) }
|
||||||
|
|
||||||
|
it 'generates one header champ, plus one champ per PJ' do
|
||||||
|
expect(subject.pluck(:libelle)).to contain_exactly("Pièces jointes", "not mandatory")
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'remembers the id of the PJ that got converted into a champ' do
|
||||||
|
expect(subject.map(&:old_pj)).to include({ 'stable_id' => tpj_not_mandatory.id })
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without pre-existing champs' do
|
||||||
|
it 'generates a sequence of order_places incrementing from zero' do
|
||||||
|
expect(subject.pluck(:order_place)).to contain_exactly(0, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with pre-existing champs' do
|
||||||
|
let(:procedure) do
|
||||||
|
create(
|
||||||
|
:procedure,
|
||||||
|
types_de_piece_justificative: tpjs,
|
||||||
|
types_de_champ: [build(:type_de_champ, order_place: 0)]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'generates a sequence of incrementing order_places that continues where the last type de champ left off' do
|
||||||
|
expect(subject.pluck(:order_place)).to contain_exactly(1, 2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,14 +1,37 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe 'layouts/_new_header.html.haml', type: :view do
|
describe 'layouts/_new_header.html.haml', type: :view do
|
||||||
describe 'logo link' do
|
|
||||||
before do
|
before do
|
||||||
|
if user
|
||||||
sign_in user
|
sign_in user
|
||||||
allow(controller).to receive(:nav_bar_profile).and_return(profile)
|
allow(controller).to receive(:nav_bar_profile).and_return(profile)
|
||||||
render
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { rendered }
|
subject { render }
|
||||||
|
|
||||||
|
context 'when rendering without context' do
|
||||||
|
let(:user) { nil }
|
||||||
|
let(:profile) { nil }
|
||||||
|
|
||||||
|
it { is_expected.to have_css("a.header-logo[href=\"#{root_path}\"]") }
|
||||||
|
|
||||||
|
it 'displays the Help link' do
|
||||||
|
expect(subject).to have_link('Aide', href: FAQ_URL)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when on a procedure page' do
|
||||||
|
let(:procedure) { create(:procedure, :with_service) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(controller).to receive(:procedure_for_help).and_return(procedure)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'displays the Help dropdown menu' do
|
||||||
|
expect(subject).to have_css(".help-dropdown")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when rendering for user' do
|
context 'when rendering for user' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
@ -29,8 +52,7 @@ describe 'layouts/_new_header.html.haml', type: :view do
|
||||||
it { is_expected.to have_css("a.header-logo[href=\"#{gestionnaire_procedures_path}\"]") }
|
it { is_expected.to have_css("a.header-logo[href=\"#{gestionnaire_procedures_path}\"]") }
|
||||||
|
|
||||||
it 'displays the Help dropdown menu' do
|
it 'displays the Help dropdown menu' do
|
||||||
expect(rendered).to have_css(".help-dropdown")
|
expect(subject).to have_css(".help-dropdown")
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue