has_history.rb 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. # Copyright (C) 2012-2024 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. attributes[:sourceable] = @history_changes_source.try(:delete, attributes[:history_attribute]) || @history_changes_source.try(:delete, "#{attributes[:history_attribute]}_id") || @history_changes_source&.dig(type)
  135. History.add(attributes)
  136. end
  137. # callback function to overwrite
  138. # default history log attributes
  139. # gets called from history_log
  140. def history_log_attributes
  141. {}
  142. end
  143. =begin
  144. get history log for this object
  145. organization = Organization.find(123)
  146. result = organization.history_get
  147. returns
  148. result = [
  149. {
  150. :type => 'created',
  151. :object => 'Organization',
  152. :created_by_id => 3,
  153. :created_at => "2013-08-19 20:41:33",
  154. },
  155. {
  156. :type => 'updated',
  157. :object => 'Organization',
  158. :attribute => 'note',
  159. :o_id => 1,
  160. :id_to => nil,
  161. :id_from => nil,
  162. :value_from => "some note",
  163. :value_to => "some other note",
  164. :created_by_id => 3,
  165. :created_at => "2013-08-19 20:41:33",
  166. },
  167. ]
  168. to get history log for this object with all assets
  169. organization = Organization.find(123)
  170. result = organization.history_get(true)
  171. returns
  172. result = {
  173. :history => [
  174. { ... },
  175. { ... },
  176. ],
  177. :assets => {
  178. ...
  179. }
  180. }
  181. =end
  182. def history_get(fulldata = false)
  183. relation_object = history_relation_object
  184. if !fulldata
  185. return History.list(self.class.name, self['id'], relation_object)
  186. end
  187. # get related objects
  188. history = History.list(self.class.name, self['id'], relation_object, true)
  189. history[:list].each do |item|
  190. record = item['object'].constantize.lookup(id: item['o_id'])
  191. if record.present?
  192. history[:assets] = record.assets(history[:assets])
  193. end
  194. next if !item['related_object']
  195. record = item['related_object'].constantize.lookup(id: item['related_o_id'])
  196. if record.present?
  197. history[:assets] = record.assets(history[:assets])
  198. end
  199. end
  200. {
  201. history: history[:list],
  202. assets: history[:assets],
  203. }
  204. end
  205. def history_relation_object
  206. @history_relation_object ||= self.class.instance_variable_get(:@history_relation_object) || []
  207. end
  208. def history_change_source_clear
  209. @history_changes_source = nil
  210. @history_changes_source_last = nil
  211. end
  212. def history_change_source_attribute(source, attribute)
  213. return if source.blank?
  214. return if [Job, Trigger, PostmasterFilter].exclude?(source.class)
  215. return if !source.persisted?
  216. @history_changes_source ||= {}
  217. @history_changes_source[attribute] = source
  218. @history_changes_source_last = source
  219. end
  220. # methods defined here are going to extend the class, not the instance of it
  221. class_methods do
  222. =begin
  223. serve method to ignore model attributes in historization
  224. class Model < ApplicationModel
  225. include HasHistory
  226. history_attributes_ignored :create_article_type_id, :preferences
  227. end
  228. =end
  229. def history_attributes_ignored(*attributes)
  230. @history_attributes_ignored = attributes
  231. end
  232. =begin
  233. serve method to ignore model attributes in historization
  234. class Model < ApplicationModel
  235. include HasHistory
  236. history_relation_object 'Some::Relation::Object'
  237. end
  238. =end
  239. def history_relation_object(*attributes)
  240. @history_relation_object ||= []
  241. @history_relation_object |= attributes
  242. end
  243. end
  244. end