@ -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'> </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'> </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) ? (' ' * 2 * level + '» ') : '') %> |
||||
<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> |
Before Width: | Height: | Size: 51 B |
Before Width: | Height: | Size: 50 B |
Before Width: | Height: | Size: 51 B |
Before Width: | Height: | Size: 49 B |
Before Width: | Height: | Size: 51 B |
Before Width: | Height: | Size: 53 B |
Before Width: | Height: | Size: 53 B |
Before Width: | Height: | Size: 52 B |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 3.1 KiB |