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.
383 lines
10 KiB
383 lines
10 KiB
#-- 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.
|
|
#++
|
|
|
|
class Report::Operator
|
|
include Report::QueryUtils
|
|
include Report::Validation
|
|
extend Forwardable
|
|
|
|
#############################################################################################
|
|
# Wrapped so we can place this at the top of the file.
|
|
def self.define_operators # :nodoc:
|
|
# Defaults
|
|
defaults do
|
|
def_delegators :'singleton_class', :forced?, :force!, :forced
|
|
|
|
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 ||= self.class.name
|
|
end
|
|
end
|
|
|
|
# Operators from Redmine
|
|
new '>t-', label: :label_less_than_ago do
|
|
include DateRange
|
|
def modify(query, field, value)
|
|
super query, field, -value.to_i, 0
|
|
end
|
|
end
|
|
|
|
new 'w', arity: 0, label: :label_this_week do
|
|
def modify(query, field, offset = nil)
|
|
offset ||= 0
|
|
first_day = begin
|
|
Integer I18n.t(:general_first_day_of_week)
|
|
rescue ArgumentError
|
|
1 # assume mondays
|
|
end
|
|
|
|
from = Time.now.at_beginning_of_week + ((first_day % 7) - 1).days
|
|
from -= offset.days
|
|
'<>d'.to_operator.modify query, field, from, from + 7.days
|
|
end
|
|
end
|
|
|
|
new 't+', label: :label_in do
|
|
include DateRange
|
|
def modify(query, field, *values)
|
|
super query, field, values.first.to_i, values.first.to_i
|
|
end
|
|
end
|
|
|
|
new '<=', label: :label_less_or_equal
|
|
|
|
new '!', label: :label_not_equals 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-', label: :label_ago do
|
|
include DateRange
|
|
def modify(query, field, *values)
|
|
super query, field, -values.first.to_i, -values.first.to_i
|
|
end
|
|
end
|
|
|
|
new '!~', arity: 1, label: :label_not_contains 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 '=', label: :label_equals do
|
|
def modify(query, field, *values)
|
|
case
|
|
when values.size == 1 && values.first.nil?
|
|
query.where "#{field} IS NULL"
|
|
when values.compact.empty?
|
|
query.where '1=0'
|
|
else
|
|
query.where "#{field} IN #{collection(*values)}"
|
|
end
|
|
query
|
|
end
|
|
end
|
|
|
|
new '~', arity: 1, label: :label_contains 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+', label: :label_in_less_than do
|
|
include DateRange
|
|
def modify(query, field, value)
|
|
super query, field, 0, value.to_i
|
|
end
|
|
end
|
|
|
|
new 't', label: :label_today do
|
|
include DateRange
|
|
def modify(query, field)
|
|
super query, field, 0, 0
|
|
end
|
|
end
|
|
|
|
new '>=', label: :label_greater_or_equal
|
|
|
|
new '!*', arity: 0, where_clause: '%s IS NULL', label: :label_none
|
|
|
|
new '<t-', label: :label_more_than_ago do
|
|
include DateRange
|
|
def modify(query, field, value)
|
|
super query, field, nil, -value.to_i
|
|
end
|
|
end
|
|
|
|
new '>t+', label: :label_in_more_than 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', label: :label_all
|
|
|
|
# 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} = #{parse_number_string(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 '>=d', label: :label_days_ago, validate: :integers do
|
|
force! :integers
|
|
|
|
def modify(query, field, value)
|
|
now = Time.now
|
|
from = (now - value.to_i.days).beginning_of_day
|
|
'<>d'.to_operator.modify query, field, from, now
|
|
end
|
|
end
|
|
|
|
new '?=', label: :label_null_or_equal do
|
|
def modify(query, field, *values)
|
|
where_clause = "(#{field} IS NULL"
|
|
where_clause += " OR #{field} IN #{collection(*values)}" unless values.compact.empty?
|
|
where_clause += ')'
|
|
query.where where_clause
|
|
query
|
|
end
|
|
end
|
|
|
|
new '?!', label: :label_not_null_and_not_equal do
|
|
def modify(query, field, *values)
|
|
where_clause = "(#{field} IS NOT NULL"
|
|
where_clause += " AND #{field} NOT IN #{collection(*values)}" unless values.compact.empty?
|
|
where_clause += ')'
|
|
query.where where_clause
|
|
query
|
|
end
|
|
end
|
|
end
|
|
#############################################################################################
|
|
|
|
module CoreExt
|
|
::String.send :include, self
|
|
::Symbol.send :include, self
|
|
def to_operator
|
|
Report::Operator.find self
|
|
end
|
|
end
|
|
|
|
def self.force!(type)
|
|
@force = type
|
|
end
|
|
|
|
def self.forced?
|
|
!!@force
|
|
end
|
|
|
|
def self.forced
|
|
@force
|
|
end
|
|
|
|
def self.new(name, values = {}, &block)
|
|
all[name.to_s] ||= super
|
|
end
|
|
|
|
# TODO: this should be inheritable by subclasses
|
|
def self.all
|
|
@@all_operators ||= {}
|
|
end
|
|
|
|
def self.load
|
|
return if @done
|
|
@done = true
|
|
define_operators
|
|
end
|
|
|
|
def self.find(name)
|
|
all[name.to_s] or raise ArgumentError, "Operator #{name.inspect} not defined"
|
|
end
|
|
|
|
def self.exists?(name)
|
|
all.has_key?(name.to_s)
|
|
end
|
|
|
|
def self.defaults(&block)
|
|
class_eval &block
|
|
end
|
|
|
|
def self.default_operator
|
|
find '='
|
|
end
|
|
|
|
def self.integer_operators
|
|
['<', '>', '<=', '>='].map(&:to_operator)
|
|
end
|
|
|
|
def self.null_operators
|
|
['*', '!*'].map(&:to_operator)
|
|
end
|
|
|
|
def self.string_operators
|
|
['!~', '~'].map(&:to_operator)
|
|
end
|
|
|
|
def self.time_operators
|
|
# ["t-", "t+", ">t-", "<t-", ">t+", "<t+"].map { |s| s.to_operator}
|
|
['t', 'w', '<>d', '>d', '<d', '=d', '>=d'].map(&:to_operator)
|
|
end
|
|
|
|
def self.default_operators
|
|
['=', '!'].map(&: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|
|
|
singleton_class.class_eval { define_method(key) { value } }
|
|
end
|
|
singleton_class.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)
|
|
name <=> other.name
|
|
end
|
|
|
|
## Creates an alias for a given operator.
|
|
def aka(alt_name, alt_label)
|
|
all = self.class.all
|
|
alt = alt_name.to_s
|
|
raise ArgumentError, "Can't alias operator with an existing one's name ( #{alt} )." if all.has_key?(alt)
|
|
op = all[name].clone
|
|
op.send(:rename_to, alt_name)
|
|
op.singleton_class.send(:define_method, 'label') { alt_label }
|
|
all[alt] = op
|
|
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
|
|
|
|
private
|
|
|
|
def rename_to(new_name)
|
|
@name = new_name
|
|
end
|
|
|
|
# Done with class method definition, let's initialize the operators
|
|
load
|
|
end
|
|
|