opf/openproject

View on GitHub
lib/api/v3/work_packages/work_package_collection_representer.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
    module WorkPackages
      class WorkPackageCollectionRepresenter < ::API::Decorators::OffsetPaginatedCollection
        attr_accessor :timestamps, :query

        def initialize(models,
                       self_link:,
                       groups:,
                       total_sums:,
                       current_user:,
                       query_params: {},
                       project: nil,
                       page: nil,
                       per_page: nil,
                       embed_schemas: false,
                       timestamps: [],
                       query: nil)
          @project = project
          @total_sums = total_sums
          @embed_schemas = embed_schemas
          @timestamps = timestamps
          @query = query

          if timestamps_active?
            query_params[:timestamps] ||= API::V3::Utilities::PathHelper::ApiV3Path.timestamps_to_param_value(timestamps)
          end

          super(models,
                self_link:,
                query_params:,
                page:,
                per_page:,
                groups:,
                current_user:)

          # In order to optimize performance we
          #   * override paged_models so that only the id is fetched from the
          #     scope (typically a query with a couple of includes for e.g.
          #     filtering), circumventing AR instantiation altogether
          #   * use the ids to fetch the actual work packages with all the fields
          #     necessary for rendering the work packages in _elements
          #
          # This results in the weird flow where the scope is passed to super (models variable),
          # which calls the overridden paged_models method fetching the ids. In order to have
          # real AR objects again, we finally get the work packages we actually want to have
          # and set those to be the represented collection.
          # A potential ordering is reapplied to the work package collection in ruby.

          @represented = ::API::V3::WorkPackages::WorkPackageEagerLoadingWrapper
            .wrap(represented, current_user, timestamps:, query:)
        end

        link :sumsSchema do
          next unless total_sums || (groups && groups.any?(&:has_sums?))

          {
            href: api_v3_paths.work_package_sums_schema
          }
        end

        link :editWorkPackage do
          next unless current_user_allowed_to_edit_work_packages?

          {
            href: api_v3_paths.work_package_form("{work_package_id}"),
            method: :post,
            templated: true
          }
        end

        link :createWorkPackage do
          next unless current_user_allowed_to_add_work_packages?

          {
            href: api_v3_paths.create_work_package_form,
            method: :post
          }
        end

        link :createWorkPackageImmediate do
          next unless current_user_allowed_to_add_work_packages?

          {
            href: api_v3_paths.work_packages,
            method: :post
          }
        end

        link :schemas do
          next if represented.empty?

          {
            href: schemas_path
          }
        end

        link :customFields do
          if project.present? &&
            current_user.allowed_in_project?(:select_custom_fields, project)
            {
              href: project_settings_custom_fields_path(project.identifier),
              type: "text/html",
              title: I18n.t("label_custom_field_plural")
            }
          end
        end

        links :representations do
          if current_user.allowed_in_any_work_package?(:export_work_packages, in_project: project)
            representation_formats
          end
        end

        collection :elements,
                   getter: ->(*) {
                     all_fields = represented.map(&:available_custom_fields).flatten.uniq

                     rep_class = element_decorator.custom_field_class(all_fields)

                     represented.map do |model|
                       # In case the work package is no longer visible (moved to a project the user
                       # lacks permission in) we treat it as if the work package were deleted.
                       representer = if model.visible?(current_user)
                                       rep_class
                                     else
                                       WorkPackageDeletedRepresenter
                                     end

                       representer.send(:new, model, current_user:, timestamps:, query:)
                     end
                   },
                   exec_context: :decorator,
                   embedded: true

        property :schemas,
                 exec_context: :decorator,
                 if: ->(*) { embed_schemas && represented.any? },
                 embedded: true,
                 render_nil: false

        property :total_sums,
                 exec_context: :decorator,
                 getter: ->(*) {
                   if total_sums
                     ::API::V3::WorkPackages::WorkPackageSumsRepresenter.create(total_sums, current_user)
                   end
                 },
                 render_nil: false

        def current_user_allowed_to_add_work_packages?
          if project
            current_user.allowed_in_project?(:add_work_packages, project)
          else
            current_user.allowed_in_any_project?(:add_work_packages)
          end
        end

        def current_user_allowed_to_edit_work_packages?
          current_user.allowed_in_any_work_package?(:edit_work_packages, in_project: project)
        end

        def schemas
          schemas = schema_pairs.map do |project, type, available_custom_fields|
            Schema::TypedWorkPackageSchema.new(project:, type:, custom_fields: available_custom_fields)
          end

          Schema::WorkPackageSchemaCollectionRepresenter.new(schemas,
                                                             self_link: schemas_path,
                                                             current_user:)
        end

        def schemas_path
          ids = schema_pairs.map do |project, type|
            [project.id, type.id]
          end

          api_v3_paths.work_package_schemas(*ids)
        end

        def schema_pairs
          @schema_pairs ||= begin
            work_packages = if timestamps_active?
                              represented
                                .flat_map(&:at_timestamps)
                            else
                              represented
                            end

            work_packages
              .select(&:persisted?)
              .uniq { |work_package| [work_package.project_id, work_package.type_id] }
              .map { |work_package| [work_package.project, work_package.type, work_package.available_custom_fields] }
          end
        end

        def paged_models(models)
          super.pluck(:id)
        end

        def _type
          "WorkPackageCollection"
        end

        def representation_formats
          formats = [
            representation_format_pdf,
            representation_format_pdf_report_with_images,
            representation_format_pdf_report,
            representation_format_pdf_gantt,
            representation_format_xls,
            representation_format_xls_descriptions,
            representation_format_xls_relations,
            representation_format_csv
          ].compact

          if Setting.feeds_enabled?
            formats << representation_format_atom
          end

          formats
        end

        def representation_format(identifier, mime_type:, format: identifier, i18n_key: format, url_query_extras: nil)
          path_params = { controller: :work_packages, action: :index, project_id: project }

          href = "#{url_for(path_params.merge(format:))}?#{href_query(@page, @per_page)}"

          if url_query_extras
            href += "&#{url_query_extras}"
          end

          {
            href:,
            identifier:,
            type: mime_type,
            title: I18n.t("export.format.#{i18n_key}")
          }
        end

        def representation_format_pdf
          representation_format "pdf",
                                i18n_key: "pdf_overview_table",
                                mime_type: "application/pdf"
        end

        def representation_format_pdf_report_with_images
          representation_format "pdf-with-descriptions",
                                format: "pdf",
                                i18n_key: "pdf_report_with_images",
                                mime_type: "application/pdf",
                                url_query_extras: "show_images=true&show_report=true"
        end

        def representation_format_pdf_report
          representation_format "pdf-descr",
                                format: "pdf",
                                i18n_key: "pdf_report",
                                mime_type: "application/pdf",
                                url_query_extras: "show_report=true"
        end

        def representation_format_pdf_gantt
          return unless EnterpriseToken.allows_to?(:gantt_pdf_export)

          representation_format "pdf",
                                format: "pdf",
                                i18n_key: "pdf_gantt",
                                mime_type: "application/pdf",
                                url_query_extras: "gantt=true"
        end

        def representation_format_xls
          representation_format "xls",
                                mime_type: "application/vnd.ms-excel"
        end

        def representation_format_xls_descriptions
          representation_format "xls-with-descriptions",
                                i18n_key: "xls_with_descriptions",
                                mime_type: "application/vnd.ms-excel",
                                format: "xls",
                                url_query_extras: "show_descriptions=true"
        end

        def representation_format_xls_relations
          representation_format "xls-with-relations",
                                i18n_key: "xls_with_relations",
                                mime_type: "application/vnd.ms-excel",
                                format: "xls",
                                url_query_extras: "show_relations=true"
        end

        def representation_format_csv
          representation_format "csv",
                                mime_type: "text/csv"
        end

        def representation_format_atom
          representation_format "atom",
                                mime_type: "application/atom+xml"
        end

        def timestamps_active?
          timestamps.present? && timestamps.any?(&:historic?)
        end

        attr_reader :project,
                    :total_sums,
                    :embed_schemas
      end
    end
  end
end