removes everything to prepare for merge of deployment branch

pull/6827/head
jwollert 14 years ago
parent 4b7984976a
commit 3266732ed0
  1. 0
      redmine_reporting/README
  2. 0
      redmine_reporting/README.rdoc
  3. 204
      redmine_reporting/app/controllers/cost_reports_controller.rb
  4. 191
      redmine_reporting/app/helpers/reporting_helper.rb
  5. 106
      redmine_reporting/app/models/cost_query.rb
  6. 272
      redmine_reporting/app/models/cost_query/chainable.rb
  7. 79
      redmine_reporting/app/models/cost_query/custom_field_mixin.rb
  8. 40
      redmine_reporting/app/models/cost_query/filter.rb
  9. 7
      redmine_reporting/app/models/cost_query/filter/activity_id.rb
  10. 10
      redmine_reporting/app/models/cost_query/filter/assigned_to_id.rb
  11. 9
      redmine_reporting/app/models/cost_query/filter/author_id.rb
  12. 115
      redmine_reporting/app/models/cost_query/filter/base.rb
  13. 11
      redmine_reporting/app/models/cost_query/filter/category_id.rb
  14. 9
      redmine_reporting/app/models/cost_query/filter/cost_object_id.rb
  15. 17
      redmine_reporting/app/models/cost_query/filter/cost_type_id.rb
  16. 5
      redmine_reporting/app/models/cost_query/filter/created_on.rb
  17. 25
      redmine_reporting/app/models/cost_query/filter/custom_field.rb
  18. 6
      redmine_reporting/app/models/cost_query/filter/due_date.rb
  19. 11
      redmine_reporting/app/models/cost_query/filter/fixed_version_id.rb
  20. 8
      redmine_reporting/app/models/cost_query/filter/issue_id.rb
  21. 7
      redmine_reporting/app/models/cost_query/filter/no_filter.rb
  22. 11
      redmine_reporting/app/models/cost_query/filter/overridden_costs.rb
  23. 34
      redmine_reporting/app/models/cost_query/filter/permission_filter.rb
  24. 9
      redmine_reporting/app/models/cost_query/filter/priority_id.rb
  25. 26
      redmine_reporting/app/models/cost_query/filter/project_id.rb
  26. 4
      redmine_reporting/app/models/cost_query/filter/spent_on.rb
  27. 6
      redmine_reporting/app/models/cost_query/filter/start_date.rb
  28. 10
      redmine_reporting/app/models/cost_query/filter/status_id.rb
  29. 6
      redmine_reporting/app/models/cost_query/filter/subject.rb
  30. 8
      redmine_reporting/app/models/cost_query/filter/tmonth.rb
  31. 9
      redmine_reporting/app/models/cost_query/filter/tracker_id.rb
  32. 8
      redmine_reporting/app/models/cost_query/filter/tweek.rb
  33. 8
      redmine_reporting/app/models/cost_query/filter/tyear.rb
  34. 5
      redmine_reporting/app/models/cost_query/filter/updated_on.rb
  35. 12
      redmine_reporting/app/models/cost_query/filter/user_id.rb
  36. 32
      redmine_reporting/app/models/cost_query/group_by.rb
  37. 5
      redmine_reporting/app/models/cost_query/group_by/activity_id.rb
  38. 63
      redmine_reporting/app/models/cost_query/group_by/base.rb
  39. 7
      redmine_reporting/app/models/cost_query/group_by/cost_object_id.rb
  40. 5
      redmine_reporting/app/models/cost_query/group_by/cost_type_id.rb
  41. 6
      redmine_reporting/app/models/cost_query/group_by/custom_field.rb
  42. 7
      redmine_reporting/app/models/cost_query/group_by/fixed_version_id.rb
  43. 5
      redmine_reporting/app/models/cost_query/group_by/issue_id.rb
  44. 7
      redmine_reporting/app/models/cost_query/group_by/priority_id.rb
  45. 5
      redmine_reporting/app/models/cost_query/group_by/project_id.rb
  46. 13
      redmine_reporting/app/models/cost_query/group_by/ruby_aggregation.rb
  47. 5
      redmine_reporting/app/models/cost_query/group_by/singleton_value.rb
  48. 5
      redmine_reporting/app/models/cost_query/group_by/spent_on.rb
  49. 19
      redmine_reporting/app/models/cost_query/group_by/sql_aggregation.rb
  50. 5
      redmine_reporting/app/models/cost_query/group_by/tmonth.rb
  51. 7
      redmine_reporting/app/models/cost_query/group_by/tracker_id.rb
  52. 5
      redmine_reporting/app/models/cost_query/group_by/tweek.rb
  53. 5
      redmine_reporting/app/models/cost_query/group_by/tyear.rb
  54. 5
      redmine_reporting/app/models/cost_query/group_by/user_id.rb
  55. 5
      redmine_reporting/app/models/cost_query/group_by/week.rb
  56. 62
      redmine_reporting/app/models/cost_query/inherited_attribute.rb
  57. 322
      redmine_reporting/app/models/cost_query/operator.rb
  58. 219
      redmine_reporting/app/models/cost_query/query_utils.rb
  59. 276
      redmine_reporting/app/models/cost_query/result.rb
  60. 304
      redmine_reporting/app/models/cost_query/sql_statement.rb
  61. 86
      redmine_reporting/app/models/cost_query/table.rb
  62. 46
      redmine_reporting/app/models/cost_query/transformer.rb
  63. 43
      redmine_reporting/app/models/cost_query/validation.rb
  64. 17
      redmine_reporting/app/models/cost_query/validation/dates.rb
  65. 17
      redmine_reporting/app/models/cost_query/validation/integers.rb
  66. 7
      redmine_reporting/app/models/cost_query/validation/sql.rb
  67. 95
      redmine_reporting/app/models/cost_query/walker.rb
  68. 80
      redmine_reporting/app/models/entry.rb
  69. 60
      redmine_reporting/app/views/cost_reports/_cost_entry_table.rhtml
  70. 102
      redmine_reporting/app/views/cost_reports/_cost_report_table.rhtml
  71. 39
      redmine_reporting/app/views/cost_reports/_filters.rhtml
  72. 38
      redmine_reporting/app/views/cost_reports/_group_by.rhtml
  73. 33
      redmine_reporting/app/views/cost_reports/_restore_query.rhtml
  74. 46
      redmine_reporting/app/views/cost_reports/_simple_cost_report_table.rhtml
  75. 13
      redmine_reporting/app/views/cost_reports/_sortable_init.rhtml
  76. 5
      redmine_reporting/app/views/cost_reports/available_values.rhtml
  77. 11
      redmine_reporting/app/views/cost_reports/filters/_activate_filter.rhtml
  78. 11
      redmine_reporting/app/views/cost_reports/filters/_available_value.rhtml
  79. 22
      redmine_reporting/app/views/cost_reports/filters/_date.rhtml
  80. 21
      redmine_reporting/app/views/cost_reports/filters/_multi_values.rhtml
  81. 25
      redmine_reporting/app/views/cost_reports/filters/_operators.rhtml
  82. 10
      redmine_reporting/app/views/cost_reports/filters/_remove_filter.rhtml
  83. 12
      redmine_reporting/app/views/cost_reports/filters/_text.rhtml
  84. 17
      redmine_reporting/app/views/cost_reports/filters/_text_box.rhtml
  85. 79
      redmine_reporting/app/views/cost_reports/index.rhtml
  86. 22
      redmine_reporting/app/views/hooks/_view_projects_show_sidebar_bottom_hook.rhtml
  87. BIN
      redmine_reporting/assets/images/arrow_B_down.gif
  88. BIN
      redmine_reporting/assets/images/arrow_B_left.gif
  89. BIN
      redmine_reporting/assets/images/arrow_B_right.gif
  90. BIN
      redmine_reporting/assets/images/arrow_B_up.gif
  91. BIN
      redmine_reporting/assets/images/arrow_D_down.gif
  92. BIN
      redmine_reporting/assets/images/arrow_D_left.gif
  93. BIN
      redmine_reporting/assets/images/arrow_D_right.gif
  94. BIN
      redmine_reporting/assets/images/arrow_D_up.gif
  95. BIN
      redmine_reporting/assets/images/arrow_both.png
  96. BIN
      redmine_reporting/assets/images/arrow_both_hover_left.png
  97. BIN
      redmine_reporting/assets/images/arrow_both_hover_right.png
  98. BIN
      redmine_reporting/assets/images/arrow_both_remove.png
  99. BIN
      redmine_reporting/assets/images/arrow_left.png
  100. BIN
      redmine_reporting/assets/images/arrow_left_hover.png
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,204 +0,0 @@
class CostReportsController < ApplicationController
before_filter :load_all
before_filter :find_optional_project, :only => [:index, :drill_down]
before_filter :generate_query, :only => [:index, :drill_down]
before_filter :set_cost_types, :only => [:index, :drill_down]
helper :reporting
include ReportingHelper
def index
@valid = valid_query?
if @valid
if @query.group_bys.empty?
@table_partial = "cost_entry_table"
elsif @query.depth_of(:column) + @query.depth_of(:row) == 1
@table_partial = "simple_cost_report_table"
else
if @query.depth_of(:column) == 0 || @query.depth_of(:row) == 0
@query.depth_of(:column) == 0 ? @query.column(:singleton_value) : @query.row(:singleton_value)
end
@table_partial = "cost_report_table"
end
end
respond_to do |format|
format.html { render :layout => !request.xhr? }
end
end
def drill_down
redirect_to :action => :index
end
def available_values
filter = filter_class(params[:filter_name].to_s)
render_404 unless filter
can_answer = filter.respond_to? :available_values
@available_values = filter.available_values
respond_to do |format|
format.html { can_answer ? render(:layout => !request.xhr?) : "" }
end
end
##
# Determines if the request contains filters to set
def set_filter? #FIXME: rename to set_query?
params[:set_filter].to_i == 1
end
##
# Determines if the request sets a unit type
def set_unit?
params[:unit]
end
##
# Find a query to search on and put it in the session
def filter_params
filters = http_filter_parameters if set_filter?
filters ||= session[:cost_query].try(:[], :filters)
filters ||= default_filter_parameters
end
def group_params
groups = http_group_parameters if set_filter?
groups ||= session[:cost_query].try(:[], :groups)
groups ||= default_group_parameters
end
##
# Extract active filters from the http params
def http_filter_parameters
params[:fields] ||= []
(params[:fields].reject { |f| f.empty? } || []).inject({:operators => {}, :values => {}}) do |hash, field|
hash[:operators][field.to_sym] = params[:operators][field]
hash[:values][field.to_sym] = params[:values][field]
hash
end
end
def http_group_parameters
if params[:groups]
rows = params[:groups][:rows].reject { |gb| gb.empty? }
columns = params[:groups][:columns].reject { |gb| gb.empty? }
end
{:rows => (rows || []), :columns => (columns || [])}
end
##
# Set a default query to cut down initial load time
def default_filter_parameters
{:operators => {:user_id => "=", :spent_on => ">d"},
:values => {:user_id => [User.current.id], :spent_on => [30.days.ago.strftime('%Y-%m-%d')]}
}.tap do |hash|
if @project
hash[:operators].merge! :project_id => "="
hash[:values].merge! :project_id => [@project.id]
end
end
end
##
# Set a default query to cut down initial load time
def default_group_parameters
{:columns => [:week], :rows => []}.tap do |h|
if @project
h[:rows] << :issue_id
else
h[:rows] << :project_id
end
end
end
def force_default?
params[:default].to_i == 1
end
##
# We apply a project filter, except when we are just applying a brand new query
def ensure_project_scope(filters)
return if set_filter? or set_unit?
if @project
filters[:operators].merge! :project_id => "="
filters[:values].merge! :project_id => @project.id.to_s
else
filters[:operators].delete :project_id
filters[:values].delete :project_id
end
end
##
# Build the query from the current request and save it to
# the session.
def generate_query
CostQuery::QueryUtils.cache.clear
filters = force_default? ? default_filter_parameters : filter_params
groups = force_default? ? default_group_parameters : group_params
ensure_project_scope filters
session[:cost_query] = {:filters => filters, :groups => groups}
@query = CostQuery.new
@query.tap do |q|
filters[:operators].each do |filter, operator|
q.filter(filter.to_sym,
:operator => operator,
:values => filters[:values][filter])
end
end
groups[:rows].reverse_each {|r| @query.row(r) }
groups[:columns].reverse_each {|c| @query.column(c) }
@query
end
def valid_query?
return true unless @query
errornous = @query.filters ? @query.filters.select { |f| !f.valid? } : []
@custom_errors = errornous.map do |err|
"Filter #{l(err.label)}: #{err.errors.join(", ")}"
end
errornous.empty?
end
##
# FIXME: Split, also ugly
# This method does three things:
# set the @unit_id -> this is used in the index for determining the active unit tab
# set the @cost_types -> this is used to determine which tabs to display
# possibly set the @cost_type -> this is used to select the proper units for display
def set_cost_types(value = nil)
@cost_types = session[:cost_query][:filters][:values][:cost_type_id].try(:collect, &:to_i) || (-1..CostType.count)
@unit_id = value || params[:unit].try(:to_i) || session[:unit_id].to_i
@unit_id = 0 unless @cost_types.include? @unit_id
session[:unit_id] = @unit_id
if @unit_id != 0
@query.filter :cost_type_id, :operator => '=', :value => @unit_id.to_s, :display => false
@cost_type = CostType.find(@unit_id) if @unit_id > 0
end
@available_cost_types = @cost_types.to_a
@available_cost_types.delete 0
@available_cost_types.unshift 0
@available_cost_types.map! do |id|
case id
when 0 then [0, l(:label_money)]
when -1 then [-1, l(:caption_labor)]
else [id, CostType.find(id).unit_plural ]
end
end
end
def load_all
CostQuery::GroupBy.all
CostQuery::Filter.all
end
private
## FIXME: Remove this once we moved to Redmine 1.0
def find_optional_project
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
allowed ? true : deny_access
rescue ActiveRecord::RecordNotFound
render_404
end
end

@ -1,191 +0,0 @@
module ReportingHelper
include QueriesHelper
def l(*values)
return values.first if values.size == 1 and values.first.respond_to? :to_str
super
end
##
# For a given CostQuery::Filter filter, return an array of hashes, that contain
# the partials that should be rendered (:name) for that filter and necessary
# parameters.
# @param [CostQuery::Filter] the filter we want to render
def html_elements(filter)
return text_elements filter if CostQuery::Operator.string_operators.all? { |o| filter.available_operators.include? o }
return date_elements filter if CostQuery::Operator.time_operators.all? { |o| filter.available_operators.include? o }
object_elements filter
end
def with_project(project)
project = Project.find(project) unless project.is_a? Project
project_was, @project = @project, project
yield
@project = project_was
end
def debug?
(!!params[:debug]) and !Rails.env.production?
end
def object_elements(filter)
[
{:name => :activate_filter, :filter_name => filter.underscore_name, :label => l(filter.label)},
{:name => :operators, :filter_name => filter.underscore_name, :operators => filter.available_operators},
{:name => :multi_values, :filter_name => filter.underscore_name},
{:name => :remove_filter, :filter_name => filter.underscore_name}]
end
def date_elements(filter)
[
{:name => :activate_filter, :filter_name => filter.underscore_name, :label => l(filter.label)},
{:name => :operators, :filter_name => filter.underscore_name, :operators => filter.available_operators},
{:name => :date, :filter_name => filter.underscore_name},
{:name => :remove_filter, :filter_name => filter.underscore_name}]
end
def text_elements(filter)
[
{:name => :activate_filter, :filter_name => filter.underscore_name, :label => l(filter.label)},
{:name => :operators, :filter_name => filter.underscore_name, :operators => filter.available_operators},
{:name => :text_box, :filter_name => filter.underscore_name},
{:name => :remove_filter, :filter_name => filter.underscore_name}]
end
def link_to_project(project)
link_to project.name, :controller => 'projects', :action => 'show', :id => project
end
def mapped(value, klass, default)
id = value.to_i
return default if id < 0
klass.find(id).name
end
def label_for(field)
name = field.to_s.camelcase
return l(field) unless CostQuery::Filter.const_defined? name
l(CostQuery::Filter.const_get(name).label)
end
def debug_fields(result, prefix = ", ")
prefix << result.fields.inspect << ", " << result.key.inspect if debug?
end
def show_field(key, value)
@show_row ||= Hash.new { |h,k| h[k] = {}}
@show_row[key][value] ||= field_representation_map(key, value)
end
def raw_field(key, value)
@raw_row ||= Hash.new { |h,k| h[k] = {}}
@raw_row[key][value] ||= field_sort_map(key, value)
end
def cost_object_link(cost_object_id)
co = CostObject.find(cost_object_id)
if User.current.allowed_to_with_inheritance?(:view_cost_objects, co.project)
link_to_cost_object(co)
else
co.subject
end
end
def field_representation_map(key, value)
return l(:label_none) if value.blank?
case key.to_sym
when :activity_id then mapped value, Enumeration, "<i>#{l(:caption_material_costs)}</i>"
when :project_id then link_to_project Project.find(value.to_i)
when :user_id, :assigned_to_id then link_to_user User.find(value.to_i)
when :tyear, :units then value
when :tweek then "#{l(:label_week)} ##{value}"
when :tmonth then month_name(value.to_i)
when :category_id then IssueCategory.find(value.to_i).name
when :cost_type_id then mapped value, CostType, l(:caption_labor)
when :cost_object_id then cost_object_link value
when :issue_id then link_to_issue Issue.find(value.to_i)
when :spent_on then format_date(value.to_date)
when :tracker_id then Tracker.find(value.to_i)
when :week then "#{l(:label_week)} #%s" % value.to_i.modulo(100)
when :priority_id then IssuePriority.find(value.to_i).name
when :fixed_version_id then Version.find(value.to_i).name
when :singleton_value then ""
else value.to_s
end
end
def field_sort_map(key, value)
return "" if value.blank?
case key.to_sym
when :issue_id, :tweek, :tmonth, :week then value.to_i
when :spent_on then value.to_date.mjd
else h(field_representation_map(key, value).gsub(/<\/?[^>]*>/, ""))
end
end
def show_result(row, unit_id = @unit_id)
case unit_id
when -1 then l_hours(row.units)
when 0 then row.real_costs ? number_to_currency(row.real_costs) : '-'
else
cost_type = @cost_type || CostType.find(unit_id)
"#{row.units} #{row.units != 1 ? cost_type.unit_plural : cost_type.unit}"
end
end
def set_filter_options(struct, key, value)
struct[:operators][key] = "="
struct[:values][key] = value.to_s
end
def link_to_details(result)
return '' # unless result.respond_to? :fields # uncomment to display
session_filter = {:operators => session[:cost_query][:filters][:operators].dup, :values => session[:cost_query][:filters][:values].dup }
filters = result.fields.inject session_filter do |struct, (key, value)|
key = key.to_sym
case key
when :week
set_filter_options struct, :tweek, value.to_i.modulo(100)
set_filter_options struct, :tyear, value.to_i / 100
when :month, :year
set_filter_options struct, :"t#{key}", value
when :count, :units, :costs, :display_costs, :sum, :real_costs
else
set_filter_options struct, key, value
end
struct
end
options = { :fields => filters[:operators].keys, :set_filter => 1, :action => :drill_down }
link_to '[+]', filters.merge(options), :class => 'drill_down', :title => l(:description_drill_down)
end
##
# Create the appropriate action for an entry with the type of log to use
def action_for(result, options = {})
options.merge :controller => result.fields['type'] == 'TimeEntry' ? 'timelog' : 'costlog', :id => result.fields['id'].to_i
end
##
# Create the appropriate action for an entry with the type of log to use
def entry_for(result)
type = result.fields['type'] == 'TimeEntry' ? TimeEntry : CostEntry
type.find(result.fields['id'].to_i)
end
##
# For a given row, determine how to render it's contents according to usability and
# localization rules
def show_row(row)
link_to_details(row) << row.render { |k,v| show_field(k,v) }
end
##
# Finds the Filter-Class for as specific filter name while being careful with the filter_name parameter as it is user input.
def filter_class(filter_name)
klass = CostQuery::Filter.const_get(filter_name.to_s.camelize)
return klass if klass.is_a? Class
nil
rescue NameError
return nil
end
end

@ -1,106 +0,0 @@
require_dependency "entry"
require 'forwardable'
class CostQuery < ActiveRecord::Base
extend Forwardable
include Enumerable
#belongs_to :user
#belongs_to :project
#attr_protected :user_id, :project_id, :created_at, :updated_at
def self.accepted_properties
@accepted_properties ||= []
end
def self.chain_initializer
return @chain_initializer ||= []
end
def available_filters
CostQuery::Filter.all
end
def transformer
@transformer ||= CostQuery::Transformer.new self
end
def walker
@walker ||= CostQuery::Walker.new self
end
def add_chain(type, name, options)
chain type.const_get(name.to_s.camelcase), options
@transformer, @table, @depths, @walker = nil, nil, nil, nil
self
end
def chain(klass = nil, options = {})
build_new_chain unless @chain
@chain = klass.new @chain, options if klass
@chain = @chain.parent until @chain.top?
@chain
end
def build_new_chain
#FIXME: is there a better way to load all filter and groups?
Filter.all && GroupBy.all
minimal_chain!
self.class.chain_initializer.each { |block| block.call self }
end
def filter(name, options = {})
add_chain Filter, name, options
end
def group_by(name, options = {})
add_chain GroupBy, name, options.reverse_merge(:type => :column)
end
def column(name, options = {})
group_by name, options.merge(:type => :column)
end
def row(name, options = {})
group_by name, options.merge(:type => :row)
end
def table
@table = Table.new(self)
end
def group_bys
chain.select { |c| c.group_by? }
end
def filters
chain.select { |c| c.filter? }
end
def depth_of(name)
@depths ||= {}
@depths[name] ||= chain.inject(0) { |sum, child| child.type == name ? sum + 1 : sum }
end
def_delegators :transformer, :column_first, :row_first
def_delegators :chain, :empty_chain, :top, :bottom, :chain_collect, :sql_statement, :all_group_fields, :child, :clear, :result
def_delegators :result, :each_direct_result, :recursive_each, :recursive_each_with_level, :each, :each_row, :count,
:units, :real_costs, :size, :final_number
def_delegators :table, :row_index, :colum_index
def to_a
chain.to_a
end
def to_s
chain.to_s
end
private
def minimal_chain!
@chain = Filter::NoFilter.new
end
end

@ -1,272 +0,0 @@
# Proviedes convinience layer and logic shared between GroupBy::Base and Filter::Base.
# Implements a dubble linked list (FIXME: is that the correct term?).
class CostQuery < ActiveRecord::Base
class Chainable
include Enumerable
include CostQuery::QueryUtils
extend CostQuery::InheritedAttribute
# this attr. should point to a symbol useable for translations
inherited_attribute :applies_for, :default => :label_cost_entry_attributes
def self.accepts_property(*list)
CostQuery.accepted_properties.push(*list.map(&:to_s))
end
def self.chain_list(*list)
options = list.extract_options!
options[:list] = true
list << options
inherited_attribute(*list)
end
def self.base?
superclass == Chainable or self == Chainable
end
def self.base
return self if base?
super
end
def self.from_base(&block)
base.instance_eval(&block)
end
def self.available
from_base { @available ||= [] }
end
def self.register(label)
available << klass
set_inherited_attribute "label", label
end
def self.table_joins
@table_joins ||= []
end
def self.table_from(value)
return value.table_name if value.respond_to? :table_name
return value unless value.respond_to? :to_ary or value.respond_to? :to_hash
table_from value.to_a.first
end
def self.join_table(*args)
@last_table = table_from(args.last)
table_joins << args
end
def self.underscore_name
name.demodulize.underscore
end
##
# The given block is called when a new chain is created for a cost_query.
# The query will be given to the block as a parameter.
# Example:
# initialize_query_with { |query| query.filter CostQuery::Filter::City, :operators => '=', :values => 'Berlin, da great City' }
def self.initialize_query_with(&block)
CostQuery.chain_initializer.push block
end
inherited_attribute :label
inherited_attribute :properties, :list => true
class << self
alias inherited_attributes inherited_attribute
alias accepts_properties accepts_property
end
attr_accessor :parent, :child, :type
accepts_property :type
def each(&block)
yield self
child.try(:each, &block)
end
def row?
type == :row
end
def column?
type == :column
end
def group_by?
!filter?
end
def to_a
returning([to_hash]) { |a| a.unshift(*child.to_a) unless bottom? }
end
def top
return self if top?
parent.top
end
def top?
parent.nil?
end
def bottom?
child.nil?
end
def bottom
return self if bottom?
child.bottom
end
def initialize(child = nil, options = {})
@options = options
options.each do |key, value|
raise ArgumentError, "may not set #{key}" unless CostQuery.accepted_properties.include? key.to_s
send "#{key}=", value if value
end
self.child, child.parent = child, self if child
move_down until correct_position?
clear
end
def to_a
cached :compute_to_a
end
def compute_to_a
[[self.class.field, @options], *child.try(:to_a)].compact
end
def to_s
URI.escape to_a.map(&:join).join(',')
end
def move_down
reorder parent, child, self, child.child
end
##
# Reorder given elements of a doubly linked list to follow the lists order.
# Don't use this for evil. Assumes there are no elements inbetween, does
# not touch the first element's parent and the last element's child.
# Does not touch elements not part of the list.
#
# @param [Array] *list Part of the linked list
def reorder(*list)
list.each_with_index do |entry, index|
next_entry = list[index + 1]
entry.try(:child=, next_entry) if index < list.size - 1
next_entry.try(:parent=, entry)
end
end
def chain_collect(name, *args, &block)
top.subchain_collect(name, *args, &block)
end
# See #chain_collect
def subchain_collect(name, *args, &block)
subchain = child.subchain_collect(name, *args, &block) unless bottom?
[* send(name, *args, &block) ].push(*subchain).compact.uniq
end
# overwrite in subclass to maintain constisten state
# ie automatically turning
# FilterFoo.new(GroupByFoo.new(FilterBar.new))
# into
# GroupByFoo.new(FilterFoo.new(FilterBar.new))
# Returning false will make the
def correct_position?
true
end
def clear
@cached = nil
child.try :clear
end
def result
cached(:compute_result)
end
def compute_result
Result.new ActiveRecord::Base.connection.select_all(sql_statement.to_s), {}, type
end
def table_joins
self.class.table_joins
end
def cached(*args)
@cached ||= {}
@cached[args] ||= send(*args)
end
def sql_statement
raise "should not get here (#{inspect})" if bottom?
child.cached(:sql_statement).tap do |q|
chain_collect(:table_joins).each { |args| q.join(*args) } if responsible_for_sql?
end
end
inherited_attribute :db_field
def self.field
db_field || (name[/[^:]+$/] || name).to_s.underscore
end
inherited_attribute :display, :default => true
def self.display!
display true
end
def self.display?
!!display
end
def self.dont_display!
display false
not_selectable!
end
inherited_attribute :selectable, :default => true
def self.selectable!
selectable true
end
def self.selectable?
!!selectable
end
def self.not_selectable!
selectable false
end
def self.last_table
@last_table ||= 'entries'
end
def self.table_name(value = nil)
@table_name = table_name_for(value) if value
@table_name || last_table
end
def display?
self.class.display?
end
def table_name
self.class.table_name
end
def with_table(fields)
fields.map { |f| field_name_for f, self }
end
def field
self.class.field
end
end
end

@ -1,79 +0,0 @@
module CostQuery::CustomFieldMixin
include CostQuery::QueryUtils
attr_reader :custom_field
SQL_TYPES = {
'string' => mysql? ? 'char' : 'varchar',
'list' => mysql? ? 'char' : 'varchar',
'text' => 'text', 'date' => 'date',
'int' => 'decimal(60,3)', 'float' => 'decimal(60,3)',
'bool' => 'boolean' }
def self.extended(base)
base.inherited_attribute :factory
base.factory = base
super
end
def all
@all ||= generate_subclasses
end
def generate_subclasses
IssueCustomField.all.map do |field|
class_name = class_name_for field.name
parent.send(:remove_const, class_name) if parent.const_defined? class_name
parent.const_set class_name, Class.new(self).prepare(field, class_name)
end
end
def factory?
factory == self
end
def on_prepare(&block)
return factory.on_prepare unless factory?
@on_prepare = block if block
@on_prepare ||= proc { }
@on_prepare
end
def table_name
@class_name.demodulize.underscore.tableize.singularize
end
def prepare(field, class_name)
@custom_field = field
label field.name
@class_name = class_name
dont_inherit :group_fields
db_field table_name
join_table (<<-SQL % [CustomValue.table_name, table_name, field.id, field.name, SQL_TYPES[field.field_format]]).gsub(/^ /, "")
-- BEGIN Custom Field Join: "%4$s"
LEFT OUTER JOIN (
\tSELECT
\t\tCAST(value AS %5$s) AS %2$s,
\t\tcustomized_type,
\t\tcustom_field_id,
\t\tcustomized_id
\tFROM
\t\t%1$s)
AS %2$s
ON %2$s.customized_type = 'Issue'
AND %2$s.custom_field_id = %3$d
AND %2$s.customized_id = entries.issue_id
-- END Custom Field Join: "%4$s"
SQL
instance_eval(&on_prepare)
self
end
def new(*)
fail "Only subclasses of #{self} should be instanciated." if factory?
super
end
def class_name_for(field)
"CustomField" << field.split(/[ \-_]/).map { |part| part.gsub(/\W/, '').capitalize }.join
end
end

@ -1,40 +0,0 @@
require "set"
module CostQuery::Filter
def self.all
@all ||= Set[
CostQuery::Filter::ActivityId,
CostQuery::Filter::AssignedToId,
CostQuery::Filter::AuthorId,
CostQuery::Filter::CategoryId,
CostQuery::Filter::CostTypeId,
CostQuery::Filter::CreatedOn,
CostQuery::Filter::DueDate,
CostQuery::Filter::FixedVersionId,
CostQuery::Filter::IssueId,
CostQuery::Filter::OverriddenCosts,
CostQuery::Filter::PriorityId,
CostQuery::Filter::ProjectId,
CostQuery::Filter::SpentOn,
CostQuery::Filter::StartDate,
CostQuery::Filter::StatusId,
CostQuery::Filter::Subject,
CostQuery::Filter::TrackerId,
#CostQuery::Filter::Tweek,
#CostQuery::Filter::Tmonth,
#CostQuery::Filter::Tyear,
CostQuery::Filter::UpdatedOn,
CostQuery::Filter::UserId,
CostQuery::Filter::PermissionFilter,
*CostQuery::Filter::CustomField.all
]
end
def self.all_grouped
all.group_by { |f| f.applies_for }.to_a.sort { |a,b| a.first.to_s <=> b.first.to_s }
end
def self.from_hash
raise NotImplementedError
end
end

@ -1,7 +0,0 @@
class CostQuery::Filter::ActivityId < CostQuery::Filter::Base
label :field_activity
def self.available_values(*)
TimeEntryActivity.find(:all, :order => 'name').map { |a| [a.name, a.id] }
end
end

@ -1,10 +0,0 @@
class CostQuery::Filter::AssignedToId < CostQuery::Filter::Base
use :null_operators
join_table Issue
applies_for :label_issue_attributes
label :field_assigned_to
def self.available_values(*)
CostQuery::Filter::UserId.available_values
end
end

@ -1,9 +0,0 @@
class CostQuery::Filter::AuthorId < CostQuery::Filter::Base
join_table Issue
applies_for :label_issue_attributes
label :field_author
def self.available_values(*)
CostQuery::Filter::UserId.available_values
end
end

@ -1,115 +0,0 @@
module CostQuery::Filter
class Base < CostQuery::Chainable
CostQuery::Operator.load
inherited_attribute :available_operators,
:list => true, :map => :to_operator,
:uniq => true
inherited_attribute :default_operator, :map => :to_operator
accepts_property :values, :value, :operator
mattr_accessor :skip_inherited_operators
self.skip_inherited_operators = [:time_operators, "y", "n"]
attr_accessor :values
def value=(val)
self.values = [val]
end
def self.use(*names)
operators = []
names.each do |name|
dont_inherit :available_operators if skip_inherited_operators.include? name
case name
when String, CostQuery::Operator then operators << name.to_operator
when Symbol then operators.push(*CostQuery::Operator.send(name))
else fail "dunno what to do with #{name.inspect}"
end
end
available_operators *operators
end
use :default_operators
def self.new(*args, &block) # :nodoc:
# this class is abstract. instances are only allowed from child classes
raise "#{self.name} is an abstract class" if base?
super
end
def self.inherited(klass)
if base?
self.dont_display!
klass.display!
end
super
end
def self.available_values(user)
raise NotImplementedError, "subclass responsibility"
end
def correct_position?
child.nil? or child.is_a? CostQuery::Filter::Base
end
def from_for(scope)
super + self.class.table_joins
end
def filter?
true
end
def valid?
@operator ? @operator.validate(values) : true
end
def errors
@operator ? @operator.errors : []
end
def group_by_fields
[]
end
def initialze(child = nil, options = {})
raise ArgumentError, "Child has to be a Filter." if child and not child.filter?
@values = []
super
end
def might_be_responsible
parent
end
def operator
(@operator || self.class.default_operator || CostQuery::Operator.default_operator).to_operator
end
def operator=(value)
@operator = value.to_operator.tap do |o|
raise ArgumentError, "#{o.inspect} not supported by #{inspect}." unless available_operators.include? o
end
end
def responsible_for_sql?
top?
end
def to_hash
raise NotImplementedError
end
def sql_statement
super.tap do |query|
arity = operator.arity
values = self.values || []
values = values[0, arity] if values and arity >= 0 and arity != values.size
operator.modify(query, field, *values) unless field.empty?
end
end
end
end

@ -1,11 +0,0 @@
class CostQuery::Filter::CategoryId < CostQuery::Filter::Base
use :null_operators
join_table Issue
applies_for :label_issue_attributes
label :field_category
def self.available_values(*)
categories = IssueCategory.find :all, :conditions => {:project_id => Project.visible.map{|p| p.id}}
categories.map { |c| ["#{c.project.name} - #{c.name} ", c.id] }.sort_by { |a| a.first.to_s + a.second.to_s }
end
end

@ -1,9 +0,0 @@
class CostQuery::Filter::CostObjectId < CostQuery::Filter::Base
join_table Project
label :field_cost_object
applies_for :label_issue_attributes
def self.available_values(*)
([[l(:caption_labor), -1]] + CostObject.find(:all, :order => 'name').map { |t| [t.name, t.id] })
end
end

@ -1,17 +0,0 @@
class CostQuery::Filter::CostTypeId < CostQuery::Filter::Base
label :field_cost_type
def initialize(child = nil, options = {})
@display = options.delete(:display)
super
end
def display?
return super if @display.nil?
@display
end
def self.available_values(*)
([[l(:caption_labor), -1]] + CostType.find(:all, :order => 'name').map { |t| [t.name, t.id] })
end
end

@ -1,5 +0,0 @@
class CostQuery::Filter::CreatedOn < CostQuery::Filter::Base
db_field "entries.created_on"
use :time_operators
label :field_created_on
end

@ -1,25 +0,0 @@
module CostQuery::Filter
class CustomField < Base
extend CostQuery::CustomFieldMixin
on_prepare do
applies_for :label_issue_attributes
# redmine internals just suck
case custom_field.field_format
when 'string', 'text' then use :string_operators
when 'list' then use :null_operators
when 'date' then use :time_operators
when 'int', 'float' then use :integer_operators
when 'bool'
@possible_values = [['true', 't'], ['false', 'f']]
use :null_operators
else
fail "cannot handle #{custom_field.field_format.inspect}"
end
end
def self.available_values(*)
@possible_values || custom_field.possible_values
end
end
end

@ -1,6 +0,0 @@
class CostQuery::Filter::DueDate < CostQuery::Filter::Base
use :time_operators
join_table Issue
applies_for :label_issue_attributes
label :field_due_date
end

@ -1,11 +0,0 @@
class CostQuery::Filter::FixedVersionId < CostQuery::Filter::Base
use :null_operators
join_table Issue
applies_for :label_issue_attributes
label :field_fixed_version
def self.available_values(*)
versions = Version.find :all, :conditions => {:project_id => Project.visible.map{|p| p.id}}
versions.map { |a| ["#{a.project.name} - #{a.name}", a.id] }.sort_by { |a| a.first.to_s + a.second.to_s }
end
end

@ -1,8 +0,0 @@
class CostQuery::Filter::IssueId < CostQuery::Filter::Base
label :field_issue
def self.available_values(*)
issues = Project.visible.collect { |p| p.issues }.flatten.uniq.sort_by { |i| i.id }
issues.map { |i| ["##{i.id} #{i.subject.length>30 ? i.subject.first(26)+'...': i.subject}", i.id] }
end
end

@ -1,7 +0,0 @@
class CostQuery::Filter::NoFilter < CostQuery::Filter::Base
dont_display!
def sql_statement
CostQuery::SqlStatement.for_entries
end
end

@ -1,11 +0,0 @@
class CostQuery::Filter::OverriddenCosts < CostQuery::Filter::Base
label :field_overridden_costs
def self.available_operators
['y', 'n'].map { |s| s.to_operator }
end
def self.available_values(*)
[]
end
end

@ -1,34 +0,0 @@
class CostQuery::Filter::PermissionFilter < CostQuery::Filter::Base
dont_display!
not_selectable!
db_field ""
initialize_query_with { |query| query.filter self.to_s.demodulize.to_sym }
def permission_statement(permission)
User.current.allowed_for(permission).gsub(/(user|project)s?\.id/, '\1_id')
end
def permission_for(type)
"(#{permission_statement :"view_own_#{type}_entries"} " \
"OR #{permission_statement :"view_#{type}_entries"})"
end
def display_costs
"(#{permission_statement :view_hourly_rates} " \
"AND #{permission_statement :view_cost_rates}) " \
"OR " \
"(#{permission_statement :view_own_hourly_rate} " \
"AND type = 'TimeEntry')"
end
def sql_statement
super.tap do |query|
query.from.each_subselect do |sub|
sub.where permission_for(sub == query.from.first ? 'time' : 'cost')
sub.select.delete_if { |f| f.end_with? "display_costs" }
sub.select :display_costs => switch(display_costs => '1', :else => 0)
end
end
end
end

@ -1,9 +0,0 @@
class CostQuery::Filter::PriorityId < CostQuery::Filter::Base
join_table Issue
applies_for :label_issue_attributes
label :field_priority
def self.available_values(*)
IssuePriority.find(:all, :order => 'position DESC').map { |i| [i.name, i.id] }
end
end

@ -1,26 +0,0 @@
class CostQuery::Filter::ProjectId < CostQuery::Filter::Base
db_field "entries.project_id"
label :field_project
def self.available_operators
["=", "!", "=_child_projects", "!_child_projects"].map { |s| s.to_operator }
end
##
# Calculates the available values for this filter.
# Gives a map of [project_name, project_id, nesting_level_of_project].
# The map is sorted such that projects appear in alphabetical order within a nesting level
# and so that descendant projects appear after their ancestors.
def self.available_values(*)
map = []
ancestors = []
Project.visible.sort_by(&:lft).each do |project|
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
end
map << [project.name, project.id, ancestors.size]
ancestors << project
end
map
end
end

@ -1,4 +0,0 @@
class CostQuery::Filter::SpentOn < CostQuery::Filter::Base
use :time_operators
label :label_spent_on_reporting
end

@ -1,6 +0,0 @@
class CostQuery::Filter::StartDate < CostQuery::Filter::Base
use :time_operators
join_table Issue
applies_for :label_issue_attributes
label :field_start_date
end

@ -1,10 +0,0 @@
class CostQuery::Filter::StatusId < CostQuery::Filter::Base
available_operators 'c', 'o'
join_table Issue, IssueStatus => [Issue, :status]
applies_for :label_issue_attributes
label :field_status
def self.available_values(*)
IssueStatus.find(:all, :order => 'name').map { |i| [i.name, i.id] }
end
end

@ -1,6 +0,0 @@
class CostQuery::Filter::Subject < CostQuery::Filter::Base
use :string_operators
join_table Issue
applies_for :label_issue_attributes
label :field_subject
end

@ -1,8 +0,0 @@
class CostQuery::Filter::Tmonth < CostQuery::Filter::Base
use :integer_operators
label :label_month_reporting
def self.available_values(*)
1.upto(12).map {|i| [ ::I18n.t('date.month_names')[i], i ]}
end
end

@ -1,9 +0,0 @@
class CostQuery::Filter::TrackerId < CostQuery::Filter::Base
join_table Issue
applies_for :label_issue_attributes
label :field_tracker
def self.available_values(*)
Tracker.find(:all, :order => 'name').map { |i| [i.name, i.id] }
end
end

@ -1,8 +0,0 @@
class CostQuery::Filter::Tweek < CostQuery::Filter::Base
use :integer_operators
label :label_week_reporting
def self.available_values(*)
1.upto(53).map {|i| [ i.to_s, i ]}
end
end

@ -1,8 +0,0 @@
class CostQuery::Filter::Tyear < CostQuery::Filter::Base
use :integer_operators
label :label_year_reporting
def self.available_values(*)
1970.upto(Date.today.year).map {|i| [ i.to_s, i ]}.reverse
end
end

@ -1,5 +0,0 @@
class CostQuery::Filter::UpdatedOn < CostQuery::Filter::Base
db_field "entries.updated_on"
use :time_operators
label :field_updated_on
end

@ -1,12 +0,0 @@
class CostQuery::Filter::UserId < CostQuery::Filter::Base
label :field_user
def self.available_values(*)
users = Project.visible.collect {|p| p.users}.flatten.uniq.sort
values = users.map { |u| [u.name, u.id] }
values.delete_if { |u| (u.first.include? "Redmine Admin") || (u.first.include? "Anonymous")}
values.sort!
values.unshift ["<< #{l(:label_me)} >>", User.current.id.to_s] if User.current.logged?
values
end
end

@ -1,32 +0,0 @@
require "set"
module CostQuery::GroupBy
def self.all
@all ||= Set[
CostQuery::GroupBy::ActivityId,
CostQuery::GroupBy::CostObjectId,
CostQuery::GroupBy::CostTypeId,
CostQuery::GroupBy::FixedVersionId,
CostQuery::GroupBy::IssueId,
CostQuery::GroupBy::PriorityId,
CostQuery::GroupBy::ProjectId,
CostQuery::GroupBy::SpentOn,
CostQuery::GroupBy::SingletonValue,
CostQuery::GroupBy::Tmonth,
CostQuery::GroupBy::TrackerId,
#CostQuery::GroupBy::Tweek,
CostQuery::GroupBy::Tyear,
CostQuery::GroupBy::UserId,
CostQuery::GroupBy::Week,
*CostQuery::GroupBy::CustomField.all
]
end
def self.all_grouped
all.group_by { |f| f.applies_for }.to_a.sort { |a,b| a.first.to_s <=> b.first.to_s }
end
def self.from_hash
raise NotImplementedError
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class ActivityId < Base
label :field_activity
end
end

@ -1,63 +0,0 @@
module CostQuery::GroupBy
class Base < CostQuery::Chainable
inherited_attributes :group_fields, :list => true, :merge => false
def self.inherited(klass)
klass.group_fields klass.field
super
end
def correct_position?
type == :row or !child.is_a?(CostQuery::GroupBy::Base) or child.type == :column
end
def filter?
false
end
def sql_aggregation?
child.filter?
end
##
# @param [FalseClass, TrueClass] prefix Whether or not add a table prefix the field names
# @return [Array<String,Symbol>] List of group by fields corresponding to self and all parents'
def all_group_fields(prefix = true)
@all_group_fields ||= []
@all_group_fields[prefix ? 0 : 1] ||= begin
(parent ? parent.all_group_fields(prefix) : []) + (prefix ? with_table(group_fields) : group_fields)
end.uniq
end
def clear
@all_group_fields = nil
super
end
def aggregation_mixin
sql_aggregation? ? SqlAggregation : RubyAggregation
end
def initialize(child = nil, optios = {})
super
extend aggregation_mixin
end
def result
super
end
def compute_result
super.tap do |r|
r.type = type
r.important_fields = group_fields
end
end
def define_group(sql)
fields = all_group_fields
sql.group_by fields
sql.select fields
end
end
end

@ -1,7 +0,0 @@
module CostQuery::GroupBy
class CostObjectId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_cost_object
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class CostTypeId < Base
label :field_cost_type
end
end

@ -1,6 +0,0 @@
module CostQuery::GroupBy
class CustomField < Base
extend CostQuery::CustomFieldMixin
on_prepare { group_fields table_name }
end
end

@ -1,7 +0,0 @@
module CostQuery::GroupBy
class FixedVersionId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_fixed_version
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class IssueId < Base
label :field_issue
end
end

@ -1,7 +0,0 @@
module CostQuery::GroupBy
class PriorityId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_priority
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class ProjectId < Base
label :field_project
end
end

@ -1,13 +0,0 @@
module CostQuery::GroupBy
module RubyAggregation
def responsible_for_sql?
false
end
##
# @return [CostQuery::Result] aggregation
def compute_result
child.result.grouped_by(all_group_fields(false), type, group_fields)
end
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class SingletonValue < Base
dont_display!
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class SpentOn < Base
label :label_spent_on_reporting
end
end

@ -1,19 +0,0 @@
module CostQuery::GroupBy
module SqlAggregation
def responsible_for_sql?
true
end
def compute_result
super.tap { |r| r.important_fields = group_fields }.grouped_by(all_group_fields(false), type, group_fields)
end
def sql_statement
super.tap do |sql|
define_group sql
sql.sum :units => :units, :real_costs => :real_costs, :display_costs => :display_costs
sql.count
end
end
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class Tmonth < Base
label :label_month_reporting
end
end

@ -1,7 +0,0 @@
module CostQuery::GroupBy
class TrackerId < Base
join_table Issue
applies_for :label_issue_attributes
label :field_tracker
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class Tweek < Base
label :label_week_reporting
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class Tyear < Base
label :label_year_reporting
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class UserId < Base
label :field_user
end
end

@ -1,5 +0,0 @@
module CostQuery::GroupBy
class Week < Base
label :label_week_reporting
end
end

@ -1,62 +0,0 @@
require 'set'
module CostQuery::InheritedAttribute
def inherited_attribute(*attributes)
options = attributes.extract_options!
list = options[:list]
merge = options.include?(:merge) ? options[:merge] : options[:list]
default = options[:default]
uniq = options[:uniq]
map = options[:map] || proc { |e| e }
default ||= [] if list
attributes.each do |name|
define_singleton_method(name) do |*values|
# FIXME: I'm ugly
return get_inherited_attribute(name, default, list, uniq) if values.empty?
if list
old = instance_variable_get("@#{name}") if merge
old ||= []
return set_inherited_attribute(name, values.map(&map) + old)
end
raise ArgumentError, "wrong number of arguments (#{values.size} for 1)" if values.size > 1
set_inherited_attribute name, map.call(values.first)
end
define_method(name) { |*values| self.class.send(name, *values) }
end
end
alias singleton_class metaclass unless respond_to? :singleton_class
def define_singleton_method(name, &block)
singleton_class.send :attr_writer, name
singleton_class.class_eval { define_method(name, &block) }
define_method(name) { instance_variable_get("@#{name}") or singleton_class.send(name) }
end
def get_inherited_attribute(name, default = nil, list = false, uniq = false)
return get_inherited_attribute(name, default, list, false).uniq if list and uniq
result = instance_variable_get("@#{name}")
super_result = superclass.get_inherited_attribute(name, default, list) if inherit? name
if result.nil?
super_result || default
else
list && super_result ? result + super_result : result
end
end
def inherit?(name)
superclass.respond_to? :get_inherited_attribute and not not_inherited.include? name
end
def not_inherited
@not_inherited ||= Set.new
end
def dont_inherit(*attributes)
not_inherited.merge attributes
end
def set_inherited_attribute(name, value)
instance_variable_set "@#{name}", value
end
end

@ -1,322 +0,0 @@
class CostQuery::Operator
include CostQuery::QueryUtils
include CostQuery::Validation
#############################################################################################
# Wrapped so we can place this at the top of the file.
def self.define_operators # :nodoc:
# Defaults
defaults do
def sql_operator
name
end
def where_clause
"%s %s '%s'"
end
def modify(query, field, *values)
query.where [where_clause, field, sql_operator, *values]
query
end
def label
@label ||= Query.operators[name]
end
end
# Operators from Redmine
new ">t-" do
include DateRange
def modify(query, field, value)
super query, field, -value.to_i, 0
end
end
new "w", :arity => 0 do
def modify(query, field, offset = nil)
offset ||= 0
from = Time.now.at_beginning_of_week - ((l(:general_first_day_of_week).to_i % 7) + 1).days
from -= offset.days
'<>d'.to_operator.modify query, field, from, from + 7.days
end
end
new "t+" do
include DateRange
def modify(query, field, *values)
super query, field, values.first.to_i, values.first.to_i
end
end
new "<="
new "!" do
def modify(query, field, *values)
where_clause = "(#{field} IS NULL"
where_clause += " OR #{field} NOT IN #{collection(*values)}" unless values.compact.empty?
where_clause += ")"
query.where where_clause
query
end
end
new "t-" do
include DateRange
def modify(query, field, *values)
super query, field, -values.first.to_i, -values.first.to_i
end
end
new "c", :arity => 0 do
def modify(query, field, *values)
raise "wrong field" if field.to_s.split('.').last != "status_id"
query.where "(#{IssueStatus.table_name}.is_closed = #{quoted_true})"
query
end
end
new "o", :arity => 0 do
def modify(query, field, *values)
raise "wrong field" if field.to_s.split('.').last != "status_id"
query.where "(#{IssueStatus.table_name}.is_closed = #{quoted_false})"
query
end
end
new "!~", :arity => 1 do
def modify(query, field, *values)
value = values.first || ''
query.where "LOWER(#{field}) NOT LIKE '%#{quote_string(value.to_s.downcase)}%'"
query
end
end
new "=" do
def modify(query, field, *values)
if values.compact.empty?
query.where "1=0"
else
query.where "#{field} IN #{collection(*values)}"
end
query
end
end
new "~", :arity => 1 do
def modify(query, field, *values)
value = values.first || ''
query.where "LOWER(#{field}) LIKE '%#{quote_string(value.to_s.downcase)}%'"
query
end
end
new "<t+" do
include DateRange
def modify(query, field, value)
super query, field, 0, value.to_i
end
end
new "t" do
include DateRange
def modify(query, field)
super query, field, 0, 0
end
end
new ">="
new "!*", :arity => 0, :where_clause => "%s IS NULL"
new "<t-" do
include DateRange
def modify(query, field, value)
super query, field, nil, -value.to_i
end
end
new ">t+" do
include DateRange
def modify(query, field, value)
super query, field, value.to_i, nil
end
end
new "*", :arity => 0, :where_clause => "%s IS NOT NULL"
# Our own operators
new "<", :label => :label_less
new ">", :label => :label_greater
new "=n", :label => :label_equals do
def modify(query, field, value)
query.where "#{field} = #{clean_currency(value)}"
query
end
end
new "0", :label => :label_none, :where_clause => "%s = 0"
new "y", :label => :label_yes, :arity => 0, :where_clause => "%s IS NOT NULL"
new "n", :label => :label_no, :arity => 0, :where_clause => "%s IS NULL"
new "<d", :label => :label_less_or_equal, :validate => :dates do
def modify(query, field, value)
return query if value.to_s.empty?
"<".to_operator.modify query, field, quoted_date(value)
end
end
new ">d", :label => :label_greater_or_equal, :validate => :dates do
def modify(query, field, value)
return query if value.to_s.empty?
">".to_operator.modify query, field, quoted_date(value)
end
end
new "<>d", :label => :label_between, :validate => :dates do
def modify(query, field, from, to)
return query if from.to_s.empty? || to.to_s.empty?
query.where "#{field} BETWEEN '#{quoted_date from}' AND '#{quoted_date to}'"
query
end
end
new "=d", :label => :label_date_on, :validate => :dates do
def modify(query, field, value)
return query if value.to_s.empty?
"=".to_operator.modify query, field, quoted_date(value)
end
end
new "=_child_projects", :validate => :integers, :label => :label_is_project_with_subprojects do
def modify(query, field, *values)
p_ids = []
values.each do |value|
p_ids += ([value] << Project.find(value).descendants.map{ |p| p.id })
end
"=".to_operator.modify query, field, p_ids
rescue ActiveRecord::RecordNotFound
query
end
end
new "!_child_projects", :validate => :integers, :label => :label_is_not_project_with_subprojects do
def modify(query, field, *values)
p_ids = []
values.each do |value|
p_ids += ([value] << Project.find(value).descendants.map{ |p| p.id })
end
"!".to_operator.modify query, field, p_ids
rescue ActiveRecord::RecordNotFound
query
end
end
end
#############################################################################################
module CoreExt
::String.send :include, self
::Symbol.send :include, self
def to_operator
CostQuery::Operator.find self
end
end
def self.new(name, values = {}, &block)
all[name.to_s] ||= super
end
def self.all
@all ||= {}
end
def self.load
return if @done
@done = true
define_operators
end
def self.find(name)
all[name.to_s] or raise ArgumentError, "Operator not defined"
end
def self.defaults(&block)
class_eval &block
end
def self.default_operator
find "="
end
def self.integer_operators
["<", ">", "<=", ">="].map { |s| s.to_operator}
end
def self.null_operators
["*", "!*"].map { |s| s.to_operator}
end
def self.string_operators
["!~", "~"].map { |s| s.to_operator}
end
def self.time_operators
#["t-", "t+", ">t-", "<t-", ">t+", "<t+"].map { |s| s.to_operator}
["t", "w", "<>d", ">d", "<d", "=d"].map { |s| s.to_operator}
end
def self.default_operators
["=", "!"].map { |s| s.to_operator}
end
attr_reader :name
def initialize(name, values = {}, &block)
@name = name.to_s
validation_methods = values.delete(:validate)
register_validations(validation_methods) unless validation_methods.nil?
values.each do |key, value|
metaclass.class_eval { define_method(key) { value } }
end
metaclass.class_eval(&block) if block
end
def to_operator
self
end
def to_s
name
end
def arity
@arity ||= begin
num = method(:modify).arity
# modify takes two more arguments before the values
num < 0 ? num + 2 : num - 2
end
end
def inspect
"#<#{self.class.name}:#{name.inspect}>"
end
def <=>(other)
self.name <=> other.name
end
module DateRange
def modify(query, field, from, to)
query.where ["#{field} > '%s'", quoted_date((Date.yesterday + from).to_time.end_of_day)] if from
query.where ["#{field} <= '%s'", quoted_date((Date.today + to).to_time.end_of_day)] if to
query
end
end
# Done with class method definition, let's initialize the operators
load
end

@ -1,219 +0,0 @@
module CostQuery::QueryUtils
include Redmine::I18n
delegate :quoted_false, :quoted_true, :to => "ActiveRecord::Base.connection"
##
# Graceful string quoting.
#
# @param [Object] str String to quote
# @return [Object] Quoted version
def quote_string(str)
return str unless str.respond_to? :to_str
ActiveRecord::Base.connection.quote_string(str)
end
##
# Graceful, internationalized quoted string.
#
# @see quote_string
# @param [Object] str String to quote/translate
# @return [Object] Quoted, translated version
def quoted_label(ident)
"'#{quote_string l(ident)}'"
end
##
# Creates a SQL fragment representing a collection/array.
#
# @see quote_string
# @param [#flatten] *values Ruby collection
# @return [String] SQL collection
def collection(*values)
"(#{values.flatten.map { |v| "'#{quote_string(v)}'" }.join ", "})"
end
def quoted_date(date)
ActiveRecord::Base.connection.quoted_date date.to_dateish
end
##
# SQL date quoting.
# @param [Date,Time] date Date to quote.
# @return [String] Quoted date.
def quote_date(date)
"'#{quoted_date date}'"
end
##
# Generate a table name for any object.
#
# @example Table names
# table_name_for Issue # => 'issues'
# table_name_for :issue # => 'issues'
# table_name_for "issue" # => 'issues'
# table_name_for "issues" # => 'issues
#
# @param [#table_name, #to_s] object Object you need the table name for.
# @return [String] The table name.
def table_name_for(object)
return object.table_name if object.respond_to? :table_name
object.to_s.tableize
end
##
# Generate a field name
#
# @example Field names
# field_name_for nil # => 'NULL'
# field_name_for 'foo' # => 'foo'
# field_name_for [Issue, 'project_id'] # => 'issues.project_id'
# field_name_for [:issue, 'project_id'], :entry # => 'issues.project_id'
# field_name_for 'project_id', :entry # => 'entries.project_id'
#
# @param [Array, Object] arg Object to generate field name for.
# @param [Object, optional] default_table Table name to use if no table name is given.
# @return [String] Field name.
def field_name_for(arg, default_table = nil)
return 'NULL' unless arg
return arg if arg.is_a? String and arg =~ /\.| |\(.*\)/
return table_name_for(arg.first || default_table) + '.' << arg.last.to_s if arg.is_a? Array and arg.size == 2
return arg.to_s unless default_table
field_name_for [default_table, arg]
end
##
# Sanitizes sql condition
#
# @see ActiveRecord::Base#sanitize_sql_for_conditions
# @param [Object] statement Not sanitized statement.
# @return [String] Sanitized statement.
def sanitize_sql_for_conditions(statement)
CostQuery.send :sanitize_sql_for_conditions, statement
end
##
# Generates string representation for a currency.
#
# @see CostRate.clean_currency
# @param [BigDecimal] value
# @return [String]
def clean_currency(value)
CostRate.clean_currency(value).to_f.to_s
end
##
# Generates a SQL case statement.
#
# @example
# switch "#{table}.overridden_costs IS NULL" => [model, :costs], :else => [model, :overridden_costs]
#
# @param [Hash] options Condition => Result.
# @return [String] Case statement.
def switch(options)
desc = "#{__method__} #{options.inspect[1..-2]}".gsub(/(Cost|Time)Entry\([^\)]*\)/, '\1Entry')
options = options.with_indifferent_access
else_part = options.delete :else
"-- #{desc}\n\t" \
"CASE #{options.map { |k,v| "\n\t\tWHEN #{field_name_for k}\n\t\t" \
"THEN #{field_name_for v}" }}\n\t\tELSE #{field_name_for else_part}\n\tEND"
end
def typed(type, value, escape = true)
value = "'#{quote_string value}'" if escape
return value unless postgresql?
"#{value}::#{type}"
end
def iso_year_week(field, default_table = nil)
field = field_name_for(field, default_table)
"-- code specific for #{adapter_name}\n\t" << \
case adapter_name
when :mysql
"yearweek(#{field}, 1)"
when :postgresql
"(EXTRACT(isoyear from #{field})*100 + \n\t\t" \
"EXTRACT(week from #{field} - \n\t\t" \
"(EXTRACT(dow FROM #{field})::int+6)%7))"
when :sqlite
# enjoy
<<-EOS
case
when strftime('%W', strftime('%Y-01-04', #{field})) = '00' then
-- 01/01 is in week 1 of the current year => %W == week - 1
case
when strftime('%W', #{field}) = '52' and strftime('%W', (strftime('%Y', #{field}) + 1) || '-01-04') = '00' then
-- we are at the end of the year, and it's the first week of the next year
(strftime('%Y', #{field}) + 1) || '01'
when strftime('%W', #{field}) < '08' then
-- we are in week 1 to 9
strftime('%Y0', #{field}) || (strftime('%W', #{field}) + 1)
else
-- we are in week 10 or later
strftime('%Y', #{field}) || (strftime('%W', #{field}) + 1)
end
else
-- 01/01 is in week 53 of the last year
case
when strftime('%W', #{field}) = '52' and strftime('%W', (strftime('%Y', #{field}) + 1) || '-01-01') = '00' then
-- we are at the end of the year, and it's the first week of the next year
(strftime('%Y', #{field}) + 1) || '01'
when strftime('%W', #{field}) = '00' then
-- we are in the week belonging to last year
(strftime('%Y', #{field}) - 1) || '53'
else
-- everything is fine
strftime('%Y%W', #{field})
end
end
EOS
else
fail "#{adapter_name} not supported"
end
end
def adapter_name
ActiveRecord::Base.connection.adapter_name.downcase.to_sym
end
def map_field(key, value)
case key.to_s
when "user_id" then value ? user_name(value.to_i) : ''
when "tweek", "tyear", "tmonth", /_id$/ then value.to_i
when "week" then value.to_i.divmod(100)
when /_(on|at)$/ then value ? Time.parse(value) : Time.at(0)
when /^custom_field/ then value.to_s
when "singleton_value" then value.to_i
else fail "add mapping for #{key}"
end
end
def user_name(id)
# we have no identity map... :(
cache[:user_name][id] ||= User.find(id).name
end
def cache
CostQuery::QueryUtils.cache
end
def mysql?
adapter_name == :mysql
end
def sqlite?
adapter_name == :sqlite
end
def postgresql?
adapter_name == :postgresql
end
def self.cache
@cache ||= Hash.new { |h,k| h[k] = {} }
end
def self.included(klass)
super
klass.extend self
end
end

@ -1,276 +0,0 @@
module CostQuery::Result
class Base
attr_accessor :parent, :type, :important_fields
attr_accessor :key
attr_reader :value
alias values value
include Enumerable
include CostQuery::QueryUtils
def initialize(value)
@important_fields ||= []
@type = :direct
@value = value
end
def recursive_each_with_level(level = 0, depth_first = true, &block)
block.call(level, self)
end
def recursive_each
recursive_each_with_level { |level, result| yield result }
end
def to_hash
fields.dup
end
def [](key)
fields[key]
end
def grouped_by(fields, type, important_fields = [])
@grouped_by ||= {}
list = begin
@grouped_by[fields] ||= begin
# sub results, have fields
# i.e. grouping by foo, bar
data = group_by do |entry|
# index for group is a hash
# i.e. { :foo => 10, :bar => 20 } <= this is just the KEY!!!!
fields.inject({}) { |hash, key| hash.merge key => entry.fields[key] }
end
# map group back to array, all fields with same key get grouped into one list
data.keys.map { |f| CostQuery::Result.new data[f], f, type, important_fields }
end
end
# create a single result from that list
CostQuery::Result.new list, {}, type, important_fields
end
def inspect
"<##{self.class}: @fields=#{fields.inspect} @type=#{type.inspect} " \
"@size=#{size} @count=#{count} @units=#{units} @real_costs=#{real_costs}>"
end
def row?
type == :row
end
def column?
type == :column
end
def direct?
type == :direct
end
def each_row
end
def final?(type)
type? type and (direct? or first.type != type)
end
def type?(type)
self.type == type
end
def depth_of(type)
if type? type or (type == :column and direct?) then 1
else 0
end
end
def final_number(type)
return 1 if final? type
return 0 if direct?
@final_number ||= {}
@final_number[type] ||= sum { |v| v.final_number type }
end
def final_row?
final? :row
end
def final_column?
final? :column
end
def render(keys = important_fields)
fields.map { |k,v| yield(k,v) if keys.include? k }.join
end
def set_key(index = [])
self.key = index.map { |k| map_field(k, fields[k]) }
end
def display_costs?
display_costs > 0
end
end
class DirectResult < Base
alias fields values
def has_children?
false
end
def display_costs
self["display_costs"].to_i
end
def count
self["count"].to_i
end
def units
self["units"].to_d
end
def real_costs
(self["real_costs"] || 0).to_d if display_costs? # FIXME: default value here?
end
##
# @return [Integer] Number of child results
def size
0
end
def each
return enum_for(__method__) unless block_given?
yield self
end
def each_direct_result(cached = false)
return enum_for(__method__) unless block_given?
yield self
end
def sort!(force = false)
force
end
end
class WrappedResult < Base
include Enumerable
def set_key(index = [])
values.each { |v| v.set_key index }
super
end
def sort!(force = false)
return false if @sorted and not force
values.sort! { |a,b| a.key <=> b.key }
values.each { |e| e.sort! force }
@sorted = true
end
def depth_of(type)
super + first.depth_of(type)
end
def has_children?
true
end
def count
sum_for :count
end
def display_costs
(sum_for :display_costs) >= 1 ? 1 : 0
end
def units
sum_for :units
end
def real_costs
sum_for :real_costs if display_costs?
end
def sum_for(field)
@sum_for ||= {}
@sum_for[field] ||= sum { |v| v.send(field) || 0 }
end
def recursive_each_with_level(level = 0, depth_first = true, &block)
if depth_first
super
each { |c| c.recursive_each_with_level(level + 1, depth_first, &block) }
else #width-first
to_evaluate = [self]
lvl = level
while !to_evaluate.empty? do
# evaluate all stored results and find the results we need to evaluate soon
to_evaluate_soon = []
to_evaluate.each do |r|
block.call(lvl,r)
to_evaluate_soon.concat r.values if r.size > 0
end
# take new results to evaluate
lvl = lvl +1
to_evaluate = to_evaluate_soon
end
end
def each_row
return enum_for(:each_row) unless block_given?
if final_row? then yield self
else each { |c| c.each_row(&Proc.new) }
end
end
end
def to_a
values
end
def each(&block)
values.each(&block)
end
def each_direct_result(cached = true)
return enum_for(__method__) unless block_given?
if @direct_results
@direct_results.each { |r| yield(r) }
else
values.each do |value|
value.each_direct_result(false) do |result|
(@direct_results ||= []) << result if cached
yield result
end
end
end
end
def fields
@fields ||= {}.with_indifferent_access
end
##
# @return [Integer] Number of child results
def size
values.size
end
end
def self.new(value, fields = {}, type = nil, important_fields = [])
result = begin
case value
when Array then WrappedResult.new value.map { |e| new e, {}, nil, important_fields }
when Hash then DirectResult.new value.with_indifferent_access
when Base then value
else raise ArgumentError, "Cannot create Result from #{value.inspect}"
end
end
result.fields.merge! fields
result.type = type if type
result.important_fields = important_fields unless result == value
result
end
end

@ -1,304 +0,0 @@
class CostQuery::SqlStatement
class Union
attr_accessor :first, :second, :as
def initialize(first, second, as = nil)
@first, @second, @as = first, second, as
end
def to_s
"((\n#{first.gsub("\n", "\n\t")}\n) UNION (\n" \
"#{second.gsub("\n", "\n\t")}\n))#{" AS #{as}" if as}\n"
end
def each_subselect
yield first
yield second
end
def gsub(*args, &block)
to_s.gsub(*args, &block)
end
end
include CostQuery::QueryUtils
attr_accessor :desc
COMMON_FIELDS = %w[
user_id project_id issue_id rate_id
comments spent_on created_on updated_on tyear tmonth tweek
costs overridden_costs type
]
##
# Generates new SqlStatement.
#
# @param [String, #to_s] table Table name (or subselect) for from part.
def initialize(table)
from table
end
##
# Generates SqlStatement that maps time_entries and cost_entries to a common structure.
#
# Mapping for direct fields:
#
# Result | Time Entires | Cost entries
# --------------------------|--------------------------|--------------------------
# id | id | id
# user_id | user_id | user_id
# project_id | project_id | project_id
# issue_id | issue_id | issue_id
# rate_id | rate_id | rate_id
# comments | comments | comments
# spent_on | spent_on | spent_on
# created_on | created_on | created_on
# updated_on | updated_on | updated_on
# tyear | tyear | tyear
# tmonth | tmonth | tmonth
# tweek | tweek | tweek
# costs | costs | costs
# overridden_costs | overridden_costs | overridden_costs
# units | hours | units
# activity_id | activity_id | -1
# cost_type_id | -1 | cost_type_id
# type | "TimeEntry" | "CostEntry"
# count | 1 | 1
#
# Also: This _should_ handle joining activities and cost_types, as the logic differs for time_entries
# and cost_entries.
#
# @param [#table_name] model The model to map
# @return [CostQuery::SqlStatement] Generated statement
def self.unified_entry(model)
table = table_name_for model
new(table).tap do |query|
query.select COMMON_FIELDS
query.desc = "Subquery for #{table}"
query.select({
:count => 1, :id => [model, :id], :display_costs => 1,
:real_costs => switch("#{table}.overridden_costs IS NULL" => [model, :costs], :else => [model, :overridden_costs]),
:week => iso_year_week(:spent_on, model),
:singleton_value => 1
})
#FIXME: build this subquery from a sql_statement
query.from "(SELECT *, #{typed :text, model.model_name} AS type FROM #{table}) AS #{table}"
send("unify_#{table}", query)
end
end
##
# Applies logic for mapping time entries to general entries structure.
#
# @param [CostQuery::SqlStatement] query The statement to adjust
def self.unify_time_entries(query)
query.select :activity_id, :units => :hours, :cost_type_id => -1
query.select :cost_type => quoted_label(:caption_labor)
end
##
# Applies logic for mapping cost entries to general entries structure.
#
# @param [CostQuery::SqlStatement] query The statement to adjust
def self.unify_cost_entries(query)
query.select :units, :cost_type_id, :activity_id => -1
query.select :cost_type => "cost_types.name"
query.join CostType
end
##
# Generates a statement based on all entries (i.e. time entries and cost entries) mapped to the general entries structure,
# and therefore usable by filters and such.
#
# @return [CostQuery::SqlStatement] Generated statement
def self.for_entries
new unified_entry(TimeEntry).union(unified_entry(CostEntry), "entries")
end
##
# Creates a uninon of the caller and the callee.
#
# @param [CostQuery::SqlStatement] other Second part of the union
# @return [String] The sql query.
def union(other, as = nil)
Union.new(self, other, as)
end
##
# Adds sum(..) part to select.
#
# @param [#to_s] field Name of the field to aggregate on
# @param [#to_s] name Name of the result (defaults to sum)
def sum(field, name = :sum, type = :sum)
@sql = nil
return sum({ name => field }, nil, type) unless field.respond_to? :to_hash
field.each { |k,v| field[k] = "#{type}(#{v})" }
select field
end
##
# Adds count(..) part to select.
#
# @param [#to_s] field Name of the field to aggregate on (defaults to *)
# @param [#to_s] name Name of the result (defaults to sum)
def count(field = "*", name = :count)
sum field, name, :count
end
##
# Generates the SQL query.
# Code looks ugly in exchange for pretty output (so one does unterstand those).
#
# @return [String] The query
def to_s
# FIXME I'm ugly
@sql ||= begin
sql = "\n-- BEGIN #{desc}\n" \
"SELECT\n#{select.map { |e| "\t#{e}" }.join ",\n"}" \
"\nFROM\n\t#{from.gsub("\n", "\n\t")}" \
"\n\t#{joins.map { |e| e.gsub("\n", "\n\t") }.join "\n\t"}" \
"\nWHERE #{where.join " AND "}\n"
sql << "GROUP BY #{group_by.join ', '}\nORDER BY #{group_by.join ', '}\n" if group_by?
sql << "-- END #{desc}\n"
sql.gsub!('--', '#') if mysql?
sql # << " LIMIT 100"
end
end
##
# @overload from
# Reads the from part.
# @return [#to_s] From part
# @overload from(table)
# Sets the from part.
# @param [#to_s] table
# @param [#to_s] From part
def from(table = nil)
return @from unless table
@sql = nil
@from = table
end
##
# Where conditions. Will be joined together by AND.
#
# @overload where
# Reads the where part
# @return [Array<#to_s>] Where clauses
# @overload where(fields)
# Adds condition to where clause
# @param [Array, Hash, String] fields Parameters passed to sanitize_sql_for_conditions.
# @see CostQuery::QueryUtils#sanitize_sql_for_conditions
def where(fields = nil)
@where ||= ["1=1"]
unless fields.nil?
@where << sanitize_sql_for_conditions(fields)
@sql = nil
end
@where
end
##
# @return [Array<String>] List of table joins
def joins
(@joins ||= []).tap { |j| j.uniq! }
end
##
# Adds an "left outer join" (guessing field names) to #joins.
#
# @overload join(name)
# @param [Symbol, String] name Singular table name to join with, will join plural from on table.id = table_id
# @overload join(model)
# @param [#table_name, #model_name] model ActiveRecord model to join with
# @overload join(hash)
# @param [Hash<#to_s => #to_s>] hash Key is singular table name to join with, value is field to join on
# @overload join(*list)
# @param [Array<String,Symbol,Array>] list Will generate join entries (according to guessings described above)
# @see #joins
def join(*list)
@sql = nil
join_syntax = "LEFT OUTER JOIN %1$s ON %1$s.id = %2$s_id"
list.each do |e|
case e
when Class then joins << (join_syntax % [table_name_for(e), e.model_name.underscore])
when / / then joins << e
when Symbol, String then joins << (join_syntax % [table_name_for(e), e])
when Hash then e.each { |k,v| joins << (join_syntax % [table_name_for(k), field_name_for(v)]) }
when Array then join(*e)
else raise ArgumentError, "cannot join #{e.inspect}"
end
end
end
##
# @overload select
# @return [Array<String>] All fields/statements for select part
#
# @overload select(*fields)
# Adds fields to select query.
# @example
# SqlStatement.new.select(some_sql_statement) # [some_sql_statement.to_s]
# SqlStatement.new.select("sum(foo)") # ["sum(foo)"]
# SqlStatement.new.select(:a).select(:b) # ["a", "b"]
# SqlStatement.new.select(:bar => :foo) # ["foo as bar"]
# SqlStatement.new.select(:bar => nil) # ["NULL as bar"]
# @param [Array, Hash, String, Symbol, SqlStatement] fields Fields to add to select part
# @return [Array<String>] All fields/statements for select part
def select(*fields)
return(@select || ["*"]) if fields.empty?
returning(@select ||= []) do
@sql = nil
fields.each do |f|
case f
when Array
if f.size == 2 and f.first.respond_to? :table_name then select field_name_for(f)
else select(*f)
end
when Hash then select f.map { |k,v| "#{field_name_for v} as #{field_name_for k}" }
when String, Symbol then @select << field_name_for(f)
when CostQuery::SqlStatement then @select << f.to_s
else raise ArgumentError, "cannot handle #{f.inspect}"
end
end
# when doing a union in sql, both subselects must have the same order.
# by sorting here we never ever have to worry about this again, sucker!
@select = @select.uniq.sort_by { |x| x.split(" as ").last }
end
end
##
# @overload group_by
# @return [Array<String>] All fields/statements for group by part
#
# @overload group(*fields)
# Adds fields to group by query
# @param [Array, String, Symbol] fields Fields to add
def group_by(*fields)
@sql = nil unless fields.empty?
returning(@group_by ||= []) do
fields.each do |e|
if e.is_a? Array and (e.size != 2 or !e.first.respond_to? :table_name)
group_by(*e)
else
@group_by << field_name_for(e)
end
end
@group_by.uniq!
end
end
##
# @return [TrueClass, FalseClass] Whether or not to add a group by part.
def group_by?
!group_by.empty?
end
def inspect
"#<SqlStatement: #{to_s.inspect}>"
end
def gsub(*args, &block)
to_s.gsub(*args, &block)
end
end

@ -1,86 +0,0 @@
# encoding: UTF-8
require 'enumerator'
class CostQuery::Table
attr_accessor :query
include CostQuery::QueryUtils
def initialize(query)
@query = query
end
def row_index
get_index :row
end
def column_index
get_index :column
end
def row_fields
fields_for :row
end
def column_fields
fields_for :column
end
def rows_for(result) fields_for result, :row end
def columns_for(result) fields_for result, :column end
def fields_from(result, type)
#fields_for(type).map { |k| result[k] }
fields_for(type).map { |k| map_field k, result.fields[k] }
end
##
# @param [Array] expected Fields expected
# @param [Array,Hash,Resul] given Fields/result to be tested
# @return [TrueClass,FalseClass]
def satisfies?(type, expected, given)
given = fields_from(given, type) if given.respond_to? :to_hash
zipped = expected.zip given
zipped.all? { |a,b| a == b or b.nil? }
end
def fields_for(type)
@fields_for ||= begin
child, fields = query.chain, Hash.new { |h,k| h[k] = [] }
until child.filter?
fields[child.type].push(*child.group_fields)
child = child.child
end
fields
end
@fields_for[type]
end
def get_row(*args)
@query.each_row { |result| return with_gaps_for(type, result) if satisfies? :row, args, result }
[]
end
def with_gaps_for(type, result)
return enum_for(:with_gaps_for, type, result) unless block_given?
stack = get_index(type).dup
result.each_direct_result do |subresult|
yield nil until stack.empty? or satisfies? type, stack.shift, subresult
yield subresult
end
stack.size.times { yield nil }
end
def [](x,y)
get_row(row_index[y]).first(x).last
end
def get_index(type)
@indexes ||= begin
indexes = Hash.new { |h,k| h[k] = Set.new }
query.each_direct_result { |result| [:row, :column].each { |t| indexes[t] << fields_from(result, t) } }
indexes.keys.each { |k| indexes[k] = indexes[k].sort { |x, y| x <=> y } }
indexes
end
@indexes[type]
end
end

@ -1,46 +0,0 @@
# encoding: UTF-8
class CostQuery::Transformer
attr_reader :query
def initialize(query)
@query = query
end
##
# @return [CostQuery::Result::Base] Result tree with row group bys at the top
# @see CostQuery::Chainable#result
def row_first
@row_first ||= query.result
end
##
# @return [CostQuery::Result::Base] Result tree with column group bys at the top
# @see CostQuery::Walker#row_first
def column_first
@column_first ||= begin
# reverse since we fake recursion ↓↓↓
list, all_fields = restructured.reverse, @all_fields.dup
result = list.inject(@ungrouped) do |aggregate, (current_fields, type)|
fields, all_fields = all_fields, all_fields - current_fields
aggregate.grouped_by fields, type, current_fields
end
result or query.result
end
end
##
# Important side effect: it sets @ungrouped, @all_fields.
# @return [Array<Array<Array<String,Symbol>, Symbol>>] Group by fields + types (:row or :column)
def restructured
rows, columns, current = [], [], query.chain
@all_fields = []
until current.filter?
@ungrouped = current.result if current.responsible_for_sql?
list = current.row? ? rows : columns
list << [current.group_fields, current.type]
@all_fields.push(*current.group_fields)
current = current.child
end
columns + rows
end
end

@ -1,43 +0,0 @@
module CostQuery::Validation
def register_validations(*validation_methods)
validation_methods.flatten.each do |val_method|
register_validation(val_method)
end
end
def register_validation(val_method)
const_name = val_method.to_s.camelize
begin
val_module = CostQuery::Validation.const_get const_name
metaclass.send(:include, val_module)
val_method = "validate_" + val_method.to_s.pluralize
if method(val_method)
validations << val_method
else
warn "#{val_module.name} does not define #{val_method}"
end
rescue NameError
warn "No Module CostQuery::Validation::#{const_name} found to validate #{val_method}"
end
self
end
def errors
@errors ||= []
@errors
end
def validations
@validations ||= []
@validations
end
def validate(*values)
errors.clear
return true if validations.empty?
validations.all? do |validation|
values.empty? ? true : send(validation, *values)
end
end
end

@ -1,17 +0,0 @@
module CostQuery::Validation
module Dates
def validate_dates(*values)
values = values.flatten
return true if values.empty?
values.flatten.all? do |val|
begin
!!val.to_dateish
rescue ArgumentError
errors << "\'#{val}\' " + l(:validation_failure_date)
validate_dates(values - [val])
false
end
end
end
end
end

@ -1,17 +0,0 @@
module CostQuery::Validation
module Integers
def validate_integers(*values)
values = values.flatten
return true if values.empty?
values.flatten.all? do |val|
if val.to_i.to_s != val.to_s
errors << "\'#{val}\'" + l(:validation_failure_integer)
validate_integers(values - [val])
false
else
true
end
end
end
end
end

@ -1,7 +0,0 @@
module CostQuery::Validation
module Sql
def validate_sql(values = [])
raise NotImplementedError, "Haven't done SQL validation just yet!"
end
end
end

@ -1,95 +0,0 @@
class CostQuery::Walker
attr_accessor :query, :header_stack
def initialize(query)
@query = query
end
def for_row(&block)
access_block(:row, &block)
end
def for_final_row(&block)
access_block(:final_row, &block) || access_block(:row)
end
def for_cell(&block)
access_block(:cell, &block)
end
def for_empty_cell(&block)
access_block(:empty_cell, &block) || access_block(:cell)
end
def access_block(name, &block)
@blocks ||= {}
@blocks[name] = block if block
@blocks[name]
end
def walk_cell(cell)
cell ? for_cell[cell] : for_empty_cell[nil]
end
def headers(result = nil, &block)
@header_stack = []
result ||= query.column_first
sort result
last_level = -1
num_in_col = 0
level_size = 1
sublevel = 0
result.recursive_each_with_level(0, false) do |level, result|
break if result.final_column?
if first_in_col = (last_level < level)
list = []
last_level = level
num_in_col = 0
level_size = sublevel
sublevel = 0
@header_stack << list
end
num_in_col += 1
sublevel += result.size
last_in_col = (num_in_col >= level_size)
@header_stack.last << [result, first_in_col, last_in_col]
yield(result, level == 0, first_in_col, last_in_col)
end
end
def reverse_headers
fail "call header first" unless @header_stack
first = true
@header_stack.reverse_each do |list|
list.each do |result, first_in_col, last_in_col|
yield(result, first, first_in_col, last_in_col)
end
first = false
end
end
def sort_keys
@sort_keys ||= query.chain.map { |c| c.group_fields.map(&:to_s) if c.group_by? }.compact.flatten
end
def sort(result)
result.set_key sort_keys
result.sort!
end
def body(result = nil)
return [*body(result)].each { |a| yield a } if block_given?
result ||= query.result.tap { |r| sort(r) }
if result.row?
if result.final_row?
subresults = query.table.with_gaps_for(:column, result).map(&method(:walk_cell))
for_final_row.call result, subresults
else
subresults = result.map { |r| body(r) }
for_row.call result, subresults
end
else
# you only get here if no rows are defined
result.each_direct_result.map(&method(:walk_cell))
end
end
end

@ -1,80 +0,0 @@
require_dependency "time_entry"
require_dependency "cost_entry"
module Entry
[TimeEntry, CostEntry].each { |e| e.send :include, self }
class Delegator < ActiveRecord::Base
self.abstract_class = true
class << self
def ===(obj)
TimeEntry === obj or CostEntry === obj
end
def calculate(type, *args)
a, b = TimeEntry.calculate(type, *args), CostEntry.calculate(type, *args)
case type
when :sum, :count then a + b
when :avg then (a + b) / 2
when :min then [a, b].min
when :max then [a, b].max
else raise NotImplementedError
end
end
%[find_by_sql count_by_sql count sum].each do |meth|
define_method(meth) { |*args| find_all(meth, *args) }
end
undef_method :create, :update, :delete, :destroy, :new, :update_counters,
:increment_counter, :decrement_counter
%w[update_all destroy_all delete_all].each do |meth|
define_method(meth) { |*args| send_all(meth, *args) }
end
private
def find_initial(options) find_one :find_initial, options end
def find_last(options) find_one :find_last, options end
def find_every(options) find_many :find_every, options end
def find_from_ids(args, options) find_many :find_from_ids, options end
def find_one(*args)
TimeEntry.send(*args) || CostEntry.send(*args)
end
def find_many(*args)
TimeEntry.send(*args) + CostEntry.send(*args)
end
def send_all(*args)
[TimeEntry.send(*args), CostEntry.send(*args)]
end
end
end
def units
super
rescue NoMethodError
hours
end
def cost_type
super
rescue NoMethodError
end
def activity
super
rescue NoMethodError
end
def activity_id
super
rescue NoMethodError
end
def self.method_missing(*a, &b)
Delegator.send(*a, &b)
end
end

@ -1,60 +0,0 @@
<% list = [:spent_on, :user_id, :activity_id, :issue_id, :comments, :project_id] %>
<table class="report detail-report" id="sortable-table">
<thead>
<tr>
<% list.each do |field| %><th><%= label_for(field) %></th><% end %>
<th class='right'><%= @cost_type.try(:unit_plural) || l(:units) %></th>
<th class="right"><%= l(:field_costs) %></th>
<th></th>
</tr>
</thead>
<tfoot>
<tr>
<% if show_result(@query, 0) != show_result(@query) %>
<th class="inner right" colspan='<%= list.size + 1 %>'>
<%= show_result @query %>
</th>
<th class="result right"><%= show_result @query, 0 %></th>
<% else %>
<th class="result right" colspan='<%= list.size + 2 %>'><%= show_result @query %></th>
<% end %>
<th class="unsortable"></th>
</tr>
</tfoot>
<tbody>
<% @query.each_direct_result do |result| %>
<tr class='<%= cycle("odd", "even") %>'>
<% list.each do |field| %>
<td
raw-data="<%= raw_field(field, result.fields[field.to_s]) -%>"
class="left">
<%= show_field field, result.fields[field.to_s] %>
</td>
<% end %>
<td class="units right" raw-data="<%= result.units -%>"><%= show_result(result, result.fields['cost_type_id'].to_i) %></td>
<td class="currency right" raw-data="<%= result.real_costs -%>"><%= show_result(result, 0) %></td>
<td style="width: 40px">
<% with_project(result.fields['project_id']) do %>
<% if entry_for(result).editable_by? User.current %>
<%= link_to image_tag('edit.png'),
action_for(result, :action => 'edit'), :title => l(:button_edit) %>
<%= link_to image_tag('delete.png'), action_for(result, :action => 'destroy'),
:title => l(:button_edit), :confirm => l(:text_are_you_sure),
:method => :post, :title => l(:button_delete) %>
<% end %>
<% end %>
</td>
</tr>
<% if debug? %>
<tr>
<td colspan='<%= list.size + 3 %>'>
<%= result.fields.reject {|k,v| list.include? k.to_sym }.inspect %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<%= render :partial => 'sortable_init', :locals => {:sort_first_row => true } %>

@ -1,102 +0,0 @@
<%
walker.for_final_row do |row, cells|
html = "<th class='normal inner left'>#{show_row row}#{debug_fields(row)}</th>"
html << cells.join
html << "<th class='normal inner right'>#{show_result(row)}#{debug_fields(row)}</th>"
end
walker.for_row do |row, subrows|
subrows.flatten!
unless row.fields.empty?
subrows[0] = <<-HTML
<th class='top left' rowspan='#{subrows.size}'>#{show_row row}#{debug_fields(row)}</th>
#{subrows[0].gsub("class='normal", "class='top")}
<th class='top right' rowspan='#{subrows.size}'>#{show_result(row)}#{debug_fields(row )}</th>
HTML
end
subrows.last.gsub!("class='normal", "class='bottom")
subrows.last.gsub!("class='top", "class='bottom top")
subrows
end
walker.for_empty_cell { "<td class='normal empty'>&nbsp;</td>" }
walker.for_cell do |result|
"<td class='normal'>#{link_to_details(result)}#{show_result result}#{debug_fields(result)}</td>"
end
%>
<table class='report'>
<thead>
<% walker.headers do |list, first, first_in_col, last_in_col| %>
<%= '<tr>' if first_in_col %>
<%= "<th rowspan='#{query.depth_of(:column)}' colspan='#{query.depth_of(:row)}'></th>" if first %>
<% list.each do |column| %>
<th colspan="<%= column.final_number(:column) %>" class="<%= "inner" if column.final? :column %>">
<%= show_row column %>
</th>
<% end %>
<%= "<th rowspan='#{@query.depth_of(:column)}' colspan='#{query.depth_of(:row)}'></th>" if first %>
<%= '</tr>' if last_in_col %>
<% end %>
</thead>
<tfoot>
<% walker.reverse_headers do |list, first, first_in_col, last_in_col| %>
<% if first_in_col %>
<tr>
<%= "<th rowspan='#{query.depth_of(:column)}' colspan='#{query.depth_of(:row)}' class='top'>&nbsp;</th>" if first %>
<% end %>
<% list.each do |column| %>
<th colspan="<%= column.final_number(:column) %>" class="<%= "inner" if first %>">
<%= show_result column %><%= debug_fields(column) %>
</th>
<% end %>
<% if last_in_col %>
<% if first %>
<th rowspan='<%= @query.depth_of(:column) %>' colspan='<%= query.depth_of(:row) %>' class='top result'>
<%= show_result query %>
</th>
<% end %>
</tr>
<% end %>
<% end %>
</tfoot>
<tbody>
<% first = true %>
<% walker.body do |line| %>
<%
if first
line.gsub!("class='normal", "class='top")
first = false
end
%>
<tr class='<%= cycle("odd", "even") %>'><%= line %></tr>
<% end %>
</tbody>
</table>
<% if debug? %>
<pre>
[ Query ]
<% query.chain.each do |child| %>
- <%= h child.class.inspect %>, <%= h child.type %>
<% end %>
[ RESULT ]
<% query.result.recursive_each_with_level do |level, result| %>
<%= ">>> " * (level+1) %><%= h result.inspect %>,
<%= " " * (level+1) %><%= h result.type.inspect %>,
<%= " " * (level+1) %><%= h result.fields.inspect %>
<% end %>
[ HEADER STACK ]
<% walker.header_stack.each do |l| %>
<%= ">>> #{l.inspect}" %>
<% end %>
</pre>
<% end %>

@ -1,39 +0,0 @@
<%#
This partial requires the following locals:
f An ActionView::Helpers::FormBuilder
query A CostQuery object
%>
<%= javascript_include_tag "reporting", :plugin => "redmine_reporting" %>
<% grouped_filters = CostQuery::Filter.all_grouped.sort_by { |label, group_by_ary| l(label) } %>
<% partial_prefix = File.join(File.basename(File.dirname(__FILE__)), 'filters') %>
<table id="filter_table">
<% grouped_filters.each do |label, filter_ary| %>
<tr id="tr_<%= label.to_s %>" style="display:none"><td><h3><%= l(label) %></h3></td></tr>
<% filter_ary.sort_by { |f| l(f.label)}.each do |filter| %>
<% next unless filter.display? %>
<tr id="tr_<%= filter.underscore_name %>" class="filter" style="display:none" data-label="tr_<%= label.to_s %>">
<% html_elements(filter).each do |element| %>
<%= render :partial => File.join(partial_prefix, element[:name].to_s),
:locals => {:element => element, :f => f, :filter => filter} %>
<% end %>
</tr>
<% end %>
<% end %>
</table>
<div id="add_filter_block">
<select onchange="add_filter(this);" id="add_filter_select" class="select-small">
<option value="">-- <%= l(:label_filter_add) %> --</option>
<% grouped_filters.each do |label, filter_ary| %>
<optgroup label="<%= l(label) %>">
<% filter_ary.sort_by { |f| l(f.label)}.each do |filter| %>
<% next unless filter.selectable? %>
<option value="<%= filter.underscore_name %>"><%= l(filter.label) %></option>
<% end %>
</optgroup>
<% end %>
</select>
</div>

@ -1,38 +0,0 @@
<%#
This partial requires the following locals:
f An ActionView::Helpers::FormBuilder
query A CostQuery object
%>
<% grouped_gbs = CostQuery::GroupBy.all_grouped %>
<% indices = {} %>
<% CostQuery::GroupBy.all.sort_by {|gb| l(gb.label)}.each_with_index {|gb, ix| indices[gb] = ix } %>
<div id="group_by_area">
<%= l(:label_columns) %>:
<div id="group_columns" class="drag_target drag_container">
<select id="group_by_columns" name="groups[columns][]" class="select-small" onchange="add_group_by(this);">
<option value=""></option>
<% grouped_gbs.each do |label, group_by_ary| %>
<optgroup label="<%= l(label) %>">
<% group_by_ary.select(&:display?).sort_by {|gb| l(gb.label)}.each do |group_by| %>
<option data-sort_by="<%= indices[group_by] %>" value="<%= group_by.underscore_name %>"><%= l(group_by.label)%></option>
<% end %>
</optgroup>
<% end %>
</select>
</div>
<%= l(:label_rows) %>:
<div id="group_rows" class="drag_target drag_container">
<select id="group_by_rows" name="groups[rows][]" class="select-small" onchange="add_group_by(this);">
<option value=""></option>
<% grouped_gbs.each do |label, group_by_ary| %>
<optgroup label="<%= l(label) %>">
<% group_by_ary.select(&:display?).sort_by {|gb| l(gb.label)}.each do |group_by| %>
<option data-sort_by="<%= indices[group_by] %>" value="<%= group_by.underscore_name %>"><%= l(group_by.label)%></option>
<% end %>
</optgroup>
<% end %>
</select>
</div>
</div>

@ -1,33 +0,0 @@
<script type="text/javascript">
//<![CDATA[
var set_filters, set_group_bys, restore_query_inputs;
set_filters = function () {
// Activate recent filters on loading
<% query.filters.select {|f| f.display? }.each do |f| %>
restore_filter("<%= f.class.underscore_name %>",
"<%= f.operator.to_s %>"<%= "," if f.values %>
<%= f.values.to_json if f.values %>);
<% end %>
};
set_group_bys = function () {
// Activate recent group_bys on loading
<% query.group_bys.each do |group_by| %>
<%= "show_group_by_column('#{group_by.class.underscore_name}');" if group_by.column? %>
<%= "show_group_by_row('#{group_by.class.underscore_name}');" if group_by.row? %>
<% end %>
};
restore_query_inputs = function () {
init_group_bys();
disable_all_filters();
disable_all_group_bys();
set_filters();
set_group_bys();
};
restore_query_inputs();
//]]>
</script>

@ -1,46 +0,0 @@
<%
list = @query.collect {|r| r.important_fields }.flatten.uniq
show_units = list.include? "cost_type_id"
%>
<table class="report" id="sortable-table">
<thead>
<tr>
<% list.each do |field| %><th class="right"><%= label_for(field) %></th><% end %>
<% if show_units %>
<th class="right"><%= label_for(:field_units) %></th>
<% end %>
<th class="right"><%= label_for(:label_sum) %></th>
</tr>
</thead>
<tfoot>
<tr>
<th class="result inner" colspan='<%= list.size %>'></th>
<th class="result right" <%= "colspan='2'" if show_units %>>
<%= show_result @query %>
</th>
</tr>
</tfoot>
<tbody>
<% @query.each do |result| %>
<tr class='<%= cycle("odd", "even") %>'>
<td raw-data="<%= raw_field(*result.fields.first) -%>">
<%= show_row result %>
</td>
<% if show_units %>
<td raw-data="<%= result.units -%>"><%= show_result result, result.fields[:cost_type_id].to_i %></td>
<% end %>
<td raw-data="<%= result.real_costs -%>"><%= show_result result %></td>
</tr>
<% if debug? %>
<tr>
<td colspan='<%= list.size + 3 %>'>
<%= result.fields.reject {|k,v| list.include? k.to_sym }.inspect %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<%= render :partial => 'sortable_init' %>

@ -1,13 +0,0 @@
<% sort_first_row = sort_first_row || false %>
<script type="text/javascript">
//<![CDATA[
var table_date_header = $$('#sortable-table th').first();
sortables_init();
<% if sort_first_row %>
if (table_date_header.childElements().size() > 0) {
ts_resortTable(table_date_header.childElements().first(), table_date_header.cellIndex);
}
<% end %>
//]]>
</script>

@ -1,5 +0,0 @@
<% @available_values.each do |name, id, *args| %>
<%= render :partial => 'cost_reports/filters/available_value',
:locals => { :name => name, :id => id, :level => args.first },
:layout => !request.xhr? %>
<% end %>

@ -1,11 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => :activate_filter
- :label => String: A text which is shown to the user as a label for this filter
- :width => Integer (optional): The width this partial may consume. If not given, a standard width will be applied
%>
<td width="<%= element[:width] || 150 %>">
<label id="label_<%= element[:filter_name]%>"><%= element[:label] %></label>
</td>

@ -1,11 +0,0 @@
<%#
This partial requires the following locals:
name String: The displayed name of the option
id Integer: The id the option refers to
level Integer: The indendation level of this option
%>
<% name_prefix = ((level && level > 0) ? ('&nbsp;' * 2 * level + '&#187; ') : '') %>
<option value="<%= id %>">
<%= name_prefix + h(name) %>
</option>

@ -1,22 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => :date
- :filter_name => String: The name of a filter (e.g. activity_id)
- :hide => Boolean (optional, default = true): whether the content of this partial is initially hidden or not
%>
<% name = "values[#{element[:filter_name]}][]"
id_prefix = "#{element[:filter_name]}_" %>
<td <%= style="display:none" if element[:hide] %>>
<div style="" id="<%= id_prefix %>arg_1" class="filter_values">
<%= text_field_tag "#{name}", "", :size => 10, :class => "select-small", :id => "#{id_prefix}arg_1_val"%>
<%= calendar_for("#{id_prefix}arg_1_val") %>
<span id="<%= id_prefix %>arg_2" class="between_tags">
<%=
text_field_tag("#{name}", "", :size => 10, :class => "select-small", :id => "#{id_prefix}arg_2_val") + " " +
calendar_for("#{id_prefix}arg_2_val")
%>
</span>
</div>
</td>

@ -1,21 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => :multi_values
- :filter_name => String: The name of a filter (e.g. activity_id)
- :hide => Boolean (optional, default = true): whether the content of this partial is initially hidden or not
%>
<td <%= style="display:none" if element[:hide] %>>
<div style="" id="<%= element[:filter_name] %>_arg_1" class="filter_values">
<select style="vertical-align: top;"
name="values[<%= element[:filter_name] %>][]"
id="<%= element[:filter_name] %>_arg_1_val"
class="select-small"
data-loading="ajax"
multiple="multiple"> <%# multiple will be disabled/enabled later by JavaScript anyhow. We need to specify multiple here because of a IE6-bug. %>
<%# content will be inserted on filter activation %>
</select>
<%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select($('#{element[:filter_name]}_arg_1_val'));", :style => "vertical-align: bottom;" %>
</div>
</td>

@ -1,25 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => :operators
- :filter_name => String: The name of a filter (e.g. activity_id)
- :operators => Array<CostQuery::Operator>, operators available for the user
- :width => Integer (optional): The width this partial may consume. If not given, a standard width will be applied
- :hide => Boolean (optional, default = true): whether the content of this partial is initially hidden or not
#TODO: javascrpt to update elements depending on which operator is selected
%>
<td width="<%= element[:width] || 100 %>" <%= style="display:none" if element[:hide] %>>
<select style="vertical-align: top;"
onchange="operator_changed('<%= element[:filter_name] %>', this);"
name="operators[<%= element[:filter_name] %>]"
id="operators_<%= element[:filter_name] %>"
class="select-small">
<% element[:operators].each do |operator| %>
<option value="<%= h(operator.to_s) %>" data-arity="<%= operator.arity %>">
<%= h(l(operator.label)) %>
</option>
<% end %>
</select>
</td>

@ -1,10 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :filter_name => String: The name of a filter (e.g. activity_id)
- :hide => Boolean (optional, default = true): whether the content of this partial is initially hidden or not
%>
<td width="25px">
<input id= "rm_<%= element[:filter_name] %>" name="fields[]" onclick="remove_filter('<%= element[:filter_name] %>');"
type="button" value="" class="icon filter_rem icon-filter-rem"/>
</td>

@ -1,12 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => 'text'
- :text => String: The text that should be displayed
- :width => Integer (optional): The width this partial may consume. If not given, a standard width will be applied
- :hide => Boolean (optional, default = true): whether the content of this partial is hidden or not
%>
<td width="<%= element[:width] || 100 %>" <%= style="display:none" if element[:hide] %>>
<%= element[:text] || '' %>
</td>

@ -1,17 +0,0 @@
<%#
This partial requires the following locals:
element a Hash containing the following keys:
- :name => :text_box
- :filter_name => String: The name of a filter (e.g. activity_id)
- :size => Integer, the size of the textboxt
- :hide => Boolean (optional, default = true): whether the content of this partial is initially hidden or not
%>
<td <%= style="display:none" if element[:hide] %>>
<div style="" id="<%= element[:filter_name] %>_arg_1" class="filter_values">
<%= text_field_tag("values[#{element[:filter_name]}]", "",
:size => element[:size],
:class => "select-small",
:id => "#{element[:filter_name]}_arg_1_val") %>
</div>
</td>

@ -1,79 +0,0 @@
<% content_for :header_tags do %>
<%= javascript_include_tag "select_list_move_optgroup", :plugin => "redmine_reporting" %>
<%= javascript_include_tag "reporting", :plugin => "redmine_reporting" %>
<%= javascript_include_tag "sortable", :plugin => "redmine_reporting" %>
<%= stylesheet_link_tag 'reporting', :plugin => 'redmine_reporting' %>
<% end %>
<% if @custom_errors && !@custom_errors.empty? %>
<% @custom_errors.each do |err| %>
<div class="flash error"><%= err %></div>
<% end %>
<% end %>
<h2><%= l(:label_cost_report) %></h2>
<% html_title( l(:label_cost_report) ) %>
<% form_for @query, :url => {:controller => 'cost_report', :action => 'new' }, :html => {:id => 'query_form', :method => :post} do |query_form| %>
<div id="query_form_content">
<fieldset id="query_fieldset" class="collapsible <%= "collapsed" unless @query.new_record? %>">
<legend onclick="toggleFieldset(this);"><%= l(:label_query) %></legend>
<div id="query_settings">
<h1><%= l(:label_filter_plural) %></h1>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'filters', :locals => {:f => query_form, :query => @query} %></div>
<h1><%= l(:label_group_by) %></h1>
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'group_by', :locals => {:f => query_form, :query => @query} %></div>
<%= render :partial => 'restore_query', :locals => {:f => query_form, :query => @query} %>
<p class="buttons">
<%= link_to_remote "<span><em>#{l(:button_apply)}</em></span>",
{ :url => { :set_filter => 1 },
:condition => 'Ajax.activeRequestCount === 0',
:before => 'select_active_group_bys();',
:after => 'reset_group_by_selects();',
:update => "content",
:with => "Form.serialize('query_form')",
:eval_scripts => true
}, :class => 'button apply' %>
<%= link_to_function l(:button_clear), "disable_all_filters(); disable_all_group_bys();", :class => 'icon icon-reload' %>
<% if User.current.allowed_to?(:save_queries, @project, :global => true) %>
<%
#link_to l(:button_save), {}, :onclick => "$('query_form').submit(); return false;", :class => 'icon icon-save'
%>
<% end %>
</p>
</div>
</fieldset>
</div>
<div class='cost_types'>
<b><%= l(:label_report) %>:</b>
<% @available_cost_types.each do |id, label| %>
<%=
if id != @unit_id
link_to_remote label, {
:url => { :set_filter => 1, :unit => id },
:before => 'select_active_group_bys();',
:after => 'reset_group_by_selects();',
:update => "content",
:with => "Form.serialize('query_form')",
:eval_scripts => true }
else
"<b>#{label}</b>"
end
%>
<% end %>
</div>
<% end %>
<% if @valid and @query.result.count > 0 %>
<%= render :partial => @table_partial, :locals => {:query => @query, :walker => @query.walker} %>
<p class="footnote">
<%= l(:text_costs_are_rounded_note) %>
<%= "<br />#{l(:information_restricted_depending_on_permission)}" if !User.current.admin?%>
</p>
<%= call_hook(:view_cost_report_table_bottom) %>
<% else %>
<p class="nodata"><%= l(:label_no_data) %></p>
<% end %>

@ -1,22 +0,0 @@
<script type="text/javascript">
//<![CDATA[
remove_old_sidebar = function() {
if ($($("sidebar").down()).innerHTML === "<%= l(:label_spent_time) %>") {
var reporting_link, old_html;
// The sidebar is showing spent time, remove the links, which are in the second para
$("sidebar").down().siblings()[1].remove();
// Make the hours a link, which are in the first para
reporting_link = "<%= link_to('PLACEHOLDER', {
:controller => 'cost_reports', :project_id => project,
:unit => -1, :set_filter => 1,
:values => {:project_id => [project.id]},
:operators => {:project_id => '='},
:fields => [:project_id] }).gsub('"', "'") %>";
old_html = $("sidebar").down().siblings().first().innerHTML;
$("sidebar").down().siblings().first().innerHTML = reporting_link.replace("PLACEHOLDER", old_html)
}
}
remove_old_sidebar();
//]]>
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save