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
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
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,
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
: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
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
# 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
@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
# 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
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
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
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| %>
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
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 {
});
}
-function buildFilterRow(field, operator, values) {
+function buildFilterRow(field, operator, values, loadedValues) {
var fieldId = field.replace('.', '_');
var filterTable = $("#filters-table");
var filterOptions = availableFilters[field];
);
$('#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); }
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
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
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'
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
: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?