commit
853e95cc37
@ -1,362 +0,0 @@ |
||||
class CostReportsController < ApplicationController |
||||
unloadable |
||||
|
||||
before_filter :find_optional_project, :only => [:index, :get_filter] |
||||
before_filter :retrieve_query |
||||
|
||||
before_filter :authorize |
||||
|
||||
helper :sort |
||||
include SortHelper |
||||
include ActionView::Helpers::NumberHelper |
||||
include ActionView::Helpers::TextHelper |
||||
|
||||
|
||||
def get_filter |
||||
scope = params[:scope].to_sym if params[:scope] |
||||
column_name = params[:column_name] if params[:column_name] |
||||
|
||||
unless scope || column_name |
||||
render_404 |
||||
return |
||||
end |
||||
|
||||
@line_index = params[:line_index] || "---INDEX---" |
||||
|
||||
filter = @query.create_filter(scope, column_name) |
||||
render :partial => "filter", :object => filter, :layout => !request.xhr? |
||||
end |
||||
|
||||
def index |
||||
sort_init(@query.sort_criteria.empty? ? [['entry__spent_on', 'desc']] : @query.sort_criteria) |
||||
sortable_columns = { |
||||
"issue__issue_id" => "issue_id", |
||||
"entry__spent_on" => "spent_on", |
||||
"entry__user_id" => "user_id", |
||||
"entry__cost_type_id" => "cost_type_id", |
||||
"entry__activity_id" => "activity_id", |
||||
"entry__costs" => "real_costs" |
||||
} |
||||
sort_update(sortable_columns) |
||||
|
||||
if @query.valid? |
||||
limit = case params[:format] |
||||
when 'html', nil |
||||
per_page_option |
||||
when 'atom' |
||||
Setting.feeds_limit.to_i |
||||
else |
||||
Setting.issues_export_limit.to_i |
||||
end |
||||
|
||||
unless @query.group_by_fields.empty? |
||||
get_aggregation |
||||
|
||||
respond_to do |format| |
||||
format.html { render :layout => !request.xhr? } |
||||
# TODO: ATOM and CSV |
||||
end |
||||
else |
||||
get_entries(limit) |
||||
|
||||
respond_to do |format| |
||||
format.html { render :layout => !request.xhr? } |
||||
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") } |
||||
format.csv { send_data(entries_to_csv(@entries, @project).read, :type => 'text/csv; header=present', :filename => 'export.csv') } |
||||
end |
||||
end |
||||
else |
||||
render :layout => !request.xhr? |
||||
end |
||||
rescue Exception => e |
||||
logger.error "#{e.class.name}: #{e.message}" if logger |
||||
$@.each {|line| logger.error line} if logger |
||||
|
||||
session.delete :cost_query |
||||
|
||||
# Give it a name, required to be valid |
||||
@query = CostQuery.new(:name => "_") |
||||
@query.project = @project |
||||
|
||||
get_entries(limit) |
||||
respond_to do |format| |
||||
format.html do |
||||
@custom_error = l(:error_generic) |
||||
render :layout => !request.xhr? |
||||
end |
||||
format.atom {render_500(l(:error_generic))} |
||||
format.csv {render_500(l(:error_generic))} |
||||
end |
||||
end |
||||
|
||||
def new |
||||
# This action saves a new query for later reference |
||||
end |
||||
|
||||
private |
||||
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 |
||||
|
||||
def retrieve_query |
||||
# tries to find a active query in the session or loads the default one |
||||
|
||||
unless params[:query_id].blank? |
||||
# The user provided an explicit query_id |
||||
cond = "project_id IS NULL" |
||||
cond << " OR project_id = #{@project.id}" if @project |
||||
@query = CostQuery.find(params[:query_id], :conditions => cond) |
||||
@query.project = @project |
||||
session[:cost_query] = {:id => @query.id, :project_id => @query.project_id} |
||||
sort_clear |
||||
else |
||||
if params[:set_filter] || session[:cost_query].nil? || session[:cost_query][:project_id] != (@project ? @project.id : nil) |
||||
# We have no current query or the query was reseted explicitly |
||||
# So generate a new query |
||||
|
||||
# Give it a name, required to be valid |
||||
@query = CostQuery.new(:name => "_") |
||||
@query.project = @project |
||||
|
||||
if params[:filters].blank? |
||||
@query.filters = [] |
||||
else |
||||
@query.filters = params[:filters].collect {|f| f[1]}.select{|f| f[:enabled] != "0"} |
||||
end |
||||
|
||||
@query.group_by = params[:group_by] || {} |
||||
|
||||
if params[:cost_query] |
||||
@query.display_cost_entries = params[:cost_query][:display_cost_entries] |
||||
@query.display_time_entries = params[:cost_query][:display_time_entries] |
||||
end |
||||
|
||||
session[:cost_query] = {:project_id => @query.project_id, |
||||
:filters => @query.filters, |
||||
:group_by => @query.group_by, |
||||
:display_cost_entries => @query.display_cost_entries, |
||||
:display_time_entries => @query.display_time_entries} |
||||
else |
||||
@query = CostQuery.find_by_id(session[:cost_query][:id]) if session[:cost_query][:id] |
||||
@query ||= CostQuery.new(:name => "_", |
||||
:project => @project, |
||||
:filters => session[:cost_query][:filters], |
||||
:group_by => session[:cost_query][:group_by], |
||||
:display_cost_entries => session[:cost_query][:display_cost_entries], |
||||
:display_time_entries => session[:cost_query][:display_time_entries]) |
||||
@query.project = @project |
||||
end |
||||
end |
||||
end |
||||
|
||||
|
||||
def get_aggregation |
||||
fields = @query.group_by_fields.join(", ") |
||||
|
||||
scopes = [] |
||||
scopes << :cost_entries if @query.display_cost_entries |
||||
scopes << :time_entries if @query.display_time_entries |
||||
return @grouped_entries = [] if scopes.blank? |
||||
|
||||
subselect = scopes.map do |type| |
||||
model, select_statement, rate_permission_statement, from, where_statement, group_by_statement = @query.sql_data_for type |
||||
table = model.table_name |
||||
<<-EOS |
||||
SELECT |
||||
#{select_statement}, |
||||
SUM( |
||||
CASE WHEN #{rate_permission_statement} THEN |
||||
CASE WHEN #{table}.overridden_costs IS NULL THEN #{table}.costs |
||||
ELSE #{table}.overridden_costs END |
||||
ELSE |
||||
0.0000 |
||||
END |
||||
) AS sum, |
||||
SUM(#{table == "time_entries" ? "hours" : "units"}) as unit_sum, |
||||
COUNT(*) AS count |
||||
FROM #{from} |
||||
WHERE #{where_statement} |
||||
#{group_by_statement} |
||||
EOS |
||||
end.join(" UNION ") |
||||
|
||||
if scopes.length == 2 |
||||
sql = "SELECT #{fields}, SUM(sum) as sum, SUM(unit_sum) as unit_sum, SUM(count) AS count FROM (#{subselect}) AS entries GROUP BY #{fields}" |
||||
else |
||||
sql = subselect |
||||
end |
||||
|
||||
@grouped_entries = ActiveRecord::Base.connection.select_all(sql) |
||||
@entry_sum, @entry_count = @grouped_entries.inject([0, 0.0]) do |r,i| |
||||
r[0] += i["sum"].to_f |
||||
r[1] += i["count"].to_i |
||||
r |
||||
end |
||||
end |
||||
|
||||
def get_entries(limit) |
||||
cost_where = @query.statement(:cost_entries) |
||||
time_where = @query.statement(:time_entries) |
||||
|
||||
aggregate_select, display_costs = [TimeEntry, CostEntry].inject([{}, {}]) do |r,table| |
||||
table_name = table.table_name |
||||
rate_permission_statement = @query.rate_permission_statement(table_name.to_sym) |
||||
|
||||
r[0][table_name] = <<-EOS |
||||
COUNT(#{table_name}.id) as count, |
||||
SUM( |
||||
CASE WHEN #{rate_permission_statement} THEN |
||||
CASE WHEN #{table_name}.overridden_costs IS NULL THEN #{table_name}.costs |
||||
ELSE #{table_name}.overridden_costs END |
||||
ELSE |
||||
0.0000 |
||||
END |
||||
) AS sum |
||||
EOS |
||||
|
||||
r[1][table_name] = table.column_names.collect{|n| "#{table_name}.#{n}"}.join(", ") + <<-EOS |
||||
, CASE WHEN #{rate_permission_statement} THEN |
||||
1 |
||||
ELSE |
||||
NULL |
||||
END AS display_costs |
||||
EOS |
||||
|
||||
r |
||||
end |
||||
|
||||
# at first get the entry ids to match the current query |
||||
unless sort_clause.nil? |
||||
(sort_column, sort_order) = sort_clause.split(" ") |
||||
|
||||
sort_column.gsub!(/\./, "__") |
||||
|
||||
case sort_column |
||||
when "real_costs" |
||||
cost_sort_column = sort_column |
||||
cost_sort_column_sql = "costs, overridden_costs," |
||||
|
||||
time_sort_column = sort_column |
||||
time_sort_column_sql = "costs, overridden_costs," |
||||
|
||||
sort_clause = "overridden_costs #{sort_order}, costs #{sort_order}" |
||||
else |
||||
cost_sort_column = (CostEntry.new.respond_to? sort_column) ? sort_column : nil |
||||
cost_sort_column_sql = cost_sort_column || "NULL as #{sort_column}" |
||||
cost_sort_column_sql += "," |
||||
|
||||
time_sort_column = (TimeEntry.new.respond_to? sort_column) ? sort_column : nil |
||||
time_sort_column_sql = time_sort_column || "NULL as #{sort_column}" |
||||
time_sort_column_sql += "," |
||||
|
||||
sort_clause = self.sort_clause |
||||
end |
||||
end |
||||
|
||||
if @query.display_time_entries |
||||
time_entry_sum, time_entry_count = TimeEntry.all( |
||||
:select => aggregate_select[TimeEntry.table_name], |
||||
:conditions => time_where, |
||||
:from => @query.from_statement(:time_entries) |
||||
).map {|i| [i.sum.to_f, i.count.to_i] }[0] |
||||
end |
||||
|
||||
if @query.display_cost_entries |
||||
cost_entry_sum, cost_entry_count = CostEntry.all( |
||||
:select => aggregate_select[CostEntry.table_name], |
||||
:conditions => cost_where, |
||||
:from => @query.from_statement(:cost_entries) |
||||
).map {|i| [i.sum.to_f, i.count.to_i] }[0] |
||||
end |
||||
|
||||
if @query.display_time_entries && !@query.display_cost_entries |
||||
@entry_sum, @entry_count = [time_entry_sum, time_entry_count] |
||||
@entry_pages = Paginator.new self, @entry_count, limit, params['page'] |
||||
|
||||
@entries = TimeEntry.all({ :select => display_costs[TimeEntry.table_name], |
||||
:order => (sort_clause if time_sort_column), |
||||
:from => @query.from_statement(:time_entries), |
||||
:conditions => time_where, |
||||
:limit => limit, |
||||
:offset => @entry_pages.current.offset}) |
||||
|
||||
return |
||||
elsif @query.display_cost_entries && !@query.display_time_entries |
||||
@entry_sum, @entry_count = [cost_entry_sum, cost_entry_count] |
||||
@entry_pages = Paginator.new self, @entry_count, limit, params['page'] |
||||
|
||||
@entries = CostEntry.all({ :select => display_costs[CostEntry.table_name], |
||||
:order => (sort_clause if cost_sort_column), |
||||
:from => @query.from_statement(:cost_entries), |
||||
:conditions => cost_where, |
||||
:limit => limit, |
||||
:offset => @entry_pages.current.offset}) |
||||
return |
||||
elsif !@query.display_time_entries && !@query.display_time_entries |
||||
@entry_sum, @entry_count = [0 , 0] |
||||
@entry_pages = Paginator.new self, @entry_count, limit, params['page'] |
||||
@entries = [] |
||||
return |
||||
end |
||||
|
||||
@entry_count = time_entry_count + cost_entry_count |
||||
@entry_sum = time_entry_sum + cost_entry_sum |
||||
@entry_pages = Paginator.new self, @entry_count, limit, params['page'] |
||||
|
||||
cost_from = @query.from_statement(:cost_entries) |
||||
time_from = @query.from_statement(:time_entries) |
||||
|
||||
# TAKE extra care for SQL injection here!!! |
||||
sql = " SELECT #{CostEntry.table_name}.id AS id, #{cost_sort_column_sql} 'cost_entry' AS entry_type" |
||||
sql << " FROM #{cost_from}" |
||||
sql << " WHERE #{cost_where}" |
||||
sql << " UNION" |
||||
sql << " SELECT #{TimeEntry.table_name}.id AS id, #{time_sort_column_sql} 'time_entry' as entry_type" |
||||
sql << " FROM #{time_from}" |
||||
sql << " WHERE #{time_where}" |
||||
sql << " ORDER BY #{sort_clause}" if sort_clause |
||||
sql << " LIMIT #{limit} OFFSET #{@entry_pages.current.offset}" |
||||
|
||||
raw_ids = ActiveRecord::Base.connection.select_all(sql) |
||||
|
||||
cost_entry_ids = [] |
||||
time_entry_ids = [] |
||||
|
||||
raw_ids.each do |row| |
||||
case row["entry_type"] |
||||
when "cost_entry" |
||||
cost_entry_ids << row["id"] |
||||
when "time_entry" |
||||
time_entry_ids << row["id"] |
||||
else |
||||
raise "Unknown entry type in SQL. Should never happen." |
||||
end |
||||
end |
||||
|
||||
|
||||
cost_entries = CostEntry.all({:select => display_costs[CostEntry.table_name], |
||||
:order => (sort_clause if cost_sort_column), |
||||
:from => @query.from_statement(:cost_entries), |
||||
:conditions => {:id => cost_entry_ids}}) |
||||
|
||||
time_entries = TimeEntry.all({:select => display_costs[TimeEntry.table_name], |
||||
:order => (sort_clause if time_sort_column), |
||||
:from => @query.from_statement(:time_entries), |
||||
:conditions => {:id => time_entry_ids}}) |
||||
|
||||
# now we merge the both entry types |
||||
if cost_sort_column && time_sort_column |
||||
@entries = cost_entries + time_entries |
||||
@entries.sort!{|a,b| a.send(sort_column) <=> b.send(sort_column)} |
||||
@entries.reverse! if sort_order && sort_order == "DESC" |
||||
elsif cost_sort_column |
||||
@entries = cost_entries + time_entries |
||||
else |
||||
@entries = time_entries + cost_entries |
||||
end |
||||
end |
||||
end |
@ -1,710 +0,0 @@ |
||||
require_dependency 'query' |
||||
|
||||
class CostQueryColumn < QueryColumn |
||||
attr_reader :scope |
||||
|
||||
def initialize(name, options={}) |
||||
self.scope = (optione.delete(:scope) || :issues) |
||||
super |
||||
end |
||||
end |
||||
|
||||
class CostQueryCustomFieldColumn < QueryCustomFieldColumn |
||||
attr_accessor :scope |
||||
|
||||
def initialize(custom_field) |
||||
self.reader = :issues |
||||
super |
||||
end |
||||
end |
||||
|
||||
class Filter |
||||
include GLoc |
||||
|
||||
|
||||
def initialize(scope, column_name, column) |
||||
@scope = scope |
||||
@column_name = column_name |
||||
@column = column |
||||
|
||||
@enabled = true |
||||
|
||||
default_operator = CostQuery.filter_types[@column[:type]][:default] |
||||
@operator = default_operator if default_operator |
||||
end |
||||
attr_reader :scope, :column_name, :column |
||||
|
||||
attr_reader :values, :sql_values |
||||
def values=(v) |
||||
values = v.is_a?(Array) ? v : [v] |
||||
sql_values = values.dup |
||||
|
||||
if column[:flags].include? :user |
||||
sql_values.push(User.current.logged? ? User.current.id.to_s : "0") if sql_values.delete("me") |
||||
end |
||||
|
||||
if available_values |
||||
available_value_keys = available_values.collect {|o| o[1].to_s } |
||||
sql_values.each do |value| |
||||
unless (available_value_keys.include? value.to_s) or (value.to_s == "") |
||||
raise ArgumentError.new("Forbidden value (#{value.inspect} not in #{available_value_keys.inspect})") |
||||
end |
||||
end |
||||
end |
||||
|
||||
@values = values |
||||
@sql_values = sql_values |
||||
end |
||||
|
||||
attr_reader :operator |
||||
def operator=(o) |
||||
raise ArgumentError.new("Forbidden operator #{o}") unless available_operators.include? o |
||||
@operator = o |
||||
end |
||||
|
||||
attr_accessor :enabled |
||||
|
||||
def type_name |
||||
@column[:type] |
||||
end |
||||
|
||||
def filter_type |
||||
CostQuery.filter_types[type_name] |
||||
end |
||||
|
||||
def label |
||||
@column[:name] || l(("field_"+@column_name.gsub(/\_id$/, "")).to_sym) |
||||
end |
||||
|
||||
def available_operators |
||||
filter_type[:operators] |
||||
end |
||||
|
||||
def available_values |
||||
@column[:values] |
||||
end |
||||
|
||||
def new_record? |
||||
return true |
||||
end |
||||
end |
||||
|
||||
class CostQuery < ActiveRecord::Base |
||||
include GLoc |
||||
|
||||
belongs_to :user |
||||
belongs_to :project |
||||
|
||||
serialize :filters |
||||
serialize :group_by |
||||
|
||||
attr_protected :user_id, :project_id, :created_at, :updated_at |
||||
|
||||
def after_initialize |
||||
self.display_time_entries = true if display_time_entries.nil? |
||||
self.display_cost_entries = true if display_cost_entries.nil? |
||||
|
||||
self.group_by ||= {} |
||||
end |
||||
|
||||
def self.operators |
||||
# These are the operators used by filter types. |
||||
|
||||
operators = {} |
||||
issue_operators = Query.operators |
||||
|
||||
issue_operators.each_pair do |op, label| |
||||
simple = (["!*", "*", "t", "w", "o", "c"].include? op) |
||||
operators[op] = {:label => label, :simple => simple} |
||||
|
||||
end |
||||
|
||||
operators.merge( |
||||
{ |
||||
"=n" => {:label => :label_equals, :simple => false}, |
||||
"0" => {:label => :label_none, :simple => true}, |
||||
"y" => {:label => :label_yes, :simple => true}, |
||||
"n" => {:label => :label_no, :simple => true}, |
||||
"<d" => {:label => :label_less_or_equal, :simple => false}, |
||||
">d" => {:label => :label_greater_or_equal, :simple => false}, |
||||
"<>d" => {:label => :label_between, :simple => false}, |
||||
"=d" => {:label => :label_date_on, :simple => false} |
||||
} |
||||
) |
||||
end |
||||
|
||||
def self.filter_types |
||||
return @filter_types if @filter_types |
||||
|
||||
filter_types = Query.operators_by_filter_type.inject({}) do |r, f| |
||||
multiple = !([:list, :list_status, :list_optional, :list_subproject].include? f[0]) |
||||
r[f[0]] = {:operators => f[1], :multiple => multiple} |
||||
r |
||||
end |
||||
@filter_types = filter_types.merge( |
||||
{ |
||||
:integer_zero => {:operators => [ "=n", ">=", "<=", "0", "*" ], :multiple => true}, |
||||
:boolean => {:operators => [ "y", "n" ], :multiple => false}, |
||||
:date_exact => {:operators => [ "<d", ">d", "<>d", "=d", "t", "w"], :multiple => true, :default => "w"} |
||||
} |
||||
) |
||||
end |
||||
|
||||
def available_filters |
||||
# This available_filters is different from the Redmine one |
||||
# available_filters[:issues] |
||||
# --> filters on issue fields. These are the one from redmine itself |
||||
# available_filters[:costs] |
||||
# --> filters on cost and time entries |
||||
|
||||
return @available_filters if @available_filters |
||||
|
||||
@available_filters = { |
||||
:costs => { |
||||
"cost_type_id" => { :type => :list_optional, :order => 2, :applies => [:cost_entries], :flags => [], :db_table => CostType.table_name, :db_field => "id", :values => CostType.find(:all, :order => 'name').collect{|s| [s.name, s.id.to_s] }}, |
||||
"activity_id" => { :type => :list_optional, :order => 3, :applies => [:time_entries], :flags => [], :db_table => TimeEntryActivity.table_name, :db_field => "id", :values => TimeEntryActivity.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] }}, |
||||
"created_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 4 }, |
||||
"updated_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 5 }, |
||||
"spent_on" => { :type => :date_exact, :applies => [:time_entries, :cost_entries], :flags => [], :order => 6}, |
||||
"overridden_costs" => { :type => :boolean, :applies => [:time_entries, :cost_entries], :flags => [], :order => 7 }, |
||||
# FIXME: Issues are not selected properly according to project selection |
||||
"issue_id" => { :type => :list_optional, :order => 8, :applies => [:cost_entries, :time_entries], :flags => [], :db_table => Issue.table_name, :db_field => "id", :values => Issue.find(:all, :order => :id, :include => :tracker).collect{|s| ["#{s.tracker} ##{s.id}: #{s.subject}", s.id.to_s] }}, |
||||
} |
||||
} |
||||
|
||||
tmp_query = Query.new(:project => project, :name => "_") |
||||
@available_filters[:issues] = tmp_query.available_filters |
||||
# flag columns that contain filters for user columns |
||||
@available_filters[:issues].each_pair do |k,v| |
||||
v[:flags] = [] |
||||
v[:flags] << :user if %w(assigned_to_id author_id watcher_id).include?(k) |
||||
if k =~ /^cf_(\d+)$/ |
||||
# custom field |
||||
v[:db_table] = CustomValue.table_name |
||||
v[:db_field] = 'value' |
||||
v[:db_field_id] = $1 # this is the numeric part in the regex above |
||||
v[:flags] << :custom_field |
||||
elsif k == "watcher_id" |
||||
v[:db_table] = Watcher.table_name |
||||
v[:db_field] = 'user_id' |
||||
v[:flags] << :watcher |
||||
else |
||||
if ["labor_costs", "material_costs", "overall_costs"].include? k |
||||
v[:type] = :integer_zero |
||||
end |
||||
if [:date_past, :date].include? v[:type] |
||||
v[:type] = :date_exact |
||||
end |
||||
v[:db_table] = Issue.table_name |
||||
v[:db_field] = k |
||||
end |
||||
end |
||||
|
||||
if @available_filters[:issues]["author_id"] |
||||
# add a filter on cost entries for user_id if it is available |
||||
user_values = @available_filters[:issues]["author_id"][:values] |
||||
@available_filters[:costs]["user_id"] = {:type => :list_optional, :order => 1, :applies => [:time_entries, :cost_entries], :values => user_values, :flags => [:user]} |
||||
end |
||||
|
||||
@available_filters |
||||
end |
||||
|
||||
def create_filter(scope, column_name) |
||||
column = available_filters[scope][column_name] |
||||
column ? Filter.new(scope, column_name, column) : nil |
||||
end |
||||
|
||||
def create_filter_from_hash(filter_hash = {}) |
||||
scope = filter_hash[:scope].to_sym |
||||
column_name = filter_hash[:column_name] |
||||
column = available_filters[scope][column_name] |
||||
|
||||
f = Filter.new(scope, column_name, column) |
||||
f.enabled = filter_hash[:enabled] unless filter_hash[:enabled].nil? |
||||
f.operator = filter_hash[:operator] unless filter_hash[:operator].nil? |
||||
f.values = filter_hash[:values] unless filter_hash[:values].nil? |
||||
|
||||
f |
||||
end |
||||
|
||||
def has_filter?(scope, column_name) |
||||
# returns the first matching filter or nil |
||||
return nil unless filters |
||||
|
||||
match = filters.select {|f| f[:scope] == scope.to_s && f[:column_name] == column_name.to_s} |
||||
return match.blank? ? nil : match[0] |
||||
end |
||||
|
||||
MAGIC_GROUP_KEYS = [:block, :time, :display, :db_field, :other_group] |
||||
|
||||
def self.grouping_column(*names, &block) |
||||
options = names.extract_options! |
||||
names.each do |name| |
||||
group_by_columns[name] = options.with_indifferent_access.merge( |
||||
:block => block, |
||||
:scope => grouping_scope |
||||
) |
||||
group_by_columns[name][:db_field] ||= name |
||||
group_by_columns[name][:display] ||= Proc.new { |e| e } |
||||
group_by_columns[name][:other_group] ||= "<em>#{l :group_by_others}</em>" |
||||
end |
||||
end |
||||
|
||||
def self.grouping_scope(type = nil) |
||||
@grouping_scope = type || @grouping_scope |
||||
yield if block_given? |
||||
@grouping_scope |
||||
end |
||||
|
||||
def self.group_by_columns |
||||
@group_by_columns ||= {}.with_indifferent_access |
||||
end |
||||
|
||||
def self.get_name(key, value) |
||||
return group_by_columns[key][:other_group] unless value |
||||
group_by_columns[key][:display].call value |
||||
end |
||||
|
||||
def self.from_field(klass, field) |
||||
Proc.new do |id| |
||||
a = klass.find_by_id(id) |
||||
(a ? a.send(field) : id).to_s |
||||
end |
||||
end |
||||
|
||||
grouping_scope(:issues) do |
||||
grouping_column :tracker_id, :display => from_field(Tracker, :name) |
||||
grouping_column :fixed_version_id, :display => from_field(Version, :name) |
||||
grouping_column :cost_object_id, :display => from_field(CostObject, :subject) |
||||
grouping_column :subproject_id, :display => from_field(Project, :name), :db_field => :project_id |
||||
end |
||||
|
||||
grouping_scope(:costs) do |
||||
grouping_column :user_id, :display => from_field(User, :name) |
||||
grouping_column :issue_id, :display => from_field(Issue, :subject), :other_group => "<em>#{l(:caption_booked_on_project)}</em>" |
||||
grouping_column :cost_type_id, :display => from_field(CostType, :name), :other_group => l(:caption_labor_costs) |
||||
grouping_column :activity_id, :display => from_field(TimeEntryActivity, :name) |
||||
grouping_column(:spent_on, :tyear, :tmonth, :tweek, :time => true) do |column, fields| |
||||
values = [] |
||||
|
||||
if fields["spent_on"] |
||||
values = [fields["spent_on"].to_date] * 2 |
||||
elsif fields["tyear"] |
||||
if fields["tmonth"] |
||||
start_of_month = Date.civil(fields["tyear"].to_i, fields["tmonth"].to_i , 1) |
||||
values = [start_of_month.to_s, start_of_month.end_of_month.to_s] |
||||
elsif fields["tweek"] |
||||
start_of_week = Date.commercial(fields["tyear"].to_i, fields["tweek"].to_i, 1) |
||||
values = [start_of_week.to_s, start_of_week.end_of_week.to_s] |
||||
else |
||||
start_of_year = Date.civil(fields["tyear"].to_i, 1, 1) |
||||
values = [start_of_year.to_s, start_of_year.end_of_year.to_s] |
||||
end |
||||
end |
||||
|
||||
raise "Invalid group by values" if values.blank? |
||||
|
||||
{ |
||||
:operator => "<>d", |
||||
:values => values, |
||||
:column_name => :spent_on |
||||
} |
||||
|
||||
end |
||||
end |
||||
|
||||
def filter_from_group_by(fields) |
||||
column_name = group_by[:name].to_sym |
||||
data = self.class.group_by_columns[column_name].dup |
||||
options = {} |
||||
MAGIC_GROUP_KEYS.each do |key| |
||||
options[key] = data.delete key |
||||
end |
||||
block = options[:block] || Proc.new { {} } |
||||
equals_hash = { |
||||
:enabled => 1, |
||||
:operator => "=", |
||||
:column_name => column_name, |
||||
:values => fields[column_name.to_s], |
||||
} |
||||
|
||||
# TODO: not all filters have this filter operator. We have to always select the correct operator |
||||
none_hash = { |
||||
:enabled => 1, |
||||
:operator => "!*", |
||||
:column_name => column_name, |
||||
:values => nil, |
||||
} |
||||
|
||||
hash = fields[column_name.to_s].nil? ? none_hash : equals_hash |
||||
hash.merge(data).merge(block.call(column_name, fields)) |
||||
end |
||||
|
||||
|
||||
def group_by_columns_for_select |
||||
self.class.group_by_columns.inject([["", ""]]) do |list, (column_name, values)| |
||||
filter = create_filter(values[:scope], column_name.to_s) |
||||
list << [filter.label, column_name] if filter |
||||
list |
||||
end |
||||
end |
||||
|
||||
def time_groups |
||||
# returns an array of group_by names where time == true |
||||
self.class.group_by_columns.inject([]) do |list, (column_name, values)| |
||||
list << column_name if values[:time] |
||||
list |
||||
end |
||||
end |
||||
|
||||
def projects |
||||
return @projects unless @projects.blank? |
||||
|
||||
projects = [project] |
||||
if project && !project.children.active.empty? |
||||
if subprojects = has_filter?(:issues, "subproject_id") |
||||
subprojects = create_filter_from_hash(subprojects) |
||||
|
||||
case subprojects.operator |
||||
when "=" |
||||
# include the selected subprojects |
||||
projects += Project.find_by_id(subprojects.values.each(&:to_i)) |
||||
when "!*" |
||||
# main project only |
||||
else |
||||
# all subprojects |
||||
projects += project.descendants |
||||
end |
||||
elsif Setting.display_subprojects_issues? |
||||
projects += project.descendants |
||||
end |
||||
elsif project |
||||
# show only the current project |
||||
else |
||||
projects = [] |
||||
end |
||||
|
||||
@projects = projects |
||||
end |
||||
|
||||
def project_statement |
||||
"#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" |
||||
end |
||||
|
||||
def group_by_fields() |
||||
# returns the group_by of the current group by query |
||||
# These fields are one of the keys of group_by_columns |
||||
|
||||
return [] unless !group_by[:name].blank? && (data = group_by_columns[group_by[:name].to_sym]) |
||||
|
||||
if data[:time] |
||||
# We have a group_by_time |
||||
return case group_by[:granularity] |
||||
when "year" then ["tyear"] |
||||
when "month" then ["tyear", "tmonth"] |
||||
when "week" then ["tyear", "tweek"] |
||||
else ["spent_on"] |
||||
end |
||||
else |
||||
[group_by[:name]] |
||||
end |
||||
end |
||||
|
||||
def group_by_columns |
||||
self.class.group_by_columns |
||||
end |
||||
|
||||
def sql_data_for(entry_scope) |
||||
case entry_scope |
||||
when :cost_entries |
||||
model = CostEntry |
||||
when :time_entries |
||||
model = TimeEntry |
||||
from_include_issue = false |
||||
end |
||||
|
||||
my_fields, nil_fields, grouping_fields = [], [], [] |
||||
|
||||
group_by_fields.each do |field| |
||||
group_by_column = group_by_columns[field.to_sym] |
||||
klass = group_by_column[:scope] == :issues ? Issue : model |
||||
db_field = group_by_column[:db_field] |
||||
|
||||
if klass.column_names.include? db_field.to_s |
||||
grouping_fields << "#{klass.table_name}.#{db_field}" |
||||
my_fields << "#{klass.table_name}.#{db_field} as #{field}" |
||||
else |
||||
nil_fields << "NULL as #{field}" |
||||
end |
||||
end |
||||
|
||||
group_by = "GROUP BY #{grouping_fields.join(", ")}" unless grouping_fields.blank? |
||||
[model, (my_fields + nil_fields).join(", "), rate_permission_statement(entry_scope), from_statement(entry_scope), statement(entry_scope), group_by] |
||||
end |
||||
|
||||
def rate_permission_statement(entry_scope) |
||||
case entry_scope |
||||
when :cost_entries |
||||
statement = User.current.allowed_for(:view_cost_rates, projects) |
||||
when :time_entries |
||||
statement = User.current.allowed_for(:view_hourly_rates, projects) |
||||
end |
||||
end |
||||
|
||||
def from_statement(entry_scope, include_issue = false) |
||||
case entry_scope |
||||
when :cost_entries |
||||
from = <<-EOS |
||||
#{CostEntry.table_name} |
||||
LEFT OUTER JOIN #{CostType.table_name} ON #{CostType.table_name}.id = #{CostEntry.table_name}.cost_type_id |
||||
LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{CostEntry.table_name}.user_id |
||||
LEFT OUTER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{CostEntry.table_name}.issue_id |
||||
LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{CostEntry.table_name}.project_id |
||||
EOS |
||||
when :time_entries |
||||
from = <<-EOS |
||||
#{TimeEntry.table_name} |
||||
LEFT OUTER JOIN #{TimeEntryActivity.table_name} ON #{TimeEntryActivity.table_name}.id = #{TimeEntry.table_name}.activity_id |
||||
LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{TimeEntry.table_name}.user_id |
||||
LEFT OUTER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{TimeEntry.table_name}.issue_id |
||||
LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{TimeEntry.table_name}.project_id |
||||
EOS |
||||
end |
||||
end |
||||
|
||||
def statement(entry_scope) |
||||
# entry_scope can currently be one of :cost_entries, :time_entries |
||||
# To not mix this with the scope (aka :issues vs. :costs) |
||||
|
||||
issue_filter_clauses = [] |
||||
entry_filter_clauses = [] |
||||
|
||||
# allow blank issue_ids if true |
||||
issue_nil_filter = true |
||||
|
||||
if filters and valid? |
||||
filters.each do |filter| |
||||
filter = create_filter_from_hash(filter) |
||||
next if filter.column_name == "subproject_id" |
||||
|
||||
sql = '' |
||||
|
||||
case filter.scope |
||||
when :issues |
||||
if filter.column[:flags].include? :custom_field |
||||
sql << "#{Issue.table_name}.id IN (" |
||||
sql << " SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{filter.column[:db_table]} ON #{filter.column[:db_table]}.customized_type='Issue' AND #{filter.column[:db_table]}.customized_id=#{Issue.table_name}.id AND #{filter.column[:db_table]}.custom_field_id=#{filter.column[:db_field_id]} WHERE " |
||||
sql << sql_for_filter(filter, nil, true) |
||||
sql << ")" |
||||
elsif filter.column[:flags].include? :watcher |
||||
sql << "#{Issue.table_name}.id #{ field.operator == '=' ? 'IN' : 'NOT IN' } (" |
||||
sql << " SELECT #{filter.column[:db_table]}.watchable_id FROM #{filter.column[:db_table]} WHERE #{filter.column[:db_table]}.watchable_type='Issue' AND " |
||||
sql << sql_for_filter(filter) |
||||
sql << ")" |
||||
else |
||||
sql << '(' + sql_for_filter(filter) + ')' |
||||
end |
||||
issue_filter_clauses << sql |
||||
when :costs |
||||
issue_nil_filter = false if filter.column_name == "issue_id" |
||||
sql << '(' + sql_for_filter(filter, entry_scope) + ')' |
||||
entry_filter_clauses << sql |
||||
end |
||||
end |
||||
end |
||||
|
||||
issue_filter_clauses = ["1=1"] if issue_filter_clauses.blank? |
||||
# FIXME: This is a hack calling a private ActiveRecord methods |
||||
# from http://pivotallabs.com/users/jsusser/blog/articles/567-hacking-a-subselect-in-activerecord |
||||
|
||||
from = "#{Issue.table_name}" |
||||
from << " LEFT OUTER JOIN #{User.table_name} ON #{User.table_name}.id = #{Issue.table_name}.assigned_to_id" |
||||
from << " LEFT OUTER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id" |
||||
from << " LEFT OUTER JOIN #{Tracker.table_name} ON #{Tracker.table_name}.id = #{Issue.table_name}.tracker_id" |
||||
from << " LEFT OUTER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id" |
||||
from << " LEFT OUTER JOIN #{IssuePriority.table_name} ON #{IssuePriority.table_name}.id = #{Issue.table_name}.priority_id" |
||||
from << " LEFT OUTER JOIN #{IssueCategory.table_name} ON #{IssueCategory.table_name}.id = #{Issue.table_name}.category_id" |
||||
from << " LEFT OUTER JOIN #{Version.table_name} ON #{Version.table_name}.id = #{Issue.table_name}.fixed_version_id" |
||||
|
||||
issue_ids = Issue.send( |
||||
:construct_finder_sql, |
||||
:select => "#{Issue.table_name}.id", |
||||
#:include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ], |
||||
:from => from, |
||||
:conditions => (issue_filter_clauses << project_statement).join(' AND ')) |
||||
|
||||
case entry_scope |
||||
when :cost_entries |
||||
clause = ["#{CostEntry.table_name}.issue_id IN (#{issue_ids})"] |
||||
clause << "#{CostEntry.table_name}.issue_id IS NULL" if issue_nil_filter |
||||
entry_filter_clauses << "(#{clause.join(" OR ")})" |
||||
when :time_entries |
||||
clause = ["#{TimeEntry.table_name}.issue_id IN (#{issue_ids})"] |
||||
clause << "#{TimeEntry.table_name}.issue_id IS NULL" if issue_nil_filter |
||||
entry_filter_clauses << "(#{clause.join(" OR ")})" |
||||
end |
||||
|
||||
entry_filter_clauses << User.current.allowed_for("view_#{entry_scope}".to_sym, projects) |
||||
|
||||
entry_filter_clauses.join(' AND ') |
||||
end |
||||
|
||||
|
||||
|
||||
def sort_criteria=(arg) |
||||
c = [] |
||||
if arg.is_a?(Hash) |
||||
arg = arg.keys.sort.collect {|k| arg[k]} |
||||
end |
||||
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} |
||||
write_attribute(:sort_criteria, c) |
||||
end |
||||
|
||||
def sort_criteria |
||||
read_attribute(:sort_criteria) || [] |
||||
end |
||||
|
||||
def sort_criteria_key(arg) |
||||
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first |
||||
end |
||||
|
||||
def sort_criteria_order(arg) |
||||
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last |
||||
end |
||||
|
||||
|
||||
private |
||||
def sql_for_filter(filter, entry_scope = nil, string_as_null = false) |
||||
db_table = filter.column[:db_table] |
||||
if filter.scope == :costs && !db_table |
||||
case entry_scope |
||||
when :cost_entries |
||||
db_table = CostEntry.table_name |
||||
when :time_entries |
||||
db_table = TimeEntry.table_name |
||||
else |
||||
raise "Need a valid entry scope. Got #{entry_scope.inspect}" unless entry_scope |
||||
end |
||||
end |
||||
|
||||
if filter.scope == :costs && (!filter.column[:applies].include? entry_scope) |
||||
# the current filter does not match the entry_scope so we just ignore it |
||||
return "1=1" |
||||
end |
||||
|
||||
db_field = filter.column[:db_field] || filter.column_name |
||||
|
||||
# Does not work for Redmine 0.8 |
||||
#@sql_for_filter_query = Query.new(:name => "_") unless @sql_for_filter_query |
||||
#sql = @sql_for_filter_query.send( |
||||
# :sql_for_field, |
||||
# filter.column_name, filter.operator, filter.sql_values, db_table, db_field, string_as_null) |
||||
|
||||
sql = sql_for_field(filter.column_name, filter.operator, filter.sql_values, db_table, db_field, string_as_null) |
||||
return sql unless sql == "1=1" |
||||
|
||||
# We have an operator that was added by us. So we provide the logic here |
||||
case filter.operator |
||||
when "0" |
||||
sql = "#{db_table}.#{db_field} = 0" |
||||
when "y" |
||||
sql = "#{db_table}.#{db_field} IS NOT NULL" |
||||
when "n" |
||||
sql = "#{db_table}.#{db_field} IS NULL" |
||||
when "=n" |
||||
sql = "#{db_table}.#{db_field} = #{CostRate.clean_currency(filter.sql_values).to_f.to_s}" |
||||
when "<>d" |
||||
begin |
||||
date1 = filter.sql_values.first.to_date |
||||
date2 = filter.sql_values.last.to_date |
||||
sql = "#{db_table}.#{db_field} BETWEEN '#{connection.quoted_date(date1)}' AND '#{connection.quoted_date(date2)}'" |
||||
rescue |
||||
end |
||||
when ">d" |
||||
begin |
||||
date = filter.sql_values.first.to_date |
||||
sql = "#{db_table}.#{db_field} >= '#{connection.quoted_date(date)}'" |
||||
rescue |
||||
end |
||||
when "<d" |
||||
begin |
||||
date = filter.sql_values.first.to_date |
||||
sql = "#{db_table}.#{db_field} <= '#{connection.quoted_date(date)}'" |
||||
rescue |
||||
end |
||||
when "=d" |
||||
begin |
||||
date = filter.sql_values.first.to_date |
||||
sql = "#{db_table}.#{db_field} = '#{connection.quoted_date(date)}'" |
||||
rescue |
||||
end |
||||
end |
||||
|
||||
return sql |
||||
end |
||||
|
||||
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ |
||||
# FIXME: This methods comes from redmine trunk. Delete this one and call the one from redmine instead! |
||||
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) |
||||
sql = '' |
||||
case operator |
||||
when "=" |
||||
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" unless value.blank? |
||||
when "!" |
||||
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" |
||||
when "!*" |
||||
sql = "#{db_table}.#{db_field} IS NULL" |
||||
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter |
||||
when "*" |
||||
sql = "#{db_table}.#{db_field} IS NOT NULL" |
||||
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter |
||||
when ">=" |
||||
sql = "#{db_table}.#{db_field} >= #{value.first.to_i}" |
||||
when "<=" |
||||
sql = "#{db_table}.#{db_field} <= #{value.first.to_i}" |
||||
when "o" |
||||
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id" |
||||
when "c" |
||||
sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id" |
||||
when ">t-" |
||||
sql = date_range_clause(db_table, db_field, - value.first.to_i, 0) |
||||
when "<t-" |
||||
sql = date_range_clause(db_table, db_field, nil, - value.first.to_i) |
||||
when "t-" |
||||
sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) |
||||
when ">t+" |
||||
sql = date_range_clause(db_table, db_field, value.first.to_i, nil) |
||||
when "<t+" |
||||
sql = date_range_clause(db_table, db_field, 0, value.first.to_i) |
||||
when "t+" |
||||
sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i) |
||||
when "t" |
||||
sql = date_range_clause(db_table, db_field, 0, 0) |
||||
when "w" |
||||
from = l(:general_first_day_of_week) == '7' ? |
||||
# week starts on sunday |
||||
((Date.today.cwday == 7) ? Time.now.at_beginning_of_day : Time.now.at_beginning_of_week - 1.day) : |
||||
# week starts on monday (Rails default) |
||||
Time.now.at_beginning_of_week |
||||
sql = "#{db_table}.#{db_field} BETWEEN '%s' AND '%s'" % [connection.quoted_date(from), connection.quoted_date(from + 7.days)] |
||||
when "~" |
||||
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" |
||||
when "!~" |
||||
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" |
||||
end |
||||
|
||||
return sql.blank? ? "1=1" : sql |
||||
end |
||||
|
||||
# Returns a SQL clause for a date or datetime field. |
||||
def date_range_clause(table, field, from, to) |
||||
s = [] |
||||
if from |
||||
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date((Date.yesterday + from).to_time.end_of_day)]) |
||||
end |
||||
if to |
||||
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date((Date.today + to).to_time.end_of_day)]) |
||||
end |
||||
s.join(' AND ') |
||||
end |
||||
end |
@ -1,48 +0,0 @@ |
||||
<%# |
||||
This partial requires the following locals: |
||||
filter The filter object to create a row for |
||||
|
||||
The following locals are optional |
||||
index The id of the filter field |
||||
%> |
||||
|
||||
<% |
||||
@line_index ||= "---INDEX---" |
||||
|
||||
prefix = "filters[]" |
||||
id_prefix = "filters_#{@line_index}" |
||||
name_prefix = "filters[#{@line_index}]" |
||||
%> |
||||
|
||||
<% fields_for prefix, filter do |f| %> |
||||
<tr class="filter" id="<%= id_prefix %>"> |
||||
<td style="width: 200px"> |
||||
<%= f.hidden_field :scope, :index => @line_index %> |
||||
<%= f.hidden_field :column_name, :index => @line_index %> |
||||
|
||||
<%= f.check_box :enabled, :index => @line_index, :checked => true, :onclick => "toggle_filter($('#{id_prefix}'));" %> |
||||
<%= f.label :enabled, filter.label, :index => @line_index, :class => scope_icon_class(filter) %> |
||||
</td> |
||||
<td style="width: 150px;"> |
||||
<%= f.select :operator, operators_for_select(filter.type_name), {}, :index => @line_index, :onchange => "toggle_operator($('#{id_prefix}'), '#{filter.type_name}')", :class => "select-small filter_operator", :style => "vertical-align: top;" %> |
||||
</td> |
||||
<td> |
||||
<% simple_operator = CostQuery.operators[filter.operator] ? CostQuery.operators[filter.operator][:simple] : false -%> |
||||
<div class="filter_values" <%= 'style="display: none"' if simple_operator %>> |
||||
<%= render :partial => "cost_reports/filter_types/#{filter.type_name}", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
|
||||
<script type="text/javascript"> |
||||
//<![CDATA[ |
||||
|
||||
toggle_filter($('<%= id_prefix %>')); |
||||
|
||||
<% if !filter.filter_type[:multiple] %> |
||||
disable_select('<%= filter.column_name %>', '<%= (filter.scope == :issues) ? "add_issue_filter_select" : "add_cost_filter_select" %>'); |
||||
<% end %> |
||||
//]]> |
||||
</script> |
||||
|
||||
<% end %> |
@ -1,170 +0,0 @@ |
||||
<% |
||||
add_issue_filter_select = [["", ""]] |
||||
add_cost_filter_select = [["", ""]] |
||||
%> |
||||
|
||||
<script type="text/javascript"> |
||||
//<![CDATA[ |
||||
|
||||
var Filterform = Class.create({ |
||||
lineIndex: 1, |
||||
parentElement: "", |
||||
initialize: function(filters, lineIndex, parentElement) { |
||||
this.filters = filters; |
||||
this.lineIndex = lineIndex; |
||||
this.parentElement = parentElement; |
||||
}, |
||||
|
||||
add_html: function(e, scope, key, insertion) { |
||||
new Ajax.Updater({ success: e.identify() }, '<%= url_for(:action => :get_filter) %>', { |
||||
parameters: { |
||||
scope: scope, |
||||
column_name: key, |
||||
line_index: this.lineIndex++ |
||||
}, |
||||
insertion: insertion, |
||||
evalScripts: true |
||||
}); |
||||
}, |
||||
|
||||
add: function(scope, key) { |
||||
var e = $(this.parentElement); |
||||
this.add_html(e, scope, key, "bottom") |
||||
return this.lineIndex-1; |
||||
} |
||||
}); |
||||
|
||||
multiFilters = new Hash(); |
||||
<% @query.available_filters.each_pair do |scope, available_filters| -%> |
||||
multiFilters.set('<%= scope %>', [<%= available_filters.select{|k,v| CostQuery.filter_types[v[:type]][:multiple]}.collect{|k,v| "'#{k}'"}.join(", ") %>]); |
||||
<%- end %> |
||||
|
||||
|
||||
function reset_select(select) { |
||||
select.selectedIndex = 0; |
||||
} |
||||
|
||||
function add_filter(select, scope) { |
||||
var column_name = select.value; |
||||
if (!column_name) { |
||||
return reset_select(select); |
||||
} |
||||
|
||||
var filter_id = filterform.add(scope, column_name); |
||||
select.selectedIndex = 0; |
||||
} |
||||
|
||||
function toggle_filter(filter) { |
||||
var check_box = filter.down("input[type=checkbox]") |
||||
|
||||
if (check_box.checked) { |
||||
filter.down("select.filter_operator").show(); |
||||
toggle_operator(filter); |
||||
} else { |
||||
filter.down("select.filter_operator").hide(); |
||||
filter.down("div.filter_values").hide(); |
||||
} |
||||
} |
||||
|
||||
function toggle_operator(filter, filter_type) { |
||||
var operator = filter.down("select.filter_operator"); |
||||
var simple_operators = [<%= CostQuery.operators.select{|k,v| v[:simple]}.collect{|k,v| "'#{k}'"}.join(", ") %>] |
||||
|
||||
var values_field = filter.down("div.filter_values"); |
||||
|
||||
if (simple_operators.include(operator.value)) { |
||||
values_field.hide(); |
||||
} else { |
||||
values_field.show(); |
||||
} |
||||
|
||||
if (filter_type == 'date_exact') { |
||||
var between_tags = filter.down("span.between_tags"); |
||||
if (operator.value == '<>d') { |
||||
between_tags.show(); |
||||
} else { |
||||
between_tags.hide(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function toggle_multi_select(filter) { |
||||
var select = filter.down("div.filter_values").down("select"); |
||||
if (select.readAttribute("multiple")) { |
||||
select.writeAttribute("multiple", null); |
||||
} else { |
||||
select.writeAttribute("multiple", "multiple"); |
||||
} |
||||
} |
||||
|
||||
finished_loading = false; |
||||
deferred_disable_select = new Array(); |
||||
function disable_select(filter_name, select_id) { |
||||
if (finished_loading) { |
||||
select = $(select_id) |
||||
// the current filter can only be applied once |
||||
for (i=0; i<select.options.length; i++) { |
||||
var option = select.options[i]; |
||||
if (option.value == filter_name) { |
||||
option.disabled = true; |
||||
} |
||||
} |
||||
} else { |
||||
deferred_disable_select.push([filter_name, select_id]); |
||||
} |
||||
} |
||||
|
||||
Event.observe(window, 'load', function() { |
||||
finished_loading = true; |
||||
deferred_disable_select.each(function(item) { |
||||
disable_select(item[0], item[1]); |
||||
}); |
||||
}); |
||||
|
||||
IssueFilterTypes = new Array(); |
||||
<% |
||||
@query.available_filters[:issues].sort_by{|c| c[1][:order]}.each do |e| |
||||
column_name = e[0] |
||||
filter = @query.create_filter(:issues, column_name) |
||||
add_issue_filter_select << [filter.label, column_name] |
||||
-%> |
||||
IssueFilterTypes.push('<%= escape_javascript(column_name) %>'); |
||||
<% end %> |
||||
|
||||
CostEntryFilterTypes = new Array() |
||||
<% |
||||
@query.available_filters[:costs].sort_by{|c| c[1][:order]}.each do |e| |
||||
column_name = e[0] |
||||
filter = @query.create_filter(:costs, column_name) |
||||
add_cost_filter_select << [filter.label, column_name] |
||||
%> |
||||
CostEntryFilterTypes.push('<%= escape_javascript(column_name) %>'); |
||||
<% end %> |
||||
|
||||
filterform = new Filterform($H({issues: IssueFilterTypes, costs: CostEntryFilterTypes}), <%= @query.filters ? @query.filters.length : 0 %>, 'filter_table'); |
||||
//]]> |
||||
</script> |
||||
|
||||
<table width="100%"> |
||||
<tbody><tr> |
||||
<td><table id="filter_table"> |
||||
<% @query.filters.each_with_index do |filter, index| %> |
||||
<% @line_index = index %> |
||||
<%= render(:partial => "filter", :object => @query.create_filter_from_hash(filter)) %> |
||||
<% end if @query.filters %> |
||||
</table></td> |
||||
<td class="add-filter"> |
||||
<%= l(:label_cost_filter_add) %>: <%= select_tag 'add_cost_filter_select', options_for_select(add_cost_filter_select), |
||||
:onchange => "add_filter(this, 'costs');", |
||||
:class => "select-small", |
||||
:name => nil %><br /> |
||||
|
||||
<%= l(:label_issue_filter_add) %>: <%= select_tag 'add_issue_filter_select', options_for_select(add_issue_filter_select), |
||||
:onchange => "add_filter(this, 'issues');", |
||||
:class => "select-small", |
||||
:name => nil %> |
||||
</td> |
||||
</tr></tbody> |
||||
</table> |
||||
|
||||
<% include_calendar_headers_tags %> |
@ -1,18 +0,0 @@ |
||||
<script type="text/javascript"> |
||||
//<![CDATA[ |
||||
function group_by_changed() |
||||
{ |
||||
$('group_by_granularity').setStyle({ |
||||
'display': <%= @query.time_groups.inject([]){|r,t| r<<t.to_s}.inspect %>.include($("group_by_name").value) ? 'inline' : 'none'}); |
||||
} |
||||
//]]> |
||||
</script> |
||||
|
||||
<%= |
||||
select_tag "group_by[name]", options_for_select(@query.group_by_columns_for_select, (@query.group_by[:name].to_s unless @query.group_by[:name].blank?)), |
||||
:class => "select-small", :onchange => "group_by_changed();" |
||||
%> |
||||
<%= |
||||
select_tag("group_by[granularity]", options_for_select([[l(:label_year), "year"], [l(:label_month), "month"], [l(:label_week), "week"], [l(:label_day_plural), "day"]], @query.group_by[:granularity]), |
||||
:class => "select-small", :onload => "group_by_changed();", :style => ("display: none" if @query.group_by["name"].blank? || !@query.time_groups.include?(@query.group_by["name"].to_s))) |
||||
%> |
@ -1,9 +0,0 @@ |
||||
<% if @grouped_entries %> |
||||
<%= render :partial => "list_group_by" %> |
||||
<% else %> |
||||
<%= render :partial => "list_items" %> |
||||
<% end %> |
||||
|
||||
<% other_formats_links do |f| %> |
||||
<%= call_hook :view_cost_report_other_formats, :f => f %> |
||||
<% end %> |
@ -1,132 +0,0 @@ |
||||
<% |
||||
def group_by_column |
||||
CostQuery.group_by_columns[@query.group_by[:name]] |
||||
end |
||||
|
||||
def display_costs |
||||
CostEntry.column_names.include?(group_by_column[:db_field].to_s) && @query.display_cost_entries |
||||
end |
||||
|
||||
def display_time |
||||
TimeEntry.column_names.include?(group_by_column[:db_field].to_s) && @query.display_time_entries |
||||
end |
||||
|
||||
|
||||
def display_js(invert=false) |
||||
return "'' + Form.serialize('filter-options')" unless group_by_column[:scope] == :costs |
||||
|
||||
if invert |
||||
display_costs = !display_costs |
||||
display_time = !display_time |
||||
end |
||||
|
||||
if display_costs && !display_time |
||||
"'cost_query[display_cost_entries]=1&cost_query[display_time_entries]=0'" |
||||
elsif !display_costs && display_time |
||||
"'cost_query[display_cost_entries]=0&cost_query[display_time_entries]=1'" |
||||
else |
||||
"Form.serialize('filter-options')" |
||||
end |
||||
end |
||||
|
||||
|
||||
def filter_js(filter_hash) |
||||
if group_by_column[:scope] == :costs && filter_hash[:values].nil? |
||||
return "" |
||||
end |
||||
|
||||
{:filters => {(@query.filters ? @query.filters.length : 0) => filter_hash}}.to_query |
||||
end |
||||
%> |
||||
|
||||
<% if @grouped_entries.blank? %> |
||||
<p class="nodata"><%= l(:label_no_data) %></p> |
||||
<% else %> |
||||
|
||||
<%= element_hidden_warning %> |
||||
<table class="list"> |
||||
<thead> |
||||
<th>Group By</th> |
||||
<th class="units">Count</th> |
||||
<% if (@query.group_by["name"] == "cost_type_id") || (!display_costs) %><th><%= l(:caption_cost_unit_plural) %></th><% end %> |
||||
<th class="currency">Sum</th> |
||||
<th>Drill Down</th> |
||||
</thead> |
||||
<tbody> |
||||
<% |
||||
unit_sum = Hash.new(0) |
||||
@grouped_entries.each do |entry| |
||||
entry &&= entry.with_indifferent_access |
||||
filter = @query.filter_from_group_by(entry) |
||||
|
||||
if filter[:values].nil? |
||||
display_js = display_js(true) |
||||
else |
||||
display_js = display_js(false) |
||||
end |
||||
filter_js = filter_js(filter) |
||||
group_by = {:group_by=>{:name=>"", :granularity=>"year"}} |
||||
|
||||
fields = entry.keys - %w[count sum unit_sum] |
||||
if fields.include? "tmonth" |
||||
name = "#{entry[:tyear]}, #{month_name(entry["tmonth"].to_i)}" |
||||
elsif fields.include? "tweek" |
||||
name = "#{entry[:tyear]}, #{l(:week)} \##{entry["tweek"]}" |
||||
else |
||||
name = fields.map { |k| CostQuery.get_name(k, entry[k]) }.join " " |
||||
end |
||||
name.strip! |
||||
%> |
||||
|
||||
<tr class="<%= cycle('odd', 'even') %>"> |
||||
<td> |
||||
<%= name %> |
||||
</td> |
||||
<td class="units"><%=l :x_entries, :count => entry["count"].to_i %></td> |
||||
|
||||
<% if (@query.group_by["name"] == "cost_type_id") || (!display_costs) |
||||
cost_type = CostType.find_by_id(entry["cost_type_id"]) |
||||
-%> |
||||
<td class="units"> |
||||
<% if cost_type %> |
||||
<%- unit_sum[cost_type] += entry["unit_sum"].to_f -%> |
||||
<%= pluralize(entry["unit_sum"], cost_type.unit, cost_type.unit_plural) %> |
||||
<%- elsif display_costs || entry[group_by_column[:db_field]] -%> |
||||
<%- unit_sum[cost_type] += entry["unit_sum"].to_f -%> |
||||
<%= l_hours(entry["unit_sum"] || "0") %> |
||||
<%- end %> |
||||
</td> |
||||
<%- end %> |
||||
<td class="currency"><%= number_to_currency(entry["sum"]) %></td> |
||||
<td> |
||||
<%= link_to_remote "Drill Down", { |
||||
:url => { :set_filter => 1 }, |
||||
:update => "content", |
||||
:with => "original_filters + '&#{filter_js}&#{group_by.to_query}&' + #{display_js}" |
||||
} %> |
||||
</td> |
||||
</tr> |
||||
<% end %> |
||||
<tr> |
||||
<td> </td> |
||||
<td class="units"><strong><%=l :x_entries, :count => @entry_count.to_i %></strong></td> |
||||
<% if (@query.group_by["name"] == "cost_type_id") || (!display_costs) %><td class="units"> |
||||
<% if unit_sum.count == 1 |
||||
cost_type = unit_sum.keys[0] |
||||
sum = unit_sum[cost_type] |
||||
-%> |
||||
<%- if cost_type -%> |
||||
<%= pluralize(sum, cost_type.unit, cost_type.unit_plural) %> |
||||
<%- else -%> |
||||
<%= l_hours(sum || "0") %> |
||||
<%- end %> |
||||
<%- else -%> |
||||
|
||||
<%- end %> |
||||
</td><% end %> |
||||
<td class="currency"><strong><%= number_to_currency @entry_sum %></strong></td> |
||||
<td> </td> |
||||
</tr> |
||||
</tbody> |
||||
</table> |
||||
<% end %> |
@ -1,5 +0,0 @@ |
||||
<p> |
||||
<%= l(:label_display_types) %>: |
||||
<%= query_form.check_box :display_cost_entries %> <%= query_form.label :display_cost_entries, l(:field_material_costs) %> |
||||
<%= query_form.check_box :display_time_entries %> <%= query_form.label :display_time_entries, l(:field_labor_costs) %> |
||||
</p> |
@ -1 +0,0 @@ |
||||
<%= f.text_field :values, :size => 3, :index => @line_index, :class => "select-small" %> <%= l(:label_day_plural) %> |
@ -1,8 +0,0 @@ |
||||
<%= text_field_tag "#{name_prefix}[values][]", (filter.values.first if filter.values), :size => 10, :class => "select-small", :id => "#{id_prefix}_date1"%> <%= calendar_for("#{id_prefix}_date1") %> |
||||
<span id="<%= id_prefix %>_between_tags" class="between_tags" <%= 'style="display:none"' unless filter.operator == "<>d" %>> |
||||
<%= |
||||
l(:label_until) |
||||
text_field_tag("#{name_prefix}[values][]", (filter.values.last if filter.values), :size => 10, :class => "select-small", :id => "#{id_prefix}_date2") + " " + |
||||
calendar_for("#{id_prefix}_date2") |
||||
%> |
||||
</span> |
@ -1 +0,0 @@ |
||||
<%= render :partial => "cost_reports/filter_types/date", :locals => {:f => f, :filter => filter, :index => @line_index, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
@ -1 +0,0 @@ |
||||
<%= f.text_field :values, :size => 3, :index => @line_index, :class => "select-small" %> |
@ -1 +0,0 @@ |
||||
<%= render :partial => "cost_reports/filter_types/integer", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
@ -1,2 +0,0 @@ |
||||
<%= f.select :values, filter.available_values, {}, {:multiple => (filter.values && filter.values.length > 1), :index => "#{@line_index}[]", :name => "#{name_prefix}[values][]" } %> |
||||
<%= link_to_function image_tag('bullet_toggle_plus.png'), "toggle_multi_select($('#{id_prefix}'));", :style => "vertical-align: bottom;" %> |
@ -1 +0,0 @@ |
||||
<%= render :partial => "cost_reports/filter_types/list", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
@ -1 +0,0 @@ |
||||
<%= render :partial => "cost_reports/filter_types/list", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
@ -1 +0,0 @@ |
||||
<%= render :partial => "cost_reports/filter_types/list", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
@ -1 +0,0 @@ |
||||
<%= f.text_field :values, :size => 30, :index => @line_index, :class => "select-small" %> |
@ -1 +0,0 @@ |
||||
<%= render :partial => "cost_reports/filter_types/string", :locals => {:f => f, :filter => filter, :id_prefix => id_prefix, :name_prefix => name_prefix} %> |
@ -1,84 +0,0 @@ |
||||
<% if @custom_error %> |
||||
<div class="flash error"><%= @custom_error %></div> |
||||
<% end %> |
||||
|
||||
|
||||
<div class="contextual"> |
||||
<% if !@query.new_record? && @query.editable_by?(User.current) %> |
||||
<%= link_to l(:button_edit), {:controller => 'cost_report', :action => 'edit', :id => @query}, :class => 'icon icon-edit' %> |
||||
<%= link_to l(:button_delete), {:controller => 'cost_report', :action => 'destroy', :id => @query}, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-del' %> |
||||
<% end %> |
||||
</div> |
||||
|
||||
<h2><%= @query.new_record? ? l(:label_cost_report) : h(@query.name) %></h2> |
||||
<% html_title( @query.new_record? ? l(:label_cost_report) : @query.name ) %> |
||||
|
||||
<script type="text/javascript"> |
||||
//<![CDATA[ |
||||
function toggle_options(select, options, state) { |
||||
for (i=0; i<select.options.length; i++) { |
||||
var option = select.options[i]; |
||||
if (options.indexOf(option.value) >= 0) { |
||||
if (state == "disable") { |
||||
option.writeAttribute("disabled", "disabled"); |
||||
} else { |
||||
option.writeAttribute("disabled", null); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
//]]> |
||||
</script> |
||||
|
||||
|
||||
<% form_for @query, :url => {:controller => 'cost_report', :action => 'new' }, :html => {:id => 'query_form', :method => :post} do |query_form| %> |
||||
<div id="query_from_content"> |
||||
<fieldset id="filters" class="collapsible <%= "collapsed" unless @query.new_record? %>"> |
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_filter_plural) %></legend> |
||||
<div <%= 'style="display:none;"' unless @query.new_record? %>><%= render :partial => 'filters', :locals => {:query_form => query_form} %></div> |
||||
</fieldset> |
||||
|
||||
<fieldset id="group-by" class="collapsible"> |
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_group_by) %></legend> |
||||
<div><%= render :partial => 'group_by', :locals => {:query_form => query_form} %></div> |
||||
</fieldset> |
||||
|
||||
<fieldset id="filter-options" class="collapsible"> |
||||
<legend onclick="toggleFieldset(this);"><%= l(:label_option_plural) %></legend> |
||||
<div><%= render :partial => 'options', :locals => {:query_form => query_form} %></div> |
||||
</fieldset> |
||||
|
||||
<p class="buttons"> |
||||
<%= link_to_remote l(:button_apply), |
||||
{ :url => { :set_filter => 1 }, |
||||
:update => "content", |
||||
:with => "Form.serialize('query_form')" |
||||
}, :class => 'icon icon-checked' %> |
||||
|
||||
<%= link_to_remote l(:button_clear), |
||||
{ :url => { :set_filter => 1 }, |
||||
:update => "content", |
||||
}, :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> |
||||
<% end %> |
||||
|
||||
<%= javascript_tag "original_filters = Form.serialize('filters');" %> |
||||
|
||||
<%= error_messages_for 'query' %> |
||||
|
||||
<% if @query.valid? %> |
||||
<%= render :partial => 'list', :locals => {:entries => @entries, :query => @query} %> |
||||
<% end %> |
||||
|
||||
<% content_for :header_tags do %> |
||||
<%= stylesheet_link_tag 'scm' %> |
||||
<%= stylesheet_link_tag 'costs', :plugin => 'redmine_costs' %> |
||||
<% end %> |
||||
|
@ -1,13 +1,7 @@ |
||||
CostRate.class_eval do |
||||
generator_for :valid_from, :method => :next_valid_from |
||||
generator_for :rate, 10 |
||||
generator_for :cost_type, :method => :next_cost_type |
||||
|
||||
|
||||
def self.next_cost_type |
||||
CostType.last or CostType.generate! |
||||
CostType.last || CostType.generate! |
||||
end |
||||
|
||||
def self.next_valid_from |
||||
1.year.ago + Rate.count |
||||
end |
||||
end |
||||
end |
||||
|
@ -0,0 +1,8 @@ |
||||
Rate.class_eval do |
||||
generator_for :valid_from, :method => :next_valid_from |
||||
generator_for :rate, 10 |
||||
|
||||
def self.next_valid_from |
||||
1.year.ago + Rate.count |
||||
end |
||||
end |
Loading…
Reference in new issue