commit
96766f0ad0
52 changed files with 322 additions and 150 deletions
|
@ -21,6 +21,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
border-top: 8px solid $orange;
|
||||
|
||||
.card-title {
|
||||
color: $orange;
|
||||
}
|
||||
}
|
||||
|
||||
&.feedback {
|
||||
max-width: 600px;
|
||||
margin: 30px auto;
|
||||
|
|
|
@ -12,4 +12,8 @@
|
|||
p {
|
||||
margin: 16px auto;
|
||||
}
|
||||
|
||||
.email-address {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -265,7 +265,7 @@ class Admin::ProceduresController < AdminController
|
|||
end
|
||||
|
||||
def procedure_params
|
||||
editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :expects_multiple_submissions, :web_hook_url, :euro_flag, :logo, :auto_archive_on]
|
||||
editable_params = [:libelle, :description, :organisation, :direction, :lien_site_web, :cadre_juridique, :deliberation, :notice, :web_hook_url, :euro_flag, :logo, :auto_archive_on]
|
||||
if @procedure&.locked?
|
||||
params.require(:procedure).permit(*editable_params)
|
||||
else
|
||||
|
|
|
@ -13,7 +13,7 @@ class FranceConnect::ParticulierController < ApplicationController
|
|||
fetched_fci.tap(&:save)
|
||||
|
||||
if fci.user.nil?
|
||||
user = User.find_or_create_by(email: fci.email_france_connect) do |new_user|
|
||||
user = User.find_or_create_by!(email: fci.email_france_connect.downcase) do |new_user|
|
||||
new_user.password = Devise.friendly_token[0, 20]
|
||||
new_user.confirmed_at = Time.zone.now
|
||||
end
|
||||
|
|
|
@ -138,7 +138,7 @@ module Gestionnaires
|
|||
end
|
||||
|
||||
def commentaire_params
|
||||
params.require(:commentaire).permit(:body, :file)
|
||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -166,7 +166,7 @@ module Gestionnaires
|
|||
end
|
||||
|
||||
def commentaire_params
|
||||
params.require(:commentaire).permit(:body, :file)
|
||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
||||
end
|
||||
|
||||
def champs_private_params
|
||||
|
|
|
@ -52,7 +52,7 @@ class SupportController < ApplicationController
|
|||
email: email,
|
||||
phone: params[:phone],
|
||||
text: params[:text],
|
||||
file: params[:file],
|
||||
file: params[:piece_jointe],
|
||||
dossier_id: dossier&.id,
|
||||
browser: browser_name,
|
||||
tags: tags
|
||||
|
@ -61,7 +61,7 @@ class SupportController < ApplicationController
|
|||
|
||||
def create_commentaire
|
||||
attributes = {
|
||||
file: params[:file],
|
||||
piece_jointe: params[:piece_jointe],
|
||||
body: "[#{params[:subject]}]<br><br>#{params[:text]}"
|
||||
}
|
||||
commentaire = CommentaireService.build(current_user, dossier, attributes)
|
||||
|
|
|
@ -354,7 +354,7 @@ module Users
|
|||
end
|
||||
|
||||
def commentaire_params
|
||||
params.require(:commentaire).permit(:body, :file)
|
||||
params.require(:commentaire).permit(:body, :piece_jointe)
|
||||
end
|
||||
|
||||
def passage_en_construction?
|
||||
|
|
|
@ -12,8 +12,9 @@ module Users
|
|||
def update_email
|
||||
if @current_user.update(update_email_params)
|
||||
flash.notice = t('devise.registrations.update_needs_confirmation')
|
||||
# to avoid leaking who has signed in
|
||||
elsif @current_user.errors&.details&.dig(:email)&.any? { |e| e[:error] == :taken }
|
||||
UserMailer.account_already_taken(@current_user, requested_email).deliver_later
|
||||
# avoid leaking information about whether an account with this email exists or not
|
||||
flash.notice = t('devise.registrations.update_needs_confirmation')
|
||||
else
|
||||
flash.alert = @current_user.errors.full_messages
|
||||
|
@ -27,5 +28,9 @@ module Users
|
|||
def update_email_params
|
||||
params.require(:user).permit(:email)
|
||||
end
|
||||
|
||||
def requested_email
|
||||
update_email_params[:email]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,7 +22,6 @@ class TypesDeChampEditor extends Component {
|
|||
type_champ: 'text',
|
||||
types_de_champ: [],
|
||||
private: props.isAnnotation,
|
||||
drop_down_list_value: '--Premier élément du menu--\n',
|
||||
libelle: `${
|
||||
props.isAnnotation ? 'Nouvelle annotation' : 'Nouveau champ'
|
||||
} ${props.typeDeChampsTypes[0][0]}`
|
||||
|
|
|
@ -115,6 +115,18 @@ function updateTypeDeChamp(
|
|||
{ typeDeChamp, field, value },
|
||||
done
|
||||
) {
|
||||
if (field == 'type_champ' && !typeDeChamp.drop_down_list_value) {
|
||||
switch (value) {
|
||||
case 'linked_drop_down_list':
|
||||
typeDeChamp.drop_down_list_value =
|
||||
'--Fromage--\nbleu de sassenage\npicodon\n--Dessert--\néclair\ntarte aux pommes\n';
|
||||
break;
|
||||
case 'drop_down_list':
|
||||
case 'multiple_drop_down_list':
|
||||
typeDeChamp.drop_down_list_value = '--Premier élément du menu--\n';
|
||||
}
|
||||
}
|
||||
|
||||
typeDeChamp[field] = value;
|
||||
|
||||
getUpdateHandler(typeDeChamp, state)(done);
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
module BizDev
|
||||
BIZ_DEV_MAPPING = {
|
||||
8 =>
|
||||
{
|
||||
full_name: "Camille Garrigue",
|
||||
pipedrive_id: 3189424
|
||||
},
|
||||
9 =>
|
||||
{
|
||||
full_name: "Philippe Vrignaud",
|
||||
|
@ -14,11 +9,6 @@ module BizDev
|
|||
{
|
||||
full_name: "Benjamin Doberset",
|
||||
pipedrive_id: 4223834
|
||||
},
|
||||
11 =>
|
||||
{
|
||||
full_name: "Rédouane Bouchane",
|
||||
pipedrive_id: 4438645
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,4 +8,12 @@ class UserMailer < ApplicationMailer
|
|||
|
||||
mail(to: user.email, subject: @subject)
|
||||
end
|
||||
|
||||
def account_already_taken(user, requested_email)
|
||||
@user = user
|
||||
@requested_email = requested_email
|
||||
@subject = "Changement d’adresse email"
|
||||
|
||||
mail(to: requested_email, subject: @subject)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,10 @@ class Commentaire < ApplicationRecord
|
|||
belongs_to :gestionnaire
|
||||
|
||||
mount_uploader :file, CommentaireFileUploader
|
||||
validates :file, file_size: { maximum: 20.megabytes, message: "La taille du fichier doit être inférieure à 20 Mo" }
|
||||
validate :is_virus_free?
|
||||
validate :messagerie_available?, on: :create
|
||||
|
||||
has_one_attached :piece_jointe
|
||||
|
||||
validates :body, presence: { message: "Votre message ne peut être vide" }
|
||||
|
||||
default_scope { order(created_at: :asc) }
|
||||
|
@ -47,10 +48,15 @@ class Commentaire < ApplicationRecord
|
|||
end
|
||||
|
||||
def file_url
|
||||
if Flipflop.remote_storage?
|
||||
if piece_jointe.attached?
|
||||
if piece_jointe.virus_scanner.safe?
|
||||
Rails.application.routes.url_helpers.url_for(piece_jointe)
|
||||
end
|
||||
elsif Flipflop.remote_storage?
|
||||
RemoteDownloader.new(file.path).url
|
||||
else
|
||||
file.url
|
||||
elsif file&.url
|
||||
# FIXME: this is horrible but used only in dev and will be removed after migration
|
||||
File.join(LOCAL_DOWNLOAD_URL, file.url)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -74,12 +80,6 @@ class Commentaire < ApplicationRecord
|
|||
DossierMailer.notify_new_answer(dossier).deliver_later
|
||||
end
|
||||
|
||||
def is_virus_free?
|
||||
if file.present? && file_changed? && !ClamavService.safe_file?(file.path)
|
||||
errors.add(:file, "Virus détecté dans le fichier joint, merci de changer de fichier")
|
||||
end
|
||||
end
|
||||
|
||||
def messagerie_available?
|
||||
return if sent_by_system?
|
||||
if dossier.present? && !dossier.messagerie_available?
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
class Dossier < ApplicationRecord
|
||||
include DossierFilteringConcern
|
||||
|
||||
self.ignored_columns = [:expects_multiple_submissions]
|
||||
|
||||
enum state: {
|
||||
brouillon: 'brouillon',
|
||||
en_construction: 'en_construction',
|
||||
|
@ -115,7 +117,7 @@ class Dossier < ApplicationRecord
|
|||
scope :nearing_end_of_retention, -> (duration = '1 month') { joins(:procedure).where("en_instruction_at + (duree_conservation_dossiers_dans_ds * interval '1 month') - now() < interval ?", duration) }
|
||||
scope :since, -> (since) { where('dossiers.en_construction_at >= ?', since) }
|
||||
scope :for_api, -> {
|
||||
includes(commentaires: [],
|
||||
includes(commentaires: { piece_jointe_attachment: :blob },
|
||||
champs: [
|
||||
:geo_areas,
|
||||
:etablissement,
|
||||
|
|
|
@ -13,6 +13,11 @@ class CommentaireService
|
|||
|
||||
def build_with_email(email, dossier, params)
|
||||
attributes = params.merge(email: email, dossier: dossier)
|
||||
# For some reason ActiveStorage trows an error in tests if we passe an empty string here.
|
||||
# I suspect it could be resolved in rails 6 by using explicit `attach()`
|
||||
if attributes[:piece_jointe].blank?
|
||||
attributes.delete(:piece_jointe)
|
||||
end
|
||||
Commentaire.new(attributes)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ class LocalDownloader
|
|||
end
|
||||
|
||||
def url
|
||||
@url ||= File.join(TPS::Application::URL, 'downloads', random_folder_name, "#{@filename_suffix}.#{@extension}")
|
||||
@url ||= File.join(LOCAL_DOWNLOAD_URL, 'downloads', random_folder_name, "#{@filename_suffix}.#{@extension}")
|
||||
end
|
||||
|
||||
protected
|
||||
|
|
|
@ -127,15 +127,6 @@
|
|||
.col-md-6
|
||||
%h4 Options avancées
|
||||
|
||||
= f.label :expects_multiple_submissions do
|
||||
= f.check_box :expects_multiple_submissions
|
||||
Ajuster pour le dépôt récurrent de dossiers
|
||||
%p.help-block
|
||||
%i.fa.fa-info-circle
|
||||
Si cette démarche est conçue pour qu’une même personne y dépose régulièrement plusieurs
|
||||
dossiers, l’interface est ajustée pour rendre plus facile la création de plusieurs dossiers
|
||||
à la suite.
|
||||
|
||||
- if Flipflop.web_hook?
|
||||
%label{ for: :web_hook_url } Lien de rappel HTTP (webhook)
|
||||
= f.text_field :web_hook_url, class: 'form-control', placeholder: 'https://callback.exemple.fr/'
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
Nous restons à votre disposition si vous avez besoin d’accompagnement.
|
||||
|
||||
%p
|
||||
= render partial: "layouts/mailers/bizdev_signature", locals: { author_name: "Camille Garrigue" }
|
||||
= render partial: "layouts/mailers/bizdev_signature", locals: { author_name: "Benjamin Doberset" }
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
%h4 En construction
|
||||
Vous permettez à l'usager de modifier ses réponses au formulaire
|
||||
%li
|
||||
= link_to passer_en_instruction_gestionnaire_dossier_path(dossier.procedure, dossier), method: :post, data: { remote: true, confirm: "Confirmer vous le passage en instruction de ce dossier ?" } do
|
||||
= link_to passer_en_instruction_gestionnaire_dossier_path(dossier.procedure, dossier), method: :post, data: { remote: true, confirm: "Confirmez-vous le passage en instruction de ce dossier ?" } do
|
||||
%span.icon.in-progress
|
||||
.dropdown-description
|
||||
%h4 Passer en instruction
|
||||
|
@ -19,7 +19,7 @@
|
|||
|
||||
- if dossier.en_instruction?
|
||||
%li
|
||||
= link_to repasser_en_construction_gestionnaire_dossier_path(dossier.procedure, dossier), method: :post, data: { remote:true, confirm: "Confirmer vous le passage en construction de ce dossier ?" } do
|
||||
= link_to repasser_en_construction_gestionnaire_dossier_path(dossier.procedure, dossier), method: :post, data: { remote:true, confirm: "Confirmez-vous le passage en construction de ce dossier ?" } do
|
||||
%span.icon.edit
|
||||
.dropdown-description
|
||||
%h4 Repasser en construction
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
.messagerie
|
||||
%ul.messages-list
|
||||
- @dossier.commentaires.each do |commentaire|
|
||||
- @dossier.commentaires.with_attached_piece_jointe.each do |commentaire|
|
||||
%li
|
||||
= render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: current_gestionnaire, messagerie_seen_at: nil }
|
||||
|
||||
|
|
|
@ -26,11 +26,11 @@
|
|||
= link_to admin_procedures_path, class: "menu-item menu-link" do
|
||||
= image_tag "icons/switch-profile.svg"
|
||||
Passer en administrateur
|
||||
|
||||
%li
|
||||
= link_to profil_path, class: "menu-item menu-link" do
|
||||
= image_tag "icons/switch-profile.svg"
|
||||
Voir mon profil
|
||||
|
||||
%li
|
||||
= link_to destroy_user_session_path, method: :delete, class: "menu-item menu-link" do
|
||||
= image_tag "icons/sign-out.svg"
|
||||
|
|
|
@ -157,6 +157,11 @@
|
|||
Titre de la carte mise en avant
|
||||
%p Et voici le contenu de la carte
|
||||
|
||||
.card.warning
|
||||
.card-title
|
||||
Titre de la carte d’avertissement
|
||||
%p Et voici le contenu de la carte
|
||||
|
||||
.card.feedback
|
||||
.card-title
|
||||
Titre de la carte pour demander un avis
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.messagerie.container
|
||||
%ul.messages-list
|
||||
- dossier.commentaires.each do |commentaire|
|
||||
- dossier.commentaires.with_attached_piece_jointe.each do |commentaire|
|
||||
%li.message{ class: commentaire_is_from_me_class(commentaire, connected_user) }
|
||||
= render partial: "shared/dossiers/messages/message", locals: { commentaire: commentaire, connected_user: connected_user, messagerie_seen_at: messagerie_seen_at }
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
= f.text_area :body, rows: 5, placeholder: 'Répondre ici', required: true, class: 'message-textarea'
|
||||
.flex.justify-between.wrap
|
||||
%div
|
||||
= f.file_field :file, id: :file, accept: commentaire.file.accept_extension_list
|
||||
%label{ for: :file }
|
||||
= f.file_field :piece_jointe, id: 'piece_jointe', direct_upload: true
|
||||
%label{ for: :piece_jointe }
|
||||
.notice
|
||||
(taille max : 20 Mo)
|
||||
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
= commentaire_date(commentaire)
|
||||
.rich-text= sanitize(simple_format(commentaire.body))
|
||||
|
||||
- if commentaire.file.present?
|
||||
- if commentaire.piece_jointe.attached?
|
||||
.attachment-link
|
||||
= render partial: "shared/attachment/show", locals: { attachment: commentaire.piece_jointe.attachment }
|
||||
- elsif commentaire.file.present?
|
||||
.attachment-link
|
||||
= link_to commentaire.file_url, class: "button", target: "_blank", rel: "noopener", title: "Télécharger" do
|
||||
%span.icon.attachment
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
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 :piece_jointe
|
||||
|
||||
= hidden_field_tag :tags, @tags&.join(',')
|
||||
|
||||
|
|
20
app/views/user_mailer/account_already_taken.haml
Normal file
20
app/views/user_mailer/account_already_taken.haml
Normal file
|
@ -0,0 +1,20 @@
|
|||
- content_for(:title, @subject)
|
||||
|
||||
%p
|
||||
Bonjour,
|
||||
|
||||
%p
|
||||
L’utilisateur « #{@user.email} » a demandé le changement de son adresse vers « #{@requested_email} ».
|
||||
|
||||
%p
|
||||
Malheureusement, votre compte « #{@requested_email} » existe déjà. Nous ne pouvons pas fusionner automatiquement vos comptes.
|
||||
|
||||
%p
|
||||
%strong Nous ne pouvons donc pas effectuer le changement d’adresse email.
|
||||
|
||||
%p
|
||||
Si vous n'êtes pas à l’origine de cette demande, vous pouvez ignorer ce message. Et si vous avez besoin d’assistance, n’hésitez pas à nous contacter à
|
||||
= succeed '.' do
|
||||
= mail_to CONTACT_EMAIL
|
||||
|
||||
= render partial: "layouts/mailers/signature"
|
|
@ -1,5 +1,5 @@
|
|||
- has_delete_action = dossier.can_be_deleted_by_user?
|
||||
- has_new_dossier_action = dossier.procedure.expects_multiple_submissions? && dossier.procedure.accepts_new_dossiers?
|
||||
- has_new_dossier_action = dossier.procedure.accepts_new_dossiers?
|
||||
|
||||
- has_actions = has_delete_action || has_new_dossier_action
|
||||
|
||||
|
|
|
@ -20,4 +20,6 @@
|
|||
et
|
||||
%b échanger avec un instructeur.
|
||||
|
||||
.flex.column.align-center
|
||||
= link_to 'Accéder à votre dossier', dossier_path(@dossier), class: 'button large primary'
|
||||
= link_to 'Déposer un autre dossier', procedure_lien(@dossier.procedure)
|
||||
|
|
|
@ -7,12 +7,16 @@
|
|||
|
||||
.card
|
||||
.card-title Coordonnées
|
||||
%p Votre email est actuellement #{current_user.email}
|
||||
- if current_user.unconfirmed_email.present?
|
||||
%p
|
||||
Un email a été envoyé à #{current_user.unconfirmed_email}.
|
||||
%br
|
||||
Merci de vérifier vos emails et de cliquer sur le lien d’activation pour finaliser la validation de votre nouvelle adresse.
|
||||
Votre email est actuellement
|
||||
%span.email-address= current_user.email
|
||||
- if current_user.unconfirmed_email.present?
|
||||
.card.warning
|
||||
.card-title
|
||||
Changement en attente :
|
||||
%span.email-address= current_user.unconfirmed_email
|
||||
%p
|
||||
Pour finaliser votre changement d’adresse, vérifiez vos emails et cliquez sur le lien de confirmation.
|
||||
|
||||
= form_for @current_user, url: update_email_path, method: :patch, html: { class: 'form' } do |f|
|
||||
= f.email_field :email, value: nil, placeholder: 'Nouvelle adresse email', required: true
|
||||
|
|
|
@ -31,8 +31,6 @@ module TPS
|
|||
config.assets.paths << Rails.root.join('app', 'assets', 'fonts')
|
||||
config.assets.precompile += ['.woff']
|
||||
|
||||
URL = ENV['APP_HOST'] || "http://localhost:3000/"
|
||||
|
||||
config.active_job.queue_adapter = :delayed_job
|
||||
|
||||
config.action_view.sanitized_allowed_tags = ActionView::Base.sanitized_allowed_tags + ['u']
|
||||
|
|
|
@ -28,3 +28,6 @@ FAQ_ADMIN_URL = "https://faq.demarches-simplifiees.fr/collection/1-administrateu
|
|||
COMMENT_TROUVER_MA_DEMARCHE_URL = [FAQ_URL, "article", "59-comment-trouver-ma-demarche"].join("/")
|
||||
STATUS_PAGE_URL = "https://status.demarches-simplifiees.fr"
|
||||
MATOMO_IFRAME_URL = "https://stats.data.gouv.fr/index.php?module=CoreAdminHome&action=optOut&language=fr&&fontColor=333333&fontSize=16px&fontFamily=Muli"
|
||||
|
||||
# FIXME: This is only used in dev in couple of places and should be removed after PJ migration
|
||||
LOCAL_DOWNLOAD_URL = "http://#{ENV.fetch('APP_HOST', 'localhost:3000')}"
|
||||
|
|
|
@ -42,7 +42,7 @@ fr:
|
|||
signed_up_but_inactive: "Vous êtes bien enregistré. Vous ne pouvez cependant pas vous connecter car votre compte n'est pas encore activé."
|
||||
signed_up_but_locked: "Vous êtes bien enregistré. Vous ne pouvez cependant pas vous connecter car votre compte est verrouillé."
|
||||
signed_up_but_unconfirmed: "Nous vous avons envoyé un email contenant un lien d'activation. Ouvrez ce lien pour activer votre compte."
|
||||
update_needs_confirmation: "Votre compte a bien été mis à jour mais nous devons vérifier votre nouvelle adresse email. Merci de vérifier vos emails et de cliquer sur le lien d’activation pour finaliser la validation de votre nouvelle adresse."
|
||||
update_needs_confirmation: "Vous devez confirmer votre nouvelle adresse email. Vérifiez vos emails, et cliquez sur le lien de confirmation pour confirmer votre changement d’adresse."
|
||||
updated: "Votre compte a été modifié avec succès."
|
||||
sessions:
|
||||
signed_in: "Connecté."
|
||||
|
|
37
lib/tasks/2019_05_29_migrate_commentaire_pj.rake
Normal file
37
lib/tasks/2019_05_29_migrate_commentaire_pj.rake
Normal file
|
@ -0,0 +1,37 @@
|
|||
namespace :'2019_05_29_migrate_commentaire_pj' do
|
||||
task run: :environment do
|
||||
commentaires = Commentaire.where
|
||||
.not(file: nil)
|
||||
.left_joins(:piece_jointe_attachment)
|
||||
.where('active_storage_attachments.id IS NULL')
|
||||
.order(:created_at)
|
||||
|
||||
limit = ENV['LIMIT']
|
||||
if limit
|
||||
commentaires.limit!(limit.to_i)
|
||||
end
|
||||
|
||||
progress = ProgressReport.new(commentaires.count)
|
||||
commentaires.find_each do |commentaire|
|
||||
if commentaire.file.present?
|
||||
uri = URI.parse(URI.escape(commentaire.file_url))
|
||||
response = Typhoeus.get(uri)
|
||||
if response.success?
|
||||
filename = commentaire.file.filename || commentaire.file_identifier
|
||||
updated_at = commentaire.updated_at
|
||||
dossier_updated_at = commentaire.dossier.updated_at
|
||||
commentaire.piece_jointe.attach(
|
||||
io: StringIO.new(response.body),
|
||||
filename: filename,
|
||||
content_type: commentaire.file.content_type,
|
||||
metadata: { virus_scan_result: ActiveStorage::VirusScanner::SAFE }
|
||||
)
|
||||
commentaire.update_column(:updated_at, updated_at)
|
||||
commentaire.dossier.update_column(:updated_at, dossier_updated_at)
|
||||
end
|
||||
end
|
||||
progress.inc
|
||||
end
|
||||
progress.finish
|
||||
end
|
||||
end
|
|
@ -71,11 +71,30 @@ describe FranceConnect::ParticulierController, type: :controller do
|
|||
end
|
||||
|
||||
context 'when france_connect_particulier_id does not have an associate user' do
|
||||
context 'when the email address is not used yet' do
|
||||
it { expect { subject }.to change(User, :count).by(1) }
|
||||
it { is_expected.to redirect_to(root_path) }
|
||||
end
|
||||
|
||||
it do
|
||||
subject
|
||||
expect(User.find_by(email: email)).not_to be_nil
|
||||
context 'when the email address is already used' do
|
||||
let!(:user) { create(:user, email: email, france_connect_information: nil) }
|
||||
|
||||
it 'associates the france_connect infos with the existing user' do
|
||||
expect { subject }.not_to change(User, :count)
|
||||
expect(user.reload.loged_in_with_france_connect).to eq(User.loged_in_with_france_connects.fetch(:particulier))
|
||||
expect(subject).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a differently cased email address is already used' do
|
||||
let(:email) { 'TEST@test.com' }
|
||||
let!(:user) { create(:user, email: email.downcase, france_connect_information: nil) }
|
||||
|
||||
it 'associates the france_connect infos with the existing user' do
|
||||
expect { subject }.not_to change(User, :count)
|
||||
expect(user.reload.loged_in_with_france_connect).to eq(User.loged_in_with_france_connects.fetch(:particulier))
|
||||
expect(subject).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -92,7 +92,7 @@ describe Gestionnaires::AvisController, type: :controller do
|
|||
let(:file) { nil }
|
||||
let(:scan_result) { true }
|
||||
|
||||
subject { post :create_commentaire, params: { id: avis_without_answer.id, commentaire: { body: 'commentaire body', file: file } } }
|
||||
subject { post :create_commentaire, params: { id: avis_without_answer.id, commentaire: { body: 'commentaire body', piece_jointe: file } } }
|
||||
|
||||
before do
|
||||
allow(ClamavService).to receive(:safe_file?).and_return(scan_result)
|
||||
|
@ -110,16 +110,10 @@ describe Gestionnaires::AvisController, type: :controller do
|
|||
|
||||
it do
|
||||
subject
|
||||
expect(Commentaire.last.file.path).to include("piece_justificative_0.pdf")
|
||||
expect(Commentaire.last.piece_jointe.filename).to eq("piece_justificative_0.pdf")
|
||||
end
|
||||
|
||||
it { expect { subject }.to change(Commentaire, :count).by(1) }
|
||||
|
||||
context "and a virus" do
|
||||
let(:scan_result) { false }
|
||||
|
||||
it { expect { subject }.not_to change(Commentaire, :count) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -350,15 +350,15 @@ describe Gestionnaires::DossiersController, type: :controller do
|
|||
expect(flash.notice).to be_present
|
||||
end
|
||||
|
||||
context "when the commentaire creation fails" do
|
||||
context "when the commentaire created with virus file" do
|
||||
let(:scan_result) { false }
|
||||
|
||||
it "renders the messagerie page with the invalid commentaire" do
|
||||
expect { subject }.not_to change(Commentaire, :count)
|
||||
it "creates a commentaire (shows message that file have a virus)" do
|
||||
expect { subject }.to change(Commentaire, :count).by(1)
|
||||
expect(gestionnaire.followed_dossiers).to include(dossier)
|
||||
|
||||
expect(response).to render_template :messagerie
|
||||
expect(flash.alert).to be_present
|
||||
expect(assigns(:commentaire).body).to eq("avant\napres")
|
||||
expect(response).to redirect_to(messagerie_gestionnaire_dossier_path(dossier.procedure, dossier))
|
||||
expect(flash.notice).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -806,7 +806,7 @@ describe Users::DossiersController, type: :controller do
|
|||
id: dossier.id,
|
||||
commentaire: {
|
||||
body: body,
|
||||
file: file
|
||||
piece_jointe: file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -822,18 +822,6 @@ describe Users::DossiersController, type: :controller do
|
|||
expect(response).to redirect_to(messagerie_dossier_path(dossier))
|
||||
expect(flash.notice).to be_present
|
||||
end
|
||||
|
||||
context "when the commentaire creation fails" do
|
||||
let(:scan_result) { false }
|
||||
|
||||
it "renders the messagerie page with the invalid commentaire" do
|
||||
expect { subject }.not_to change(Commentaire, :count)
|
||||
|
||||
expect(response).to render_template :messagerie
|
||||
expect(flash.alert).to be_present
|
||||
expect(assigns(:commentaire).body).to eq("avant\napres")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ask_deletion' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Users::ProfilController, type: :controller do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before { sign_in(user) }
|
||||
|
@ -34,13 +36,17 @@ describe Users::ProfilController, type: :controller do
|
|||
end
|
||||
|
||||
context 'when the mail is already taken' do
|
||||
let!(:user2) { create(:user) }
|
||||
let(:existing_user) { create(:user) }
|
||||
|
||||
before do
|
||||
patch :update_email, params: { user: { email: user2.email } }
|
||||
perform_enqueued_jobs do
|
||||
patch :update_email, params: { user: { email: existing_user.email } }
|
||||
end
|
||||
user.reload
|
||||
end
|
||||
|
||||
it { expect(user.unconfirmed_email).to be_nil }
|
||||
it { expect(ActionMailer::Base.deliveries.last.to).to eq([existing_user.email]) }
|
||||
it { expect(response).to redirect_to(profil_path) }
|
||||
it { expect(flash.notice).to eq(I18n.t('devise.registrations.update_needs_confirmation')) }
|
||||
end
|
||||
|
|
|
@ -41,12 +41,12 @@ FactoryBot.define do
|
|||
|
||||
factory :champ_date, class: 'Champs::DateChamp' do
|
||||
type_de_champ { create(:type_de_champ_date) }
|
||||
value { 1.day.ago.iso8601 }
|
||||
value { '2019-07-10' }
|
||||
end
|
||||
|
||||
factory :champ_datetime, class: 'Champs::DatetimeChamp' do
|
||||
type_de_champ { create(:type_de_champ_datetime) }
|
||||
value { 1.day.ago.iso8601 }
|
||||
value { '15/09/1962 15:35' }
|
||||
end
|
||||
|
||||
factory :champ_number, class: 'Champs::NumberChamp' do
|
||||
|
@ -96,17 +96,17 @@ FactoryBot.define do
|
|||
|
||||
factory :champ_drop_down_list, class: 'Champs::DropDownListChamp' do
|
||||
type_de_champ { create(:type_de_champ_drop_down_list) }
|
||||
value { '' }
|
||||
value { 'choix 1' }
|
||||
end
|
||||
|
||||
factory :champ_multiple_drop_down_list, class: 'Champs::MultipleDropDownListChamp' do
|
||||
type_de_champ { create(:type_de_champ_multiple_drop_down_list) }
|
||||
value { '' }
|
||||
value { '["choix 1", "choix 2"]' }
|
||||
end
|
||||
|
||||
factory :champ_linked_drop_down_list, class: 'Champs::LinkedDropDownListChamp' do
|
||||
type_de_champ { create(:type_de_champ_linked_drop_down_list) }
|
||||
value { '{}' }
|
||||
value { '["categorie 1", "choix 1"]' }
|
||||
end
|
||||
|
||||
factory :champ_pays, class: 'Champs::PaysChamp' do
|
||||
|
@ -116,12 +116,12 @@ FactoryBot.define do
|
|||
|
||||
factory :champ_regions, class: 'Champs::RegionChamp' do
|
||||
type_de_champ { create(:type_de_champ_regions) }
|
||||
value { '' }
|
||||
value { 'Guadeloupe' }
|
||||
end
|
||||
|
||||
factory :champ_departements, class: 'Champs::DepartementChamp' do
|
||||
type_de_champ { create(:type_de_champ_departements) }
|
||||
value { '' }
|
||||
value { '971 - Guadeloupe' }
|
||||
end
|
||||
|
||||
factory :champ_engagement, class: 'Champs::EngagementChamp' do
|
||||
|
@ -136,7 +136,7 @@ FactoryBot.define do
|
|||
|
||||
factory :champ_explication, class: 'Champs::ExplicationChamp' do
|
||||
type_de_champ { create(:type_de_champ_explication) }
|
||||
value { 'une explication' }
|
||||
value { '' }
|
||||
end
|
||||
|
||||
factory :champ_dossier_link, class: 'Champs::DossierLinkChamp' do
|
||||
|
@ -164,5 +164,15 @@ FactoryBot.define do
|
|||
|
||||
factory :champ_repetition, class: 'Champs::RepetitionChamp' do
|
||||
type_de_champ { create(:type_de_champ_repetition) }
|
||||
|
||||
after(:build) do |champ_repetition, _evaluator|
|
||||
type_de_champ_text = create(:type_de_champ_text, order_place: 0, parent: champ_repetition.type_de_champ, libelle: 'Nom')
|
||||
type_de_champ_number = create(:type_de_champ_number, order_place: 1, parent: champ_repetition.type_de_champ, libelle: 'Age')
|
||||
|
||||
create(:champ_text, row: 0, type_de_champ: type_de_champ_text, parent: champ_repetition)
|
||||
create(:champ_number, row: 0, type_de_champ: type_de_champ_number, parent: champ_repetition)
|
||||
create(:champ_text, row: 1, type_de_champ: type_de_champ_text, parent: champ_repetition)
|
||||
create(:champ_number, row: 1, type_de_champ: type_de_champ_number, parent: champ_repetition)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,5 +7,9 @@ FactoryBot.define do
|
|||
commentaire.dossier = create :dossier, :en_construction
|
||||
end
|
||||
end
|
||||
|
||||
trait :with_file do
|
||||
file { Rack::Test::UploadedFile.new("./spec/fixtures/files/logo_test_procedure.png", 'application/pdf') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,14 +19,15 @@ feature 'Changing an email' do
|
|||
click_button 'Changer mon adresse'
|
||||
end
|
||||
|
||||
user.reload
|
||||
expect(user.email).to eq(old_email)
|
||||
expect(user.unconfirmed_email).to eq(new_email)
|
||||
expect(page).to have_content(I18n.t('devise.registrations.update_needs_confirmation'))
|
||||
expect(page).to have_content(old_email)
|
||||
expect(page).to have_content(new_email)
|
||||
|
||||
click_confirmation_link_for(new_email)
|
||||
|
||||
user.reload
|
||||
expect(user.email).to eq(new_email)
|
||||
expect(user.unconfirmed_email).to be_nil
|
||||
expect(page).to have_content(I18n.t('devise.confirmations.confirmed'))
|
||||
expect(page).not_to have_content(old_email)
|
||||
expect(page).to have_content(new_email)
|
||||
expect(user.reload.email).to eq(new_email)
|
||||
end
|
||||
end
|
||||
|
|
42
spec/lib/tasks/2019_05_29_migrate_commentaire_pj_spec.rb
Normal file
42
spec/lib/tasks/2019_05_29_migrate_commentaire_pj_spec.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
describe '2019_05_29_migrate_commentaire_pj.rake' do
|
||||
let(:rake_task) { Rake::Task['2019_05_29_migrate_commentaire_pj:run'] }
|
||||
|
||||
let!(:commentaires) do
|
||||
create(:commentaire)
|
||||
create(:commentaire, :with_file)
|
||||
create(:commentaire, :with_file)
|
||||
end
|
||||
|
||||
before do
|
||||
Commentaire.all.each do |commentaire|
|
||||
if commentaire.file.present?
|
||||
stub_request(:get, commentaire.file_url)
|
||||
.to_return(status: 200, body: File.read(commentaire.file.path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
ENV['LIMIT'] = nil
|
||||
rake_task.reenable
|
||||
end
|
||||
|
||||
it 'should migrate pj' do
|
||||
comment_updated_at = Commentaire.last.updated_at
|
||||
dossier_updated_at = Commentaire.last.dossier.updated_at
|
||||
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, false, false])
|
||||
rake_task.invoke
|
||||
expect(Commentaire.where(file: nil).count).to eq(1)
|
||||
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, true, true])
|
||||
expect(Commentaire.last.updated_at).to eq(comment_updated_at)
|
||||
expect(Commentaire.last.dossier.updated_at).to eq(dossier_updated_at)
|
||||
end
|
||||
|
||||
it 'should migrate pj within limit' do
|
||||
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, false, false])
|
||||
ENV['LIMIT'] = '1'
|
||||
rake_task.invoke
|
||||
expect(Commentaire.where(file: nil).count).to eq(1)
|
||||
expect(Commentaire.all.map(&:piece_jointe).map(&:attached?)).to eq([false, true, false])
|
||||
end
|
||||
end
|
|
@ -3,6 +3,10 @@ class UserMailerPreview < ActionMailer::Preview
|
|||
UserMailer.new_account_warning(user)
|
||||
end
|
||||
|
||||
def account_already_taken
|
||||
UserMailer.account_already_taken(user, 'dircab@territoires.gouv.fr')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user
|
||||
|
|
21
spec/mailers/user_mailer_spec.rb
Normal file
21
spec/mailers/user_mailer_spec.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe UserMailer, type: :mailer do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
describe '.new_account_warning' do
|
||||
subject { described_class.new_account_warning(user) }
|
||||
|
||||
it { expect(subject.to).to eq([user.email]) }
|
||||
it { expect(subject.body).to include(user.email) }
|
||||
end
|
||||
|
||||
describe '.account_already_taken' do
|
||||
let(:requested_email) { 'new@exemple.fr' }
|
||||
|
||||
subject { described_class.account_already_taken(user, requested_email) }
|
||||
|
||||
it { expect(subject.to).to eq([requested_email]) }
|
||||
it { expect(subject.body).to include(requested_email) }
|
||||
end
|
||||
end
|
|
@ -395,7 +395,7 @@ describe Champ do
|
|||
|
||||
describe "repetition" do
|
||||
let(:dossier) { create(:dossier) }
|
||||
let(:champ) { create(:champ_repetition, dossier: dossier) }
|
||||
let(:champ) { Champs::RepetitionChamp.create(dossier: dossier) }
|
||||
let(:champ_text) { create(:champ_text, row: 0) }
|
||||
let(:champ_integer_number) { create(:champ_integer_number, row: 0) }
|
||||
let(:champ_text_attrs) { attributes_for(:champ_text, row: 1) }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
require 'spec_helper'
|
||||
require 'rails_helper'
|
||||
|
||||
describe Dossier do
|
||||
include ActiveJob::TestHelper
|
||||
|
@ -31,7 +31,7 @@ describe Dossier do
|
|||
let(:procedure) { create(:procedure, duree_conservation_dossiers_dans_ds: 6) }
|
||||
let!(:young_dossier) { create(:dossier, procedure: procedure) }
|
||||
let!(:expiring_dossier) { create(:dossier, :en_instruction, en_instruction_at: 170.days.ago, procedure: procedure) }
|
||||
let!(:just_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: (6.months + 1.hour + 1.second).ago, procedure: procedure) }
|
||||
let!(:just_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: (6.months + 1.hour + 10.seconds).ago, procedure: procedure) }
|
||||
let!(:long_expired_dossier) { create(:dossier, :en_instruction, en_instruction_at: 1.year.ago, procedure: procedure) }
|
||||
|
||||
context 'with default delay to end of retention' do
|
||||
|
@ -420,7 +420,6 @@ describe Dossier do
|
|||
|
||||
it "send an email when the dossier is created for the very first time" do
|
||||
dossier = nil
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
expect do
|
||||
perform_enqueued_jobs do
|
||||
dossier = Dossier.create(procedure: procedure, state: Dossier.states.fetch(:brouillon), user: user)
|
||||
|
@ -887,7 +886,7 @@ describe Dossier do
|
|||
|
||||
describe "#check_mandatory_champs" do
|
||||
let(:procedure) { create(:procedure, :with_type_de_champ) }
|
||||
let(:dossier) { create(:dossier, :with_all_champs, procedure: procedure) }
|
||||
let(:dossier) { create(:dossier, procedure: procedure) }
|
||||
|
||||
it 'no mandatory champs' do
|
||||
expect(dossier.check_mandatory_champs).to be_empty
|
||||
|
@ -946,7 +945,11 @@ describe Dossier do
|
|||
end
|
||||
|
||||
context "when no champs" do
|
||||
let(:champ_with_error) { dossier.champs.first }
|
||||
let(:champ_with_error) do
|
||||
repetition_champ = dossier.champs.first
|
||||
text_champ = repetition_champ.rows.first.first
|
||||
text_champ
|
||||
end
|
||||
|
||||
it 'should have errors' do
|
||||
errors = dossier.check_mandatory_champs
|
||||
|
|
|
@ -41,7 +41,7 @@ describe DossierSerializer do
|
|||
|
||||
expect(subject[3][:value]).to eq(42)
|
||||
expect(subject[4][:value]).to eq(42.1)
|
||||
expect(subject[5][:value]).to eq({ primary: nil, secondary: nil })
|
||||
expect(subject[5][:value]).to eq({ primary: 'categorie 1', secondary: 'choix 1' })
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CommentaireService do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
describe '.create' do
|
||||
let(:dossier) { create :dossier, :en_construction }
|
||||
let(:sender) { dossier.user }
|
||||
let(:body) { 'Contenu du message.' }
|
||||
let(:file) { nil }
|
||||
let(:scan_result) { true }
|
||||
|
||||
subject(:commentaire) { CommentaireService.build(sender, dossier, { body: body, file: file }) }
|
||||
|
||||
before do
|
||||
allow(ClamavService).to receive(:safe_file?).and_return(scan_result)
|
||||
end
|
||||
subject(:commentaire) { CommentaireService.build(sender, dossier, { body: body, piece_jointe: file }) }
|
||||
|
||||
it 'creates a new valid commentaire' do
|
||||
expect(commentaire.email).to eq sender.email
|
||||
expect(commentaire.dossier).to eq dossier
|
||||
expect(commentaire.body).to eq 'Contenu du message.'
|
||||
expect(commentaire.file).to be_blank
|
||||
expect(commentaire.piece_jointe.attached?).to be_falsey
|
||||
expect(commentaire).to be_valid
|
||||
end
|
||||
|
||||
|
@ -34,14 +31,15 @@ describe CommentaireService do
|
|||
context 'when it has a file' do
|
||||
let(:file) { Rack::Test::UploadedFile.new("./spec/fixtures/files/piece_justificative_0.pdf", 'application/pdf') }
|
||||
|
||||
it 'saves the attached file' do
|
||||
expect(commentaire.file).to be_present
|
||||
expect(commentaire).to be_valid
|
||||
before do
|
||||
expect(ClamavService).to receive(:safe_file?).and_return(true)
|
||||
end
|
||||
|
||||
context 'and a virus' do
|
||||
let(:scan_result) { false }
|
||||
it { expect(commentaire).not_to be_valid }
|
||||
it 'saves the attached file' do
|
||||
perform_enqueued_jobs do
|
||||
commentaire.save
|
||||
expect(commentaire.piece_jointe.attached?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -157,15 +157,6 @@ describe ProcedureExportV2Service do
|
|||
context 'with repetitions' do
|
||||
let!(:dossier) { create(:dossier, :en_instruction, :with_all_champs, :for_individual, procedure: procedure) }
|
||||
let(:champ_repetition) { dossier.champs.find { |champ| champ.type_champ == 'repetition' } }
|
||||
let(:type_de_champ_text) { create(:type_de_champ_text, order_place: 0, parent: champ_repetition.type_de_champ) }
|
||||
let(:type_de_champ_number) { create(:type_de_champ_number, order_place: 1, parent: champ_repetition.type_de_champ) }
|
||||
|
||||
before do
|
||||
create(:champ_text, row: 0, type_de_champ: type_de_champ_text, parent: champ_repetition)
|
||||
create(:champ_number, row: 0, type_de_champ: type_de_champ_number, parent: champ_repetition)
|
||||
create(:champ_text, row: 1, type_de_champ: type_de_champ_text, parent: champ_repetition)
|
||||
create(:champ_number, row: 1, type_de_champ: type_de_champ_number, parent: champ_repetition)
|
||||
end
|
||||
|
||||
it 'should have sheets' do
|
||||
expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.libelle])
|
||||
|
@ -175,8 +166,8 @@ describe ProcedureExportV2Service do
|
|||
expect(repetition_sheet.headers).to eq([
|
||||
"Dossier ID",
|
||||
"Ligne",
|
||||
type_de_champ_text.libelle,
|
||||
type_de_champ_number.libelle
|
||||
"Nom",
|
||||
"Age"
|
||||
])
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
describe 'users/dossiers/dossier_actions.html.haml', type: :view do
|
||||
let(:procedure) { create(:procedure, :published, expects_multiple_submissions: true) }
|
||||
let(:procedure) { create(:procedure, :published) }
|
||||
let(:dossier) { create(:dossier, :en_construction, procedure: procedure) }
|
||||
|
||||
subject { render 'users/dossiers/dossier_actions.html.haml', dossier: dossier }
|
||||
|
@ -12,18 +12,13 @@ describe 'users/dossiers/dossier_actions.html.haml', type: :view do
|
|||
it { is_expected.not_to have_link('Supprimer le dossier') }
|
||||
end
|
||||
|
||||
context 'when the procedure doesn’t expect multiple submissions' do
|
||||
let(:procedure) { create(:procedure, :published, expects_multiple_submissions: false) }
|
||||
it { is_expected.not_to have_link('Commencer un autre dossier') }
|
||||
end
|
||||
|
||||
context 'when the procedure is closed' do
|
||||
let(:procedure) { create(:procedure, :archived, expects_multiple_submissions: true) }
|
||||
let(:procedure) { create(:procedure, :archived) }
|
||||
it { is_expected.not_to have_link('Commencer un autre dossier') }
|
||||
end
|
||||
|
||||
context 'when there are no actions to display' do
|
||||
let(:procedure) { create(:procedure, :published, expects_multiple_submissions: false) }
|
||||
let(:procedure) { create(:procedure, :archived) }
|
||||
let(:dossier) { create(:dossier, :accepte, procedure: procedure) }
|
||||
|
||||
it 'doesn’t render the menu at all' do
|
||||
|
|
Loading…
Reference in a new issue