opf/openproject

View on GitHub
app/services/api/v3/work_package_collection_from_query_service.rb

Summary

Maintainability
A
0 mins
Test Coverage
#-- 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.
#++

module API
  module V3
    class WorkPackageCollectionFromQueryService
      include Utilities::PathHelper
      include ::API::Utilities::UrlPropsParsingHelper

      def initialize(query, user, scope: nil)
        self.query = query
        self.current_user = user
        self.scope = scope
      end

      def call(params = {}, valid_subset: false)
        update = UpdateQueryFromV3ParamsService
                 .new(query, current_user)
                 .call(params, valid_subset:)

        if update.success?
          representer = results_to_representer(params)

          ServiceResult.success(result: representer)
        else
          update
        end
      end

      private

      def results_to_representer(params)
        results_scope = query.results.work_packages

        if scope
          results_scope = results_scope.where(id: scope.select(:id))
        end

        collection_representer(results_scope,
                               params:,
                               project: query.project,
                               groups: generate_groups,
                               sums: generate_total_sums)
      end

      attr_accessor :query,
                    :current_user,
                    :scope

      def representer
        ::API::V3::WorkPackages::WorkPackageCollectionRepresenter
      end

      def calculate_resulting_params(provided_params)
        calculate_default_params
          .merge(provided_params.slice("offset", "pageSize").symbolize_keys)
          .tap do |params|
          if query.manually_sorted?
            params[:query_id] = query.id
            params[:offset] = 1
            # Force the setting value in all cases except when 0 is requested explicitly. Fetching with pageSize = 0
            # is done for performance reasons to simply get the query without the results.
            params[:pageSize] = pageSizeParam(params) == 0 ? pageSizeParam(params) : Setting.forced_single_page_size
          else
            params[:offset] = to_i_or_nil(params[:offset])
            params[:pageSize] = pageSizeParam(params)
          end

          params[:select] = nested_from_csv(provided_params["select"]) if provided_params["select"]
        end
      end

      def calculate_default_params
        ::API::V3::Queries::QueryParamsRepresenter
          .new(query)
          .to_h
      end

      def generate_groups
        return unless query.grouped?

        results = query.results
        sums = generate_group_sums

        results.work_package_count_by_group.map do |group, count|
          ::API::V3::WorkPackages::WorkPackageAggregationGroup.new(
            group, count, query:, sums: sums[group], current_user:
          )
        end
      end

      def generate_total_sums
        return unless query.display_sums?

        format_query_sums query.results.all_total_sums
      end

      def generate_group_sums
        return {} unless query.display_sums?

        query.results.all_group_sums.transform_values do |v|
          format_query_sums(v)
        end
      end

      def format_query_sums(sums)
        API::ParserStruct.new(format_column_keys(sums).merge(available_custom_fields: WorkPackageCustomField.summable.to_a))
      end

      def format_column_keys(hash_by_column)
        hash_by_column.map do |column, value|
          match = /cf_(\d+)/.match(column.name.to_s)

          column_name = if match
                          "custom_field_#{match[1]}"
                        else
                          column.name.to_s
                        end

          [column_name, value]
        end.to_h
      end

      def collection_representer(work_packages, params:, project:, groups:, sums:)
        resulting_params = calculate_resulting_params(params)

        if resulting_params[:select]
          ::API::V3::Utilities::SqlRepresenterWalker
            .new(work_packages,
                 current_user:,
                 self_path: self_link(project),
                 url_query: resulting_params)
            .walk(::API::V3::WorkPackages::WorkPackageSqlCollectionRepresenter)
        else
          ::API::V3::WorkPackages::WorkPackageCollectionRepresenter.new(
            work_packages,
            self_link: self_link(project),
            project:,
            query_params: resulting_params,
            page: resulting_params[:offset],
            per_page: resulting_params[:pageSize],
            groups:,
            total_sums: sums,
            embed_schemas: true,
            current_user:,
            timestamps: query.timestamps,
            query:
          )
        end
      end

      def to_i_or_nil(value)
        value ? value.to_i : nil
      end

      def pageSizeParam(params)
        to_i_or_nil(params[:pageSize])
      end

      def self_link(project)
        if project
          api_v3_paths.work_packages_by_project(project.id)
        else
          api_v3_paths.work_packages
        end
      end

      def convert_to_v3(attribute)
        ::API::Utilities::PropertyNameConverter.from_ar_name(attribute).to_sym
      end
    end
  end
end