query.rb
901 lines
| 29.5 KiB
| text/x-ruby
|
RubyLexer
|
r1796 | # Redmine - project management software | ||
|
r13490 | # Copyright (C) 2006-2015 Jean-Philippe Lang | ||
|
r92 | # | ||
# 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. | ||||
|
r5702 | # | ||
|
r92 | # 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. | ||||
|
r5702 | # | ||
|
r92 | # 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. | ||||
|
r5702 | class QueryColumn | ||
|
r2983 | attr_accessor :name, :sortable, :groupable, :default_order | ||
|
r2430 | include Redmine::I18n | ||
|
r5702 | |||
|
r771 | def initialize(name, options={}) | ||
self.name = name | ||||
self.sortable = options[:sortable] | ||||
|
r2604 | self.groupable = options[:groupable] || false | ||
|
r2957 | if groupable == true | ||
self.groupable = name.to_s | ||||
end | ||||
|
r1107 | self.default_order = options[:default_order] | ||
|
r10721 | @inline = options.key?(:inline) ? options[:inline] : true | ||
|
r11217 | @caption_key = options[:caption] || "field_#{name}".to_sym | ||
@frozen = options[:frozen] | ||||
|
r771 | end | ||
|
r5702 | |||
|
r876 | def caption | ||
|
r13176 | case @caption_key | ||
when Symbol | ||||
l(@caption_key) | ||||
when Proc | ||||
@caption_key.call | ||||
else | ||||
@caption_key | ||||
end | ||||
|
r876 | end | ||
|
r5702 | |||
|
r2504 | # Returns true if the column is sortable, otherwise false | ||
def sortable? | ||||
|
r7818 | !@sortable.nil? | ||
end | ||||
|
r10262 | |||
|
r7818 | def sortable | ||
@sortable.is_a?(Proc) ? @sortable.call : @sortable | ||||
|
r2504 | end | ||
|
r5702 | |||
|
r10721 | def inline? | ||
@inline | ||||
end | ||||
|
r11217 | def frozen? | ||
@frozen | ||||
end | ||||
|
r10738 | def value(object) | ||
object.send name | ||||
|
r2998 | end | ||
|
r5702 | |||
|
r12779 | def value_object(object) | ||
object.send name | ||||
end | ||||
|
r5212 | def css_classes | ||
name | ||||
end | ||||
|
r876 | end | ||
class QueryCustomFieldColumn < QueryColumn | ||||
def initialize(custom_field) | ||||
self.name = "cf_#{custom_field.id}".to_sym | ||||
|
r2255 | self.sortable = custom_field.order_statement || false | ||
|
r9888 | self.groupable = custom_field.group_statement || false | ||
|
r10721 | @inline = true | ||
|
r876 | @cf = custom_field | ||
end | ||||
|
r5702 | |||
|
r876 | def caption | ||
@cf.name | ||||
end | ||||
|
r5702 | |||
|
r876 | def custom_field | ||
@cf | ||||
end | ||||
|
r5702 | |||
|
r12779 | def value_object(object) | ||
|
r11782 | if custom_field.visible_by?(object.project, User.current) | ||
|
r12779 | cv = object.custom_values.select {|v| v.custom_field_id == @cf.id} | ||
cv.size > 1 ? cv.sort {|a,b| a.value.to_s <=> b.value.to_s} : cv.first | ||||
else | ||||
nil | ||||
end | ||||
end | ||||
def value(object) | ||||
raw = value_object(object) | ||||
if raw.is_a?(Array) | ||||
raw.map {|r| @cf.cast_value(r.value)} | ||||
elsif raw | ||||
@cf.cast_value(raw.value) | ||||
|
r11782 | else | ||
nil | ||||
end | ||||
|
r2998 | end | ||
|
r5702 | |||
|
r5212 | def css_classes | ||
@css_classes ||= "#{name} #{@cf.field_format}" | ||||
end | ||||
|
r771 | end | ||
|
r10944 | class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn | ||
def initialize(association, custom_field) | ||||
super(custom_field) | ||||
self.name = "#{association}.cf_#{custom_field.id}".to_sym | ||||
# TODO: support sorting/grouping by association custom field | ||||
self.sortable = false | ||||
self.groupable = false | ||||
@association = association | ||||
end | ||||
|
r12779 | def value_object(object) | ||
|
r10944 | if assoc = object.send(@association) | ||
super(assoc) | ||||
end | ||||
end | ||||
def css_classes | ||||
@css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}" | ||||
end | ||||
end | ||||
|
r92 | class Query < ActiveRecord::Base | ||
|
r2990 | class StatementInvalid < ::ActiveRecord::StatementInvalid | ||
end | ||||
|
r5702 | |||
|
r11764 | VISIBILITY_PRIVATE = 0 | ||
VISIBILITY_ROLES = 1 | ||||
VISIBILITY_PUBLIC = 2 | ||||
|
r92 | belongs_to :project | ||
belongs_to :user | ||||
|
r11764 | has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id" | ||
|
r92 | serialize :filters | ||
|
r771 | serialize :column_names | ||
|
r2504 | serialize :sort_criteria, Array | ||
|
r11790 | serialize :options, Hash | ||
|
r5702 | |||
|
r1018 | attr_protected :project_id, :user_id | ||
|
r5702 | |||
|
r8858 | validates_presence_of :name | ||
|
r590 | validates_length_of :name, :maximum => 255 | ||
|
r11764 | validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] } | ||
|
r7305 | validate :validate_query_filters | ||
|
r11764 | validate do |query| | ||
errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank? | ||||
end | ||||
after_save do |query| | ||||
if query.visibility_changed? && query.visibility != VISIBILITY_ROLES | ||||
|
r12813 | query.roles.clear | ||
end | ||||
|
r11764 | end | ||
|
r5702 | |||
|
r10736 | class_attribute :operators | ||
self.operators = { | ||||
"=" => :label_equals, | ||||
"!" => :label_not_equals, | ||||
"o" => :label_open_issues, | ||||
"c" => :label_closed_issues, | ||||
"!*" => :label_none, | ||||
"*" => :label_any, | ||||
">=" => :label_greater_or_equal, | ||||
"<=" => :label_less_or_equal, | ||||
"><" => :label_between, | ||||
"<t+" => :label_in_less_than, | ||||
">t+" => :label_in_more_than, | ||||
"><t+"=> :label_in_the_next_days, | ||||
"t+" => :label_in, | ||||
"t" => :label_today, | ||||
|
r10740 | "ld" => :label_yesterday, | ||
|
r10736 | "w" => :label_this_week, | ||
|
r10740 | "lw" => :label_last_week, | ||
|
r10742 | "l2w" => [:label_last_n_weeks, {:count => 2}], | ||
|
r10740 | "m" => :label_this_month, | ||
"lm" => :label_last_month, | ||||
"y" => :label_this_year, | ||||
|
r10736 | ">t-" => :label_less_than_ago, | ||
"<t-" => :label_more_than_ago, | ||||
"><t-"=> :label_in_the_past_days, | ||||
"t-" => :label_ago, | ||||
"~" => :label_contains, | ||||
"!~" => :label_not_contains, | ||||
"=p" => :label_any_issues_in_project, | ||||
"=!p" => :label_any_issues_not_in_project, | ||||
"!p" => :label_no_issues_in_project | ||||
} | ||||
class_attribute :operators_by_filter_type | ||||
self.operators_by_filter_type = { | ||||
:list => [ "=", "!" ], | ||||
:list_status => [ "o", "=", "!", "c", "*" ], | ||||
:list_optional => [ "=", "!", "!*", "*" ], | ||||
:list_subprojects => [ "*", "!*", "=" ], | ||||
|
r10740 | :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ], | ||
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ], | ||||
|
r10736 | :string => [ "=", "~", "!", "!~", "!*", "*" ], | ||
:text => [ "~", "!~", "!*", "*" ], | ||||
:integer => [ "=", ">=", "<=", "><", "!*", "*" ], | ||||
:float => [ "=", ">=", "<=", "><", "!*", "*" ], | ||||
:relation => ["=", "=p", "=!p", "!p", "!*", "*"] | ||||
} | ||||
class_attribute :available_columns | ||||
|
r10737 | self.available_columns = [] | ||
|
r6345 | |||
|
r10738 | class_attribute :queried_class | ||
def queried_table_name | ||||
@queried_table_name ||= self.class.queried_class.table_name | ||||
end | ||||
|
r8167 | def initialize(attributes=nil, *args) | ||
|
r92 | super attributes | ||
|
r1296 | @is_for_all = project.nil? | ||
end | ||||
|
r5702 | |||
|
r10739 | # Builds the query from the given params | ||
def build_from_params(params) | ||||
if params[:fields] || params[:f] | ||||
self.filters = {} | ||||
add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) | ||||
else | ||||
available_filters.keys.each do |field| | ||||
add_short_filter(field, params[field]) if params[field] | ||||
end | ||||
end | ||||
self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) | ||||
self.column_names = params[:c] || (params[:query] && params[:query][:column_names]) | ||||
self | ||||
end | ||||
|
r10740 | # Builds a new query from the given params and attributes | ||
def self.build_from_params(params, attributes={}) | ||||
new(attributes).build_from_params(params) | ||||
end | ||||
|
r7305 | def validate_query_filters | ||
|
r92 | filters.each_key do |field| | ||
|
r6136 | if values_for(field) | ||
case type_for(field) | ||||
|
r6345 | when :integer | ||
|
r9725 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } | ||
|
r6345 | when :float | ||
|
r9725 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } | ||
|
r6144 | when :date, :date_past | ||
case operator_for(field) | ||||
when "=", ">=", "<=", "><" | ||||
|
r12202 | add_filter_error(field, :invalid) if values_for(field).detect {|v| | ||
|
r12204 | v.present? && (!v.match(/\A\d{4}-\d{2}-\d{2}(T\d{2}((:)?\d{2}){0,2}(Z|\d{2}:?\d{2})?)?\z/) || parse_date(v).nil?) | ||
|
r12202 | } | ||
|
r10546 | when ">t-", "<t-", "t-", ">t+", "<t+", "t+", "><t+", "><t-" | ||
|
r8268 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } | ||
|
r6144 | end | ||
|
r6136 | end | ||
|
r6109 | end | ||
|
r6345 | |||
|
r8268 | add_filter_error(field, :blank) unless | ||
|
r92 | # filter requires one or more values | ||
|
r5702 | (values_for(field) and !values_for(field).first.blank?) or | ||
|
r92 | # filter doesn't require any value | ||
|
r10740 | ["o", "c", "!*", "*", "t", "ld", "w", "lw", "l2w", "m", "lm", "y"].include? operator_for(field) | ||
|
r92 | end if filters | ||
end | ||||
|
r6345 | |||
|
r8268 | def add_filter_error(field, message) | ||
m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages') | ||||
errors.add(:base, m) | ||||
end | ||||
|
r563 | def editable_by?(user) | ||
return false unless user | ||||
|
r1296 | # Admin can edit them all and regular users can edit their private queries | ||
|
r11764 | return true if user.admin? || (is_private? && self.user_id == user.id) | ||
|
r1296 | # Members can not edit public queries that are for all project (only admin is allowed to) | ||
|
r11764 | is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project) | ||
|
r563 | end | ||
|
r5702 | |||
|
r9729 | def trackers | ||
|
r13100 | @trackers ||= project.nil? ? Tracker.sorted.to_a : project.rolled_up_trackers | ||
|
r9729 | end | ||
|
r9979 | # Returns a hash of localized labels for all filter operators | ||
def self.operators_labels | ||||
|
r10740 | operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h} | ||
|
r9979 | end | ||
|
r10261 | # Returns a representation of the available filters for JSON serialization | ||
|
r9979 | def available_filters_as_json | ||
json = {} | ||||
available_filters.each do |field, options| | ||||
json[field] = options.slice(:type, :name, :values).stringify_keys | ||||
end | ||||
json | ||||
end | ||||
|
r10303 | def all_projects | ||
|
r13100 | @all_projects ||= Project.visible.to_a | ||
|
r10303 | end | ||
def all_projects_values | ||||
return @all_projects_values if @all_projects_values | ||||
values = [] | ||||
Project.project_tree(all_projects) do |p, level| | ||||
prefix = (level > 0 ? ('--' * level + ' ') : '') | ||||
values << ["#{prefix}#{p.name}", p.id.to_s] | ||||
end | ||||
@all_projects_values = values | ||||
end | ||||
|
r11142 | # Adds available filters | ||
def initialize_available_filters | ||||
# implemented by sub-classes | ||||
end | ||||
protected :initialize_available_filters | ||||
# Adds an available filter | ||||
def add_available_filter(field, options) | ||||
@available_filters ||= ActiveSupport::OrderedHash.new | ||||
@available_filters[field] = options | ||||
@available_filters | ||||
end | ||||
# Removes an available filter | ||||
def delete_available_filter(field) | ||||
if @available_filters | ||||
@available_filters.delete(field) | ||||
end | ||||
end | ||||
# Return a hash of available filters | ||||
def available_filters | ||||
unless @available_filters | ||||
initialize_available_filters | ||||
@available_filters.each do |field, options| | ||||
options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) | ||||
end | ||||
end | ||||
@available_filters | ||||
end | ||||
|
r10740 | def add_filter(field, operator, values=nil) | ||
|
r92 | # values must be an array | ||
|
r6106 | return unless values.nil? || values.is_a?(Array) | ||
|
r92 | # check if field is defined as an available filter | ||
if available_filters.has_key? field | ||||
filter_options = available_filters[field] | ||||
|
r6109 | filters[field] = {:operator => operator, :values => (values || [''])} | ||
|
r92 | end | ||
end | ||||
|
r5702 | |||
|
r92 | def add_short_filter(field, expression) | ||
|
r7504 | return unless expression && available_filters.has_key?(field) | ||
field_type = available_filters[field][:type] | ||||
|
r10736 | operators_by_filter_type[field_type].sort.reverse.detect do |operator| | ||
|
r7504 | next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/ | ||
|
r11050 | values = $1 | ||
add_filter field, operator, values.present? ? values.split('|') : [''] | ||||
|
r7504 | end || add_filter(field, '=', expression.split('|')) | ||
|
r92 | end | ||
|
r3570 | |||
# Add multiple filters using +add_filter+ | ||||
def add_filters(fields, operators, values) | ||||
|
r6106 | if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash)) | ||
|
r4273 | fields.each do |field| | ||
|
r6106 | add_filter(field, operators[field], values && values[field]) | ||
|
r4273 | end | ||
|
r3570 | end | ||
end | ||||
|
r5702 | |||
|
r92 | def has_filter?(field) | ||
filters and filters[field] | ||||
end | ||||
|
r6345 | |||
|
r6106 | def type_for(field) | ||
available_filters[field][:type] if available_filters.has_key?(field) | ||||
end | ||||
|
r5702 | |||
|
r92 | def operator_for(field) | ||
has_filter?(field) ? filters[field][:operator] : nil | ||||
end | ||||
|
r5702 | |||
|
r92 | def values_for(field) | ||
has_filter?(field) ? filters[field][:values] : nil | ||||
end | ||||
|
r6345 | |||
|
r6097 | def value_for(field, index=0) | ||
(values_for(field) || [])[index] | ||||
end | ||||
|
r5702 | |||
|
r444 | def label_for(field) | ||
|
r1440 | label = available_filters[field][:name] if available_filters.has_key?(field) | ||
|
r8268 | label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) | ||
|
r444 | end | ||
|
r771 | |||
|
r3571 | def self.add_available_column(column) | ||
self.available_columns << (column) if column.is_a?(QueryColumn) | ||||
end | ||||
|
r5702 | |||
|
r2604 | # Returns an array of columns that can be used to group the results | ||
def groupable_columns | ||||
available_columns.select {|c| c.groupable} | ||||
end | ||||
|
r3490 | |||
# Returns a Hash of columns and the key for sorting | ||||
def sortable_columns | ||||
|
r10738 | available_columns.inject({}) {|h, column| | ||
h[column.name.to_s] = column.sortable | ||||
h | ||||
} | ||||
|
r3490 | end | ||
|
r5702 | |||
|
r771 | def columns | ||
|
r7418 | # preserve the column_names order | ||
|
r11217 | cols = (has_default_columns? ? default_columns_names : column_names).collect do |name| | ||
|
r7418 | available_columns.find { |col| col.name == name } | ||
end.compact | ||||
|
r11217 | available_columns.select(&:frozen?) | cols | ||
|
r7418 | end | ||
|
r10721 | def inline_columns | ||
columns.select(&:inline?) | ||||
end | ||||
def block_columns | ||||
columns.reject(&:inline?) | ||||
end | ||||
def available_inline_columns | ||||
available_columns.select(&:inline?) | ||||
end | ||||
def available_block_columns | ||||
available_columns.reject(&:inline?) | ||||
end | ||||
|
r7418 | def default_columns_names | ||
|
r10738 | [] | ||
|
r771 | end | ||
|
r5702 | |||
|
r771 | def column_names=(names) | ||
|
r2991 | if names | ||
names = names.select {|n| n.is_a?(Symbol) || !n.blank? } | ||||
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } | ||||
# Set column_names to nil if default columns | ||||
|
r7418 | if names == default_columns_names | ||
|
r2991 | names = nil | ||
end | ||||
end | ||||
|
r771 | write_attribute(:column_names, names) | ||
end | ||||
|
r5702 | |||
|
r771 | def has_column?(column) | ||
|
r7953 | column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) | ||
|
r771 | end | ||
|
r5702 | |||
|
r11993 | def has_custom_field_column? | ||
columns.any? {|column| column.is_a? QueryCustomFieldColumn} | ||||
end | ||||
|
r772 | def has_default_columns? | ||
column_names.nil? || column_names.empty? | ||||
end | ||||
|
r5702 | |||
|
r2504 | def sort_criteria=(arg) | ||
c = [] | ||||
if arg.is_a?(Hash) | ||||
arg = arg.keys.sort.collect {|k| arg[k]} | ||||
end | ||||
|
r10543 | c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']} | ||
|
r2504 | write_attribute(:sort_criteria, c) | ||
end | ||||
|
r5702 | |||
|
r2504 | def sort_criteria | ||
read_attribute(:sort_criteria) || [] | ||||
end | ||||
|
r5702 | |||
|
r2504 | def sort_criteria_key(arg) | ||
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first | ||||
end | ||||
|
r5702 | |||
|
r2504 | def sort_criteria_order(arg) | ||
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last | ||||
end | ||||
|
r5702 | |||
|
r10543 | def sort_criteria_order_for(key) | ||
sort_criteria.detect {|k, order| key.to_s == k}.try(:last) | ||||
end | ||||
|
r2604 | # Returns the SQL sort order that should be prepended for grouping | ||
def group_by_sort_order | ||||
if grouped? && (column = group_by_column) | ||||
|
r13612 | order = (sort_criteria_order_for(column.name) || column.default_order).try(:upcase) | ||
|
r2604 | column.sortable.is_a?(Array) ? | ||
|
r10543 | column.sortable.collect {|s| "#{s} #{order}"}.join(',') : | ||
"#{column.sortable} #{order}" | ||||
|
r2604 | end | ||
end | ||||
|
r5702 | |||
|
r2604 | # Returns true if the query is a grouped query | ||
def grouped? | ||||
|
r4439 | !group_by_column.nil? | ||
|
r2604 | end | ||
|
r5702 | |||
|
r2604 | def group_by_column | ||
|
r4439 | groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} | ||
|
r2604 | end | ||
|
r5702 | |||
|
r2957 | def group_by_statement | ||
|
r4439 | group_by_column.try(:groupable) | ||
|
r2957 | end | ||
|
r5702 | |||
|
r1796 | def project_statement | ||
|
r1417 | project_clauses = [] | ||
|
r7777 | if project && !project.descendants.active.empty? | ||
|
r1164 | ids = [project.id] | ||
|
r1184 | if has_filter?("subproject_id") | ||
case operator_for("subproject_id") | ||||
when '=' | ||||
# include the selected subprojects | ||||
ids += values_for("subproject_id").each(&:to_i) | ||||
when '!*' | ||||
# main project only | ||||
else | ||||
# all subprojects | ||||
|
r2302 | ids += project.descendants.collect(&:id) | ||
|
r1184 | end | ||
elsif Setting.display_subprojects_issues? | ||||
|
r2302 | ids += project.descendants.collect(&:id) | ||
|
r347 | end | ||
|
r1796 | project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') | ||
|
r673 | elsif project | ||
|
r1796 | project_clauses << "#{Project.table_name}.id = %d" % project.id | ||
|
r347 | end | ||
|
r5322 | project_clauses.any? ? project_clauses.join(' AND ') : nil | ||
|
r1796 | end | ||
def statement | ||||
|
r662 | # filters clauses | ||
filters_clauses = [] | ||||
|
r92 | filters.each_key do |field| | ||
|
r347 | next if field == "subproject_id" | ||
|
r517 | v = values_for(field).clone | ||
|
r444 | next unless v and !v.empty? | ||
|
r2395 | operator = operator_for(field) | ||
|
r5702 | |||
|
r12801 | # "me" value substitution | ||
|
r10743 | if %w(assigned_to_id author_id user_id watcher_id).include?(field) | ||
|
r6212 | if v.delete("me") | ||
if User.current.logged? | ||||
v.push(User.current.id.to_s) | ||||
v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' | ||||
else | ||||
v.push("0") | ||||
end | ||||
end | ||||
|
r2395 | end | ||
|
r5702 | |||
|
r8517 | if field == 'project_id' | ||
if v.delete('mine') | ||||
v += User.current.memberships.map(&:project_id).map(&:to_s) | ||||
end | ||||
end | ||||
|
r9981 | if field =~ /cf_(\d+)$/ | ||
|
r444 | # custom field | ||
|
r6181 | filters_clauses << sql_for_custom_field(field, operator, v, $1) | ||
elsif respond_to?("sql_for_#{field}_field") | ||||
# specific statement | ||||
filters_clauses << send("sql_for_#{field}_field", field, operator, v) | ||||
|
r444 | else | ||
# regular field | ||||
|
r10738 | filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')' | ||
|
r444 | end | ||
|
r92 | end if filters and valid? | ||
|
r5702 | |||
|
r11782 | if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn) | ||
# Excludes results for which the grouped custom field is not visible | ||||
filters_clauses << c.custom_field.visibility_by_project_condition | ||||
end | ||||
|
r5322 | filters_clauses << project_statement | ||
filters_clauses.reject!(&:blank?) | ||||
|
r5702 | |||
|
r5322 | filters_clauses.any? ? filters_clauses.join(' AND ') : nil | ||
|
r92 | end | ||
|
r5702 | |||
|
r6181 | private | ||
|
r6345 | |||
|
r6181 | def sql_for_custom_field(field, operator, value, custom_field_id) | ||
db_table = CustomValue.table_name | ||||
db_field = 'value' | ||||
|
r8518 | filter = @available_filters[field] | ||
|
r9981 | return nil unless filter | ||
|
r12125 | if filter[:field].format.target_class && filter[:field].format.target_class <= User | ||
|
r8518 | if value.delete('me') | ||
value.push User.current.id.to_s | ||||
end | ||||
end | ||||
|
r8601 | not_in = nil | ||
if operator == '!' | ||||
# Makes ! operator work for custom fields with multiple values | ||||
operator = '=' | ||||
not_in = 'NOT' | ||||
end | ||||
|
r9981 | customized_key = "id" | ||
|
r10738 | customized_class = queried_class | ||
|
r9981 | if field =~ /^(.+)\.cf_/ | ||
assoc = $1 | ||||
customized_key = "#{assoc}_id" | ||||
|
r10738 | customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil | ||
raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class | ||||
|
r9981 | end | ||
|
r11620 | where = sql_for_field(field, operator, value, db_table, db_field, true) | ||
if operator =~ /[<>]/ | ||||
where = "(#{where}) AND #{db_table}.#{db_field} <> ''" | ||||
end | ||||
|
r11782 | "#{queried_table_name}.#{customized_key} #{not_in} IN (" + | ||
"SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" + | ||||
" LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='#{customized_class}' AND #{db_table}.customized_id=#{customized_class.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id}" + | ||||
" WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))" | ||||
|
r6181 | end | ||
|
r6345 | |||
|
r2395 | # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ | ||
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) | ||||
|
r2088 | sql = '' | ||
|
r2395 | case operator | ||
|
r2088 | when "=" | ||
|
r6136 | if value.any? | ||
case type_for(field) | ||||
when :date, :date_past | ||||
|
r13611 | sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first), is_custom_filter) | ||
|
r6136 | when :integer | ||
|
r7978 | if is_custom_filter | ||
|
r10873 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) = #{value.first.to_i})" | ||
|
r7978 | else | ||
sql = "#{db_table}.#{db_field} = #{value.first.to_i}" | ||||
end | ||||
|
r6136 | when :float | ||
|
r7978 | if is_custom_filter | ||
|
r10873 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})" | ||
|
r7978 | else | ||
sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" | ||||
end | ||||
|
r6106 | else | ||
|
r13100 | sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" | ||
|
r6106 | end | ||
|
r6136 | else | ||
# IN an empty set | ||||
sql = "1=0" | ||||
|
r4768 | end | ||
|
r2088 | when "!" | ||
|
r4768 | if value.any? | ||
|
r13100 | sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + "))" | ||
|
r4768 | else | ||
# NOT IN an empty set | ||||
sql = "1=1" | ||||
end | ||||
|
r2088 | when "!*" | ||
sql = "#{db_table}.#{db_field} IS NULL" | ||||
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter | ||||
when "*" | ||||
sql = "#{db_table}.#{db_field} IS NOT NULL" | ||||
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter | ||||
when ">=" | ||||
|
r6106 | if [:date, :date_past].include?(type_for(field)) | ||
|
r13611 | sql = date_clause(db_table, db_field, parse_date(value.first), nil, is_custom_filter) | ||
|
r6096 | else | ||
|
r6106 | if is_custom_filter | ||
|
r10873 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) >= #{value.first.to_f})" | ||
|
r6106 | else | ||
|
r6107 | sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" | ||
|
r6106 | end | ||
|
r6096 | end | ||
|
r2088 | when "<=" | ||
|
r6106 | if [:date, :date_past].include?(type_for(field)) | ||
|
r13611 | sql = date_clause(db_table, db_field, nil, parse_date(value.first), is_custom_filter) | ||
|
r6096 | else | ||
|
r6106 | if is_custom_filter | ||
|
r10873 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) <= #{value.first.to_f})" | ||
|
r6106 | else | ||
|
r6107 | sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" | ||
|
r6106 | end | ||
|
r6096 | end | ||
|
r6097 | when "><" | ||
|
r6106 | if [:date, :date_past].include?(type_for(field)) | ||
|
r13611 | sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]), is_custom_filter) | ||
|
r6097 | else | ||
|
r6106 | if is_custom_filter | ||
|
r10873 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(CASE #{db_table}.#{db_field} WHEN '' THEN '0' ELSE #{db_table}.#{db_field} END AS decimal(30,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})" | ||
|
r6106 | else | ||
|
r6107 | sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" | ||
|
r6106 | end | ||
|
r6097 | end | ||
|
r2088 | when "o" | ||
|
r13100 | sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_false})" if field == "status_id" | ||
|
r2088 | when "c" | ||
|
r13100 | sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{self.class.connection.quoted_true})" if field == "status_id" | ||
|
r10546 | when "><t-" | ||
# between today - n days and today | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0, is_custom_filter) | ||
|
r10546 | when ">t-" | ||
# >= today - n days | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil, is_custom_filter) | ||
|
r2088 | when "<t-" | ||
|
r10546 | # <= today - n days | ||
|
r13611 | sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i, is_custom_filter) | ||
|
r2088 | when "t-" | ||
|
r10546 | # = n days in past | ||
|
r13611 | sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i, is_custom_filter) | ||
|
r10546 | when "><t+" | ||
# between today and today + n days | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, 0, value.first.to_i, is_custom_filter) | ||
|
r2088 | when ">t+" | ||
|
r10546 | # >= today + n days | ||
|
r13611 | sql = relative_date_clause(db_table, db_field, value.first.to_i, nil, is_custom_filter) | ||
|
r2088 | when "<t+" | ||
|
r10546 | # <= today + n days | ||
|
r13611 | sql = relative_date_clause(db_table, db_field, nil, value.first.to_i, is_custom_filter) | ||
|
r2088 | when "t+" | ||
|
r10546 | # = today + n days | ||
|
r13611 | sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i, is_custom_filter) | ||
|
r2088 | when "t" | ||
|
r10546 | # = today | ||
|
r13611 | sql = relative_date_clause(db_table, db_field, 0, 0, is_custom_filter) | ||
|
r10740 | when "ld" | ||
# = yesterday | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, -1, -1, is_custom_filter) | ||
|
r2088 | when "w" | ||
|
r10546 | # = this week | ||
|
r5476 | first_day_of_week = l(:general_first_day_of_week).to_i | ||
day_of_week = Date.today.cwday | ||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6, is_custom_filter) | ||
|
r10740 | when "lw" | ||
# = last week | ||||
first_day_of_week = l(:general_first_day_of_week).to_i | ||||
day_of_week = Date.today.cwday | ||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1, is_custom_filter) | ||
|
r10740 | when "l2w" | ||
# = last 2 weeks | ||||
first_day_of_week = l(:general_first_day_of_week).to_i | ||||
day_of_week = Date.today.cwday | ||||
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) | ||||
|
r13611 | sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1, is_custom_filter) | ||
|
r10740 | when "m" | ||
# = this month | ||||
date = Date.today | ||||
|
r13611 | sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter) | ||
|
r10740 | when "lm" | ||
# = last month | ||||
date = Date.today.prev_month | ||||
|
r13611 | sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month, is_custom_filter) | ||
|
r10740 | when "y" | ||
# = this year | ||||
date = Date.today | ||||
|
r13611 | sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year, is_custom_filter) | ||
|
r2088 | when "~" | ||
|
r13100 | sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'" | ||
|
r2088 | when "!~" | ||
|
r13100 | sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{self.class.connection.quote_string(value.first.to_s.downcase)}%'" | ||
|
r6097 | else | ||
raise "Unknown query operator #{operator}" | ||||
|
r2088 | end | ||
|
r5702 | |||
|
r2089 | return sql | ||
|
r2088 | end | ||
|
r5702 | |||
|
r11687 | # Adds a filter for the given custom field | ||
def add_custom_field_filter(field, assoc=nil) | ||||
|
r12125 | options = field.format.query_filter_options(field, self) | ||
if field.format.target_class && field.format.target_class <= User | ||||
if options[:values].is_a?(Array) && User.current.logged? | ||||
options[:values].unshift ["<< #{l(:label_me)} >>", "me"] | ||||
|
r9981 | end | ||
|
r11687 | end | ||
|
r12125 | |||
|
r11687 | filter_id = "cf_#{field.id}" | ||
filter_name = field.name | ||||
if assoc.present? | ||||
filter_id = "#{assoc}.#{filter_id}" | ||||
filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) | ||||
|
r9981 | end | ||
|
r11687 | add_available_filter filter_id, options.merge({ | ||
:name => filter_name, | ||||
:field => field | ||||
}) | ||||
|
r9981 | end | ||
|
r11687 | # Adds filters for the given custom fields scope | ||
def add_custom_fields_filters(scope, assoc=nil) | ||||
|
r11782 | scope.visible.where(:is_filter => true).sorted.each do |field| | ||
|
r11687 | add_custom_field_filter(field, assoc) | ||
end | ||||
end | ||||
# Adds filters for the given associations custom fields | ||||
|
r9981 | def add_associations_custom_fields_filters(*associations) | ||
|
r11782 | fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class) | ||
|
r9981 | associations.each do |assoc| | ||
|
r10738 | association_klass = queried_class.reflect_on_association(assoc).klass | ||
|
r9981 | fields_by_class.each do |field_class, fields| | ||
if field_class.customized_class <= association_klass | ||||
|
r11687 | fields.sort.each do |field| | ||
add_custom_field_filter(field, assoc) | ||||
end | ||||
|
r9981 | end | ||
end | ||||
|
r1562 | end | ||
end | ||||
|
r6345 | |||
|
r13611 | def quoted_time(time, is_custom_filter) | ||
if is_custom_filter | ||||
# Custom field values are stored as strings in the DB | ||||
# using this format that does not depend on DB date representation | ||||
time.strftime("%Y-%m-%d %H:%M:%S") | ||||
else | ||||
self.class.connection.quoted_date(time) | ||||
end | ||||
end | ||||
|
r2052 | # Returns a SQL clause for a date or datetime field. | ||
|
r13611 | def date_clause(table, field, from, to, is_custom_filter) | ||
|
r2052 | s = [] | ||
if from | ||||
|
r12202 | if from.is_a?(Date) | ||
|
r12203 | from = Time.local(from.year, from.month, from.day).yesterday.end_of_day | ||
else | ||||
from = from - 1 # second | ||||
|
r12202 | end | ||
|
r9543 | if self.class.default_timezone == :utc | ||
|
r12202 | from = from.utc | ||
|
r9543 | end | ||
|
r13611 | s << ("#{table}.#{field} > '%s'" % [quoted_time(from, is_custom_filter)]) | ||
|
r2052 | end | ||
if to | ||||
|
r12202 | if to.is_a?(Date) | ||
to = Time.local(to.year, to.month, to.day).end_of_day | ||||
end | ||||
|
r9543 | if self.class.default_timezone == :utc | ||
|
r12202 | to = to.utc | ||
|
r9543 | end | ||
|
r13611 | s << ("#{table}.#{field} <= '%s'" % [quoted_time(to, is_custom_filter)]) | ||
|
r2052 | end | ||
s.join(' AND ') | ||||
end | ||||
|
r6345 | |||
|
r6106 | # Returns a SQL clause for a date or datetime field using relative dates. | ||
|
r13611 | def relative_date_clause(table, field, days_from, days_to, is_custom_filter) | ||
date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil), is_custom_filter) | ||||
|
r6106 | end | ||
|
r9890 | |||
|
r12202 | # Returns a Date or Time from the given filter value | ||
def parse_date(arg) | ||||
if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/ | ||||
Time.parse(arg) rescue nil | ||||
else | ||||
Date.parse(arg) rescue nil | ||||
end | ||||
end | ||||
|
r9890 | # Additional joins required for the given sort options | ||
def joins_for_order_statement(order_options) | ||||
joins = [] | ||||
if order_options | ||||
if order_options.include?('authors') | ||||
|
r10738 | joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id" | ||
|
r9890 | end | ||
order_options.scan(/cf_\d+/).uniq.each do |name| | ||||
column = available_columns.detect {|c| c.name.to_s == name} | ||||
|
r9891 | join = column && column.custom_field.join_for_order_statement | ||
|
r9890 | if join | ||
joins << join | ||||
end | ||||
end | ||||
end | ||||
joins.any? ? joins.join(' ') : nil | ||||
end | ||||
|
r92 | end | ||