ticket.rb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  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. update_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)
  71. .users.where(active: true)
  72. .joins(:roles)
  73. .where('roles.name' => Z_ROLENAME_AGENT, 'roles.active' => true)
  74. .order('users.login')
  75. .uniq()
  76. end
  77. =begin
  78. get user access conditions
  79. conditions = Ticket.access_condition( User.find(1) )
  80. returns
  81. result = [user1, user2, ...]
  82. =end
  83. def self.access_condition(user)
  84. access_condition = []
  85. if user.role?(Z_ROLENAME_AGENT)
  86. group_ids = Group.select( 'groups.id' ).joins(:users)
  87. .where( 'groups_users.user_id = ?', user.id )
  88. .where( 'groups.active = ?', true )
  89. .map( &:id )
  90. access_condition = [ 'group_id IN (?)', group_ids ]
  91. else
  92. access_condition = if !user.organization || ( !user.organization.shared || user.organization.shared == false )
  93. [ 'tickets.customer_id = ?', user.id ]
  94. else
  95. [ '( tickets.customer_id = ? OR tickets.organization_id = ? )', user.id, user.organization.id ]
  96. end
  97. end
  98. access_condition
  99. end
  100. =begin
  101. processes tickets which have reached their pending time and sets next state_id
  102. processed_tickets = Ticket.process_pending
  103. returns
  104. processed_tickets = [<Ticket>, ...]
  105. =end
  106. def self.process_pending
  107. result = []
  108. # process pending action tickets
  109. pending_action = Ticket::StateType.find_by(name: 'pending action')
  110. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  111. .where.not(next_state_id: nil)
  112. if !ticket_states_pending_action.empty?
  113. next_state_map = {}
  114. ticket_states_pending_action.each { |state|
  115. next_state_map[state.id] = state.next_state_id
  116. }
  117. tickets = where(state_id: next_state_map.keys)
  118. .where('pending_time <= ?', Time.zone.now)
  119. tickets.each { |ticket|
  120. ticket.state_id = next_state_map[ticket.state_id]
  121. ticket.updated_at = Time.zone.now
  122. ticket.updated_by_id = 1
  123. ticket.save!
  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.push ticket
  128. }
  129. end
  130. # process pending reminder tickets
  131. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  132. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  133. if !ticket_states_pending_reminder.empty?
  134. reminder_state_map = {}
  135. ticket_states_pending_reminder.each { |state|
  136. reminder_state_map[state.id] = state.next_state_id
  137. }
  138. tickets = where(state_id: reminder_state_map.keys)
  139. .where('pending_time <= ?', Time.zone.now)
  140. tickets.each { |ticket|
  141. # send notification
  142. bg = Observer::Ticket::Notification::BackgroundJob.new(
  143. ticket_id: ticket.id,
  144. article_id: ticket.articles.last.id,
  145. type: 'reminder_reached',
  146. )
  147. bg.perform
  148. result.push ticket
  149. }
  150. end
  151. result
  152. end
  153. =begin
  154. processes escalated tickets
  155. processed_tickets = Ticket.process_escalation
  156. returns
  157. processed_tickets = [<Ticket>, ...]
  158. =end
  159. def self.process_escalation
  160. result = []
  161. # get max warning diff
  162. tickets = where('escalation_time <= ?', Time.zone.now + 15.minutes)
  163. tickets.each {|ticket|
  164. # get sla
  165. sla = ticket.escalation_calculation_get_sla
  166. # send escalation
  167. if ticket.escalation_time < Time.zone.now
  168. bg = Observer::Ticket::Notification::BackgroundJob.new(
  169. ticket_id: ticket.id,
  170. article_id: ticket.articles.last.id,
  171. type: 'escalation',
  172. )
  173. bg.perform
  174. result.push ticket
  175. next
  176. end
  177. # check if warning need to be sent
  178. bg = Observer::Ticket::Notification::BackgroundJob.new(
  179. ticket_id: ticket.id,
  180. article_id: ticket.articles.last.id,
  181. type: 'escalation_warning',
  182. )
  183. bg.perform
  184. result.push ticket
  185. }
  186. result
  187. end
  188. =begin
  189. merge tickets
  190. ticket = Ticket.find(123)
  191. result = ticket.merge_to(
  192. ticket_id: 123,
  193. user_id: 123,
  194. )
  195. returns
  196. result = true|false
  197. =end
  198. def merge_to(data)
  199. # update articles
  200. Ticket::Article.where( ticket_id: id ).each(&:touch)
  201. # quiet update of reassign of articles
  202. Ticket::Article.where( ticket_id: id ).update_all( ['ticket_id = ?', data[:ticket_id] ] )
  203. # touch new ticket (to broadcast change)
  204. Ticket.find( data[:ticket_id] ).touch
  205. # update history
  206. # create new merge article
  207. Ticket::Article.create(
  208. ticket_id: id,
  209. type_id: Ticket::Article::Type.lookup( name: 'note' ).id,
  210. sender_id: Ticket::Article::Sender.lookup( name: Z_ROLENAME_AGENT ).id,
  211. body: 'merged',
  212. internal: false,
  213. created_by_id: data[:user_id],
  214. updated_by_id: data[:user_id],
  215. )
  216. # add history to both
  217. # link tickets
  218. Link.add(
  219. link_type: 'parent',
  220. link_object_source: 'Ticket',
  221. link_object_source_value: data[:ticket_id],
  222. link_object_target: 'Ticket',
  223. link_object_target_value: id
  224. )
  225. # set state to 'merged'
  226. self.state_id = Ticket::State.lookup( name: 'merged' ).id
  227. # rest owner
  228. self.owner_id = User.find_by( login: '-' ).id
  229. # save ticket
  230. save
  231. end
  232. =begin
  233. check if online notifcation should be shown in general as already seen with current state
  234. ticket = Ticket.find(1)
  235. seen = ticket.online_notification_seen_state(user_id_check)
  236. returns
  237. result = true # or false
  238. check if online notifcation should be shown for this user as already seen with current state
  239. ticket = Ticket.find(1)
  240. seen = ticket.online_notification_seen_state(check_user_id)
  241. returns
  242. result = true # or false
  243. =end
  244. def online_notification_seen_state(user_id_check = nil)
  245. state = Ticket::State.lookup(id: state_id)
  246. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  247. # always to set unseen for ticket owner
  248. if state_type.name != 'merged'
  249. if user_id_check
  250. return false if user_id_check == owner_id && user_id_check != updated_by_id
  251. end
  252. end
  253. # set all to seen if pending action state is a closed or merged state
  254. if state_type.name == 'pending action' && state.next_state_id
  255. state = Ticket::State.lookup(id: state.next_state_id)
  256. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  257. end
  258. # set all to seen if new state is pending reminder state
  259. if state_type.name == 'pending reminder'
  260. if user_id_check
  261. return false if owner_id == 1
  262. return false if updated_by_id != owner_id && user_id_check == owner_id
  263. return true
  264. end
  265. return true
  266. end
  267. # set all to seen if new state is a closed or merged state
  268. return true if state_type.name == 'closed'
  269. return true if state_type.name == 'merged'
  270. false
  271. end
  272. =begin
  273. get count of tickets and tickets which match on selector
  274. ticket_count, tickets = Ticket.selectors(params[:condition], limit, current_user)
  275. =end
  276. def self.selectors(selectors, limit = 10, current_user = nil)
  277. raise 'no selectors given' if !selectors
  278. query, bind_params, tables = selector2sql(selectors, current_user)
  279. return [] if !query
  280. if !current_user
  281. ticket_count = Ticket.where(query, *bind_params).joins(tables).count
  282. tickets = Ticket.where(query, *bind_params).joins(tables).limit(limit)
  283. return [ticket_count, tickets]
  284. end
  285. access_condition = Ticket.access_condition(current_user)
  286. ticket_count = Ticket.where(access_condition).where(query, *bind_params).joins(tables).count
  287. tickets = Ticket.where(access_condition).where(query, *bind_params).joins(tables).limit(limit)
  288. [ticket_count, tickets]
  289. end
  290. =begin
  291. generate condition query to search for tickets based on condition
  292. query_condition, bind_condition = selector2sql(params[:condition], current_user)
  293. condition example
  294. {
  295. 'ticket.state_id' => {
  296. operator: 'is',
  297. value: [1,2,5]
  298. },
  299. 'ticket.created_at' => {
  300. operator: 'after (absolute)', # after,before
  301. value: '2015-10-17T06:00:00.000Z',
  302. },
  303. 'ticket.created_at' => {
  304. operator: 'within next (relative)', # before,within,in,after
  305. range: 'day', # minute|hour|day|month|year
  306. value: '25',
  307. },
  308. 'ticket.owner_id' => {
  309. operator: 'is', # is not
  310. pre_condition: 'current_user.id',
  311. },
  312. 'ticket.owner_id' => {
  313. operator: 'is', # is not
  314. pre_condition: 'specific',
  315. value: 4711,
  316. },
  317. 'ticket.escalation_time' => {
  318. operator: 'is not', # not
  319. value: nil,
  320. }
  321. }
  322. =end
  323. def self.selector2sql(selectors, current_user = nil)
  324. current_user_id = UserInfo.current_user_id
  325. if current_user
  326. current_user_id = current_user.id
  327. end
  328. return if !selectors
  329. # remember query and bind params
  330. query = ''
  331. bind_params = []
  332. like = Rails.application.config.db_like
  333. # get tables to join
  334. tables = ''
  335. selectors.each {|attribute, selector|
  336. selector = attribute.split(/\./)
  337. next if !selector[1]
  338. next if selector[0] == 'ticket'
  339. next if tables.include?(selector[0])
  340. if query != ''
  341. query += ' AND '
  342. end
  343. if selector[0] == 'customer'
  344. tables += ', users customers'
  345. query += 'tickets.customer_id = customers.id'
  346. elsif selector[0] == 'organization'
  347. tables += ', organizations'
  348. query += 'tickets.organization_id = organizations.id'
  349. elsif selector[0] == 'owner'
  350. tables += ', users owners'
  351. query += 'tickets.owner_id = owners.id'
  352. else
  353. raise "invalid selector #{attribute.inspect}->#{selector.inspect}"
  354. end
  355. }
  356. # add conditions
  357. selectors.each {|attribute, selector_raw|
  358. # validation
  359. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
  360. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
  361. selector = selector_raw.stringify_keys
  362. raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
  363. # validate value / allow empty but only if pre_condition exists
  364. if (selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:empty?) && selector['value'].empty?)
  365. return nil if selector['pre_condition'].nil? || (selector['pre_condition'].respond_to?(:empty?) && selector['pre_condition'].empty?)
  366. end
  367. # validate pre_condition values
  368. return nil if selector['pre_condition'] && selector['pre_condition'] !~ /^(set|current_user\.|specific)/
  369. # get attributes
  370. attributes = attribute.split(/\./)
  371. attribute = "#{attributes[0]}s.#{attributes[1]}"
  372. if query != ''
  373. query += ' AND '
  374. end
  375. if selector['operator'] == 'is'
  376. if selector['pre_condition'] == 'set'
  377. if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/
  378. query += "#{attribute} NOT IN (?)"
  379. bind_params.push 1
  380. else
  381. query += "#{attribute} IS NOT NULL"
  382. end
  383. elsif selector['pre_condition'] == 'current_user.id'
  384. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  385. query += "#{attribute} IN (?)"
  386. bind_params.push current_user_id
  387. elsif selector['pre_condition'] == 'current_user.organization_id'
  388. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  389. query += "#{attribute} IN (?)"
  390. user = User.lookup(id: current_user_id)
  391. bind_params.push user.organization_id
  392. else
  393. # rubocop:disable Style/IfInsideElse
  394. if selector['value'].nil?
  395. query += "#{attribute} IS NOT NULL"
  396. else
  397. query += "#{attribute} IN (?)"
  398. bind_params.push selector['value']
  399. end
  400. # rubocop:enable Style/IfInsideElse
  401. end
  402. elsif selector['operator'] == 'is not'
  403. if selector['pre_condition'] == 'set'
  404. if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/
  405. query += "#{attribute} IN (?)"
  406. bind_params.push 1
  407. else
  408. query += "#{attribute} IS NULL"
  409. end
  410. elsif selector['pre_condition'] == 'current_user.id'
  411. query += "#{attribute} NOT IN (?)"
  412. bind_params.push current_user_id
  413. elsif selector['pre_condition'] == 'current_user.organization_id'
  414. query += "#{attribute} NOT IN (?)"
  415. user = User.lookup(id: current_user_id)
  416. bind_params.push user.organization_id
  417. else
  418. # rubocop:disable Style/IfInsideElse
  419. if selector['value'].nil?
  420. query += "#{attribute} IS NOT NULL"
  421. else
  422. query += "#{attribute} NOT IN (?)"
  423. bind_params.push selector['value']
  424. end
  425. # rubocop:enable Style/IfInsideElse
  426. end
  427. elsif selector['operator'] == 'contains'
  428. query += "#{attribute} #{like} (?)"
  429. value = "%#{selector['value']}%"
  430. bind_params.push value
  431. elsif selector['operator'] == 'contains not'
  432. query += "#{attribute} NOT #{like} (?)"
  433. value = "%#{selector['value']}%"
  434. bind_params.push value
  435. elsif selector['operator'] == 'before (absolute)'
  436. query += "#{attribute} <= ?"
  437. bind_params.push selector['value']
  438. elsif selector['operator'] == 'after (absolute)'
  439. query += "#{attribute} >= ?"
  440. bind_params.push selector['value']
  441. elsif selector['operator'] == 'within last (relative)'
  442. query += "#{attribute} >= ?"
  443. time = nil
  444. if selector['range'] == 'minute'
  445. time = Time.zone.now - selector['value'].to_i.minutes
  446. elsif selector['range'] == 'hour'
  447. time = Time.zone.now - selector['value'].to_i.hours
  448. elsif selector['range'] == 'day'
  449. time = Time.zone.now - selector['value'].to_i.days
  450. elsif selector['range'] == 'month'
  451. time = Time.zone.now - selector['value'].to_i.months
  452. elsif selector['range'] == 'year'
  453. time = Time.zone.now - selector['value'].to_i.years
  454. else
  455. raise "Unknown selector attributes '#{selector.inspect}'"
  456. end
  457. bind_params.push time
  458. elsif selector['operator'] == 'within next (relative)'
  459. query += "#{attribute} <= ?"
  460. time = nil
  461. if selector['range'] == 'minute'
  462. time = Time.zone.now + selector['value'].to_i.minutes
  463. elsif selector['range'] == 'hour'
  464. time = Time.zone.now + selector['value'].to_i.hours
  465. elsif selector['range'] == 'day'
  466. time = Time.zone.now + selector['value'].to_i.days
  467. elsif selector['range'] == 'month'
  468. time = Time.zone.now + selector['value'].to_i.months
  469. elsif selector['range'] == 'year'
  470. time = Time.zone.now + selector['value'].to_i.years
  471. else
  472. raise "Unknown selector attributes '#{selector.inspect}'"
  473. end
  474. bind_params.push time
  475. elsif selector['operator'] == 'before (relative)'
  476. query += "#{attribute} <= ?"
  477. time = nil
  478. if selector['range'] == 'minute'
  479. time = Time.zone.now - selector['value'].to_i.minutes
  480. elsif selector['range'] == 'hour'
  481. time = Time.zone.now - selector['value'].to_i.hours
  482. elsif selector['range'] == 'day'
  483. time = Time.zone.now - selector['value'].to_i.days
  484. elsif selector['range'] == 'month'
  485. time = Time.zone.now - selector['value'].to_i.months
  486. elsif selector['range'] == 'year'
  487. time = Time.zone.now - selector['value'].to_i.years
  488. else
  489. raise "Unknown selector attributes '#{selector.inspect}'"
  490. end
  491. bind_params.push time
  492. elsif selector['operator'] == 'after (relative)'
  493. query += "#{attribute} >= ?"
  494. time = nil
  495. if selector['range'] == 'minute'
  496. time = Time.zone.now + selector['value'].to_i.minutes
  497. elsif selector['range'] == 'hour'
  498. time = Time.zone.now + selector['value'].to_i.hours
  499. elsif selector['range'] == 'day'
  500. time = Time.zone.now + selector['value'].to_i.days
  501. elsif selector['range'] == 'month'
  502. time = Time.zone.now + selector['value'].to_i.months
  503. elsif selector['range'] == 'year'
  504. time = Time.zone.now + selector['value'].to_i.years
  505. else
  506. raise "Unknown selector attributes '#{selector.inspect}'"
  507. end
  508. bind_params.push time
  509. else
  510. raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
  511. end
  512. }
  513. [query, bind_params, tables]
  514. end
  515. =begin
  516. get all email references headers of a ticket, to exclude some, parse it as array into method
  517. references = ticket.get_references
  518. result
  519. ['message-id-1234', 'message-id-5678']
  520. ignore references header(s)
  521. references = ticket.get_references(['message-id-5678'])
  522. result
  523. ['message-id-1234']
  524. =end
  525. def get_references(ignore = [])
  526. references = []
  527. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each {|article|
  528. if !article.in_reply_to.empty?
  529. references.push article.in_reply_to
  530. end
  531. next if !article.message_id
  532. next if article.message_id.empty?
  533. references.push article.message_id
  534. }
  535. ignore.each {|item|
  536. references.delete(item)
  537. }
  538. references
  539. end
  540. private
  541. def check_generate
  542. return if number
  543. self.number = Ticket::Number.generate
  544. end
  545. def check_title
  546. return if !title
  547. title.gsub!(/\s|\t|\r/, ' ')
  548. end
  549. def check_defaults
  550. if !owner_id
  551. self.owner_id = 1
  552. end
  553. return if !customer_id
  554. customer = User.find( customer_id )
  555. return if organization_id == customer.organization_id
  556. self.organization_id = customer.organization_id
  557. end
  558. def reset_pending_time
  559. # ignore if no state has changed
  560. return if !changes['state_id']
  561. # check if new state isn't pending*
  562. current_state = Ticket::State.lookup( id: state_id )
  563. current_state_type = Ticket::StateType.lookup( id: current_state.state_type_id )
  564. # in case, set pending_time to nil
  565. return if current_state_type.name =~ /^pending/i
  566. self.pending_time = nil
  567. end
  568. def destroy_dependencies
  569. # delete articles
  570. articles.destroy_all
  571. # destroy online notifications
  572. OnlineNotification.remove( self.class.to_s, id )
  573. end
  574. end