]> source.dussan.org Git - redmine.git/commitdiff
Introduces a UserQuery model for admin/users (#37674).
authorGo MAEDA <maeda@farend.jp>
Tue, 20 Sep 2022 03:16:05 +0000 (03:16 +0000)
committerGo MAEDA <maeda@farend.jp>
Tue, 20 Sep 2022 03:16:05 +0000 (03:16 +0000)
Patch by Jens Krämer.

git-svn-id: https://svn.redmine.org/redmine/trunk@21823 e93f8b46-1217-0410-a6f0-8f06a7374b81

13 files changed:
app/controllers/context_menus_controller.rb
app/controllers/queries_controller.rb
app/controllers/users_controller.rb
app/helpers/user_queries_helper.rb [new file with mode: 0644]
app/models/query.rb
app/models/user_query.rb [new file with mode: 0644]
app/views/context_menus/users.html.erb [new file with mode: 0644]
app/views/users/_list.html.erb [new file with mode: 0644]
app/views/users/index.html.erb
config/locales/de.yml
config/locales/en.yml
config/routes.rb
test/functional/users_controller_test.rb

index f4ecb9547f347ff4b84cabac0e02f349e70b33ee..d5b34ca4080b7584e17d84503a8383f30c9cf4fd 100644 (file)
@@ -108,4 +108,14 @@ class ContextMenusController < ApplicationController
     end
     render layout: false
   end
+
+  def users
+    @users = User.where(id: params[:ids]).to_a
+
+    (render_404; return) unless @users.present?
+    if @users.size == 1
+      @user = @users.first
+    end
+    render layout: false
+  end
 end
index d2fc5efb9511567996b583969856a9b15572b580..6b34913bc65370345c6fc44f4193da543cf47039 100644 (file)
@@ -177,6 +177,10 @@ class QueriesController < ApplicationController
     end
   end
 
+  def redirect_to_user_query(options)
+    redirect_to users_path(options)
+  end
+
   # Returns the Query subclass, IssueQuery by default
   # for compatibility with previous behaviour
   def query_class
index 5664237041c8e3a00036f69aa978522b4c0218b4..5cd9eb9314bfee3b415e5c6fffed8b049deba791 100644 (file)
@@ -34,49 +34,46 @@ class UsersController < ApplicationController
   helper :principal_memberships
   helper :activities
   include ActivitiesHelper
+  helper :queries
+  include QueriesHelper
+  helper :user_queries
+  include UserQueriesHelper
 
   require_sudo_mode :create, :update, :destroy
 
   def index
-    sort_init 'login', 'asc'
-    sort_update %w(login firstname lastname admin created_on last_login_on)
+    use_session = !request.format.csv?
+    retrieve_query(UserQuery, use_session)
 
-    case params[:format]
-    when 'xml', 'json'
-      @offset, @limit = api_offset_and_limit
-    else
-      @limit = per_page_option
-    end
-
-    @status = params[:status] || 1
+    if @query.valid?
+      scope = @query.results_scope
 
-    scope = User.logged.status(@status).preload(:email_address)
-    scope = scope.like(params[:name]) if params[:name].present?
-    scope = scope.in_group(params[:group_id]) if params[:group_id].present?
+      @user_count = scope.count
 
-    if params[:twofa].present?
-      case params[:twofa].to_i
-      when 1
-        scope = scope.where.not(twofa_scheme: nil)
-      when 0
-        scope = scope.where(twofa_scheme: nil)
-      end
-    end
-
-    @user_count = scope.count
-    @user_pages = Paginator.new @user_count, @limit, params['page']
-    @offset ||= @user_pages.offset
-    @users =  scope.order(sort_clause).limit(@limit).offset(@offset).to_a
-
-    respond_to do |format|
-      format.html do
-        @groups = Group.givable.sort
-        render :layout => !request.xhr?
+      respond_to do |format|
+        format.html do
+          @limit = per_page_option
+          @user_pages = Paginator.new @user_count, @limit, params['page']
+          @offset ||= @user_pages.offset
+          @users = scope.limit(@limit).offset(@offset).to_a
+          render :layout => !request.xhr?
+        end
+        format.csv do
+          # Export all entries
+          @entries = scope.to_a
+          send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'users.csv')
+        end
+        format.api do
+          @offset, @limit = api_offset_and_limit
+          @users = scope.limit(@limit).offset(@offset).to_a
+        end
       end
-      format.csv do
-        send_data(users_to_csv(scope.order(sort_clause)), :type => 'text/csv; header=present', :filename => 'users.csv')
+    else
+      respond_to do |format|
+        format.html {render :layout => !request.xhr?}
+        format.csv {head :unprocessable_entity}
+        format.api {render_validation_errors(@query)}
       end
-      format.api
     end
   end
 
diff --git a/app/helpers/user_queries_helper.rb b/app/helpers/user_queries_helper.rb
new file mode 100644 (file)
index 0000000..f66da2e
--- /dev/null
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2022  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+module UserQueriesHelper
+  def column_value(column, object, value)
+    if object.is_a?(User) && column.name == :status
+      user_status_label(column.value_object(object))
+    else
+      super
+    end
+  end
+
+  def csv_value(column, object, value)
+    if object.is_a?(User)
+      case column.name
+      when :status
+        user_status_label(column.value_object(object))
+      when :twofa_scheme
+        twofa_scheme_label value
+      else
+        super
+      end
+    else
+      super
+    end
+  end
+
+  def user_status_label(value)
+    case value.to_i
+    when User::STATUS_ACTIVE
+      l(:status_active)
+    when User::STATUS_REGISTERED
+      l(:status_registered)
+    when User::STATUS_LOCKED
+      l(:status_locked)
+    end
+  end
+
+  def twofa_scheme_label(value)
+    if value
+      ::I18n.t :"twofa__#{value}__name"
+    else
+      ::I18n.t :label_disabled
+    end
+  end
+end
index 1a614f1753cd4d549863b7482c558107d1be11ac..196568379ce755bf2abe981229a7cdb52332a54e 100644 (file)
@@ -150,7 +150,8 @@ class QueryCustomFieldColumn < QueryColumn
   end
 
   def value_object(object)
-    if custom_field.visible_by?(object.project, User.current)
+    project = object.project if object.respond_to?(:project)
+    if custom_field.visible_by?(project, User.current)
       cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
       cv.size > 1 ? cv.sort_by {|e| e.value.to_s} : cv.first
     else
diff --git a/app/models/user_query.rb b/app/models/user_query.rb
new file mode 100644 (file)
index 0000000..95572db
--- /dev/null
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2022  Jean-Philippe Lang
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+
+class UserQuery < Query
+  self.queried_class = Principal # must be Principal (not User) for custom field filters to work
+
+  self.available_columns = [
+    QueryColumn.new(:login, sortable: "#{User.table_name}.login"),
+    QueryColumn.new(:firstname, sortable: "#{User.table_name}.firstname"),
+    QueryColumn.new(:lastname, sortable: "#{User.table_name}.lastname"),
+    QueryColumn.new(:mail, sortable: "#{EmailAddress.table_name}.address"),
+    QueryColumn.new(:admin, sortable: "#{User.table_name}.admin"),
+    QueryColumn.new(:created_on, :sortable => "#{User.table_name}.created_on"),
+    QueryColumn.new(:updated_on, :sortable => "#{User.table_name}.updated_on"),
+    QueryColumn.new(:last_login_on, :sortable => "#{User.table_name}.last_login_on"),
+    QueryColumn.new(:passwd_changed_on, :sortable => "#{User.table_name}.passwd_changed_on"),
+    QueryColumn.new(:status, sortable: "#{User.table_name}.status"),
+    QueryAssociationColumn.new(:auth_source, :name, caption: :field_auth_source, sortable: "#{AuthSource.table_name}.name")
+  ]
+
+  def initialize(attributes=nil, *args)
+    super attributes
+    self.filters ||= { 'status' => {operator: "=", values: [User::STATUS_ACTIVE]} }
+  end
+
+  def initialize_available_filters
+    add_available_filter "status",
+      type: :list, values: ->{ user_statuses_values }
+    add_available_filter "auth_source_id",
+      type: :list_optional, values: ->{ auth_sources_values }
+    add_available_filter "is_member_of_group",
+      type: :list_optional,
+      values: ->{ Group.givable.visible.map {|g| [g.name, g.id.to_s] } }
+    if Setting.twofa?
+      add_available_filter "twofa_scheme",
+        type: :list_optional,
+        values: ->{ Redmine::Twofa.available_schemes.map {|s| [I18n.t("twofa__#{s}__name"), s] } }
+    end
+    add_available_filter "login", type: :string
+    add_available_filter "firstname", type: :string
+    add_available_filter "lastname", type: :string
+    add_available_filter "mail", type: :string
+    add_available_filter "created_on", type: :date_past
+    add_available_filter "last_login_on", type: :date_past
+    add_available_filter "admin",
+      type: :list,
+      values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']]
+    add_custom_fields_filters(user_custom_fields)
+  end
+
+  def auth_sources_values
+    AuthSource.order(name: :asc).to_a.map do |auth_source|
+      [auth_source.name, auth_source.id]
+    end
+  end
+
+  def user_statuses_values
+    [
+      [l(:status_active), User::STATUS_ACTIVE.to_s],
+      [l(:status_registered), User::STATUS_REGISTERED.to_s],
+      [l(:status_locked), User::STATUS_LOCKED.to_s]
+    ]
+  end
+
+  def available_columns
+    return @available_columns if @available_columns
+
+    @available_columns = self.class.available_columns.dup
+    if Setting.twofa?
+      @available_columns << QueryColumn.new(:twofa_scheme, sortable: "#{User.table_name}.twofa_scheme")
+    end
+    @available_columns += user_custom_fields.visible.
+                            map {|cf| QueryCustomFieldColumn.new(cf)}
+
+    @available_columns
+  end
+
+  # Returns a scope of user custom fields that are available as columns or filters
+  def user_custom_fields
+    UserCustomField.sorted
+  end
+
+  def default_columns_names
+    @default_columns_names ||= [:login, :firstname, :lastname, :mail, :admin, :created_on, :last_login_on]
+  end
+
+  def default_sort_criteria
+    [['login', 'asc']]
+  end
+
+  def base_scope
+    User.logged.where(statement).includes(:email_address)
+  end
+
+  def results_scope(options={})
+    order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
+
+    base_scope.
+      order(order_option).
+      joins(joins_for_order_statement(order_option.join(',')))
+  end
+
+  def sql_for_admin_field(field, operator, value)
+    return unless value = value.first
+
+    true_value = operator == '=' ? '1' : '0'
+    val =
+      if value.to_s == true_value
+        self.class.connection.quoted_true
+      else
+        self.class.connection.quoted_false
+      end
+    "(#{User.table_name}.admin = #{val})"
+  end
+
+  def sql_for_is_member_of_group_field(field, operator, value)
+    if ["*", "!*"].include? operator
+      value = Group.givable.map(&:id)
+    end
+
+    e = operator.start_with?("!") ? "NOT EXISTS" : "EXISTS"
+
+    "(#{e} (SELECT 1 FROM groups_users WHERE #{User.table_name}.id = groups_users.user_id AND #{sql_for_field(field, '=', value, 'groups_users', 'group_id')}))"
+  end
+
+  def sql_for_mail_field(field, operator, value)
+    if operator == '!*'
+      match = false
+      operator = '*'
+    else
+      match = true
+    end
+    emails = EmailAddress.table_name
+    <<-SQL
+      #{match ? 'EXISTS' : 'NOT EXISTS'}
+      (SELECT 1 FROM #{emails} WHERE
+        #{emails}.user_id = #{User.table_name}.id AND
+        #{sql_for_field(:mail, operator, value, emails, 'address')})
+    SQL
+  end
+end
diff --git a/app/views/context_menus/users.html.erb b/app/views/context_menus/users.html.erb
new file mode 100644 (file)
index 0000000..2a0c12b
--- /dev/null
@@ -0,0 +1,24 @@
+<ul>
+  <% if @user %>
+    <% if @user.locked? %>
+      <li>
+        <%= context_menu_link l(:button_unlock), user_path(@user, user: { status: User::STATUS_ACTIVE }, back_url: @back), method: :put, class: 'icon icon-unlock' %>
+      </li>
+    <% elsif User.current != @user %>
+      <li>
+        <%= context_menu_link l(:button_lock), user_path(@user, user: { status: User::STATUS_LOCKED }, back_url: @back), method: :put, class: 'icon icon-lock' %>
+      </li>
+    <% end %>
+
+    <li>
+      <%= context_menu_link l(:button_edit), edit_user_path(@user, back_url: @back), class: 'icon icon-edit' %>
+    </li>
+
+    <% unless User.current == @user %>
+      <li>
+        <%= context_menu_link l(:button_delete), user_path(@user, back_url: @back),
+          method: :delete, class: 'icon icon-del' %>
+      </li>
+    <% end %>
+  <% end %>
+</ul>
diff --git a/app/views/users/_list.html.erb b/app/views/users/_list.html.erb
new file mode 100644 (file)
index 0000000..59c26da
--- /dev/null
@@ -0,0 +1,65 @@
+<%= form_tag({}, data: {cm_url: users_context_menu_path}) do -%>
+<%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %>
+<div class="autoscroll">
+<table class="list odd-even users">
+<thead>
+  <tr>
+    <th class="checkbox hide-when-print">
+      <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection',
+        :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
+    </th>
+    <% @query.inline_columns.each do |column| %>
+      <%= column_header(@query, column) %>
+    <% end %>
+    <th></th>
+  </tr>
+</thead>
+<tbody>
+<% grouped_query_results(users, @query) do |user, group_name, group_count, group_totals| -%>
+  <% if group_name %>
+    <% reset_cycle %>
+    <tr class="group open">
+      <td colspan="<%= @query.inline_columns.size + 2 %>">
+        <span class="expander" onclick="toggleRowGroup(this);">&nbsp;</span>
+        <span class="name"><%= group_name %></span>
+        <% if group_count %>
+        <span class="count"><%= group_count %></span>
+        <% end %>
+        <span class="totals"><%= group_totals %></span>
+        <%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
+                             "toggleAllRowGroups(this)", :class => 'toggle-all') %>
+      </td>
+    </tr>
+  <% end %>
+  <tr id="user-<%= user.id %>" class="user <%= cycle("odd", "even") %> hascontextmenu">
+    <td class="checkbox hide-when-print"><%= check_box_tag("ids[]", user.id, false, id: nil) %></td>
+    <% @query.inline_columns.each do |column| %>
+      <% if column.name == :login %>
+        <%= content_tag('td', link_to(user.login, edit_user_path(user)), class: column.css_classes) %>
+      <% else %>
+        <%= content_tag('td', column_content(column, user), class: column.css_classes) %>
+      <% end %>
+    <% end %>
+    <td class="buttons">
+      <%= link_to_context_menu %>
+    </td>
+  </tr>
+  <% @query.block_columns.each do |column|
+       if (text = column_content(column, issue)) && text.present? -%>
+  <tr class="<%= current_cycle %>">
+    <td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>">
+    <% if query.block_columns.count > 1 %>
+      <span><%= column.caption %></span>
+    <% end %>
+    <%= text %>
+    </td>
+  </tr>
+  <% end -%>
+  <% end -%>
+<% end -%>
+</tbody>
+</table>
+</div>
+<% end -%>
+
+<%= context_menu %>
index 101b00efd7ef345f9b81b8e6e1069b16821b2363..f5c8a1cd1c009759a0431d1777f88091ed8631c6 100644 (file)
@@ -7,94 +7,54 @@
   <% end %>
 </div>
 
-<h2><%=l(:label_user_plural)%></h2>
+<h2><%= @query.new_record? ? l(:label_user_plural) : @query.name %></h2>
 
-<%= form_tag(users_path, { :method => :get, :id => 'users_form' }) do %>
-<fieldset><legend><%= l(:label_filter_plural) %></legend>
-<label for='status'><%= l(:field_status) %>:</label>
-<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;"  %>
-
-<% if @groups.present? %>
-<label for='group_id'><%= l(:label_group) %>:</label>
-<%= select_tag 'group_id', content_tag('option') + options_from_collection_for_select(@groups, :id, :name, params[:group_id].to_i), :onchange => "this.form.submit(); return false;"  %>
-<% end %>
-
-<% if Setting.twofa_required? || Setting.twofa_optional? %>
-  <label for='twofa'><%= l(:setting_twofa) %>:</label>
-  <%= select_tag 'twofa', options_for_select([[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], params[:twofa]), :onchange => "this.form.submit(); return false;", :include_blank => true %>
+<%= form_tag(users_path, method: :get, id: 'query_form') do %>
+  <%= render partial: 'queries/query_form' %>
 <% end %>
 
-<label for='name'><%= l(:label_user) %>:</label>
-<%= text_field_tag 'name', params[:name], :size => 30 %>
-<%= submit_tag l(:button_apply), :class => "small", :name => nil %>
-<%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %>
-</fieldset>
-<%= hidden_field_tag 'encoding', l(:general_csv_encoding) unless l(:general_csv_encoding).casecmp('UTF-8') == 0 %>
-<% end %>
-&nbsp;
-
-<% if @users.any? %>
-<div class="autoscroll">
-<table class="list users">
-  <thead><tr>
-  <%= sort_header_tag('login', :caption => l(:field_login)) %>
-  <%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
-  <%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
-  <th><%= l(:field_mail) %></th>
-  <%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
-  <% if Setting.twofa_required? || Setting.twofa_optional? %>
-    <th class="whitespace-normal"><%= l(:setting_twofa) %></th>
+<% if @query.valid? %>
+  <% if @users.empty? %>
+    <p class="nodata"><%= l(:label_no_data) %></p>
+  <% else %>
+    <%= render_query_totals(@query) %>
+    <%= render partial: 'list', :locals => { :users => @users }%>
+    <span class="pagination"><%= pagination_links_full @user_pages, @user_count %></span>
   <% end %>
-  <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
-  <%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
-    <th></th>
-  </tr></thead>
-  <tbody>
-<% for user in @users -%>
-  <tr class="<%= user.css_classes %>">
-  <td class="username"><%= avatar(user, :size => "14") %><%= link_to user.login, edit_user_path(user) %></td>
-  <td class="firstname"><%= user.firstname %></td>
-  <td class="lastname"><%= user.lastname %></td>
-  <td class="email"><%= mail_to(user.mail) %></td>
-  <td class="tick"><%= checked_image user.admin? %></td>
-  <% if Setting.twofa_required? || Setting.twofa_optional? %>
-    <td class="twofa tick"><%= checked_image user.twofa_active? %></td>
+  <% other_formats_links do |f| %>
+    <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
   <% end %>
-  <td class="created_on"><%= format_time(user.created_on) %></td>
-  <td class="last_login_on"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td>
-    <td class="buttons">
-      <%= change_status_link(user) %>
-      <%= delete_link user_path(user, :back_url => request.original_fullpath), :data => {} unless User.current == user %>
-    </td>
-  </tr>
-<% end -%>
-  </tbody>
-</table>
-</div>
-<span class="pagination"><%= pagination_links_full @user_pages, @user_count %></span>
-<% other_formats_links do |f| %>
-  <%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
-<% end %>
-<div id="csv-export-options" style="display: none;">
-  <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
-  <%= export_csv_encoding_select_tag %>
-  <p class="buttons">
-    <%= submit_tag l(:button_export), :name => nil, :id => 'csv-export-button' %>
-    <%= submit_tag l(:button_cancel), :name => nil, :onclick => 'hideModal(this);', :type => 'button' %>
-  </p>
-</div>
-<%= javascript_tag do %>
-$(document).ready(function(){
-  $('input#csv-export-button').click(function(){
-    $('form input#encoding').val($('select#encoding option:selected').val());
-    $('form#users_form').attr('action', "<%= users_path(:format => 'csv') %>").submit();
-    $('form#users_form').attr('action', '<%= users_path %>');
-    hideModal(this);
-  });
-});
-<% end %>
-<% else %>
-<p class="nodata"><%= l(:label_no_data) %></p>
+
+  <div id="csv-export-options" style="display:none;">
+    <h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3>
+    <%= form_tag(users_path(format: 'csv'), method: :get, id: 'csv-export-form') do %>
+    <%= query_as_hidden_field_tags(@query) %>
+    <%= hidden_field_tag('query_name', @query.name) %>
+    <p>
+      <label><%= radio_button_tag 'c[]', '', true %> <%= l(:description_selected_columns) %></label><br />
+      <label><%= radio_button_tag 'c[]', 'all_inline' %> <%= l(:description_all_columns) %></label>
+    </p>
+    <% if @query.available_block_columns.any? %>
+      <fieldset id="csv-export-block-columns">
+        <legend>
+          <%= toggle_checkboxes_link('#csv-export-block-columns input[type=checkbox]') %>
+        </legend>
+        <% @query.available_block_columns.each do |column| %>
+          <label><%= check_box_tag 'c[]', column.name, @query.has_column?(column), :id => nil %> <%= column.caption %></label>
+        <% end %>
+      </fieldset>
+    <% end %>
+    <%= export_csv_encoding_select_tag %>
+    <p class="buttons">
+      <%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);", :data => { :disable_with => false } %>
+      <%= link_to_function l(:button_cancel), "hideModal(this);" %>
+    </p>
+    <% end %>
+  </div>
 <% end %>
 
+<% content_for :sidebar do %>
+  <%= render_sidebar_queries(UserQuery, nil) %>
+  <%= call_hook(:view_users_sidebar_queries_bottom) %>
+<% end %>
 <% html_title(l(:label_user_plural)) -%>
index bda3f8dbe29b97bf86fb7351106c50dffb7bb4c7..00e2c11b3470ce011316483ef1ad558e27684221 100644 (file)
@@ -331,6 +331,7 @@ de:
   field_is_filter: Als Filter benutzen
   field_is_for_all: Für alle Projekte
   field_is_in_roadmap: In der Roadmap anzeigen
+  field_is_member_of_group: Mitglied in Gruppe
   field_is_private: Privat
   field_is_public: Öffentlich
   field_is_required: Erforderlich
index a96c48b825da677f2a3de1161412e2b02bc49bc0..8f80d309e0cf38b0fdeae79bd1668feaa53d9525 100644 (file)
@@ -302,6 +302,7 @@ en:
   field_title: Title
   field_project: Project
   field_issue: Issue
+  field_is_member_of_group: Member of group
   field_status: Status
   field_notes: Notes
   field_is_closed: Issue closed
index 1e4b20d94f07ebf88cd84de5bbcdbb917396706d..f2106d90f7c5258deb19ea195ade79fdc913233e 100644 (file)
@@ -108,6 +108,7 @@ Rails.application.routes.draw do
   match 'my/twofa/backup_codes', :controller => 'twofa_backup_codes', :action => 'show', :via => [:get]
   match 'users/:user_id/twofa/deactivate', :controller => 'twofa', :action => 'admin_deactivate', :via => :post
 
+  match '/users/context_menu', to: 'context_menus#users', as: :users_context_menu, via: [:get, :post]
   resources :users do
     resources :memberships, :controller => 'principal_memberships'
     resources :email_addresses, :only => [:index, :create, :update, :destroy]
index e753c387300ec3e76e4330633b2a8cb17017423c..af2f1871f98b471fe3e8a8b1ed50f9d6ecef57fd 100644 (file)
@@ -37,33 +37,33 @@ class UsersControllerTest < Redmine::ControllerTest
   def test_index
     get :index
     assert_response :success
+    active = User.active.first
+    locked = User.where(status: User::STATUS_LOCKED).first
     assert_select 'table.users'
-    assert_select 'tr.user.active'
-    assert_select 'tr.user.locked', 0
+    assert_select "tr#user-#{active.id}"
+    assert_select "tr#user-#{locked.id}", 0
   end
 
   def test_index_with_status_filter
-    get :index, :params => {:status => 3}
+    get :index, params: { set_filter: 1, f: ['status'], op: {status: '='}, v: {status: [3]} }
     assert_response :success
-    assert_select 'tr.user.active', 0
-    assert_select 'tr.user.locked'
+    assert_select "tr.user", User.where(status: 3).count
   end
 
-  def test_index_with_name_filter
-    get :index, :params => {:name => 'john'}
+  def test_index_with_firstname_filter
+    get :index, params: { set_filter: 1, f: ['firstname'], op: {firstname: '~'}, v: {firstname: ['john']} }
     assert_response :success
-    assert_select 'tr.user td.username', :text => 'jsmith'
+    assert_select 'tr.user td.login', text: 'jsmith'
     assert_select 'tr.user', 1
   end
 
   def test_index_with_group_filter
-    get :index, :params => {:group_id => '10'}
+    get :index, params: {
+      set_filter: 1,
+      f: ['is_member_of_group'], op: {is_member_of_group: '='}, v: {is_member_of_group: ['10']}
+    }
     assert_response :success
-
     assert_select 'tr.user', Group.find(10).users.count
-    assert_select 'select[name=group_id]' do
-      assert_select 'option[value="10"][selected=selected]'
-    end
   end
 
   def test_index_should_not_show_2fa_filter_and_column_if_disabled
@@ -71,8 +71,12 @@ class UsersControllerTest < Redmine::ControllerTest
       get :index
       assert_response :success
 
-      assert_select "select#twofa", 0
-      assert_select 'td.twofa', 0
+      assert_select "select#add_filter_select" do
+        assert_select "option[value=twofa_scheme]", 0
+      end
+      assert_select "select#available_c" do
+        assert_select "option[value=twofa_scheme]", 0
+      end
     end
   end
 
@@ -83,30 +87,91 @@ class UsersControllerTest < Redmine::ControllerTest
       user.twofa_scheme = "totp"
       user.save
 
-      get :index, :params => {:twofa => '1'}
+      get :index, params: { set_filter: 1, f: ['twofa_scheme'], op: {twofa_scheme: '*'} }
       assert_response :success
 
-      assert_select "select#twofa", 1
-
+      assert_select 'tr#user-1', 1
       assert_select 'tr.user', 1
-      assert_select 'td.twofa.tick .icon-checked'
+
+      assert_select "select#add_filter_select" do
+        assert_select "option[value=twofa_scheme]"
+      end
+      assert_select "select#available_c" do
+        assert_select "option[value=twofa_scheme]"
+      end
     end
   end
 
-  def test_index_filter_by_twofa_no
+  def test_index_filter_by_twofa_scheme
     with_settings twofa: "1" do
       user = User.find(1)
       user.twofa_totp_key = "AVYA3RARZ3GY3VWT7MIEJ72I5TTJRO3X"
       user.twofa_scheme = "totp"
       user.save
 
-      get :index, :params => {:twofa => '0'}
+      get :index, params: {
+        set_filter: 1,
+        f: ['twofa_scheme'], op: {twofa_scheme: '='}, v: {twofa_scheme: ['totp']}
+      }
       assert_response :success
 
-      assert_select "select#twofa", 1
-      assert_select "td.twofa.tick" do
-        assert_select "span.icon-checked", 0
+      assert_select 'tr#user-1', 1
+
+      assert_select "select#add_filter_select" do
+        assert_select "option[value=twofa_scheme]"
       end
+      assert_select "select#available_c" do
+        assert_select "option[value=twofa_scheme]"
+      end
+    end
+  end
+
+  def test_index_filter_by_twofa_no
+    with_settings twofa: "1" do
+      user = User.find(1)
+      user.twofa_totp_key = "AVYA3RARZ3GY3VWT7MIEJ72I5TTJRO3X"
+      user.twofa_scheme = "totp"
+      user.save
+
+      get :index, params: { set_filter: 1, f: ['twofa_scheme'], op: {twofa_scheme: '!*'} }
+      assert_response :success
+
+      assert_select 'tr#user-1', 0
+      assert_select 'tr.user'
+    end
+  end
+
+  def test_index_filter_by_auth_source_none
+    user = User.find(1)
+    user.update_column :auth_source_id, 1
+
+    get :index, params: {
+      set_filter: 1,
+      f: ['auth_source_id'], op: {auth_source_id: '!*'}
+    }
+    assert_response :success
+
+    assert_select 'tr.user'
+    assert_select 'tr#user-1', 0
+  end
+
+  def test_index_filter_by_auth_source
+    user = User.find(1)
+    user.update_column :auth_source_id, 1
+
+    get :index, params: {
+      set_filter: 1,
+      f: ['auth_source_id'], op: {auth_source_id: '='}, v: {auth_source_id: ['1']}
+    }
+    assert_response :success
+
+    assert_select 'tr#user-1', 1
+
+    assert_select "select#add_filter_select" do
+      assert_select "option[value=auth_source_id]"
+    end
+    assert_select "select#available_c" do
+      assert_select "option[value='auth_source.name']"
     end
   end
 
@@ -114,7 +179,7 @@ class UsersControllerTest < Redmine::ControllerTest
     with_settings :default_language => 'en' do
       user = User.logged.status(1).first
       user.update(passwd_changed_on: Time.current.last_month, twofa_scheme: 'totp')
-      get :index, params: {format: 'csv'}
+      get :index, params: {format: 'csv', c: ['updated_on', 'status', 'passwd_changed_on', 'twofa_scheme']}
       assert_response :success
 
       assert_equal User.logged.status(1).count, response.body.chomp.split("\n").size - 1
@@ -142,7 +207,13 @@ class UsersControllerTest < Redmine::ControllerTest
 
     User.find(@request.session[:user_id]).update(:language => nil)
     with_settings :default_language => 'fr' do
-      get :index, :params => {:name => user.lastname, :format => 'csv'}
+      get :index, params: {
+        c: ["cf_#{float_custom_field.id}", "cf_#{date_custom_field.id}"],
+        f: ["name"],
+        op: { name: "~" },
+        v: { name: [user.lastname] },
+        format: 'csv'
+      }
       assert_response :success
 
       assert_include 'float field;date field', response.body
@@ -153,7 +224,12 @@ class UsersControllerTest < Redmine::ControllerTest
 
   def test_index_csv_with_status_filter
     with_settings :default_language => 'en' do
-      get :index, :params => {:status => 3, :format => 'csv'}
+      get :index, :params => {
+        :set_filter => '1',
+        :f => [:status], :op => { :status => '=' }, :v => { :status => [3] },
+        :c => [:login, :status],
+        :format => 'csv'
+      }
       assert_response :success
 
       assert_equal User.logged.status(3).count, response.body.chomp.split("\n").size - 1
@@ -164,7 +240,12 @@ class UsersControllerTest < Redmine::ControllerTest
   end
 
   def test_index_csv_with_name_filter
-    get :index, :params => {:name => 'John', :format => 'csv'}
+    get :index, :params => {
+      :set_filter => '1',
+      :f => [:firstname], :op => { :firstname => '~' }, :v => { :firstname => ['John'] },
+      :c => [:login, :firstname, :status],
+      :format => 'csv'
+    }
     assert_response :success
 
     assert_equal User.logged.like('John').count, response.body.chomp.split("\n").size - 1
@@ -173,7 +254,12 @@ class UsersControllerTest < Redmine::ControllerTest
   end
 
   def test_index_csv_with_group_filter
-    get :index, :params => {:group_id => '10', :format => 'csv'}
+    get :index, :params => {
+      :set_filter => '1',
+      :f => [:is_member_of_group], :op => { :is_member_of_group => '=' }, :v => { :is_member_of_group => [10] },
+      :c => [:login, :status],
+      :format => 'csv'
+    }
     assert_response :success
 
     assert_equal Group.find(10).users.count, response.body.chomp.split("\n").size - 1