From e5e40a12b2301fae701b3407ff2e9afe98d9ed63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Gaya?= <sebastien.gaya@gmail.com>
Date: Mon, 7 Feb 2022 10:42:59 +0100
Subject: [PATCH] filter users

---
 Gemfile                                       |  1 +
 Gemfile.lock                                  |  4 ++
 app/controllers/admin/users_controller.rb     |  6 ++-
 app/models/user.rb                            | 10 +++++
 app/services/filters/base.rb                  | 39 +++++++++++++++++
 app/services/filters/user.rb                  |  9 ++++
 app/views/admin/application/_filters.html.erb | 43 +++++++++++++++++++
 app/views/admin/users/index.html.erb          |  3 ++
 config/locales/en.yml                         |  9 ++++
 config/locales/fr.yml                         |  9 ++++
 .../20220207093702_add_unaccent_extension.rb  |  5 +++
 db/schema.rb                                  |  3 +-
 12 files changed, 139 insertions(+), 2 deletions(-)
 create mode 100644 app/services/filters/base.rb
 create mode 100644 app/services/filters/user.rb
 create mode 100644 app/views/admin/application/_filters.html.erb
 create mode 100644 db/migrate/20220207093702_add_unaccent_extension.rb

diff --git a/Gemfile b/Gemfile
index 7342df7bc..ff36280ae 100644
--- a/Gemfile
+++ b/Gemfile
@@ -37,6 +37,7 @@ gem 'two_factor_authentication', git: 'https://github.com/noesya/two_factor_auth
 # gem 'two_factor_authentication', path: '../two_factor_authentication'
 gem 'curation'#, path: '../../arnaudlevy/curation'
 gem "cocoon", "~> 1.2"
+gem "has_scope", "~> 0.8.0"
 
 # Front
 gem 'jquery-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 164634b66..cb51e670c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -204,6 +204,9 @@ GEM
       sassc-rails
     globalid (1.0.0)
       activesupport (>= 5.0)
+    has_scope (0.8.0)
+      actionpack (>= 5.2)
+      activesupport (>= 5.2)
     http-cookie (1.0.4)
       domain_name (~> 0.5)
     i18n (1.9.1)
@@ -437,6 +440,7 @@ DEPENDENCIES
   figaro
   front_matter_parser
   gdpr
+  has_scope (~> 0.8.0)
   image_processing
   jbuilder
   jquery-rails
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1d00d143e..36cb5ed0b 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -1,8 +1,12 @@
 class Admin::UsersController < Admin::ApplicationController
   load_and_authorize_resource through: :current_university
 
+  has_scope :for_role
+  has_scope :for_search_term
+
   def index
-    @users = @users.ordered.page(params[:page])
+    @filters = ::Filters::User.new(current_user).list
+    @users = apply_scopes(@users).ordered.page(params[:page])
     breadcrumb
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 602fa6c6f..e5d355cea 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -63,6 +63,16 @@ class User < ApplicationRecord
   belongs_to :language
 
   scope :ordered, -> { order(:last_name, :first_name) }
+  scope :for_search_term, -> (term) {
+    where("
+      unaccent(concat(users.first_name, ' ', users.last_name)) ILIKE unaccent(:term) OR
+      unaccent(concat(users.last_name, ' ', users.first_name)) ILIKE unaccent(:term) OR
+      unaccent(users.first_name) ILIKE unaccent(:term) OR
+      unaccent(users.last_name) ILIKE unaccent(:term) OR
+      unaccent(users.email) ILIKE unaccent(:term) OR
+      unaccent(users.mobile_phone) ILIKE unaccent(:term)
+    ", term: "%#{sanitize_sql_like(term)}%")
+  }
 
   def to_s
     "#{first_name} #{last_name}"
diff --git a/app/services/filters/base.rb b/app/services/filters/base.rb
new file mode 100644
index 000000000..b3a1a5674
--- /dev/null
+++ b/app/services/filters/base.rb
@@ -0,0 +1,39 @@
+module Filters
+  class Base
+    attr_accessor :list
+
+    def initialize(user)
+      @user = user
+      @list = []
+    end
+
+    protected
+
+    def add(scope_name, choices, label, multiple = false)
+      @list << {
+        scope_name: scope_name,
+        choices: choices,
+        label: label,
+        multiple: multiple
+      }
+    end
+
+    def add_if(condition, args)
+      add *args if condition
+    end
+
+    def add_search
+      add :for_search_term, nil, I18n.t('search')
+    end
+
+    def add_date_filter(objects, attribute)
+      dates = objects.map { |obj|
+        {
+          to_s: I18n.l(obj[attribute], format: "%B %Y"),
+          id: I18n.l(obj[attribute], format: "%Y-%m")
+        }
+      }.uniq
+      add :for_date, dates, t('filters.attributes.date')
+    end
+  end
+end
diff --git a/app/services/filters/user.rb b/app/services/filters/user.rb
new file mode 100644
index 000000000..a71e11eff
--- /dev/null
+++ b/app/services/filters/user.rb
@@ -0,0 +1,9 @@
+module Filters
+  class User < Base
+    def initialize(user)
+      super
+      add_search
+      add :for_role, ::User.roles.keys.map { |r| { to_s: r.humanize, id: r } }, I18n.t('filters.attributes.role')
+    end
+  end
+end
diff --git a/app/views/admin/application/_filters.html.erb b/app/views/admin/application/_filters.html.erb
new file mode 100644
index 000000000..4c2238c74
--- /dev/null
+++ b/app/views/admin/application/_filters.html.erb
@@ -0,0 +1,43 @@
+<%
+collapsable = true if collapsable.nil? # not ||= because collapsable can be "false"
+should_be_open = false
+filters.each { |filter| should_be_open = true if params.has_key?(filter[:scope_name]) }
+%>
+<div class="row">
+  <% if collapsable %>
+    <div class="col-md-2">
+      <a  class="btn btn-primary"
+          data-bs-toggle="collapse"
+          href="#collapseFilters"
+          role="button"
+          aria-expanded="false"
+          aria-controls="collapseFilters">
+        <%= t('filters.buttons.expand') %>
+      </a>
+    </div>
+  <% end %>
+  <div class="col-md-<%= collapsable ? '10' : '12' %>">
+    <div class="collapse <%= (!collapsable || should_be_open) ? 'show' : '' %>" id="collapseFilters">
+        <%= form_tag current_path, method: :get, class: 'do-not-unlock' do |f| %>
+        <div class="row">
+          <div class="col-md-4">
+            <% filters.each do |filter| %>
+              <% if filter[:scope_name] == :for_search_term %>
+                <%= text_field_tag filter[:scope_name], params[filter[:scope_name]], placeholder: filter[:label], class: 'form-control mb-1 filter' %>
+              <% else %>
+                <% choices = filter[:choices].map { |elmt| elmt.is_a?(String) ?  [elmt, elmt] : [elmt.is_a?(Hash) ? elmt[:to_s] : elmt.to_s, elmt[:id]] } %>
+                <% field_name = filter[:multiple] ? "#{filter[:scope_name]}[]" : filter[:scope_name] %>
+                <%= select_tag field_name, options_for_select(choices, params[filter[:scope_name]]), include_blank: filter[:label], class: 'form-select mb-1 filter' %>
+              <% end %>
+            <% end %>
+          </div>
+          <div class="col-md-3">
+            <%= submit_tag t('filters.buttons.submit'), class: 'btn btn-primary btn-submit' %>
+            <%= link_to t('reset'), current_path, class: 'btn btn-warning' %>
+          </div>
+        </div>
+        <% end %>
+    </div>
+  </div>
+</div>
+<br>
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb
index b161dcbcd..af80ff5b3 100644
--- a/app/views/admin/users/index.html.erb
+++ b/app/views/admin/users/index.html.erb
@@ -1,5 +1,8 @@
 <% content_for :title, "#{User.model_name.human(count: 2)} (#{@users.total_count})" %>
 
+<%= render 'admin/application/filters',
+    current_path: admin_users_path,
+    filters: @filters if @filters.any? %>
 
 <table class="table">
   <thead>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9153a8cc3..e535a3d13 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -88,6 +88,13 @@ en:
   edit: Edit
   empty_folder: Empty folder
   false: No
+  filters:
+    attributes:
+      date: Filter by Date
+      role: Filter by Role
+    buttons:
+      expand: Filter table
+      submit: Filter
   gdpr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
   hello: "Hello %{name}!"
@@ -113,7 +120,9 @@ en:
   privacy_policy_url: https://osuny.org/politique-de-confidentialite
   quit: Quit
   remove: Remove
+  reset: Reset
   save: Save
+  search: Search
   select_language: Select language
   simple_form:
     error_notification:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ca2c06384..f16346f8d 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -88,6 +88,13 @@ fr:
   edit: Modifier
   empty_folder: Dossier vide
   false: Non
+  filters:
+    attributes:
+      date: Filtrer par Date
+      role: Filtrer par Rôle
+    buttons:
+      expand: Filtrer le tableau
+      submit: Filtrer
   gdpr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
   hello: "Bonjour %{name} !"
@@ -113,7 +120,9 @@ fr:
   privacy_policy_url: https://osuny.org/politique-de-confidentialite
   quit: Quitter
   remove: Retirer
+  reset: Réinitialiser
   save: Enregistrer
+  search: Rechercher
   select_language: Sélectionnez une langue
   simple_form:
     error_notification:
diff --git a/db/migrate/20220207093702_add_unaccent_extension.rb b/db/migrate/20220207093702_add_unaccent_extension.rb
new file mode 100644
index 000000000..f1d34afad
--- /dev/null
+++ b/db/migrate/20220207093702_add_unaccent_extension.rb
@@ -0,0 +1,5 @@
+class AddUnaccentExtension < ActiveRecord::Migration[6.1]
+  def change
+    enable_extension "unaccent"
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 69faf1dd0..6fa4a6e61 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,11 +10,12 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2022_02_03_160802) do
+ActiveRecord::Schema.define(version: 2022_02_07_093702) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pgcrypto"
   enable_extension "plpgsql"
+  enable_extension "unaccent"
 
   create_table "action_text_rich_texts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
     t.string "name", null: false
-- 
GitLab