diff --git a/app/assets/stylesheets/utils.scss b/app/assets/stylesheets/utils.scss index 62e44fab0..19d084813 100644 --- a/app/assets/stylesheets/utils.scss +++ b/app/assets/stylesheets/utils.scss @@ -54,6 +54,10 @@ margin-top: 2 * $default-spacer; } +.mt-4 { + margin-top: 4 * $default-spacer; +} + .mb-2 { margin-bottom: 2 * $default-spacer; } diff --git a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb index 91375a9a4..25f83e4e8 100644 --- a/app/controllers/new_administrateur/groupe_instructeurs_controller.rb +++ b/app/controllers/new_administrateur/groupe_instructeurs_controller.rb @@ -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 diff --git a/app/services/instructeurs_import_service.rb b/app/services/instructeurs_import_service.rb index fa10a00de..c8db3ed3b 100644 --- a/app/services/instructeurs_import_service.rb +++ b/app/services/instructeurs_import_service.rb @@ -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 - if !gi.instructeurs.include?(instructeur) - gi.instructeurs << instructeur + target_groupes = procedure.reload.groupe_instructeurs - end - else - errors << instructeur_email + 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 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, diff --git a/app/views/new_administrateur/groupe_instructeurs/index.html.haml b/app/views/new_administrateur/groupe_instructeurs/index.html.haml index d24a706e3..facd55648 100644 --- a/app/views/new_administrateur/groupe_instructeurs/index.html.haml +++ b/app/views/new_administrateur/groupe_instructeurs/index.html.haml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 214001c4f..9dba2d095 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -428,6 +428,7 @@ Rails.application.routes.draw do collection do patch 'update_routing_criteria_name' + post 'import' end end diff --git a/public/import-groupe-test.csv b/public/import-groupe-test.csv new file mode 100644 index 000000000..1045de444 --- /dev/null +++ b/public/import-groupe-test.csv @@ -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 \ No newline at end of file diff --git a/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb b/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb index 2940a17ee..d27ae1f75 100644 --- a/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb +++ b/spec/controllers/new_administrateur/groupe_instructeurs_controller_spec.rb @@ -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, diff --git a/spec/fixtures/files/groupe-instructeur.csv b/spec/fixtures/files/groupe-instructeur.csv new file mode 100644 index 000000000..fc157e6dd --- /dev/null +++ b/spec/fixtures/files/groupe-instructeur.csv @@ -0,0 +1,3 @@ +Email,Groupe +kara@repat.africa,Afrique +kara,Afrique \ No newline at end of file diff --git a/spec/services/instructeurs_import_service_spec.rb b/spec/services/instructeurs_import_service_spec.rb index bf30936b2..285d3dc98 100644 --- a/spec/services/instructeurs_import_service_spec.rb +++ b/spec/services/instructeurs_import_service_spec.rb @@ -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