From a339cb35e7f561e12656a53faa85319fb2201fb1 Mon Sep 17 00:00:00 2001
From: pabois <pierreandre.boissinot@noesya.coop>
Date: Mon, 23 May 2022 18:14:52 +0200
Subject: [PATCH] wip refont imports

---
 Gemfile                                       |  1 +
 Gemfile.lock                                  |  4 +
 .../organizations/imports_controller.rb       | 17 ++--
 app/models/import.rb                          | 86 +++++++++++++++++++
 app/models/university.rb                      |  2 +
 .../with_people_and_organizations.rb          | 10 ---
 .../admin/university/organizations/imports.rb | 13 +++
 app/services/importers/base.rb                | 59 +++++++++++++
 app/services/importers/organizations.rb       | 83 ++++++++++++++++++
 app/views/admin/imports/_list.html.erb        | 22 +++++
 app/views/admin/imports/show.html.erb         | 27 ++++++
 .../admin/university/alumni/index.html.erb    |  4 +-
 .../organizations/imports/index.html.erb      | 23 ++---
 .../organizations/imports/new.html.erb        | 14 +--
 .../organizations/imports/show.html.erb       |  5 --
 .../university/organizations/index.html.erb   |  4 +-
 config/initializers/delayed_jobs.rb           |  5 ++
 config/locales/en.yml                         | 32 ++++++-
 config/locales/fr.yml                         | 32 ++++++-
 config/locales/university/en.yml              |  6 +-
 config/locales/university/fr.yml              |  6 +-
 db/migrate/20220523102030_create_imports.rb   | 13 +++
 db/schema.rb                                  | 26 +++---
 23 files changed, 419 insertions(+), 75 deletions(-)
 create mode 100644 app/models/import.rb
 create mode 100644 app/services/filters/admin/university/organizations/imports.rb
 create mode 100644 app/services/importers/base.rb
 create mode 100644 app/services/importers/organizations.rb
 create mode 100644 app/views/admin/imports/_list.html.erb
 create mode 100644 app/views/admin/imports/show.html.erb
 delete mode 100644 app/views/admin/university/organizations/imports/show.html.erb
 create mode 100644 config/initializers/delayed_jobs.rb
 create mode 100644 db/migrate/20220523102030_create_imports.rb

diff --git a/Gemfile b/Gemfile
index b01ab29ad..74724d81f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -39,6 +39,7 @@ gem 'pg', '~> 1.1'
 gem 'puma'
 gem 'rails', '~> 6.1'
 gem 'rails-i18n'
+gem "roo", "~> 2.9"
 gem 'sanitize'
 gem 'sassc-rails'
 gem 'sib-api-v3-sdk'
diff --git a/Gemfile.lock b/Gemfile.lock
index e76deb9ae..3ee382ac0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -381,6 +381,9 @@ GEM
       actionpack (>= 5.0)
       railties (>= 5.0)
     rexml (3.2.5)
+    roo (2.9.0)
+      nokogiri (~> 1)
+      rubyzip (>= 1.3.0, < 3.0.0)
     rotp (6.2.0)
     ruby-saml (1.14.0)
       nokogiri (>= 1.10.5)
@@ -523,6 +526,7 @@ DEPENDENCIES
   rack-mini-profiler (~> 2.0)
   rails (~> 6.1)
   rails-i18n
+  roo (~> 2.9)
   sanitize
   sassc-rails
   selenium-webdriver
diff --git a/app/controllers/admin/university/organizations/imports_controller.rb b/app/controllers/admin/university/organizations/imports_controller.rb
index 608d208ac..31b2f8b3e 100644
--- a/app/controllers/admin/university/organizations/imports_controller.rb
+++ b/app/controllers/admin/university/organizations/imports_controller.rb
@@ -1,14 +1,18 @@
 class Admin::University::Organizations::ImportsController < Admin::University::ApplicationController
-  load_and_authorize_resource class: University::Organization::Import,
+  load_and_authorize_resource class: Import,
                               through: :current_university,
-                              through_association: :organization_imports
+                              through_association: :imports
+
+  has_scope :for_status
 
   def index
+    @imports = apply_scopes(@imports.kind_organizations).ordered.page(params[:page])
     breadcrumb
   end
 
   def show
     breadcrumb
+    render 'admin/imports/show'
   end
 
   def new
@@ -16,11 +20,14 @@ class Admin::University::Organizations::ImportsController < Admin::University::A
   end
 
   def create
+    @import.kind = :organizations
     @import.university = current_university
     @import.user = current_user
     if @import.save
-      redirect_to [:admin, @import], notice: "Import was successfully created."
+      redirect_to admin_university_organizations_import_path(@import),
+                  notice: t('admin.successfully_created_html', model: @import.to_s)
     else
+      breadcrumb
       render :new, status: :unprocessable_entity
     end
   end
@@ -31,7 +38,7 @@ class Admin::University::Organizations::ImportsController < Admin::University::A
     super
     add_breadcrumb  University::Organization.model_name.human(count: 2),
                     admin_university_organizations_path
-    add_breadcrumb  University::Organization::Import.model_name.human(count: 2),
+    add_breadcrumb  Import.model_name.human(count: 2),
                     admin_university_organizations_imports_path
     return unless @import
     @import.persisted?  ? add_breadcrumb(@import, admin_university_organizations_import_path(@import))
@@ -39,7 +46,7 @@ class Admin::University::Organizations::ImportsController < Admin::University::A
   end
 
   def import_params
-    params.require(:university_organization_import)
+    params.require(:import)
           .permit(:file)
   end
 end
diff --git a/app/models/import.rb b/app/models/import.rb
new file mode 100644
index 000000000..0d1181faa
--- /dev/null
+++ b/app/models/import.rb
@@ -0,0 +1,86 @@
+# == Schema Information
+#
+# Table name: imports
+#
+#  id                :uuid             not null, primary key
+#  kind              :integer
+#  number_of_lines   :integer
+#  processing_errors :jsonb
+#  status            :integer          default("pending")
+#  created_at        :datetime         not null
+#  updated_at        :datetime         not null
+#  university_id     :uuid             not null, indexed
+#  user_id           :uuid             not null, indexed
+#
+# Indexes
+#
+#  index_imports_on_university_id  (university_id)
+#  index_imports_on_user_id        (user_id)
+#
+# Foreign Keys
+#
+#  fk_rails_42cc64a226  (university_id => universities.id)
+#  fk_rails_b1e2154c26  (user_id => users.id)
+#
+class Import < ApplicationRecord
+  belongs_to :university
+  belongs_to :user
+
+  has_one_attached_deletable :file
+
+
+  enum kind: { organizations: 0, alumni: 1 }, _prefix: :kind
+  enum status: { pending: 0, finished: 1, finished_with_errors: 2 }
+
+  validate :file_validation
+
+  after_create :queue_for_processing
+  after_commit :send_mail_to_creator, on: :update, if: :status_changed_from_pending?
+
+  scope :for_status, -> (status) { where(status: status) }
+  scope :ordered, -> { order('created_at DESC') }
+
+  def to_s
+    I18n.l created_at, format: :date_with_hour
+  end
+
+  def status_class
+    return 'text-danger' if finished_with_errors?
+    return 'text-info' if finished?
+    return ''
+  end
+
+  # Setter to serialize data as JSON
+  def processing_errors=(value)
+    value = JSON.parse value if value.is_a? String
+    super(value)
+  end
+
+  private
+
+  def file_validation
+    if file.attached?
+      unless file.blob.content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+        file.purge if persisted?
+        errors.add(:file, :incorrect_type)
+      end
+    else
+      errors.add(:file, :no_file)
+    end
+  end
+
+  def queue_for_processing
+    "Importers::#{kind.capitalize}".constantize.delay(queue: 'imports', priority: 100).execute(self)
+  end
+
+  def send_mail_to_creator
+    # TODO
+    # ImportsMailer.companies(self).deliver_later
+  end
+
+  def status_changed_from_pending?
+    saved_change_to_status? && status_before_last_save == 'pending'
+  end
+
+
+end
diff --git a/app/models/university.rb b/app/models/university.rb
index 67325b683..8780933af 100644
--- a/app/models/university.rb
+++ b/app/models/university.rb
@@ -41,6 +41,8 @@ class University < ApplicationRecord
   # We use after_destroy to let the attachment go first
   has_many :active_storage_blobs, class_name: 'ActiveStorage::Blob'
 
+  has_many :imports, dependent: :destroy
+
   validates_presence_of :name
   validates :sms_sender_name, presence: true, length: { maximum: 11 }
 
diff --git a/app/models/university/with_people_and_organizations.rb b/app/models/university/with_people_and_organizations.rb
index ad18670f9..d543dcbc3 100644
--- a/app/models/university/with_people_and_organizations.rb
+++ b/app/models/university/with_people_and_organizations.rb
@@ -12,15 +12,5 @@ module University::WithPeopleAndOrganizations
               dependent: :destroy
     alias_attribute :organizations, :university_organizations
 
-    has_many  :university_organization_imports,
-              class_name: 'University::Organization::Import',
-              dependent: :destroy
-    alias_attribute :organization_imports, :university_organization_imports
-
-    has_many :university_person_alumnus_imports,
-              class_name: 'University::Person::Alumnus::Import',
-              dependent: :destroy
-    alias_attribute :person_alumnus_imports, :university_person_alumnus_imports
-    alias_attribute :alumnus_imports, :university_person_alumnus_imports
   end
 end
diff --git a/app/services/filters/admin/university/organizations/imports.rb b/app/services/filters/admin/university/organizations/imports.rb
new file mode 100644
index 000000000..c0dc57631
--- /dev/null
+++ b/app/services/filters/admin/university/organizations/imports.rb
@@ -0,0 +1,13 @@
+module Filters
+  class Admin::University::Organizations::Imports < Filters::Base
+    def initialize(user)
+      super
+      add :for_status,
+          ::Import::statuses.keys.map { |r| { to_s: I18n.t("enums.import.status.#{r}"), id: r } },
+          I18n.t(
+            'filters.attributes.element',
+            element: Import.human_attribute_name('status').downcase
+          )
+    end
+  end
+end
diff --git a/app/services/importers/base.rb b/app/services/importers/base.rb
new file mode 100644
index 000000000..45b317bd6
--- /dev/null
+++ b/app/services/importers/base.rb
@@ -0,0 +1,59 @@
+module Importers
+  class Base
+    def self.execute(import)
+      new(import)
+    end
+
+    def initialize(import)
+      @import = import
+      @university = import.university
+      @errors = []
+      analyze_xlsx
+      manage_errors
+      save
+    end
+
+    protected
+
+    def xlsx
+      return @xlsx if defined?(@xlsx)
+      @xlsx = Roo::Spreadsheet.open(@import.file.url, extension: :xlsx)
+      begin @xlsx.info
+        # ensure we can access basic infos on the excel file. If not the file was incorrect
+      rescue
+        add_error("Unable to analyse the xlsx file", 0)
+        @xlsx = nil
+      end
+    end
+
+    def analyze_xlsx
+      xlsx.each.with_index do |hash, index|
+        next if index == 0 # Column labels
+        analyze_hash(hash, index)
+      end if xlsx
+    end
+
+    def analyze_hash(hash, index)
+      raise NotImplementedError
+    end
+
+    def add_error(error, line)
+      @errors << { line: line, error: error }
+    end
+
+    def manage_errors
+      if @errors.count > 0
+        @import.status = :finished_with_errors
+        @import.processing_errors = @errors
+      else
+        @import.status = :finished
+      end
+    end
+
+    def save
+      @import.number_of_lines = xlsx.nil? ? 0 : xlsx.count - 1
+      @import.save
+    end
+  end
+
+end
diff --git a/app/services/importers/organizations.rb b/app/services/importers/organizations.rb
new file mode 100644
index 000000000..ecbb55d66
--- /dev/null
+++ b/app/services/importers/organizations.rb
@@ -0,0 +1,83 @@
+module Importers
+  class Organizations < Base
+
+    protected
+
+    def analyze_hash(hash, index)
+      hash_to_organization = HashToOrganization.new(@university, hash)
+      add_error(hash_to_organization.error, index + 1) unless hash_to_organization.valid?
+    end
+
+  end
+
+  class HashToOrganization
+    def initialize(university, hash)
+      @university = university
+      @hash = hash
+      @error = nil
+      extract_variables
+      save if valid?
+    end
+
+    def valid?
+      if country_not_found?
+        @error = "Country #{@country} not found"
+      elsif !organization.valid?
+        @error = "Unable to create the organization: #{organization.errors.full_messages}"
+      end
+      @error.nil?
+    end
+
+    def error
+      @error
+    end
+
+    def organization_name
+      @organization_name ||= @hash[0].to_s.strip
+    end
+
+    protected
+
+    def extract_variables
+      @long_name = @hash[1].to_s.strip
+      @kind = @hash[2].to_s.strip
+      @siren = @hash[3].to_s.strip
+      @nic = @hash[4].to_s.strip
+      @description = @hash[5].to_s.strip
+      @address = @hash[6].to_s.strip
+      @zipcode = @hash[7].to_s.strip
+      @city = @hash[8].to_s.strip
+      @country = @hash[9].to_s.strip
+      @email = @hash[10].to_s.strip
+      @phone = @hash[11].to_s.strip
+      @url = @hash[12].to_s.strip
+    end
+
+    def country_not_found?
+      ISO3166::Country[@country].nil?
+    end
+
+    def organization
+      unless @organization
+        @organization = University::Organization.where(university_id: @university.id, name: organization_name).first_or_initialize
+        @organization.long_name = @long_name
+        @organization.kind = @kind.to_sym
+        @organization.siren = @siren
+        @organization.nic = @nic
+        @organization.description = @description
+        @organization.address = @address
+        @organization.zipcode = @zipcode
+        @organization.city = @city
+        @organization.country = @country
+        @organization.email = @email
+        @organization.phone = @phone
+        @organization.url = @url
+      end
+      @organization
+    end
+
+    def save
+      organization.save
+    end
+  end
+end
diff --git a/app/views/admin/imports/_list.html.erb b/app/views/admin/imports/_list.html.erb
new file mode 100644
index 000000000..91a993564
--- /dev/null
+++ b/app/views/admin/imports/_list.html.erb
@@ -0,0 +1,22 @@
+<table class="<%= table_classes %>">
+  <thead>
+    <tr>
+      <th><%= Import.human_attribute_name('date') %></th>
+      <th><%= Import.human_attribute_name('number_of_lines') %></th>
+      <th><%= Import.human_attribute_name('status') %></th>
+      <th><%= Import.human_attribute_name('file') %></th>
+    </tr>
+  </thead>
+  <tbody>
+    <% imports.each do |import| %>
+      <tr>
+        <td><%= link_to import, send(path_pattern, import) %></td>
+        <td><%= import.number_of_lines %></td>
+        <td class="<%= import.status_class %>"><%= t("enums.import.status.#{import.status}") %></td>
+        <td><%= link_to t('download'), url_for(import.file), class: button_classes if import.file.attached? %></td>
+      </tr>
+    <% end %>
+  </tbody>
+</table>
+
+<%= paginate imports, theme: 'bootstrap-5' %>
diff --git a/app/views/admin/imports/show.html.erb b/app/views/admin/imports/show.html.erb
new file mode 100644
index 000000000..468671382
--- /dev/null
+++ b/app/views/admin/imports/show.html.erb
@@ -0,0 +1,27 @@
+<% content_for :title, @import.to_s %>
+
+<div class="row">
+  <div class="col-md-6">
+    <p><%= t('imports.initiated_by') %> <%= link_to_if can?(:read, @import.user), @import.user, [:admin, @import.user] %></p>
+    <% if @import.file.attached? %>
+      <p><%= link_to t('download_with_size', size: number_to_human_size(@import.file.byte_size)), url_for(@import.file), class: button_classes  %></p>
+    <% end %>
+  </div>
+  <div class="col-md-6">
+    <h2><%= t('imports.status') %> <span class="<%= @import.status_class %>"><%= t("enums.import.status.#{@import.status}") %></span></h2>
+    <% unless @import.pending? %>
+      <p><%= t('imports.number_of_lines') %> <%= @import.number_of_lines %></p>
+    <% end %>
+    <% if @import.pending? && @import.user == current_user %>
+      <p><%= t('imports.still_pending') %></p>
+    <% end %>
+    <% if @import.finished_with_errors? %>
+      <h3><%= t('imports.errors') %></h3>
+      <ul>
+        <% @import.processing_errors.each do |obj| %>
+          <li><%= t('imports.error_msg', line: obj['line'], error: obj['error']) %></li>
+        <% end %>
+      </ul>
+    <% end %>
+  </div>
+</div>
diff --git a/app/views/admin/university/alumni/index.html.erb b/app/views/admin/university/alumni/index.html.erb
index 6db0169c7..a9bd4f7f8 100644
--- a/app/views/admin/university/alumni/index.html.erb
+++ b/app/views/admin/university/alumni/index.html.erb
@@ -7,9 +7,9 @@
 <%= paginate @alumni, theme: 'bootstrap-5' %>
 
 <% content_for :action_bar_left do %>
-  <%= link_to t('import'),
+  <%= link_to t('import_btn'),
               new_admin_university_alumni_import_path,
-              class: button_classes if can? :manage, University::Person::Alumnus::Import %>
+              class: button_classes if can? :create, University::Person::Alumnus %>
 <% end %>
 
 <% content_for :action_bar_right do %>
diff --git a/app/views/admin/university/organizations/imports/index.html.erb b/app/views/admin/university/organizations/imports/index.html.erb
index 898580ffa..5b504270c 100644
--- a/app/views/admin/university/organizations/imports/index.html.erb
+++ b/app/views/admin/university/organizations/imports/index.html.erb
@@ -1,24 +1,11 @@
-<% content_for :title, University::Organization::Import.model_name.human(count: 2) %>
+<% content_for :title, Import.model_name.human(count: 2) %>
 
-<table class="<%= table_classes %>">
-  <thead>
-    <tr>
-      <th><%= University::Organization::Import.human_attribute_name('name') %></th>
-      <th><%= University::Organization::Import.human_attribute_name('lines') %></th>
-    </tr>
-  </thead>
-  <tbody>
-    <% @imports.each do |import| %>
-      <tr>
-        <td><%= link_to import, admin_university_organizations_import_path(import) %></td>
-        <td><%= import.lines %></td>
-      </tr>
-    <% end %>
-  </tbody>
-</table>
+<%= render 'filters', current_path: admin_university_organizations_imports_path, filters: @filters if @filters.any?  %>
+
+<%= render 'admin/imports/list', imports: @imports, path_pattern: 'admin_university_organizations_import_path' %>
 
 <% content_for :action_bar_right do %>
-  <%= link_to_if  can?(:create, University::Organization::Import),
+  <%= link_to_if  can?(:create, University::Organization),
                   t('create'),
                   new_admin_university_organizations_import_path,
                   class: button_classes %>
diff --git a/app/views/admin/university/organizations/imports/new.html.erb b/app/views/admin/university/organizations/imports/new.html.erb
index 579fe470d..2ff9088b4 100644
--- a/app/views/admin/university/organizations/imports/new.html.erb
+++ b/app/views/admin/university/organizations/imports/new.html.erb
@@ -1,21 +1,21 @@
-<% content_for :title, University::Organization::Import.model_name.human %>
+<% content_for :title, Import.model_name.human %>
 
 <div class="row">
   <div class="col-md-6">
     <p>
-      Les données doivent être au format csv, comme l'exemple suivant.<br>
-      La première ligne doit être dédiée aux entêtes.<br>
-      Les noms des entêtes sont obligatoires et doivent être respectés strictement.<br>
-      Le champ name est obligatoire.<br>
-      Les caractères doivent être encodés en UTF-8.<br>
-      Les valeurs possibles pour kind sont : company, non_profit, government.
+      <%= t('imports.hint_html') %>
+      <br>
+      <%= t('university.import_hint_html') %>
     </p>
     <%= simple_form_for @import,
                         url: admin_university_organizations_imports_path do |f| %>
+      <%# as file can be empty the global object can be unset. To prevent crash in controller add an (unused) hidden field %>
+      <%= f.input :id, as: :hidden %>
       <%= f.error_notification %>
       <%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
 
       <%= f.input :file %>
+
       <% content_for :action_bar_right do %>
         <%= submit f %>
       <% end %>
diff --git a/app/views/admin/university/organizations/imports/show.html.erb b/app/views/admin/university/organizations/imports/show.html.erb
deleted file mode 100644
index 179493a22..000000000
--- a/app/views/admin/university/organizations/imports/show.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<% content_for :title, @import %>
-
-<%= link_to "#{@import.file.filename} (#{number_to_human_size @import.file.byte_size})",
-            url_for(@import.file),
-            class: button_classes %>
diff --git a/app/views/admin/university/organizations/index.html.erb b/app/views/admin/university/organizations/index.html.erb
index ad22a22de..d578aa1d6 100644
--- a/app/views/admin/university/organizations/index.html.erb
+++ b/app/views/admin/university/organizations/index.html.erb
@@ -6,9 +6,9 @@
 <%= paginate @organizations, theme: 'bootstrap-5' %>
 
 <% content_for :action_bar_left do %>
-  <%= link_to t('import'),
+  <%= link_to t('import_btn'),
               new_admin_university_organizations_import_path,
-              class: button_classes if can? :manage, University::Organization::Import %>
+              class: button_classes if can? :create, University::Organization %>
 <% end %>
 
 <% content_for :action_bar_right do %>
diff --git a/config/initializers/delayed_jobs.rb b/config/initializers/delayed_jobs.rb
new file mode 100644
index 000000000..1ed52dd7c
--- /dev/null
+++ b/config/initializers/delayed_jobs.rb
@@ -0,0 +1,5 @@
+Delayed::Worker.queue_attributes = {
+  high_priority: { priority: -10 },
+  low_priority: { priority: 10 },
+  imports: { priority: 5 }
+}
diff --git a/config/locales/en.yml b/config/locales/en.yml
index f4364a70e..ae85766c0 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -1,6 +1,11 @@
 en:
   activerecord:
     attributes:
+      import:
+        date: Date
+        file: File
+        number_of_lines: Number of lines
+        status: Status
       language:
         iso_code: Iso code
         name: Name
@@ -26,11 +31,19 @@ en:
         websites_to_manage: Websites managed
     errors:
       models:
+        import:
+          attributes:
+            file:
+              incorrect_type: type is incorrect
+              no_file: must be selected
         user:
           attributes:
             password:
               password_strength: doesn't match the security policy
     models:
+      import:
+        one: Import
+        other: Imports
       language:
         one: Language
         other: Languages
@@ -99,7 +112,14 @@ en:
       send_email_code: 'Send me a code via email'
       success: ""
   download: Download
+  download_with_size: Download (%{size})
   edit: Edit
+  enums:
+    import:
+      status:
+        finished: Finished
+        finished_with_errors: Finished with errors
+        pending: Pending
   false: No
   featured_image:
     title: Image
@@ -127,7 +147,15 @@ en:
     privacy_policy: https://osuny.org/politique-de-confidentialite
   hello: "Hello %{name}!"
   home: Home
-  import: Import
+  import:
+    error_msg: "Line %{line}: %{error}"
+    errors: Errors
+    hint_html: "File must be an .xlsx excel file.<br>The first line must contain the headers.<br>The hearders are mandatory and must be strictly respected.<br>All characters must be encoded as UTF-8."
+    initiated_by: "Initiated by:"
+    number_of_lines: "Number of lines in the file:"
+    status: "Status:"
+    still_pending: "Your import has not been processed yet. You will receive an email as soon as it's ready."
+  import_btn: Import
   inactivity_alert: "It seems you have been away a little bit too long. Could you please retry?"
   languages:
     en: English
@@ -166,6 +194,8 @@ en:
     error_notification:
       default_message: "Please review the problems below:"
     hints:
+      import:
+        file: .xlsx file only
       user:
         mobile_phone: "International format (+XX). By filling this field, you accept to receive your two-factor authentication codes via SMS."
     include_blanks:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 623e0d9cd..e034e354a 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -1,6 +1,11 @@
 fr:
   activerecord:
     attributes:
+      import:
+        date: Date
+        file: Fichier
+        number_of_lines: Nombre de lignes
+        status: Status
       language:
         iso_code: Code Iso
         name: Nom
@@ -26,11 +31,19 @@ fr:
         websites_to_manage: Sites gérés
     errors:
       models:
+        import:
+          attributes:
+            file:
+              incorrect_type: n'est pas au bon format
+              no_file: doit être choisi
         user:
           attributes:
             password:
               password_strength: ne répond pas aux critères de sécurité
     models:
+      import:
+        one: Import
+        other: Imports
       language:
         one: Langue
         other: Langues
@@ -99,7 +112,14 @@ fr:
       send_email_code: 'Envoyer le code par email'
       success: ""
   download: Télécharger
+  download_with_size: Télécharger (%{size})
   edit: Modifier
+  enums:
+    import:
+      status:
+        finished: Traité
+        finished_with_errors: Traité avec des erreurs
+        pending: En cours de traitement
   false: Non
   featured_image:
     title: Image
@@ -127,7 +147,15 @@ fr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
   hello: "Bonjour %{name} !"
   home: Accueil
-  import: Importer
+  imports:
+    error_msg: "Ligne %{line} : %{error}"
+    errors: Erreurs
+    hint_html: "Les données doivent être au format xlsx.<br>La première ligne doit être dédiée aux entêtes.<br>Les noms des entêtes sont obligatoires et doivent être respectés strictement.<br>Les caractères doivent être encodés en UTF-8."
+    initiated_by: "Initié par :"
+    number_of_lines: "Nombre de lignes dans le fichier :"
+    status: "Status :"
+    still_pending: "Votre import est toujours en cours de traitement. Vous recevrez un email dès qu'il est prêt."
+  import_btn: Importer
   inactivity_alert: "Il semble que vous soyez resté sur la page un peu trop longtemps. Pouvez-vous ré-essayer ?"
   languages:
     en: Anglais
@@ -166,6 +194,8 @@ fr:
     error_notification:
       default_message: "Les erreurs ci-dessous empêchent la validation :"
     hints:
+      import:
+        file: Fichier .xlsx uniquement
       user:
         mobile_phone: "Format international (+XX). En renseignant ce champ, vous acceptez de recevoir vos codes de double authentification par SMS."
     include_blanks:
diff --git a/config/locales/university/en.yml b/config/locales/university/en.yml
index 0b9428754..be8382d7d 100644
--- a/config/locales/university/en.yml
+++ b/config/locales/university/en.yml
@@ -77,8 +77,6 @@ en:
         email: Email
         kind: Kind
         siren: Legal identification number
-      university/organization/import:
-        file: File (.csv)
       university/role:
         description: Description
         people: People
@@ -107,9 +105,6 @@ en:
       university/organization:
         one: Organization
         other: Organizations
-      university/organization/import:
-        one: Import
-        other: Imports
       university/role:
         one: Role
         other: Roles
@@ -142,6 +137,7 @@ en:
           non_profit: Association
           government: Government
   university:
+    import_hint_html: "Name field is mandatory.<br>Possible values for kind are: company, non_profit, government.<br>Siren, nic, zipcode and phone fields must have a text format, not numbers.<br>Country field must contain the ISO 3166 code of the country, so to upcase characters (<a href=\"https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes\" target=\_blank\">list</a>)"
     internal_key: Internal Key
     invoice_informations: Invoice informations
     manage_alumni: Manage alumni
diff --git a/config/locales/university/fr.yml b/config/locales/university/fr.yml
index 9cb44fa03..38e9bf9f6 100644
--- a/config/locales/university/fr.yml
+++ b/config/locales/university/fr.yml
@@ -77,8 +77,6 @@ fr:
         email: Email
         kind: Type
         siren: Numéro de SIREN
-      university/organization/import:
-        file: Fichier (.csv)
       university/role:
         description: Description
         people: Personnes
@@ -107,9 +105,6 @@ fr:
       university/organization:
         one: Organisation
         other: Organisations
-      university/organization/import:
-        one: Import
-        other: Imports
       university/role:
         one: Rôle
         other: Rôles
@@ -142,6 +137,7 @@ fr:
           non_profit: Association
           government: Structure gouvernementale
   university:
+    import_hint_html: "Le champ name est obligatoire.<br>Les valeurs possibles pour kind sont : company, non_profit, government.<br>Les champs siren, nic, zipcode et phone doivent être au format texte, pas nombre.<br>Le champ pays doit contenir le code ISO 3166 du pays, sur 2 caratères en majuscule (<a href=\"https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes\" target=\_blank\">liste</a>)"
     internal_key: Clé interne
     invoice_informations: Données de facturation
     manage_alumni: Gérer les alumni
diff --git a/db/migrate/20220523102030_create_imports.rb b/db/migrate/20220523102030_create_imports.rb
new file mode 100644
index 000000000..652737036
--- /dev/null
+++ b/db/migrate/20220523102030_create_imports.rb
@@ -0,0 +1,13 @@
+class CreateImports < ActiveRecord::Migration[6.1]
+  def change
+    create_table :imports, id: :uuid do |t|
+      t.integer :number_of_lines
+      t.jsonb :processing_errors
+      t.integer :kind
+      t.integer :status, default: 0
+      t.references :university, null: false, foreign_key: true, type: :uuid
+      t.references :user, null: false, foreign_key: true, type: :uuid
+      t.timestamps
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 51b0235db..f00d75039 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2022_05_19_100506) do
+ActiveRecord::Schema.define(version: 2022_05_23_102030) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pgcrypto"
@@ -502,21 +502,17 @@ ActiveRecord::Schema.define(version: 2022_05_19_100506) do
     t.index ["university_id"], name: "index_education_schools_on_university_id"
   end
 
-  create_table "external_organizations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
-    t.string "title"
-    t.text "description"
-    t.string "address"
-    t.string "zipcode"
-    t.string "city"
-    t.string "country"
-    t.string "website"
-    t.string "phone"
-    t.string "mail"
-    t.boolean "active"
-    t.string "sirene"
+  create_table "imports", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+    t.integer "number_of_lines"
+    t.jsonb "processing_errors"
     t.integer "kind"
+    t.integer "status", default: 0
+    t.uuid "university_id", null: false
+    t.uuid "user_id", null: false
     t.datetime "created_at", precision: 6, null: false
     t.datetime "updated_at", precision: 6, null: false
+    t.index ["university_id"], name: "index_imports_on_university_id"
+    t.index ["user_id"], name: "index_imports_on_user_id"
   end
 
   create_table "languages", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
@@ -709,8 +705,8 @@ ActiveRecord::Schema.define(version: 2022_05_19_100506) do
     t.string "linkedin"
     t.boolean "is_alumnus", default: false
     t.text "description_short"
-    t.string "name"
     t.boolean "is_author"
+    t.string "name"
     t.index ["university_id"], name: "index_university_people_on_university_id"
     t.index ["user_id"], name: "index_university_people_on_user_id"
   end
@@ -858,6 +854,8 @@ ActiveRecord::Schema.define(version: 2022_05_19_100506) do
   add_foreign_key "education_programs", "education_programs", column: "parent_id"
   add_foreign_key "education_programs", "universities"
   add_foreign_key "education_schools", "universities"
+  add_foreign_key "imports", "universities"
+  add_foreign_key "imports", "users"
   add_foreign_key "research_journal_articles", "research_journal_volumes"
   add_foreign_key "research_journal_articles", "research_journals"
   add_foreign_key "research_journal_articles", "universities"
-- 
GitLab