opf/openproject

View on GitHub
app/models/type/attribute_groups.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 Type::AttributeGroups
  extend ActiveSupport::Concern

  included do
    before_save :write_attribute_groups_objects
    after_save :unset_attribute_groups_objects
    after_destroy :remove_attribute_groups_queries

    serialize :attribute_groups, type: Array
    attr_accessor :attribute_groups_objects

    # Mapping from AR attribute name to a default group
    # May be extended by plugins
    mattr_accessor :default_group_map do
      {
        assignee: :people,
        responsible: :people,
        estimated_time: :estimates_and_progress,
        remaining_time: :estimates_and_progress,
        percentage_done: :estimates_and_progress,
        spent_time: :estimates_and_progress,
        priority: :details
      }
    end

    # All known default
    mattr_accessor :default_groups do
      {
        people: :label_people,
        estimates_and_progress: :label_estimates_and_progress,
        details: :label_details,
        other: :label_other,
        children: :"activerecord.attributes.work_package.children"
      }
    end
  end

  class_methods do
    ##
    # Add a new default group name
    def add_default_group(name, label_key)
      default_groups[name.to_sym] = label_key
    end

    ##
    # Add a mapping from attribute key to an existing default group
    def add_default_mapping(group, *keys)
      unless default_groups.include? group
        raise ArgumentError, "Can't add mapping for '#{keys.inspect}'. Unknown default group '#{group}'."
      end

      keys.each do |key|
        default_group_map[key.to_sym] = group
      end
    end
  end

  ##
  # Read the serialized attribute groups, if customized.
  # Otherwise, return +default_attribute_groups+
  def attribute_groups
    self.attribute_groups_objects ||= begin
      groups = custom_attribute_groups || default_attribute_groups

      to_attribute_group_class(groups)
    end

    attribute_groups_objects
  end

  ##
  # Resets the default attribute groups
  def reset_attribute_groups
    # Remove all active custom fields
    self.custom_field_ids = []

    self.attribute_groups_objects = to_attribute_group_class(default_attribute_groups)
  end

  ##
  # Update the attribute groups object.
  def attribute_groups=(groups)
    attribute_groups_will_change!
    self.attribute_groups_objects = to_attribute_group_class(groups)
  end

  ##
  # Returns the default +attribute_groups+ put together by
  # the default group map.
  def default_attribute_groups
    values = work_package_attributes_by_default_group_key
    values.reject! { |k, _| k == :estimates_and_progress } if is_milestone?

    default_groups.keys.each_with_object([]) do |groupkey, array|
      members = values[groupkey]
      array << [groupkey, members] if members.present?
    end
  end

  def reload(*args)
    unset_attribute_groups_objects
    super
  end

  def unset_attribute_groups_objects
    self.attribute_groups_objects = nil
  end

  private

  def write_attribute_groups_objects
    return if attribute_groups_objects.nil?

    groups = if attribute_groups_objects == to_attribute_group_class(default_attribute_groups)
               nil
             else
               to_attribute_group_array(attribute_groups_objects)
             end

    self[:attribute_groups] = groups

    cleanup_query_groups_queries
  end

  def custom_attribute_groups
    self[:attribute_groups].presence
  end

  def default_group_key(key)
    if CustomField.custom_field_attribute?(key)
      :other
    else
      default_group_map.fetch(key.to_sym, :details)
    end
  end

  ##
  # Get the default attribute groups for this type.
  # If it has activated custom fields through +custom_field_ids=+,
  # it will put them into the other group.
  def work_package_attributes_by_default_group_key
    active_cfs = active_custom_field_attributes

    work_package_attributes
      .keys
      .select { |key| default_attribute?(active_cfs, key) }
      .sort_by { |key| default_group_map.keys.index(key.to_sym) || default_group_map.keys.size }
      .group_by { |key| default_group_key(key.to_sym) }
  end

  ##
  # Custom fields should not get included into the default form configuration.
  # This method might get patched by modules.
  def default_attribute?(active_cfs, key)
    !(CustomField.custom_field_attribute?(key) && !active_cfs.include?(key))
  end

  def to_attribute_group_class(groups)
    groups.map do |group|
      attributes = group[1]
      first_attribute = attributes[0]
      key = group[0]

      if first_attribute.is_a?(Query)
        new_query_group(key, first_attribute)
      elsif first_attribute.is_a?(Symbol) && Type::QueryGroup.query_attribute?(first_attribute)
        query = Query.find_by(id: Type::QueryGroup.query_attribute_id(first_attribute))
        new_query_group(key, query)
      else
        new_attribute_group(key, attributes)
      end
    end
  end

  def to_attribute_group_array(groups)
    groups.map do |group|
      attributes = if group.is_a?(Type::QueryGroup)
                     query = group.query

                     query.save

                     [group.query_attribute_name]
                   else
                     group.attributes
                   end
      [group.key, attributes]
    end
  end

  def new_attribute_group(key, attributes)
    Type::AttributeGroup.new(self, key, attributes)
  end

  def new_query_group(key, query)
    Type::QueryGroup.new(self, key, query)
  end

  def cleanup_query_groups_queries
    return unless attribute_groups_changed?

    new_groups = self[:attribute_groups]
    old_groups = attribute_groups_was

    ids = (old_groups.map(&:last).flatten - new_groups.map(&:last).flatten)
          .filter_map { |k| ::Type::QueryGroup.query_attribute_id(k) }

    Query.where(id: ids).destroy_all
  end

  def remove_attribute_groups_queries
    attribute_groups
      .select { |g| g.is_a?(Type::QueryGroup) }
      .map(&:query)
      .each(&:destroy)
  end
end