kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
336 lines
8.2 KiB
336 lines
8.2 KiB
11 years ago
|
#-- copyright
|
||
|
# ReportingEngine
|
||
|
#
|
||
|
# Copyright (C) 2010 - 2014 the OpenProject Foundation (OPF)
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or
|
||
|
# modify it under the terms of the GNU General Public License
|
||
|
# version 3.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
#
|
||
|
# You should have received a copy of the GNU General Public License
|
||
|
# along with this program; if not, write to the Free Software
|
||
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
#++
|
||
|
|
||
14 years ago
|
# Provides convinience layer and logic shared between GroupBy::Base and Filter::Base.
|
||
|
# Implements a double linked list (FIXME: is that the correct term?).
|
||
14 years ago
|
class Report < ActiveRecord::Base
|
||
15 years ago
|
class Chainable
|
||
15 years ago
|
include Enumerable
|
||
14 years ago
|
include Report::QueryUtils
|
||
|
extend Report::InheritedAttribute
|
||
14 years ago
|
extend Forwardable
|
||
15 years ago
|
|
||
14 years ago
|
# this attr. should point to a symbol useable for translations
|
||
10 years ago
|
inherited_attribute :applies_for, default: :label_cost_entry_attributes
|
||
8 years ago
|
def_delegators :'self.class', :table_joins, :table_name, :field, :display?, :underscore_name
|
||
15 years ago
|
|
||
15 years ago
|
def self.accepts_property(*list)
|
||
14 years ago
|
engine.accepted_properties.push(*list.map(&:to_s))
|
||
15 years ago
|
end
|
||
|
|
||
|
def self.chain_list(*list)
|
||
|
options = list.extract_options!
|
||
|
options[:list] = true
|
||
|
list << options
|
||
|
inherited_attribute(*list)
|
||
|
end
|
||
|
|
||
|
def self.base?
|
||
14 years ago
|
superclass == engine::Chainable or self == engine::Chainable or
|
||
10 years ago
|
superclass == Chainable or self == Chainable or
|
||
|
self == engine::Filter::Base or self == engine::GroupBy::Base
|
||
15 years ago
|
end
|
||
|
|
||
|
def self.base
|
||
|
return self if base?
|
||
14 years ago
|
superclass.base
|
||
15 years ago
|
end
|
||
|
|
||
|
def self.from_base(&block)
|
||
|
base.instance_eval(&block)
|
||
|
end
|
||
|
|
||
|
def self.available
|
||
|
from_base { @available ||= [] }
|
||
|
end
|
||
|
|
||
|
def self.register(label)
|
||
|
available << klass
|
||
10 years ago
|
set_inherited_attribute 'label', label
|
||
15 years ago
|
end
|
||
|
|
||
|
def self.table_joins
|
||
14 years ago
|
(@table_joins ||= []).clone
|
||
15 years ago
|
end
|
||
|
|
||
|
def self.table_from(value)
|
||
14 years ago
|
return value.table_name if value.respond_to? :table_name
|
||
15 years ago
|
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)
|
||
14 years ago
|
(@table_joins ||= []) << args
|
||
15 years ago
|
end
|
||
14 years ago
|
|
||
15 years ago
|
def self.underscore_name
|
||
|
name.demodulize.underscore
|
||
|
end
|
||
15 years ago
|
|
||
14 years ago
|
def self.put_sql_table_names(table_prefix_placement = {})
|
||
|
@table_prefix_placement ||= {}
|
||
|
@table_prefix_placement.merge! table_prefix_placement
|
||
|
@table_prefix_placement
|
||
|
end
|
||
|
|
||
14 years ago
|
##
|
||
14 years ago
|
# The given block is called when a new chain is created for a report.
|
||
14 years ago
|
# The query will be given to the block as a parameter.
|
||
|
# Example:
|
||
14 years ago
|
# initialize_query_with { |query| query.filter Report::Filter::City, :operators => '=', :values => 'Berlin, da great City' }
|
||
14 years ago
|
def self.initialize_query_with(&block)
|
||
14 years ago
|
engine.chain_initializer.push block
|
||
14 years ago
|
end
|
||
|
|
||
14 years ago
|
def self.cache_key
|
||
|
@cache_key ||= underscore_name
|
||
|
end
|
||
|
|
||
10 years ago
|
inherited_attribute :properties, list: true
|
||
15 years ago
|
|
||
12 years ago
|
def self.label
|
||
10 years ago
|
'Translation needed'
|
||
12 years ago
|
end
|
||
|
|
||
15 years ago
|
class << self
|
||
|
alias inherited_attributes inherited_attribute
|
||
|
alias accepts_properties accepts_property
|
||
|
end
|
||
|
|
||
15 years ago
|
attr_accessor :parent, :child, :type
|
||
|
accepts_property :type
|
||
15 years ago
|
|
||
15 years ago
|
def each(&block)
|
||
|
yield self
|
||
|
child.try(:each, &block)
|
||
|
end
|
||
|
|
||
15 years ago
|
def row?
|
||
|
type == :row
|
||
|
end
|
||
|
|
||
|
def column?
|
||
|
type == :column
|
||
15 years ago
|
end
|
||
|
|
||
15 years ago
|
def group_by?
|
||
|
!filter?
|
||
|
end
|
||
|
|
||
15 years ago
|
def to_a
|
||
14 years ago
|
[to_hash].tap { |a| a.unshift(*child.to_a) unless bottom? }
|
||
15 years ago
|
end
|
||
|
|
||
|
def top
|
||
|
return self if top?
|
||
|
parent.top
|
||
|
end
|
||
|
|
||
|
def top?
|
||
|
parent.nil?
|
||
|
end
|
||
|
|
||
|
def bottom?
|
||
|
child.nil?
|
||
|
end
|
||
14 years ago
|
|
||
15 years ago
|
def bottom
|
||
|
return self if bottom?
|
||
|
child.bottom
|
||
|
end
|
||
|
|
||
|
def initialize(child = nil, options = {})
|
||
15 years ago
|
@options = options
|
||
15 years ago
|
options.each do |key, value|
|
||
14 years ago
|
unless self.class.extra_options.include? key
|
||
14 years ago
|
raise ArgumentError, "may not set #{key}" unless engine.accepted_properties.include? key.to_s
|
||
14 years ago
|
send "#{key}=", value
|
||
14 years ago
|
end
|
||
15 years ago
|
end
|
||
15 years ago
|
self.child, child.parent = child, self if child
|
||
|
move_down until correct_position?
|
||
15 years ago
|
clear
|
||
15 years ago
|
end
|
||
|
|
||
15 years ago
|
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
|
||
|
|
||
14 years ago
|
def serialize
|
||
|
[self.class.to_s.demodulize, @options]
|
||
|
end
|
||
|
|
||
15 years ago
|
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)
|
||
15 years ago
|
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?
|
||
10 years ago
|
[* send(name, *args, &block)].push(*subchain).compact.uniq
|
||
15 years ago
|
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
|
||
|
|
||
15 years ago
|
def clear
|
||
15 years ago
|
@cached = nil
|
||
15 years ago
|
child.try :clear
|
||
|
end
|
||
|
|
||
15 years ago
|
def result
|
||
15 years ago
|
cached(:compute_result)
|
||
15 years ago
|
end
|
||
|
|
||
|
def compute_result
|
||
14 years ago
|
engine::Result.new engine.reporting_connection.select_all(sql_statement.to_s), {}, type
|
||
15 years ago
|
end
|
||
|
|
||
15 years ago
|
def cached(*args)
|
||
|
@cached ||= {}
|
||
|
@cached[args] ||= send(*args)
|
||
|
end
|
||
|
|
||
15 years ago
|
def sql_statement
|
||
|
raise "should not get here (#{inspect})" if bottom?
|
||
15 years ago
|
child.cached(:sql_statement).tap do |q|
|
||
|
chain_collect(:table_joins).each { |args| q.join(*args) } if responsible_for_sql?
|
||
|
end
|
||
15 years ago
|
end
|
||
|
|
||
14 years ago
|
inherited_attribute :db_field
|
||
15 years ago
|
def self.field
|
||
14 years ago
|
db_field || (name[/[^:]+$/] || name).to_s.underscore
|
||
15 years ago
|
end
|
||
|
|
||
14 years ago
|
def display?
|
||
|
self.class.display?
|
||
|
end
|
||
|
|
||
10 years ago
|
inherited_attribute :display, default: true
|
||
14 years ago
|
def self.display!
|
||
|
display true
|
||
|
end
|
||
|
|
||
|
def self.display?
|
||
|
!!display
|
||
|
end
|
||
|
|
||
|
def self.dont_display!
|
||
|
display false
|
||
14 years ago
|
not_selectable!
|
||
14 years ago
|
end
|
||
|
|
||
10 years ago
|
inherited_attribute :selectable, default: true
|
||
14 years ago
|
def self.selectable!
|
||
|
selectable true
|
||
|
end
|
||
|
|
||
|
def self.selectable?
|
||
|
!!selectable
|
||
|
end
|
||
|
|
||
|
def self.not_selectable!
|
||
|
selectable false
|
||
|
end
|
||
|
|
||
14 years ago
|
# Extra options this chainable accepts that are not defined in accepted_properties
|
||
|
def self.extra_options(*symbols)
|
||
|
@extra_option ||= []
|
||
|
@extra_option += symbols
|
||
|
end
|
||
|
|
||
14 years ago
|
# This chainable type can only ever occur once in a chain
|
||
|
def self.singleton
|
||
|
class << self
|
||
|
def new(chain = nil, options = {})
|
||
10 years ago
|
return chain if chain and chain.map(&:class).include? self
|
||
14 years ago
|
super
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
15 years ago
|
def self.last_table
|
||
14 years ago
|
@last_table ||= engine::Filter::NoFilter.table_name
|
||
15 years ago
|
end
|
||
|
|
||
14 years ago
|
def self.table_name(value = nil)
|
||
|
@table_name = table_name_for(value) if value
|
||
|
@table_name || last_table
|
||
|
end
|
||
|
|
||
15 years ago
|
def with_table(fields)
|
||
14 years ago
|
fields.map do |f|
|
||
|
place_field_name = self.class.put_sql_table_names[f] || self.class.put_sql_table_names[f].nil?
|
||
|
place_field_name ? (field_name_for f, self) : f
|
||
|
end
|
||
15 years ago
|
end
|
||
|
|
||
14 years ago
|
def mapping
|
||
|
self.class.method(:mapping).to_proc
|
||
|
end
|
||
|
|
||
|
def self.mapping(value)
|
||
|
value.to_s
|
||
|
end
|
||
|
|
||
|
def self.mapping_for(field)
|
||
10 years ago
|
@field_map ||= (engine::Filter.all + engine.GroupBy.all).inject(Hash.new { |h, k| h[k] = [] }) do |hash, cbl|
|
||
14 years ago
|
hash[cbl.field] << cbl.mapping
|
||
|
end
|
||
|
@field_map[field]
|
||
|
end
|
||
15 years ago
|
end
|
||
14 years ago
|
end
|