Feature #37674 » 0001-introduces-a-UserQuery-model-for-admin-users.patch
| app/controllers/context_menus_controller.rb | ||
|---|---|---|
| 108 | 108 |
end |
| 109 | 109 |
render layout: false |
| 110 | 110 |
end |
| 111 | ||
| 112 |
def users |
|
| 113 |
@users = User.where(id: params[:ids]).to_a |
|
| 114 | ||
| 115 |
(render_404; return) unless @users.present? |
|
| 116 |
if (@users.size == 1) |
|
| 117 |
@user = @users.first |
|
| 118 |
end |
|
| 119 |
render layout: false |
|
| 120 |
end |
|
| 111 | 121 |
end |
| app/controllers/queries_controller.rb | ||
|---|---|---|
| 177 | 177 |
end |
| 178 | 178 |
end |
| 179 | 179 | |
| 180 |
def redirect_to_user_query(options) |
|
| 181 |
redirect_to users_path(options) |
|
| 182 |
end |
|
| 183 | ||
| 180 | 184 |
# Returns the Query subclass, IssueQuery by default |
| 181 | 185 |
# for compatibility with previous behaviour |
| 182 | 186 |
def query_class |
| app/controllers/users_controller.rb | ||
|---|---|---|
| 34 | 34 |
helper :principal_memberships |
| 35 | 35 |
helper :activities |
| 36 | 36 |
include ActivitiesHelper |
| 37 |
helper :queries |
|
| 38 |
include QueriesHelper |
|
| 39 |
helper :user_queries |
|
| 40 |
include UserQueriesHelper |
|
| 37 | 41 | |
| 38 | 42 |
require_sudo_mode :create, :update, :destroy |
| 39 | 43 | |
| 40 | 44 |
def index |
| 41 |
sort_init 'login', 'asc'
|
|
| 42 |
sort_update %w(login firstname lastname admin created_on last_login_on)
|
|
| 45 |
use_session = !request.format.csv?
|
|
| 46 |
retrieve_query(UserQuery, use_session)
|
|
| 43 | 47 | |
| 44 |
case params[:format] |
|
| 45 |
when 'xml', 'json' |
|
| 46 |
@offset, @limit = api_offset_and_limit |
|
| 47 |
else |
|
| 48 |
@limit = per_page_option |
|
| 49 |
end |
|
| 50 | ||
| 51 |
@status = params[:status] || 1 |
|
| 52 | ||
| 53 |
scope = User.logged.status(@status).preload(:email_address) |
|
| 54 |
scope = scope.like(params[:name]) if params[:name].present? |
|
| 55 |
scope = scope.in_group(params[:group_id]) if params[:group_id].present? |
|
| 48 |
if @query.valid? |
|
| 49 |
scope = @query.results_scope |
|
| 56 | 50 | |
| 57 |
if params[:twofa].present? |
|
| 58 |
case params[:twofa].to_i |
|
| 59 |
when 1 |
|
| 60 |
scope = scope.where.not(twofa_scheme: nil) |
|
| 61 |
when 0 |
|
| 62 |
scope = scope.where(twofa_scheme: nil) |
|
| 63 |
end |
|
| 64 |
end |
|
| 51 |
# sort_init 'login', 'asc' |
|
| 52 |
# sort_update %w(login firstname lastname admin created_on last_login_on) |
|
| 65 | 53 | |
| 66 |
@user_count = scope.count |
|
| 67 |
@user_pages = Paginator.new @user_count, @limit, params['page'] |
|
| 68 |
@offset ||= @user_pages.offset |
|
| 69 |
@users = scope.order(sort_clause).limit(@limit).offset(@offset).to_a |
|
| 54 |
@user_count = scope.count |
|
| 70 | 55 | |
| 71 |
respond_to do |format| |
|
| 72 |
format.html do |
|
| 73 |
@groups = Group.givable.sort |
|
| 74 |
render :layout => !request.xhr? |
|
| 56 |
respond_to do |format| |
|
| 57 |
format.html do |
|
| 58 |
@limit = per_page_option |
|
| 59 |
@user_pages = Paginator.new @user_count, @limit, params['page'] |
|
| 60 |
@offset ||= @user_pages.offset |
|
| 61 |
@users = scope.limit(@limit).offset(@offset).to_a |
|
| 62 |
render :layout => !request.xhr? |
|
| 63 |
end |
|
| 64 |
format.csv do |
|
| 65 |
# Export all entries |
|
| 66 |
@entries = scope.to_a |
|
| 67 |
send_data(query_to_csv(@entries, @query, params), :type => 'text/csv; header=present', :filename => 'users.csv') |
|
| 68 |
end |
|
| 69 |
format.api do |
|
| 70 |
@offset, @limit = api_offset_and_limit |
|
| 71 |
@users = scope.limit(@limit).offset(@offset).to_a |
|
| 72 |
end |
|
| 75 | 73 |
end |
| 76 |
format.csv do |
|
| 77 |
send_data(users_to_csv(scope.order(sort_clause)), :type => 'text/csv; header=present', :filename => 'users.csv') |
|
| 74 |
else |
|
| 75 |
respond_to do |format| |
|
| 76 |
format.html {render :layout => !request.xhr?}
|
|
| 77 |
format.csv {head 422}
|
|
| 78 |
format.api {render_validation_errors(@query)}
|
|
| 78 | 79 |
end |
| 79 |
format.api |
|
| 80 | 80 |
end |
| 81 | 81 |
end |
| 82 | 82 | |
| app/helpers/user_queries_helper.rb | ||
|---|---|---|
| 1 |
module UserQueriesHelper |
|
| 2 | ||
| 3 |
def column_value(column, object, value) |
|
| 4 |
if object.is_a?(User) && column.name == :status |
|
| 5 |
user_status_label(column.value_object(object)) |
|
| 6 |
else |
|
| 7 |
super |
|
| 8 |
end |
|
| 9 |
end |
|
| 10 | ||
| 11 |
def csv_value(column, object, value) |
|
| 12 |
if object.is_a?(User) |
|
| 13 |
case column.name |
|
| 14 |
when :status |
|
| 15 |
user_status_label(column.value_object(object)) |
|
| 16 |
when :twofa_scheme |
|
| 17 |
twofa_scheme_label value |
|
| 18 |
else |
|
| 19 |
super |
|
| 20 |
end |
|
| 21 |
else |
|
| 22 |
super |
|
| 23 |
end |
|
| 24 |
end |
|
| 25 | ||
| 26 |
def user_status_label(value) |
|
| 27 |
case value.to_i |
|
| 28 |
when User::STATUS_ACTIVE |
|
| 29 |
l(:status_active) |
|
| 30 |
when User::STATUS_REGISTERED |
|
| 31 |
l(:status_registered) |
|
| 32 |
when User::STATUS_LOCKED |
|
| 33 |
l(:status_locked) |
|
| 34 |
end |
|
| 35 |
end |
|
| 36 | ||
| 37 |
def twofa_scheme_label(value) |
|
| 38 |
if value |
|
| 39 |
::I18n.t :"twofa__#{value}__name"
|
|
| 40 |
else |
|
| 41 |
::I18n.t :label_disabled |
|
| 42 |
end |
|
| 43 |
end |
|
| 44 |
end |
|
| app/models/query.rb | ||
|---|---|---|
| 150 | 150 |
end |
| 151 | 151 | |
| 152 | 152 |
def value_object(object) |
| 153 |
if custom_field.visible_by?(object.project, User.current) |
|
| 153 |
project = object.project if object.respond_to?(:project) |
|
| 154 |
if custom_field.visible_by?(project, User.current) |
|
| 154 | 155 |
cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}
|
| 155 | 156 |
cv.size > 1 ? cv.sort_by {|e| e.value.to_s} : cv.first
|
| 156 | 157 |
else |
| app/models/user_query.rb | ||
|---|---|---|
| 1 |
# frozen_string_literal: true |
|
| 2 | ||
| 3 |
# Redmine - project management software |
|
| 4 |
# Copyright (C) 2006-2022 Jean-Philippe Lang |
|
| 5 |
# |
|
| 6 |
# This program is free software; you can redistribute it and/or |
|
| 7 |
# modify it under the terms of the GNU General Public License |
|
| 8 |
# as published by the Free Software Foundation; either version 2 |
|
| 9 |
# of the License, or (at your option) any later version. |
|
| 10 |
# |
|
| 11 |
# This program is distributed in the hope that it will be useful, |
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
| 14 |
# GNU General Public License for more details. |
|
| 15 |
# |
|
| 16 |
# You should have received a copy of the GNU General Public License |
|
| 17 |
# along with this program; if not, write to the Free Software |
|
| 18 | ||
| 19 |
class UserQuery < Query |
|
| 20 |
self.queried_class = Principal # must be Principal (not User) for custom field filters to work |
|
| 21 | ||
| 22 |
self.available_columns = [ |
|
| 23 |
QueryColumn.new(:login, sortable: "#{User.table_name}.login"),
|
|
| 24 |
QueryColumn.new(:firstname, sortable: "#{User.table_name}.firstname"),
|
|
| 25 |
QueryColumn.new(:lastname, sortable: "#{User.table_name}.lastname"),
|
|
| 26 |
QueryColumn.new(:mail, sortable: "#{EmailAddress.table_name}.address"),
|
|
| 27 |
QueryColumn.new(:admin, sortable: "#{User.table_name}.admin"),
|
|
| 28 |
QueryColumn.new(:created_on, :sortable => "#{User.table_name}.created_on"),
|
|
| 29 |
QueryColumn.new(:updated_on, :sortable => "#{User.table_name}.updated_on"),
|
|
| 30 |
QueryColumn.new(:last_login_on, :sortable => "#{User.table_name}.last_login_on"),
|
|
| 31 |
QueryColumn.new(:passwd_changed_on, :sortable => "#{User.table_name}.passwd_changed_on"),
|
|
| 32 |
QueryColumn.new(:status, sortable: "#{User.table_name}.status")
|
|
| 33 |
] |
|
| 34 | ||
| 35 |
def initialize(attributes=nil, *args) |
|
| 36 |
super attributes |
|
| 37 |
self.filters ||= { 'status' => {operator: "=", values: [User::STATUS_ACTIVE]} }
|
|
| 38 |
end |
|
| 39 | ||
| 40 |
def initialize_available_filters |
|
| 41 |
add_available_filter "status", |
|
| 42 |
type: :list, values: ->{ user_statuses_values }
|
|
| 43 |
add_available_filter "is_member_of_group", |
|
| 44 |
type: :list_optional, |
|
| 45 |
values: ->{ Group.givable.visible.map {|g| [g.name, g.id.to_s] } }
|
|
| 46 |
if Setting.twofa? |
|
| 47 |
add_available_filter "twofa_scheme", |
|
| 48 |
type: :list_optional, |
|
| 49 |
values: ->{ Redmine::Twofa.available_schemes.map {|s| [I18n.t("twofa__#{s}__name"), s] } }
|
|
| 50 |
end |
|
| 51 |
add_available_filter "name", type: :text |
|
| 52 |
add_available_filter "created_on", type: :date_past |
|
| 53 |
add_available_filter "last_login_on", type: :date_past |
|
| 54 |
add_available_filter "admin", |
|
| 55 |
type: :list, |
|
| 56 |
values: [ [l(:general_text_yes), '1'], [l(:general_text_no), '0'] ] |
|
| 57 |
add_custom_fields_filters(user_custom_fields) |
|
| 58 |
end |
|
| 59 | ||
| 60 |
def user_statuses_values |
|
| 61 |
[ |
|
| 62 |
[l(:status_active), User::STATUS_ACTIVE.to_s], |
|
| 63 |
[l(:status_registered), User::STATUS_REGISTERED.to_s], |
|
| 64 |
[l(:status_locked), User::STATUS_LOCKED.to_s] |
|
| 65 |
] |
|
| 66 |
end |
|
| 67 | ||
| 68 |
def available_columns |
|
| 69 |
return @available_columns if @available_columns |
|
| 70 | ||
| 71 |
@available_columns = self.class.available_columns.dup |
|
| 72 |
if Setting.twofa? |
|
| 73 |
@available_columns << QueryColumn.new(:twofa_scheme, sortable: "#{User.table_name}.twofa_scheme")
|
|
| 74 |
end |
|
| 75 |
@available_columns += user_custom_fields.visible. |
|
| 76 |
map {|cf| QueryCustomFieldColumn.new(cf)}
|
|
| 77 | ||
| 78 |
@available_columns |
|
| 79 |
end |
|
| 80 | ||
| 81 |
# Returns a scope of user custom fields that are available as columns or filters |
|
| 82 |
def user_custom_fields |
|
| 83 |
UserCustomField.sorted |
|
| 84 |
end |
|
| 85 | ||
| 86 | ||
| 87 |
def default_columns_names |
|
| 88 |
@default_columns_names ||= %i[ login firstname lastname mail admin created_on last_login_on ] |
|
| 89 |
end |
|
| 90 | ||
| 91 |
def default_sort_criteria |
|
| 92 |
[['login', 'asc']] |
|
| 93 |
end |
|
| 94 | ||
| 95 |
def base_scope |
|
| 96 |
User.logged.where(statement).includes(:email_address) |
|
| 97 |
end |
|
| 98 | ||
| 99 |
def results_scope(options={})
|
|
| 100 |
order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?) |
|
| 101 | ||
| 102 |
base_scope. |
|
| 103 |
order(order_option). |
|
| 104 |
joins(joins_for_order_statement(order_option.join(',')))
|
|
| 105 |
end |
|
| 106 | ||
| 107 |
def sql_for_admin_field(field, operator, value) |
|
| 108 |
return unless value = value.first |
|
| 109 |
true_value = operator == '=' ? '1' : '0' |
|
| 110 |
val = (value.to_s == true_value) ? |
|
| 111 |
self.class.connection.quoted_true : |
|
| 112 |
self.class.connection.quoted_false |
|
| 113 |
"(#{User.table_name}.admin = #{val})"
|
|
| 114 |
end |
|
| 115 | ||
| 116 |
def sql_for_is_member_of_group_field(field, operator, value) |
|
| 117 |
if ["*", "!*"].include? operator |
|
| 118 |
value = Group.givable.map(&:id) |
|
| 119 |
end |
|
| 120 | ||
| 121 |
e = operator.start_with?("!") ? "NOT EXISTS" : "EXISTS"
|
|
| 122 | ||
| 123 |
"(#{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")}))"
|
|
| 124 |
end |
|
| 125 | ||
| 126 |
def sql_for_name_field(field, operator, value) |
|
| 127 |
match = operator == '~' |
|
| 128 |
name_sql = sql_contains("CONCAT(login, ' ', firstname, ' ', lastname)",
|
|
| 129 |
value.first, match: match) |
|
| 130 | ||
| 131 |
emails = EmailAddress.table_name |
|
| 132 |
email_sql = <<-SQL |
|
| 133 |
#{match ? "EXISTS" : "NOT EXISTS"}
|
|
| 134 |
(SELECT 1 FROM #{emails} WHERE
|
|
| 135 |
#{emails}.user_id = #{User.table_name}.id AND
|
|
| 136 |
#{sql_contains("#{emails}.address", value.first, match: true)})
|
|
| 137 |
SQL |
|
| 138 | ||
| 139 |
if match |
|
| 140 |
"(#{name_sql}) OR (#{email_sql})"
|
|
| 141 |
else |
|
| 142 |
"(#{name_sql}) AND (#{email_sql})"
|
|
| 143 |
end |
|
| 144 |
end |
|
| 145 | ||
| 146 |
end |
|
| app/views/context_menus/users.html.erb | ||
|---|---|---|
| 1 |
<ul> |
|
| 2 |
<% if @user %> |
|
| 3 |
<% if @user.locked? %> |
|
| 4 |
<li> |
|
| 5 |
<%= context_menu_link l(:button_unlock), user_path(@user, user: { status: User::STATUS_ACTIVE }, back_url: @back), method: :put, class: 'icon icon-unlock' %>
|
|
| 6 |
</li> |
|
| 7 |
<% elsif User.current != @user %> |
|
| 8 |
<li> |
|
| 9 |
<%= context_menu_link l(:button_lock), user_path(@user, user: { status: User::STATUS_LOCKED }, back_url: @back), method: :put, class: 'icon icon-lock' %>
|
|
| 10 |
</li> |
|
| 11 |
<% end %> |
|
| 12 | ||
| 13 |
<li> |
|
| 14 |
<%= context_menu_link l(:button_edit), edit_user_path(@user, back_url: @back), class: 'icon icon-edit' %> |
|
| 15 |
</li> |
|
| 16 | ||
| 17 |
<% unless User.current == @user %> |
|
| 18 |
<li> |
|
| 19 |
<%= context_menu_link l(:button_delete), user_path(@user, back_url: @back), |
|
| 20 |
method: :delete, class: 'icon icon-del' %> |
|
| 21 |
</li> |
|
| 22 |
<% end %> |
|
| 23 |
<% end %> |
|
| 24 |
</ul> |
|
| app/views/users/_list.html.erb | ||
|---|---|---|
| 1 |
<%= form_tag({}, data: {cm_url: users_context_menu_path}) do -%>
|
|
| 2 |
<%= hidden_field_tag 'back_url', url_for(params: request.query_parameters), id: nil %> |
|
| 3 |
<div class="autoscroll"> |
|
| 4 |
<table class="list odd-even users"> |
|
| 5 |
<thead> |
|
| 6 |
<tr> |
|
| 7 |
<th class="checkbox hide-when-print"> |
|
| 8 |
<%= check_box_tag 'check_all', '', false, :class => 'toggle-selection', |
|
| 9 |
:title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %>
|
|
| 10 |
</th> |
|
| 11 |
<% @query.inline_columns.each do |column| %> |
|
| 12 |
<%= column_header(@query, column) %> |
|
| 13 |
<% end %> |
|
| 14 |
<th></th> |
|
| 15 |
</tr> |
|
| 16 |
</thead> |
|
| 17 |
<tbody> |
|
| 18 |
<% grouped_query_results(users, @query) do |user, group_name, group_count, group_totals| -%> |
|
| 19 |
<% if group_name %> |
|
| 20 |
<% reset_cycle %> |
|
| 21 |
<tr class="group open"> |
|
| 22 |
<td colspan="<%= @query.inline_columns.size + 2 %>"> |
|
| 23 |
<span class="expander" onclick="toggleRowGroup(this);"> </span> |
|
| 24 |
<span class="name"><%= group_name %></span> |
|
| 25 |
<% if group_count %> |
|
| 26 |
<span class="count"><%= group_count %></span> |
|
| 27 |
<% end %> |
|
| 28 |
<span class="totals"><%= group_totals %></span> |
|
| 29 |
<%= link_to_function("#{l(:button_collapse_all)}/#{l(:button_expand_all)}",
|
|
| 30 |
"toggleAllRowGroups(this)", :class => 'toggle-all') %> |
|
| 31 |
</td> |
|
| 32 |
</tr> |
|
| 33 |
<% end %> |
|
| 34 |
<tr id="user-<%= user.id %>" class="user <%= cycle("odd", "even") %> hascontextmenu">
|
|
| 35 |
<td class="checkbox hide-when-print"><%= check_box_tag("ids[]", user.id, false, id: nil) %></td>
|
|
| 36 |
<% @query.inline_columns.each do |column| %> |
|
| 37 |
<% if column.name == :login %> |
|
| 38 |
<%= content_tag('td', link_to(user.login, edit_user_path(user)), class: column.css_classes) %>
|
|
| 39 |
<% else %> |
|
| 40 |
<%= content_tag('td', column_content(column, user), class: column.css_classes) %>
|
|
| 41 |
<% end %> |
|
| 42 |
<% end %> |
|
| 43 |
<td class="buttons"> |
|
| 44 |
<%= link_to_context_menu %> |
|
| 45 |
</td> |
|
| 46 |
</tr> |
|
| 47 |
<% @query.block_columns.each do |column| |
|
| 48 |
if (text = column_content(column, issue)) && text.present? -%> |
|
| 49 |
<tr class="<%= current_cycle %>"> |
|
| 50 |
<td colspan="<%= @query.inline_columns.size + 1 %>" class="<%= column.css_classes %>"> |
|
| 51 |
<% if query.block_columns.count > 1 %> |
|
| 52 |
<span><%= column.caption %></span> |
|
| 53 |
<% end %> |
|
| 54 |
<%= text %> |
|
| 55 |
</td> |
|
| 56 |
</tr> |
|
| 57 |
<% end -%> |
|
| 58 |
<% end -%> |
|
| 59 |
<% end -%> |
|
| 60 |
</tbody> |
|
| 61 |
</table> |
|
| 62 |
</div> |
|
| 63 |
<% end -%> |
|
| 64 | ||
| 65 |
<%= context_menu %> |
|
| 66 | ||
| app/views/users/index.html.erb | ||
|---|---|---|
| 7 | 7 |
<% end %> |
| 8 | 8 |
</div> |
| 9 | 9 | |
| 10 |
<h2><%=l(:label_user_plural)%></h2>
|
|
| 10 |
<h2><%= @query.new_record? ? l(:label_user_plural) : @query.name %></h2>
|
|
| 11 | 11 | |
| 12 |
<%= form_tag(users_path, { :method => :get, :id => 'users_form' }) do %>
|
|
| 13 |
<fieldset><legend><%= l(:label_filter_plural) %></legend> |
|
| 14 |
<label for='status'><%= l(:field_status) %>:</label> |
|
| 15 |
<%= select_tag 'status', users_status_options_for_select(@status), :class => "small", :onchange => "this.form.submit(); return false;" %> |
|
| 16 | ||
| 17 |
<% if @groups.present? %> |
|
| 18 |
<label for='group_id'><%= l(:label_group) %>:</label> |
|
| 19 |
<%= 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;" %>
|
|
| 20 |
<% end %> |
|
| 21 | ||
| 22 |
<% if Setting.twofa_required? || Setting.twofa_optional? %> |
|
| 23 |
<label for='twofa'><%= l(:setting_twofa) %>:</label> |
|
| 24 |
<%= 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 %> |
|
| 12 |
<%= form_tag(users_path, method: :get, id: 'query_form') do %> |
|
| 13 |
<%= render partial: 'queries/query_form' %> |
|
| 25 | 14 |
<% end %> |
| 26 | 15 | |
| 27 |
<label for='name'><%= l(:label_user) %>:</label> |
|
| 28 |
<%= text_field_tag 'name', params[:name], :size => 30 %> |
|
| 29 |
<%= submit_tag l(:button_apply), :class => "small", :name => nil %> |
|
| 30 |
<%= link_to l(:button_clear), users_path, :class => 'icon icon-reload' %> |
|
| 31 |
</fieldset> |
|
| 32 |
<%= hidden_field_tag 'encoding', l(:general_csv_encoding) unless l(:general_csv_encoding).casecmp('UTF-8') == 0 %>
|
|
| 33 |
<% end %> |
|
| 34 |
|
|
| 35 | ||
| 36 |
<% if @users.any? %> |
|
| 37 |
<div class="autoscroll"> |
|
| 38 |
<table class="list users"> |
|
| 39 |
<thead><tr> |
|
| 40 |
<%= sort_header_tag('login', :caption => l(:field_login)) %>
|
|
| 41 |
<%= sort_header_tag('firstname', :caption => l(:field_firstname)) %>
|
|
| 42 |
<%= sort_header_tag('lastname', :caption => l(:field_lastname)) %>
|
|
| 43 |
<th><%= l(:field_mail) %></th> |
|
| 44 |
<%= sort_header_tag('admin', :caption => l(:field_admin), :default_order => 'desc') %>
|
|
| 45 |
<% if Setting.twofa_required? || Setting.twofa_optional? %> |
|
| 46 |
<th class="whitespace-normal"><%= l(:setting_twofa) %></th> |
|
| 16 |
<% if @query.valid? %> |
|
| 17 |
<% if @users.empty? %> |
|
| 18 |
<p class="nodata"><%= l(:label_no_data) %></p> |
|
| 19 |
<% else %> |
|
| 20 |
<%= render_query_totals(@query) %> |
|
| 21 |
<%= render partial: 'list', :locals => { :users => @users }%>
|
|
| 22 |
<span class="pagination"><%= pagination_links_full @user_pages, @user_count %></span> |
|
| 47 | 23 |
<% end %> |
| 48 |
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
|
|
| 49 |
<%= sort_header_tag('last_login_on', :caption => l(:field_last_login_on), :default_order => 'desc') %>
|
|
| 50 |
<th></th> |
|
| 51 |
</tr></thead> |
|
| 52 |
<tbody> |
|
| 53 |
<% for user in @users -%> |
|
| 54 |
<tr class="<%= user.css_classes %>"> |
|
| 55 |
<td class="username"><%= avatar(user, :size => "14") %><%= link_to user.login, edit_user_path(user) %></td> |
|
| 56 |
<td class="firstname"><%= user.firstname %></td> |
|
| 57 |
<td class="lastname"><%= user.lastname %></td> |
|
| 58 |
<td class="email"><%= mail_to(user.mail) %></td> |
|
| 59 |
<td class="tick"><%= checked_image user.admin? %></td> |
|
| 60 |
<% if Setting.twofa_required? || Setting.twofa_optional? %> |
|
| 61 |
<td class="twofa tick"><%= checked_image user.twofa_active? %></td> |
|
| 24 |
<% other_formats_links do |f| %> |
|
| 25 |
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '350px'); return false;" %>
|
|
| 62 | 26 |
<% end %> |
| 63 |
<td class="created_on"><%= format_time(user.created_on) %></td> |
|
| 64 |
<td class="last_login_on"><%= format_time(user.last_login_on) unless user.last_login_on.nil? %></td> |
|
| 65 |
<td class="buttons"> |
|
| 66 |
<%= change_status_link(user) %> |
|
| 67 |
<%= delete_link user_path(user, :back_url => request.original_fullpath), :data => {} unless User.current == user %>
|
|
| 68 |
</td> |
|
| 69 |
</tr> |
|
| 70 |
<% end -%> |
|
| 71 |
</tbody> |
|
| 72 |
</table> |
|
| 73 |
</div> |
|
| 74 |
<span class="pagination"><%= pagination_links_full @user_pages, @user_count %></span> |
|
| 75 |
<% other_formats_links do |f| %> |
|
| 76 |
<%= f.link_to_with_query_parameters 'CSV', {}, :onclick => "showModal('csv-export-options', '330px'); return false;" %>
|
|
| 77 |
<% end %> |
|
| 78 |
<div id="csv-export-options" style="display: none;"> |
|
| 79 |
<h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3> |
|
| 80 |
<%= export_csv_encoding_select_tag %> |
|
| 81 |
<p class="buttons"> |
|
| 82 |
<%= submit_tag l(:button_export), :name => nil, :id => 'csv-export-button' %> |
|
| 83 |
<%= submit_tag l(:button_cancel), :name => nil, :onclick => 'hideModal(this);', :type => 'button' %> |
|
| 84 |
</p> |
|
| 85 |
</div> |
|
| 86 |
<%= javascript_tag do %> |
|
| 87 |
$(document).ready(function(){
|
|
| 88 |
$('input#csv-export-button').click(function(){
|
|
| 89 |
$('form input#encoding').val($('select#encoding option:selected').val());
|
|
| 90 |
$('form#users_form').attr('action', "<%= users_path(:format => 'csv') %>").submit();
|
|
| 91 |
$('form#users_form').attr('action', '<%= users_path %>');
|
|
| 92 |
hideModal(this); |
|
| 93 |
}); |
|
| 94 |
}); |
|
| 95 |
<% end %> |
|
| 96 |
<% else %> |
|
| 97 |
<p class="nodata"><%= l(:label_no_data) %></p> |
|
| 27 | ||
| 28 |
<div id="csv-export-options" style="display:none;"> |
|
| 29 |
<h3 class="title"><%= l(:label_export_options, :export_format => 'CSV') %></h3> |
|
| 30 |
<%= form_tag(users_path(format: 'csv'), method: :get, id: 'csv-export-form') do %> |
|
| 31 |
<%= query_as_hidden_field_tags(@query) %> |
|
| 32 |
<%= hidden_field_tag('query_name', @query.name) %>
|
|
| 33 |
<p> |
|
| 34 |
<label><%= radio_button_tag 'c[]', '', true %> <%= l(:description_selected_columns) %></label><br /> |
|
| 35 |
<label><%= radio_button_tag 'c[]', 'all_inline' %> <%= l(:description_all_columns) %></label> |
|
| 36 |
</p> |
|
| 37 |
<% if @query.available_block_columns.any? %> |
|
| 38 |
<fieldset id="csv-export-block-columns"> |
|
| 39 |
<legend> |
|
| 40 |
<%= toggle_checkboxes_link('#csv-export-block-columns input[type=checkbox]') %>
|
|
| 41 |
</legend> |
|
| 42 |
<% @query.available_block_columns.each do |column| %> |
|
| 43 |
<label><%= check_box_tag 'c[]', column.name, @query.has_column?(column), :id => nil %> <%= column.caption %></label> |
|
| 44 |
<% end %> |
|
| 45 |
</fieldset> |
|
| 46 |
<% end %> |
|
| 47 |
<%= export_csv_encoding_select_tag %> |
|
| 48 |
<p class="buttons"> |
|
| 49 |
<%= submit_tag l(:button_export), :name => nil, :onclick => "hideModal(this);", :data => { :disable_with => false } %>
|
|
| 50 |
<%= link_to_function l(:button_cancel), "hideModal(this);" %> |
|
| 51 |
</p> |
|
| 52 |
<% end %> |
|
| 53 |
</div> |
|
| 98 | 54 |
<% end %> |
| 99 | 55 | |
| 56 |
<% content_for :sidebar do %> |
|
| 57 |
<%= render_sidebar_queries(UserQuery, nil) %> |
|
| 58 |
<%= call_hook(:view_users_sidebar_queries_bottom) %> |
|
| 59 |
<% end %> |
|
| 100 | 60 |
<% html_title(l(:label_user_plural)) -%> |
| config/locales/de.yml | ||
|---|---|---|
| 331 | 331 |
field_is_filter: Als Filter benutzen |
| 332 | 332 |
field_is_for_all: Für alle Projekte |
| 333 | 333 |
field_is_in_roadmap: In der Roadmap anzeigen |
| 334 |
field_is_member_of_group: Mitglied in Gruppe |
|
| 334 | 335 |
field_is_private: Privat |
| 335 | 336 |
field_is_public: Öffentlich |
| 336 | 337 |
field_is_required: Erforderlich |
| config/locales/en.yml | ||
|---|---|---|
| 302 | 302 |
field_title: Title |
| 303 | 303 |
field_project: Project |
| 304 | 304 |
field_issue: Issue |
| 305 |
field_is_member_of_group: Member of group |
|
| 305 | 306 |
field_status: Status |
| 306 | 307 |
field_notes: Notes |
| 307 | 308 |
field_is_closed: Issue closed |
| config/routes.rb | ||
|---|---|---|
| 108 | 108 |
match 'my/twofa/backup_codes', :controller => 'twofa_backup_codes', :action => 'show', :via => [:get] |
| 109 | 109 |
match 'users/:user_id/twofa/deactivate', :controller => 'twofa', :action => 'admin_deactivate', :via => :post |
| 110 | 110 | |
| 111 |
match '/users/context_menu', to: 'context_menus#users', as: :users_context_menu, via: [:get, :post] |
|
| 111 | 112 |
resources :users do |
| 112 | 113 |
resources :memberships, :controller => 'principal_memberships' |
| 113 | 114 |
resources :email_addresses, :only => [:index, :create, :update, :destroy] |
| test/functional/users_controller_test.rb | ||
|---|---|---|
| 37 | 37 |
def test_index |
| 38 | 38 |
get :index |
| 39 | 39 |
assert_response :success |
| 40 |
active = User.active.first |
|
| 41 |
locked = User.where(status: User::STATUS_LOCKED).first |
|
| 40 | 42 |
assert_select 'table.users' |
| 41 |
assert_select 'tr.user.active'
|
|
| 42 |
assert_select 'tr.user.locked', 0
|
|
| 43 |
assert_select "tr#user-#{active.id}"
|
|
| 44 |
assert_select "tr#user-#{locked.id}", 0
|
|
| 43 | 45 |
end |
| 44 | 46 | |
| 45 | 47 |
def test_index_with_status_filter |
| 46 |
get :index, :params => {:status => 3}
|
|
| 48 |
get :index, params: { set_filter: 1, f: ['status'], op: {status: '='}, v: {status: [3]} }
|
|
| 47 | 49 |
assert_response :success |
| 48 |
assert_select 'tr.user.active', 0 |
|
| 49 |
assert_select 'tr.user.locked' |
|
| 50 |
assert_select "tr.user", User.where(status: 3).count |
|
| 50 | 51 |
end |
| 51 | 52 | |
| 52 | 53 |
def test_index_with_name_filter |
| 53 |
get :index, :params => {:name => 'john'}
|
|
| 54 |
get :index, params: { set_filter: 1, f: ['name'], op: {name: '~'}, v: {name: ['john']} }
|
|
| 54 | 55 |
assert_response :success |
| 55 |
assert_select 'tr.user td.username', :text => 'jsmith'
|
|
| 56 |
assert_select 'tr.user td.login', text: 'jsmith'
|
|
| 56 | 57 |
assert_select 'tr.user', 1 |
| 57 | 58 |
end |
| 58 | 59 | |
| 59 | 60 |
def test_index_with_group_filter |
| 60 |
get :index, :params => {:group_id => '10'}
|
|
| 61 |
get :index, params: {
|
|
| 62 |
set_filter: 1, |
|
| 63 |
f: ['is_member_of_group'], op: {is_member_of_group: '='}, v: {is_member_of_group: ['10']}
|
|
| 64 |
} |
|
| 61 | 65 |
assert_response :success |
| 62 | ||
| 63 | 66 |
assert_select 'tr.user', Group.find(10).users.count |
| 64 |
assert_select 'select[name=group_id]' do |
|
| 65 |
assert_select 'option[value="10"][selected=selected]' |
|
| 66 |
end |
|
| 67 | 67 |
end |
| 68 | 68 | |
| 69 | 69 |
def test_index_should_not_show_2fa_filter_and_column_if_disabled |
| ... | ... | |
| 71 | 71 |
get :index |
| 72 | 72 |
assert_response :success |
| 73 | 73 | |
| 74 |
assert_select "select#twofa", 0 |
|
| 75 |
assert_select 'td.twofa', 0 |
|
| 74 |
assert_select "select#add_filter_select" do |
|
| 75 |
assert_select "option[value=twofa_scheme]", 0 |
|
| 76 |
end |
|
| 77 |
assert_select "select#available_c" do |
|
| 78 |
assert_select "option[value=twofa_scheme]", 0 |
|
| 79 |
end |
|
| 76 | 80 |
end |
| 77 | 81 |
end |
| 78 | 82 | |
| ... | ... | |
| 83 | 87 |
user.twofa_scheme = "totp" |
| 84 | 88 |
user.save |
| 85 | 89 | |
| 86 |
get :index, :params => {:twofa => '1'}
|
|
| 90 |
get :index, params: { set_filter: 1, f: ['twofa_scheme'], op: {twofa_scheme: '*'} }
|
|
| 87 | 91 |
assert_response :success |
| 88 | 92 | |
| 89 |
assert_select "select#twofa", 1 |
|
| 90 | ||
| 93 |
assert_select 'tr#user-1', 1 |
|
| 91 | 94 |
assert_select 'tr.user', 1 |
| 92 |
assert_select 'td.twofa.tick .icon-checked' |
|
| 95 | ||
| 96 |
assert_select "select#add_filter_select" do |
|
| 97 |
assert_select "option[value=twofa_scheme]" |
|
| 98 |
end |
|
| 99 |
assert_select "select#available_c" do |
|
| 100 |
assert_select "option[value=twofa_scheme]" |
|
| 101 |
end |
|
| 102 |
end |
|
| 103 | ||
| 104 |
def test_index_filter_by_twofa_scheme |
|
| 105 |
get :index, params: {
|
|
| 106 |
set_filter: 1, |
|
| 107 |
f: ['twofa_scheme'], op: {twofa_scheme: '='}, v: {twofa_scheme: ['totp']}
|
|
| 108 |
} |
|
| 109 |
assert_response :success |
|
| 110 | ||
| 111 |
assert_select 'tr#user-1', 1 |
|
| 112 | ||
| 113 |
assert_select "select#add_filter_select" do |
|
| 114 |
assert_select "option[value=twofa_scheme]" |
|
| 115 |
end |
|
| 116 |
assert_select "select#available_c" do |
|
| 117 |
assert_select "option[value=twofa_scheme]" |
|
| 118 |
end |
|
| 93 | 119 |
end |
| 94 | 120 |
end |
| 95 | 121 | |
| ... | ... | |
| 100 | 126 |
user.twofa_scheme = "totp" |
| 101 | 127 |
user.save |
| 102 | 128 | |
| 103 |
get :index, :params => {:twofa => '0'}
|
|
| 129 |
get :index, params: { set_filter: 1, f: ['twofa_scheme'], op: {twofa_scheme: '!*'} }
|
|
| 104 | 130 |
assert_response :success |
| 105 | 131 | |
| 106 |
assert_select "select#twofa", 1 |
|
| 107 |
assert_select "td.twofa.tick" do |
|
| 108 |
assert_select "span.icon-checked", 0 |
|
| 109 |
end |
|
| 132 |
assert_select 'tr#user-1', 0 |
|
| 133 |
assert_select 'tr.user' |
|
| 110 | 134 |
end |
| 111 | 135 |
end |
| 112 | 136 | |
| ... | ... | |
| 114 | 138 |
with_settings :default_language => 'en' do |
| 115 | 139 |
user = User.logged.status(1).first |
| 116 | 140 |
user.update(passwd_changed_on: Time.current.last_month, twofa_scheme: 'totp') |
| 117 |
get :index, params: {format: 'csv'}
|
|
| 141 |
get :index, params: {format: 'csv', c: ['updated_on', 'status', 'passwd_changed_on', 'twofa_scheme']}
|
|
| 118 | 142 |
assert_response :success |
| 119 | 143 | |
| 120 | 144 |
assert_equal User.logged.status(1).count, response.body.chomp.split("\n").size - 1
|
| ... | ... | |
| 142 | 166 | |
| 143 | 167 |
User.find(@request.session[:user_id]).update(:language => nil) |
| 144 | 168 |
with_settings :default_language => 'fr' do |
| 145 |
get :index, :params => {:name => user.lastname, :format => 'csv'}
|
|
| 169 |
get :index, params: {
|
|
| 170 |
c: [ "cf_#{float_custom_field.id}", "cf_#{date_custom_field.id}" ],
|
|
| 171 |
f: ["name"], |
|
| 172 |
op: { name: "~" },
|
|
| 173 |
v: { name: [user.lastname] },
|
|
| 174 |
format: 'csv' |
|
| 175 |
} |
|
| 146 | 176 |
assert_response :success |
| 147 | 177 | |
| 148 | 178 |
assert_include 'float field;date field', response.body |
| ... | ... | |
| 153 | 183 | |
| 154 | 184 |
def test_index_csv_with_status_filter |
| 155 | 185 |
with_settings :default_language => 'en' do |
| 156 |
get :index, :params => {:status => 3, :format => 'csv'}
|
|
| 186 |
get :index, :params => {
|
|
| 187 |
:set_filter => '1', |
|
| 188 |
f: [:status], :op => { :status => '=' }, :v => { :status => [3] },
|
|
| 189 |
c: [:login, :status], |
|
| 190 |
:format => 'csv' |
|
| 191 |
} |
|
| 157 | 192 |
assert_response :success |
| 158 | 193 | |
| 159 | 194 |
assert_equal User.logged.status(3).count, response.body.chomp.split("\n").size - 1
|
| ... | ... | |
| 164 | 199 |
end |
| 165 | 200 | |
| 166 | 201 |
def test_index_csv_with_name_filter |
| 167 |
get :index, :params => {:name => 'John', :format => 'csv'}
|
|
| 202 |
get :index, :params => {
|
|
| 203 |
:set_filter => '1', |
|
| 204 |
f: [:name], :op => { :name => '~' }, :v => { :name => ['John'] },
|
|
| 205 |
c: [:login, :firstname, :status], |
|
| 206 |
:format => 'csv' |
|
| 207 |
} |
|
| 168 | 208 |
assert_response :success |
| 169 | 209 | |
| 170 | 210 |
assert_equal User.logged.like('John').count, response.body.chomp.split("\n").size - 1
|
| ... | ... | |
| 173 | 213 |
end |
| 174 | 214 | |
| 175 | 215 |
def test_index_csv_with_group_filter |
| 176 |
get :index, :params => {:group_id => '10', :format => 'csv'}
|
|
| 216 |
get :index, :params => {
|
|
| 217 |
:set_filter => '1', |
|
| 218 |
f: [:is_member_of_group], :op => { :is_member_of_group => '=' }, :v => { :is_member_of_group => [10] },
|
|
| 219 |
c: [:login, :status], |
|
| 220 |
:format => 'csv' |
|
| 221 |
} |
|
| 177 | 222 |
assert_response :success |
| 178 | 223 | |
| 179 | 224 |
assert_equal Group.find(10).users.count, response.body.chomp.split("\n").size - 1
|