Project

General

Profile

Feature #29482 » 0001-Filters-for-Projects-page.patch

Marius BĂLTEANU, 2019-09-28 18:45

View differences:

app/controllers/projects_controller.rb
33 33
  helper :custom_fields
34 34
  helper :issues
35 35
  helper :queries
36
  include QueriesHelper
36 37
  helper :repositories
37 38
  helper :members
38 39
  helper :trackers
......
44 45
      return
45 46
    end
46 47

  
47
    scope = Project.visible.sorted
48
    retrieve_project_query
49
    scope = project_scope
48 50

  
49 51
    respond_to do |format|
50 52
      format.html {
51
        unless params[:closed]
52
          scope = scope.active
53
        end
54 53
        @projects = scope.to_a
55 54
      }
56 55
      format.api  {
......
257 256
    # hide project in layout
258 257
    @project = nil
259 258
  end
259

  
260
  private
261

  
262
  # Returns the ProjectEntry scope for index
263
  def project_scope(options={})
264
    @query.results_scope(options)
265
  end
266

  
267
  def retrieve_project_query
268
    retrieve_query(ProjectQuery, false)
269
  end
260 270
end
app/controllers/queries_controller.rb
126 126
    @query.column_names = nil if params[:default_columns]
127 127
    @query.sort_criteria = (params[:query] && params[:query][:sort_criteria]) || @query.sort_criteria
128 128
    @query.name = params[:query] && params[:query][:name]
129
    if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
129
    if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin? || (@query.type == 'ProjectQuery' && User.current.allowed_to?(:manage_public_queries, @query.project, :global => true))
130 130
      @query.visibility = (params[:query] && params[:query][:visibility]) || Query::VISIBILITY_PRIVATE
131 131
      @query.role_ids = params[:query] && params[:query][:role_ids]
132 132
    else
......
156 156
    redirect_to _time_entries_path(@project, nil, options)
157 157
  end
158 158

  
159
  def redirect_to_project_query(options)
160
    redirect_to projects_path(options)
161
  end
162

  
159 163
  # Returns the Query subclass, IssueQuery by default
160 164
  # for compatibility with previous behaviour
161 165
  def query_class
app/models/project_query.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2017  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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
class ProjectQuery < Query
21

  
22
  self.queried_class = Project
23

  
24
  self.available_columns = []
25

  
26
  def initialize(attributes=nil, *args)
27
    super attributes
28
    self.filters ||= { 'status' => {:operator => "=", :values => ['1']} }
29
  end
30

  
31
  def initialize_available_filters
32
    add_available_filter "status",
33
      :type => :list, :values => lambda { project_statuses_values }
34
    add_available_filter "name", :type => :text
35
    add_available_filter "description", :type => :text
36
    add_available_filter "is_public",
37
      :type => :list,
38
      :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
39
    add_available_filter "created_on", :type => :date_past
40
  end
41

  
42
  def available_columns
43
    []
44
  end
45

  
46
  def base_scope
47
    Project.visible.where(statement)
48
  end
49

  
50
  def results_scope(options={})
51
    order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
52

  
53
    order_option << "#{Project.table_name}.id ASC"
54
    scope = base_scope.
55
      order(order_option).
56
      joins(joins_for_order_statement(order_option.join(',')))
57

  
58
    if has_custom_field_column?
59
      scope = scope.preload(:custom_values)
60
    end
61

  
62
    if has_column?(:parent_id)
63
      scope = scope.preload(:parent)
64
    end
65

  
66
    scope
67
  end
68
end
app/models/query.rb
716 716
  end
717 717

  
718 718
  def columns
719
    return [] if available_columns.empty?
719 720
    # preserve the column_names order
720 721
    cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
721 722
       available_columns.find { |col| col.name == name }
app/views/projects/_sidebar.html.erb
1
<%= render_sidebar_queries(ProjectQuery, @project) %>
2
<%= call_hook(:view_projects_sidebar_queries_bottom) %>
app/views/projects/index.html.erb
1
<% content_for :header_tags do %>
2
    <%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
3
<% end %>
4

  
5 1
<div class="contextual">
6 2
  <%= form_tag({}, :method => :get) do %>
7
    <label for="closed">
8
      <%= check_box_tag 'closed', 1, params[:closed], :onchange => "this.form.submit();" %>
9
      <%= l(:label_show_closed_projects) %>
10
    </label>
11 3
  <% end %>
12 4
  <%= render_project_action_links %>
13 5
</div>
14 6

  
15
<h2><%= l(:label_project_plural) %></h2>
7
<h2><%= @query.new_record? ? l(:label_project_plural) : @query.name %></h2>
16 8

  
17
<div id="projects-index">
18
<%= render_project_hierarchy(@projects) %>
19
</div>
9
<%= form_tag(projects_path(@project, nil), :method => :get, :id => 'query_form') do %>
10
<%= render :partial => 'queries/query_form' %>
11
<% end %>
12

  
13
<% if @query.valid? %>
14
  <% if @projects.empty? %>
15
    <p class="nodata"><%= l(:label_no_data) %></p>
16
  <% else %>
17
    <div id="projects-index">
18
      <%= render_project_hierarchy(@projects) %>
19
    </div>
20
  <% end %>
21
<% end %>
20 22

  
21 23
<% if User.current.logged? %>
22 24
<p style="text-align:right;">
......
24 26
</p>
25 27
<% end %>
26 28

  
29
<% content_for :sidebar do %>
30
  <%= render :partial => 'projects/sidebar' %>
31
<% end %>
32

  
27 33
<% other_formats_links do |f| %>
28 34
  <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
29 35
<% end %>
app/views/queries/_form.html.erb
7 7
<p><label for="query_name"><%=l(:field_name)%></label>
8 8
<%= text_field 'query', 'name', :size => 80 %></p>
9 9

  
10
<% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @query.project) %>
10
<% if User.current.admin? ||
11
      User.current.allowed_to?(:manage_public_queries, @query.project) ||
12
      @query.type == 'ProjectQuery' &&  User.current.allowed_to?(:manage_public_queries, @query.project, :global => true) %>
11 13
<p><label><%=l(:field_visible)%></label>
12 14
  <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PRIVATE %> <%= l(:label_visibility_private) %></label>
13 15
  <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PUBLIC %> <%= l(:label_visibility_public) %></label>
14
  <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
15
  <% Role.givable.sorted.each do |role| %>
16
    <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
16
  <% unless @query.type == 'ProjectQuery' %>
17
    <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
18
      <% Role.givable.sorted.each do |role| %>
19
        <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
20
      <% end %>
21
      <%= hidden_field_tag 'query[role_ids][]', '' %>
17 22
  <% end %>
18
  <%= hidden_field_tag 'query[role_ids][]', '' %>
19 23
</p>
20 24
<% end %>
21 25

  
22
<p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
23
<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %></p>
26
<% unless @query.type == 'ProjectQuery' %>
27
  <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
28
  <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %></p>
29
<% end %>
24 30

  
25 31
<fieldset id="options"><legend><%= l(:label_options) %></legend>
26 32
<p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
......
28 34
      :data => {:disables => "#columns, .block_columns input"} %></p>
29 35

  
30 36
<% unless params[:gantt] %>
31
<p><label for="query_group_by"><%= l(:field_group_by) %></label>
32
<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
37
  <p><label for="query_group_by"><%= l(:field_group_by) %></label>
38
  <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
33 39

  
34
<p class="block_columns"><label><%= l(:button_show) %></label>
35
<%= available_block_columns_tags(@query) %></p>
40
  <% unless @query.available_block_columns.empty? %>
41
    <p class="block_columns"><label><%= l(:button_show) %></label>
42
    <%= available_block_columns_tags(@query) %></p>
43
  <% end %>
36 44

  
37
<p><label><%= l(:label_total_plural) %></label>
38
<%= available_totalable_columns_tags(@query) %></p>
45
  <% unless @query.available_totalable_columns.empty? %>
46
    <p class="totable_columns"><label><%= l(:label_total_plural) %></label>
47
    <%= available_totalable_columns_tags(@query) %></p>
48
  <% end %>
39 49
<% else %>
40 50
  <p><label><%= l(:button_show) %></label>
41 51
  <%= hidden_field_tag 'query[draw_relations]', '0' %>
......
54 64
</fieldset>
55 65

  
56 66
<% unless params[:gantt] %>
57
<fieldset><legend><%= l(:label_sort) %></legend>
67
<fieldset id="sort"><legend><%= l(:label_sort) %></legend>
58 68
<% 3.times do |i| %>
59 69
<%= content_tag(:span, "#{i+1}:", :class => 'query_sort_criteria_count')%>
60 70
<%= label_tag "query_sort_criteria_attribute_" + i.to_s,
app/views/queries/_query_form.html.erb
11 11
    </div>
12 12
  </fieldset>
13 13

  
14
  <fieldset id="options" class="collapsible collapsed">
15
    <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
16
    <div style="display: none;">
17
      <table>
18
        <tr>
19
          <td class="field"><%= l(:field_column_names) %></td>
20
          <td><%= render_query_columns_selection(@query) %></td>
21
        </tr>
22
        <% if @query.groupable_columns.any? %>
23
        <tr>
24
          <td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td>
25
          <td><%= group_by_column_select_tag(@query) %></td>
26
        </tr>
27
        <% end %>
28
        <% if @query.available_block_columns.any? %>
29
        <tr>
30
          <td class="field"><%= l(:button_show) %></td>
31
          <td><%= available_block_columns_tags(@query) %></td>
32
        </tr>
33
        <% end %>
34
        <% if @query.available_totalable_columns.any? %>
35
        <tr>
36
          <td><%= l(:label_total_plural) %></td>
37
          <td><%= available_totalable_columns_tags(@query) %></td>
38
        </tr>
39
        <% end %>
40
      </table>
41
    </div>
42
  </fieldset>
14
  <% if @query.available_columns.any? %>
15
    <fieldset id="options" class="collapsible collapsed">
16
      <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
17
      <div style="display: none;">
18
        <table>
19
          <% if @query.available_columns.any? %>
20
            <tr>
21
              <td class="field"><%= l(:field_column_names) %></td>
22
              <td><%= render_query_columns_selection(@query) %></td>
23
            </tr>
24
          <% end %>
25
          <% if @query.groupable_columns.any? %>
26
            <tr>
27
              <td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td>
28
              <td><%= group_by_column_select_tag(@query) %></td>
29
            </tr>
30
          <% end %>
31
          <% if @query.available_block_columns.any? %>
32
            <tr>
33
              <td class="field"><%= l(:button_show) %></td>
34
              <td><%= available_block_columns_tags(@query) %></td>
35
            </tr>
36
          <% end %>
37
          <% if @query.available_totalable_columns.any? %>
38
            <tr>
39
              <td><%= l(:label_total_plural) %></td>
40
              <td><%= available_totalable_columns_tags(@query) %></td>
41
            </tr>
42
          <% end %>
43
        </table>
44
      </div>
45
    </fieldset>
46
  <% end %>
43 47
</div>
44 48

  
45 49
<p class="buttons">
config/locales/en.yml
977 977
  label_completed_versions: Completed versions
978 978
  label_search_for_watchers: Search for watchers to add
979 979
  label_session_expiration: Session expiration
980
  label_show_closed_projects: View closed projects
981 980
  label_status_transitions: Status transitions
982 981
  label_fields_permissions: Fields permissions
983 982
  label_readonly: Read-only
public/stylesheets/application.css
649 649
  -moz-column-count: auto;
650 650
  -moz-column-width: 400px;
651 651
  -moz-column-gap : 0.5rem;
652
  margin-bottom: 1.2em;
652 653
}
653 654
#projects-index li.root ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
654 655
#projects-index ul.projects li.root {
test/functional/queries_controller_test.rb
69 69
    assert_response 404
70 70
  end
71 71

  
72
  def test_new_should_not_render_show_inline_columns_option_for_query_without_available_inline_columns
73
    @request.session[:user_id] = 1
74
    get :new, :params => {
75
        :type => 'ProjectQuery'
76
      }
77

  
78
    assert_response :success
79
    assert_select 'p[class=?]', 'block_columns', 0
80
  end
81

  
82
  def test_new_should_not_render_show_totals_option_for_query_without_totable_columns
83
    @request.session[:user_id] = 1
84
    get :new, :params => {
85
        :type => 'ProjectQuery'
86
      }
87

  
88
    assert_response :success
89
    assert_select 'p[class=?]', 'totables_columns', 0
90
  end
91

  
72 92
  def test_new_time_entry_query
73 93
    @request.session[:user_id] = 2
74 94
    get :new, :params => {
......
77 97
      }
78 98
    assert_response :success
79 99
    assert_select 'input[name=type][value=?]', 'TimeEntryQuery'
100
    assert_select 'p[class=?]', 'totable_columns', 1
101
    assert_select 'p[class=?]', 'block_columns', 0
102
  end
103

  
104
  def test_new_project_query_for_projects
105
    @request.session[:user_id] = 1
106
    get :new, :params => {
107
        :type => 'ProjectQuery'
108
      }
109
    assert_response :success
110
    assert_select 'input[name=type][value=?]', 'ProjectQuery'
111
  end
112

  
113
  def test_new_project_query_should_not_render_roles_visibility_options
114
    @request.session[:user_id] = 1
115
    get :new, :params => {
116
        :type => 'ProjectQuery'
117
      }
118

  
119
    assert_response :success
120
    assert_select 'input[id=?]', 'query_visibility_0', 1
121
    assert_select 'input[id=?]', 'query_visibility_2', 1
122
    assert_select 'input[id=?]', 'query_visibility_1', 0
123
  end
124

  
125
  def test_new_project_query_should_not_render_for_all_projects_option
126
    @request.session[:user_id] = 1
127
    get :new, :params => {
128
        :type => 'ProjectQuery'
129
      }
130

  
131
    assert_response :success
132
    assert_select 'input[name=?]', 'for_all_projects', 0
80 133
  end
81 134

  
82 135
  def test_new_time_entry_query_should_select_spent_time_from_main_menu
......
441 494
    assert q.valid?
442 495
  end
443 496

  
497
  def test_create_public_project_query
498
    @request.session[:user_id] = 2
499

  
500
    q = new_record(ProjectQuery) do
501
      post :create, :params => {
502
          :type => 'ProjectQuery',
503
          :default_columns => '1',
504
          :f => ["status"],
505
          :op => {
506
            "status" => "="
507
          },
508
          :v => {
509
            "status" => ['1']
510
          },
511
          :query => {
512
            "name" => "test_new_project_public_query", "visibility" => "2"
513
          }
514
        }
515
    end
516

  
517
    assert_redirected_to :controller => 'projects', :action => 'index', :query_id => q.id
518

  
519
    assert q.is_public?
520
    assert q.valid?
521
  end
522

  
444 523
  def test_edit_global_public_query
445 524
    @request.session[:user_id] = 1
446 525
    get :edit, :params => {
test/unit/project_query_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-2017  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
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require File.expand_path('../../test_helper', __FILE__)
21

  
22
class ProjectQueryTest < ActiveSupport::TestCase
23
  fixtures :projects, :users,
24
           :members, :roles, :member_roles,
25
           :issue_categories, :enumerations,
26
           :groups_users,
27
           :enabled_modules
28

  
29
  def test_filter_values_be_arrays
30
    q = ProjectQuery.new
31
    assert_nil q.project
32

  
33
    q.available_filters.each do |name, filter|
34
      values = filter.values
35
      assert (values.nil? || values.is_a?(Array)),
36
        "#values for #{name} filter returned a #{values.class.name}"
37
    end
38
  end
39

  
40
  def test_project_statuses_filter_should_return_project_statuses
41
    query = ProjectQuery.new(:name => '_')
42
    query.filters = {'status' => {:operator => '=', :values => []}}
43

  
44
    values = query.available_filters['status'][:values]
45
    assert_equal ['active', 'closed'], values.map(&:first)
46
    assert_equal ['1', '5'], values.map(&:second)
47

  
48
  end
49
end
(10-10/16)