diff options
author | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2017-01-09 19:59:54 +0000 |
---|---|---|
committer | Jean-Philippe Lang <jp_lang@yahoo.fr> | 2017-01-09 19:59:54 +0000 |
commit | fd3c08aaa107e2c5a412a06aea6890b147e46aaf (patch) | |
tree | 6e55b3f482eddc77ff7cc7f593aa0754e9494a0b | |
parent | 72f59192525bd6dc3af2cfa00d5b184892335c8d (diff) | |
download | redmine-fd3c08aaa107e2c5a412a06aea6890b147e46aaf.tar.gz redmine-fd3c08aaa107e2c5a412a06aea6890b147e46aaf.zip |
Don't preload all query filters (#24787).
git-svn-id: http://svn.redmine.org/redmine/trunk@16170 e93f8b46-1217-0410-a6f0-8f06a7374b81
-rw-r--r-- | app/controllers/queries_controller.rb | 21 | ||||
-rw-r--r-- | app/models/issue_query.rb | 81 | ||||
-rw-r--r-- | app/models/query.rb | 115 | ||||
-rw-r--r-- | app/models/time_entry_query.rb | 55 | ||||
-rw-r--r-- | app/views/queries/_filters.html.erb | 4 | ||||
-rw-r--r-- | config/routes.rb | 1 | ||||
-rw-r--r-- | public/javascripts/application.js | 18 | ||||
-rw-r--r-- | test/functional/queries_controller_test.rb | 27 | ||||
-rw-r--r-- | test/integration/routing/queries_test.rb | 1 | ||||
-rw-r--r-- | test/unit/query_test.rb | 22 | ||||
-rw-r--r-- | test/unit/time_entry_query_test.rb | 22 |
11 files changed, 249 insertions, 118 deletions
diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb index fda9d9711..f7ef74882 100644 --- a/app/controllers/queries_controller.rb +++ b/app/controllers/queries_controller.rb @@ -17,7 +17,7 @@ class QueriesController < ApplicationController menu_item :issues - before_action :find_query, :except => [:new, :create, :index] + before_action :find_query, :only => [:edit, :update, :destroy] before_action :find_optional_project, :only => [:new, :create] accept_api_auth :index @@ -85,6 +85,25 @@ class QueriesController < ApplicationController redirect_to_items(:set_filter => 1) end + # Returns the values for a query filter + def filter + q = query_class.new + if params[:project_id].present? + q.project = Project.find(params[:project_id]) + end + + unless User.current.allowed_to?(q.class.view_permission, q.project, :global => true) + raise Unauthorized + end + + filter = q.available_filters[params[:name].to_s] + values = filter ? filter.values : [] + + render :json => values + rescue ActiveRecord::RecordNotFound + render_404 + end + private def find_query diff --git a/app/models/issue_query.rb b/app/models/issue_query.rb index 137764e82..3e2e7a64d 100644 --- a/app/models/issue_query.rb +++ b/app/models/issue_query.rb @@ -78,80 +78,37 @@ class IssueQuery < Query end def initialize_available_filters - principals = [] - subprojects = [] - versions = [] - categories = [] - issue_custom_fields = [] - - if project - principals += project.principals.visible - unless project.leaf? - subprojects = project.descendants.visible.to_a - principals += Principal.member_of(subprojects).visible - end - versions = project.shared_versions.to_a - categories = project.issue_categories.to_a - issue_custom_fields = project.all_issue_custom_fields - else - if all_projects.any? - principals += Principal.member_of(all_projects).visible - end - versions = Version.visible.where(:sharing => 'system').to_a - issue_custom_fields = IssueCustomField.where(:is_for_all => true) - end - principals.uniq! - principals.sort! - principals.reject! {|p| p.is_a?(GroupBuiltin)} - users = principals.select {|p| p.is_a?(User)} - add_available_filter "status_id", - :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } + :type => :list_status, :values => lambda { IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] } } - if project.nil? - project_values = [] - if User.current.logged? && User.current.memberships.any? - project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] - end - project_values += all_projects_values - add_available_filter("project_id", - :type => :list, :values => project_values - ) unless project_values.empty? - end + add_available_filter("project_id", + :type => :list, :values => lambda { project_values } + ) if project.nil? add_available_filter "tracker_id", :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] } + add_available_filter "priority_id", :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } - author_values = [] - author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - author_values += users.collect{|s| [s.name, s.id.to_s] } add_available_filter("author_id", - :type => :list, :values => author_values - ) unless author_values.empty? + :type => :list, :values => lambda { author_values } + ) - assigned_to_values = [] - assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - assigned_to_values += (Setting.issue_group_assignment? ? - principals : users).collect{|s| [s.name, s.id.to_s] } add_available_filter("assigned_to_id", - :type => :list_optional, :values => assigned_to_values - ) unless assigned_to_values.empty? + :type => :list_optional, :values => lambda { assigned_to_values } + ) - group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] } add_available_filter("member_of_group", - :type => :list_optional, :values => group_values - ) unless group_values.empty? + :type => :list_optional, :values => lambda { Group.givable.visible.collect {|g| [g.name, g.id.to_s] } } + ) - role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } add_available_filter("assigned_to_role", - :type => :list_optional, :values => role_values - ) unless role_values.empty? + :type => :list_optional, :values => lambda { Role.givable.collect {|r| [r.name, r.id.to_s] } } + ) add_available_filter "fixed_version_id", - :type => :list_optional, - :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] } + :type => :list_optional, :values => lambda { fixed_version_values } add_available_filter "fixed_version.due_date", :type => :date, @@ -164,7 +121,7 @@ class IssueQuery < Query add_available_filter "category_id", :type => :list_optional, - :values => categories.collect{|s| [s.name, s.id.to_s] } + :values => lambda { project.issue_categories.collect{|s| [s.name, s.id.to_s] } } if project add_available_filter "subject", :type => :text add_available_filter "description", :type => :text @@ -188,18 +145,20 @@ class IssueQuery < Query :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]] end - if subprojects.any? + if project && !project.leaf? add_available_filter "subproject_id", :type => :list_subprojects, - :values => subprojects.collect{|s| [s.name, s.id.to_s] } + :values => lambda { subproject_values } end + + issue_custom_fields = project ? project.all_issue_custom_fields : IssueCustomField.where(:is_for_all => true) add_custom_fields_filters(issue_custom_fields) add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version IssueRelation::TYPES.each do |relation_type, options| - add_available_filter relation_type, :type => :relation, :label => options[:name] + add_available_filter relation_type, :type => :relation, :label => options[:name], :values => lambda {all_projects_values} end add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural diff --git a/app/models/query.rb b/app/models/query.rb index b0b39da73..1e2263d77 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -160,6 +160,40 @@ class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn end end +class QueryFilter + include Redmine::I18n + + def initialize(field, options) + @field = field.to_s + @options = options + @options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) + # Consider filters with a Proc for values as remote by default + @remote = options.key?(:remote) ? options[:remote] : options[:values].is_a?(Proc) + end + + def [](arg) + if arg == :values + values + else + @options[arg] + end + end + + def values + @values ||= begin + values = @options[:values] + if values.is_a?(Proc) + values = values.call + end + values + end + end + + def remote + @remote + end +end + class Query < ActiveRecord::Base class StatementInvalid < ::ActiveRecord::StatementInvalid end @@ -404,12 +438,17 @@ class Query < ActiveRecord::Base # Returns a representation of the available filters for JSON serialization def available_filters_as_json json = {} - available_filters.each do |field, options| - options = options.slice(:type, :name, :values) - if options[:values] && values_for(field) - missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last) - if missing.any? && respond_to?(method = "find_#{field}_filter_values") - options[:values] += send(method, missing) + available_filters.each do |field, filter| + options = {:type => filter[:type], :name => filter[:name]} + options[:remote] = true if filter.remote + + if has_filter?(field) || !filter.remote + options[:values] = filter.values + if options[:values] && values_for(field) + missing = Array(values_for(field)).select(&:present?) - options[:values].map(&:last) + if missing.any? && respond_to?(method = "find_#{field}_filter_values") + options[:values] += send(method, missing) + end end end json[field] = options.stringify_keys @@ -432,6 +471,65 @@ class Query < ActiveRecord::Base @all_projects_values = values end + def project_values + project_values = [] + if User.current.logged? && User.current.memberships.any? + project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] + end + project_values += all_projects_values + project_values + end + + def subproject_values + project.descendants.visible.collect{|s| [s.name, s.id.to_s] } + end + + def principals + @principal ||= begin + principals = [] + if project + principals += project.principals.visible + unless project.leaf? + principals += Principal.member_of(project.descendants.visible).visible + end + else + principals += Principal.member_of(all_projects).visible + end + principals.uniq! + principals.sort! + principals.reject! {|p| p.is_a?(GroupBuiltin)} + principals + end + end + + def users + principals.select {|p| p.is_a?(User)} + end + + def author_values + author_values = [] + author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + author_values += users.collect{|s| [s.name, s.id.to_s] } + author_values + end + + def assigned_to_values + assigned_to_values = [] + assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? + assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] } + assigned_to_values + end + + def fixed_version_values + versions = [] + if project + versions = project.shared_versions.to_a + else + versions = Version.visible.where(:sharing => 'system').to_a + end + Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] } + end + # Adds available filters def initialize_available_filters # implemented by sub-classes @@ -441,7 +539,7 @@ class Query < ActiveRecord::Base # Adds an available filter def add_available_filter(field, options) @available_filters ||= ActiveSupport::OrderedHash.new - @available_filters[field] = options + @available_filters[field] = QueryFilter.new(field, options) @available_filters end @@ -457,9 +555,6 @@ class Query < ActiveRecord::Base unless @available_filters initialize_available_filters @available_filters ||= {} - @available_filters.each do |field, options| - options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) - end end @available_filters end diff --git a/app/models/time_entry_query.rb b/app/models/time_entry_query.rb index 620847da0..5b39bd34e 100644 --- a/app/models/time_entry_query.rb +++ b/app/models/time_entry_query.rb @@ -42,65 +42,38 @@ class TimeEntryQuery < Query def initialize_available_filters add_available_filter "spent_on", :type => :date_past - principals = [] - versions = [] - if project - principals += project.principals.visible.sort - unless project.leaf? - subprojects = project.descendants.visible.to_a - if subprojects.any? - add_available_filter "subproject_id", - :type => :list_subprojects, - :values => subprojects.collect{|s| [s.name, s.id.to_s] } - principals += Principal.member_of(subprojects).visible - end - end - versions = project.shared_versions.to_a - else - if all_projects.any? - # members of visible projects - principals += Principal.member_of(all_projects).visible - # project filter - project_values = [] - if User.current.logged? && User.current.memberships.any? - project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] - end - project_values += all_projects_values - add_available_filter("project_id", - :type => :list, :values => project_values - ) unless project_values.empty? - end + add_available_filter("project_id", + :type => :list, :values => lambda { project_values } + ) if project.nil? + + if project && !project.leaf? + add_available_filter "subproject_id", + :type => :list_subprojects, + :values => lambda { subproject_values } end add_available_filter("issue_id", :type => :tree, :label => :label_issue) add_available_filter("issue.tracker_id", :type => :list, :name => l("label_attribute_of_issue", :name => l(:field_tracker)), - :values => Tracker.sorted.map {|t| [t.name, t.id.to_s]}) + :values => lambda { Tracker.sorted.map {|t| [t.name, t.id.to_s]} }) add_available_filter("issue.status_id", :type => :list, :name => l("label_attribute_of_issue", :name => l(:field_status)), - :values => IssueStatus.sorted.map {|s| [s.name, s.id.to_s]}) + :values => lambda { IssueStatus.sorted.map {|s| [s.name, s.id.to_s]} }) add_available_filter("issue.fixed_version_id", :type => :list, :name => l("label_attribute_of_issue", :name => l(:field_fixed_version)), - :values => Version.sort_by_status(versions).collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s, l("version_status_#{s.status}")] }) - - principals.uniq! - principals.sort! - users = principals.select {|p| p.is_a?(User)} + :values => lambda { fixed_version_values }) if project - users_values = [] - users_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? - users_values += users.collect{|s| [s.name, s.id.to_s] } add_available_filter("user_id", - :type => :list_optional, :values => users_values - ) unless users_values.empty? + :type => :list_optional, :values => lambda { author_values } + ) activities = (project ? project.activities : TimeEntryActivity.shared) add_available_filter("activity_id", :type => :list, :values => activities.map {|a| [a.name, a.id.to_s]} - ) unless activities.empty? + ) add_available_filter "comments", :type => :text add_available_filter "hours", :type => :float diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb index 13dcc704d..cb95df1c8 100644 --- a/app/views/queries/_filters.html.erb +++ b/app/views/queries/_filters.html.erb @@ -3,7 +3,9 @@ var operatorLabels = <%= raw_json Query.operators_labels %>; var operatorByType = <%= raw_json Query.operators_by_filter_type %>; var availableFilters = <%= raw_json query.available_filters_as_json %>; var labelDayPlural = <%= raw_json l(:label_day_plural) %>; -var allProjects = <%= raw_json query.all_projects_values %>; + +var filtersUrl = <%= raw_json queries_filter_path(:project_id => @query.project.try(:id), :type => @query.type) %>; + $(document).ready(function(){ initFilters(); <% query.filters.each do |field, options| %> diff --git a/config/routes.rb b/config/routes.rb index 374e2b294..c2a400349 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -199,6 +199,7 @@ Rails.application.routes.draw do match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete resources :queries, :except => [:show] + get '/queries/filter', :to => 'queries#filter', :as => 'queries_filter' resources :news, :only => [:index, :show, :edit, :update, :destroy] match '/news/:id/comments', :to => 'comments#create', :via => :post diff --git a/public/javascripts/application.js b/public/javascripts/application.js index f38b69b3c..098d9d3b2 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -120,6 +120,18 @@ function initFilters() { function addFilter(field, operator, values) { var fieldId = field.replace('.', '_'); var tr = $('#tr_'+fieldId); + + var filterOptions = availableFilters[field]; + if (!filterOptions) return; + + if (filterOptions['remote'] && filterOptions['values'] == null) { + $.getJSON(filtersUrl, {'name': field}).done(function(data) { + filterOptions['values'] = data; + addFilter(field, operator, values) ; + }); + return; + } + if (tr.length > 0) { tr.show(); } else { @@ -134,7 +146,7 @@ function addFilter(field, operator, values) { }); } -function buildFilterRow(field, operator, values) { +function buildFilterRow(field, operator, values, loadedValues) { var fieldId = field.replace('.', '_'); var filterTable = $("#filters-table"); var filterOptions = availableFilters[field]; @@ -212,8 +224,8 @@ function buildFilterRow(field, operator, values) { ); $('#values_'+fieldId).val(values[0]); select = tr.find('td.values select'); - for (i = 0; i < allProjects.length; i++) { - var filterValue = allProjects[i]; + for (i = 0; i < filterValues.length; i++) { + var filterValue = filterValues[i]; var option = $('<option>'); option.val(filterValue[1]).text(filterValue[0]); if (values[0] == filterValue[1]) { option.attr('selected', true); } diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb index 4a8812777..cabdab968 100644 --- a/test/functional/queries_controller_test.rb +++ b/test/functional/queries_controller_test.rb @@ -18,7 +18,12 @@ require File.expand_path('../../test_helper', __FILE__) class QueriesControllerTest < Redmine::ControllerTest - fixtures :projects, :users, :members, :member_roles, :roles, :trackers, :issue_statuses, :issue_categories, :enumerations, :issues, :custom_fields, :custom_values, :queries, :enabled_modules + fixtures :projects, :enabled_modules, + :users, :email_addresses, + :members, :member_roles, :roles, + :trackers, :issue_statuses, :issue_categories, :enumerations, :versions, + :issues, :custom_fields, :custom_values, + :queries def setup User.current = nil @@ -397,4 +402,24 @@ class QueriesControllerTest < Redmine::ControllerTest assert_response :success assert_include 'addFilter("subject", "=", ["foo\/bar"]);', response.body end + + def test_filter_with_project_id_should_return_filter_values + @request.session[:user_id] = 2 + get :filter, :project_id => 1, :name => 'fixed_version_id' + + assert_response :success + assert_equal 'application/json', response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_include ["eCookbook - 2.0", "3", "open"], json + end + + def test_filter_without_project_id_should_return_filter_values + @request.session[:user_id] = 2 + get :filter, :name => 'fixed_version_id' + + assert_response :success + assert_equal 'application/json', response.content_type + json = ActiveSupport::JSON.decode(response.body) + assert_include ["OnlineStore - Systemwide visible version", "7", "open"], json + end end diff --git a/test/integration/routing/queries_test.rb b/test/integration/routing/queries_test.rb index d98af0c8f..dbd0b92a1 100644 --- a/test/integration/routing/queries_test.rb +++ b/test/integration/routing/queries_test.rb @@ -21,6 +21,7 @@ class RoutingQueriesTest < Redmine::RoutingTest def test_queries should_route 'GET /queries/new' => 'queries#new' should_route 'POST /queries' => 'queries#create' + should_route 'GET /queries/filter' => 'queries#filter' should_route 'GET /queries/1/edit' => 'queries#edit', :id => '1' should_route 'PUT /queries/1' => 'queries#update', :id => '1' diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb index 6d27106cc..8c1c3b43c 100644 --- a/test/unit/query_test.rb +++ b/test/unit/query_test.rb @@ -118,6 +118,28 @@ class QueryTest < ActiveSupport::TestCase assert_not_include 'start_date', query.available_filters end + def test_filter_values_without_project_should_be_arrays + q = IssueQuery.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_filter_values_with_project_should_be_arrays + q = IssueQuery.new(:project => Project.find(1)) + assert_not_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 find_issues_with_query(query) Issue.joins(:status, :tracker, :project, :priority).where( query.statement diff --git a/test/unit/time_entry_query_test.rb b/test/unit/time_entry_query_test.rb index 599aae52c..317f037d0 100644 --- a/test/unit/time_entry_query_test.rb +++ b/test/unit/time_entry_query_test.rb @@ -27,6 +27,28 @@ class TimeEntryQueryTest < ActiveSupport::TestCase :groups_users, :enabled_modules + def test_filter_values_without_project_should_be_arrays + q = TimeEntryQuery.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_filter_values_with_project_should_be_arrays + q = TimeEntryQuery.new(:project => Project.find(1)) + assert_not_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_cross_project_activity_filter_should_propose_non_active_activities activity = TimeEntryActivity.create!(:name => 'Disabled', :active => false) assert !activity.active? |