can_perform_changes.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. def perform_changes(performable, origin, context_data = nil, user_id = nil, activator_type: nil)
  32. return if !execute?(performable, activator_type)
  33. perform_changes_data = {
  34. performable: performable,
  35. origin: origin,
  36. context_data: context_data,
  37. user_id: user_id,
  38. }
  39. Rails.logger.debug { "Perform #{origin} #{performable.perform.inspect} on #{self.class.name}.find(#{id})" }
  40. try(:pre_execute, perform_changes_data)
  41. execute(perform_changes_data)
  42. true
  43. end
  44. private
  45. class_methods do
  46. # Defines the actions that are performed for the object.
  47. def available_perform_change_actions(*actions)
  48. @available_perform_change_actions ||= actions
  49. end
  50. end
  51. def execute?(performable, activator_type)
  52. performable_on_result = performable.try(:performable_on?, self, activator_type:)
  53. # performable_on_result can be nil, false or true
  54. return false if performable_on_result.eql?(false)
  55. true
  56. end
  57. def execute(perform_changes_data)
  58. prepared_actions = prepare_actions(perform_changes_data)
  59. raise "The given #{perform_changes_data[:origin]} contains no valid actions, stopping!" if prepared_actions.all? { |_, v| v.blank? }
  60. prepared_actions[:initial].each do |instance|
  61. instance.execute(prepared_actions)
  62. end
  63. execute_before_save(prepared_actions[:before_save])
  64. prepared_actions[:after_save]&.each(&:execute)
  65. true
  66. end
  67. def execute_before_save(before_save_actions)
  68. save_needed = false
  69. before_save_actions&.each do |instance|
  70. changed = instance.execute
  71. save_needed = true if changed && !save_needed
  72. end
  73. save! if save_needed
  74. end
  75. def prepare_actions(perform_changes_data)
  76. action_checks = %w[notification additional_object object attribute_update]
  77. actions = {}
  78. perform_changes_data[:performable].perform.each do |attribute, action_value|
  79. (object_name, object_key) = attribute.split('.', 2)
  80. action = nil
  81. action_checks.each do |key|
  82. action = send(:"#{key}_action", object_name, object_key, action_value, actions)
  83. break if action
  84. end
  85. next if action.nil? || self.class.available_perform_change_actions.exclude?(action[:name])
  86. actions[action[:name]] = action[:value]
  87. end
  88. prepared_actions = {
  89. initial: [],
  90. before_save: [],
  91. after_save: [],
  92. }
  93. actions.each do |action, value|
  94. instance = create_action_instance(action, value, perform_changes_data)
  95. prepared_actions[instance.class.phase].push(instance)
  96. end
  97. prepared_actions
  98. end
  99. def notification_action(object_name, object_key, action_value, _prepared_actions)
  100. return if !object_name.eql?('notification')
  101. { name: :"notification_#{object_key}", value: action_value }
  102. end
  103. def additional_object_action(*)
  104. return if !respond_to?(:additional_object_actions)
  105. additional_object_action(*)
  106. end
  107. def object_action(object_name, object_key, action_value, _prepared_actions)
  108. if self.class.name.downcase.eql?(object_name) && object_key.eql?('action')
  109. return { name: action_value['value'].to_sym, value: true }
  110. end
  111. nil
  112. end
  113. def attribute_update_action(object_name, object_key, action_value, prepared_actions)
  114. return if !self.class.name.downcase.eql?(object_name)
  115. prepared_actions[:attribute_updates] ||= {}
  116. prepared_actions[:attribute_updates][object_key] = action_value
  117. { name: :attribute_updates, value: prepared_actions[:attribute_updates] }
  118. end
  119. def create_action_instance(action, data, perform_changes_data)
  120. PerformChanges::Action.action_lookup[action].new(self, data, perform_changes_data)
  121. end
  122. end