can_perform_changes.rb 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. ##
  3. # A mixin for ActiveRecord models that enables the possibilitty to perform actions.
  4. #
  5. # It's normally used to perform actions for the following functionalities: `Trigger`, `Job`, `Macro`.
  6. #
  7. # With `available_perform_change_actions` you need to define which action is supported from the model.
  8. # It's also possible to run a `pre_execution` for a specifc model, to prepare special data for the actions (e.g. fetch
  9. # the article in the ticket context, when a `article_id` is present inside the `context_data`).
  10. #
  11. # The actions can run in different phases: `initial`, `before_save`, `after_save`. The initial phase could
  12. # also manipulate the actions for the other phases (e.g. the delete action will skip the attribute updates).
  13. #
  14. # In the ticket context you can see how it's possible to add custom model actions and also to extend the
  15. # action layer in general (e.g. usage of `pre_execution`)
  16. #
  17. # @example
  18. #
  19. # class User < ApplicationRecord
  20. # include CanPerformChanges
  21. #
  22. # available_perform_change_actions :data_privacy_deletion_task, :attribute_updates
  23. # end
  24. #
  25. # user.perform_changes(trigger, 'trigger', item, current_user_id)
  26. #
  27. # user.perform_changes(job, 'job', item, current_user_id)
  28. #
  29. module CanPerformChanges
  30. extend ActiveSupport::Concern
  31. # Perform changes on self according to perform rules
  32. #
  33. # @param performable [Trigger, Macro, Job] object
  34. # @param origin [String] name of the object to be performed
  35. # @param context_data [Hash]
  36. # @param user_id [Integer] to run as
  37. # @param activator_type [String] activator of time-based triggers reminder_reached, escalation, null otherwise
  38. # @yield [object, save_needed] alternative way to save object during application
  39. # @yieldparam [object, [Ticket, User, Organization] object performed on
  40. # @yieldparam [save_needed, [Boolean] if changes were applied that should be saved
  41. def perform_changes(performable, origin, context_data = nil, user_id = nil, activator_type: nil, &)
  42. return if !execute?(performable, activator_type)
  43. perform_changes_data = {
  44. performable: performable,
  45. origin: origin,
  46. context_data: context_data,
  47. user_id: user_id,
  48. }
  49. Rails.logger.debug { "Perform #{origin} #{performable.perform.inspect} on #{self.class.name}.find(#{id})" }
  50. try(:pre_execute, perform_changes_data)
  51. execute(perform_changes_data, &)
  52. true
  53. end
  54. private
  55. class_methods do
  56. # Defines the actions that are performed for the object.
  57. def available_perform_change_actions(*actions)
  58. @available_perform_change_actions ||= actions
  59. end
  60. end
  61. def execute?(performable, activator_type)
  62. performable_on_result = performable.try(:performable_on?, self, activator_type:)
  63. # performable_on_result can be nil, false or true
  64. return false if performable_on_result.eql?(false)
  65. true
  66. end
  67. def execute(perform_changes_data)
  68. prepared_actions = prepare_actions(perform_changes_data)
  69. raise "The given #{perform_changes_data[:origin]} contains no valid actions, stopping!" if prepared_actions.all? { |_, v| v.blank? }
  70. prepared_actions[:initial].each do |instance|
  71. instance.execute(prepared_actions)
  72. end
  73. save_needed = execute_before_save(prepared_actions[:before_save])
  74. if block_given?
  75. yield(self, save_needed)
  76. elsif save_needed
  77. save!
  78. end
  79. prepared_actions[:after_save]&.each(&:execute)
  80. true
  81. end
  82. def execute_before_save(before_save_actions)
  83. return if !before_save_actions
  84. before_save_actions.reduce(false) do |memo, elem|
  85. changed = elem.execute
  86. memo || changed
  87. end
  88. end
  89. def prepare_actions(perform_changes_data)
  90. action_checks = %w[notification additional_object object attribute_update]
  91. actions = {}
  92. perform_changes_data[:performable].perform.each do |attribute, action_value|
  93. (object_name, object_key) = attribute.split('.', 2)
  94. action = nil
  95. action_checks.each do |key|
  96. action = send(:"#{key}_action", object_name, object_key, action_value, actions)
  97. break if action
  98. end
  99. next if action.nil? || self.class.available_perform_change_actions.exclude?(action[:name])
  100. actions[action[:name]] = action[:value]
  101. end
  102. prepared_actions = {
  103. initial: [],
  104. before_save: [],
  105. after_save: [],
  106. }
  107. actions.each do |action, value|
  108. instance = create_action_instance(action, value, perform_changes_data)
  109. prepared_actions[instance.class.phase].push(instance)
  110. end
  111. prepared_actions
  112. end
  113. def notification_action(object_name, object_key, action_value, _prepared_actions)
  114. return if !object_name.eql?('notification')
  115. { name: :"notification_#{object_key}", value: action_value }
  116. end
  117. def additional_object_action(*)
  118. return if !respond_to?(:additional_object_actions)
  119. additional_object_action(*)
  120. end
  121. def object_action(object_name, object_key, action_value, _prepared_actions)
  122. if self.class.name.downcase.eql?(object_name) && object_key.eql?('action')
  123. return { name: action_value['value'].to_sym, value: true }
  124. end
  125. nil
  126. end
  127. def attribute_update_action(object_name, object_key, action_value, prepared_actions)
  128. return if !self.class.name.downcase.eql?(object_name)
  129. prepared_actions[:attribute_updates] ||= {}
  130. prepared_actions[:attribute_updates][object_key] = action_value
  131. { name: :attribute_updates, value: prepared_actions[:attribute_updates] }
  132. end
  133. def create_action_instance(action, data, perform_changes_data)
  134. PerformChanges::Action.action_lookup[action].new(self, data, perform_changes_data)
  135. end
  136. end