#-- copyright # OpenProject is an open source project management software. # Copyright (C) 2012-2021 the OpenProject GmbH # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 3. # # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # Copyright (C) 2006-2013 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # 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. # # See docs/COPYRIGHT.rdoc for more details. #++ # Provides convenience layer and logic shared between GroupBy::Base and Filter::Base. # Implements a double linked list (FIXME: is that the correct term?). module Report class Chainable include Enumerable include Report::QueryUtils extend Report::InheritedAttribute extend Forwardable # this attr. should point to a symbol useable for translations inherited_attribute :applies_for, default: :label_cost_entry_attributes def_delegators :'self.class', :table_joins, :table_name, :field, :display?, :underscore_name def self.accepts_property(*list) engine.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 == Report::Chainable or self == Report::Chainable or superclass == Chainable or self == Chainable or self == Report::Filter::Base or self == Report::GroupBy::Base end def self.base return self if base? superclass.base 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 ||= []).clone 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 def self.put_sql_table_names(table_prefix_placement = {}) @table_prefix_placement ||= {} @table_prefix_placement.merge! table_prefix_placement @table_prefix_placement end ## # The given block is called when a new chain is created for a report. # The query will be given to the block as a parameter. # Example: # initialize_query_with { |query| query.filter Report::Filter::City, :operators => '=', :values => 'Berlin, da great City' } def self.initialize_query_with(&block) engine.chain_initializer.push block end def self.cache_key @cache_key ||= underscore_name end inherited_attribute :properties, list: true def self.label 'Translation needed' end 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 [to_hash].tap { |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| unless self.class.extra_options.include? key raise ArgumentError, "may not set #{key}" unless engine.accepted_properties.include? key.to_s send "#{key}=", value end end if child self.child = child child.parent = self end move_down until correct_position? clear 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 serialize [self.class.to_s.demodulize, @options] 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 engine::Result.new engine.reporting_connection.select_all(sql_statement.to_s), {}, type 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 def display? self.class.display? 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 # Extra options this chainable accepts that are not defined in accepted_properties def self.extra_options(*symbols) @extra_option ||= [] @extra_option += symbols end # This chainable type can only ever occur once in a chain def self.singleton class << self def new(chain = nil, options = {}) return chain if chain and chain.map(&:class).include? self super end end end def self.last_table @last_table ||= engine::Filter::NoFilter.table_name end def self.table_name(value = nil) @table_name = table_name_for(value) if value @table_name || last_table end def with_table(fields) 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 end def mapping self.class.method(:mapping).to_proc end def self.mapping(value) value.to_s end def self.mapping_for(field) @field_map ||= (engine::Filter.all + engine.GroupBy.all).inject(Hash.new { |h, k| h[k] = [] }) do |hash, cbl| hash[cbl.field] << cbl.mapping end @field_map[field] end end end