kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
706 lines
25 KiB
706 lines
25 KiB
require_dependency 'query'
|
|
|
|
class CostQueryColumn < QueryColumn
|
|
attr_reader :scope
|
|
|
|
def initialize(name, options={})
|
|
self.scope = (optione.delete(:scope) || :issues)
|
|
super
|
|
end
|
|
end
|
|
|
|
class CostQueryCustomFieldColumn < QueryCustomFieldColumn
|
|
attr_accessor :scope
|
|
|
|
def initialize(custom_field)
|
|
self.reader = :issues
|
|
super
|
|
end
|
|
end
|
|
|
|
class Filter
|
|
include GLoc
|
|
|
|
|
|
def initialize(scope, column_name, column)
|
|
@scope = scope
|
|
@column_name = column_name
|
|
@column = column
|
|
|
|
@enabled = true
|
|
|
|
default_operator = CostQuery.filter_types[@column[:type]][:default]
|
|
@operator = default_operator if default_operator
|
|
end
|
|
attr_reader :scope, :column_name, :column
|
|
|
|
attr_reader :values, :sql_values
|
|
def values=(v)
|
|
values = v.is_a?(Array) ? v : [v]
|
|
sql_values = values.dup
|
|
|
|
if column[:flags].include? :user
|
|
sql_values.push(User.current.logged? ? User.current.id.to_s : "0") if sql_values.delete("me")
|
|
end
|
|
|
|
if available_values
|
|
available_value_keys = available_values.collect {|o| o[1].to_s }
|
|
sql_values.each do |value|
|
|
unless (available_value_keys.include? value.to_s) or (value.to_s == "")
|
|
raise ArgumentError.new("Forbidden value (#{value.inspect} not in #{available_value_keys.inspect})")
|
|
end
|
|
end
|
|
end
|
|
|
|
@values = values
|
|
@sql_values = sql_values
|
|
end
|
|
|
|
attr_reader :operator
|
|
def operator=(o)
|
|
raise ArgumentError.new("Forbidden operator #{o}") unless available_operators.include? o
|
|
@operator = o
|
|
end
|
|
|
|
attr_accessor :enabled
|
|
|
|
def type_name
|
|
@column[:type]
|
|
end
|
|
|
|
def label
|
|
@column[:name] || l(("field_"+@column_name.gsub(/\_id$/, "")).to_sym)
|
|
end
|
|
|
|
def available_operators
|
|
CostQuery.filter_types[@column[:type]][:operators]
|
|
end
|
|
|
|
def available_values
|
|
@column[:values]
|
|
end
|
|
|
|
def new_record?
|
|
return true
|
|
end
|
|
end
|
|
|
|
class CostQuery < ActiveRecord::Base
|
|
include GLoc
|
|
|
|
belongs_to :user
|
|
belongs_to :project
|
|
|
|
serialize :filters
|
|
serialize :group_by
|
|
|
|
attr_protected :user_id, :project_id, :created_at, :updated_at
|
|
|
|
def after_initialize
|
|
self.display_time_entries = true if display_time_entries.nil?
|
|
self.display_cost_entries = true if display_cost_entries.nil?
|
|
|
|
self.group_by ||= {}
|
|
end
|
|
|
|
def self.operators
|
|
# These are the operators used by filter types.
|
|
|
|
operators = {}
|
|
issue_operators = Query.operators
|
|
|
|
issue_operators.each_pair do |op, label|
|
|
simple = (["!*", "*", "t", "w", "o", "c"].include? op)
|
|
operators[op] = {:label => label, :simple => simple}
|
|
|
|
end
|
|
|
|
operators.merge(
|
|
{
|
|
"=n" => {:label => :label_equals, :simple => false},
|
|
"0" => {:label => :label_none, :simple => true},
|
|
"y" => {:label => :label_yes, :simple => true},
|
|
"n" => {:label => :label_no, :simple => true},
|
|
"<d" => {:label => :label_less_or_equal, :simple => false},
|
|
">d" => {:label => :label_greater_or_equal, :simple => false},
|
|
"<>d" => {:label => :label_between, :simple => false},
|
|
"=d" => {:label => :label_date_on, :simple => false}
|
|
}
|
|
)
|
|
end
|
|
|
|
def self.filter_types
|
|
return @filter_types if @filter_types
|
|
|
|
filter_types = Query.operators_by_filter_type.inject({}) do |r, f|
|
|
multiple = !([:list, :list_status, :list_optional, :list_subproject].include? f[0])
|
|
r[f[0]] = {:operators => f[1], :multiple => multiple}
|
|
r
|
|
end
|
|
@filter_types = filter_types.merge(
|
|
{
|
|
:integer_zero => {:operators => [ "=n", ">=", "<=", "0", "*" ], :multiple => true},
|
|
:boolean => {:operators => [ "y", "n" ], :multiple => false},
|
|
:date_exact => {:operators => [ "<d", ">d", "<>d", "=d", "t", "w"], :multiple => true, :default => "w"}
|
|
}
|
|
)
|
|
end
|
|
|
|
def available_filters
|
|
# This available_filters is different from the Redmine one
|
|
# available_filters[:issues]
|
|
# --> filters on issue fields. These are the one from redmine itself
|
|
# available_filters[:costs]
|
|
# --> filters on cost and time entries
|
|
|
|
return @available_filters if @available_filters
|
|
|
|
@available_filters = {
|
|
:costs => {
|
|
"cost_type_id" => { :type => :list_optional, :order => 2, :applies => [:cost_entries], :flags => [], :db_table => CostType.table_name, :db_field => "id", :values => CostType.find(:all, :order => 'name').collect{|s| [s.name, s.id.to_s] }},
|
|
"activity_id" => { :type => :list_optional, :order => 3, :applies => [:time_entries], :flags => [], :db_table => TimeEntryActivity.table_name, :db_field => "id", :values => TimeEntryActivity.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }},
|
|
"created_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 4 },
|
|
"updated_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 5 },
|
|
"spent_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 6},
|
|
"overridden_costs" => { :type => :boolean, :applies => [:time_entries, :cost_entries], :flags => [], :order => 7 },
|
|
# FIXME: Issues are not selected properly according to project selection
|
|
"issue_id" => { :type => :list_optional, :order => 8, :applies => [:cost_entries, :time_entries], :flags => [], :db_table => Issue.table_name, :db_field => "id", :values => Issue.find(:all, :order => :id, :include => :tracker).collect{|s| ["#{s.tracker} ##{s.id}: #{s.subject}", s.id.to_s] }},
|
|
}
|
|
}
|
|
|
|
tmp_query = Query.new(:project => project, :name => "_")
|
|
@available_filters[:issues] = tmp_query.available_filters
|
|
# flag columns that contain filters for user columns
|
|
@available_filters[:issues].each_pair do |k,v|
|
|
v[:flags] = []
|
|
v[:flags] << :user if %w(assigned_to_id author_id watcher_id).include?(k)
|
|
if k =~ /^cf_(\d+)$/
|
|
# custom field
|
|
v[:db_table] = CustomValue.table_name
|
|
v[:db_field] = 'value'
|
|
v[:db_field_id] = $1 # this is the numeric part in the regex above
|
|
v[:flags] << :custom_field
|
|
elsif k == "watcher_id"
|
|
v[:db_table] = Watcher.table_name
|
|
v[:db_field] = 'user_id'
|
|
v[:flags] << :watcher
|
|
else
|
|
if ["labor_costs", "material_costs", "overall_costs"].include? k
|
|
v[:type] = :integer_zero
|
|
end
|
|
if [:date_past, :date].include? v[:type]
|
|
v[:type] = :date_exact
|
|
end
|
|
v[:db_table] = Issue.table_name
|
|
v[:db_field] = k
|
|
end
|
|
end
|
|
|
|
if @available_filters[:issues]["author_id"]
|
|
# add a filter on cost entries for user_id if it is available
|
|
user_values = @available_filters[:issues]["author_id"][:values]
|
|
@available_filters[:costs]["user_id"] = {:type => :list_optional, :order => 1, :applies => [:time_entries, :cost_entries], :values => user_values, :flags => [:user]}
|
|
end
|
|
|
|
@available_filters
|
|
end
|
|
|
|
def create_filter(scope, column_name)
|
|
column = available_filters[scope][column_name]
|
|
column ? Filter.new(scope, column_name, column) : nil
|
|
end
|
|
|
|
def create_filter_from_hash(filter_hash = {})
|
|
scope = filter_hash[:scope].to_sym
|
|
column_name = filter_hash[:column_name]
|
|
column = available_filters[scope][column_name]
|
|
|
|
f = Filter.new(scope, column_name, column)
|
|
f.enabled = filter_hash[:enabled] unless filter_hash[:enabled].nil?
|
|
f.operator = filter_hash[:operator] unless filter_hash[:operator].nil?
|
|
f.values = filter_hash[:values] unless filter_hash[:values].nil?
|
|
|
|
f
|
|
end
|
|
|
|
def has_filter?(scope, column_name)
|
|
# returns the first matching filter or nil
|
|
return nil unless filters
|
|
|
|
match = filters.select {|f| f[:scope] == scope.to_s && f[:column_name] == column_name.to_s}
|
|
return match.blank? ? nil : match[0]
|
|
end
|
|
|
|
MAGIC_GROUP_KEYS = [:block, :time, :display, :db_field, :other_group]
|
|
|
|
def self.grouping_column(*names, &block)
|
|
options = names.extract_options!
|
|
names.each do |name|
|
|
group_by_columns[name] = options.with_indifferent_access.merge(
|
|
:block => block,
|
|
:scope => grouping_scope
|
|
)
|
|
group_by_columns[name][:db_field] ||= name
|
|
group_by_columns[name][:display] ||= Proc.new { |e| e }
|
|
group_by_columns[name][:other_group] ||= "<em>#{l :group_by_others}</em>"
|
|
end
|
|
end
|
|
|
|
def self.grouping_scope(type = nil)
|
|
@grouping_scope = type || @grouping_scope
|
|
yield if block_given?
|
|
@grouping_scope
|
|
end
|
|
|
|
def self.group_by_columns
|
|
@group_by_columns ||= {}.with_indifferent_access
|
|
end
|
|
|
|
def self.get_name(key, value)
|
|
return group_by_columns[key][:other_group] unless value
|
|
group_by_columns[key][:display].call value
|
|
end
|
|
|
|
def self.from_field(klass, field)
|
|
Proc.new do |id|
|
|
a = klass.find_by_id(id)
|
|
(a ? a.send(field) : id).to_s
|
|
end
|
|
end
|
|
|
|
grouping_scope(:issues) do
|
|
grouping_column :tracker_id, :display => from_field(Tracker, :name)
|
|
grouping_column :fixed_version_id, :display => from_field(Version, :name)
|
|
grouping_column :cost_object_id, :display => from_field(CostObject, :subject)
|
|
grouping_column :subproject_id, :display => from_field(Project, :name), :db_field => :project_id
|
|
end
|
|
|
|
grouping_scope(:costs) do
|
|
grouping_column :user_id, :display => from_field(User, :name)
|
|
grouping_column :issue_id, :display => from_field(Issue, :subject), :other_group => "<em>#{l(:caption_booked_on_project)}</em>"
|
|
grouping_column :cost_type_id, :display => from_field(CostType, :name), :other_group => l(:caption_labor_costs)
|
|
grouping_column :activity_id, :display => from_field(Enumeration, :name)
|
|
grouping_column(:spent_on, :tyear, :tmonth, :tweek, :time => true) do |column, fields|
|
|
values = []
|
|
|
|
if fields["spent_on"]
|
|
values = [fields["spent_on"].to_date] * 2
|
|
elsif fields["tyear"]
|
|
if fields["tmonth"]
|
|
start_of_month = Date.civil(fields["tyear"].to_i, fields["tmonth"].to_i , 1)
|
|
values = [start_of_month.to_s, start_of_month.end_of_month.to_s]
|
|
elsif fields["tweek"]
|
|
start_of_week = Date.commercial(fields["tyear"].to_i, fields["tweek"].to_i, 1)
|
|
values = [start_of_week.to_s, start_of_week.end_of_week.to_s]
|
|
else
|
|
start_of_year = Date.civil(fields["tyear"].to_i, 1, 1)
|
|
values = [start_of_year.to_s, start_of_year.end_of_year.to_s]
|
|
end
|
|
end
|
|
|
|
raise "Invalid group by values" if values.blank?
|
|
|
|
{
|
|
:operator => "<>d",
|
|
:values => values,
|
|
:column_name => :spent_on
|
|
}
|
|
|
|
end
|
|
end
|
|
|
|
def filter_from_group_by(fields)
|
|
column_name = group_by[:name].to_sym
|
|
data = self.class.group_by_columns[column_name].dup
|
|
options = {}
|
|
MAGIC_GROUP_KEYS.each do |key|
|
|
options[key] = data.delete key
|
|
end
|
|
block = options[:block] || Proc.new { {} }
|
|
equals_hash = {
|
|
:enabled => 1,
|
|
:operator => "=",
|
|
:column_name => column_name,
|
|
:values => fields[column_name.to_s],
|
|
}
|
|
|
|
# TODO: not all filters have this filter operator. We have to always select the correct operator
|
|
none_hash = {
|
|
:enabled => 1,
|
|
:operator => "!*",
|
|
:column_name => column_name,
|
|
:values => nil,
|
|
}
|
|
|
|
hash = fields[column_name.to_s].nil? ? none_hash : equals_hash
|
|
hash.merge(data).merge(block.call(column_name, fields))
|
|
end
|
|
|
|
|
|
def group_by_columns_for_select
|
|
self.class.group_by_columns.inject([["", ""]]) do |list, (column_name, values)|
|
|
filter = create_filter(values[:scope], column_name.to_s)
|
|
list << [filter.label, column_name] if filter
|
|
list
|
|
end
|
|
end
|
|
|
|
def time_groups
|
|
# returns an array of group_by names where time == true
|
|
self.class.group_by_columns.inject([]) do |list, (column_name, values)|
|
|
list << column_name if values[:time]
|
|
list
|
|
end
|
|
end
|
|
|
|
def projects
|
|
return @projects unless @projects.blank?
|
|
|
|
projects = [project]
|
|
if project && !project.children.active.empty?
|
|
if subprojects = has_filter?(:issues, "subproject_id")
|
|
subprojects = create_filter_from_hash(subprojects)
|
|
|
|
case subprojects.operator
|
|
when "="
|
|
# include the selected subprojects
|
|
projects += Project.find_by_id(subprojects.values.each(&:to_i))
|
|
when "!*"
|
|
# main project only
|
|
else
|
|
# all subprojects
|
|
projects += project.descendants
|
|
end
|
|
elsif Setting.display_subprojects_issues?
|
|
projects += project.descendants
|
|
end
|
|
elsif project
|
|
# show only the current project
|
|
else
|
|
projects = []
|
|
end
|
|
|
|
@projects = projects
|
|
end
|
|
|
|
def project_statement
|
|
"#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
|
|
end
|
|
|
|
def group_by_fields()
|
|
# returns the group_by of the current group by query
|
|
# These fields are one of the keys of group_by_columns
|
|
|
|
return [] unless !group_by[:name].blank? && (data = group_by_columns[group_by[:name].to_sym])
|
|
|
|
if data[:time]
|
|
# We have a group_by_time
|
|
return case group_by[:granularity]
|
|
when "year" then ["tyear"]
|
|
when "month" then ["tyear", "tmonth"]
|
|
when "week" then ["tyear", "tweek"]
|
|
else ["spent_on"]
|
|
end
|
|
else
|
|
[group_by[:name]]
|
|
end
|
|
end
|
|
|
|
def group_by_columns
|
|
self.class.group_by_columns
|
|
end
|
|
|
|
def sql_data_for(entry_scope)
|
|
case entry_scope
|
|
when :cost_entries
|
|
model = CostEntry
|
|
when :time_entries
|
|
model = TimeEntry
|
|
from_include_issue = false
|
|
end
|
|
|
|
my_fields, nil_fields, grouping_fields = [], [], []
|
|
|
|
group_by_fields.each do |field|
|
|
group_by_column = group_by_columns[field.to_sym]
|
|
klass = group_by_column[:scope] == :issues ? Issue : model
|
|
db_field = group_by_column[:db_field]
|
|
|
|
if klass.column_names.include? db_field.to_s
|
|
grouping_fields << "#{klass.table_name}.#{db_field}"
|
|
my_fields << "#{klass.table_name}.#{db_field} as #{field}"
|
|
else
|
|
nil_fields << "NULL as #{field}"
|
|
end
|
|
end
|
|
|
|
group_by = "GROUP BY #{grouping_fields.join(", ")}" unless grouping_fields.blank?
|
|
[model, (my_fields + nil_fields).join(", "), rate_permission_statement(entry_scope), from_statement(entry_scope), statement(entry_scope), group_by]
|
|
end
|
|
|
|
def rate_permission_statement(entry_scope)
|
|
case entry_scope
|
|
when :cost_entries
|
|
statement = User.current.allowed_for(:view_cost_rates, projects)
|
|
when :time_entries
|
|
statement = User.current.allowed_for(:view_hourly_rates, projects)
|
|
end
|
|
end
|
|
|
|
def from_statement(entry_scope, include_issue = false)
|
|
case entry_scope
|
|
when :cost_entries
|
|
from = <<-EOS
|
|
#{CostEntry.table_name}
|
|
LEFT OUTER JOIN #{CostType.table_name} ON #{CostType.table_name}.id = #{CostEntry.table_name}.cost_type_id
|
|
LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{CostEntry.table_name}.user_id
|
|
LEFT OUTER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{CostEntry.table_name}.issue_id
|
|
LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{CostEntry.table_name}.project_id
|
|
EOS
|
|
when :time_entries
|
|
from = <<-EOS
|
|
#{TimeEntry.table_name}
|
|
LEFT OUTER JOIN #{TimeEntryActivity.table_name} ON #{TimeEntryActivity.table_name}.id = #{TimeEntry.table_name}.activity_id
|
|
LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{TimeEntry.table_name}.user_id
|
|
LEFT OUTER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{TimeEntry.table_name}.issue_id
|
|
LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id
|
|
EOS
|
|
end
|
|
end
|
|
|
|
def statement(entry_scope)
|
|
# entry_scope can currently be one of :cost_entries, :time_entries
|
|
# To not mix this with the scope (aka :issues vs. :costs)
|
|
|
|
issue_filter_clauses = []
|
|
entry_filter_clauses = []
|
|
|
|
# allow blank issue_ids if true
|
|
issue_nil_filter = true
|
|
|
|
if filters and valid?
|
|
filters.each do |filter|
|
|
filter = create_filter_from_hash(filter)
|
|
next if filter.column_name == "subproject_id"
|
|
|
|
sql = ''
|
|
|
|
case filter.scope
|
|
when :issues
|
|
if filter.column[:flags].include? :custom_field
|
|
sql << "#{Issue.table_name}.id IN ("
|
|
sql << " SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{filter.column[:db_table]} ON #{filter.column[:db_table]}.customized_type='Issue' AND #{filter.column[:db_table]}.customized_id=#{Issue.table_name}.id AND #{filter.column[:db_table]}.custom_field_id=#{filter.column[:db_field_id]} WHERE "
|
|
sql << sql_for_filter(filter, nil, true)
|
|
sql << ")"
|
|
elsif filter.column[:flags].include? :watcher
|
|
sql << "#{Issue.table_name}.id #{ field.operator == '=' ? 'IN' : 'NOT IN' } ("
|
|
sql << " SELECT #{filter.column[:db_table]}.watchable_id FROM #{filter.column[:db_table]} WHERE #{filter.column[:db_table]}.watchable_type='Issue' AND "
|
|
sql << sql_for_filter(filter)
|
|
sql << ")"
|
|
else
|
|
sql << '(' + sql_for_filter(filter) + ')'
|
|
end
|
|
issue_filter_clauses << sql
|
|
when :costs
|
|
issue_nil_filter = false if filter.column_name == "issue_id"
|
|
sql << '(' + sql_for_filter(filter, entry_scope) + ')'
|
|
entry_filter_clauses << sql
|
|
end
|
|
end
|
|
end
|
|
|
|
issue_filter_clauses = ["1=1"] if issue_filter_clauses.blank?
|
|
# FIXME: This is a hack calling a private ActiveRecord methods
|
|
# from http://pivotallabs.com/users/jsusser/blog/articles/567-hacking-a-subselect-in-activerecord
|
|
|
|
from = "#{Issue.table_name}"
|
|
from << " LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{Issue.table_name}.assigned_to_id"
|
|
from << " LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id"
|
|
from << " LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id"
|
|
from << " LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id"
|
|
from << " LEFT OUTER JOIN #{IssuePriority.table_name} ON #{IssuePriority.table_name}.id = #{Issue.table_name}.priority_id"
|
|
from << " LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id"
|
|
from << " LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Issue.table_name}.fixed_version_id"
|
|
|
|
issue_ids = Issue.send(
|
|
:construct_finder_sql,
|
|
:select => "#{Issue.table_name}.id",
|
|
#:include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
|
|
:from => from,
|
|
:conditions => (issue_filter_clauses << project_statement).join(' AND '))
|
|
|
|
case entry_scope
|
|
when :cost_entries
|
|
clause = ["#{CostEntry.table_name}.issue_id IN (#{issue_ids})"]
|
|
clause << "#{CostEntry.table_name}.issue_id IS NULL" if issue_nil_filter
|
|
entry_filter_clauses << "(#{clause.join(" OR ")})"
|
|
when :time_entries
|
|
clause = ["#{TimeEntry.table_name}.issue_id IN (#{issue_ids})"]
|
|
clause << "#{TimeEntry.table_name}.issue_id IS NULL" if issue_nil_filter
|
|
entry_filter_clauses << "(#{clause.join(" OR ")})"
|
|
end
|
|
|
|
entry_filter_clauses << User.current.allowed_for("view_#{entry_scope}".to_sym, projects)
|
|
|
|
entry_filter_clauses.join(' AND ')
|
|
end
|
|
|
|
|
|
|
|
def sort_criteria=(arg)
|
|
c = []
|
|
if arg.is_a?(Hash)
|
|
arg = arg.keys.sort.collect {|k| arg[k]}
|
|
end
|
|
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
|
|
write_attribute(:sort_criteria, c)
|
|
end
|
|
|
|
def sort_criteria
|
|
read_attribute(:sort_criteria) || []
|
|
end
|
|
|
|
def sort_criteria_key(arg)
|
|
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
|
|
end
|
|
|
|
def sort_criteria_order(arg)
|
|
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
|
|
end
|
|
|
|
|
|
private
|
|
def sql_for_filter(filter, entry_scope = nil, string_as_null = false)
|
|
db_table = filter.column[:db_table]
|
|
if filter.scope == :costs && !db_table
|
|
case entry_scope
|
|
when :cost_entries
|
|
db_table = CostEntry.table_name
|
|
when :time_entries
|
|
db_table = TimeEntry.table_name
|
|
else
|
|
raise "Need a valid entry scope. Got #{entry_scope.inspect}" unless entry_scope
|
|
end
|
|
end
|
|
|
|
if filter.scope == :costs && (!filter.column[:applies].include? entry_scope)
|
|
# the current filter does not match the entry_scope so we just ignore it
|
|
return "1=1"
|
|
end
|
|
|
|
db_field = filter.column[:db_field] || filter.column_name
|
|
|
|
# Does not work for Redmine 0.8
|
|
#@sql_for_filter_query = Query.new(:name => "_") unless @sql_for_filter_query
|
|
#sql = @sql_for_filter_query.send(
|
|
# :sql_for_field,
|
|
# filter.column_name, filter.operator, filter.sql_values, db_table, db_field, string_as_null)
|
|
|
|
sql = sql_for_field(filter.column_name, filter.operator, filter.sql_values, db_table, db_field, string_as_null)
|
|
return sql unless sql == "1=1"
|
|
|
|
# We have an operator that was added by us. So we provide the logic here
|
|
case filter.operator
|
|
when "0"
|
|
sql = "#{db_table}.#{db_field} = 0"
|
|
when "y"
|
|
sql = "#{db_table}.#{db_field} IS NOT NULL"
|
|
when "n"
|
|
sql = "#{db_table}.#{db_field} IS NULL"
|
|
when "=n"
|
|
sql = "#{db_table}.#{db_field} = #{CostRate.clean_currency(filter.sql_values).to_f.to_s}"
|
|
when "<>d"
|
|
begin
|
|
date1 = filter.sql_values.first.to_date
|
|
date2 = filter.sql_values.last.to_date
|
|
sql = "#{db_table}.#{db_field} BETWEEN '#{connection.quoted_date(date1)}' AND '#{connection.quoted_date(date2)}'"
|
|
rescue
|
|
end
|
|
when ">d"
|
|
begin
|
|
date = filter.sql_values.first.to_date
|
|
sql = "#{db_table}.#{db_field} >= '#{connection.quoted_date(date)}'"
|
|
rescue
|
|
end
|
|
when "<d"
|
|
begin
|
|
date = filter.sql_values.first.to_date
|
|
sql = "#{db_table}.#{db_field} <= '#{connection.quoted_date(date)}'"
|
|
rescue
|
|
end
|
|
when "=d"
|
|
begin
|
|
date = filter.sql_values.first.to_date
|
|
sql = "#{db_table}.#{db_field} = '#{connection.quoted_date(date)}'"
|
|
rescue
|
|
end
|
|
end
|
|
|
|
return sql
|
|
end
|
|
|
|
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
|
|
# FIXME: This methods comes from redmine trunk. Delete this one and call the one from redmine instead!
|
|
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
|
|
sql = ''
|
|
case operator
|
|
when "="
|
|
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" unless value.blank?
|
|
when "!"
|
|
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
|
|
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 ">="
|
|
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
|
|
when "<="
|
|
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
|
|
when "o"
|
|
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
|
|
when "c"
|
|
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
|
|
when ">t-"
|
|
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
|
|
when "<t-"
|
|
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
|
|
when "t-"
|
|
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
|
|
when ">t+"
|
|
sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
|
|
when "<t+"
|
|
sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
|
|
when "t+"
|
|
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
|
|
when "t"
|
|
sql = date_range_clause(db_table, db_field, 0, 0)
|
|
when "w"
|
|
from = l(:general_first_day_of_week) == '7' ?
|
|
# week starts on sunday
|
|
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) :
|
|
# week starts on monday (Rails default)
|
|
Time.now.at_beginning_of_week
|
|
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)]
|
|
when "~"
|
|
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
|
|
when "!~"
|
|
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
|
|
end
|
|
|
|
return sql.blank? ? "1=1" : sql
|
|
end
|
|
|
|
# Returns a SQL clause for a date or datetime field.
|
|
def date_range_clause(table, field, from, to)
|
|
s = []
|
|
if from
|
|
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)])
|
|
end
|
|
if to
|
|
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)])
|
|
end
|
|
s.join(' AND ')
|
|
end
|
|
end
|
|
|