ticket.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. # Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
  2. class Ticket < ApplicationModel
  3. include Ticket::Escalation
  4. include Ticket::Subject
  5. include Ticket::Permission
  6. load 'ticket/assets.rb'
  7. include Ticket::Assets
  8. load 'ticket/history_log.rb'
  9. include Ticket::HistoryLog
  10. load 'ticket/activity_stream_log.rb'
  11. include Ticket::ActivityStreamLog
  12. load 'ticket/search_index.rb'
  13. include Ticket::SearchIndex
  14. extend Ticket::Search
  15. store :preferences
  16. before_create :check_generate, :check_defaults, :check_title
  17. before_update :check_defaults, :check_title, :reset_pending_time
  18. before_destroy :destroy_dependencies
  19. notify_clients_support
  20. latest_change_support
  21. activity_stream_support ignore_attributes: {
  22. create_article_type_id: true,
  23. create_article_sender_id: true,
  24. article_count: true,
  25. first_response: true,
  26. first_response_escal_date: true,
  27. first_response_sla_time: true,
  28. first_response_in_min: true,
  29. first_response_diff_in_min: true,
  30. close_time: true,
  31. close_time_escal_date: true,
  32. close_time_sla_time: true,
  33. close_time_in_min: true,
  34. close_time_diff_in_min: true,
  35. update_time_escal_date: true,
  36. updtate_time_sla_time: true,
  37. update_time_in_min: true,
  38. update_time_diff_in_min: true,
  39. last_contact: true,
  40. last_contact_agent: true,
  41. last_contact_customer: true,
  42. }
  43. history_support ignore_attributes: {
  44. create_article_type_id: true,
  45. create_article_sender_id: true,
  46. article_count: true,
  47. }
  48. search_index_support
  49. belongs_to :group
  50. has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update
  51. belongs_to :organization
  52. belongs_to :state, class_name: 'Ticket::State'
  53. belongs_to :priority, class_name: 'Ticket::Priority'
  54. belongs_to :owner, class_name: 'User'
  55. belongs_to :customer, class_name: 'User'
  56. belongs_to :created_by, class_name: 'User'
  57. belongs_to :updated_by, class_name: 'User'
  58. belongs_to :create_article_type, class_name: 'Ticket::Article::Type'
  59. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender'
  60. self.inheritance_column = nil
  61. attr_accessor :callback_loop
  62. =begin
  63. list of agents in group of ticket
  64. ticket = Ticket.find(123)
  65. result = ticket.agent_of_group
  66. returns
  67. result = [user1, user2, ...]
  68. =end
  69. def agent_of_group
  70. Group.find( group_id ).users.where( active: true ).joins(:roles).where( 'roles.name' => Z_ROLENAME_AGENT, 'roles.active' => true ).uniq()
  71. end
  72. =begin
  73. get user access conditions
  74. conditions = Ticket.access_condition( User.find(1) )
  75. returns
  76. result = [user1, user2, ...]
  77. =end
  78. def self.access_condition(user)
  79. access_condition = []
  80. if user.role?(Z_ROLENAME_AGENT)
  81. group_ids = Group.select( 'groups.id' ).joins(:users)
  82. .where( 'groups_users.user_id = ?', user.id )
  83. .where( 'groups.active = ?', true )
  84. .map( &:id )
  85. access_condition = [ 'group_id IN (?)', group_ids ]
  86. else
  87. if !user.organization || ( !user.organization.shared || user.organization.shared == false )
  88. access_condition = [ 'tickets.customer_id = ?', user.id ]
  89. else
  90. access_condition = [ '( tickets.customer_id = ? OR tickets.organization_id = ? )', user.id, user.organization.id ]
  91. end
  92. end
  93. access_condition
  94. end
  95. =begin
  96. processes tickets which have reached their pending time and sets next state_id
  97. processed_tickets = Ticket.process_pending()
  98. returns
  99. processed_tickets = [<Ticket>, ...]
  100. =end
  101. def self.process_pending
  102. ticket_states = Ticket::State.where(
  103. state_type_id: Ticket::StateType.find_by( name: 'pending action' ),
  104. )
  105. .where.not(next_state_id: nil) # rubocop:disable Style/MultilineOperationIndentation
  106. return [] if !ticket_states
  107. next_state_map = {}
  108. ticket_states.each { |state|
  109. next_state_map[state.id] = state.next_state_id
  110. }
  111. tickets = where(
  112. state_id: next_state_map.keys,
  113. )
  114. .where( 'pending_time <= ?', Time.zone.now ) # rubocop:disable Style/MultilineOperationIndentation
  115. return [] if !tickets
  116. result = []
  117. tickets.each { |ticket|
  118. ticket.state_id = next_state_map[ticket.state_id]
  119. ticket.updated_at = Time.zone.now
  120. ticket.updated_by_id = 1
  121. ticket.save!
  122. result.push ticket
  123. }
  124. # we do not have an destructor at this point, so we need to
  125. # execute ticket events manually
  126. Observer::Ticket::Notification.transaction
  127. result
  128. end
  129. =begin
  130. merge tickets
  131. ticket = Ticket.find(123)
  132. result = ticket.merge_to(
  133. ticket_id: 123,
  134. user_id: 123,
  135. )
  136. returns
  137. result = true|false
  138. =end
  139. def merge_to(data)
  140. # update articles
  141. Ticket::Article.where( ticket_id: id ).each(&:touch)
  142. # quiet update of reassign of articles
  143. Ticket::Article.where( ticket_id: id ).update_all( ['ticket_id = ?', data[:ticket_id] ] )
  144. # touch new ticket (to broadcast change)
  145. Ticket.find( data[:ticket_id] ).touch
  146. # update history
  147. # create new merge article
  148. Ticket::Article.create(
  149. ticket_id: id,
  150. type_id: Ticket::Article::Type.lookup( name: 'note' ).id,
  151. sender_id: Ticket::Article::Sender.lookup( name: Z_ROLENAME_AGENT ).id,
  152. body: 'merged',
  153. internal: false,
  154. created_by_id: data[:user_id],
  155. updated_by_id: data[:user_id],
  156. )
  157. # add history to both
  158. # link tickets
  159. Link.add(
  160. link_type: 'parent',
  161. link_object_source: 'Ticket',
  162. link_object_source_value: data[:ticket_id],
  163. link_object_target: 'Ticket',
  164. link_object_target_value: id
  165. )
  166. # set state to 'merged'
  167. self.state_id = Ticket::State.lookup( name: 'merged' ).id
  168. # rest owner
  169. self.owner_id = User.find_by( login: '-' ).id
  170. # save ticket
  171. save
  172. end
  173. =begin
  174. check if online notifcation should be shown in general as already seen with current state
  175. ticket = Ticket.find(1)
  176. seen = ticket.online_notification_seen_state(user_id_check)
  177. returns
  178. result = true # or false
  179. check if online notifcation should be shown for this user as already seen with current state
  180. ticket = Ticket.find(1)
  181. seen = ticket.online_notification_seen_state(check_user_id)
  182. returns
  183. result = true # or false
  184. =end
  185. def online_notification_seen_state(user_id_check = nil)
  186. state = Ticket::State.lookup( id: state_id )
  187. state_type = Ticket::StateType.lookup( id: state.state_type_id )
  188. # set all to seen if pending action state is a closed or merged state
  189. if state_type.name == 'pending action' && state.next_state_id
  190. state = Ticket::State.lookup( id: state.next_state_id )
  191. state_type = Ticket::StateType.lookup( id: state.state_type_id )
  192. end
  193. # set all to seen if new state is pending reminder state
  194. if state_type.name == 'pending reminder'
  195. if user_id_check
  196. return false if owner_id == 1
  197. return false if updated_by_id != owner_id && user_id_check == owner_id
  198. return true
  199. end
  200. return true
  201. end
  202. # set all to seen if new state is a closed or merged state
  203. return true if state_type.name == 'closed'
  204. return true if state_type.name == 'merged'
  205. false
  206. end
  207. =begin
  208. get count of tickets and tickets which match on selector
  209. ticket_count, tickets = Ticket.selectors(params[:condition], limit, current_user)
  210. =end
  211. def self.selectors(selectors, limit = 10, current_user = nil)
  212. fail 'no selectors given' if !selectors
  213. query, bind_params, tables = selector2sql(selectors, current_user)
  214. return [] if !query
  215. if !current_user
  216. ticket_count = Ticket.where(query, *bind_params).joins(tables).count
  217. tickets = Ticket.where(query, *bind_params).joins(tables).limit(limit)
  218. return [ticket_count, tickets]
  219. end
  220. access_condition = Ticket.access_condition(current_user)
  221. ticket_count = Ticket.where(access_condition).where(query, *bind_params).joins(tables).count
  222. tickets = Ticket.where(access_condition).where(query, *bind_params).joins(tables).limit(limit)
  223. [ticket_count, tickets]
  224. end
  225. =begin
  226. generate condition query to search for tickets based on condition
  227. query_condition, bind_condition = selector2sql(params[:condition], current_user)
  228. condition example
  229. {
  230. 'ticket.state_id' => {
  231. operator: 'is',
  232. value: [1,2,5]
  233. },
  234. 'ticket.created_at' => {
  235. operator: 'after (absolute)', # after,before
  236. value: '2015-10-17T06:00:00.000Z',
  237. },
  238. 'ticket.created_at' => {
  239. operator: 'within next (relative)', # before,within,in,after
  240. range: 'day', # minute|hour|day|month|year
  241. value: '25',
  242. },
  243. 'ticket.owner_id' => {
  244. operator: 'is', # is not
  245. pre_condition: 'current_user.id',
  246. },
  247. 'ticket.owner_id' => {
  248. operator: 'is', # is not
  249. pre_condition: 'specific',
  250. value: 4711,
  251. },
  252. }
  253. =end
  254. def self.selector2sql(selectors, current_user = nil)
  255. current_user_id = UserInfo.current_user_id
  256. if current_user
  257. current_user_id = current_user.id
  258. end
  259. return if !selectors
  260. # remember query and bind params
  261. query = ''
  262. bind_params = []
  263. # get tables to join
  264. tables = ''
  265. selectors.each {|attribute, selector|
  266. selector = attribute.split(/\./)
  267. next if !selector[1]
  268. next if selector[0] == 'ticket'
  269. next if tables.include?(selector[0])
  270. if query != ''
  271. query += ' AND '
  272. end
  273. if selector[0] == 'customer'
  274. tables += ', users customers'
  275. query += 'tickets.customer_id = customers.id'
  276. elsif selector[0] == 'organization'
  277. tables += ', organizations'
  278. query += 'tickets.organization_id = organizations.id'
  279. elsif selector[0] == 'owner'
  280. tables += ', users owners'
  281. query += 'tickets.owner_id = owners.id'
  282. else
  283. fail "invalid selector #{attribute.inspect}->#{selector.inspect}"
  284. end
  285. }
  286. # add conditions
  287. selectors.each {|attribute, selector_raw|
  288. # validation
  289. fail "Invalid selector #{selector_raw.inspect}" if !selector_raw
  290. fail "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
  291. selector = selector_raw.stringify_keys
  292. fail "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
  293. # validate value / allow empty but only if pre_condition eyists
  294. if selector['value'].nil? || (selector['value'].respond_to?(:empty?) && selector['value'].empty?)
  295. return nil if selector['pre_condition'].nil? || (selector['pre_condition'].respond_to?(:empty?) && selector['pre_condition'].empty?)
  296. end
  297. # validate pre_condition values
  298. return nil if selector['pre_condition'] && selector['pre_condition'] !~ /^(set|current_user\.|specific)/
  299. # get attributes
  300. attributes = attribute.split(/\./)
  301. attribute = "#{attributes[0]}s.#{attributes[1]}"
  302. if query != ''
  303. query += ' AND '
  304. end
  305. if selector['operator'] == 'is'
  306. if selector['pre_condition'] == 'set'
  307. if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/
  308. query += "#{attribute} NOT IN (?)"
  309. bind_params.push 1
  310. else
  311. query += "#{attribute} IS NOT NULL"
  312. end
  313. elsif selector['pre_condition'] == 'current_user.id'
  314. fail "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  315. query += "#{attribute} IN (?)"
  316. bind_params.push current_user_id
  317. elsif selector['pre_condition'] == 'current_user.organization_id'
  318. fail "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  319. query += "#{attribute} IN (?)"
  320. user = User.lookup(id: current_user_id)
  321. bind_params.push user.organization_id
  322. else
  323. query += "#{attribute} IN (?)"
  324. bind_params.push selector['value']
  325. end
  326. elsif selector['operator'] == 'is not'
  327. if selector['pre_condition'] == 'set'
  328. if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/
  329. query += "#{attribute} IN (?)"
  330. bind_params.push 1
  331. else
  332. query += "#{attribute} IS NULL"
  333. end
  334. elsif selector['pre_condition'] == 'current_user.id'
  335. query += "#{attribute} NOT IN (?)"
  336. bind_params.push current_user_id
  337. elsif selector['pre_condition'] == 'current_user.organization_id'
  338. query += "#{attribute} NOT IN (?)"
  339. user = User.lookup(id: current_user_id)
  340. bind_params.push user.organization_id
  341. else
  342. query += "#{attribute} NOT IN (?)"
  343. bind_params.push selector['value']
  344. end
  345. elsif selector['operator'] == 'contains'
  346. query += "#{attribute} LIKE (?)"
  347. value = "%#{selector['value']}%"
  348. bind_params.push value
  349. elsif selector['operator'] == 'contains not'
  350. query += "#{attribute} NOT LIKE (?)"
  351. value = "%#{selector['value']}%"
  352. bind_params.push value
  353. elsif selector['operator'] == 'before (absolute)'
  354. query += "#{attribute} <= ?"
  355. bind_params.push selector['value']
  356. elsif selector['operator'] == 'after (absolute)'
  357. query += "#{attribute} >= ?"
  358. bind_params.push selector['value']
  359. elsif selector['operator'] == 'within last (relative)'
  360. query += "#{attribute} >= ?"
  361. time = nil
  362. if selector['range'] == 'minute'
  363. time = Time.zone.now - selector['value'].to_i.minutes
  364. elsif selector['range'] == 'hour'
  365. time = Time.zone.now - selector['value'].to_i.hours
  366. elsif selector['range'] == 'day'
  367. time = Time.zone.now - selector['value'].to_i.days
  368. elsif selector['range'] == 'month'
  369. time = Time.zone.now - selector['value'].to_i.months
  370. elsif selector['range'] == 'year'
  371. time = Time.zone.now - selector['value'].to_i.years
  372. else
  373. fail "Unknown selector attributes '#{selector.inspect}'"
  374. end
  375. bind_params.push time
  376. elsif selector['operator'] == 'within next (relative)'
  377. query += "#{attribute} <= ?"
  378. time = nil
  379. if selector['range'] == 'minute'
  380. time = Time.zone.now + selector['value'].to_i.minutes
  381. elsif selector['range'] == 'hour'
  382. time = Time.zone.now + selector['value'].to_i.hours
  383. elsif selector['range'] == 'day'
  384. time = Time.zone.now + selector['value'].to_i.days
  385. elsif selector['range'] == 'month'
  386. time = Time.zone.now + selector['value'].to_i.months
  387. elsif selector['range'] == 'year'
  388. time = Time.zone.now + selector['value'].to_i.years
  389. else
  390. fail "Unknown selector attributes '#{selector.inspect}'"
  391. end
  392. bind_params.push time
  393. elsif selector['operator'] == 'before (relative)'
  394. query += "#{attribute} <= ?"
  395. time = nil
  396. if selector['range'] == 'minute'
  397. time = Time.zone.now - selector['value'].to_i.minutes
  398. elsif selector['range'] == 'hour'
  399. time = Time.zone.now - selector['value'].to_i.hours
  400. elsif selector['range'] == 'day'
  401. time = Time.zone.now - selector['value'].to_i.days
  402. elsif selector['range'] == 'month'
  403. time = Time.zone.now - selector['value'].to_i.months
  404. elsif selector['range'] == 'year'
  405. time = Time.zone.now - selector['value'].to_i.years
  406. else
  407. fail "Unknown selector attributes '#{selector.inspect}'"
  408. end
  409. bind_params.push time
  410. elsif selector['operator'] == 'after (relative)'
  411. query += "#{attribute} >= ?"
  412. time = nil
  413. if selector['range'] == 'minute'
  414. time = Time.zone.now + selector['value'].to_i.minutes
  415. elsif selector['range'] == 'hour'
  416. time = Time.zone.now + selector['value'].to_i.hours
  417. elsif selector['range'] == 'day'
  418. time = Time.zone.now + selector['value'].to_i.days
  419. elsif selector['range'] == 'month'
  420. time = Time.zone.now + selector['value'].to_i.months
  421. elsif selector['range'] == 'year'
  422. time = Time.zone.now + selector['value'].to_i.years
  423. else
  424. fail "Unknown selector attributes '#{selector.inspect}'"
  425. end
  426. bind_params.push time
  427. else
  428. fail "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
  429. end
  430. }
  431. [query, bind_params, tables]
  432. end
  433. private
  434. def check_generate
  435. return if number
  436. self.number = Ticket::Number.generate
  437. end
  438. def check_title
  439. return if !title
  440. title.gsub!(/\s|\t|\r/, ' ')
  441. end
  442. def check_defaults
  443. if !owner_id
  444. self.owner_id = 1
  445. end
  446. return if !customer_id
  447. customer = User.find( customer_id )
  448. return if organization_id == customer.organization_id
  449. self.organization_id = customer.organization_id
  450. end
  451. def reset_pending_time
  452. # ignore if no state has changed
  453. return if !changes['state_id']
  454. # check if new state isn't pending*
  455. current_state = Ticket::State.lookup( id: state_id )
  456. current_state_type = Ticket::StateType.lookup( id: current_state.state_type_id )
  457. # in case, set pending_time to nil
  458. return if current_state_type.name =~ /^pending/i
  459. self.pending_time = nil
  460. end
  461. def destroy_dependencies
  462. # delete articles
  463. articles.destroy_all
  464. # destroy online notifications
  465. OnlineNotification.remove( self.class.to_s, id )
  466. end
  467. end