diff --git a/app/components/custom_actions/table_component.rb b/app/components/automations/index_component.rb similarity index 73% rename from app/components/custom_actions/table_component.rb rename to app/components/automations/index_component.rb index 324f8f7dc576..fdaa9b214d01 100644 --- a/app/components/custom_actions/table_component.rb +++ b/app/components/automations/index_component.rb @@ -28,14 +28,16 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions - class TableComponent < ::TableComponent - columns :name, :description, :sort +module Automations + class IndexComponent < ::TableComponent + columns :name, :triggers, :conditions, :actions, :sort def headers [ - ["name", { caption: CustomAction.human_attribute_name(:name) }], - ["description", { caption: CustomAction.human_attribute_name(:description) }], + ["name", { caption: Automation.human_attribute_name(:name) }], + ["triggers", { caption: I18n.t("automations.triggers.name") }], + ["conditions", { caption: I18n.t("automations.conditions") }], + ["actions", { caption: I18n.t("automations.actions.name") }], ["sort", { caption: I18n.t(:label_sort) }] ] end @@ -45,10 +47,10 @@ def sortable? end def inline_create_link - link_to new_custom_action_path, - aria: { label: t("custom_actions.new") }, + link_to new_automation_path, + aria: { label: t("automations.new") }, class: "wp-inline-create--add-link", - title: t("custom_actions.new") do + title: t("automations.new") do helpers.op_icon("icon icon-add") end end diff --git a/app/components/custom_actions/row_component.rb b/app/components/automations/row_component.rb similarity index 77% rename from app/components/custom_actions/row_component.rb rename to app/components/automations/row_component.rb index 2f3f065f605a..f00b4d26e8f5 100644 --- a/app/components/custom_actions/row_component.rb +++ b/app/components/automations/row_component.rb @@ -28,20 +28,30 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions +module Automations class RowComponent < ::RowComponent - def action + def automation row end def name - link_to action.name, edit_custom_action_path(action) + link_to automation.name, edit_automation_path(automation) end - delegate :description, to: :action + def triggers + automation.triggers.map(&:human_name).join(", ") + end + + def conditions + automation.conditions.map(&:human_name).join(", ") + end + + def actions + automation.actions.map(&:human_name).join(", ") + end def sort - helpers.reorder_links("custom_action", { action: "update", id: action }, method: :put) + helpers.reorder_links("automation", { action: "update", id: automation }, method: :put) end def button_links @@ -54,7 +64,7 @@ def button_links def edit_link link_to( helpers.op_icon("icon icon-edit"), - helpers.edit_custom_action_path(action), + helpers.edit_automation_path(automation), title: t(:button_edit) ) end @@ -62,7 +72,7 @@ def edit_link def delete_link link_to( helpers.op_icon("icon icon-delete"), - helpers.custom_action_path(action), + helpers.automation_path(automation), data: { turbo_method: :delete, turbo_confirm: I18n.t(:text_are_you_sure) diff --git a/app/contracts/custom_actions/cu_contract.rb b/app/contracts/automations/cu_contract.rb similarity index 79% rename from app/contracts/custom_actions/cu_contract.rb rename to app/contracts/automations/cu_contract.rb index d9a479bbf9fb..b3b22e0d3898 100644 --- a/app/contracts/custom_actions/cu_contract.rb +++ b/app/contracts/automations/cu_contract.rb @@ -30,33 +30,23 @@ require "model_contract" -# Contract for create (c) and update (u) -module CustomActions +module Automations class CuContract < ::ModelContract def self.model - CustomAction - end - - def initialize(model, user = nil) - super + Automation end attribute :name attribute :description attribute :actions do - if model.actions.empty? - errors.add :actions, :empty - end - model.actions.each do |action| - action.validate(errors) - end + live_actions = model.actions.reject(&:marked_for_destruction?) + errors.add(:actions, :empty) if live_actions.empty? + live_actions.each { |action| action.validate(errors) } end attribute :conditions do - model.conditions.each do |condition| - condition.validate(errors) - end + model.conditions.each { |condition| condition.validate(errors) } end end end diff --git a/app/contracts/custom_actions/execute_contract.rb b/app/contracts/automations/execute_contract.rb similarity index 79% rename from app/contracts/custom_actions/execute_contract.rb rename to app/contracts/automations/execute_contract.rb index 8f327dd78062..4252e0fd283c 100644 --- a/app/contracts/custom_actions/execute_contract.rb +++ b/app/contracts/automations/execute_contract.rb @@ -28,35 +28,31 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions +module Automations class ExecuteContract < BaseContract property :lock_version property :work_package_id validates :work_package_id, presence: true validate :work_package_visible - validate :custom_action_conditions_fulfilled + validate :automation_conditions_fulfilled private def work_package_visible return unless model.work_package_id - unless WorkPackage.visible(user).where(id: model.work_package_id).exists? - errors.add(:work_package_id, :does_not_exist) - end + errors.add(:work_package_id, :does_not_exist) unless WorkPackage.visible(user).exists?(id: model.work_package_id) end - def custom_action_conditions_fulfilled + def automation_conditions_fulfilled return unless model.work_package_id - return unless options[:custom_action] + return unless options[:automation] work_package = WorkPackage.visible(user).find_by(id: model.work_package_id) return unless work_package - unless options[:custom_action].conditions_fulfilled?(work_package, user) - errors.add(:base, :error_unauthorized) - end + errors.add(:base, :error_unauthorized) unless options[:automation].conditions_fulfilled?(work_package, user) end end end diff --git a/app/controllers/custom_actions_controller.rb b/app/controllers/automations_controller.rb similarity index 61% rename from app/controllers/custom_actions_controller.rb rename to app/controllers/automations_controller.rb index 62c766a58961..249754d5dbd0 100644 --- a/app/controllers/custom_actions_controller.rb +++ b/app/controllers/automations_controller.rb @@ -28,75 +28,72 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActionsController < ApplicationController +class AutomationsController < ApplicationController before_action :require_admin guard_enterprise_feature(:custom_actions, only: %i[new create edit update]) do redirect_to action: :index end - before_action :find_custom_action, only: %i(edit update destroy) - before_action :pad_params, only: %i(create update) + before_action :find_automation, only: %i[edit update destroy] + before_action :pad_params, only: %i[create update] layout "admin" def index - @custom_actions = CustomAction.order_by_position + @automations = Automation.order_by_position.includes(:triggers) end def new - @custom_action = CustomAction.new + @automation = Automation.new + @automation.triggers.build(type: "Automations::Triggers::Manual") end def edit; end def create - CustomActions::CreateService + Automations::CreateService .new(user: current_user) - .call(attributes: permitted_params.custom_action.to_h, + .call(attributes: permitted_params.automation.to_h, &index_or_render(:new)) end def update - CustomActions::UpdateService - .new(action: @custom_action, user: current_user) - .call(attributes: permitted_params.custom_action.to_h, + Automations::UpdateService + .new(action: @automation, user: current_user) + .call(attributes: permitted_params.automation.to_h, &index_or_render(:edit)) end def destroy - @custom_action.destroy + @automation.destroy! - redirect_to custom_actions_path, status: :see_other + redirect_to automations_path, status: :see_other end private - def find_custom_action - @custom_action = CustomAction.find(params[:id]) + def find_automation + @automation = Automation.find(params.expect(:id)) end def index_or_render(render_action) ->(call) { call.on_success do - redirect_to custom_actions_path, status: :see_other + redirect_to automations_path, status: :see_other end call.on_failure do - @custom_action = call.result + @automation = call.result render action: render_action, status: :unprocessable_entity end } end - # If no action/condition is set in the view, the - # actions/conditions already existing on a custom action should be removed. - # But because it is not feasible to have an empty and hidden hash object in a form - # we have to pad the params here. def pad_params - return if !params[:custom_action] || params[:custom_action][:move_to] + return if !params[:automation] || params[:automation][:move_to] - params[:custom_action][:conditions] ||= {} - params[:custom_action][:actions] ||= {} + params[:automation][:conditions] ||= {} + params[:automation][:actions] ||= {} end end diff --git a/app/models/custom_action.rb b/app/models/automation.rb similarity index 56% rename from app/models/custom_action.rb rename to app/models/automation.rb index c37d6ddb831f..89cb99d0ca22 100644 --- a/app/models/custom_action.rb +++ b/app/models/automation.rb @@ -28,13 +28,34 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomAction < ApplicationRecord +class Automation < ApplicationRecord validates :name, length: { maximum: 255, minimum: 1 } - serialize :actions, coder: CustomActions::Actions::Serializer - has_and_belongs_to_many :status_conditions, class_name: "Status" - has_and_belongs_to_many :role_conditions, class_name: "Role" - has_and_belongs_to_many :type_conditions, class_name: "Type" - has_and_belongs_to_many :project_conditions, class_name: "Project" + validate :must_have_at_least_one_trigger + validate :must_not_have_more_than_one_manual_trigger + + before_validation :ensure_manual_trigger + + # This code was moved from custom actions and it's decisions unchanged + # rubocop:disable Rails/HasAndBelongsToMany + has_and_belongs_to_many :status_conditions, class_name: "Status", join_table: :automations_statuses + has_and_belongs_to_many :role_conditions, class_name: "Role", join_table: :automations_roles + has_and_belongs_to_many :type_conditions, class_name: "Type", join_table: :automations_types + has_and_belongs_to_many :project_conditions, class_name: "Project", join_table: :automations_projects + # rubocop:enable Rails/HasAndBelongsToMany + + has_many :triggers, + -> { order(:position, :id) }, + class_name: "Automations::Triggers::Base", + dependent: :destroy, + inverse_of: :automation + accepts_nested_attributes_for :triggers + + has_many :actions, + -> { order(:position, :id) }, + class_name: "Automations::Actions::Base", + dependent: :destroy, + inverse_of: :automation + accepts_nested_attributes_for :actions, allow_destroy: true after_save :persist_conditions @@ -43,24 +64,12 @@ class CustomAction < ApplicationRecord acts_as_list - def initialize(*args) - ret = super - - if actions.nil? - self.actions = [] - end - - ret - end + scope :with_manual_trigger, -> { + joins(:triggers).where(automation_triggers: { type: "Automations::Triggers::Manual" }).distinct + } def reload(*args) @conditions = nil - - super - end - - def actions=(values) - actions_will_change! super end @@ -72,21 +81,23 @@ def self.order_by_position order(:position) end + def available_triggers + ::Automations::Register.triggers + end + def all_actions all_of(available_actions, actions) end def available_actions - ::CustomActions::Register.actions.map(&:all).flatten + ::Automations::Register.actions.flat_map(&:templates) end def all_conditions all_of(available_conditions, conditions) end - def available_conditions - self.class.available_conditions - end + delegate :available_conditions, to: :class def conditions @conditions ||= available_conditions.filter_map do |condition_class| @@ -104,16 +115,22 @@ def conditions_fulfilled?(work_package, user) end def self.available_conditions - ::CustomActions::Register.conditions + ::Automations::Register.conditions end private + def ensure_manual_trigger + return unless triggers.reject(&:marked_for_destruction?).empty? + + triggers.build(type: "Automations::Triggers::Manual", options: { button_label: name }) + end + def all_of(availables, actual) availables.map do |available| existing = actual.detect { |a| a.key == available.key } - existing || available.new + existing || (available.is_a?(Class) ? available.new : available) end end @@ -124,4 +141,13 @@ def persist_conditions condition_class.setter(self, condition) end end + + def must_have_at_least_one_trigger + errors.add(:triggers, :blank) if triggers.reject(&:marked_for_destruction?).empty? + end + + def must_not_have_more_than_one_manual_trigger + manual_triggers = triggers.reject(&:marked_for_destruction?).count { |trigger| trigger.type == "Automations::Triggers::Manual" } + errors.add(:triggers, :invalid) if manual_triggers > 1 + end end diff --git a/app/models/custom_actions/actions/assigned_to.rb b/app/models/automations/actions/assigned_to.rb similarity index 92% rename from app/models/custom_actions/actions/assigned_to.rb rename to app/models/automations/actions/assigned_to.rb index 2d0655e161be..87923628d76b 100644 --- a/app/models/custom_actions/actions/assigned_to.rb +++ b/app/models/automations/actions/assigned_to.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::AssignedTo < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::MeAssociated +class Automations::Actions::AssignedTo < Automations::Actions::Base + include Automations::Actions::Strategies::MeAssociated def self.key :assigned_to diff --git a/app/models/custom_actions/actions/base.rb b/app/models/automations/actions/base.rb similarity index 81% rename from app/models/custom_actions/actions/base.rb rename to app/models/automations/actions/base.rb index 3b131d6f80dc..b3a8edc089ed 100644 --- a/app/models/custom_actions/actions/base.rb +++ b/app/models/automations/actions/base.rb @@ -28,17 +28,24 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Base - attr_reader :values +class Automations::Actions::Base < ApplicationRecord + self.table_name = "automation_actions" DEFAULT_PRIORITY = 100 - def initialize(values = []) - self.values = values + belongs_to :automation, inverse_of: :actions + acts_as_list scope: :automation + + store_attribute :options, :values + + after_initialize :coerce_persisted_values + + def values + Array(super) end - def values=(values) - @values = Array(values) + def write_raw_values(new_values) + self.options = options.is_a?(Hash) ? options.merge("values" => Array(new_values)) : { "values" => Array(new_values) } end def allowed_values @@ -67,14 +74,8 @@ def self.key raise SubclassResponsibilityError end - def self.all - [self] - end - - def self.for(key) - if key == self.key - self - end + def self.templates + [new] end delegate :key, to: :class @@ -102,6 +103,12 @@ def deconstruct_keys(*) { type:, custom_field_based: respond_to?(:custom_field) } end + def coerce_persisted_values + return if new_record? || values.empty? + + self.values = values + end + def validate_value_required(errors) if required? && values.empty? errors.add :actions, diff --git a/app/models/automations/actions/custom_field.rb b/app/models/automations/actions/custom_field.rb new file mode 100644 index 000000000000..bc88a0a27bcc --- /dev/null +++ b/app/models/automations/actions/custom_field.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 Automations::Actions::CustomField < Automations::Actions::Base + store_attribute :options, :custom_field_id, :integer + + FORMAT_TO_SUBCLASS = { + "string" => "Automations::Actions::CustomField::ForString", + "text" => "Automations::Actions::CustomField::ForText", + "link" => "Automations::Actions::CustomField::ForLink", + "int" => "Automations::Actions::CustomField::ForInteger", + "float" => "Automations::Actions::CustomField::ForFloat", + "date" => "Automations::Actions::CustomField::ForDate", + "bool" => "Automations::Actions::CustomField::ForBoolean", + "user" => "Automations::Actions::CustomField::ForUser", + "list" => "Automations::Actions::CustomField::ForAssociated", + "version" => "Automations::Actions::CustomField::ForAssociated" + }.freeze + + def self.templates + WorkPackageCustomField.usable_as_automation.filter_map do |cf| + subclass = subclass_for(cf) + next unless subclass + + template = subclass.new(custom_field_id: cf.id) + template.instance_variable_set(:@custom_field, cf) + template + end + end + + def self.subclass_for(custom_field) + name = FORMAT_TO_SUBCLASS[custom_field.field_format] + name&.constantize + end + + def custom_field + return nil if custom_field_id.blank? + return @custom_field if defined?(@custom_field) + + @custom_field = WorkPackageCustomField.find_by(id: custom_field_id) + end + + def key + cf = custom_field + cf ? cf.attribute_name.to_sym : :inexistent_custom_field + end + + def human_name + custom_field&.name || super + end + + private + + def set_custom_field_value(work_package) + work_package.send(custom_field.attribute_setter, values) + end + + def validate_custom_field(work_package) + work_package.custom_values_to_validate += Array(work_package.custom_value_for(custom_field)) + end +end diff --git a/app/models/automations/actions/custom_field/for_associated.rb b/app/models/automations/actions/custom_field/for_associated.rb new file mode 100644 index 000000000000..ac24f90d2b9b --- /dev/null +++ b/app/models/automations/actions/custom_field/for_associated.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForAssociated < Automations::Actions::CustomField + include Automations::Actions::Strategies::AssociatedCustomField +end diff --git a/app/models/automations/actions/custom_field/for_boolean.rb b/app/models/automations/actions/custom_field/for_boolean.rb new file mode 100644 index 000000000000..f6eebfecc9eb --- /dev/null +++ b/app/models/automations/actions/custom_field/for_boolean.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForBoolean < Automations::Actions::CustomField + include Automations::Actions::Strategies::Boolean + include Automations::Actions::Strategies::CustomField +end diff --git a/app/models/automations/actions/custom_field/for_date.rb b/app/models/automations/actions/custom_field/for_date.rb new file mode 100644 index 000000000000..bdcdf1bfa2ee --- /dev/null +++ b/app/models/automations/actions/custom_field/for_date.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForDate < Automations::Actions::CustomField + include Automations::Actions::Strategies::CustomField + include Automations::Actions::Strategies::Date +end diff --git a/app/models/automations/actions/custom_field/for_float.rb b/app/models/automations/actions/custom_field/for_float.rb new file mode 100644 index 000000000000..fd793dfb4b96 --- /dev/null +++ b/app/models/automations/actions/custom_field/for_float.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForFloat < Automations::Actions::CustomField + include Automations::Actions::Strategies::Float + include Automations::Actions::Strategies::CustomField +end diff --git a/app/models/automations/actions/custom_field/for_integer.rb b/app/models/automations/actions/custom_field/for_integer.rb new file mode 100644 index 000000000000..e280efbc572a --- /dev/null +++ b/app/models/automations/actions/custom_field/for_integer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForInteger < Automations::Actions::CustomField + include Automations::Actions::Strategies::Integer + include Automations::Actions::Strategies::CustomField +end diff --git a/app/models/automations/actions/custom_field/for_link.rb b/app/models/automations/actions/custom_field/for_link.rb new file mode 100644 index 000000000000..7af4e73c2ee0 --- /dev/null +++ b/app/models/automations/actions/custom_field/for_link.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForLink < Automations::Actions::CustomField + include Automations::Actions::Strategies::Link + include Automations::Actions::Strategies::CustomField +end diff --git a/app/models/automations/actions/custom_field/for_string.rb b/app/models/automations/actions/custom_field/for_string.rb new file mode 100644 index 000000000000..6b4ffd316c0d --- /dev/null +++ b/app/models/automations/actions/custom_field/for_string.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForString < Automations::Actions::CustomField + include Automations::Actions::Strategies::String + include Automations::Actions::Strategies::CustomField +end diff --git a/app/models/automations/actions/custom_field/for_text.rb b/app/models/automations/actions/custom_field/for_text.rb new file mode 100644 index 000000000000..1ad39881bcf6 --- /dev/null +++ b/app/models/automations/actions/custom_field/for_text.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForText < Automations::Actions::CustomField + include Automations::Actions::Strategies::Text + include Automations::Actions::Strategies::CustomField +end diff --git a/app/models/automations/actions/custom_field/for_user.rb b/app/models/automations/actions/custom_field/for_user.rb new file mode 100644 index 000000000000..e393a3f91410 --- /dev/null +++ b/app/models/automations/actions/custom_field/for_user.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Automations::Actions::CustomField::ForUser < Automations::Actions::CustomField + include Automations::Actions::Strategies::UserCustomField +end diff --git a/app/models/custom_actions/actions/date.rb b/app/models/automations/actions/date.rb similarity index 92% rename from app/models/custom_actions/actions/date.rb rename to app/models/automations/actions/date.rb index 7e4f3bee3802..571788b33137 100644 --- a/app/models/custom_actions/actions/date.rb +++ b/app/models/automations/actions/date.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Date < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Date +class Automations::Actions::Date < Automations::Actions::Base + include Automations::Actions::Strategies::Date def self.key :date diff --git a/app/models/custom_actions/actions/done_ratio.rb b/app/models/automations/actions/done_ratio.rb similarity index 91% rename from app/models/custom_actions/actions/done_ratio.rb rename to app/models/automations/actions/done_ratio.rb index c885bb79e4bc..36e5fcfb0f25 100644 --- a/app/models/custom_actions/actions/done_ratio.rb +++ b/app/models/automations/actions/done_ratio.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::DoneRatio < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Integer +class Automations::Actions::DoneRatio < Automations::Actions::Base + include Automations::Actions::Strategies::Integer def self.key :done_ratio @@ -47,7 +47,7 @@ def maximum 100 end - def self.all + def self.templates if WorkPackage.work_based_mode? super else diff --git a/app/models/custom_actions/actions/due_date.rb b/app/models/automations/actions/due_date.rb similarity index 90% rename from app/models/custom_actions/actions/due_date.rb rename to app/models/automations/actions/due_date.rb index e0836b1f032d..658e346291df 100644 --- a/app/models/custom_actions/actions/due_date.rb +++ b/app/models/automations/actions/due_date.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::DueDate < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::DateProperty +class Automations::Actions::DueDate < Automations::Actions::Base + include Automations::Actions::Strategies::DateProperty def self.key :due_date diff --git a/app/models/custom_actions/actions/estimated_hours.rb b/app/models/automations/actions/estimated_hours.rb similarity index 91% rename from app/models/custom_actions/actions/estimated_hours.rb rename to app/models/automations/actions/estimated_hours.rb index dcec6856db95..1c2b18e79b36 100644 --- a/app/models/custom_actions/actions/estimated_hours.rb +++ b/app/models/automations/actions/estimated_hours.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::EstimatedHours < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Float +class Automations::Actions::EstimatedHours < Automations::Actions::Base + include Automations::Actions::Strategies::Float def self.key :estimated_hours diff --git a/app/models/custom_actions/actions/inexistent.rb b/app/models/automations/actions/inexistent.rb similarity index 95% rename from app/models/custom_actions/actions/inexistent.rb rename to app/models/automations/actions/inexistent.rb index 0be4c169ba7d..d5f8a757e7c3 100644 --- a/app/models/custom_actions/actions/inexistent.rb +++ b/app/models/automations/actions/inexistent.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Inexistent < CustomActions::Actions::Base +class Automations::Actions::Inexistent < Automations::Actions::Base def self.key :inexistent end diff --git a/app/models/custom_actions/actions/notify.rb b/app/models/automations/actions/notify.rb similarity index 93% rename from app/models/custom_actions/actions/notify.rb rename to app/models/automations/actions/notify.rb index e88a50edf5d0..dda26006920f 100644 --- a/app/models/custom_actions/actions/notify.rb +++ b/app/models/automations/actions/notify.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Notify < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Associated +class Automations::Actions::Notify < Automations::Actions::Base + include Automations::Actions::Strategies::Associated def apply(work_package) comment = principals.where(id: values).map do |p| diff --git a/app/models/custom_actions/actions/priority.rb b/app/models/automations/actions/priority.rb similarity index 91% rename from app/models/custom_actions/actions/priority.rb rename to app/models/automations/actions/priority.rb index 7826a99c87e0..97011b57fc26 100644 --- a/app/models/custom_actions/actions/priority.rb +++ b/app/models/automations/actions/priority.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Priority < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Associated +class Automations::Actions::Priority < Automations::Actions::Base + include Automations::Actions::Strategies::Associated def associated IssuePriority diff --git a/app/models/custom_actions/actions/project.rb b/app/models/automations/actions/project.rb similarity index 92% rename from app/models/custom_actions/actions/project.rb rename to app/models/automations/actions/project.rb index 5c893303fd08..c1d6f78ba9c5 100644 --- a/app/models/custom_actions/actions/project.rb +++ b/app/models/automations/actions/project.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Project < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Associated +class Automations::Actions::Project < Automations::Actions::Base + include Automations::Actions::Strategies::Associated PRIORITY = 10 diff --git a/app/models/custom_actions/actions/responsible.rb b/app/models/automations/actions/responsible.rb similarity index 92% rename from app/models/custom_actions/actions/responsible.rb rename to app/models/automations/actions/responsible.rb index c0b8e59f09f2..c83d1531620b 100644 --- a/app/models/custom_actions/actions/responsible.rb +++ b/app/models/automations/actions/responsible.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Responsible < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::MeAssociated +class Automations::Actions::Responsible < Automations::Actions::Base + include Automations::Actions::Strategies::MeAssociated def type :user diff --git a/app/models/custom_actions/actions/start_date.rb b/app/models/automations/actions/start_date.rb similarity index 90% rename from app/models/custom_actions/actions/start_date.rb rename to app/models/automations/actions/start_date.rb index 9d5fc185d48c..5fd880de18b2 100644 --- a/app/models/custom_actions/actions/start_date.rb +++ b/app/models/automations/actions/start_date.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::StartDate < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::DateProperty +class Automations::Actions::StartDate < Automations::Actions::Base + include Automations::Actions::Strategies::DateProperty def self.key :start_date diff --git a/app/models/custom_actions/actions/status.rb b/app/models/automations/actions/status.rb similarity index 91% rename from app/models/custom_actions/actions/status.rb rename to app/models/automations/actions/status.rb index 30a939d38130..e1942004e504 100644 --- a/app/models/custom_actions/actions/status.rb +++ b/app/models/automations/actions/status.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Status < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Associated +class Automations::Actions::Status < Automations::Actions::Base + include Automations::Actions::Strategies::Associated def self.key :status diff --git a/app/models/custom_actions/actions/strategies/associated.rb b/app/models/automations/actions/strategies/associated.rb similarity index 92% rename from app/models/custom_actions/actions/strategies/associated.rb rename to app/models/automations/actions/strategies/associated.rb index e608b67b6389..ffbeb2615ded 100644 --- a/app/models/custom_actions/actions/strategies/associated.rb +++ b/app/models/automations/actions/strategies/associated.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Associated - include CustomActions::ValidateAllowedValue - include CustomActions::ValuesToInteger +module Automations::Actions::Strategies::Associated + include Automations::ValidateAllowedValue + include Automations::ValuesToInteger def allowed_values @allowed_values ||= begin diff --git a/app/models/custom_actions/actions/strategies/associated_custom_field.rb b/app/models/automations/actions/strategies/associated_custom_field.rb similarity index 88% rename from app/models/custom_actions/actions/strategies/associated_custom_field.rb rename to app/models/automations/actions/strategies/associated_custom_field.rb index 97ccda753dc9..ee731e0be06e 100644 --- a/app/models/custom_actions/actions/strategies/associated_custom_field.rb +++ b/app/models/automations/actions/strategies/associated_custom_field.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::AssociatedCustomField - include CustomActions::Actions::Strategies::Associated - include CustomActions::Actions::Strategies::CustomField +module Automations::Actions::Strategies::AssociatedCustomField + include Automations::Actions::Strategies::Associated + include Automations::Actions::Strategies::CustomField def associated custom_field diff --git a/app/models/custom_actions/actions/strategies/boolean.rb b/app/models/automations/actions/strategies/boolean.rb similarity index 50% rename from app/models/custom_actions/actions/strategies/boolean.rb rename to app/models/automations/actions/strategies/boolean.rb index 96ed10bbbbda..dc098d69513f 100644 --- a/app/models/custom_actions/actions/strategies/boolean.rb +++ b/app/models/automations/actions/strategies/boolean.rb @@ -1,5 +1,33 @@ # frozen_string_literal: true +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team @@ -21,8 +49,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Boolean - include CustomActions::ValidateAllowedValue +module Automations::Actions::Strategies::Boolean + include Automations::ValidateAllowedValue def allowed_values [ diff --git a/app/models/custom_actions/actions/strategies/custom_field.rb b/app/models/automations/actions/strategies/custom_field.rb similarity index 96% rename from app/models/custom_actions/actions/strategies/custom_field.rb rename to app/models/automations/actions/strategies/custom_field.rb index 3d9c2071f9d5..f9fbe832e283 100644 --- a/app/models/custom_actions/actions/strategies/custom_field.rb +++ b/app/models/automations/actions/strategies/custom_field.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::CustomField +module Automations::Actions::Strategies::CustomField def apply(work_package) if work_package.respond_to?(custom_field.attribute_setter) set_custom_field_value(work_package) diff --git a/app/models/custom_actions/actions/strategies/date.rb b/app/models/automations/actions/strategies/date.rb similarity index 94% rename from app/models/custom_actions/actions/strategies/date.rb rename to app/models/automations/actions/strategies/date.rb index 2ad203fafa83..e91e6b6f23b4 100644 --- a/app/models/custom_actions/actions/strategies/date.rb +++ b/app/models/automations/actions/strategies/date.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Date +module Automations::Actions::Strategies::Date def values=(values) super(Array(values).map { |v| to_date_or_nil(v) }.uniq) end @@ -38,7 +38,7 @@ def type end def apply(work_package) - accessor = :"#{self.class.key}=" + accessor = :"#{key}=" if work_package.respond_to? accessor work_package.send(accessor, date_to_apply) end @@ -48,7 +48,7 @@ def apply(work_package) def date_to_apply if values.first == "%CURRENT_DATE%" - Date.today + Time.zone.today else values.first end diff --git a/app/models/custom_actions/actions/strategies/date_property.rb b/app/models/automations/actions/strategies/date_property.rb similarity index 92% rename from app/models/custom_actions/actions/strategies/date_property.rb rename to app/models/automations/actions/strategies/date_property.rb index e599ca8263da..dfe6c63f6525 100644 --- a/app/models/custom_actions/actions/strategies/date_property.rb +++ b/app/models/automations/actions/strategies/date_property.rb @@ -28,6 +28,6 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::DateProperty - include CustomActions::Actions::Strategies::Date +module Automations::Actions::Strategies::DateProperty + include Automations::Actions::Strategies::Date end diff --git a/app/models/custom_actions/actions/strategies/float.rb b/app/models/automations/actions/strategies/float.rb similarity index 92% rename from app/models/custom_actions/actions/strategies/float.rb rename to app/models/automations/actions/strategies/float.rb index 69692717366d..103f0101b0b2 100644 --- a/app/models/custom_actions/actions/strategies/float.rb +++ b/app/models/automations/actions/strategies/float.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Float - include CustomActions::Actions::Strategies::ValidateInRange +module Automations::Actions::Strategies::Float + include Automations::Actions::Strategies::ValidateInRange def values=(values) super(Array(values).map { |v| to_float_or_nil(v) }.uniq) diff --git a/app/models/custom_actions/actions/strategies/integer.rb b/app/models/automations/actions/strategies/integer.rb similarity index 89% rename from app/models/custom_actions/actions/strategies/integer.rb rename to app/models/automations/actions/strategies/integer.rb index d69938c4f85c..58942e37a38d 100644 --- a/app/models/custom_actions/actions/strategies/integer.rb +++ b/app/models/automations/actions/strategies/integer.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Integer - include CustomActions::ValuesToInteger - include CustomActions::Actions::Strategies::ValidateInRange +module Automations::Actions::Strategies::Integer + include Automations::ValuesToInteger + include Automations::Actions::Strategies::ValidateInRange def type :integer_property diff --git a/app/models/custom_actions/actions/strategies/link.rb b/app/models/automations/actions/strategies/link.rb similarity index 93% rename from app/models/custom_actions/actions/strategies/link.rb rename to app/models/automations/actions/strategies/link.rb index 94fc5e11523a..5c822ead478b 100644 --- a/app/models/custom_actions/actions/strategies/link.rb +++ b/app/models/automations/actions/strategies/link.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Link - include CustomActions::Actions::Strategies::ValuesToString +module Automations::Actions::Strategies::Link + include Automations::Actions::Strategies::ValuesToString def type :link_property diff --git a/app/models/custom_actions/actions/strategies/me_associated.rb b/app/models/automations/actions/strategies/me_associated.rb similarity index 89% rename from app/models/custom_actions/actions/strategies/me_associated.rb rename to app/models/automations/actions/strategies/me_associated.rb index 241df7b15bec..0a5e7a2e070c 100644 --- a/app/models/custom_actions/actions/strategies/me_associated.rb +++ b/app/models/automations/actions/strategies/me_associated.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::MeAssociated - include ::CustomActions::Actions::Strategies::Associated +module Automations::Actions::Strategies::MeAssociated + include ::Automations::Actions::Strategies::Associated def me_value [current_user_value_key, current_user_name] @@ -40,7 +40,7 @@ def associated end def values=(values) - values = Array(values).map do |v| + cast = Array(values).map do |v| if v == current_user_value_key v else @@ -48,7 +48,7 @@ def values=(values) end end - @values = values.uniq + write_raw_values(cast.uniq) end ## @@ -66,7 +66,7 @@ def current_user_value_key end def current_user_name - I18n.t("custom_actions.actions.assigned_to.executing_user_value") + I18n.t("automations.actions.assigned_to.executing_user_value") end def has_me_value? diff --git a/app/models/custom_actions/actions/strategies/string.rb b/app/models/automations/actions/strategies/string.rb similarity index 92% rename from app/models/custom_actions/actions/strategies/string.rb rename to app/models/automations/actions/strategies/string.rb index ca8707e5c290..1bf1ff84f926 100644 --- a/app/models/custom_actions/actions/strategies/string.rb +++ b/app/models/automations/actions/strategies/string.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::String - include CustomActions::Actions::Strategies::ValuesToString +module Automations::Actions::Strategies::String + include Automations::Actions::Strategies::ValuesToString def type :string_property diff --git a/app/models/custom_actions/actions/strategies/text.rb b/app/models/automations/actions/strategies/text.rb similarity index 92% rename from app/models/custom_actions/actions/strategies/text.rb rename to app/models/automations/actions/strategies/text.rb index 1ceea8308582..d733868aad1a 100644 --- a/app/models/custom_actions/actions/strategies/text.rb +++ b/app/models/automations/actions/strategies/text.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::Text - include CustomActions::Actions::Strategies::ValuesToString +module Automations::Actions::Strategies::Text + include Automations::Actions::Strategies::ValuesToString def type :text_property diff --git a/app/models/custom_actions/actions/strategies/user_custom_field.rb b/app/models/automations/actions/strategies/user_custom_field.rb similarity index 93% rename from app/models/custom_actions/actions/strategies/user_custom_field.rb rename to app/models/automations/actions/strategies/user_custom_field.rb index 022c723cf855..8609e9e117e0 100644 --- a/app/models/custom_actions/actions/strategies/user_custom_field.rb +++ b/app/models/automations/actions/strategies/user_custom_field.rb @@ -28,9 +28,9 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::UserCustomField - include ::CustomActions::Actions::Strategies::CustomField - include ::CustomActions::Actions::Strategies::MeAssociated +module Automations::Actions::Strategies::UserCustomField + include ::Automations::Actions::Strategies::CustomField + include ::Automations::Actions::Strategies::MeAssociated def type :user diff --git a/app/models/custom_actions/actions/strategies/validate_in_range.rb b/app/models/automations/actions/strategies/validate_in_range.rb similarity index 97% rename from app/models/custom_actions/actions/strategies/validate_in_range.rb rename to app/models/automations/actions/strategies/validate_in_range.rb index 51b83d111207..b90e13800b6a 100644 --- a/app/models/custom_actions/actions/strategies/validate_in_range.rb +++ b/app/models/automations/actions/strategies/validate_in_range.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::ValidateInRange +module Automations::Actions::Strategies::ValidateInRange def minimum nil end diff --git a/app/models/custom_actions/actions/strategies/values_to_string.rb b/app/models/automations/actions/strategies/values_to_string.rb similarity index 96% rename from app/models/custom_actions/actions/strategies/values_to_string.rb rename to app/models/automations/actions/strategies/values_to_string.rb index 94e26c46905e..d2e90002ad85 100644 --- a/app/models/custom_actions/actions/strategies/values_to_string.rb +++ b/app/models/automations/actions/strategies/values_to_string.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Strategies::ValuesToString +module Automations::Actions::Strategies::ValuesToString def values=(values) super(Array(values).map { |v| to_string_or_nil(v) }.uniq) end diff --git a/app/models/custom_actions/actions/type.rb b/app/models/automations/actions/type.rb similarity index 92% rename from app/models/custom_actions/actions/type.rb rename to app/models/automations/actions/type.rb index 8d156c2ee856..0fa83ce52252 100644 --- a/app/models/custom_actions/actions/type.rb +++ b/app/models/automations/actions/type.rb @@ -28,8 +28,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Actions::Type < CustomActions::Actions::Base - include CustomActions::Actions::Strategies::Associated +class Automations::Actions::Type < Automations::Actions::Base + include Automations::Actions::Strategies::Associated PRIORITY = 20 diff --git a/app/models/custom_actions/conditions/base.rb b/app/models/automations/conditions/base.rb similarity index 76% rename from app/models/custom_actions/conditions/base.rb rename to app/models/automations/conditions/base.rb index 9852d382fbc3..c4875674c265 100644 --- a/app/models/custom_actions/conditions/base.rb +++ b/app/models/automations/conditions/base.rb @@ -28,11 +28,11 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Conditions::Base +class Automations::Conditions::Base attr_reader :values - prepend CustomActions::ValuesToInteger - include CustomActions::ValidateAllowedValue + prepend Automations::ValuesToInteger + include Automations::ValidateAllowedValue def initialize(values = nil) self.values = values @@ -62,9 +62,7 @@ def fulfilled_by?(work_package, _user) (work_package.respond_to?(:"#{key}_id") && values.include?(work_package.send(:"#{key}_id"))) end - def key - self.class.key - end + delegate :key, to: :class def self.key raise SubclassResponsibilityError @@ -74,38 +72,38 @@ def validate(errors) validate_allowed_value(errors, :conditions) end - def self.getter(custom_action) - ids = custom_action.send(association_ids) + def self.getter(automation) + ids = automation.send(association_ids) new(ids) if ids.any? end - def self.setter(custom_action, condition) + def self.setter(automation, condition) if condition - custom_action.send(:"#{association_ids}=", condition.values) + automation.send(:"#{association_ids}=", condition.values) else - custom_action.send(:"#{association_key}").clear + automation.send(:"#{association_key}").clear end end - def self.custom_action_scope(work_packages, user) - custom_action_scope_has_current(work_packages, user) - .or(custom_action_scope_has_no) + def self.automation_scope(work_packages, user) + automation_scope_has_current(work_packages, user) + .or(automation_scope_has_no) end - def self.custom_action_scope_has_current(work_packages, _user) - CustomAction + def self.automation_scope_has_current(work_packages, _user) + Automation .includes(association_key) .where(habtm_table => { key_id => Array(work_packages).map { |w| w.send(key_id) }.uniq }) end - private_class_method :custom_action_scope_has_current + private_class_method :automation_scope_has_current - def self.custom_action_scope_has_no - CustomAction + def self.automation_scope_has_no + Automation .includes(association_key) .where(habtm_table => { key_id => nil }) end - private_class_method :custom_action_scope_has_no + private_class_method :automation_scope_has_no def self.pluralized_key key.to_s.pluralize.to_sym @@ -113,7 +111,7 @@ def self.pluralized_key private_class_method :pluralized_key def self.habtm_table - :"custom_actions_#{pluralized_key}" + :"automations_#{pluralized_key}" end private_class_method :habtm_table diff --git a/app/models/custom_actions/conditions/inexistent.rb b/app/models/automations/conditions/inexistent.rb similarity index 94% rename from app/models/custom_actions/conditions/inexistent.rb rename to app/models/automations/conditions/inexistent.rb index 5cbf25aac5fe..04702b51e0d9 100644 --- a/app/models/custom_actions/conditions/inexistent.rb +++ b/app/models/automations/conditions/inexistent.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Conditions::Inexistent < CustomActions::Conditions::Base +class Automations::Conditions::Inexistent < Automations::Conditions::Base def self.key :inexistent end diff --git a/app/models/custom_actions/conditions/project.rb b/app/models/automations/conditions/project.rb similarity index 95% rename from app/models/custom_actions/conditions/project.rb rename to app/models/automations/conditions/project.rb index 807a7bdfc79a..296532dfc3b8 100644 --- a/app/models/custom_actions/conditions/project.rb +++ b/app/models/automations/conditions/project.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Conditions::Project < CustomActions::Conditions::Base +class Automations::Conditions::Project < Automations::Conditions::Base def self.key :project end diff --git a/app/models/custom_actions/conditions/role.rb b/app/models/automations/conditions/role.rb similarity index 86% rename from app/models/custom_actions/conditions/role.rb rename to app/models/automations/conditions/role.rb index 77af34115ff9..29d2fa37637d 100644 --- a/app/models/custom_actions/conditions/role.rb +++ b/app/models/automations/conditions/role.rb @@ -28,10 +28,10 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Conditions::Role < CustomActions::Conditions::Base +class Automations::Conditions::Role < Automations::Conditions::Base def fulfilled_by?(work_package, user) values.empty? || - (self.class.roles_in_project(work_package, user).map(&:id) & values).any? + self.class.roles_in_project(work_package, user).map(&:id).intersect?(values) end class << self @@ -49,8 +49,8 @@ def roles_in_project(work_packages, user) private - def custom_action_scope_has_current(work_packages, user) - CustomAction + def automation_scope_has_current(work_packages, user) + Automation .includes(association_key) .where(habtm_table => { key_id => roles_in_project(work_packages, user) }) end @@ -67,11 +67,11 @@ def projects_of(work_packages) end def with_request_store(projects) - RequestStore.store[:custom_actions_role] ||= Hash.new do |hash, hash_projects| + RequestStore.store[:automations_role] ||= Hash.new do |hash, hash_projects| hash[hash_projects] = yield hash_projects end - RequestStore.store[:custom_actions_role][projects] + RequestStore.store[:automations_role][projects] end end diff --git a/app/models/custom_actions/conditions/status.rb b/app/models/automations/conditions/status.rb similarity index 94% rename from app/models/custom_actions/conditions/status.rb rename to app/models/automations/conditions/status.rb index c6b246e0d65a..c5069e596d83 100644 --- a/app/models/custom_actions/conditions/status.rb +++ b/app/models/automations/conditions/status.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Conditions::Status < CustomActions::Conditions::Base +class Automations::Conditions::Status < Automations::Conditions::Base def self.key :status end diff --git a/app/models/custom_actions/conditions/type.rb b/app/models/automations/conditions/type.rb similarity index 95% rename from app/models/custom_actions/conditions/type.rb rename to app/models/automations/conditions/type.rb index af6ff9ba148f..ba60a021e22e 100644 --- a/app/models/custom_actions/conditions/type.rb +++ b/app/models/automations/conditions/type.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::Conditions::Type < CustomActions::Conditions::Base +class Automations::Conditions::Type < Automations::Conditions::Base def self.key :type end diff --git a/app/models/custom_actions/register.rb b/app/models/automations/register.rb similarity index 63% rename from app/models/custom_actions/register.rb rename to app/models/automations/register.rb index e49123fd52cb..18aee3b00f82 100644 --- a/app/models/custom_actions/register.rb +++ b/app/models/automations/register.rb @@ -28,32 +28,38 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Register +module Automations::Register class << self def actions [ - CustomActions::Actions::AssignedTo, - CustomActions::Actions::Responsible, - CustomActions::Actions::Status, - CustomActions::Actions::Priority, - CustomActions::Actions::CustomField, - CustomActions::Actions::Type, - CustomActions::Actions::Project, - CustomActions::Actions::Notify, - CustomActions::Actions::DoneRatio, - CustomActions::Actions::EstimatedHours, - CustomActions::Actions::StartDate, - CustomActions::Actions::DueDate, - CustomActions::Actions::Date + Automations::Actions::AssignedTo, + Automations::Actions::Responsible, + Automations::Actions::Status, + Automations::Actions::Priority, + Automations::Actions::CustomField, + Automations::Actions::Type, + Automations::Actions::Project, + Automations::Actions::Notify, + Automations::Actions::DoneRatio, + Automations::Actions::EstimatedHours, + Automations::Actions::StartDate, + Automations::Actions::DueDate, + Automations::Actions::Date + ] + end + + def triggers + [ + Automations::Triggers::Manual ] end def conditions [ - CustomActions::Conditions::Status, - CustomActions::Conditions::Role, - CustomActions::Conditions::Type, - CustomActions::Conditions::Project + Automations::Conditions::Status, + Automations::Conditions::Role, + Automations::Conditions::Type, + Automations::Conditions::Project ] end end diff --git a/app/models/custom_actions/actions/serializer.rb b/app/models/automations/triggers/base.rb similarity index 70% rename from app/models/custom_actions/actions/serializer.rb rename to app/models/automations/triggers/base.rb index 81ca8efab3e1..38ec9e84d8c2 100644 --- a/app/models/custom_actions/actions/serializer.rb +++ b/app/models/automations/triggers/base.rb @@ -28,30 +28,26 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::Actions::Serializer - module_function +module Automations + module Triggers + class Base < ApplicationRecord + self.table_name = "automation_triggers" - def load(value) - return [] unless value + belongs_to :automation, inverse_of: :triggers - YAML - .safe_load(value, permitted_classes: [Symbol, Date]) - .filter_map do |key, values| - klass = nil + acts_as_list scope: :automation - CustomActions::Register - .actions - .detect do |a| - klass = a.for(key) + validates :type, inclusion: { in: ->(_) { Automations::Register.triggers.map(&:name) } } + + def self.key + name.demodulize.underscore.to_sym end - klass ||= CustomActions::Actions::Inexistent + def self.human_name + I18n.t("automations.triggers.#{key}.label", default: name.demodulize) + end - klass.new(values) + delegate :key, :human_name, to: :class end end - - def dump(actions) - YAML::dump(actions.map { |a| [a.key, a.values.map(&:to_s)] }) - end end diff --git a/spec/factories/custom_action_factory.rb b/app/models/automations/triggers/manual.rb similarity index 86% rename from spec/factories/custom_action_factory.rb rename to app/models/automations/triggers/manual.rb index 1fa4b6cc3f35..3b80a7016f8b 100644 --- a/spec/factories/custom_action_factory.rb +++ b/app/models/automations/triggers/manual.rb @@ -28,9 +28,12 @@ # See COPYRIGHT and LICENSE files for more details. #++ -FactoryBot.define do - factory :custom_action do - sequence(:name) { |n| "Custom action #{n} - name" } - sequence(:description) { |n| "Custom action #{n} - description" } +module Automations + module Triggers + class Manual < Base + store_attribute :options, :button_label, :string + + validates :button_label, presence: true, length: { maximum: 255 } + end end end diff --git a/app/models/custom_actions/validate_allowed_value.rb b/app/models/automations/validate_allowed_value.rb similarity index 50% rename from app/models/custom_actions/validate_allowed_value.rb rename to app/models/automations/validate_allowed_value.rb index b85f7678b066..32dd280e20d0 100644 --- a/app/models/custom_actions/validate_allowed_value.rb +++ b/app/models/automations/validate_allowed_value.rb @@ -1,5 +1,33 @@ # frozen_string_literal: true +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + # OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: # Copyright (C) 2006-2017 Jean-Philippe Lang # Copyright (C) 2010-2013 the ChiliProject Team @@ -21,7 +49,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::ValidateAllowedValue +module Automations::ValidateAllowedValue private def validate_allowed_value(errors, attribute) diff --git a/app/models/custom_actions/values_to_integer.rb b/app/models/automations/values_to_integer.rb similarity index 97% rename from app/models/custom_actions/values_to_integer.rb rename to app/models/automations/values_to_integer.rb index c36aa8fbea69..6894a3f90c70 100644 --- a/app/models/custom_actions/values_to_integer.rb +++ b/app/models/automations/values_to_integer.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module CustomActions::ValuesToInteger +module Automations::ValuesToInteger def values=(values) super(Array(values).map { |v| to_integer_or_nil(v) }.uniq) end diff --git a/app/models/custom_actions/actions/custom_field.rb b/app/models/custom_actions/actions/custom_field.rb deleted file mode 100644 index dd796273c4fc..000000000000 --- a/app/models/custom_actions/actions/custom_field.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -#-- copyright -# OpenProject is an open source project management software. -# Copyright (C) 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 CustomActions::Actions::CustomField < CustomActions::Actions::Base - class << self - def key - custom_field.attribute_name.to_sym - end - - def custom_field - raise SubclassResponsibilityError - end - - def all - WorkPackageCustomField - .usable_as_custom_action - .map do |cf| - create_subclass(cf) - end - end - - def for(key) - match_result = key.match /custom_field_(\d+)/ - - if match_result && (cf = WorkPackageCustomField.find_by(id: match_result[1])) - create_subclass(cf) - end - end - - private - - def create_subclass(custom_field) - klass = Class.new(CustomActions::Actions::CustomField) - klass.define_singleton_method(:custom_field) do - custom_field - end - - klass.include(strategy(custom_field)) - klass - end - - def strategy(custom_field) - case custom_field.field_format - when "string" - CustomActions::Actions::Strategies::String - when "text" - CustomActions::Actions::Strategies::Text - when "link" - CustomActions::Actions::Strategies::Link - when "int" - CustomActions::Actions::Strategies::Integer - when "float" - CustomActions::Actions::Strategies::Float - when "date" - CustomActions::Actions::Strategies::Date - when "bool" - CustomActions::Actions::Strategies::Boolean - when "user" - CustomActions::Actions::Strategies::UserCustomField - when "list", "version" - CustomActions::Actions::Strategies::AssociatedCustomField - end - end - end - - def custom_field - self.class.custom_field - end - - def human_name - custom_field.name - end - - def apply(work_package) - if work_package.respond_to?(custom_field.attribute_setter) - set_custom_field_value(work_package) - validate_custom_field(work_package) - end - end - - private - - def set_custom_field_value(work_package) - work_package.send(custom_field.attribute_setter, values) - end - - def validate_custom_field(work_package) - # Validate the custom field the custom action is changing. - work_package.custom_values_to_validate += Array(work_package.custom_value_for(custom_field)) - end -end diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index ec0b77f51ecf..8ac8956447cc 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -92,12 +92,12 @@ def custom_field params.require(:custom_field).permit(*self.class.permitted_attributes[:custom_field]) end - def custom_action + def automation whitelisted = params - .require(:custom_action) - .permit(*self.class.permitted_attributes[:custom_action]) + .require(:automation) + .permit(*self.class.permitted_attributes[:automation]) - whitelisted.merge(params[:custom_action].slice(:actions, :conditions).permit!) + whitelisted.merge(params[:automation].slice(:actions, :conditions).permit!) end def custom_field_type @@ -507,11 +507,12 @@ def self.permitted_attributes hexcode move_to ), - custom_action: %i( - name - description - move_to - ), + automation: [ + :name, + :description, + :move_to, + { triggers_attributes: [:id, :type, :position, { options: {} }, :_destroy] } + ], custom_field: [ :editable, :field_format, diff --git a/app/models/work_package.rb b/app/models/work_package.rb index 0706f73d421e..d4066af82f45 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -36,7 +36,7 @@ class WorkPackage < ApplicationRecord include WorkPackage::AskBeforeDestruction include WorkPackage::TimeEntriesCleaner include WorkPackage::Ancestors - include WorkPackage::CustomActioned + include WorkPackage::Automatable include WorkPackage::Hooks # Must stay above WorkPackage::Journalized: its after_save persists the # version rows that the journal snapshot then reads. diff --git a/app/models/work_package/custom_actioned.rb b/app/models/work_package/automatable.rb similarity index 79% rename from app/models/work_package/custom_actioned.rb rename to app/models/work_package/automatable.rb index 0d88d078c315..cf63ed4a3b76 100644 --- a/app/models/work_package/custom_actioned.rb +++ b/app/models/work_package/automatable.rb @@ -28,16 +28,21 @@ # See COPYRIGHT and LICENSE files for more details. #++ -module WorkPackage::CustomActioned +module WorkPackage::Automatable extend ActiveSupport::Concern included do - def custom_actions(user) - @custom_actions = CustomAction - .available_conditions - .inject(CustomAction.all) do |scope, condition| - scope.merge(condition.custom_action_scope(self, user)) + def automations(user) + @automations = Automation + .available_conditions + .inject(Automation.all) do |scope, condition| + scope.merge(condition.automation_scope(self, user)) end end + + # API compatibility for /api/v3/custom_actions + def custom_actions(user) + automations(user) + end end end diff --git a/app/models/work_package_custom_field.rb b/app/models/work_package_custom_field.rb index 45547925f280..792076725138 100644 --- a/app/models/work_package_custom_field.rb +++ b/app/models/work_package_custom_field.rb @@ -44,7 +44,7 @@ class WorkPackageCustomField < CustomField scopes :visible, :on_visible_type_and_project - scope :usable_as_custom_action, -> { + scope :usable_as_automation, -> { where.not(field_format: %w[hierarchy weighted_item_list]) .order(:name) } diff --git a/app/services/custom_actions/base_service.rb b/app/services/automations/base_service.rb similarity index 61% rename from app/services/custom_actions/base_service.rb rename to app/services/automations/base_service.rb index dfc18ab4f7ee..fb705ca3e280 100644 --- a/app/services/custom_actions/base_service.rb +++ b/app/services/automations/base_service.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::BaseService +class Automations::BaseService include Shared::BlockService attr_accessor :user @@ -38,7 +38,7 @@ def call(attributes:, &) set_attributes(action, attributes) - contract = CustomActions::CuContract.new(action) + contract = Automations::CuContract.new(action, user) result = ServiceResult.new(success: contract.validate && action.save, result: action, errors: contract.errors) @@ -51,48 +51,39 @@ def call(attributes:, def set_attributes(action, attributes) actions_attributes = attributes.delete(:actions) conditions_attributes = attributes.delete(:conditions) - action.attributes = attributes + triggers_attributes = attributes.delete(:triggers_attributes) + action.attributes = attributes set_actions(action, actions_attributes.symbolize_keys) if actions_attributes set_conditions(action, conditions_attributes.symbolize_keys) if conditions_attributes + set_triggers(action, triggers_attributes) end - def set_actions(action, actions_attributes) - existing_action_keys = action.actions.map(&:key) + def set_actions(automation, actions_attributes) + existing_by_key = automation.actions.index_by(&:key) + incoming_keys = actions_attributes.keys.map(&:to_sym) - remove_actions(action, existing_action_keys - actions_attributes.keys) - update_actions(action, actions_attributes.slice(*existing_action_keys)) - add_actions(action, actions_attributes.slice(*(actions_attributes.keys - existing_action_keys))) - end - - def remove_actions(action, keys) - keys.each do |key| - remove_action(action, key) + (existing_by_key.keys - incoming_keys).each do |key| + existing_by_key[key].mark_for_destruction end - end - def update_actions(action, key_values) - key_values.each do |key, values| - update_action(action, key, values) + actions_attributes.each do |key, values| + key = key.to_sym + if (existing = existing_by_key[key]) + existing.values = values + else + add_action(automation, key, values) + end end end - def add_actions(action, key_values) - key_values.each do |key, values| - add_action(action, key, values) - end - end - - def update_action(action, key, values) - action.actions.detect { |a| a.key == key }.values = values - end + def add_action(automation, key, values) + template = automation.available_actions.detect { |a| a.key == key } || + Automations::Actions::Inexistent.new - def add_action(action, key, values) - action.actions << available_action_for(action, key).new(values) - end - - def remove_action(action, key) - action.actions.reject! { |a| a.key == key } + new_action = template.dup + new_action.values = values + automation.actions << new_action end def set_conditions(action, conditions_attributes) @@ -101,11 +92,20 @@ def set_conditions(action, conditions_attributes) end end - def available_action_for(action, key) - action.available_actions.detect { |a| a.key == key } || CustomActions::Actions::Inexistent + def available_condition_for(action, key) + action.available_conditions.detect { |a| a.key == key } || Automations::Conditions::Inexistent end - def available_condition_for(action, key) - action.available_conditions.detect { |a| a.key == key } || CustomActions::Conditions::Inexistent + def set_triggers(action, attributes) + attributes ||= default_trigger_attributes(action) + return if attributes.blank? + + action.assign_attributes(triggers_attributes: attributes) + end + + def default_trigger_attributes(action) + return [] if action.triggers.any?(Automations::Triggers::Manual) + + [{ type: "Automations::Triggers::Manual", options: { button_label: action.name } }] end end diff --git a/app/services/custom_actions/create_service.rb b/app/services/automations/create_service.rb similarity index 90% rename from app/services/custom_actions/create_service.rb rename to app/services/automations/create_service.rb index 1feb0a003972..32def61a4c06 100644 --- a/app/services/custom_actions/create_service.rb +++ b/app/services/automations/create_service.rb @@ -28,14 +28,13 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::CreateService < CustomActions::BaseService +class Automations::CreateService < Automations::BaseService def initialize(user:) self.user = user + super() end - def call(attributes:, - action: CustomAction.new, - &block) + def call(attributes:, action: Automation.new, &block) super end end diff --git a/app/services/custom_actions/update_service.rb b/app/services/automations/update_service.rb similarity index 95% rename from app/services/custom_actions/update_service.rb rename to app/services/automations/update_service.rb index 12d1d04add66..193b6c10e782 100644 --- a/app/services/custom_actions/update_service.rb +++ b/app/services/automations/update_service.rb @@ -28,13 +28,14 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::UpdateService < CustomActions::BaseService +class Automations::UpdateService < Automations::BaseService attr_accessor :user, :action def initialize(action:, user:) self.action = action self.user = user + super() end def call(attributes:, &) diff --git a/app/services/custom_actions/update_work_package_service.rb b/app/services/automations/update_work_package_service.rb similarity index 91% rename from app/services/custom_actions/update_work_package_service.rb rename to app/services/automations/update_work_package_service.rb index d4058deaf335..03e5d6e8f004 100644 --- a/app/services/custom_actions/update_work_package_service.rb +++ b/app/services/automations/update_work_package_service.rb @@ -28,7 +28,7 @@ # See COPYRIGHT and LICENSE files for more details. #++ -class CustomActions::UpdateWorkPackageService +class Automations::UpdateWorkPackageService include Shared::BlockService include Contracted @@ -56,13 +56,10 @@ def call(work_package:, &) def apply_actions(work_package, actions) changes_before = work_package.changes.dup - apply_actions_sorted(work_package, actions) success, errors = validate(work_package, user) - unless success - retry_apply_actions(work_package, actions, errors, changes_before) - end + retry_apply_actions(work_package, actions, errors, changes_before) unless success end def retry_apply_actions(work_package, actions, errors, changes_before) @@ -76,14 +73,11 @@ def retry_apply_actions(work_package, actions, errors, changes_before) def without_invalid_actions(actions, errors) invalid_keys = errors.attribute_names.map { |k| append_id(k) } - actions.reject { |a| invalid_keys.include?(append_id(a.key)) } end def apply_actions_sorted(work_package, actions) - actions - .sort_by(&:priority) - .each { |a| a.apply(work_package) } + actions.sort_by(&:priority).each { |a| a.apply(work_package) } end def append_id(sym) diff --git a/app/views/custom_actions/_form.html.erb b/app/views/automations/_form.html.erb similarity index 58% rename from app/views/custom_actions/_form.html.erb rename to app/views/automations/_form.html.erb index 90bfaa92e1f1..426a02d2d7ad 100644 --- a/app/views/custom_actions/_form.html.erb +++ b/app/views/automations/_form.html.erb @@ -1,4 +1,35 @@ -<% active_section_keys = @custom_action.actions.map(&:key) %> +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) 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. + +++#%> + +<% active_section_keys = @automation.actions.map(&:key) %> +<% trigger = @automation.triggers.detect { |t| t.is_a?(Automations::Triggers::Manual) } || @automation.triggers.build(type: "Automations::Triggers::Manual") %> +<% trigger_options = @automation.available_triggers.map { |klass| [klass.human_name, klass.name] } %>