has_history.rb 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. module HasHistory
  3. extend ActiveSupport::Concern
  4. included do
  5. attr_accessor :history_changes_last_done
  6. after_create :history_prefill, :history_create, :history_change_source_clear
  7. after_update :history_update, :history_change_source_clear
  8. after_destroy :history_destroy
  9. end
  10. def history_prefill
  11. return if @history_changes_source.blank?
  12. @history_changes_source.each do |key, value|
  13. next if !value.is_a?(PostmasterFilter)
  14. attribute_name = history_attribute_name(key)
  15. attribute_value = history_attribute_changes(key, [nil, self[key]])
  16. data = {
  17. history_attribute: attribute_name,
  18. }.merge(attribute_value)
  19. history_log('set', created_by_id, data)
  20. end
  21. end
  22. =begin
  23. log object create history, if configured - will be executed automatically
  24. model = Model.find(123)
  25. model.history_create
  26. =end
  27. def history_create
  28. history_log('created', created_by_id)
  29. end
  30. =begin
  31. log object update history with all updated attributes, if configured - will be executed automatically
  32. model = Model.find(123)
  33. model.history_update
  34. =end
  35. def history_update
  36. return if !saved_changes?
  37. # return if it's no update
  38. return if new_record?
  39. # new record also triggers update, so ignore new records
  40. changes = saved_changes
  41. history_changes_last_done&.each do |key, value|
  42. if changes.key?(key) && changes[key] == value
  43. changes.delete(key)
  44. end
  45. end
  46. self.history_changes_last_done = changes
  47. # logger.info 'updated ' + self.changes.inspect
  48. return if changes['id'] && !changes['id'][0]
  49. ignored_attributes = self.class.instance_variable_get(:@history_attributes_ignored) || []
  50. ignored_attributes += %i[created_at updated_at created_by_id updated_by_id]
  51. changes.each do |key, value|
  52. next if ignored_attributes.include?(key.to_sym)
  53. # get attribute name
  54. attribute_name = history_attribute_name(key)
  55. attribute_value = history_attribute_changes(key, value)
  56. data = {
  57. history_attribute: attribute_name,
  58. }.merge(attribute_value)
  59. # logger.info "HIST NEW #{self.class.to_s}.find(#{self.id}) #{data.inspect}"
  60. history_log('updated', updated_by_id, data)
  61. end
  62. end
  63. def history_attribute_name(key)
  64. attribute_name = key.to_s
  65. if attribute_name[-3, 3] == '_id'
  66. attribute_name = attribute_name[ 0, attribute_name.length - 3 ]
  67. end
  68. attribute_name
  69. end
  70. def history_attribute_changes(key, value_changes)
  71. attribute_name = history_attribute_name(key)
  72. value_id = []
  73. value_str = [ value_changes[0], value_changes[1] ]
  74. if key.to_s[-3, 3] == '_id'
  75. value_id[0] = value_changes[0]
  76. value_id[1] = value_changes[1]
  77. if respond_to?(attribute_name) && send(attribute_name)
  78. relation_class = send(attribute_name).class
  79. if relation_class && value_id[0]
  80. relation_model = relation_class.lookup(id: value_id[0])
  81. if relation_model
  82. if relation_model['name']
  83. value_str[0] = relation_model['name']
  84. elsif relation_model.respond_to?(:fullname)
  85. value_str[0] = relation_model.send(:fullname)
  86. end
  87. end
  88. end
  89. if relation_class && value_id[1]
  90. relation_model = relation_class.lookup(id: value_id[1])
  91. if relation_model
  92. if relation_model['name']
  93. value_str[1] = relation_model['name']
  94. elsif relation_model.respond_to?(:fullname)
  95. value_str[1] = relation_model.send(:fullname)
  96. end
  97. end
  98. end
  99. end
  100. end
  101. {
  102. value_from: value_str[0].to_s,
  103. value_to: value_str[1].to_s,
  104. id_from: value_id[0],
  105. id_to: value_id[1],
  106. }
  107. end
  108. =begin
  109. delete object history, will be executed automatically
  110. model = Model.find(123)
  111. model.history_destroy
  112. =end
  113. def history_destroy
  114. History.remove(self.class.to_s, id)
  115. end
  116. =begin
  117. create history entry for this object
  118. organization = Organization.find(123)
  119. result = organization.history_log('created', user_id)
  120. returns
  121. result = true # false
  122. =end
  123. def history_log(type, user_id, attributes = {})
  124. attributes.merge!(
  125. o_id: self['id'],
  126. history_type: type,
  127. history_object: self.class.name,
  128. related_o_id: nil,
  129. related_history_object: nil,
  130. created_by_id: user_id,
  131. updated_at: updated_at,
  132. created_at: updated_at,
  133. ).merge!(history_log_attributes)
  134. if attributes[:sourceable].blank?
  135. attributes[:sourceable] = @history_changes_source.try(:delete, attributes[:history_attribute]) || @history_changes_source.try(:delete, "#{attributes[:history_attribute]}_id") || @history_changes_source&.dig(type)
  136. end
  137. History.add(attributes)
  138. end
  139. # callback function to overwrite
  140. # default history log attributes
  141. # gets called from history_log
  142. def history_log_attributes
  143. {}
  144. end
  145. =begin
  146. get history log for this object
  147. organization = Organization.find(123)
  148. result = organization.history_get
  149. returns
  150. result = [
  151. {
  152. :type => 'created',
  153. :object => 'Organization',
  154. :created_by_id => 3,
  155. :created_at => "2013-08-19 20:41:33",
  156. },
  157. {
  158. :type => 'updated',
  159. :object => 'Organization',
  160. :attribute => 'note',
  161. :o_id => 1,
  162. :id_to => nil,
  163. :id_from => nil,
  164. :value_from => "some note",
  165. :value_to => "some other note",
  166. :created_by_id => 3,
  167. :created_at => "2013-08-19 20:41:33",
  168. },
  169. ]
  170. to get history log for this object with all assets
  171. organization = Organization.find(123)
  172. result = organization.history_get(true)
  173. returns
  174. result = {
  175. :history => [
  176. { ... },
  177. { ... },
  178. ],
  179. :assets => {
  180. ...
  181. }
  182. }
  183. =end
  184. def history_get(fulldata = false)
  185. relation_object = history_relation_object
  186. if !fulldata
  187. return History.list(self.class.name, self['id'], relation_object)
  188. end
  189. # get related objects
  190. history = History.list(self.class.name, self['id'], relation_object, true)
  191. history[:list].each do |item|
  192. record = item['object'].constantize.lookup(id: item['o_id'])
  193. if record.present?
  194. history[:assets] = record.assets(history[:assets])
  195. end
  196. next if !item['related_object']
  197. record = item['related_object'].constantize.lookup(id: item['related_o_id'])
  198. if record.present?
  199. history[:assets] = record.assets(history[:assets])
  200. end
  201. end
  202. {
  203. history: history[:list],
  204. assets: history[:assets],
  205. }
  206. end
  207. def history_relation_object
  208. @history_relation_object ||= self.class.instance_variable_get(:@history_relation_object) || []
  209. end
  210. def history_change_source_clear
  211. @history_changes_source = nil
  212. @history_changes_source_last = nil
  213. end
  214. def history_change_source_attribute(source, attribute)
  215. return if source.blank?
  216. return if [Job, Trigger, PostmasterFilter].exclude?(source.class)
  217. return if !source.persisted?
  218. @history_changes_source ||= {}
  219. @history_changes_source[attribute] = source
  220. @history_changes_source_last = source
  221. end
  222. # methods defined here are going to extend the class, not the instance of it
  223. class_methods do
  224. =begin
  225. serve method to ignore model attributes in historization
  226. class Model < ApplicationModel
  227. include HasHistory
  228. history_attributes_ignored :create_article_type_id, :preferences
  229. end
  230. =end
  231. def history_attributes_ignored(*attributes)
  232. @history_attributes_ignored = attributes
  233. end
  234. =begin
  235. serve method to ignore model attributes in historization
  236. class Model < ApplicationModel
  237. include HasHistory
  238. history_relation_object 'Some::Relation::Object'
  239. end
  240. =end
  241. def history_relation_object(*attributes)
  242. @history_relation_object ||= []
  243. @history_relation_object |= attributes
  244. end
  245. end
  246. end