diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb index fda9d97..f7ef748 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 137764e..3e2e7a6 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 b0b39da..1e2263d 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 620847d..5b39bd3 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 13dcc70..cb95df1 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 374e2b2..c2a4003 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 f38b69b..098d9d3 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 = $('