diff --git a/app/controllers/queries/loading.rb b/app/controllers/queries/loading.rb index f89278f41d30..f29ddf2a3221 100644 --- a/app/controllers/queries/loading.rb +++ b/app/controllers/queries/loading.rb @@ -62,7 +62,15 @@ def permitted_query_params end def query_class - "#{self.class.name.chomp('Controller').singularize}Query".constantize + controller_name = self.class.name.demodulize + + model_name = if controller_name == "QueriesController" + self.class.name.deconstantize + else + controller_name.chomp("Controller") + end + + "#{model_name.singularize}Query".constantize end end end diff --git a/app/models/queries/factory.rb b/app/models/queries/factory.rb index e15674f2436e..df025dc8e9c0 100644 --- a/app/models/queries/factory.rb +++ b/app/models/queries/factory.rb @@ -67,14 +67,18 @@ def find_persisted_query_and_set_attributes(id, query_class, params, user, dupli end def duplicate_query(query) - query.class.new(query.attributes.slice("filters", "orders", "selects")) + if query.is_a?(ApplicationRecord) + query.class.new(query.attributes.slice("filters", "orders", "selects")) + else + query + end end def set_query_attributes(query, query_class, params, user) query_namespace(query_class)::SetAttributesService .new(user:, model: query, - contract_class: Queries::LoadingContract) + contract_class: query_loading_contract_class(query_class)) .call(params) .result end @@ -82,5 +86,13 @@ def set_query_attributes(query, query_class, params, user) def query_namespace(query_class) query_class.name.pluralize.constantize end + + def query_loading_contract_class(query_class) + if query_class.is_a?(ApplicationRecord) + Queries::LoadingContract + else + EmptyContract + end + end end end diff --git a/app/services/project_queries/set_attributes_service.rb b/app/services/project_queries/set_attributes_service.rb index 82c7b993cddc..b74832d70b28 100644 --- a/app/services/project_queries/set_attributes_service.rb +++ b/app/services/project_queries/set_attributes_service.rb @@ -27,71 +27,23 @@ # ++ class ProjectQueries::SetAttributesService < BaseServices::SetAttributes - private - - def set_attributes(params) - set_filters(params.delete(:filters)) - set_order(params.delete(:orders)) - set_select(params.delete(:selects)) - - super - end - - def set_default_attributes(_params) - set_default_user - set_default_filter - set_default_order - set_default_selects - end + include Queries::AttributeSetting - def set_default_user - model.change_by_system do - model.user = user - end - end + private def set_default_order - return if model.orders.any? - model.order(lft: :asc) end def set_default_filter - return if model.filters.any? - model.where("active", "=", OpenProject::Database::DB_VALUE_TRUE) end def set_default_selects - return if model.selects.any? - model.select(*default_columns, add_not_existing: false) end - def set_filters(filters) - return unless filters - - model.filters.clear - filters.each do |filter| - model.where(filter[:attribute], filter[:operator], filter[:values]) - end - end - - def set_order(orders) - return unless orders - - 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 - (["favored", "name"] + Setting.enabled_projects_columns).uniq + (%w[favored name] + Setting.enabled_projects_columns).uniq end end diff --git a/app/services/queries/attribute_setting.rb b/app/services/queries/attribute_setting.rb new file mode 100644 index 000000000000..7bca5a546350 --- /dev/null +++ b/app/services/queries/attribute_setting.rb @@ -0,0 +1,91 @@ +# -- 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::AttributeSetting + private + + def set_attributes(params) + set_filters(params.delete(:filters)) + set_order(params.delete(:orders)) + set_select(params.delete(:selects)) + + if model.is_a?(ApplicationRecord) + super + else + set_default_attributes(params) + end + end + + def set_default_attributes(_params) + set_default_user if model.is_a?(ApplicationRecord) + set_default_filter if model.filters.empty? + set_default_order if model.orders.empty? + set_default_selects if model.selects.empty? + end + + def set_default_user + model.change_by_system do + model.user = user + end + end + + def set_default_order + # Nothing or now but overwritable by including class + end + + def set_default_filter + # Nothing or now but overwritable by including class + end + + def set_default_selects + # Nothing or now but overwritable by including class + end + + def set_filters(filters) + return unless filters + + model.filters.clear + filters.each do |filter| + model.where(filter[:attribute], filter[:operator], filter[:values]) + end + end + + def set_order(orders) + return unless orders + + 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 +end diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 0375a030891e..c22356d2d529 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -29,6 +29,7 @@ class MeetingsController < ApplicationController around_action :set_time_zone before_action :load_and_authorize_in_optional_project, only: %i[index new show create history] + before_action :load_query_or_deny_access, only: %i[index] before_action :verify_activities_module_activated, only: %i[history] before_action :determine_date_range, only: %i[history] before_action :determine_author, only: %i[history] @@ -47,6 +48,7 @@ class MeetingsController < ApplicationController include WatchersHelper include PaginationHelper include SortHelper + include Queries::Loading include OpTurbo::ComponentStream include ApplicationComponentStreams @@ -56,7 +58,6 @@ class MeetingsController < ApplicationController menu_item :new_meeting, only: %i[new create] def index - @query = load_query render "index", locals: { menu_name: project_or_global_menu } end @@ -247,38 +248,20 @@ def notify private - def load_query - # TODO: move into Factory - query = ParamsToQueryService.new( - Meeting, - current_user - ).call(params) - - query = apply_default_filter_if_none_given(query) - query = apply_default_order_if_none_given(query) + def load_query(duplicate:) + super.tap do |query| + # We want the project column in here in case the request is not executed inside a project. + # At the same time, we want the project filter if the request is executed inside a project. + # Both is currently not possible to be handled inside the factory loading the query. + if @project + query.where("project_id", "=", @project.id) + end - if @project - query.where("project_id", "=", @project.id) + query.selects.clear + query.select(:title) + query.select(:project) unless @project + query.select(:type, :start_time, :duration, :location) end - - query.select(:title) - query.select(:project) unless @project - query.select(:type, :start_time, :duration, :location) - - query - end - - def apply_default_filter_if_none_given(query) - return query if query.filters.any? - - query.where("time", "=", Queries::Meetings::Filters::TimeFilter::FUTURE_VALUE) - query.where("invited_user_id", "=", [User.current.id.to_s]) - end - - def apply_default_order_if_none_given(query) - return query if query.orders.any? - - query.order(start_time: :asc) end def set_time_zone(&) diff --git a/modules/meeting/app/menus/meetings/menu.rb b/modules/meeting/app/menus/meetings/menu.rb index 6528374a1ebe..d1242ae37407 100644 --- a/modules/meeting/app/menus/meetings/menu.rb +++ b/modules/meeting/app/menus/meetings/menu.rb @@ -44,43 +44,29 @@ def menu_items end def top_level_menu_items - upcoming_filter = [{ time: { operator: "=", values: ["future"] } }].to_json - past_filter = [{ time: { operator: "=", values: ["past"] } }].to_json - [ menu_item(I18n.t(:label_upcoming_meetings), - filters: upcoming_filter, sort: "start_time"), + query_id: MeetingQueries::Static::UPCOMING), menu_item(I18n.t(:label_past_meetings), - filters: past_filter, sort: "start_time:desc") + query_id: MeetingQueries::Static::PAST) ] end def involvement_sidebar_menu_items - past_filter = [ - { time: { operator: "=", values: ["past"] } }, - { invited_user_id: { operator: "=", values: [User.current.id.to_s] } } - ].to_json - attendee_filter = [{ attended_user_id: { operator: "=", values: [User.current.id.to_s] } }].to_json - author_filter = [{ author_id: { operator: "=", values: [User.current.id.to_s] } }].to_json - [ menu_item(I18n.t(:label_upcoming_invitations), - {}), + query_id: MeetingQueries::Static::UPCOMING_INVITATIONS), menu_item(I18n.t(:label_past_invitations), - { filters: past_filter, sort: "start_time:desc" }), + query_id: MeetingQueries::Static::PAST_INVITATIONS), menu_item(I18n.t(:label_attendee), - { filters: attendee_filter }), + query_id: MeetingQueries::Static::ATTENDEE), menu_item(I18n.t(:label_author), - { filters: author_filter }) + query_id: MeetingQueries::Static::CREATOR) ] end def query_path(query_params) - if project.present? - project_meetings_path(project, params.permit(query_params.keys).merge!(query_params)) - else - meetings_path(params.permit(query_params.keys).merge!(query_params)) - end + polymorphic_path([@project, :meetings], query_params) end end end diff --git a/modules/meeting/app/models/meeting_queries/static.rb b/modules/meeting/app/models/meeting_queries/static.rb new file mode 100644 index 000000000000..b3b6144c5f2c --- /dev/null +++ b/modules/meeting/app/models/meeting_queries/static.rb @@ -0,0 +1,107 @@ +# -- 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 MeetingQueries::Static + UPCOMING = "upcoming".freeze + PAST = "past".freeze + UPCOMING_INVITATIONS = "upcoming_invitations".freeze + PAST_INVITATIONS = "past_invitations".freeze + ATTENDEE = "attendee".freeze + CREATOR = "creator".freeze + + DEFAULT = UPCOMING + + class << self + def query(id) + case id + when UPCOMING, nil + static_query_upcoming + when PAST + static_query_past + when UPCOMING_INVITATIONS + static_query_upcoming_invitations + when PAST_INVITATIONS + static_query_past_invitations + when ATTENDEE + static_query_attendee + when CREATOR + static_query_creator + end + end + + private + + def static_query_upcoming + list_with(:label_upcoming_meetings) do |query| + query.where("time", "=", ["future"]) + query.order(start_time: :asc) + end + end + + def static_query_past + list_with(:label_past_meetings) do |query| + query.where("time", "=", ["past"]) + query.order(start_time: :desc) + end + end + + def static_query_upcoming_invitations + list_with(:label_upcoming_invitations) do |query| + query.where("time", "=", ["future"]) + query.where("invited_user_id", "=", [User.current.id.to_s]) + query.order(start_time: :asc) + end + end + + def static_query_past_invitations + list_with(:label_past_invitations) do |query| + query.where("time", "=", ["past"]) + query.where("invited_user_id", "=", [User.current.id.to_s]) + query.order(start_time: :desc) + end + end + + def static_query_attendee + list_with(:label_attendee) do |query| + query.where("attended_user_id", "=", [User.current.id.to_s]) + query.order(start_time: :asc) + end + end + + def static_query_creator + list_with(:label_creator) do |query| + query.where("author_id", "=", [User.current.id.to_s]) + query.order(start_time: :asc) + end + end + + def list_with(name, &) + MeetingQuery.new(name: I18n.t(name)).tap(&) + end + end +end diff --git a/modules/meeting/app/models/queries/meetings/meeting_query.rb b/modules/meeting/app/models/meeting_query.rb similarity index 77% rename from modules/meeting/app/models/queries/meetings/meeting_query.rb rename to modules/meeting/app/models/meeting_query.rb index 1560acc1e628..c1c12f5bd4e1 100644 --- a/modules/meeting/app/models/queries/meetings/meeting_query.rb +++ b/modules/meeting/app/models/meeting_query.rb @@ -24,24 +24,25 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -#++ +# ++ -module Queries::Meetings - class MeetingQuery - include ::Queries::BaseQuery - include ::Queries::UnpersistedQuery +class MeetingQuery + include ::Queries::BaseQuery + include ::Queries::UnpersistedQuery - def self.model - Meeting - end + def self.model + Meeting + end - def results - super - .includes(:project, :author) - end + def results + super + .includes(:project, :author) + end - def default_scope - Meeting.visible(user) - end + def default_scope + Meeting.visible(user) end end + +# This is necessary to have the filters, orders and selects loaded in dev environment +require 'queries/meetings' diff --git a/modules/meeting/app/models/queries/meetings.rb b/modules/meeting/app/models/queries/meetings.rb index b36ddf00b2ab..948ec32b0acf 100644 --- a/modules/meeting/app/models/queries/meetings.rb +++ b/modules/meeting/app/models/queries/meetings.rb @@ -27,7 +27,7 @@ #++ module Queries::Meetings - ::Queries::Register.register(MeetingQuery) do + ::Queries::Register.register(::MeetingQuery) do filter Filters::ProjectFilter filter Filters::TimeFilter filter Filters::AttendedUserFilter diff --git a/modules/meeting/app/services/meeting_queries/set_attributes_service.rb b/modules/meeting/app/services/meeting_queries/set_attributes_service.rb new file mode 100644 index 000000000000..619c83ca743e --- /dev/null +++ b/modules/meeting/app/services/meeting_queries/set_attributes_service.rb @@ -0,0 +1,46 @@ +# -- 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 MeetingQueries::SetAttributesService < BaseServices::SetAttributes + include Queries::AttributeSetting + + private + + def set_default_order + query.order(start_time: :asc) + end + + def set_default_filter + model.where("time", "=", Queries::Meetings::Filters::TimeFilter::FUTURE_VALUE) + model.where("invited_user_id", "=", [user.id.to_s]) + end + + def set_default_selects + model.select(:title, :type, :start_time, :duration, :location) + end +end diff --git a/modules/meeting/spec/models/queries/meeting_query_spec.rb b/modules/meeting/spec/models/meeting_query_spec.rb similarity index 98% rename from modules/meeting/spec/models/queries/meeting_query_spec.rb rename to modules/meeting/spec/models/meeting_query_spec.rb index 4947e56d7f58..c6ce2b952fa7 100644 --- a/modules/meeting/spec/models/queries/meeting_query_spec.rb +++ b/modules/meeting/spec/models/meeting_query_spec.rb @@ -24,11 +24,11 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # See COPYRIGHT and LICENSE files for more details. -#++ +# ++ require "spec_helper" -RSpec.describe Queries::Meetings::MeetingQuery do +RSpec.describe MeetingQuery do subject { described_class.new(user:) } let(:user) { create(:user) } diff --git a/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb b/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb index cf5f24a4bb30..b39c24c91391 100644 --- a/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb +++ b/modules/meeting/spec/services/params_to_query_service_meeting_query_spec.rb @@ -41,7 +41,7 @@ context "when sending neither filters nor orders props" do it "returns a new query" do expect(service_call) - .to be_a Queries::Meetings::MeetingQuery + .to be_a MeetingQuery end it "applies no filter" do @@ -62,7 +62,7 @@ it "returns a new query" do expect(service_call) - .to be_a Queries::Meetings::MeetingQuery + .to be_a MeetingQuery end it "applies no filter" do