Merge pull request #6265 from betagouv/feat/6263

ETQ Administrateur, je peux ajouter des groupes instructeurs via un fichier csv
This commit is contained in:
Kara Diaby 2021-06-15 22:16:17 +02:00 committed by GitHub
commit c7a2a5025b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 125 additions and 16 deletions

View file

@ -54,6 +54,10 @@
margin-top: 2 * $default-spacer;
}
.mt-4 {
margin-top: 4 * $default-spacer;
}
.mb-2 {
margin-bottom: 2 * $default-spacer;
}

View file

@ -1,6 +1,8 @@
module NewAdministrateur
class GroupeInstructeursController < AdministrateurController
include ActiveSupport::NumberHelper
ITEMS_PER_PAGE = 25
CSV_MAX_SIZE = 1.megabytes
def index
@procedure = procedure
@ -158,6 +160,31 @@ module NewAdministrateur
notice: "Le libellé est maintenant « #{procedure.routing_criteria_name} »."
end
def import
if (group_csv_file.content_type != "text/csv") && (marcel_content_type != "text/csv")
flash[:alert] = "Importation impossible : veuillez importer un fichier CSV"
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
elsif group_csv_file.size > CSV_MAX_SIZE
flash[:alert] = "Importation impossible : la poids du fichier est supérieur à #{number_to_human_size(CSV_MAX_SIZE)}"
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
else
groupes_emails = CSV.new(group_csv_file.read, headers: true, header_converters: :downcase)
.map { |r| r.to_h.slice('groupe', 'email') }
add_instructeurs_and_get_errors = InstructeursImportService.import(procedure, groupes_emails)
if add_instructeurs_and_get_errors.empty?
flash[:notice] = "La liste des instructeurs a été importée avec succès"
else
flash[:alert] = "Import terminé. Cependant les emails suivants ne sont pas pris en compte: #{add_instructeurs_and_get_errors.join(', ')}"
end
redirect_to admin_procedure_groupe_instructeurs_path(procedure)
end
end
private
def create_instructeur(email)
@ -214,5 +241,13 @@ module NewAdministrateur
assigned = groupe_instructeur.instructeurs.map(&:email)
(all - assigned).sort
end
def group_csv_file
params[:group_csv_file]
end
def marcel_content_type
Marcel::MimeType.for(group_csv_file.read, name: group_csv_file.original_filename, declared_type: group_csv_file.content_type)
end
end
end

View file

@ -1,24 +1,39 @@
class InstructeursImportService
def import(procedure, groupes_emails)
def self.import(procedure, groupes_emails)
created_at = Time.zone.now
updated_at = Time.zone.now
admins = procedure.administrateurs
errors = []
groupes_emails, error_groupe_emails = groupes_emails
.map { |groupe_email| { "groupe" => groupe_email["groupe"].strip, "email" => groupe_email["email"].gsub(/[[:space:]]/, '').downcase } }
.partition { |groupe_email| Devise.email_regexp.match?(groupe_email['email']) && groupe_email['groupe'].present? }
groupes_emails.each do |groupe_emails|
groupe = groupe_emails["groupe"].strip
instructeur_email = groupe_emails["email"].strip.downcase
errors = error_groupe_emails.map { |group_email| group_email['email'] }
if groupe.present? && Devise.email_regexp.match?(instructeur_email)
gi = procedure.groupe_instructeurs.find_or_create_by!(label: groupe)
target_labels = groupes_emails.map { |groupe_email| groupe_email['groupe'] }.uniq
missing_labels = target_labels - procedure.groupe_instructeurs.pluck(:label)
instructeur = Instructeur.by_email(instructeur_email) || create_instructeur(admins, instructeur_email)
if missing_labels.present?
GroupeInstructeur.insert_all(missing_labels.map { |label| { label: label, procedure_id: procedure.id, created_at: created_at, updated_at: updated_at } })
end
target_groupes = procedure.reload.groupe_instructeurs
target_emails = groupes_emails.map { |groupe_email| groupe_email["email"] }.uniq
existing_emails = Instructeur.where(user: { email: target_emails }).pluck(:email)
missing_emails = target_emails - existing_emails
missing_emails.each { |email| create_instructeur(admins, email) }
target_instructeurs = User.where(email: target_emails).map(&:instructeur)
groupes_emails.each do |groupe_email|
gi = target_groupes.find { |g| g.label == groupe_email['groupe'] }
instructeur = target_instructeurs.find { |i| i.email == groupe_email['email'] }
if !gi.instructeurs.include?(instructeur)
gi.instructeurs << instructeur
end
else
errors << instructeur_email
end
end
@ -27,7 +42,7 @@ class InstructeursImportService
private
def create_instructeur(administrateurs, email)
def self.create_instructeur(administrateurs, email)
user = User.create_or_promote_to_instructeur(
email,
SecureRandom.hex,

View file

@ -25,6 +25,15 @@
= f.text_field :label, placeholder: 'ex. Ville de Bordeaux', required: true
= f.submit 'Ajouter le groupe', class: 'button primary send'
- csv_max_size = NewAdministrateur::GroupeInstructeursController::CSV_MAX_SIZE
= form_tag import_admin_procedure_groupe_instructeurs_path(@procedure), method: :post, multipart: true, class: "mt-4 form" do
= label_tag "Importer par fichier CSV"
%p.notice Le fichier csv doit comporter 2 colonnes (Groupe, Email) et être séparé par des virgules. L'import n'écrase pas les groupes et les instructeurs existants.
%p.notice Le poids du fichier doit être inférieur à #{number_to_human_size(csv_max_size)}
%p.mt-2.mb-2= link_to "Télécharger l'exemple de fichier CSV", "/import-groupe-test.csv"
= file_field_tag :group_csv_file, required: true, accept: 'text/csv', size: "1"
= submit_tag "Importer le fichier", class: 'button primary send', data: { disable_with: "Envoi..." }
%table.table.mt-2
%thead
%tr

View file

@ -428,6 +428,7 @@ Rails.application.routes.draw do
collection do
patch 'update_routing_criteria_name'
post 'import'
end
end

View file

@ -0,0 +1,5 @@
Email,Groupe
camilia@gouv.fr,Nord
kara@gouv.fr,Finistère
simon@gouv.fr,Isère
pauline@gouv.fr,Bouches-du-Rhône
1 Email Groupe
2 camilia@gouv.fr Nord
3 kara@gouv.fr Finistère
4 simon@gouv.fr Isère
5 pauline@gouv.fr Bouches-du-Rhône

View file

@ -349,6 +349,44 @@ describe NewAdministrateur::GroupeInstructeursController, type: :controller do
end
end
describe '#add_groupe_instructeurs_via_csv_file' do
subject do
post :import, params: { procedure_id: procedure.id, group_csv_file: csv_file }
end
context 'when the csv file is less than 1 mo' do
let(:csv_file) { fixture_file_upload('spec/fixtures/files/groupe-instructeur.csv', 'text/csv') }
before { subject }
it { expect(response.status).to eq(302) }
it { expect(procedure.groupe_instructeurs.last.label).to eq("Afrique") }
it { expect(flash.alert).to be_present }
it { expect(flash.alert).to eq("Import terminé. Cependant les emails suivants ne sont pas pris en compte: kara") }
end
context 'when the csv file length is more than 1 mo' do
let(:csv_file) { fixture_file_upload('spec/fixtures/files/groupe-instructeur.csv', 'text/csv') }
before do
allow_any_instance_of(ActionDispatch::Http::UploadedFile).to receive(:size).and_return(3.megabytes)
subject
end
it { expect(flash.alert).to be_present }
it { expect(flash.alert).to eq("Importation impossible : la poids du fichier est supérieur à 1 Mo") }
end
context 'when the file is not a csv' do
let(:csv_file) { fixture_file_upload('spec/fixtures/files/french-flag.gif', 'image/gif') }
before { subject }
it { expect(flash.alert).to be_present }
it { expect(flash.alert).to eq("Importation impossible : veuillez importer un fichier CSV") }
end
end
describe '#update_routing_criteria_name' do
before do
patch :update_routing_criteria_name,

View file

@ -0,0 +1,3 @@
Email,Groupe
kara@repat.africa,Afrique
kara,Afrique
1 Email Groupe
2 kara@repat.africa Afrique
3 kara Afrique

View file

@ -1,6 +1,5 @@
describe InstructeursImportService do
describe '#import' do
let(:service) { InstructeursImportService.new }
let(:procedure) { create(:procedure) }
let(:procedure_groupes) do
@ -10,7 +9,7 @@ describe InstructeursImportService do
.to_h
end
subject { service.import(procedure, lines) }
subject { described_class.import(procedure, lines) }
context 'nominal case' do
let(:lines) do