diff --git a/app/controllers/api/experimental/queries_controller.rb b/app/controllers/api/experimental/queries_controller.rb index 356b7cca67..dfc592be96 100644 --- a/app/controllers/api/experimental/queries_controller.rb +++ b/app/controllers/api/experimental/queries_controller.rb @@ -160,14 +160,14 @@ module Api::Experimental end def fetch_custom_field_filters(project) - filters = Queries::WorkPackages::Filter::CustomFieldFilter.create(project) + filters = Queries::WorkPackages::Filter::CustomFieldFilter.all_for(project) - filters.each_with_object({}) do |(key, filter), hash| - new_key = API::Utilities::PropertyNameConverter.from_ar_name(key) + filters.each_with_object({}) do |filter, hash| + new_key = API::Utilities::PropertyNameConverter.from_ar_name(filter.name) hash[new_key] = { type: filter.type, - values: filter.values, + values: filter.allowed_values, order: filter.order, - name: filter.name } + name: filter.human_name } end end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index dda1cb9af1..d7490ddf7f 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -56,21 +56,6 @@ module IssuesHelper #{@cached_label_priority}: #{h(issue.priority.name)}".html_safe) end - # Find the name of an associated record stored in the field attribute - def find_name_by_reflection(field, id) - association = WorkPackage.reflect_on_association(field.to_sym) - if association - record = association.class_name.constantize.find_by(id: id) - return record.name if record - end - end - - def entries_for_filter_select_sorted(query) - [['', '']] + query.available_work_package_filters.map { |field| [field[1][:name] || WorkPackage.human_attribute_name(field[0]), field[0]] unless query.has_filter?(field[0]) }.compact.sort_by { |el| - ActiveSupport::Inflector.transliterate(el[0]).downcase - } - end - def last_issue_note(issue) note_journals = issue.journals.select(&:notes?) return t(:text_no_notes) if note_journals.empty? diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb index 0c993d4555..00a4c0541a 100644 --- a/app/helpers/queries_helper.rb +++ b/app/helpers/queries_helper.rb @@ -29,11 +29,19 @@ module QueriesHelper def operators_for_select(filter_type) - Queries::Filter.operators_by_filter_type[filter_type].map { |o| [l(Queries::Filter.operators[o]), o] } + Queries::BaseFilter.operators_by_filter_type[filter_type].map { |o| [l(Queries::BaseFilter.operators[o]), o] } + end + + def entries_for_filter_select_sorted(query) + [['', '']] + + query.available_filters + .reject { |filter| query.has_filter?(filter.name) } + .map { |filter| [filter.human_name, filter.name] } + .sort_by { |el| ActiveSupport::Inflector.transliterate(el[0]).downcase } end def column_locale(column) - (column.is_a? QueryCustomFieldColumn) ? column.custom_field.name_locale : nil + column.is_a?(QueryCustomFieldColumn) ? column.custom_field.name_locale : nil end def add_filter_from_params @@ -62,7 +70,7 @@ module QueriesHelper if params[:fields] || params[:f] add_filter_from_params else - @query.available_work_package_filters.keys.each do |field| + @query.available_filters.map(&:name).each do |field| @query.add_short_filter(field, params[field]) if params[field] end end @@ -163,7 +171,7 @@ module QueriesHelper return [] if field_names.nil? context = WorkPackage.new - available_keys = query.available_work_package_filters.keys + available_keys = query.available_filters.map(&:name) field_names .map { |name| API::Utilities::PropertyNameConverter.to_ar_name name, context: context } diff --git a/app/models/custom_value/bool_strategy.rb b/app/models/custom_value/bool_strategy.rb index 12d5702b04..37d1900ef2 100644 --- a/app/models/custom_value/bool_strategy.rb +++ b/app/models/custom_value/bool_strategy.rb @@ -28,6 +28,9 @@ #++ class CustomValue::BoolStrategy < CustomValue::FormatStrategy + DB_VALUE_FALSE = 'f'.freeze + DB_VALUE_TRUE = 't'.freeze + def value_present? # can't use :blank? safely, because false.blank? == true # can't use :present? safely, because false.present? == false @@ -44,9 +47,9 @@ class CustomValue::BoolStrategy < CustomValue::FormatStrategy if !value_present? nil elsif ActiveRecord::Type::Boolean::FALSE_VALUES.include?(value) - 'f' + DB_VALUE_FALSE else - 't' + DB_VALUE_TRUE end end diff --git a/app/models/queries/work_packages/available_filter_options.rb b/app/models/queries/available_filters.rb similarity index 55% rename from app/models/queries/work_packages/available_filter_options.rb rename to app/models/queries/available_filters.rb index 80d489ac56..9774c033cd 100644 --- a/app/models/queries/work_packages/available_filter_options.rb +++ b/app/models/queries/available_filters.rb @@ -27,59 +27,74 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -module Queries::WorkPackages::AvailableFilterOptions - def available_work_package_filters - uninitialized = registered_work_package_filters - already_initialized_work_package_filters +module Queries::AvailableFilters + def available_filters + uninitialized = registered_filters - already_initialized_filters uninitialized.each do |filter| - initialize_work_package_filter(filter) + initialize_filter(filter) end - initialized_available_work_package_filters + initialized_filters.select(&:available?) end - def work_package_filter_available?(key) + def filter_for(key, no_memoization = false) + filter_instance = get_initialized_filter(key, no_memoization) || Queries::NotExistingFilter.new + filter_instance.name = key + + filter_instance + end + + private + + def get_initialized_filter(key, no_memoization) filter = find_registered_filter(key) return unless filter - initialize_work_package_filter(filter) + if no_memoization + filter.new + else + initialize_filter(filter) - initialized_available_work_package_filters[key] + find_initialized_filter(key) + end end - private - - def initialize_work_package_filter(filter) - return if already_initialized_work_package_filters.include?(filter) - already_initialized_work_package_filters << filter - - new_filters = filter.create(project) + def initialize_filter(filter) + return if already_initialized_filters.include?(filter) + already_initialized_filters << filter - available_filters = new_filters.reject { |_, f| !f.available? } + new_filters = filter.all_for(context) - initialized_available_work_package_filters.merge! available_filters + initialized_filters.push(*Array(new_filters)) end def find_registered_filter(key) - registered_work_package_filters.detect do |f| + registered_filters.detect do |f| f.key === key.to_sym end end - def already_initialized_work_package_filters - @already_initialized_work_package_filters ||= [] + def find_initialized_filter(key) + initialized_filters.detect do |f| + f.name == key.to_sym + end + end + + def already_initialized_filters + @already_initialized_filters ||= [] end - def registered_work_package_filters - @registered_work_package_filters ||= filter_register.filters + def registered_filters + @registered_filters ||= filter_register end - def initialized_available_work_package_filters - @initialized_available_work_package_filters ||= {}.with_indifferent_access + def initialized_filters + @initialized_filters ||= [] end def filter_register - Queries::WorkPackages::FilterRegister + Queries::Register.filters[self.class] end end diff --git a/app/models/queries/available_orders.rb b/app/models/queries/available_orders.rb new file mode 100644 index 0000000000..d7667b240c --- /dev/null +++ b/app/models/queries/available_orders.rb @@ -0,0 +1,46 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module Queries::AvailableOrders + def order_for(key) + (find_registered_order(key) || Queries::NotExistingOrder).new(key) + end + + private + + def find_registered_order(key) + orders_register.detect do |s| + s.key === key.to_sym + end + end + + def orders_register + Queries::Register.orders[self.class] + end +end diff --git a/app/models/queries/filter.rb b/app/models/queries/base_filter.rb similarity index 56% rename from app/models/queries/filter.rb rename to app/models/queries/base_filter.rb index f10c2e0b21..1fc505e1de 100644 --- a/app/models/queries/filter.rb +++ b/app/models/queries/base_filter.rb @@ -27,18 +27,86 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::Filter +class Queries::BaseFilter include ActiveModel::Validations - include ActiveModel::Serialization + include Queries::SqlForField - class_attribute :filter_types_by_field, instance_writer: false + attr_accessor :context - self.filter_types_by_field = { - created_at: :date_past, - updated_at: :date_past - } + class_attribute :model + + @@filter_params = [:operator, :values] + + attr_accessor *@@filter_params + + def initialize(options = {}) + self.context = options[:context] + + @@filter_params.each do |param_field| + send("#{param_field}=", options[param_field]) + end + end + + def [](name) + send(name) + end + + def name + @name || self.class.key + end + alias :field :name + + def name=(name) + @name = name.to_sym + end + + def human_name + raise NotImplementedError + end - @@filter_params = [:operator, :values] # will be serialized and persisted with the query + def type + raise NotImplementedError + end + + def allowed_values + nil + end + + def available? + true + end + + def scope + scope = model.where(where) + scope = scope.joins(joins) if joins + scope + end + + def self.key + name.to_sym + end + + def self.name + to_s.demodulize.underscore.gsub(/_filter$/, '') + end + + def self.connection + model.connection + end + + def self.all_for(context = nil) + filter = new + filter.context = context + filter + end + + def where + sql_for_field(self.class.key, operator, values, self.class.model.table_name, self.class.key) + end + + def joins + nil + end @@operators = { label_equals: '=', @@ -77,87 +145,64 @@ class Queries::Filter cattr_reader :operators, :operators_by_filter_type - attr_accessor :field, *@@filter_params + attr_accessor *@@filter_params - validates_presence_of :field, :operator - validate :validate_presence_of_values, unless: Proc.new { |filter| @@operators_not_requiring_values.include?(filter.operator) } + validate :validate_inclusion_of_operator + validate :validate_presence_of_values, + unless: Proc.new { |filter| @@operators_not_requiring_values.include?(filter.operator) } validate :validate_filter_values - def initialize(field = nil, options = {}) - self.field = field - values = [] - - @@filter_params.each do |param_field| - send("#{param_field}=", options[param_field]) - end - - stringify_values - end - - # (de-)serialization - def self.from_hash(filter_hash) - filter_hash.keys.map { |field| new(field, filter_hash[field]) } - end - - def to_hash - { field => attributes_hash } - end - - alias_method :name, :field - - def attributes - { name: name, operator: operator, values: values } - end - - def field=(field) - @field = field.try :to_sym + def values + @values || [] end - def possible_types_by_operator - @@operators_by_filter_type.select { |_key, operators| operators.include?(operator) }.keys.sort - end - - def type - filter_types_by_field[field] - end - - def ==(filter) - filter.attributes_hash == attributes_hash + def values=(values) + @values = Array(values).reject(&:blank?).map(&:to_s) end protected - def attributes_hash - @@filter_params.inject({}) do |params, param_field| - params.merge(param_field => send(param_field)) - end - end - - private - - def stringify_values - unless values.nil? - values.map!(&:to_s) + def validate_inclusion_of_operator + unless @@operators_by_filter_type[type].include? operator + errors.add(:operator, :inclusion) end end def validate_presence_of_values - errors.add(:values, I18n.t('activerecord.errors.messages.blank')) if values.nil? || values.reject(&:blank?).empty? + if values.nil? || values.reject(&:blank?).empty? + errors.add(:values, I18n.t('activerecord.errors.messages.blank')) + end end def validate_filter_values return true if @@operators_not_requiring_values.include?(operator) case type - when :integer - errors.add(:values, I18n.t('activerecord.errors.messages.not_an_integer')) unless values.all? { |value| is_integer?(value) } - when :date, :date_past - errors.add(:values, I18n.t('activerecord.errors.messages.not_an_integer')) unless values.all? { |value| is_integer?(value) } - # ... + when :integer, :date, :date_past + validate_values_all_integer + when :list, :list_optional + validate_values_in_allowed_values_list + end + end + + def validate_values_all_integer + unless values.all? { |value| integer?(value) } + errors.add(:values, I18n.t('activerecord.errors.messages.not_an_integer')) + end + end + + def validate_values_in_allowed_values_list + # TODO: the -1 is a special value that exists for historical reasons + # so one can send the operator '=' and the values ['-1'] + # which results in a IS NULL check in the DB + if (values & (allowed_values.map(&:last).map(&:to_s) + ['-1'])) != values + errors.add(:values, :inclusion) end end - def is_integer?(str) - true if Integer(str) rescue false + def integer?(str) + true if Integer(str) + rescue + false end end diff --git a/app/models/queries/base_order.rb b/app/models/queries/base_order.rb new file mode 100644 index 0000000000..40f6713533 --- /dev/null +++ b/app/models/queries/base_order.rb @@ -0,0 +1,66 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::BaseOrder + include ActiveModel::Validations + + validates :direction, inclusion: { in: %i(asc desc) } + + class_attribute :model + attr_accessor :direction, + :attribute + + def initialize(attribute) + self.attribute = attribute + end + + def self.key + raise NotImplementedError + end + + def scope + scope = order + scope = scope.joins(joins) if joins + scope + end + + def name + attribute + end + + private + + def order + model.order(name => direction) + end + + def joins + nil + end +end diff --git a/app/models/queries/base_query.rb b/app/models/queries/base_query.rb new file mode 100644 index 0000000000..ead68a5929 --- /dev/null +++ b/app/models/queries/base_query.rb @@ -0,0 +1,124 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::BaseQuery + class << self + attr_accessor :model, :default_scope + end + + include Queries::AvailableFilters + include Queries::AvailableOrders + include ActiveModel::Validations + + validate :filters_valid, + :sortation_valid + + def initialize + @scope = self.class.default_scope + @filters = [] + @orders = [] + end + + def results + if valid? + filters.each do |filter| + self.scope = scope.merge(filter.scope) + end + + orders.each do |order| + self.scope = scope.merge(order.scope) + end + else + empty_scope + end + + scope + end + + def where(attribute, operator, values) + filter = filter_for(attribute) + filter.operator = operator + filter.values = values + filter.context = context + + filters << filter + + self + end + + def order(hash) + hash.each do |attribute, direction| + order = order_for(attribute) + order.direction = direction + orders << order + end + + self + end + + protected + + attr_accessor :scope, + :filters, + :orders + + def filters_valid + filters.each do |filter| + next if filter.valid? + + add_error(:filters, filter.human_name, filter) + end + end + + def sortation_valid + orders.each do |order| + next if order.valid? + + add_error(:orders, order.class.key, order) + end + end + + def add_error(local_attribute, attribute_name, object) + messages = object + .errors + .messages + .values + .flatten + .join(" #{I18n.t('support.array.sentence_connector')} ") + + errors.add local_attribute, errors.full_message(attribute_name, messages) + end + + def empty_scope + self.scope = self.class.model.where(Arel::Nodes::Equality.new(1, 0)) + end + + def context + nil + end +end diff --git a/app/models/queries/filter_serializer.rb b/app/models/queries/filter_serializer.rb index cf35267469..3b0f217e24 100644 --- a/app/models/queries/filter_serializer.rb +++ b/app/models/queries/filter_serializer.rb @@ -28,12 +28,25 @@ #++ module Queries::FilterSerializer + extend Queries::AvailableFilters + def self.load(serialized_filter_hash) return [] if serialized_filter_hash.nil? - Queries::WorkPackages::Filter.from_hash(YAML.load(serialized_filter_hash) || {}) + + (YAML.load(serialized_filter_hash) || {}).each_with_object([]) do |(field, options), array| + options = options.with_indifferent_access + filter = filter_for(field, true) + filter.operator = options['operator'] + filter.values = options['values'] + array << filter + end end def self.dump(filters) YAML.dump ((filters || []).map(&:to_hash).reduce(:merge) || {}).stringify_keys end + + def self.filter_register + Queries::Register.filters[Query] + end end diff --git a/app/models/queries/not_existing_filter.rb b/app/models/queries/not_existing_filter.rb new file mode 100644 index 0000000000..2f808f8e97 --- /dev/null +++ b/app/models/queries/not_existing_filter.rb @@ -0,0 +1,55 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::NotExistingFilter < Queries::BaseFilter + def available? + false + end + + def type + :inexistent + end + + def self.key + :not_existent + end + + def human_name + name + end + + validate :always_false + + def always_false + errors.add :base, I18n.t(:'activerecord.errors.messages.does_not_exist') + end + + # deactivating superclass validation + def validate_inclusion_of_operator; end +end diff --git a/app/models/queries/not_existing_order.rb b/app/models/queries/not_existing_order.rb new file mode 100644 index 0000000000..7137fb6173 --- /dev/null +++ b/app/models/queries/not_existing_order.rb @@ -0,0 +1,42 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::NotExistingOrder < Queries::BaseOrder + validate :always_false + + def self.key + :inexistent + end + + private + + def always_false + errors.add :base, I18n.t(:'activerecord.errors.messages.does_not_exist') + end +end diff --git a/app/models/queries/register.rb b/app/models/queries/register.rb new file mode 100644 index 0000000000..5f299538ea --- /dev/null +++ b/app/models/queries/register.rb @@ -0,0 +1,40 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +require Rails.root.join('config/constants/query_register') + +module Queries::Register + class << self + delegate :filter, + :filters, + :order, + :orders, + to: ::Constants::QueryRegister + end +end diff --git a/app/models/queries/sql_for_field.rb b/app/models/queries/sql_for_field.rb new file mode 100644 index 0000000000..71ee2785d7 --- /dev/null +++ b/app/models/queries/sql_for_field.rb @@ -0,0 +1,125 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module Queries::SqlForField + private + + # Helper method to generate the WHERE sql for a +field+, +operator+ and a +values+ array + def sql_for_field(field, operator, values, db_table, db_field, is_custom_filter = false) + # code expects strings (e.g. for quoting), but ints would work as well: unify them here + values = values.map(&:to_s) + + sql = '' + case operator + when '=' + if values.present? + if values.include?('-1') + sql = "#{db_table}.#{db_field} IS NULL OR " + end + + sql += "#{db_table}.#{db_field} IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + ')' + else + # empty set of allowed values produces no result + sql = '0=1' + end + when '!' + if values.present? + sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + '))' + else + # empty set of forbidden values allows all results + sql = '1=1' + end + 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 '>=' + if is_custom_filter + sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) >= #{values.first.to_f}" + else + sql = "#{db_table}.#{db_field} >= #{values.first.to_f}" + end + when '<=' + if is_custom_filter + sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) <= #{values.first.to_f}" + else + sql = "#{db_table}.#{db_field} <= #{values.first.to_f}" + end + when 'o' + sql = "#{Status.table_name}.is_closed=#{connection.quoted_false}" if field == 'status_id' + when 'c' + sql = "#{Status.table_name}.is_closed=#{connection.quoted_true}" if field == 'status_id' + when '>t-' + sql = date_range_clause(db_table, db_field, - values.first.to_i, 0) + when 't+' + sql = date_range_clause(db_table, db_field, values.first.to_i, nil) + when ' '%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 + + def connection + self.class.connection + end +end diff --git a/app/models/queries/users.rb b/app/models/queries/users.rb new file mode 100644 index 0000000000..e57f8c5c04 --- /dev/null +++ b/app/models/queries/users.rb @@ -0,0 +1,38 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module Queries::Users + Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::NameFilter + Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::GroupFilter + Queries::Register.filter Queries::Users::UserQuery, Queries::Users::Filters::StatusFilter + + Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::DefaultOrder + Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::NameOrder + Queries::Register.order Queries::Users::UserQuery, Queries::Users::Orders::GroupOrder +end diff --git a/app/models/queries/work_packages/filter/base_filter.rb b/app/models/queries/users/filters/group_filter.rb similarity index 73% rename from app/models/queries/work_packages/filter/base_filter.rb rename to app/models/queries/users/filters/group_filter.rb index 88ab587064..0a275d54ad 100644 --- a/app/models/queries/work_packages/filter/base_filter.rb +++ b/app/models/queries/users/filters/group_filter.rb @@ -27,43 +27,34 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::BaseFilter - attr_accessor :project - - def initialize(project) - self.project = project - end - - def [](name) - send(name) +class Queries::Users::Filters::GroupFilter < Queries::Users::Filters::UserFilter + def allowed_values + @allowed_values ||= begin + ::Group.pluck(:name, :id).map { |g| [g[0], g[1].to_s] } + end end - def name - WorkPackage.human_attribute_name(self.class.name) - end - - def values - nil + def available? + ::Group.exists? end - def available? - true + def type + :list_optional end - def key - self.class.key + def human_name + I18n.t('query_fields.member_of_group') end def self.key - name.to_sym + :group end - def self.create(project) - { key => new(project) } + def joins + :groups end - private_class_method :new - def self.name - to_s.demodulize.underscore.gsub(/_filter$/, '') + def where + sql_for_field(self.class.key, operator, values, 'groups', 'id') end end diff --git a/app/models/queries/users/filters/name_filter.rb b/app/models/queries/users/filters/name_filter.rb new file mode 100644 index 0000000000..2878bd5a76 --- /dev/null +++ b/app/models/queries/users/filters/name_filter.rb @@ -0,0 +1,75 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::Users::Filters::NameFilter < Queries::Users::Filters::UserFilter + def type + :string + end + + def self.key + :name + end + + def where + case operator + when '=' + ["#{sql_concat_name} IN (?)", sql_value] + when '!' + ["#{sql_concat_name} NOT IN (?)", sql_value] + when '~' + ["#{sql_concat_name} LIKE ?", "%#{sql_value}%"] + when '!~' + ["#{sql_concat_name} NOT LIKE ?", "%#{sql_value}%"] + end + end + + private + + def sql_value + case operator + when '=', '!' + values.map { |val| connection.quote_string(val.downcase) }.join(',') + when '~', '!~' + values.first.downcase + end + end + + def sql_concat_name + case Setting.user_format + when :firstname_lastname, :lastname_coma_firstname + "LOWER(CONCAT(firstname, CONCAT(' ', lastname)))" + when :firstname + 'LOWER(firstname)' + when :lastname_firstname + "LOWER(CONCAT(lastname, CONCAT(' ', firstname)))" + when :username + "LOWER(login)" + end + end +end diff --git a/app/models/queries/users/filters/status_filter.rb b/app/models/queries/users/filters/status_filter.rb new file mode 100644 index 0000000000..f6e3a9e975 --- /dev/null +++ b/app/models/queries/users/filters/status_filter.rb @@ -0,0 +1,44 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::Users::Filters::StatusFilter < Queries::Users::Filters::UserFilter + def allowed_values + Principal::STATUSES.keys.map do |key| + [I18n.t(:"status_#{key}"), key] + end + end + + def type + :list + end + + def self.key + :status + end +end diff --git a/app/models/queries/work_packages/filter_register.rb b/app/models/queries/users/filters/user_filter.rb similarity index 86% rename from app/models/queries/work_packages/filter_register.rb rename to app/models/queries/users/filters/user_filter.rb index 5e5c0396a7..085f24cc8c 100644 --- a/app/models/queries/work_packages/filter_register.rb +++ b/app/models/queries/users/filters/user_filter.rb @@ -27,10 +27,10 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -require Rails.root.join('config/constants/work_package_filter') +class Queries::Users::Filters::UserFilter < Queries::BaseFilter + self.model = User -module Queries::WorkPackages::FilterRegister - class << self - delegate :register, :filters, to: ::Constants::WorkPackageFilter + def human_name + User.human_attribute_name(name) end end diff --git a/config/constants/work_package_filter.rb b/app/models/queries/users/orders/default_order.rb similarity index 86% rename from config/constants/work_package_filter.rb rename to app/models/queries/users/orders/default_order.rb index 4c2bcd0eca..904e51fc27 100644 --- a/config/constants/work_package_filter.rb +++ b/app/models/queries/users/orders/default_order.rb @@ -27,16 +27,10 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -module Constants - module WorkPackageFilter - class << self - def register(filter) - self.filters ||= [] +class Queries::Users::Orders::DefaultOrder < Queries::BaseOrder + self.model = User - self.filters << filter - end - - attr_accessor :filters - end + def self.key + /id|lastname|firstname|mail|login/ end end diff --git a/app/models/queries/users/orders/group_order.rb b/app/models/queries/users/orders/group_order.rb new file mode 100644 index 0000000000..f3a5341de4 --- /dev/null +++ b/app/models/queries/users/orders/group_order.rb @@ -0,0 +1,50 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::Users::Orders::GroupOrder < Queries::BaseOrder + self.model = User + + def self.key + :group + end + + private + + def order + order_string = "groups_users.lastname" + + order_string += " DESC" if direction == :desc + + model.order(order_string) + end + + def joins + :groups + end +end diff --git a/app/models/queries/users/orders/name_order.rb b/app/models/queries/users/orders/name_order.rb new file mode 100644 index 0000000000..0ba8f88bbe --- /dev/null +++ b/app/models/queries/users/orders/name_order.rb @@ -0,0 +1,48 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::Users::Orders::NameOrder < Queries::BaseOrder + self.model = User + + def self.key + :name + end + + private + + def order + ordered = User.order_by_name + + if direction == :desc + ordered = ordered.reverse_order + end + + ordered + end +end diff --git a/spec/factories/queries/filter_factory.rb b/app/models/queries/users/user_query.rb similarity index 85% rename from spec/factories/queries/filter_factory.rb rename to app/models/queries/users/user_query.rb index 3c76bd50d7..b977851737 100644 --- a/spec/factories/queries/filter_factory.rb +++ b/app/models/queries/users/user_query.rb @@ -26,13 +26,12 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -FactoryGirl.define do - factory :filter, class: Queries::Filter do - field :subject - operator '=' - values ['Feature'] +class Queries::Users::UserQuery < Queries::BaseQuery + def self.model + User + end - factory :work_packages_filter, class: Queries::WorkPackages::Filter do - end + def self.default_scope + User.not_builtin end end diff --git a/app/models/queries/work_packages.rb b/app/models/queries/work_packages.rb new file mode 100644 index 0000000000..811bc53cbd --- /dev/null +++ b/app/models/queries/work_packages.rb @@ -0,0 +1,52 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module Queries::WorkPackages + Queries::Register.filter Query, Queries::WorkPackages::Filter::AssignedToFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::AuthorFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::CategoryFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::CreatedAtFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::CustomFieldFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::DoneRatioFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::DueDateFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::EstimatedHoursFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::GroupFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::PriorityFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::ProjectFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::ResponsibleFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::RoleFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::StartDateFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::StatusFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::SubjectFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::SubprojectFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::TypeFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::UpdatedAtFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::VersionFilter + Queries::Register.filter Query, Queries::WorkPackages::Filter::WatcherFilter +end diff --git a/app/models/queries/work_packages/filter.rb b/app/models/queries/work_packages/filter.rb index 5509a32cf8..ae56b35f54 100644 --- a/app/models/queries/work_packages/filter.rb +++ b/app/models/queries/work_packages/filter.rb @@ -27,31 +27,5 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter < Queries::Filter - self.filter_types_by_field = filter_types_by_field.merge( - status_id: :list_status, - type_id: :list, - priority_id: :list, - subject: :text, - start_date: :date, - due_date: :date, - estimated_hours: :integer, - done_ratio: :integer, - project_id: :list, - category_id: :list_optional, - fixed_version_id: :list_optional, - subproject_id: :list_subprojects, - assigned_to_id: :list_optional, - author_id: :list, - member_of_group: :list_optional, - assigned_to_role: :list_optional, - responsible_id: :list_optional, - watcher_id: :list - ) - - validates :field, inclusion: { in: Proc.new { filter_types_by_field.keys }, message: '%(value) is not a valid filter' }, unless: Proc.new { |filter| filter.field.to_s.starts_with?('cf_') } - - def self.add_filter_type_by_field(field, filter_type) - filter_types_by_field[field.to_sym] = filter_type.to_sym - end +module Queries::WorkPackages::Filter end diff --git a/app/models/queries/work_packages/filter/assigned_to_filter.rb b/app/models/queries/work_packages/filter/assigned_to_filter.rb index 1c9bfa43f4..101cdcebf3 100644 --- a/app/models/queries/work_packages/filter/assigned_to_filter.rb +++ b/app/models/queries/work_packages/filter/assigned_to_filter.rb @@ -29,8 +29,8 @@ class Queries::WorkPackages::Filter::AssignedToFilter < Queries::WorkPackages::Filter::PrincipalBaseFilter - def values - @values ||= begin + def allowed_values + @allowed_values ||= begin values = principal_loader.user_values if Setting.work_package_group_assignment? @@ -49,7 +49,7 @@ class Queries::WorkPackages::Filter::AssignedToFilter < 4 end - def name + def human_name WorkPackage.human_attribute_name('assigned_to_id') end diff --git a/app/models/queries/work_packages/filter/author_filter.rb b/app/models/queries/work_packages/filter/author_filter.rb index fd17689a05..7716852cb8 100644 --- a/app/models/queries/work_packages/filter/author_filter.rb +++ b/app/models/queries/work_packages/filter/author_filter.rb @@ -29,7 +29,7 @@ class Queries::WorkPackages::Filter::AuthorFilter < Queries::WorkPackages::Filter::PrincipalBaseFilter - def values + def allowed_values @author_values ||= begin me_value + principal_loader.user_values end diff --git a/app/models/queries/work_packages/filter/category_filter.rb b/app/models/queries/work_packages/filter/category_filter.rb index eaa46d2b60..4c50094a51 100644 --- a/app/models/queries/work_packages/filter/category_filter.rb +++ b/app/models/queries/work_packages/filter/category_filter.rb @@ -27,15 +27,11 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::CategoryFilter < Queries::WorkPackages::Filter::BaseFilter - attr_accessor :project +class Queries::WorkPackages::Filter::CategoryFilter < + Queries::WorkPackages::Filter::WorkPackageFilter - def initialize(project) - self.project = project - end - - def values - @values ||= begin + def allowed_values + @allowed_values ||= begin project.categories.map { |s| [s.name, s.id.to_s] } end end diff --git a/app/models/queries/work_packages/filter/created_at_filter.rb b/app/models/queries/work_packages/filter/created_at_filter.rb index ec0600252c..81fc83e799 100644 --- a/app/models/queries/work_packages/filter/created_at_filter.rb +++ b/app/models/queries/work_packages/filter/created_at_filter.rb @@ -27,7 +27,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::CreatedAtFilter < Queries::WorkPackages::Filter::BaseFilter +class Queries::WorkPackages::Filter::CreatedAtFilter < Queries::WorkPackages::Filter::WorkPackageFilter def type :date_past end diff --git a/app/models/queries/work_packages/filter/custom_field_filter.rb b/app/models/queries/work_packages/filter/custom_field_filter.rb index d2fde2bb55..531622f528 100644 --- a/app/models/queries/work_packages/filter/custom_field_filter.rb +++ b/app/models/queries/work_packages/filter/custom_field_filter.rb @@ -27,28 +27,30 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::CustomFieldFilter < Queries::WorkPackages::Filter::BaseFilter - attr_accessor :field +require 'custom_value' - def initialize(field, project) - self.field = field - self.project = project - end +class Queries::WorkPackages::Filter::CustomFieldFilter < + Queries::WorkPackages::Filter::WorkPackageFilter + attr_accessor :custom_field + + validate :custom_field_valid - def values - case field.field_format + def allowed_values + case custom_field.field_format when 'list' - field.possible_values + custom_field.possible_values.map { |value| [value, value] } when 'bool' - [[I18n.t(:general_text_yes), ActiveRecord::Base.connection.unquoted_true], - [I18n.t(:general_text_no), ActiveRecord::Base.connection.unquoted_false]] + [[I18n.t(:general_text_yes), CustomValue::BoolStrategy::DB_VALUE_TRUE], + [I18n.t(:general_text_no), CustomValue::BoolStrategy::DB_VALUE_FALSE]] when 'user', 'version' - field.possible_values_options(project) + custom_field.possible_values_options(context) end end def type - case field.field_format + return nil unless custom_field + + case custom_field.field_format when 'int', 'float' :integer when 'text' @@ -68,33 +70,73 @@ class Queries::WorkPackages::Filter::CustomFieldFilter < Queries::WorkPackages:: 20 end - def key - "cf_#{field.id}".to_sym + def name + :"cf_#{custom_field.id}" end - def name - field.name + def human_name + custom_field ? custom_field.name : '' end - def self.create(project) - custom_fields = if project - project - .all_work_package_custom_fields(include: :translations) - else - WorkPackageCustomField.filter - .for_all - .where.not(field_format: ['user', 'version']) - .includes(:translations) - end - - custom_fields.each_with_object({}.with_indifferent_access) do |cf, hash| - filter = new(cf, project) - - hash[filter.key] = filter - end + def name=(field_name) + cf_id = self.class.key.match(field_name)[1] + + self.custom_field = WorkPackageCustomField.find_by_id(cf_id.to_i) + + super end def self.key - /cf_\d+/ + /cf_(\d+)/ + end + + def self.all_for(context = nil) + custom_fields(context).map do |cf| + filter = new + filter.custom_field = cf + filter.context = context + filter + end + end + + def self.custom_fields(context) + if context + context + .all_work_package_custom_fields + else + WorkPackageCustomField + .filter + .for_all + .where.not(field_format: ['user', 'version']) + end + end + + private + + def custom_field_valid + if custom_field.nil? + errors.add(:base, I18n.t('activerecord.errors.models.query.filters.custom_fields.inexistent')) + elsif invalid_custom_field_for_context? + errors.add(:base, I18n.t('activerecord.errors.models.query.filters.custom_fields.invalid')) + end + end + + def validate_inclusion_of_operator + super if custom_field + end + + def invalid_custom_field_for_context? + context && invalid_custom_field_for_project? || + !context && invalid_custom_field_globally? + end + + def invalid_custom_field_globally? + !self.class.custom_fields(context) + .exists?(custom_field.id) + end + + def invalid_custom_field_for_project? + !self.class.custom_fields(context) + .map(&:id).include? custom_field.id end end diff --git a/app/models/queries/work_packages/filter/done_ratio_filter.rb b/app/models/queries/work_packages/filter/done_ratio_filter.rb index e4b52303e2..3ceaad6c33 100644 --- a/app/models/queries/work_packages/filter/done_ratio_filter.rb +++ b/app/models/queries/work_packages/filter/done_ratio_filter.rb @@ -27,7 +27,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::DoneRatioFilter < Queries::WorkPackages::Filter::BaseFilter +class Queries::WorkPackages::Filter::DoneRatioFilter < Queries::WorkPackages::Filter::WorkPackageFilter def type :integer end diff --git a/app/models/queries/work_packages/filter/due_date_filter.rb b/app/models/queries/work_packages/filter/due_date_filter.rb index 5f468ff4f1..4957d0c513 100644 --- a/app/models/queries/work_packages/filter/due_date_filter.rb +++ b/app/models/queries/work_packages/filter/due_date_filter.rb @@ -27,7 +27,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::DueDateFilter < Queries::WorkPackages::Filter::BaseFilter +class Queries::WorkPackages::Filter::DueDateFilter < Queries::WorkPackages::Filter::WorkPackageFilter def type :date end diff --git a/app/models/queries/work_packages/filter/estimated_hours_filter.rb b/app/models/queries/work_packages/filter/estimated_hours_filter.rb index 9c45d96ed3..0a46aa6dde 100644 --- a/app/models/queries/work_packages/filter/estimated_hours_filter.rb +++ b/app/models/queries/work_packages/filter/estimated_hours_filter.rb @@ -28,7 +28,7 @@ #++ class Queries::WorkPackages::Filter::EstimatedHoursFilter < - Queries::WorkPackages::Filter::BaseFilter + Queries::WorkPackages::Filter::WorkPackageFilter def type :integer end diff --git a/app/models/queries/work_packages/filter/group_filter.rb b/app/models/queries/work_packages/filter/group_filter.rb index 6fed5ff54c..d349d4cace 100644 --- a/app/models/queries/work_packages/filter/group_filter.rb +++ b/app/models/queries/work_packages/filter/group_filter.rb @@ -27,9 +27,9 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter::BaseFilter - def values - @values ||= begin +class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin ::Group.all.map { |g| [g.name, g.id.to_s] } end end @@ -46,7 +46,7 @@ class Queries::WorkPackages::Filter::GroupFilter < Queries::WorkPackages::Filter 6 end - def name + def human_name I18n.t('query_fields.member_of_group') end diff --git a/app/models/queries/work_packages/filter/principal_base_filter.rb b/app/models/queries/work_packages/filter/principal_base_filter.rb index 8474a184c5..49254bb737 100644 --- a/app/models/queries/work_packages/filter/principal_base_filter.rb +++ b/app/models/queries/work_packages/filter/principal_base_filter.rb @@ -27,20 +27,10 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::PrincipalBaseFilter < Queries::WorkPackages::Filter::BaseFilter - attr_accessor :principal_loader - - def initialize(principal_loader) - self.principal_loader = principal_loader - end - +class Queries::WorkPackages::Filter::PrincipalBaseFilter < + Queries::WorkPackages::Filter::WorkPackageFilter def available? - User.current.logged? || values.any? - end - - def self.create(project) - principal_loader = ::Queries::WorkPackages::Filter::PrincipalLoader.new(project) - { key => new(principal_loader) } + User.current.logged? || allowed_values.any? end private @@ -50,4 +40,8 @@ class Queries::WorkPackages::Filter::PrincipalBaseFilter < Queries::WorkPackages values << [I18n.t(:label_me), 'me'] if User.current.logged? values end + + def principal_loader + @principal_loader ||= ::Queries::WorkPackages::Filter::PrincipalLoader.new(project) + end end diff --git a/app/models/queries/work_packages/filter/priority_filter.rb b/app/models/queries/work_packages/filter/priority_filter.rb index 4e43042451..e3ac294e3e 100644 --- a/app/models/queries/work_packages/filter/priority_filter.rb +++ b/app/models/queries/work_packages/filter/priority_filter.rb @@ -27,9 +27,9 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::PriorityFilter < Queries::WorkPackages::Filter::BaseFilter - def values - @values ||= begin +class Queries::WorkPackages::Filter::PriorityFilter < Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin priorities.map { |s| [s.name, s.id.to_s] } end end diff --git a/app/models/queries/work_packages/filter/project_filter.rb b/app/models/queries/work_packages/filter/project_filter.rb index 04a5fed8fa..81c62f01ca 100644 --- a/app/models/queries/work_packages/filter/project_filter.rb +++ b/app/models/queries/work_packages/filter/project_filter.rb @@ -27,15 +27,9 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filter::BaseFilter - attr_accessor :project - - def initialize(project) - self.project = project - end - - def values - @values ||= begin +class Queries::WorkPackages::Filter::ProjectFilter < Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin project_values = [] Project.project_tree(visible_projects) do |p, level| prefix = (level > 0 ? ('--' * level + ' ') : '') diff --git a/app/models/queries/work_packages/filter/responsible_filter.rb b/app/models/queries/work_packages/filter/responsible_filter.rb index c047b27ddd..8925ff630a 100644 --- a/app/models/queries/work_packages/filter/responsible_filter.rb +++ b/app/models/queries/work_packages/filter/responsible_filter.rb @@ -29,8 +29,8 @@ class Queries::WorkPackages::Filter::ResponsibleFilter < Queries::WorkPackages::Filter::PrincipalBaseFilter - def values - @values ||= begin + def allowed_values + @allowed_values ||= begin values = principal_loader.user_values me_value + values end diff --git a/app/models/queries/work_packages/filter/role_filter.rb b/app/models/queries/work_packages/filter/role_filter.rb index 556a094f8a..f73863a5e1 100644 --- a/app/models/queries/work_packages/filter/role_filter.rb +++ b/app/models/queries/work_packages/filter/role_filter.rb @@ -27,9 +27,9 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter::BaseFilter - def values - @values ||= begin +class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin roles.map { |r| [r.name, r.id.to_s] } end end @@ -46,7 +46,7 @@ class Queries::WorkPackages::Filter::RoleFilter < Queries::WorkPackages::Filter: 7 end - def name + def human_name I18n.t('query_fields.assigned_to_role') end diff --git a/app/models/queries/work_packages/filter/start_date_filter.rb b/app/models/queries/work_packages/filter/start_date_filter.rb index a52a536938..a82810b8bd 100644 --- a/app/models/queries/work_packages/filter/start_date_filter.rb +++ b/app/models/queries/work_packages/filter/start_date_filter.rb @@ -27,7 +27,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::StartDateFilter < Queries::WorkPackages::Filter::BaseFilter +class Queries::WorkPackages::Filter::StartDateFilter < Queries::WorkPackages::Filter::WorkPackageFilter def type :date end diff --git a/app/models/queries/work_packages/filter/status_filter.rb b/app/models/queries/work_packages/filter/status_filter.rb index cf44c287ab..4010a325ca 100644 --- a/app/models/queries/work_packages/filter/status_filter.rb +++ b/app/models/queries/work_packages/filter/status_filter.rb @@ -27,9 +27,9 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::StatusFilter < Queries::WorkPackages::Filter::BaseFilter - def values - @values ||= begin +class Queries::WorkPackages::Filter::StatusFilter < Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin Status.all.map { |s| [s.name, s.id.to_s] } end end diff --git a/app/models/queries/work_packages/filter/subject_filter.rb b/app/models/queries/work_packages/filter/subject_filter.rb index 0f75132147..0d8a10dd32 100644 --- a/app/models/queries/work_packages/filter/subject_filter.rb +++ b/app/models/queries/work_packages/filter/subject_filter.rb @@ -27,7 +27,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::SubjectFilter < Queries::WorkPackages::Filter::BaseFilter +class Queries::WorkPackages::Filter::SubjectFilter < Queries::WorkPackages::Filter::WorkPackageFilter def type :text end diff --git a/app/models/queries/work_packages/filter/subproject_filter.rb b/app/models/queries/work_packages/filter/subproject_filter.rb index b764ff4369..2f7759d4c4 100644 --- a/app/models/queries/work_packages/filter/subproject_filter.rb +++ b/app/models/queries/work_packages/filter/subproject_filter.rb @@ -27,15 +27,10 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::SubprojectFilter < Queries::WorkPackages::Filter::BaseFilter - attr_accessor :project - - def initialize(project) - self.project = project - end - - def values - @values ||= begin +class Queries::WorkPackages::Filter::SubprojectFilter < + Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin project.descendants.visible.map { |s| [s.name, s.id.to_s] } end end @@ -54,7 +49,7 @@ class Queries::WorkPackages::Filter::SubprojectFilter < Queries::WorkPackages::F 13 end - def name + def human_name I18n.t('query_fields.subproject_id') end diff --git a/app/models/queries/work_packages/filter/type_filter.rb b/app/models/queries/work_packages/filter/type_filter.rb index 3fb0a8d30d..3153d85cfa 100644 --- a/app/models/queries/work_packages/filter/type_filter.rb +++ b/app/models/queries/work_packages/filter/type_filter.rb @@ -27,15 +27,10 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::TypeFilter < Queries::WorkPackages::Filter::BaseFilter - attr_accessor :project - - def initialize(project) - self.project = project - end - - def values - @values ||= begin +class Queries::WorkPackages::Filter::TypeFilter < + Queries::WorkPackages::Filter::WorkPackageFilter + def allowed_values + @allowed_values ||= begin types.map { |s| [s.name, s.id.to_s] } end end diff --git a/app/models/queries/work_packages/filter/updated_at_filter.rb b/app/models/queries/work_packages/filter/updated_at_filter.rb index 7fac3d5c87..a2b665e5b6 100644 --- a/app/models/queries/work_packages/filter/updated_at_filter.rb +++ b/app/models/queries/work_packages/filter/updated_at_filter.rb @@ -27,7 +27,7 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::UpdatedAtFilter < Queries::WorkPackages::Filter::BaseFilter +class Queries::WorkPackages::Filter::UpdatedAtFilter < Queries::WorkPackages::Filter::WorkPackageFilter def type :date_past end diff --git a/app/models/queries/work_packages/filter/version_filter.rb b/app/models/queries/work_packages/filter/version_filter.rb index fa2c78670d..cfb994431b 100644 --- a/app/models/queries/work_packages/filter/version_filter.rb +++ b/app/models/queries/work_packages/filter/version_filter.rb @@ -27,17 +27,15 @@ # See doc/COPYRIGHT.rdoc for more details. #++ -class Queries::WorkPackages::Filter::VersionFilter < Queries::WorkPackages::Filter::BaseFilter - def values - @values ||= begin +class Queries::WorkPackages::Filter::VersionFilter < + Queries::WorkPackages::Filter::WorkPackageFilter + + def allowed_values + @allowed_values ||= begin versions.sort.map { |s| ["#{s.project.name} - #{s.name}", s.id.to_s] } end end - def available? - versions.exists? - end - def type :list_optional end @@ -46,7 +44,7 @@ class Queries::WorkPackages::Filter::VersionFilter < Queries::WorkPackages::Filt 7 end - def name + def human_name WorkPackage.human_attribute_name('fixed_version_id') end diff --git a/app/models/queries/work_packages/filter/watcher_filter.rb b/app/models/queries/work_packages/filter/watcher_filter.rb index 3f40849d9b..3ffe235787 100644 --- a/app/models/queries/work_packages/filter/watcher_filter.rb +++ b/app/models/queries/work_packages/filter/watcher_filter.rb @@ -29,8 +29,8 @@ class Queries::WorkPackages::Filter::WatcherFilter < Queries::WorkPackages::Filter::PrincipalBaseFilter - def values - @values ||= begin + def allowed_values + @allowed_values ||= begin # populate the watcher list with the same user list as other user filters # if the user has the :view_work_package_watchers permission # in at least one project @@ -38,7 +38,7 @@ class Queries::WorkPackages::Filter::WatcherFilter < # more, e.g. all users could watch issues in public projects, # but won't necessarily be shown here values = me_value - if User.current.allowed_to?(:view_work_packages_watchers, project, global: project.nil?) + if User.current.allowed_to?(:view_work_package_watchers, project, global: project.nil?) values += principal_loader.user_values end values diff --git a/app/models/queries/work_packages/filter/work_package_filter.rb b/app/models/queries/work_packages/filter/work_package_filter.rb new file mode 100644 index 0000000000..a5b4e15a54 --- /dev/null +++ b/app/models/queries/work_packages/filter/work_package_filter.rb @@ -0,0 +1,75 @@ +#-- encoding: UTF-8 +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +class Queries::WorkPackages::Filter::WorkPackageFilter < ::Queries::BaseFilter + include ActiveModel::Serialization + # (de-)serialization + def self.from_hash(filter_hash) + filter_hash.keys.map { |field| new(field, filter_hash[field]) } + end + + def to_hash + { name => attributes_hash } + end + + def human_name + WorkPackage.human_attribute_name(name) + end + + alias :project :context + alias :project= :context= + + def attributes + { name: name, operator: operator, values: values } + end + + def possible_types_by_operator + @@operators_by_filter_type.select { |_key, operators| operators.include?(operator) }.keys.sort + end + + def ==(filter) + filter.attributes_hash == attributes_hash + end + + protected + + def attributes_hash + @@filter_params.inject({}) do |params, param_field| + params.merge(param_field => send(param_field)) + end + end + + private + + def stringify_values + unless values.nil? + values.map!(&:to_s) + end + end +end diff --git a/app/models/query.rb b/app/models/query.rb index b651f2f0f2..ffbbdf5fb7 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -28,10 +28,8 @@ #++ class Query < ActiveRecord::Base - include Queries::WorkPackages::AvailableFilterOptions - - # referenced in plugin patches - currently there are only work package queries and filters - alias_method :available_filters, :available_work_package_filters + include Queries::AvailableFilters + include Queries::SqlForField @@user_filters = %w{assigned_to_id author_id watcher_id responsible_id}.freeze @@ -131,7 +129,7 @@ class Query < ActiveRecord::Base groupable: true), QueryColumn.new(:created_at, sortable: "#{WorkPackage.table_name}.created_at", - default_order: 'desc'), + default_order: 'desc') ] cattr_reader :available_columns @@ -140,21 +138,47 @@ class Query < ActiveRecord::Base add_default_filter if options[:initialize_with_default_filter] end + after_initialize :set_context + + def set_context + # We need to set the project for each filter if a project + # is present because the information is not available when + # deserializing the filters from the db. + + # Allow to use AR's select(...) without + # the filters attribute + return unless respond_to?(:filters) + + filters.each do |filter| + filter.context = project + end + end + + alias :context :project + def add_default_filter - self.filters = [Queries::WorkPackages::Filter.new('status_id', operator: 'o', values: [''])] if filters.blank? + return unless filters.blank? + + add_filter('status_id', 'o', ['']) end def validate_work_package_filters filters.each do |filter| unless filter.valid? - messages = filter.errors.messages.values.flatten.join(" #{I18n.t('support.array.sentence_connector')} ") - cf_id = custom_field_id filter - - if cf_id && CustomField.find(cf_id) - attribute_name = CustomField.find(cf_id).name - errors.add :base, attribute_name + I18n.t(default: ' %{message}', message: messages) + messages = filter + .errors + .messages + .values + .flatten + .join(" #{I18n.t('support.array.sentence_connector')} ") + + attribute_name = filter.human_name + + # TODO: check if this can be handled without the case statment + case filter + when Queries::WorkPackages::Filter::CustomFieldFilter + errors.add :base, attribute_name + I18n.t(default: ' %{message}', message: messages) else - attribute_name = WorkPackage.human_attribute_name(filter.field) errors.add :base, errors.full_message(attribute_name, messages) end end @@ -170,14 +194,12 @@ class Query < ActiveRecord::Base end def add_filter(field, operator, values) - return unless work_package_filter_available?(field) + filter = filter_for(field) - if filter = filter_for(field) - filter.operator = operator - filter.values = values - else - filters << Queries::WorkPackages::Filter.new(field, operator: operator, values: values) - end + filter.operator = operator + filter.values = values + + filters << filter end def add_short_filter(field, expression) @@ -198,28 +220,15 @@ class Query < ActiveRecord::Base end def has_filter?(field) - filters.present? && filters.any? { |filter| filter.field.to_s == field.to_s } + filters.present? && filters.any? { |f| f.field.to_s == field.to_s } end def filter_for(field) - (filters || []).detect { |filter| filter.field.to_s == field.to_s } - end + filter = (filters || []).detect { |f| f.field.to_s == field.to_s } || super(field) - # Deprecated - def operator_for(field) - warn '#operator_for is deprecated. Query the filter object directly, instead.' - filter_for(field).try :operator - end + filter.context = project - # Deprecated - def values_for(field) - warn '#values_for is deprecated. Query the filter object directly, instead.' - filter_for(field).try :values - end - - def label_for(field) - label = available_work_package_filters[field][:name] if work_package_filter_available?(field) - label ||= field.gsub(/\_id\z/, '') + filter end def normalized_name @@ -241,11 +250,11 @@ class Query < ActiveRecord::Base end def self.available_columns=(v) - self.available_columns = (v) + self.available_columns = v end def self.add_available_column(column) - available_columns << (column) if column.is_a?(QueryColumn) + available_columns << column if column.is_a?(QueryColumn) end # Returns an array of columns that can be used to group the results @@ -255,10 +264,13 @@ class Query < ActiveRecord::Base # Returns a Hash of columns and the key for sorting def sortable_columns - { 'id' => "#{WorkPackage.table_name}.id" }.merge(available_columns.inject({}) {|h, column| - h[column.name.to_s] = column.sortable - h - }) + column_sortability = available_columns.inject({}) do |h, column| + h[column.name.to_s] = column.sortable + h + end + + { 'id' => "#{WorkPackage.table_name}.id" } + .merge(column_sortability) end def columns @@ -307,7 +319,6 @@ class Query < ActiveRecord::Base end def sort_criteria=(arg) - c = [] if arg.is_a?(Hash) arg = arg.keys.sort.map { |k| arg[k] } end @@ -523,112 +534,14 @@ class Query < ActiveRecord::Base raise ::Query::StatementInvalid.new(e.message) end - # Note: Convenience method to allow the angular front end to deal with query menu items in a non implementation-specific way + # Note: Convenience method to allow the angular front end to deal with query + # menu items in a non implementation-specific way def starred !!query_menu_item end private - def custom_field_id(filter) - matchdata = /cf\_(?\d+)/.match(filter.field.to_s) - - matchdata.nil? ? nil : matchdata[:id] - end - - # Helper method to generate the WHERE sql for a +field+, +operator+ and a +values+ array - def sql_for_field(field, operator, values, db_table, db_field, is_custom_filter = false) - # code expects strings (e.g. for quoting), but ints would work as well: unify them here - values = values.map(&:to_s) - - sql = '' - case operator - when '=' - if values.present? - if values.include?('-1') - sql = "#{db_table}.#{db_field} IS NULL OR " - end - - sql += "#{db_table}.#{db_field} IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + ')' - else - # empty set of allowed values produces no result - sql = '0=1' - end - when '!' - if values.present? - sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + values.map { |val| "'#{connection.quote_string(val)}'" }.join(',') + '))' - else - # empty set of forbidden values allows all results - sql = '1=1' - end - 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 '>=' - if is_custom_filter - sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) >= #{values.first.to_f}" - else - sql = "#{db_table}.#{db_field} >= #{values.first.to_f}" - end - when '<=' - if is_custom_filter - sql = "#{db_table}.#{db_field} != '' AND CAST(#{db_table}.#{db_field} AS decimal(60,4)) <= #{values.first.to_f}" - else - sql = "#{db_table}.#{db_field} <= #{values.first.to_f}" - end - when 'o' - sql = "#{Status.table_name}.is_closed=#{connection.quoted_false}" if field == 'status_id' - when 'c' - sql = "#{Status.table_name}.is_closed=#{connection.quoted_true}" if field == 'status_id' - when '>t-' - sql = date_range_clause(db_table, db_field, - values.first.to_i, 0) - when 't+' - sql = date_range_clause(db_table, db_field, values.first.to_i, nil) - when ' '%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 - - def connection - self.class.connection - end - def for_all? @for_all ||= project.nil? end diff --git a/app/seeders/demo_data/query_seeder.rb b/app/seeders/demo_data/query_seeder.rb index 7dfb255537..76ee72e7af 100644 --- a/app/seeders/demo_data/query_seeder.rb +++ b/app/seeders/demo_data/query_seeder.rb @@ -53,12 +53,44 @@ module DemoData end def data + admin = User.admin.first + bug_type = Type.find_by(name: I18n.t('default_type_bug')) + milestone_type = Type.find_by(name: I18n.t('default_type_milestone')) + phase_type = Type.find_by(name: I18n.t('default_type_phase')) + task_type = Type.find_by(name: I18n.t('default_type_task')) + story_type = Type.find_by(name: I18n.t('default_type_user_story')) + [ - { name: "Bugs", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['7'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :priority, :subject, :assigned_to, :create_at] }, - { name: "Milestones", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['2'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :subject, :start_date, :due_date] }, - { name: "Phases", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['3'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :subject, :start_date, :due_date] }, - { name: "Tasks", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['1'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :priority, :subject, :assigned_to] }, - { name: "User Stories", filters: [Queries::WorkPackages::Filter.new(:status_id, operator: "o"), Queries::WorkPackages::Filter.new(:type_id, operator: "=", values: ['6'])], user_id: User.admin.first.id, is_public: true, column_names: [:id, :type, :status, :priority, :subject, :assigned_to] } + { name: "Bugs", + filters: [status_id: { operator: "o" }, + type_id: { operator: "=", values: [bug_type.id.to_s] }], + user_id: admin.id, + is_public: true, + column_names: [:id, :type, :status, :priority, :subject, :assigned_to, :create_at] }, + { name: "Milestones", + filters: [status_id: { operator: "o" }, + type_id: { operator: "=", values: [milestone_type.id.to_s] }], + user_id: admin.id, + is_public: true, + column_names: [:id, :type, :status, :subject, :start_date, :due_date] }, + { name: "Phases", + filters: [status_id: { operator: "o" }, + type_id: { operator: "=", values: [phase_type.id.to_s] }], + user_id: admin.id, + is_public: true, + column_names: [:id, :type, :status, :subject, :start_date, :due_date] }, + { name: "Tasks", + filters: [status_id: { operator: "o" }, + type_id: { operator: "=", values: [task_type.id.to_s] }], + user_id: admin.id, + is_public: true, + column_names: [:id, :type, :status, :priority, :subject, :assigned_to] }, + { name: "User Stories", + filters: [status_id: { operator: "o" }, + type_id: { operator: "=", values: [story_type.id.to_s] }], + user_id: admin.id, + is_public: true, + column_names: [:id, :type, :status, :priority, :subject, :assigned_to] } ] end end diff --git a/app/services/api/v3/params_to_query_service.rb b/app/services/api/v3/params_to_query_service.rb new file mode 100644 index 0000000000..ca072502ce --- /dev/null +++ b/app/services/api/v3/params_to_query_service.rb @@ -0,0 +1,128 @@ +#-- copyright +# OpenProject is a project management system. +# Copyright (C) 2012-2015 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. +# +# 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 doc/COPYRIGHT.rdoc for more details. +#++ + +module API + module V3 + class ParamsToQueryService + attr_accessor :model + + def initialize(model) + self.model = model + end + + def call(params) + query = new_query + + query = apply_filters(query, params) + query = apply_order(query, params) + + query + end + + private + + def new_query + model_name = model.name + + query_class = Kernel.const_get "::Queries::#{model_name.pluralize}::#{model_name}Query" + + query_class.new + end + + def apply_filters(query, params) + return query unless params[:filters] + + filters = parse_filters_from_json(params[:filters]) + + filters[:attributes].each do |filter_name| + query = query.where(filter_name, + filters[:operators][filter_name], + filters[:values][filter_name]) + end + + query + end + + def apply_order(query, params) + return query unless params[:sortBy] + + sort = parse_sorting_from_json(params[:sortBy]) + + hash_sort = sort.each_with_object({}) do |(attribute, direction), hash| + hash[attribute.to_sym] = direction.to_sym + end + + query.order(hash_sort) + end + + # Expected format looks like: + # [ + # { + # "filtered_field_name": { + # "operator": "a name for a filter operation", + # "values": ["values", "for the", "operation"] + # } + # }, + # { /* more filters if needed */} + # ] + def parse_filters_from_json(json) + filters = JSON.parse(json) + operators = {} + values = {} + filters.each do |filter| + attribute = filter.keys.first # there should only be one attribute per filter + ar_attribute = convert_attribute attribute, append_id: true + operators[ar_attribute] = filter[attribute]['operator'] + values[ar_attribute] = filter[attribute]['values'] + end + + { + attributes: values.keys, + operators: operators, + values: values + } + end + + def parse_sorting_from_json(json) + JSON.parse(json).map do |(attribute, order)| + [convert_attribute(attribute), order] + end + end + + def convert_attribute(attribute, append_id: false) + ::API::Utilities::PropertyNameConverter.to_ar_name(attribute, + context: conversion_model, + refer_to_ids: append_id) + end + + def conversion_model + @conversion_model ||= model.new + end + end + end +end diff --git a/app/views/queries/_filters.html.erb b/app/views/queries/_filters.html.erb index c1f8e7c020..25647c4c22 100644 --- a/app/views/queries/_filters.html.erb +++ b/app/views/queries/_filters.html.erb @@ -103,13 +103,13 @@ See doc/COPYRIGHT.rdoc for more details. //]]>
    - <% query.available_work_package_filters.sort { |a, b| a[1][:order]<=>b[1][:order] }.each do |filter| %> - <% field = filter[0] - options = filter[1] %> + <% query.available_filters.sort { |a, b| a[:order]<=>b[:order] }.each do |filter| %> + <% field = filter.name + options = filter %>
  • id="tr_<%= field %>" class="filter advanced-filters--filter">
    <%= label_tag "op_#{field}", l(:description_filter), class: "hidden-for-sighted" %> @@ -124,7 +124,7 @@ See doc/COPYRIGHT.rdoc for more details.