Merge pull request #3699 from betagouv/dev

2019-03-28-01
This commit is contained in:
Frederic Merizen 2019-03-28 15:01:21 +01:00 committed by GitHub
commit 048c167a25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 935 additions and 227 deletions

View file

@ -8,4 +8,8 @@ $contact-padding: $default-space * 2;
.description { .description {
padding-bottom: $contact-padding; padding-bottom: $contact-padding;
} }
.hidden {
display: none;
}
} }

View file

@ -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;
}

View file

@ -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;
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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);
}
});

View file

@ -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?)

View 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 blobs 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

View file

@ -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 doesnt 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

View file

@ -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",

View file

@ -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')

View file

@ -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 laide 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}.

View 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

View file

@ -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'

View file

@ -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
&nbsp; &nbsp; 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
&nbsp; &nbsp; Instructeur
Instructeur
- if administrateur_signed_in?
%li
= link_to(admin_procedures_path) do
%i.fa.fa-user
&nbsp;
Administrateur

View file

@ -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 }

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -0,0 +1 @@
= link_to 'Aide', FAQ_URL, class: 'button primary'

View 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 daide 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'

View file

@ -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'

View 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'

View file

@ -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}.

View 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 laide en ligne.

View file

@ -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 à linstructeur.

View 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 ladministration :
%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

View file

@ -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 ladministration en charge de votre démarche pour quelle 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 ladministration en charge de votre démarche pour quelle 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 linstruction 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 linstruction 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 lendroit à 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(',')

View file

@ -1,42 +0,0 @@
.dropdown.help-dropdown
.button.primary.dropdown-button Aide
.dropdown-content.fade-in-down
%ul.dropdown-items
- title = dossier.brouillon? ? "Besoin daide 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 à linstructeur.
- 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 ladministration :
%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 laide en ligne.

View file

@ -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)

View file

@ -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 :

View file

@ -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'

View 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

View file

@ -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

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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