can_perform_changes.rb 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  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. performable.try(:performed_on, self, activator_type:)
  53. true
  54. end
  55. private
  56. class_methods do
  57. # Defines the actions that are performed for the object.
  58. def available_perform_change_actions(*actions)
  59. @available_perform_change_actions ||= actions
  60. end
  61. end
  62. def execute?(performable, activator_type)
  63. performable_on_result = performable.try(:performable_on?, self, activator_type:)
  64. # performable_on_result can be nil, false or true
  65. return false if performable_on_result.eql?(false)
  66. true
  67. end
  68. def execute(perform_changes_data)
  69. prepared_actions = prepare_actions(perform_changes_data)
  70. raise "The given #{perform_changes_data[:origin]} contains no valid actions, stopping!" if prepared_actions.all? { |_, v| v.blank? }
  71. prepared_actions[:initial].each do |instance|
  72. instance.execute(prepared_actions)
  73. end
  74. save_needed = execute_before_save(prepared_actions[:before_save])
  75. if block_given?
  76. yield(self, save_needed)
  77. elsif save_needed
  78. save!
  79. end
  80. prepared_actions[:after_save]&.each(&:execute)
  81. true
  82. end
  83. def execute_before_save(before_save_actions)
  84. return if !before_save_actions
  85. before_save_actions.reduce(false) do |memo, elem|
  86. changed = elem.execute
  87. memo || changed
  88. end
  89. end
  90. def prepare_actions(perform_changes_data)
  91. action_checks = %w[notification additional_object object attribute_update]
  92. actions = {}
  93. perform_changes_data[:performable].perform.each do |attribute, action_value|
  94. (object_name, object_key) = attribute.split('.', 2)
  95. action = nil
  96. action_checks.each do |key|
  97. action = send(:"#{key}_action", object_name, object_key, action_value, actions)
  98. break if action
  99. end
  100. next if action.nil? || self.class.available_perform_change_actions.exclude?(action[:name])
  101. actions[action[:name]] = action[:value]
  102. end
  103. prepared_actions = {
  104. initial: [],
  105. before_save: [],
  106. after_save: [],
  107. }
  108. actions.each do |action, value|
  109. instance = create_action_instance(action, value, perform_changes_data)
  110. prepared_actions[instance.class.phase].push(instance)
  111. end
  112. prepared_actions
  113. end
  114. def notification_action(object_name, object_key, action_value, _prepared_actions)
  115. return if !object_name.eql?('notification')
  116. { name: :"notification_#{object_key}", value: action_value }
  117. end
  118. def additional_object_action(*)
  119. return if !respond_to?(:additional_object_actions)
  120. additional_object_action(*)
  121. end
  122. def object_action(object_name, object_key, action_value, _prepared_actions)
  123. if self.class.name.downcase.eql?(object_name) && object_key.eql?('action')
  124. return { name: action_value['value'].to_sym, value: true }
  125. end
  126. nil
  127. end
  128. def attribute_update_action(object_name, object_key, action_value, prepared_actions)
  129. return if !self.class.name.downcase.eql?(object_name)
  130. prepared_actions[:attribute_updates] ||= {}
  131. prepared_actions[:attribute_updates][object_key] = action_value
  132. { name: :attribute_updates, value: prepared_actions[:attribute_updates] }
  133. end
  134. def create_action_instance(action, data, perform_changes_data)
  135. PerformChanges::Action.action_lookup[action].new(self, data, perform_changes_data)
  136. end
  137. end