diff --git a/app/components/projects/configure_view_modal_component.rb b/app/components/projects/configure_view_modal_component.rb index 928e3f71d1b..fee9a76dbb0 100644 --- a/app/components/projects/configure_view_modal_component.rb +++ b/app/components/projects/configure_view_modal_component.rb @@ -35,15 +35,14 @@ class Projects::ConfigureViewModalComponent < ApplicationComponent options :query def all_columns - @all_columns ||= Projects::TableComponent - .new(current_user: User.current) - .all_columns - .map { |c| { id: c.first, name: c.last[:caption] } } + @all_columns ||= query + .available_selects + .map { |c| { id: c.attribute, name: c.caption } } end def selected_columns @selected_columns ||= query - .columns - .flat_map { |name| all_columns.detect { |column| column[:id].to_s == name } } + .selects + .map { |c| { id: c.attribute, name: c.caption } } end end diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 597ec588aa7..0e3d444b51e 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -43,17 +43,17 @@ module Projects end def column_value(column) - if column.to_s.start_with? 'cf_' + if custom_field_column?(column) custom_field_column(column) else - super + send(column.attribute) end end def custom_field_column(column) return nil unless user_can_view_project? - cf = custom_field(column) + cf = column.custom_field custom_value = project.formatted_custom_value_for(cf) if cf.field_format == 'text' && custom_value.present? @@ -118,6 +118,7 @@ module Projects def description return nil unless user_can_view_project? + if project.description.present? render OpenProject::Common::AttributeComponent.new("dialog-#{project.id}-description", I18n.t('activerecord.attributes.project.description'), project.description) end @@ -154,21 +155,16 @@ module Projects end def column_css_class(column) - "#{super} #{additional_css_class(column)}" - end - - def custom_field(name) - table.project_custom_fields.fetch(name) + "#{column.attribute} #{additional_css_class(column)}" end def additional_css_class(column) - case column - when :name + if column.attribute == :name "project--hierarchy #{project.archived? ? 'archived' : ''}" - when :status_explanation, :description + elsif [:status_explanation, :description].include?(column.attribute) "project-long-text-container" - when /\Acf_/ - cf = custom_field(column) + elsif custom_field_column?(column) + cf = column.custom_field formattable = cf.field_format == 'text' ? ' project-long-text-container' : '' "format-#{cf.field_format}#{formattable}" end @@ -256,5 +252,9 @@ module Projects def user_can_view_project? User.current.allowed_in_project?(:view_project, project) end + + def custom_field_column?(column) + column.is_a?(Queries::Projects::Selects::CustomField) + end end end diff --git a/app/components/projects/table_component.html.erb b/app/components/projects/table_component.html.erb index 5c9f81d7037..612f99123f0 100644 --- a/app/components/projects/table_component.html.erb +++ b/app/components/projects/table_component.html.erb @@ -32,15 +32,15 @@ See COPYRIGHT and LICENSE files for more details.
> - <% headers.each do |_name, _options| %> - > + <% columns.each do |column| %> + > <% end %> - + - <% headers.each do |name, options| %> - <% if name == :hierarchy %> + <% columns.each do |column| %> + <% if column.attribute == :hierarchy %> - <% elsif sortable_column?(name) %> - <%= build_sort_header name, - options.merge(data: - { - controller: "params-from-query", - 'application-target': "dynamic", - 'params-from-query-allowed-value': '["query_id"]', - 'params-from-query-all-anchors-value': "true" - } - ) %> + <% elsif sortable_column?(column) %> + <%= build_sort_header column.attribute, + order_options(column) %> <% else %>
@@ -52,22 +52,15 @@ See COPYRIGHT and LICENSE files for more details.
- <%= options[:caption] %> + <%= column.caption %>
diff --git a/app/components/projects/table_component.rb b/app/components/projects/table_component.rb index 3acbc757316..d62db03a58f 100644 --- a/app/components/projects/table_component.rb +++ b/app/components/projects/table_component.rb @@ -86,75 +86,35 @@ module Projects def href_only_when_not_sort_lft unless sorted_by_lft? - projects_path(sortBy: JSON::dump([['lft', 'asc']])) + projects_path(filters: params[:filters], sortBy: JSON::dump([['lft', 'asc']])) end end - def all_columns - @all_columns ||= [ - hierarchy_column, - [:name, { builtin: true, caption: Project.human_attribute_name(:name) }], - [:project_status, { caption: Project.human_attribute_name(:status) }], - [:status_explanation, { caption: Project.human_attribute_name(:status_explanation) }], - [:public, { caption: Project.human_attribute_name(:public) }], - [:description, { caption: Project.human_attribute_name(:description) }], - *custom_field_columns, - *admin_columns - ] + def order_options(select) + { + caption: select.caption, + data: + { + controller: "params-from-query", + 'application-target': "dynamic", + 'params-from-query-allowed-value': '["query_id"]', + 'params-from-query-all-anchors-value': "true" + } + } end - def headers - headers = query - .columns - .map do |name| - all_columns.detect { |column| column.first.to_s == name } - end - - index = headers.index { |column| column.first == :name } - headers.insert(index, hierarchy_column) - - headers - end - - def sortable_column?(_column) - true + def sortable_column?(select) + query.known_order?(select.attribute) end def columns - @columns ||= headers.map(&:first) - end + @columns ||= begin + columns = query.selects.reject { |select| select.is_a?(Queries::Selects::NotExistingSelect) } - def admin_columns - return [] unless current_user.admin? + index = columns.index { |column| column.attribute == :name } + columns.insert(index, Queries::Projects::Selects::Default.new(:hierarchy)) if index - [ - [:created_at, { caption: Project.human_attribute_name(:created_at) }], - [:latest_activity_at, { caption: Project.human_attribute_name(:latest_activity_at) }], - [:required_disk_space, { caption: I18n.t(:label_required_disk_storage) }] - ] - end - - def custom_field_columns - project_custom_fields.values.map do |custom_field| - [custom_field.column_name.to_sym, { caption: custom_field.name, custom_field: true }] - end - end - - def hierarchy_column - [:hierarchy, { builtin: true }] - end - - def project_custom_fields - @project_custom_fields ||= begin - fields = - if EnterpriseToken.allows_to?(:custom_fields_in_projects_list) - ProjectCustomField.visible(current_user).order(:position) - else - ProjectCustomField.none - end - - fields - .index_by { |cf| cf.column_name.to_sym } + columns end end diff --git a/app/contracts/queries/projects/project_queries/base_contract.rb b/app/contracts/queries/projects/project_queries/base_contract.rb index 1d007b22dc6..4577e3e82ac 100644 --- a/app/contracts/queries/projects/project_queries/base_contract.rb +++ b/app/contracts/queries/projects/project_queries/base_contract.rb @@ -29,7 +29,7 @@ module Queries::Projects::ProjectQueries class BaseContract < ::ModelContract attribute :name - attribute :columns + attribute :selects attribute :filters attribute :orders @@ -42,11 +42,18 @@ module Queries::Projects::ProjectQueries length: { maximum: 255 } validate :user_is_current_user_and_logged_in + validate :name_select_included def user_is_current_user_and_logged_in unless user.logged? && user == model.user errors.add :base, :error_unauthorized end end + + def name_select_included + if model.selects.none? { |s| s.attribute == :name } + errors.add :selects, :name_not_included + end + end end end diff --git a/app/contracts/queries/projects/project_queries/loading_contract.rb b/app/contracts/queries/projects/project_queries/loading_contract.rb index 7b0caa6730a..5c4eff0aa06 100644 --- a/app/contracts/queries/projects/project_queries/loading_contract.rb +++ b/app/contracts/queries/projects/project_queries/loading_contract.rb @@ -30,6 +30,6 @@ module Queries::Projects::ProjectQueries class LoadingContract < ::ModelContract attribute :filters attribute :orders - attribute :columns + attribute :selects end end diff --git a/app/controllers/queries/params_parser.rb b/app/controllers/queries/params_parser.rb index e1971a1b466..301d2dd15de 100644 --- a/app/controllers/queries/params_parser.rb +++ b/app/controllers/queries/params_parser.rb @@ -33,7 +33,7 @@ module Queries query_params[:filters] = parse_filters_from_params(params) if params[:filters].present? query_params[:orders] = parse_orders_from_params(params) if params[:sortBy].present? - query_params[:columns] = parse_columns_from_params(params) if params[:columns].present? + query_params[:selects] = parse_columns_from_params(params) if params[:columns].present? query_params end diff --git a/app/models/queries/base_query.rb b/app/models/queries/base_query.rb index bc2d1befa9f..d393e11d764 100644 --- a/app/models/queries/base_query.rb +++ b/app/models/queries/base_query.rb @@ -31,6 +31,7 @@ module Queries::BaseQuery included do include Queries::Filters::AvailableFilters + include Queries::Selects::AvailableSelects include Queries::Orders::AvailableOrders include Queries::GroupBys::AvailableGroupBys include ActiveModel::Validations @@ -82,6 +83,18 @@ module Queries::BaseQuery self end + def select(*select_values, add_not_existing: true) + select_values.each do |select_value| + select_column = select_for(select_value) + + if !select_column.is_a?(::Queries::Selects::NotExistingSelect) || add_not_existing + selects << select_column + end + end + + self + end + def order(hash) hash.each do |attribute, direction| order = order_for(attribute) diff --git a/app/models/queries/orders/available_orders.rb b/app/models/queries/orders/available_orders.rb index e505d416748..9528a533dbb 100644 --- a/app/models/queries/orders/available_orders.rb +++ b/app/models/queries/orders/available_orders.rb @@ -33,6 +33,10 @@ module Queries (find_registered_order(key) || ::Queries::Orders::NotExistingOrder).new(key) end + def known_order?(key) + find_registered_order(key).present? + end + private def find_registered_order(key) diff --git a/app/models/queries/projects.rb b/app/models/queries/projects.rb index e6f77e488bc..1e25ddb19eb 100644 --- a/app/models/queries/projects.rb +++ b/app/models/queries/projects.rb @@ -54,5 +54,12 @@ module Queries::Projects order Orders::ProjectStatusOrder order Orders::NameOrder order Orders::TypeaheadOrder + + select Selects::CreatedAt + select Selects::CustomField + select Selects::Default + select Selects::LatestActivityAt + select Selects::RequiredDiskSpace + select Selects::Status end end diff --git a/app/models/queries/projects/factory.rb b/app/models/queries/projects/factory.rb index bd5721326ac..af4fdf1eebd 100644 --- a/app/models/queries/projects/factory.rb +++ b/app/models/queries/projects/factory.rb @@ -97,7 +97,7 @@ class Queries::Projects::Factory def list_with(name) Queries::Projects::ProjectQuery.new(name: I18n.t(name)) do |query| query.order('lft' => 'asc') - query.columns = Setting.enabled_projects_columns + query.select(*(['name'] + Setting.enabled_projects_columns).uniq, add_not_existing: false) yield query end @@ -128,7 +128,7 @@ class Queries::Projects::Factory end def new_query(source_query, params, user) - update_query(Queries::Projects::ProjectQuery.new(source_query.attributes.slice('filters', 'orders')), + update_query(Queries::Projects::ProjectQuery.new(source_query.attributes.slice('filters', 'orders', 'selects')), params, user) end diff --git a/app/models/queries/projects/project_query.rb b/app/models/queries/projects/project_query.rb index 3bf50a435f7..6a44a497199 100644 --- a/app/models/queries/projects/project_query.rb +++ b/app/models/queries/projects/project_query.rb @@ -34,6 +34,7 @@ class Queries::Projects::ProjectQuery < ApplicationRecord serialize :filters, coder: Queries::Serialization::Filters.new(self) serialize :orders, coder: Queries::Serialization::Orders.new(self) + serialize :selects, coder: Queries::Serialization::Selects.new(self) def self.model Project diff --git a/app/models/queries/projects/selects/created_at.rb b/app/models/queries/projects/selects/created_at.rb new file mode 100644 index 00000000000..db06809ffd7 --- /dev/null +++ b/app/models/queries/projects/selects/created_at.rb @@ -0,0 +1,37 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Projects::Selects::CreatedAt < Queries::Selects::Base + def self.key + :created_at + end + + def self.available? + User.current.admin? + end +end diff --git a/app/models/queries/work_packages/columns/work_package_column.rb b/app/models/queries/projects/selects/custom_field.rb similarity index 58% rename from app/models/queries/work_packages/columns/work_package_column.rb rename to app/models/queries/projects/selects/custom_field.rb index cf754f82811..4e10c2a6ca0 100644 --- a/app/models/queries/work_packages/columns/work_package_column.rb +++ b/app/models/queries/projects/selects/custom_field.rb @@ -1,6 +1,6 @@ -#-- copyright +# -- copyright # OpenProject is an open source project management software. -# Copyright (C) 2012-2024 the OpenProject GmbH +# Copyright (C) 2010-2024 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. @@ -24,32 +24,41 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -#++ +# ++ -class Queries::WorkPackages::Columns::WorkPackageColumn < Queries::Columns::Base - attr_accessor :highlightable - alias_method :highlightable?, :highlightable +class Queries::Projects::Selects::CustomField < Queries::Selects::Base + validates :custom_field, presence: { message: I18n.t(:'activerecord.errors.messages.does_not_exist') } - def initialize(name, options = {}) - super(name, options) - self.highlightable = !!options.fetch(:highlightable, false) + def self.key + /cf_(\d+)/ + end + + def self.available? + EnterpriseToken.allows_to?(:custom_fields_in_projects_list) + end + + def self.all_available + return [] unless available? + + ProjectCustomField + .visible + .pluck(:id) + .map { |cf_id| new("cf_#{cf_id}") } end def caption - WorkPackage.human_attribute_name(name) + custom_field.name end - def self.scoped_column_sum(scope, select, group_by) - scope = scope - .except(:order, :select) + def custom_field + @custom_field ||= begin + ProjectCustomField + .visible + .find_by_id(self.class.key.match(attribute)[1]) + end + end - if group_by - scope - .group(group_by) - .select("#{group_by} id", select) - else - scope - .select(select) - end + def scope + super.select(custom_field.order_statements) end end diff --git a/app/models/queries/projects/selects/default.rb b/app/models/queries/projects/selects/default.rb new file mode 100644 index 00000000000..9a0a93542d4 --- /dev/null +++ b/app/models/queries/projects/selects/default.rb @@ -0,0 +1,39 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Projects::Selects::Default < Queries::Selects::Base + KEYS = %i[status_explanation hierarchy name public description].freeze + + def self.key + Regexp.new(KEYS.join('|')) + end + + def self.all_available + KEYS.map { new(_1) } + end +end diff --git a/app/models/queries/projects/selects/latest_activity_at.rb b/app/models/queries/projects/selects/latest_activity_at.rb new file mode 100644 index 00000000000..e82fdd4c133 --- /dev/null +++ b/app/models/queries/projects/selects/latest_activity_at.rb @@ -0,0 +1,37 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Projects::Selects::LatestActivityAt < Queries::Selects::Base + def self.key + :latest_activity_at + end + + def self.available? + User.current.admin? + end +end diff --git a/app/models/queries/projects/selects/required_disk_space.rb b/app/models/queries/projects/selects/required_disk_space.rb new file mode 100644 index 00000000000..bc433247af0 --- /dev/null +++ b/app/models/queries/projects/selects/required_disk_space.rb @@ -0,0 +1,41 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Projects::Selects::RequiredDiskSpace < Queries::Selects::Base + def self.key + :required_disk_space + end + + def self.available? + User.current.admin? + end + + def caption + I18n.t(:label_required_disk_storage) + end +end diff --git a/app/models/queries/projects/selects/status.rb b/app/models/queries/projects/selects/status.rb new file mode 100644 index 00000000000..cbd85b3f288 --- /dev/null +++ b/app/models/queries/projects/selects/status.rb @@ -0,0 +1,37 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Projects::Selects::Status < Queries::Selects::Base + def self.key + :project_status + end + + def caption + I18n.t(:'attributes.status') + end +end diff --git a/app/models/queries/register.rb b/app/models/queries/register.rb index 43d1f0ae6bb..fa1d16e075b 100644 --- a/app/models/queries/register.rb +++ b/app/models/queries/register.rb @@ -58,12 +58,12 @@ module Queries::Register @group_bys[query] << group_by end - def column(query, column) - @columns ||= Hash.new do |hash, column_key| - hash[column_key] = [] + def select(query, select) + @selects ||= Hash.new do |hash, select_key| + hash[select_key] = [] end - @columns[query] << column + @selects[query] << select end def register(query, &) @@ -73,7 +73,7 @@ module Queries::Register attr_accessor :filters, :excluded_filters, :orders, - :columns, + :selects, :group_bys end @@ -101,8 +101,8 @@ module Queries::Register Queries::Register.group_by(query, group_by) end - def column(column) - Queries::Register.column(query, column) + def select(select) + Queries::Register.select(query, select) end end end diff --git a/app/models/queries/selects/available_selects.rb b/app/models/queries/selects/available_selects.rb new file mode 100644 index 00000000000..54224313581 --- /dev/null +++ b/app/models/queries/selects/available_selects.rb @@ -0,0 +1,56 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +module Queries + module Selects + module AvailableSelects + def select_for(key) + (find_available_select(key) || ::Queries::Selects::NotExistingSelect).new(key.to_sym) + end + + def available_selects + registered_and_available + .flat_map(&:all_available) + end + + private + + def find_available_select(key) + registered_and_available.detect do |s| + s.key === key.to_sym + end + end + + def registered_and_available + ::Queries::Register + .selects[self.class] + .select(&:available?) + end + end + end +end diff --git a/app/models/queries/selects/base.rb b/app/models/queries/selects/base.rb new file mode 100644 index 00000000000..0776ac5cf46 --- /dev/null +++ b/app/models/queries/selects/base.rb @@ -0,0 +1,62 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 COPYRIGHT and LICENSE files for more details. +#++ + +class Queries::Selects::Base + include ActiveModel::Validations + + def self.key + raise NotImplementedError + end + + def self.available? + true + end + + def self.all_available + if available? + [new(key)] + else + [] + end + end + + def caption + model = self.class.name.split('::')[1].singularize.constantize + model.human_attribute_name(attribute) + end + + attr_accessor :attribute + + def initialize(attribute) + self.attribute = attribute + end + + def scope + model.select(attribute) + end +end diff --git a/app/models/queries/selects/not_existing_select.rb b/app/models/queries/selects/not_existing_select.rb new file mode 100644 index 00000000000..6fa44ff4b78 --- /dev/null +++ b/app/models/queries/selects/not_existing_select.rb @@ -0,0 +1,49 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +module Queries + module Selects + class NotExistingSelect < Base + validate :always_false + + def self.key + :inexistent + end + + def caption + I18n.t('activerecord.errors.messages.does_not_exist') + end + + private + + def always_false + errors.add :base, I18n.t(:'activerecord.errors.messages.does_not_exist') + end + end + end +end diff --git a/app/models/queries/serialization/selects.rb b/app/models/queries/serialization/selects.rb new file mode 100644 index 00000000000..90da3e105fa --- /dev/null +++ b/app/models/queries/serialization/selects.rb @@ -0,0 +1,55 @@ +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2010-2024 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 COPYRIGHT and LICENSE files for more details. +# ++ + +class Queries::Serialization::Selects + include Queries::Selects::AvailableSelects + + def load(serialized_selects) + return [] if serialized_selects.nil? + + serialized_selects.map do |o| + select_for(o.to_sym) + end + end + + def dump(selects) + selects.map(&:attribute) + end + + def registered_and_available + ::Queries::Register + .selects[klass] + .select(&:available?) + end + + def initialize(klass) + @klass = klass + end + + attr_reader :klass +end diff --git a/app/models/queries/work_packages.rb b/app/models/queries/work_packages.rb index 2cf6c651df8..ec8363a8121 100644 --- a/app/models/queries/work_packages.rb +++ b/app/models/queries/work_packages.rb @@ -79,11 +79,11 @@ module Queries::WorkPackages filter Filter::DurationFilter exclude Filter::RelatableFilter - column Columns::PropertyColumn - column Columns::CustomFieldColumn - column Columns::RelationToTypeColumn - column Columns::RelationOfTypeColumn - column Columns::ManualSortingColumn - column Columns::TypeaheadColumn + select Selects::PropertySelect + select Selects::CustomFieldSelect + select Selects::RelationToTypeSelect + select Selects::RelationOfTypeSelect + select Selects::ManualSortingSelect + select Selects::TypeaheadSelect end end diff --git a/app/models/queries/work_packages/columns/custom_field_column.rb b/app/models/queries/work_packages/selects/custom_field_select.rb similarity index 94% rename from app/models/queries/work_packages/columns/custom_field_column.rb rename to app/models/queries/work_packages/selects/custom_field_select.rb index 96108eb855a..6f10eedefc5 100644 --- a/app/models/queries/work_packages/columns/custom_field_column.rb +++ b/app/models/queries/work_packages/selects/custom_field_select.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::CustomFieldColumn < Queries::WorkPackages::Columns::WorkPackageColumn +class Queries::WorkPackages::Selects::CustomFieldSelect < Queries::WorkPackages::Selects::WorkPackageSelect def initialize(custom_field) super @@ -86,7 +86,7 @@ class Queries::WorkPackages::Columns::CustomFieldColumn < Queries::WorkPackages: select = summable_select_statement ->(query, grouped) { - Queries::WorkPackages::Columns::WorkPackageColumn + Queries::WorkPackages::Selects::WorkPackageSelect .scoped_column_sum(summable_scope(query), select, grouped && query.group_by_statement) } else diff --git a/app/models/queries/work_packages/columns/manual_sorting_column.rb b/app/models/queries/work_packages/selects/manual_sorting_select.rb similarity index 93% rename from app/models/queries/work_packages/columns/manual_sorting_column.rb rename to app/models/queries/work_packages/selects/manual_sorting_select.rb index 33cae657a48..6f1d7560a7a 100644 --- a/app/models/queries/work_packages/columns/manual_sorting_column.rb +++ b/app/models/queries/work_packages/selects/manual_sorting_select.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::ManualSortingColumn < Queries::WorkPackages::Columns::WorkPackageColumn +class Queries::WorkPackages::Selects::ManualSortingSelect < Queries::WorkPackages::Selects::WorkPackageSelect include ::Queries::WorkPackages::Common::ManualSorting def initialize diff --git a/app/models/queries/work_packages/columns/property_column.rb b/app/models/queries/work_packages/selects/property_select.rb similarity index 95% rename from app/models/queries/work_packages/columns/property_column.rb rename to app/models/queries/work_packages/selects/property_select.rb index 93fc1c8e1d9..a1b0bbd21b5 100644 --- a/app/models/queries/work_packages/columns/property_column.rb +++ b/app/models/queries/work_packages/selects/property_select.rb @@ -26,14 +26,14 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::PropertyColumn < Queries::WorkPackages::Columns::WorkPackageColumn +class Queries::WorkPackages::Selects::PropertySelect < Queries::WorkPackages::Selects::WorkPackageSelect def caption WorkPackage.human_attribute_name(name) end - class_attribute :property_columns + class_attribute :property_selects - self.property_columns = { + self.property_selects = { id: { sortable: "#{WorkPackage.table_name}.id", groupable: false @@ -141,7 +141,7 @@ class Queries::WorkPackages::Columns::PropertyColumn < Queries::WorkPackages::Co } def self.instances(_context = nil) - property_columns.filter_map do |name, options| + property_selects.filter_map do |name, options| next unless !options[:if] || options[:if].call new(name, options.except(:if)) diff --git a/app/models/queries/work_packages/columns/relation_of_type_column.rb b/app/models/queries/work_packages/selects/relation_of_type_select.rb similarity index 93% rename from app/models/queries/work_packages/columns/relation_of_type_column.rb rename to app/models/queries/work_packages/selects/relation_of_type_select.rb index 102d1a8c25c..b7e6b432254 100644 --- a/app/models/queries/work_packages/columns/relation_of_type_column.rb +++ b/app/models/queries/work_packages/selects/relation_of_type_select.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::RelationOfTypeColumn < Queries::WorkPackages::Columns::RelationColumn +class Queries::WorkPackages::Selects::RelationOfTypeSelect < Queries::WorkPackages::Selects::RelationSelect def initialize(type) self.type = type super(name) diff --git a/app/models/queries/work_packages/columns/relation_column.rb b/app/models/queries/work_packages/selects/relation_select.rb similarity index 93% rename from app/models/queries/work_packages/columns/relation_column.rb rename to app/models/queries/work_packages/selects/relation_select.rb index 72385301c0e..ddada8dfa64 100644 --- a/app/models/queries/work_packages/columns/relation_column.rb +++ b/app/models/queries/work_packages/selects/relation_select.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::RelationColumn < Queries::WorkPackages::Columns::WorkPackageColumn +class Queries::WorkPackages::Selects::RelationSelect < Queries::WorkPackages::Selects::WorkPackageSelect attr_accessor :type def self.granted_by_enterprise_token diff --git a/app/models/queries/work_packages/columns/relation_to_type_column.rb b/app/models/queries/work_packages/selects/relation_to_type_select.rb similarity index 93% rename from app/models/queries/work_packages/columns/relation_to_type_column.rb rename to app/models/queries/work_packages/selects/relation_to_type_select.rb index 60a714f92cd..c2cc95651b5 100644 --- a/app/models/queries/work_packages/columns/relation_to_type_column.rb +++ b/app/models/queries/work_packages/selects/relation_to_type_select.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::RelationToTypeColumn < Queries::WorkPackages::Columns::RelationColumn +class Queries::WorkPackages::Selects::RelationToTypeSelect < Queries::WorkPackages::Selects::RelationSelect def initialize(type) super diff --git a/app/models/queries/work_packages/columns/typeahead_column.rb b/app/models/queries/work_packages/selects/typeahead_select.rb similarity index 94% rename from app/models/queries/work_packages/columns/typeahead_column.rb rename to app/models/queries/work_packages/selects/typeahead_select.rb index 895df19ad50..a9efbe8b5dc 100644 --- a/app/models/queries/work_packages/columns/typeahead_column.rb +++ b/app/models/queries/work_packages/selects/typeahead_select.rb @@ -26,7 +26,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::WorkPackages::Columns::TypeaheadColumn < Queries::WorkPackages::Columns::WorkPackageColumn +class Queries::WorkPackages::Selects::TypeaheadSelect < Queries::WorkPackages::Selects::WorkPackageSelect def self.instances(_context = nil) new :typeahead, displayable: false, diff --git a/app/models/queries/columns/base.rb b/app/models/queries/work_packages/selects/work_package_select.rb similarity index 86% rename from app/models/queries/columns/base.rb rename to app/models/queries/work_packages/selects/work_package_select.rb index e56616ea9c4..4f0412fa9d5 100644 --- a/app/models/queries/columns/base.rb +++ b/app/models/queries/work_packages/selects/work_package_select.rb @@ -26,7 +26,10 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class Queries::Columns::Base +class Queries::WorkPackages::Selects::WorkPackageSelect + attr_accessor :highlightable + alias_method :highlightable?, :highlightable + attr_reader :groupable, :sortable, :displayable @@ -41,31 +44,10 @@ class Queries::Columns::Base :summable_select, :summable_work_packages_select - def initialize(name, options = {}) - self.name = name - - %i(sortable - sortable_join - displayable - groupable - summable - summable_select - summable_work_packages_select - association - null_handling - default_order).each do |attribute| - send(:"#{attribute}=", options[attribute]) - end - end - def sortable_join_statement(_query) sortable_join end - def caption - raise NotImplementedError - end - def null_handling(_asc) @null_handling end @@ -139,4 +121,41 @@ class Queries::Columns::Base value end end + + def initialize(name, options = {}) + self.name = name + + %i(sortable + sortable_join + displayable + groupable + summable + summable_select + summable_work_packages_select + association + null_handling + default_order).each do |attribute| + send(:"#{attribute}=", options[attribute]) + end + + self.highlightable = !!options.fetch(:highlightable, false) + end + + def caption + WorkPackage.human_attribute_name(name) + end + + def self.scoped_column_sum(scope, select, group_by) + scope = scope + .except(:order, :select) + + if group_by + scope + .group(group_by) + .select("#{group_by} id", select) + else + scope + .select(select) + end + end end diff --git a/app/models/query/manual_sorting.rb b/app/models/query/manual_sorting.rb index c107398c9bf..94ca3ccaaed 100644 --- a/app/models/query/manual_sorting.rb +++ b/app/models/query/manual_sorting.rb @@ -34,11 +34,11 @@ module Query::ManualSorting -> { order(position: :asc) } def manually_sorted? - sort_criteria_columns.any? { |clz, _| clz.is_a?(::Queries::WorkPackages::Columns::ManualSortingColumn) } + sort_criteria_columns.any? { |clz, _| clz.is_a?(::Queries::WorkPackages::Selects::ManualSortingSelect) } end def self.manual_sorting_column - ::Queries::WorkPackages::Columns::ManualSortingColumn.new + ::Queries::WorkPackages::Selects::ManualSortingSelect.new end delegate :manual_sorting_column, to: :class end diff --git a/app/models/query/results/group_by.rb b/app/models/query/results/group_by.rb index db8caa22c4a..14d2194d8fd 100644 --- a/app/models/query/results/group_by.rb +++ b/app/models/query/results/group_by.rb @@ -76,7 +76,7 @@ module ::Query::Results::GroupBy end def transform_group_keys(groups) - if query.group_by_column.is_a?(Queries::WorkPackages::Columns::CustomFieldColumn) + if query.group_by_column.is_a?(Queries::WorkPackages::Selects::Queries::WorkPackages::Selects::CustomFieldSelect) transform_custom_field_keys(groups) else transform_property_keys(groups) diff --git a/app/models/work_package/exports/query_exporter.rb b/app/models/work_package/exports/query_exporter.rb index db0f2c45381..a72d20d68d2 100644 --- a/app/models/work_package/exports/query_exporter.rb +++ b/app/models/work_package/exports/query_exporter.rb @@ -44,7 +44,7 @@ module WorkPackage::Exports def get_columns query .columns - .reject { |c| c.is_a?(Queries::WorkPackages::Columns::RelationColumn) } + .reject { |c| c.is_a?(Queries::WorkPackages::Selects::RelationSelect) } end def page diff --git a/app/models/work_package/pdf_export/common.rb b/app/models/work_package/pdf_export/common.rb index 54c9d44073b..24c24d65aca 100644 --- a/app/models/work_package/pdf_export/common.rb +++ b/app/models/work_package/pdf_export/common.rb @@ -244,7 +244,7 @@ module WorkPackage::PDFExport::Common end def text_column?(column) - column.is_a?(Queries::WorkPackages::Columns::CustomFieldColumn) && + column.is_a?(Queries::WorkPackages::Selects::Queries::WorkPackages::Selects::CustomFieldSelect) && %w(string text).include?(column.custom_field.field_format) end diff --git a/app/models/work_package/pdf_export/overview_table.rb b/app/models/work_package/pdf_export/overview_table.rb index e8d167f77ba..1c3bd8b573e 100644 --- a/app/models/work_package/pdf_export/overview_table.rb +++ b/app/models/work_package/pdf_export/overview_table.rb @@ -77,7 +77,7 @@ module WorkPackage::PDFExport::OverviewTable def transformed_sum_group sums = query.results.all_group_sums - if query.group_by_column.is_a?(Queries::WorkPackages::Columns::CustomFieldColumn) + if query.group_by_column.is_a?(Queries::WorkPackages::Selects::Queries::WorkPackages::Selects::CustomFieldSelect) transform_custom_field_keys(sums) else sums diff --git a/app/services/custom_fields/create_service.rb b/app/services/custom_fields/create_service.rb index 917af372853..f27308e20ca 100644 --- a/app/services/custom_fields/create_service.rb +++ b/app/services/custom_fields/create_service.rb @@ -54,7 +54,7 @@ module CustomFields def after_perform(call) cf = call.result - if cf.is_a?(ProjectCustomField) + if cf.is_a?(ProjectCustomField) && EnterpriseToken.allows_to?(:custom_fields_in_projects_list) add_cf_to_visible_columns(cf) end diff --git a/app/services/queries/projects/project_queries/set_attributes_service.rb b/app/services/queries/projects/project_queries/set_attributes_service.rb index 742d4ba9107..cde9ef948a4 100644 --- a/app/services/queries/projects/project_queries/set_attributes_service.rb +++ b/app/services/queries/projects/project_queries/set_attributes_service.rb @@ -32,6 +32,7 @@ class Queries::Projects::ProjectQueries::SetAttributesService < BaseServices::Se def set_attributes(params) set_filters(params.delete(:filters)) set_order(params.delete(:orders)) + set_select(params.delete(:selects)) super end @@ -40,7 +41,7 @@ class Queries::Projects::ProjectQueries::SetAttributesService < BaseServices::Se set_default_user set_default_filter set_default_order - set_default_columns + set_default_selects end def set_default_user @@ -61,10 +62,10 @@ class Queries::Projects::ProjectQueries::SetAttributesService < BaseServices::Se model.where('active', '=', OpenProject::Database::DB_VALUE_TRUE) end - def set_default_columns - return if model.columns.any? + def set_default_selects + return if model.selects.any? - model.columns = Setting.enabled_projects_columns + model.select(*default_columns, add_not_existing: false) end def set_filters(filters) @@ -82,4 +83,15 @@ class Queries::Projects::ProjectQueries::SetAttributesService < BaseServices::Se model.orders.clear model.order(orders.to_h { |o| [o[:attribute], o[:direction]] }) end + + def set_select(selects) + return unless selects + + model.selects.clear + model.select(*selects) + end + + def default_columns + (['name'] + Setting.enabled_projects_columns).uniq + end end diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index 51891c44194..6d8048ef565 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -423,7 +423,8 @@ module Settings }, enabled_projects_columns: { default: %w[project_status public created_at latest_activity_at required_disk_space], - allowed: -> { Projects::TableComponent.new(current_user: User.admin.first).all_columns.map(&:first).map(&:to_s) } + # TODO: write a method on ProjectQuery to get all available column names + allowed: -> { Queries::Projects::ProjectQuery.available_select_keys.map(&:to_s) } }, enabled_scm: { default: %w[subversion git] diff --git a/config/locales/en.yml b/config/locales/en.yml index 876c53ad34f..79f0f4cfe48 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -894,6 +894,11 @@ en: enabled_modules: dependency_missing: "The module '%{dependency}' needs to be enabled as well since the module '%{module}' depends on it." format: "%{message}" + queries/projects/project_query: + attributes: + selects: + name_not_included: "The 'Name' column needs to be included" + format: "%{message}" query: attributes: project: diff --git a/db/migrate/20240215155909_rename_columns_on_project_queries.rb b/db/migrate/20240215155909_rename_columns_on_project_queries.rb new file mode 100644 index 00000000000..a6a54427280 --- /dev/null +++ b/db/migrate/20240215155909_rename_columns_on_project_queries.rb @@ -0,0 +1,5 @@ +class RenameColumnsOnProjectQueries < ActiveRecord::Migration[7.1] + def change + rename_column :project_queries, :columns, :selects + end +end diff --git a/lib/api/v3/queries/columns/query_columns_factory.rb b/lib/api/v3/queries/columns/query_columns_factory.rb index 1716d19a951..2ed94b2fc72 100644 --- a/lib/api/v3/queries/columns/query_columns_factory.rb +++ b/lib/api/v3/queries/columns/query_columns_factory.rb @@ -33,10 +33,10 @@ module API module QueryColumnsFactory def self.representer(column) case column - when ::Queries::WorkPackages::Columns::RelationToTypeColumn - ::API::V3::Queries::Columns::QueryRelationToTypeColumnRepresenter - when ::Queries::WorkPackages::Columns::RelationOfTypeColumn - ::API::V3::Queries::Columns::QueryRelationOfTypeColumnRepresenter + when ::Queries::WorkPackages::Selects::RelationToTypeSelect + ::API::V3::Queries::Columns::QueryQueries::WorkPackages::Selects::RelationToTypeSelectRepresenter + when ::Queries::WorkPackages::Selects::RelationOfTypeSelect + ::API::V3::Queries::Columns::QueryQueries::WorkPackages::Selects::RelationOfTypeSelectRepresenter else ::API::V3::Queries::Columns::QueryPropertyColumnRepresenter end diff --git a/lib/api/v3/queries/columns/query_relation_of_type_column_representer.rb b/lib/api/v3/queries/columns/query_relation_of_type_column_representer.rb index f4fe9422039..85e0d1c023b 100644 --- a/lib/api/v3/queries/columns/query_relation_of_type_column_representer.rb +++ b/lib/api/v3/queries/columns/query_relation_of_type_column_representer.rb @@ -30,7 +30,7 @@ module API module V3 module Queries module Columns - class QueryRelationOfTypeColumnRepresenter < QueryColumnRepresenter + class QueryQueries::WorkPackages::Selects::RelationOfTypeSelectRepresenter < QueryColumnRepresenter def _type 'QueryColumn::RelationOfType' end diff --git a/lib/api/v3/queries/columns/query_relation_to_type_column_representer.rb b/lib/api/v3/queries/columns/query_relation_to_type_column_representer.rb index cc0524ef133..cf6a57406a2 100644 --- a/lib/api/v3/queries/columns/query_relation_to_type_column_representer.rb +++ b/lib/api/v3/queries/columns/query_relation_to_type_column_representer.rb @@ -30,7 +30,7 @@ module API module V3 module Queries module Columns - class QueryRelationToTypeColumnRepresenter < QueryColumnRepresenter + class QueryQueries::WorkPackages::Selects::RelationToTypeSelectRepresenter < QueryColumnRepresenter link :type do { href: api_v3_paths.type(represented.type.id), diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 1e85a1db84d..7b76092b3c3 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -206,7 +206,7 @@ module OpenProject::Backlogs ::Queries::Register.register(::Query) do filter OpenProject::Backlogs::WorkPackageFilter - column OpenProject::Backlogs::QueryBacklogsColumn + select OpenProject::Backlogs::QueryBacklogsSelect end end end diff --git a/modules/backlogs/lib/open_project/backlogs/query_backlogs_column.rb b/modules/backlogs/lib/open_project/backlogs/query_backlogs_select.rb similarity index 90% rename from modules/backlogs/lib/open_project/backlogs/query_backlogs_column.rb rename to modules/backlogs/lib/open_project/backlogs/query_backlogs_select.rb index ad2fe928c41..ed96c9b7faf 100644 --- a/modules/backlogs/lib/open_project/backlogs/query_backlogs_column.rb +++ b/modules/backlogs/lib/open_project/backlogs/query_backlogs_select.rb @@ -27,10 +27,10 @@ #++ module OpenProject::Backlogs - class QueryBacklogsColumn < Queries::WorkPackages::Columns::WorkPackageColumn - class_attribute :backlogs_columns + class QueryBacklogsSelect < Queries::WorkPackages::Selects::WorkPackageSelect + class_attribute :backlogs_selects - self.backlogs_columns = { + self.backlogs_selects = { story_points: { sortable: "#{WorkPackage.table_name}.story_points", summable: true @@ -45,7 +45,7 @@ module OpenProject::Backlogs def self.instances(context = nil) return [] if context && !context.backlogs_enabled? - backlogs_columns.map do |name, options| + backlogs_selects.map do |name, options| new(name, options) end end diff --git a/modules/bim/app/models/bim/queries/work_packages/columns/bcf_thumbnail_column.rb b/modules/bim/app/models/bim/queries/work_packages/selects/bcf_thumbnail_select.rb similarity index 92% rename from modules/bim/app/models/bim/queries/work_packages/columns/bcf_thumbnail_column.rb rename to modules/bim/app/models/bim/queries/work_packages/selects/bcf_thumbnail_select.rb index 5333f27368f..b68efe0b36a 100644 --- a/modules/bim/app/models/bim/queries/work_packages/columns/bcf_thumbnail_column.rb +++ b/modules/bim/app/models/bim/queries/work_packages/selects/bcf_thumbnail_select.rb @@ -26,8 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module ::Bim::Queries::WorkPackages::Columns - class BcfThumbnailColumn < Queries::WorkPackages::Columns::WorkPackageColumn +module ::Bim::Queries::WorkPackages::Selects + class BcfThumbnailSelect < Queries::WorkPackages::Selects::WorkPackageSelect def caption I18n.t('attributes.bcf_thumbnail') end diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index 206703c9639..accd3e845ab 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -227,7 +227,7 @@ module OpenProject::Bim ::Queries::Register.register(::Query) do filter ::Bim::Queries::WorkPackages::Filter::BcfIssueAssociatedFilter - column ::Bim::Queries::WorkPackages::Columns::BcfThumbnailColumn + select ::Bim::Queries::WorkPackages::Selects::BcfThumbnailSelect end ::API::Root.class_eval do diff --git a/modules/bim/spec/models/queries/work_packages/columns/bcf_thumbnail_column_spec.rb b/modules/bim/spec/models/queries/work_packages/selects/bcf_thumbnail_select_spec.rb similarity index 76% rename from modules/bim/spec/models/queries/work_packages/columns/bcf_thumbnail_column_spec.rb rename to modules/bim/spec/models/queries/work_packages/selects/bcf_thumbnail_select_spec.rb index 7aba00c9afc..5539de18bde 100644 --- a/modules/bim/spec/models/queries/work_packages/columns/bcf_thumbnail_column_spec.rb +++ b/modules/bim/spec/models/queries/work_packages/selects/bcf_thumbnail_select_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -require Rails.root + 'spec/models/queries/work_packages/columns/shared_query_column_specs' +require Rails.root.join("spec/models/queries/work_packages/selects/shared_query_select_specs").to_s -RSpec.describe Bim::Queries::WorkPackages::Columns::BcfThumbnailColumn do +RSpec.describe Bim::Queries::WorkPackages::Selects::BcfThumbnailSelect do let(:instance) { described_class.new(:query_column) } it_behaves_like 'query column' diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index aaee8ae8c8f..a2aa49a45b7 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -301,7 +301,7 @@ module Costs end ::Queries::Register.register(::Query) do - column Costs::QueryCurrencyColumn + select Costs::QueryCurrencySelect end end end diff --git a/modules/costs/lib/costs/query_currency_column.rb b/modules/costs/lib/costs/query_currency_select.rb similarity index 90% rename from modules/costs/lib/costs/query_currency_column.rb rename to modules/costs/lib/costs/query_currency_select.rb index 18f8496002e..285c7b7d2f7 100644 --- a/modules/costs/lib/costs/query_currency_column.rb +++ b/modules/costs/lib/costs/query_currency_select.rb @@ -27,7 +27,7 @@ #++ module Costs - class QueryCurrencyColumn < Queries::WorkPackages::Columns::WorkPackageColumn + class QueryCurrencySelect < Queries::WorkPackages::Selects::WorkPackageSelect include ActionView::Helpers::NumberHelper alias :super_value :value @@ -43,9 +43,9 @@ module Costs super_value work_package end - class_attribute :currency_columns + class_attribute :currenty_selects - self.currency_columns = { + self.currenty_selects = { budget: {}, material_costs: { summable: ->(query, grouped) { @@ -54,7 +54,7 @@ module Costs .add_to_work_package_collection(WorkPackage.where(id: query.results.work_packages)) .except(:order, :select) - Queries::WorkPackages::Columns::WorkPackageColumn + Queries::WorkPackages::Selects::WorkPackageSelect .scoped_column_sum(scope, "COALESCE(ROUND(SUM(cost_entries_sum), 2)::FLOAT, 0.0) material_costs", grouped && query.group_by_statement) @@ -67,7 +67,7 @@ module Costs .add_to_work_package_collection(WorkPackage.where(id: query.results.work_packages)) .except(:order, :select) - Queries::WorkPackages::Columns::WorkPackageColumn + Queries::WorkPackages::Selects::WorkPackageSelect .scoped_column_sum(scope, "COALESCE(ROUND(SUM(time_entries_sum), 2)::FLOAT, 0.0) labor_costs", grouped && query.group_by_statement) @@ -83,7 +83,7 @@ module Costs def self.instances(context = nil) return [] if context && !context.costs_enabled? - currency_columns.map do |name, options| + currenty_selects.map do |name, options| new(name, options) end end diff --git a/modules/costs/spec/lib/costs/query_currency_column_spec.rb b/modules/costs/spec/lib/costs/query_currency_select_spec.rb similarity index 98% rename from modules/costs/spec/lib/costs/query_currency_column_spec.rb rename to modules/costs/spec/lib/costs/query_currency_select_spec.rb index 8d0a736749c..dd706c91a10 100644 --- a/modules/costs/spec/lib/costs/query_currency_column_spec.rb +++ b/modules/costs/spec/lib/costs/query_currency_select_spec.rb @@ -28,7 +28,7 @@ require 'spec_helper' -RSpec.describe Costs::QueryCurrencyColumn, type: :model do +RSpec.describe Costs::QueryCurrencySelect, type: :model do let(:project) do build_stubbed(:project).tap do |p| allow(p) diff --git a/spec/contracts/queries/projects/project_queries/create_contract_spec.rb b/spec/contracts/queries/projects/project_queries/create_contract_spec.rb index 4a991387132..6b56f3cfe07 100644 --- a/spec/contracts/queries/projects/project_queries/create_contract_spec.rb +++ b/spec/contracts/queries/projects/project_queries/create_contract_spec.rb @@ -38,6 +38,8 @@ RSpec.describe Queries::Projects::ProjectQueries::CreateContract do query.change_by_system do query.user = query_user end + + query.select(*query_selects) end end diff --git a/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb b/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb index 29890dbfcdd..fc469506d00 100644 --- a/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb +++ b/spec/contracts/queries/projects/project_queries/loading_contract_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Queries::Projects::ProjectQueries::LoadingContract do end query.order(query_orders) - query.columns = query_columns + query.select(*query_columns) end end let(:contract) { described_class.new(query, current_user) } diff --git a/spec/contracts/queries/projects/project_queries/shared_contract_examples.rb b/spec/contracts/queries/projects/project_queries/shared_contract_examples.rb index 562dc8bc91f..fb1a395ec19 100644 --- a/spec/contracts/queries/projects/project_queries/shared_contract_examples.rb +++ b/spec/contracts/queries/projects/project_queries/shared_contract_examples.rb @@ -35,6 +35,7 @@ RSpec.shared_examples_for 'project queries contract' do let(:current_user) { build_stubbed(:user) } let(:query_name) { "Query name" } let(:query_user) { current_user } + let(:query_selects) { %i[name project_status created_at] } describe 'validation' do it_behaves_like 'contract is valid' @@ -62,5 +63,11 @@ RSpec.shared_examples_for 'project queries contract' do it_behaves_like 'contract is invalid', base: :error_unauthorized end + + context 'if the selects do not include the name column' do + let(:query_selects) { %i[project_status created_at] } + + it_behaves_like 'contract is invalid', selects: :name_not_included + end end end diff --git a/spec/controllers/queries/params_parser_spec.rb b/spec/controllers/queries/params_parser_spec.rb index 5bd136b947c..a4fcae942e0 100644 --- a/spec/controllers/queries/params_parser_spec.rb +++ b/spec/controllers/queries/params_parser_spec.rb @@ -265,7 +265,7 @@ RSpec.describe Queries::ParamsParser, type: :model do end it 'returns an invalid sort order' do - expect(subject[:columns]) + expect(subject[:selects]) .to eql %w[name cf_1 project_status] end end diff --git a/spec/lib/api/v3/queries/columns/query_relation_of_type_column_representer_spec.rb b/spec/lib/api/v3/queries/columns/query_relation_of_type_column_representer_spec.rb index 766679eae98..7b95b5fb444 100644 --- a/spec/lib/api/v3/queries/columns/query_relation_of_type_column_representer_spec.rb +++ b/spec/lib/api/v3/queries/columns/query_relation_of_type_column_representer_spec.rb @@ -32,7 +32,7 @@ RSpec.describe API::V3::Queries::Columns::QueryRelationOfTypeColumnRepresenter d include API::V3::Utilities::PathHelper let(:type) { { name: :label_relates_to, sym_name: :label_relates_to, order: 1, sym: :relation1 } } - let(:column) { Queries::WorkPackages::Columns::RelationOfTypeColumn.new(type) } + let(:column) { Queries::WorkPackages::Selects::RelationOfTypeSelect.new(type) } let(:representer) { described_class.new(column) } subject { representer.to_json } diff --git a/spec/lib/api/v3/queries/columns/query_relation_to_type_column_representer_spec.rb b/spec/lib/api/v3/queries/columns/query_relation_to_type_column_representer_spec.rb index 884980ecedf..79d4af9e0b4 100644 --- a/spec/lib/api/v3/queries/columns/query_relation_to_type_column_representer_spec.rb +++ b/spec/lib/api/v3/queries/columns/query_relation_to_type_column_representer_spec.rb @@ -32,7 +32,7 @@ RSpec.describe API::V3::Queries::Columns::QueryRelationToTypeColumnRepresenter d include API::V3::Utilities::PathHelper let(:type) { build_stubbed(:type) } - let(:column) { Queries::WorkPackages::Columns::RelationToTypeColumn.new(type) } + let(:column) { Queries::WorkPackages::Selects::RelationToTypeSelect.new(type) } let(:representer) { described_class.new(column) } subject { representer.to_json } diff --git a/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb b/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb index 7f92d4c5a73..68dae0b4adb 100644 --- a/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb +++ b/spec/lib/api/v3/queries/schemas/query_schema_representer_spec.rb @@ -353,11 +353,11 @@ RSpec.describe API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:form_embedded) { true } let(:type) { build_stubbed(:type) } let(:available_values) do - [Queries::WorkPackages::Columns::PropertyColumn.new(:bogus1), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus2), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus3), - Queries::WorkPackages::Columns::RelationToTypeColumn.new(type), - Queries::WorkPackages::Columns::RelationOfTypeColumn.new( + [Queries::WorkPackages::Selects::PropertySelect.new(:bogus1), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus2), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus3), + Queries::WorkPackages::Selects::RelationToTypeSelect.new(type), + Queries::WorkPackages::Selects::RelationOfTypeSelect.new( name: :label_relates_to, sym: :relation1, sym_name: :label_relates_to @@ -404,8 +404,8 @@ RSpec.describe API::V3::Queries::Schemas::QuerySchemaRepresenter do let(:form_embedded) { true } let(:type) { build_stubbed(:type) } let(:available_values) do - [Queries::WorkPackages::Columns::PropertyColumn.new(:bogus1, highlightable: true), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus2, highlightable: true)] + [Queries::WorkPackages::Selects::PropertySelect.new(:bogus1, highlightable: true), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus2, highlightable: true)] end let(:available_values_method) { :displayable_columns } @@ -472,9 +472,9 @@ RSpec.describe API::V3::Queries::Schemas::QuerySchemaRepresenter do it_behaves_like 'has a collection of allowed values' do let(:available_values) do - [Queries::WorkPackages::Columns::PropertyColumn.new(:bogus1), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus2), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus3)] + [Queries::WorkPackages::Selects::PropertySelect.new(:bogus1), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus2), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus3)] end let(:available_values_method) { :groupable_columns } let(:expected_hrefs) do @@ -511,9 +511,9 @@ RSpec.describe API::V3::Queries::Schemas::QuerySchemaRepresenter do end let(:available_values) do - [Queries::WorkPackages::Columns::PropertyColumn.new(:bogus1), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus2), - Queries::WorkPackages::Columns::PropertyColumn.new(:bogus3)] + [Queries::WorkPackages::Selects::PropertySelect.new(:bogus1), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus2), + Queries::WorkPackages::Selects::PropertySelect.new(:bogus3)] end let(:available_values_method) { :sortable_columns } diff --git a/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb b/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb index 9904a30e1cd..75376e8418a 100644 --- a/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb +++ b/spec/lib/api/v3/queries/sort_bys/query_sort_by_representer_spec.rb @@ -33,7 +33,7 @@ RSpec.describe API::V3::Queries::SortBys::QuerySortByRepresenter do let(:column_name) { 'status' } let(:direction) { 'desc' } - let(:column) { Queries::WorkPackages::Columns::PropertyColumn.new(column_name) } + let(:column) { Queries::WorkPackages::Selects::PropertySelect.new(column_name) } let(:decorator) { API::V3::Queries::SortBys::SortByDecorator.new(column, direction) } let(:representer) do described_class diff --git a/spec/models/queries/projects/factory_spec.rb b/spec/models/queries/projects/factory_spec.rb index bec481070d6..c194386911b 100644 --- a/spec/models/queries/projects/factory_spec.rb +++ b/spec/models/queries/projects/factory_spec.rb @@ -29,8 +29,7 @@ require 'spec_helper' require 'services/base_services/behaves_like_create_service' -RSpec.describe Queries::Projects::Factory do - let(:current_user) { build_stubbed(:user) } +RSpec.describe Queries::Projects::Factory, with_settings: { enabled_projects_columns: %w[name project_status active] } do let!(:query_finder) do scope = instance_double(ActiveRecord::Relation) @@ -48,12 +47,15 @@ RSpec.describe Queries::Projects::Factory do build_stubbed(:project_query) do |query| query.order(id: :asc) query.where(:project_status, '=', [Project.status_codes[:on_track].to_s]) + query.select(:project_status, :name, :created_at) end end let(:id) { nil } let(:params) { {} } + current_user { build_stubbed(:user) } + describe '.find' do subject(:find) { described_class.find(id, params:, user: current_user) } @@ -77,6 +79,22 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end + end + + context 'without id and with ee and admin privileges', + with_ee: %i[custom_fields_in_projects_list], + with_settings: { enabled_projects_columns: %w[name created_at cf_1] } do + current_user { build_stubbed(:admin) } + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'active\' id' do @@ -101,6 +119,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'my\' id' do @@ -125,6 +148,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'archived\' id' do @@ -149,6 +177,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'on_track\' id' do @@ -173,6 +206,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'off_track\' id' do @@ -197,6 +235,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'at_risk\' id' do @@ -221,6 +264,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with an integer id for which the user has a query' do @@ -267,7 +315,8 @@ RSpec.describe Queries::Projects::Factory do attribute: 'name', direction: 'desc' } - ] + ], + selects: %w[created_at name] } end @@ -290,6 +339,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['id', :asc], ['name', :desc]]) end + + it 'has the selects' do + expect(find.selects.map(&:attribute)) + .to eq(%i[created_at name]) + end end context 'with the \'active\' id and with order params' do @@ -328,6 +382,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['id', :asc], ['name', :desc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end context 'with the \'active\' id and with filter params' do @@ -368,6 +427,45 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([%i[lft asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end + end + + context 'with the \'active\' id and with select params' do + let(:id) { nil } + let(:params) do + { + selects: %w[created_at project_status] + } + end + + it 'returns a project query' do + expect(find) + .to be_a(Queries::Projects::ProjectQuery) + end + + it 'has no name' do + expect(find.name) + .to be_nil + end + + it 'has the filters of the default \'active\' query applied' do + expect(find.filters.map { |filter| [filter.field, filter.operator, filter.values] }) + .to eq([[:active, '=', ['t']]]) + end + + it 'has the orders of the default \'active\' query applied' do + expect(find.orders.map { |order| [order.attribute, order.direction] }) + .to eq([%i[lft asc]]) + end + + it 'has the selects overwritten' do + expect(find.selects.map(&:attribute)) + .to eq(%i[created_at project_status]) + end end context 'with an integer id for which the user has a query and with filter params' do @@ -408,6 +506,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq(persisted_query.orders.map { |order| [order.attribute, order.direction] }) end + + it 'has the selects of the persisted query' do + expect(find.selects.map(&:attribute)) + .to eq(persisted_query.selects.map(&:attribute)) + end end context 'with an integer id for which the user has a query and with order params' do @@ -446,6 +549,45 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq [["id", :asc], ["name", :desc]] end + + it 'has the selects of the persisted query' do + expect(find.selects.map(&:attribute)) + .to eq(persisted_query.selects.map(&:attribute)) + end + end + + context 'with an integer id for which the user has a query and with select params' do + let(:id) { 42 } + let(:params) do + { + selects: %w[created_at project_status] + } + end + + it 'returns a project query' do + expect(find) + .to be_a(Queries::Projects::ProjectQuery) + end + + it 'keeps the name' do + expect(find.name) + .to eql(persisted_query.name) + end + + it 'has the filters of the persisted query' do + expect(find.filters.map { |filter| [filter.field, filter.operator, filter.values] }) + .to eq(persisted_query.filters.map { |filter| [filter.field, filter.operator, filter.values] }) + end + + it 'has the orders of the persisted query' do + expect(find.orders.map { |order| [order.attribute, order.direction] }) + .to eq(persisted_query.orders.map { |order| [order.attribute, order.direction] }) + end + + it 'has the selects specified by the params' do + expect(find.selects.map(&:attribute)) + .to eq(%i[created_at project_status]) + end end context 'with an integer id for which the user does not have a query and with params' do @@ -507,6 +649,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end describe '.static_query_my' do @@ -531,6 +678,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end describe '.static_query_archived' do @@ -555,6 +707,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end describe '.static_query_status_on_track' do @@ -579,6 +736,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end describe '.static_query_status_off_track' do @@ -603,6 +765,11 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end describe '.static_query_status_at_risk' do @@ -627,5 +794,10 @@ RSpec.describe Queries::Projects::Factory do expect(find.orders.map { |order| [order.attribute, order.direction] }) .to eq([['lft', :asc]]) end + + it 'has the enabled project columns columns as selects' do + expect(find.selects.map(&:attribute)) + .to eq(Setting.enabled_projects_columns.map(&:to_sym)) + end end end diff --git a/spec/models/queries/projects/project_query_spec.rb b/spec/models/queries/projects/project_query_spec.rb index 972421823cb..3327b219763 100644 --- a/spec/models/queries/projects/project_query_spec.rb +++ b/spec/models/queries/projects/project_query_spec.rb @@ -31,13 +31,14 @@ require 'spec_helper' RSpec.describe Queries::Projects::ProjectQuery do let(:instance) { described_class.new } - shared_let(:current_user) { create(:user) } + shared_let(:user) { create(:user) } + shared_let(:admin) { create(:admin) } context 'when persisting' do let(:properties) do { name: 'some name', - user: current_user + user: } end @@ -76,5 +77,51 @@ RSpec.describe Queries::Projects::ProjectQuery do expect(described_class.find(instance.id).orders.map { |o| { o.attribute => o.direction } }) .to eql [{ id: :desc }] end + + it 'takes selects' do + instance = described_class.new(**properties) + + instance.select(:name, :public) + + instance.save! + + expect(described_class.find(instance.id).selects.map(&:attribute)) + .to eql %i[name public] + end + end + + describe '.available_selects' do + current_user { user } + + it 'lists registered selects' do + expect(instance.available_selects) + .to contain_exactly(Queries::Projects::Selects::Default) + end + + context 'with the user being admin' do + current_user { admin } + + it 'includes admin columns' do + expect(instance.available_selects) + .to contain_exactly(Queries::Projects::Selects::Default, + Queries::Projects::Selects::Admin) + end + end + + context 'with an enterprise token', + with_ee: %i[custom_fields_in_projects_list] do + it 'includes custom field columns' do + expect(instance.available_selects) + .to contain_exactly(Queries::Projects::Selects::Default, + Queries::Projects::Selects::CustomField) + end + end + end + + describe '.available_select_names' do + it 'lists registered selects' do + expect(instance.available_select_keys) + .to include(:name, :public) + end end end diff --git a/spec/models/queries/work_packages/columns/custom_field_column_spec.rb b/spec/models/queries/work_packages/selects/custom_field_select_spec.rb similarity index 96% rename from spec/models/queries/work_packages/columns/custom_field_column_spec.rb rename to spec/models/queries/work_packages/selects/custom_field_select_spec.rb index f590f41cfb4..618ada5177a 100644 --- a/spec/models/queries/work_packages/columns/custom_field_column_spec.rb +++ b/spec/models/queries/work_packages/selects/custom_field_select_spec.rb @@ -27,9 +27,9 @@ #++ require 'spec_helper' -require_relative 'shared_query_column_specs' +require_relative 'shared_query_select_specs' -RSpec.describe Queries::WorkPackages::Columns::CustomFieldColumn do +RSpec.describe Queries::WorkPackages::Selects::CustomFieldSelect do let(:project) { build_stubbed(:project) } let(:custom_field) { build_stubbed(:string_wp_custom_field) } let(:instance) { described_class.new(custom_field) } diff --git a/spec/models/queries/work_packages/columns/property_column_spec.rb b/spec/models/queries/work_packages/selects/property_select_spec.rb similarity index 95% rename from spec/models/queries/work_packages/columns/property_column_spec.rb rename to spec/models/queries/work_packages/selects/property_select_spec.rb index 26ac053659a..bd22e08b3e3 100644 --- a/spec/models/queries/work_packages/columns/property_column_spec.rb +++ b/spec/models/queries/work_packages/selects/property_select_spec.rb @@ -27,9 +27,9 @@ #++ require 'spec_helper' -require_relative 'shared_query_column_specs' +require_relative 'shared_query_select_specs' -RSpec.describe Queries::WorkPackages::Columns::PropertyColumn do +RSpec.describe Queries::WorkPackages::Selects::PropertySelect do let(:instance) { described_class.new(:query_column) } it_behaves_like 'query column' diff --git a/spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb b/spec/models/queries/work_packages/selects/relation_of_type_select_spec.rb similarity index 95% rename from spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb rename to spec/models/queries/work_packages/selects/relation_of_type_select_spec.rb index 4fc33e9c4da..c31b53e4dcc 100644 --- a/spec/models/queries/work_packages/columns/relation_of_type_column_spec.rb +++ b/spec/models/queries/work_packages/selects/relation_of_type_select_spec.rb @@ -27,9 +27,9 @@ #++ require 'spec_helper' -require_relative 'shared_query_column_specs' +require_relative 'shared_query_select_specs' -RSpec.describe Queries::WorkPackages::Columns::RelationOfTypeColumn do +RSpec.describe Queries::WorkPackages::Selects::RelationOfTypeSelect do let(:project) { build_stubbed(:project) } let(:type) { build_stubbed(:type) } let(:instance) { described_class.new(type) } diff --git a/spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb b/spec/models/queries/work_packages/selects/relation_to_type_select_spec.rb similarity index 96% rename from spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb rename to spec/models/queries/work_packages/selects/relation_to_type_select_spec.rb index 4e20a2a806a..c96454b53b5 100644 --- a/spec/models/queries/work_packages/columns/relation_to_type_column_spec.rb +++ b/spec/models/queries/work_packages/selects/relation_to_type_select_spec.rb @@ -27,9 +27,9 @@ #++ require 'spec_helper' -require_relative 'shared_query_column_specs' +require_relative 'shared_query_select_specs' -RSpec.describe Queries::WorkPackages::Columns::RelationToTypeColumn do +RSpec.describe Queries::WorkPackages::Selects::RelationToTypeSelect do let(:project) { build_stubbed(:project) } let(:type) { build_stubbed(:type) } let(:instance) { described_class.new(type) } diff --git a/spec/models/queries/work_packages/columns/shared_query_column_specs.rb b/spec/models/queries/work_packages/selects/shared_query_select_specs.rb similarity index 100% rename from spec/models/queries/work_packages/columns/shared_query_column_specs.rb rename to spec/models/queries/work_packages/selects/shared_query_select_specs.rb diff --git a/spec/models/queries/work_packages/columns/work_package_column_spec.rb b/spec/models/queries/work_packages/selects/work_package_select_spec.rb similarity index 96% rename from spec/models/queries/work_packages/columns/work_package_column_spec.rb rename to spec/models/queries/work_packages/selects/work_package_select_spec.rb index b875991ff79..57d8eb4dd33 100644 --- a/spec/models/queries/work_packages/columns/work_package_column_spec.rb +++ b/spec/models/queries/work_packages/selects/work_package_select_spec.rb @@ -28,7 +28,7 @@ require 'spec_helper' -RSpec.describe Queries::WorkPackages::Columns::WorkPackageColumn do +RSpec.describe Queries::WorkPackages::Selects::WorkPackageSelect do it "allows to be constructed with attribute highlightable" do expect(described_class.new('foo', highlightable: true).highlightable?).to be(true) end diff --git a/spec/models/query_spec.rb b/spec/models/query_spec.rb index e80bb6b6b13..12bed04655b 100644 --- a/spec/models/query_spec.rb +++ b/spec/models/query_spec.rb @@ -262,7 +262,7 @@ RSpec.describe Query, no_highlight: {} } - allow(Queries::WorkPackages::Columns::PropertyColumn).to receive(:property_columns) + allow(Queries::WorkPackages::Selects::PropertySelect).to receive(:property_columns) .and_return(available_columns) expect(query.available_highlighting_columns.map(&:name)).to eq(%i{highlightable1 highlightable2}) diff --git a/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb b/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb index a195281ec65..76866cf7b54 100644 --- a/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb +++ b/spec/services/queries/projects/project_queries/set_attributes_service_spec.rb @@ -158,11 +158,25 @@ RSpec.describe Queries::Projects::ProjectQueries::SetAttributesService, type: :m .to eql [[:active, '=', %w[t]]] end - it 'assigns default columns' do + it 'assigns default selects including those for admin and ee if allowed', + with_ee: %i[custom_fields_in_projects_list], + with_settings: { enabled_projects_columns: %w[name created_at cf_1] } do + allow(User.current) + .to receive(:admin?) + .and_return(true) + subject - expect(model_instance.columns) - .to eql Setting.enabled_projects_columns + expect(model_instance.selects.map(&:attribute)) + .to eql Setting.enabled_projects_columns.map(&:to_sym) + end + + it 'assigns default selects excluding those for admin and ee if not allowed', + with_settings: { enabled_projects_columns: %w[name created_at cf_1] } do + subject + + expect(model_instance.selects.map(&:attribute)) + .to eql [:name] end end @@ -229,23 +243,23 @@ RSpec.describe Queries::Projects::ProjectQueries::SetAttributesService, type: :m end end - context 'with the query already having columns and with column params' do + context 'with the query already having selects and with selects params' do let(:model_instance) do Queries::Projects::ProjectQuery.new.tap do |query| - query.columns = %w[id name] + query.select(:id, :name) end end let(:params) do { - columns: %w[project_status created_at] + selects: %w[project_status created_at] } end - it 'assigns the columns param' do + it 'assigns the select param' do subject - expect(model_instance.columns) - .to eql %w[project_status created_at] + expect(model_instance.selects.map(&:attribute)) + .to eql %i[project_status created_at] end end