]> source.dussan.org Git - redmine.git/commitdiff
Filters for Projects page (#29482).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 19 Oct 2019 11:36:13 +0000 (11:36 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 19 Oct 2019 11:36:13 +0000 (11:36 +0000)
Patch by Marius BALTEANU.

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

12 files changed:
app/controllers/projects_controller.rb
app/controllers/queries_controller.rb
app/models/project_query.rb [new file with mode: 0644]
app/models/query.rb
app/views/projects/_sidebar.html.erb [new file with mode: 0644]
app/views/projects/index.html.erb
app/views/queries/_form.html.erb
app/views/queries/_query_form.html.erb
config/locales/en.yml
public/stylesheets/application.css
test/functional/queries_controller_test.rb
test/unit/project_query_test.rb [new file with mode: 0644]

index 98571b8f959896043426b0b2f49bdcc2a63c364a..e7a79e4954dff0ebe073628ad107e358dbd08a2a 100644 (file)
@@ -33,6 +33,7 @@ class ProjectsController < ApplicationController
   helper :custom_fields
   helper :issues
   helper :queries
+  include QueriesHelper
   helper :repositories
   helper :members
   helper :trackers
@@ -44,13 +45,11 @@ class ProjectsController < ApplicationController
       return
     end
 
-    scope = Project.visible.sorted
+    retrieve_project_query
+    scope = project_scope
 
     respond_to do |format|
       format.html {
-        unless params[:closed]
-          scope = scope.active
-        end
         @projects = scope.to_a
       }
       format.api  {
@@ -257,4 +256,15 @@ class ProjectsController < ApplicationController
     # hide project in layout
     @project = nil
   end
+
+  private
+
+  # Returns the ProjectEntry scope for index
+  def project_scope(options={})
+    @query.results_scope(options)
+  end
+
+  def retrieve_project_query
+    retrieve_query(ProjectQuery, false)
+  end
 end
index 10051ab02d81330c63b683ac8ebd0132880e606d..3488061983242742515b63580dfde947c44a5d49 100644 (file)
@@ -126,7 +126,7 @@ class QueriesController < ApplicationController
     @query.column_names = nil if params[:default_columns]
     @query.sort_criteria = (params[:query] && params[:query][:sort_criteria]) || @query.sort_criteria
     @query.name = params[:query] && params[:query][:name]
-    if User.current.allowed_to?(:manage_public_queries, @query.project) || User.current.admin?
+    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))
       @query.visibility = (params[:query] && params[:query][:visibility]) || Query::VISIBILITY_PRIVATE
       @query.role_ids = params[:query] && params[:query][:role_ids]
     else
@@ -156,6 +156,10 @@ class QueriesController < ApplicationController
     redirect_to _time_entries_path(@project, nil, options)
   end
 
+  def redirect_to_project_query(options)
+    redirect_to projects_path(options)
+  end
+
   # Returns the Query subclass, IssueQuery by default
   # for compatibility with previous behaviour
   def query_class
diff --git a/app/models/project_query.rb b/app/models/project_query.rb
new file mode 100644 (file)
index 0000000..ca8e9fb
--- /dev/null
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2017  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.
+
+class ProjectQuery < Query
+
+  self.queried_class = Project
+
+  self.available_columns = []
+
+  def initialize(attributes=nil, *args)
+    super attributes
+    self.filters ||= { 'status' => {:operator => "=", :values => ['1']} }
+  end
+
+  def initialize_available_filters
+    add_available_filter "status",
+      :type => :list, :values => lambda { project_statuses_values }
+    add_available_filter "name", :type => :text
+    add_available_filter "description", :type => :text
+    add_available_filter "is_public",
+      :type => :list,
+      :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
+    add_available_filter "created_on", :type => :date_past
+  end
+
+  def available_columns
+    []
+  end
+
+  def base_scope
+    Project.visible.where(statement)
+  end
+
+  def results_scope(options={})
+    order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?)
+
+    order_option << "#{Project.table_name}.id ASC"
+    scope = base_scope.
+      order(order_option).
+      joins(joins_for_order_statement(order_option.join(',')))
+
+    if has_custom_field_column?
+      scope = scope.preload(:custom_values)
+    end
+
+    if has_column?(:parent_id)
+      scope = scope.preload(:parent)
+    end
+
+    scope
+  end
+end
index 3b37a8dd031f15196049e8849cced14a6f7466e5..17271d7739e94ae486da43f08696612a97b7b952 100644 (file)
@@ -717,6 +717,7 @@ class Query < ActiveRecord::Base
   end
 
   def columns
+    return [] if available_columns.empty?
     # preserve the column_names order
     cols = (has_default_columns? ? default_columns_names : column_names).collect do |name|
        available_columns.find { |col| col.name == name }
diff --git a/app/views/projects/_sidebar.html.erb b/app/views/projects/_sidebar.html.erb
new file mode 100644 (file)
index 0000000..f9518e6
--- /dev/null
@@ -0,0 +1,2 @@
+<%= render_sidebar_queries(ProjectQuery, @project) %>
+<%= call_hook(:view_projects_sidebar_queries_bottom) %>
index 13e3332c3bf3e1836775c756b1238883134bb6a4..3c2543b43243511acaffbdfb1e1ddcd1390f2f6a 100644 (file)
@@ -1,22 +1,24 @@
-<% content_for :header_tags do %>
-    <%= auto_discovery_link_tag(:atom, {:action => 'index', :format => 'atom', :key => User.current.rss_key}) %>
-<% end %>
-
 <div class="contextual">
   <%= form_tag({}, :method => :get) do %>
-    <label for="closed">
-      <%= check_box_tag 'closed', 1, params[:closed], :onchange => "this.form.submit();" %>
-      <%= l(:label_show_closed_projects) %>
-    </label>
   <% end %>
   <%= render_project_action_links %>
 </div>
 
-<h2><%= l(:label_project_plural) %></h2>
+<h2><%= @query.new_record? ? l(:label_project_plural) : @query.name %></h2>
 
-<div id="projects-index">
-<%= render_project_hierarchy(@projects) %>
-</div>
+<%= form_tag(projects_path(@project, nil), :method => :get, :id => 'query_form') do %>
+<%= render :partial => 'queries/query_form' %>
+<% end %>
+
+<% if @query.valid? %>
+  <% if @projects.empty? %>
+    <p class="nodata"><%= l(:label_no_data) %></p>
+  <% else %>
+    <div id="projects-index">
+      <%= render_project_hierarchy(@projects) %>
+    </div>
+  <% end %>
+<% end %>
 
 <% if User.current.logged? %>
 <p style="text-align:right;">
 </p>
 <% end %>
 
+<% content_for :sidebar do %>
+  <%= render :partial => 'projects/sidebar' %>
+<% end %>
+
 <% other_formats_links do |f| %>
   <%= f.link_to 'Atom', :url => {:key => User.current.rss_key} %>
 <% end %>
index f1ef5ecaca9e117b45a4d1b4245b1bfe8f04cb71..a2dd9f5891c37f498b6207278bcae240d7718f73 100644 (file)
@@ -7,20 +7,26 @@
 <p><label for="query_name"><%=l(:field_name)%></label>
 <%= text_field 'query', 'name', :size => 80 %></p>
 
-<% if User.current.admin? || User.current.allowed_to?(:manage_public_queries, @query.project) %>
+<% if User.current.admin? ||
+      User.current.allowed_to?(:manage_public_queries, @query.project) ||
+      @query.type == 'ProjectQuery' &&  User.current.allowed_to?(:manage_public_queries, @query.project, :global => true) %>
 <p><label><%=l(:field_visible)%></label>
   <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PRIVATE %> <%= l(:label_visibility_private) %></label>
   <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_PUBLIC %> <%= l(:label_visibility_public) %></label>
-  <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
-  <% Role.givable.sorted.each do |role| %>
-    <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
+  <% unless @query.type == 'ProjectQuery' %>
+    <label class="block"><%= radio_button 'query', 'visibility', Query::VISIBILITY_ROLES %> <%= l(:label_visibility_roles) %>:</label>
+      <% Role.givable.sorted.each do |role| %>
+        <label class="block role-visibility"><%= check_box_tag 'query[role_ids][]', role.id, @query.roles.include?(role), :id => nil %> <%= role.name %></label>
+      <% end %>
+      <%= hidden_field_tag 'query[role_ids][]', '' %>
   <% end %>
-  <%= hidden_field_tag 'query[role_ids][]', '' %>
 </p>
 <% end %>
 
-<p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
-<%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %></p>
+<% unless @query.type == 'ProjectQuery' %>
+  <p><label for="query_is_for_all"><%=l(:field_is_for_all)%></label>
+  <%= check_box_tag 'query_is_for_all', 1, @query.project.nil?, :class => (User.current.admin? ? '' : 'disable-unless-private') %></p>
+<% end %>
 
 <fieldset id="options"><legend><%= l(:label_options) %></legend>
 <p><label for="query_default_columns"><%=l(:label_default_columns)%></label>
       :data => {:disables => "#columns, .block_columns input"} %></p>
 
 <% unless params[:gantt] %>
-<p><label for="query_group_by"><%= l(:field_group_by) %></label>
-<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
+  <p><label for="query_group_by"><%= l(:field_group_by) %></label>
+  <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
 
-<p class="block_columns"><label><%= l(:button_show) %></label>
-<%= available_block_columns_tags(@query) %></p>
+  <% unless @query.available_block_columns.empty? %>
+    <p class="block_columns"><label><%= l(:button_show) %></label>
+    <%= available_block_columns_tags(@query) %></p>
+  <% end %>
 
-<p><label><%= l(:label_total_plural) %></label>
-<%= available_totalable_columns_tags(@query) %></p>
+  <% unless @query.available_totalable_columns.empty? %>
+    <p class="totable_columns"><label><%= l(:label_total_plural) %></label>
+    <%= available_totalable_columns_tags(@query) %></p>
+  <% end %>
 <% else %>
   <p><label><%= l(:button_show) %></label>
   <%= hidden_field_tag 'query[draw_relations]', '0' %>
@@ -54,7 +64,7 @@
 </fieldset>
 
 <% unless params[:gantt] %>
-<fieldset><legend><%= l(:label_sort) %></legend>
+<fieldset id="sort"><legend><%= l(:label_sort) %></legend>
 <% 3.times do |i| %>
 <%= content_tag(:span, "#{i+1}:", :class => 'query_sort_criteria_count')%>
 <%= label_tag "query_sort_criteria_attribute_" + i.to_s,
index c8be6b7717c58e67725e941f071975a798aefb71..65bcc3eb51c3e98503456fe98a0868c7efe271d8 100644 (file)
     </div>
   </fieldset>
 
-  <fieldset id="options" class="collapsible collapsed">
-    <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
-    <div style="display: none;">
-      <table>
-        <tr>
-          <td class="field"><%= l(:field_column_names) %></td>
-          <td><%= render_query_columns_selection(@query) %></td>
-        </tr>
-        <% if @query.groupable_columns.any? %>
-        <tr>
-          <td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td>
-          <td><%= group_by_column_select_tag(@query) %></td>
-        </tr>
-        <% end %>
-        <% if @query.available_block_columns.any? %>
-        <tr>
-          <td class="field"><%= l(:button_show) %></td>
-          <td><%= available_block_columns_tags(@query) %></td>
-        </tr>
-        <% end %>
-        <% if @query.available_totalable_columns.any? %>
-        <tr>
-          <td><%= l(:label_total_plural) %></td>
-          <td><%= available_totalable_columns_tags(@query) %></td>
-        </tr>
-        <% end %>
-      </table>
-    </div>
-  </fieldset>
+  <% if @query.available_columns.any? %>
+    <fieldset id="options" class="collapsible collapsed">
+      <legend onclick="toggleFieldset(this);" class="icon icon-collapsed"><%= l(:label_options) %></legend>
+      <div style="display: none;">
+        <table>
+          <% if @query.available_columns.any? %>
+            <tr>
+              <td class="field"><%= l(:field_column_names) %></td>
+              <td><%= render_query_columns_selection(@query) %></td>
+            </tr>
+          <% end %>
+          <% if @query.groupable_columns.any? %>
+            <tr>
+              <td class="field"><label for='group_by'><%= l(:field_group_by) %></label></td>
+              <td><%= group_by_column_select_tag(@query) %></td>
+            </tr>
+          <% end %>
+          <% if @query.available_block_columns.any? %>
+            <tr>
+              <td class="field"><%= l(:button_show) %></td>
+              <td><%= available_block_columns_tags(@query) %></td>
+            </tr>
+          <% end %>
+          <% if @query.available_totalable_columns.any? %>
+            <tr>
+              <td><%= l(:label_total_plural) %></td>
+              <td><%= available_totalable_columns_tags(@query) %></td>
+            </tr>
+          <% end %>
+        </table>
+      </div>
+    </fieldset>
+  <% end %>
 </div>
 
 <p class="buttons">
index 33bf510da319e52c08cd1b4494c033a736d435ea..4c90deb9dbe9e22e23e92a4873fdf1d12e3fdcb3 100644 (file)
@@ -978,7 +978,6 @@ en:
   label_completed_versions: Completed versions
   label_search_for_watchers: Search for watchers to add
   label_session_expiration: Session expiration
-  label_show_closed_projects: View closed projects
   label_status_transitions: Status transitions
   label_fields_permissions: Fields permissions
   label_readonly: Read-only
index f9ec3f086489770874cc62aa256cae81c433e17d..7aa1bca54c45b6a5e97ef4c1d5010e8a3025cbdb 100644 (file)
@@ -654,6 +654,7 @@ ul.projects div.description li {list-style-type:initial;}
   -moz-column-count: auto;
   -moz-column-width: 400px;
   -moz-column-gap : 0.5rem;
+  margin-bottom: 1.2em;
 }
 #projects-index li.root ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
 #projects-index ul.projects li.root {
index 1439a908e41c9492c355c31591a963a7cc4e3cb4..95afdbe4e808f4e3d64f029e62e32e36052abb13 100644 (file)
@@ -69,6 +69,26 @@ class QueriesControllerTest < Redmine::ControllerTest
     assert_response 404
   end
 
+  def test_new_should_not_render_show_inline_columns_option_for_query_without_available_inline_columns
+    @request.session[:user_id] = 1
+    get :new, :params => {
+        :type => 'ProjectQuery'
+      }
+
+    assert_response :success
+    assert_select 'p[class=?]', 'block_columns', 0
+  end
+
+  def test_new_should_not_render_show_totals_option_for_query_without_totable_columns
+    @request.session[:user_id] = 1
+    get :new, :params => {
+        :type => 'ProjectQuery'
+      }
+
+    assert_response :success
+    assert_select 'p[class=?]', 'totables_columns', 0
+  end
+
   def test_new_time_entry_query
     @request.session[:user_id] = 2
     get :new, :params => {
@@ -77,6 +97,39 @@ class QueriesControllerTest < Redmine::ControllerTest
       }
     assert_response :success
     assert_select 'input[name=type][value=?]', 'TimeEntryQuery'
+    assert_select 'p[class=?]', 'totable_columns', 1
+    assert_select 'p[class=?]', 'block_columns', 0
+  end
+
+  def test_new_project_query_for_projects
+    @request.session[:user_id] = 1
+    get :new, :params => {
+        :type => 'ProjectQuery'
+      }
+    assert_response :success
+    assert_select 'input[name=type][value=?]', 'ProjectQuery'
+  end
+
+  def test_new_project_query_should_not_render_roles_visibility_options
+    @request.session[:user_id] = 1
+    get :new, :params => {
+        :type => 'ProjectQuery'
+      }
+
+    assert_response :success
+    assert_select 'input[id=?]', 'query_visibility_0', 1
+    assert_select 'input[id=?]', 'query_visibility_2', 1
+    assert_select 'input[id=?]', 'query_visibility_1', 0
+  end
+
+  def test_new_project_query_should_not_render_for_all_projects_option
+    @request.session[:user_id] = 1
+    get :new, :params => {
+        :type => 'ProjectQuery'
+      }
+
+    assert_response :success
+    assert_select 'input[name=?]', 'for_all_projects', 0
   end
 
   def test_new_time_entry_query_should_select_spent_time_from_main_menu
@@ -441,6 +494,32 @@ class QueriesControllerTest < Redmine::ControllerTest
     assert q.valid?
   end
 
+  def test_create_public_project_query
+    @request.session[:user_id] = 2
+
+    q = new_record(ProjectQuery) do
+      post :create, :params => {
+          :type => 'ProjectQuery',
+          :default_columns => '1',
+          :f => ["status"],
+          :op => {
+            "status" => "="
+          },
+          :v => {
+            "status" => ['1']
+          },
+          :query => {
+            "name" => "test_new_project_public_query", "visibility" => "2"
+          }
+        }
+    end
+
+    assert_redirected_to :controller => 'projects', :action => 'index', :query_id => q.id
+
+    assert q.is_public?
+    assert q.valid?
+  end
+
   def test_edit_global_public_query
     @request.session[:user_id] = 1
     get :edit, :params => {
diff --git a/test/unit/project_query_test.rb b/test/unit/project_query_test.rb
new file mode 100644 (file)
index 0000000..f4182e5
--- /dev/null
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+# Redmine - project management software
+# Copyright (C) 2006-2017  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.
+
+require File.expand_path('../../test_helper', __FILE__)
+
+class ProjectQueryTest < ActiveSupport::TestCase
+  fixtures :projects, :users,
+           :members, :roles, :member_roles,
+           :issue_categories, :enumerations,
+           :groups_users,
+           :enabled_modules
+
+  def test_filter_values_be_arrays
+    q = ProjectQuery.new
+    assert_nil q.project
+
+    q.available_filters.each do |name, filter|
+      values = filter.values
+      assert (values.nil? || values.is_a?(Array)),
+        "#values for #{name} filter returned a #{values.class.name}"
+    end
+  end
+
+  def test_project_statuses_filter_should_return_project_statuses
+    query = ProjectQuery.new(:name => '_')
+    query.filters = {'status' => {:operator => '=', :values => []}}
+
+    values = query.available_filters['status'][:values]
+    assert_equal ['active', 'closed'], values.map(&:first)
+    assert_equal ['1', '5'], values.map(&:second)
+
+  end
+end