attributes.rb 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'digest/md5'
  3. class CoreWorkflow::Attributes
  4. attr_accessor :user, :payload, :assets
  5. def initialize(result_object:)
  6. @result_object = result_object
  7. @user = result_object.user
  8. @payload = result_object.payload
  9. @assets = result_object.assets
  10. end
  11. def payload_class
  12. @payload['class_name'].constantize
  13. end
  14. def article
  15. @article ||= begin
  16. if @payload.dig('params', 'article').present?
  17. clean_params = Ticket::Article.param_cleanup(@payload.dig('params', 'article'), true, false, false)
  18. Ticket::Article.new(clean_params)
  19. end
  20. end
  21. end
  22. def selected_only
  23. # params loading and preparing is very expensive so cache it
  24. checksum = Digest::MD5.hexdigest(Marshal.dump(@payload['params']))
  25. return @selected_only[checksum] if @selected_only.present? && @selected_only[checksum]
  26. @selected_only = {}
  27. @selected_only[checksum] = begin
  28. clean_params = payload_class.association_name_to_id_convert(@payload['params'])
  29. clean_params = payload_class.param_cleanup(clean_params, true, false, false)
  30. payload_class.new(clean_params)
  31. end
  32. end
  33. def selectable_field?(key)
  34. return if key == 'id'
  35. return if !@payload['params'].key?(key)
  36. # some objects have no attributes like "CoreWorkflow"-object as well.
  37. # attributes only exists in the frontend so we skip this check
  38. return true if object_elements.blank?
  39. object_elements_hash.key?(key)
  40. end
  41. def overwrite_selected(result)
  42. selected_attributes = selected_only.attributes
  43. selected_attributes.each_key do |key|
  44. next if !selectable_field?(key)
  45. # special behaviour for owner id
  46. if key == 'owner_id' && selected_attributes[key].nil?
  47. selected_attributes[key] = 1
  48. end
  49. result[key.to_sym] = selected_attributes[key]
  50. end
  51. result
  52. end
  53. def exists?
  54. return if @payload['params']['id'].blank?
  55. @exists ||= payload_class.exists?(id: @payload['params']['id'])
  56. end
  57. def overwritten
  58. # params loading and preparing is very expensive so cache it
  59. checksum = Digest::MD5.hexdigest(Marshal.dump(@payload['params']))
  60. return @overwritten[checksum] if @overwritten.present? && @overwritten[checksum]
  61. @overwritten = {}
  62. @overwritten[checksum] = begin
  63. result = saved_only(dump: true)
  64. overwrite_selected(result)
  65. end
  66. end
  67. def selected
  68. if exists?
  69. overwritten
  70. else
  71. selected_only
  72. end
  73. end
  74. def saved_only(dump: false)
  75. return if !exists?
  76. # dont use lookup here because the cache will not
  77. # know about new attributes and make crashes
  78. @saved_only ||= payload_class.find_by(id: @payload['params']['id'])
  79. return @saved_only if !dump
  80. # we use marshal here because clone still uses references and dup can't
  81. # detect changes for the rails object
  82. Marshal.load(Marshal.dump(@saved_only))
  83. end
  84. def saved
  85. @saved ||= saved_only || payload_class.new
  86. end
  87. def object_elements
  88. @object_elements ||= begin
  89. object_elements_class + object_elements_ticket_create_middle
  90. end
  91. end
  92. def object_elements_class
  93. ObjectManager::Object.new(@payload['class_name']).attributes(@user, saved_only, data_only: false).each_with_object([]) do |element, result|
  94. result << element.data.merge(screens: element.screens)
  95. end
  96. end
  97. # for ticket create add the ticket article body so the field can be switched properly between set/unset readonly.
  98. # https://github.com/zammad/zammad/issues/4540
  99. def object_elements_ticket_create_middle
  100. return [] if @payload['class_name'] != 'Ticket' || @payload['screen'] != 'create_middle'
  101. ObjectManager::Object.new('TicketArticle').attributes(@user, saved_only, data_only: false).each_with_object([]) do |element, result|
  102. result << element.data.merge(screens: element.screens)
  103. end.select { |o| o[:name] == 'body' }
  104. end
  105. def object_elements_hash
  106. @object_elements_hash ||= object_elements.index_by { |x| x[:name] }
  107. end
  108. def screen_value(attribute, type)
  109. screen_value = attribute[:screens].dig(@payload['screen'], type)
  110. return screen_value if !screen_value.nil?
  111. attribute[type.to_sym]
  112. end
  113. def request_id_default
  114. payload['request_id']
  115. end
  116. # dont cache this else the result object will work with references and cache bugs occur
  117. def visibility_default
  118. object_elements.each_with_object({}) do |attribute, result|
  119. result[ attribute[:name] ] = screen_value(attribute, 'shown') == false ? 'remove' : 'show'
  120. end
  121. end
  122. def attribute_mandatory?(attribute)
  123. return screen_value(attribute, 'required').present? if !screen_value(attribute, 'required').nil?
  124. return screen_value(attribute, 'null').blank? if !screen_value(attribute, 'null').nil?
  125. false
  126. end
  127. # dont cache this else the result object will work with references and cache bugs occur
  128. def mandatory_default
  129. object_elements.each_with_object({}) do |attribute, result|
  130. result[ attribute[:name] ] = attribute_mandatory?(attribute)
  131. end
  132. end
  133. # dont cache this else the result object will work with references and cache bugs occur
  134. def auto_select_default
  135. object_elements.each_with_object({}) do |attribute, result|
  136. next if !attribute[:only_shown_if_selectable]
  137. result[ attribute[:name] ] = true
  138. end
  139. end
  140. # dont cache this else the result object will work with references and cache bugs occur
  141. def readonly_default
  142. object_elements.each_with_object({}) do |attribute, result|
  143. result[ attribute[:name] ] = false
  144. end
  145. end
  146. def select_default
  147. @result_object.result[:select] || {}
  148. end
  149. def fill_in_default
  150. @result_object.result[:fill_in] || {}
  151. end
  152. def eval_default
  153. []
  154. end
  155. def matched_workflows_default
  156. @result_object.result[:matched_workflows] || []
  157. end
  158. def rerun_count_default
  159. @result_object.result[:rerun_count] || 0
  160. end
  161. def options_array(options)
  162. result = []
  163. options.each do |option|
  164. result << option['value']
  165. if option['children'].present?
  166. result += options_array(option['children'])
  167. end
  168. end
  169. result
  170. end
  171. def options_hash(options)
  172. options.keys
  173. end
  174. def options_relation(attribute)
  175. key = "#{attribute[:relation]}_#{attribute[:name]}"
  176. @options_relation ||= {}
  177. @options_relation[key] ||= "CoreWorkflow::Attributes::#{attribute[:relation]}".constantize.new(attributes: self, attribute: attribute)
  178. @options_relation[key].values
  179. end
  180. def options_relation_default(attribute)
  181. key = "#{attribute[:relation]}_#{attribute[:name]}"
  182. @options_relation ||= {}
  183. @options_relation[key] ||= "CoreWorkflow::Attributes::#{attribute[:relation]}".constantize.new(attributes: self, attribute: attribute)
  184. @options_relation[key].try(:default_value)
  185. end
  186. def attribute_filter?(attribute)
  187. screen_value(attribute, 'filter').present?
  188. end
  189. def attribute_options_array?(attribute)
  190. attribute[:options].present? && attribute[:options].instance_of?(Array)
  191. end
  192. def attribute_options_hash?(attribute)
  193. attribute[:options].present? && attribute[:options].instance_of?(Hash)
  194. end
  195. def attribute_options_relation?(attribute)
  196. %w[select multiselect tree_select multi_tree_select].include?(attribute[:tag]) && attribute[:relation].present?
  197. end
  198. def values(attribute)
  199. values = nil
  200. if attribute_filter?(attribute)
  201. values = screen_value(attribute, 'filter')
  202. elsif attribute_options_array?(attribute)
  203. values = options_array(attribute[:options])
  204. elsif attribute_options_hash?(attribute)
  205. values = options_hash(attribute[:options])
  206. elsif attribute_options_relation?(attribute)
  207. values = options_relation(attribute)
  208. end
  209. values
  210. end
  211. def values_empty(attribute, values)
  212. return values if values == ['']
  213. saved_value = saved_attribute_value(attribute)
  214. if saved_value.present?
  215. values |= Array(saved_value).map(&:to_s)
  216. end
  217. if screen_value(attribute, 'nulloption') && values.exclude?('')
  218. values.unshift('')
  219. end
  220. values
  221. end
  222. def restrict_values_default
  223. result = {}
  224. object_elements.each do |attribute|
  225. values = values(attribute)
  226. next if values.blank?
  227. values = values_empty(attribute, values)
  228. result[ attribute[:name] ] = values.map(&:to_s)
  229. end
  230. result
  231. end
  232. def all_options_default
  233. object_elements.each_with_object({}) do |attribute, result|
  234. next if !attribute_options_array?(attribute) && !attribute_options_hash?(attribute)
  235. result[ attribute[:name] ] = attribute[:options]
  236. end
  237. end
  238. def historical_options_default
  239. object_elements.each_with_object({}) do |attribute, result|
  240. next if attribute[:historical_options].blank?
  241. result[ attribute[:name] ] = attribute[:historical_options]
  242. end
  243. end
  244. def saved_attribute_value(attribute)
  245. # special case for owner_id
  246. return if saved_only&.class == Ticket && attribute[:name] == 'owner_id'
  247. saved_only&.try(attribute[:name])
  248. end
  249. end