Chris@0: # Redmine - project management software Chris@1494: # Copyright (C) 2006-2014 Jean-Philippe Lang Chris@0: # Chris@0: # This program is free software; you can redistribute it and/or Chris@0: # modify it under the terms of the GNU General Public License Chris@0: # as published by the Free Software Foundation; either version 2 Chris@0: # of the License, or (at your option) any later version. Chris@441: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@441: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Chris@0: Chris@441: class QueryColumn Chris@0: attr_accessor :name, :sortable, :groupable, :default_order Chris@0: include Redmine::I18n Chris@441: Chris@0: def initialize(name, options={}) Chris@0: self.name = name Chris@0: self.sortable = options[:sortable] Chris@0: self.groupable = options[:groupable] || false Chris@0: if groupable == true Chris@0: self.groupable = name.to_s Chris@0: end Chris@0: self.default_order = options[:default_order] Chris@1115: @inline = options.key?(:inline) ? options[:inline] : true Chris@1464: @caption_key = options[:caption] || "field_#{name}".to_sym Chris@1464: @frozen = options[:frozen] Chris@0: end Chris@441: Chris@0: def caption Chris@1464: @caption_key.is_a?(Symbol) ? l(@caption_key) : @caption_key Chris@0: end Chris@441: Chris@0: # Returns true if the column is sortable, otherwise false Chris@0: def sortable? Chris@909: !@sortable.nil? Chris@909: end Chris@1115: Chris@909: def sortable Chris@909: @sortable.is_a?(Proc) ? @sortable.call : @sortable Chris@0: end Chris@441: Chris@1115: def inline? Chris@1115: @inline Chris@1115: end Chris@1115: Chris@1464: def frozen? Chris@1464: @frozen Chris@1464: end Chris@1464: Chris@1464: def value(object) Chris@1464: object.send name Chris@0: end Chris@441: Chris@441: def css_classes Chris@441: name Chris@441: end Chris@0: end Chris@0: Chris@0: class QueryCustomFieldColumn < QueryColumn Chris@0: Chris@0: def initialize(custom_field) Chris@0: self.name = "cf_#{custom_field.id}".to_sym Chris@0: self.sortable = custom_field.order_statement || false Chris@1115: self.groupable = custom_field.group_statement || false Chris@1115: @inline = true Chris@0: @cf = custom_field Chris@0: end Chris@441: Chris@0: def caption Chris@0: @cf.name Chris@0: end Chris@441: Chris@0: def custom_field Chris@0: @cf Chris@0: end Chris@441: Chris@1464: def value(object) Chris@1464: if custom_field.visible_by?(object.project, User.current) Chris@1464: cv = object.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} Chris@1464: cv.size > 1 ? cv.sort {|a,b| a.to_s <=> b.to_s} : cv.first Chris@1464: else Chris@1464: nil Chris@1464: end Chris@0: end Chris@441: Chris@441: def css_classes Chris@441: @css_classes ||= "#{name} #{@cf.field_format}" Chris@441: end Chris@0: end Chris@0: Chris@1464: class QueryAssociationCustomFieldColumn < QueryCustomFieldColumn Chris@1464: Chris@1464: def initialize(association, custom_field) Chris@1464: super(custom_field) Chris@1464: self.name = "#{association}.cf_#{custom_field.id}".to_sym Chris@1464: # TODO: support sorting/grouping by association custom field Chris@1464: self.sortable = false Chris@1464: self.groupable = false Chris@1464: @association = association Chris@1464: end Chris@1464: Chris@1464: def value(object) Chris@1464: if assoc = object.send(@association) Chris@1464: super(assoc) Chris@1464: end Chris@1464: end Chris@1464: Chris@1464: def css_classes Chris@1464: @css_classes ||= "#{@association}_cf_#{@cf.id} #{@cf.field_format}" Chris@1464: end Chris@1464: end Chris@1464: Chris@0: class Query < ActiveRecord::Base Chris@0: class StatementInvalid < ::ActiveRecord::StatementInvalid Chris@0: end Chris@441: Chris@1464: VISIBILITY_PRIVATE = 0 Chris@1464: VISIBILITY_ROLES = 1 Chris@1464: VISIBILITY_PUBLIC = 2 Chris@1464: Chris@0: belongs_to :project Chris@0: belongs_to :user Chris@1464: has_and_belongs_to_many :roles, :join_table => "#{table_name_prefix}queries_roles#{table_name_suffix}", :foreign_key => "query_id" Chris@0: serialize :filters Chris@0: serialize :column_names Chris@0: serialize :sort_criteria, Array Chris@1464: serialize :options, Hash Chris@441: Chris@0: attr_protected :project_id, :user_id Chris@441: Chris@1115: validates_presence_of :name Chris@0: validates_length_of :name, :maximum => 255 Chris@1464: validates :visibility, :inclusion => { :in => [VISIBILITY_PUBLIC, VISIBILITY_ROLES, VISIBILITY_PRIVATE] } Chris@909: validate :validate_query_filters Chris@1464: validate do |query| Chris@1464: errors.add(:base, l(:label_role_plural) + ' ' + l('activerecord.errors.messages.blank')) if query.visibility == VISIBILITY_ROLES && roles.blank? Chris@1464: end Chris@441: Chris@1464: after_save do |query| Chris@1464: if query.visibility_changed? && query.visibility != VISIBILITY_ROLES Chris@1464: query.roles.clear Chris@1464: end Chris@1464: end Chris@0: Chris@1464: class_attribute :operators Chris@1464: self.operators = { Chris@1464: "=" => :label_equals, Chris@1464: "!" => :label_not_equals, Chris@1464: "o" => :label_open_issues, Chris@1464: "c" => :label_closed_issues, Chris@1464: "!*" => :label_none, Chris@1464: "*" => :label_any, Chris@1464: ">=" => :label_greater_or_equal, Chris@1464: "<=" => :label_less_or_equal, Chris@1464: "><" => :label_between, Chris@1464: " :label_in_less_than, Chris@1464: ">t+" => :label_in_more_than, Chris@1464: "> :label_in_the_next_days, Chris@1464: "t+" => :label_in, Chris@1464: "t" => :label_today, Chris@1464: "ld" => :label_yesterday, Chris@1464: "w" => :label_this_week, Chris@1464: "lw" => :label_last_week, Chris@1464: "l2w" => [:label_last_n_weeks, {:count => 2}], Chris@1464: "m" => :label_this_month, Chris@1464: "lm" => :label_last_month, Chris@1464: "y" => :label_this_year, Chris@1464: ">t-" => :label_less_than_ago, Chris@1464: " :label_more_than_ago, Chris@1464: "> :label_in_the_past_days, Chris@1464: "t-" => :label_ago, Chris@1464: "~" => :label_contains, Chris@1464: "!~" => :label_not_contains, Chris@1464: "=p" => :label_any_issues_in_project, Chris@1464: "=!p" => :label_any_issues_not_in_project, Chris@1464: "!p" => :label_no_issues_in_project Chris@1464: } Chris@441: Chris@1464: class_attribute :operators_by_filter_type Chris@1464: self.operators_by_filter_type = { Chris@1464: :list => [ "=", "!" ], Chris@1464: :list_status => [ "o", "=", "!", "c", "*" ], Chris@1464: :list_optional => [ "=", "!", "!*", "*" ], Chris@1464: :list_subprojects => [ "*", "!*", "=" ], Chris@1464: :date => [ "=", ">=", "<=", "><", "t+", ">t-", " [ "=", ">=", "<=", "><", ">t-", " [ "=", "~", "!", "!~", "!*", "*" ], Chris@1464: :text => [ "~", "!~", "!*", "*" ], Chris@1464: :integer => [ "=", ">=", "<=", "><", "!*", "*" ], Chris@1464: :float => [ "=", ">=", "<=", "><", "!*", "*" ], Chris@1464: :relation => ["=", "=p", "=!p", "!p", "!*", "*"] Chris@1464: } Chris@0: Chris@1464: class_attribute :available_columns Chris@1464: self.available_columns = [] Chris@0: Chris@1464: class_attribute :queried_class Chris@441: Chris@1464: def queried_table_name Chris@1464: @queried_table_name ||= self.class.queried_class.table_name Chris@1464: end Chris@909: Chris@1115: def initialize(attributes=nil, *args) Chris@0: super attributes Chris@0: @is_for_all = project.nil? Chris@0: end Chris@441: Chris@1464: # Builds the query from the given params Chris@1464: def build_from_params(params) Chris@1464: if params[:fields] || params[:f] Chris@1464: self.filters = {} Chris@1464: add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) Chris@1464: else Chris@1464: available_filters.keys.each do |field| Chris@1464: add_short_filter(field, params[field]) if params[field] Chris@1464: end Chris@1464: end Chris@1464: self.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) Chris@1464: self.column_names = params[:c] || (params[:query] && params[:query][:column_names]) Chris@1464: self Chris@1464: end Chris@1464: Chris@1464: # Builds a new query from the given params and attributes Chris@1464: def self.build_from_params(params, attributes={}) Chris@1464: new(attributes).build_from_params(params) Chris@1464: end Chris@1464: Chris@909: def validate_query_filters Chris@0: filters.each_key do |field| Chris@909: if values_for(field) Chris@909: case type_for(field) Chris@909: when :integer Chris@1115: add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } Chris@909: when :float Chris@1115: add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } Chris@909: when :date, :date_past Chris@909: case operator_for(field) Chris@909: when "=", ">=", "<=", "><" Chris@1115: add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) } Chris@1115: when ">t-", "t+", " 'activerecord.errors.messages') Chris@1115: errors.add(:base, m) Chris@1115: end Chris@1115: Chris@0: def editable_by?(user) Chris@0: return false unless user Chris@0: # Admin can edit them all and regular users can edit their private queries Chris@1464: return true if user.admin? || (is_private? && self.user_id == user.id) Chris@0: # Members can not edit public queries that are for all project (only admin is allowed to) Chris@1464: is_public? && !@is_for_all && user.allowed_to?(:manage_public_queries, project) Chris@0: end Chris@441: Chris@1115: def trackers Chris@1464: @trackers ||= project.nil? ? Tracker.sorted.all : project.rolled_up_trackers Chris@1115: end Chris@1115: Chris@1115: # Returns a hash of localized labels for all filter operators Chris@1115: def self.operators_labels Chris@1464: operators.inject({}) {|h, operator| h[operator.first] = l(*operator.last); h} Chris@0: end Chris@441: Chris@1115: # Returns a representation of the available filters for JSON serialization Chris@1115: def available_filters_as_json Chris@1115: json = {} Chris@1115: available_filters.each do |field, options| Chris@1115: json[field] = options.slice(:type, :name, :values).stringify_keys Chris@1115: end Chris@1115: json Chris@1115: end Chris@1115: Chris@1115: def all_projects Chris@1115: @all_projects ||= Project.visible.all Chris@1115: end Chris@1115: Chris@1115: def all_projects_values Chris@1115: return @all_projects_values if @all_projects_values Chris@1115: Chris@1115: values = [] Chris@1115: Project.project_tree(all_projects) do |p, level| Chris@1115: prefix = (level > 0 ? ('--' * level + ' ') : '') Chris@1115: values << ["#{prefix}#{p.name}", p.id.to_s] Chris@1115: end Chris@1115: @all_projects_values = values Chris@1115: end Chris@1115: Chris@1464: # Adds available filters Chris@1464: def initialize_available_filters Chris@1464: # implemented by sub-classes Chris@1464: end Chris@1464: protected :initialize_available_filters Chris@1464: Chris@1464: # Adds an available filter Chris@1464: def add_available_filter(field, options) Chris@1464: @available_filters ||= ActiveSupport::OrderedHash.new Chris@1464: @available_filters[field] = options Chris@1464: @available_filters Chris@1464: end Chris@1464: Chris@1464: # Removes an available filter Chris@1464: def delete_available_filter(field) Chris@1464: if @available_filters Chris@1464: @available_filters.delete(field) Chris@1464: end Chris@1464: end Chris@1464: Chris@1464: # Return a hash of available filters Chris@1464: def available_filters Chris@1464: unless @available_filters Chris@1464: initialize_available_filters Chris@1464: @available_filters.each do |field, options| Chris@1464: options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, '')) Chris@1464: end Chris@1464: end Chris@1464: @available_filters Chris@1464: end Chris@1464: Chris@1464: def add_filter(field, operator, values=nil) Chris@0: # values must be an array Chris@909: return unless values.nil? || values.is_a?(Array) Chris@0: # check if field is defined as an available filter Chris@0: if available_filters.has_key? field Chris@0: filter_options = available_filters[field] Chris@909: filters[field] = {:operator => operator, :values => (values || [''])} Chris@0: end Chris@0: end Chris@441: Chris@0: def add_short_filter(field, expression) Chris@909: return unless expression && available_filters.has_key?(field) Chris@909: field_type = available_filters[field][:type] Chris@1464: operators_by_filter_type[field_type].sort.reverse.detect do |operator| Chris@909: next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/ Chris@1464: values = $1 Chris@1464: add_filter field, operator, values.present? ? values.split('|') : [''] Chris@909: end || add_filter(field, '=', expression.split('|')) Chris@0: end Chris@0: Chris@0: # Add multiple filters using +add_filter+ Chris@0: def add_filters(fields, operators, values) Chris@909: if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash)) chris@37: fields.each do |field| Chris@909: add_filter(field, operators[field], values && values[field]) chris@37: end Chris@0: end Chris@0: end Chris@441: Chris@0: def has_filter?(field) Chris@0: filters and filters[field] Chris@0: end Chris@441: Chris@909: def type_for(field) Chris@909: available_filters[field][:type] if available_filters.has_key?(field) Chris@909: end Chris@909: Chris@0: def operator_for(field) Chris@0: has_filter?(field) ? filters[field][:operator] : nil Chris@0: end Chris@441: Chris@0: def values_for(field) Chris@0: has_filter?(field) ? filters[field][:values] : nil Chris@0: end Chris@441: Chris@909: def value_for(field, index=0) Chris@909: (values_for(field) || [])[index] Chris@909: end Chris@909: Chris@0: def label_for(field) Chris@0: label = available_filters[field][:name] if available_filters.has_key?(field) Chris@1115: label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) Chris@0: end Chris@0: Chris@0: def self.add_available_column(column) Chris@0: self.available_columns << (column) if column.is_a?(QueryColumn) Chris@0: end Chris@441: Chris@0: # Returns an array of columns that can be used to group the results Chris@0: def groupable_columns Chris@0: available_columns.select {|c| c.groupable} Chris@0: end Chris@0: Chris@0: # Returns a Hash of columns and the key for sorting Chris@0: def sortable_columns Chris@1464: available_columns.inject({}) {|h, column| Chris@1464: h[column.name.to_s] = column.sortable Chris@1464: h Chris@1464: } Chris@0: end Chris@441: Chris@0: def columns Chris@909: # preserve the column_names order Chris@1464: cols = (has_default_columns? ? default_columns_names : column_names).collect do |name| Chris@909: available_columns.find { |col| col.name == name } Chris@909: end.compact Chris@1464: available_columns.select(&:frozen?) | cols Chris@909: end Chris@909: Chris@1115: def inline_columns Chris@1115: columns.select(&:inline?) Chris@1115: end Chris@1115: Chris@1115: def block_columns Chris@1115: columns.reject(&:inline?) Chris@1115: end Chris@1115: Chris@1115: def available_inline_columns Chris@1115: available_columns.select(&:inline?) Chris@1115: end Chris@1115: Chris@1115: def available_block_columns Chris@1115: available_columns.reject(&:inline?) Chris@1115: end Chris@1115: Chris@909: def default_columns_names Chris@1464: [] Chris@0: end Chris@441: Chris@0: def column_names=(names) Chris@0: if names Chris@0: names = names.select {|n| n.is_a?(Symbol) || !n.blank? } Chris@0: names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } Chris@0: # Set column_names to nil if default columns Chris@909: if names == default_columns_names Chris@0: names = nil Chris@0: end Chris@0: end Chris@0: write_attribute(:column_names, names) Chris@0: end Chris@441: Chris@0: def has_column?(column) Chris@1115: column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) Chris@0: end Chris@441: Chris@1464: def has_custom_field_column? Chris@1464: columns.any? {|column| column.is_a? QueryCustomFieldColumn} Chris@1464: end Chris@1464: Chris@0: def has_default_columns? Chris@0: column_names.nil? || column_names.empty? Chris@0: end Chris@441: Chris@0: def sort_criteria=(arg) Chris@0: c = [] Chris@0: if arg.is_a?(Hash) Chris@0: arg = arg.keys.sort.collect {|k| arg[k]} Chris@0: end Chris@1115: c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, (o == 'desc' || o == false) ? 'desc' : 'asc']} Chris@0: write_attribute(:sort_criteria, c) Chris@0: end Chris@441: Chris@0: def sort_criteria Chris@0: read_attribute(:sort_criteria) || [] Chris@0: end Chris@441: Chris@0: def sort_criteria_key(arg) Chris@0: sort_criteria && sort_criteria[arg] && sort_criteria[arg].first Chris@0: end Chris@441: Chris@0: def sort_criteria_order(arg) Chris@0: sort_criteria && sort_criteria[arg] && sort_criteria[arg].last Chris@0: end Chris@441: Chris@1115: def sort_criteria_order_for(key) Chris@1115: sort_criteria.detect {|k, order| key.to_s == k}.try(:last) Chris@1115: end Chris@1115: Chris@0: # Returns the SQL sort order that should be prepended for grouping Chris@0: def group_by_sort_order Chris@0: if grouped? && (column = group_by_column) Chris@1115: order = sort_criteria_order_for(column.name) || column.default_order Chris@0: column.sortable.is_a?(Array) ? Chris@1115: column.sortable.collect {|s| "#{s} #{order}"}.join(',') : Chris@1115: "#{column.sortable} #{order}" Chris@0: end Chris@0: end Chris@441: Chris@0: # Returns true if the query is a grouped query Chris@0: def grouped? Chris@119: !group_by_column.nil? Chris@0: end Chris@441: Chris@0: def group_by_column Chris@119: groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} Chris@0: end Chris@441: Chris@0: def group_by_statement Chris@119: group_by_column.try(:groupable) Chris@0: end Chris@441: Chris@0: def project_statement Chris@0: project_clauses = [] Chris@909: if project && !project.descendants.active.empty? Chris@0: ids = [project.id] Chris@0: if has_filter?("subproject_id") Chris@0: case operator_for("subproject_id") Chris@0: when '=' Chris@0: # include the selected subprojects Chris@0: ids += values_for("subproject_id").each(&:to_i) Chris@0: when '!*' Chris@0: # main project only Chris@0: else Chris@0: # all subprojects Chris@0: ids += project.descendants.collect(&:id) Chris@0: end Chris@0: elsif Setting.display_subprojects_issues? Chris@0: ids += project.descendants.collect(&:id) Chris@0: end Chris@0: project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') Chris@0: elsif project Chris@0: project_clauses << "#{Project.table_name}.id = %d" % project.id Chris@0: end Chris@441: project_clauses.any? ? project_clauses.join(' AND ') : nil Chris@0: end Chris@0: Chris@0: def statement Chris@0: # filters clauses Chris@0: filters_clauses = [] Chris@0: filters.each_key do |field| Chris@0: next if field == "subproject_id" Chris@0: v = values_for(field).clone Chris@0: next unless v and !v.empty? Chris@0: operator = operator_for(field) Chris@441: Chris@0: # "me" value subsitution Chris@1464: if %w(assigned_to_id author_id user_id watcher_id).include?(field) Chris@909: if v.delete("me") Chris@909: if User.current.logged? Chris@909: v.push(User.current.id.to_s) Chris@909: v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' Chris@909: else Chris@909: v.push("0") Chris@909: end Chris@909: end Chris@0: end Chris@441: Chris@1115: if field == 'project_id' Chris@1115: if v.delete('mine') Chris@1115: v += User.current.memberships.map(&:project_id).map(&:to_s) Chris@1115: end Chris@1115: end Chris@1115: Chris@1115: if field =~ /cf_(\d+)$/ Chris@0: # custom field Chris@909: filters_clauses << sql_for_custom_field(field, operator, v, $1) Chris@909: elsif respond_to?("sql_for_#{field}_field") Chris@909: # specific statement Chris@909: filters_clauses << send("sql_for_#{field}_field", field, operator, v) Chris@0: else Chris@0: # regular field Chris@1464: filters_clauses << '(' + sql_for_field(field, operator, v, queried_table_name, field) + ')' Chris@0: end Chris@0: end if filters and valid? Chris@441: Chris@1464: if (c = group_by_column) && c.is_a?(QueryCustomFieldColumn) Chris@1464: # Excludes results for which the grouped custom field is not visible Chris@1464: filters_clauses << c.custom_field.visibility_by_project_condition Chris@1464: end Chris@1464: Chris@441: filters_clauses << project_statement Chris@441: filters_clauses.reject!(&:blank?) Chris@441: Chris@441: filters_clauses.any? ? filters_clauses.join(' AND ') : nil Chris@0: end Chris@441: Chris@0: private Chris@441: Chris@909: def sql_for_custom_field(field, operator, value, custom_field_id) Chris@909: db_table = CustomValue.table_name Chris@909: db_field = 'value' Chris@1115: filter = @available_filters[field] Chris@1115: return nil unless filter Chris@1115: if filter[:format] == 'user' Chris@1115: if value.delete('me') Chris@1115: value.push User.current.id.to_s Chris@1115: end Chris@1115: end Chris@1115: not_in = nil Chris@1115: if operator == '!' Chris@1115: # Makes ! operator work for custom fields with multiple values Chris@1115: operator = '=' Chris@1115: not_in = 'NOT' Chris@1115: end Chris@1115: customized_key = "id" Chris@1464: customized_class = queried_class Chris@1115: if field =~ /^(.+)\.cf_/ Chris@1115: assoc = $1 Chris@1115: customized_key = "#{assoc}_id" Chris@1464: customized_class = queried_class.reflect_on_association(assoc.to_sym).klass.base_class rescue nil Chris@1464: raise "Unknown #{queried_class.name} association #{assoc}" unless customized_class Chris@1115: end Chris@1464: where = sql_for_field(field, operator, value, db_table, db_field, true) Chris@1464: if operator =~ /[<>]/ Chris@1464: where = "(#{where}) AND #{db_table}.#{db_field} <> ''" Chris@1464: end Chris@1464: "#{queried_table_name}.#{customized_key} #{not_in} IN (" + Chris@1464: "SELECT #{customized_class.table_name}.id FROM #{customized_class.table_name}" + Chris@1464: " 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}" + Chris@1464: " WHERE (#{where}) AND (#{filter[:field].visibility_by_project_condition}))" Chris@909: end Chris@909: Chris@0: # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ Chris@0: def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) Chris@0: sql = '' Chris@0: case operator Chris@0: when "=" Chris@245: if value.any? Chris@909: case type_for(field) Chris@909: when :date, :date_past Chris@909: sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) Chris@909: when :integer Chris@1115: if is_custom_filter Chris@1464: 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})" Chris@1115: else Chris@1115: sql = "#{db_table}.#{db_field} = #{value.first.to_i}" Chris@1115: end Chris@909: when :float Chris@1115: if is_custom_filter Chris@1464: 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})" Chris@1115: else Chris@1115: sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" Chris@1115: end Chris@909: else Chris@909: sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" Chris@909: end Chris@245: else Chris@245: # IN an empty set Chris@245: sql = "1=0" Chris@245: end Chris@0: when "!" Chris@245: if value.any? Chris@245: sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" Chris@245: else Chris@245: # NOT IN an empty set Chris@245: sql = "1=1" Chris@245: end Chris@0: when "!*" Chris@0: sql = "#{db_table}.#{db_field} IS NULL" Chris@0: sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter Chris@0: when "*" Chris@0: sql = "#{db_table}.#{db_field} IS NOT NULL" Chris@0: sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter Chris@0: when ">=" Chris@909: if [:date, :date_past].include?(type_for(field)) Chris@909: sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) Chris@909: else Chris@909: if is_custom_filter Chris@1464: 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})" Chris@909: else Chris@909: sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" Chris@909: end Chris@909: end Chris@0: when "<=" Chris@909: if [:date, :date_past].include?(type_for(field)) Chris@909: sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) Chris@909: else Chris@909: if is_custom_filter Chris@1464: 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})" Chris@909: else Chris@909: sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" Chris@909: end Chris@909: end Chris@909: when "><" Chris@909: if [:date, :date_past].include?(type_for(field)) Chris@909: sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) Chris@909: else Chris@909: if is_custom_filter Chris@1464: 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})" Chris@909: else Chris@909: sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" Chris@909: end Chris@909: end Chris@0: when "o" Chris@1464: sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id" Chris@0: when "c" Chris@1464: sql = "#{queried_table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id" Chris@1115: when ">t-" Chris@1115: # >= today - n days Chris@1115: sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil) Chris@0: when "t+" Chris@1115: # >= today + n days Chris@909: sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) Chris@0: when "= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) Chris@909: sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) Chris@1464: when "lw" Chris@1464: # = last week Chris@1464: first_day_of_week = l(:general_first_day_of_week).to_i Chris@1464: day_of_week = Date.today.cwday Chris@1464: days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) Chris@1464: sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1) Chris@1464: when "l2w" Chris@1464: # = last 2 weeks Chris@1464: first_day_of_week = l(:general_first_day_of_week).to_i Chris@1464: day_of_week = Date.today.cwday Chris@1464: days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week) Chris@1464: sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1) Chris@1464: when "m" Chris@1464: # = this month Chris@1464: date = Date.today Chris@1464: sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month) Chris@1464: when "lm" Chris@1464: # = last month Chris@1464: date = Date.today.prev_month Chris@1464: sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month) Chris@1464: when "y" Chris@1464: # = this year Chris@1464: date = Date.today Chris@1464: sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year) Chris@0: when "~" Chris@0: sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" Chris@0: when "!~" Chris@0: sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" Chris@909: else Chris@909: raise "Unknown query operator #{operator}" Chris@0: end Chris@441: Chris@0: return sql Chris@0: end Chris@441: Chris@1464: # Adds a filter for the given custom field Chris@1464: def add_custom_field_filter(field, assoc=nil) Chris@1464: case field.field_format Chris@1464: when "text" Chris@1464: options = { :type => :text } Chris@1464: when "list" Chris@1464: options = { :type => :list_optional, :values => field.possible_values } Chris@1464: when "date" Chris@1464: options = { :type => :date } Chris@1464: when "bool" Chris@1464: options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] } Chris@1464: when "int" Chris@1464: options = { :type => :integer } Chris@1464: when "float" Chris@1464: options = { :type => :float } Chris@1464: when "user", "version" Chris@1464: return unless project Chris@1464: values = field.possible_values_options(project) Chris@1464: if User.current.logged? && field.field_format == 'user' Chris@1464: values.unshift ["<< #{l(:label_me)} >>", "me"] Chris@1464: end Chris@1464: options = { :type => :list_optional, :values => values } Chris@1464: else Chris@1464: options = { :type => :string } Chris@1464: end Chris@1464: filter_id = "cf_#{field.id}" Chris@1464: filter_name = field.name Chris@1464: if assoc.present? Chris@1464: filter_id = "#{assoc}.#{filter_id}" Chris@1464: filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) Chris@1464: end Chris@1464: add_available_filter filter_id, options.merge({ Chris@1464: :name => filter_name, Chris@1464: :format => field.field_format, Chris@1464: :field => field Chris@1464: }) Chris@1464: end Chris@441: Chris@1464: # Adds filters for the given custom fields scope Chris@1464: def add_custom_fields_filters(scope, assoc=nil) Chris@1464: scope.visible.where(:is_filter => true).sorted.each do |field| Chris@1464: add_custom_field_filter(field, assoc) Chris@1115: end Chris@1115: end Chris@1115: Chris@1464: # Adds filters for the given associations custom fields Chris@1115: def add_associations_custom_fields_filters(*associations) Chris@1464: fields_by_class = CustomField.visible.where(:is_filter => true).group_by(&:class) Chris@1115: associations.each do |assoc| Chris@1464: association_klass = queried_class.reflect_on_association(assoc).klass Chris@1115: fields_by_class.each do |field_class, fields| Chris@1115: if field_class.customized_class <= association_klass Chris@1464: fields.sort.each do |field| Chris@1464: add_custom_field_filter(field, assoc) Chris@1464: end Chris@1115: end Chris@1115: end Chris@0: end Chris@0: end Chris@441: Chris@0: # Returns a SQL clause for a date or datetime field. Chris@909: def date_clause(table, field, from, to) Chris@0: s = [] Chris@0: if from Chris@909: from_yesterday = from - 1 Chris@1115: from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day) Chris@1115: if self.class.default_timezone == :utc Chris@1115: from_yesterday_time = from_yesterday_time.utc Chris@1115: end Chris@1115: s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) Chris@0: end Chris@0: if to Chris@1115: to_time = Time.local(to.year, to.month, to.day) Chris@1115: if self.class.default_timezone == :utc Chris@1115: to_time = to_time.utc Chris@1115: end Chris@1115: s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)]) Chris@0: end Chris@0: s.join(' AND ') Chris@0: end Chris@909: Chris@909: # Returns a SQL clause for a date or datetime field using relative dates. Chris@909: def relative_date_clause(table, field, days_from, days_to) Chris@909: date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) Chris@909: end Chris@1115: Chris@1115: # Additional joins required for the given sort options Chris@1115: def joins_for_order_statement(order_options) Chris@1115: joins = [] Chris@1115: Chris@1115: if order_options Chris@1115: if order_options.include?('authors') Chris@1464: joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{queried_table_name}.author_id" Chris@1115: end Chris@1115: order_options.scan(/cf_\d+/).uniq.each do |name| Chris@1115: column = available_columns.detect {|c| c.name.to_s == name} Chris@1115: join = column && column.custom_field.join_for_order_statement Chris@1115: if join Chris@1115: joins << join Chris@1115: end Chris@1115: end Chris@1115: end Chris@1115: Chris@1115: joins.any? ? joins.join(' ') : nil Chris@1115: end Chris@0: end