ticket.rb 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. class Ticket < ApplicationModel
  3. include HasActivityStreamLog
  4. include ChecksClientNotification
  5. include ChecksLatestChangeObserved
  6. include HasHistory
  7. include HasTags
  8. include HasSearchIndexBackend
  9. include HasOnlineNotifications
  10. include HasKarmaActivityLog
  11. include HasLinks
  12. include Ticket::ChecksAccess
  13. include Ticket::Escalation
  14. include Ticket::Subject
  15. load 'ticket/assets.rb'
  16. include Ticket::Assets
  17. load 'ticket/search_index.rb'
  18. include Ticket::SearchIndex
  19. extend Ticket::Search
  20. store :preferences
  21. before_create :check_generate, :check_defaults, :check_title, :set_default_state, :set_default_priority
  22. after_create :check_escalation_update
  23. before_update :check_defaults, :check_title, :reset_pending_time
  24. after_update :check_escalation_update
  25. validates :group_id, presence: true
  26. activity_stream_permission 'ticket.agent'
  27. activity_stream_attributes_ignored :organization_id, # organization_id will channge automatically on user update
  28. :create_article_type_id,
  29. :create_article_sender_id,
  30. :article_count,
  31. :first_response_at,
  32. :first_response_escalation_at,
  33. :first_response_in_min,
  34. :first_response_diff_in_min,
  35. :close_at,
  36. :close_escalation_at,
  37. :close_in_min,
  38. :close_diff_in_min,
  39. :update_escalation_at,
  40. :update_in_min,
  41. :update_diff_in_min,
  42. :last_contact_at,
  43. :last_contact_agent_at,
  44. :last_contact_customer_at,
  45. :last_owner_update_at,
  46. :preferences
  47. history_attributes_ignored :create_article_type_id,
  48. :create_article_sender_id,
  49. :article_count,
  50. :preferences
  51. belongs_to :group, class_name: 'Group'
  52. has_many :articles, class_name: 'Ticket::Article', after_add: :cache_update, after_remove: :cache_update, dependent: :destroy
  53. has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy
  54. belongs_to :organization, class_name: 'Organization'
  55. belongs_to :state, class_name: 'Ticket::State'
  56. belongs_to :priority, class_name: 'Ticket::Priority'
  57. belongs_to :owner, class_name: 'User'
  58. belongs_to :customer, class_name: 'User'
  59. belongs_to :created_by, class_name: 'User'
  60. belongs_to :updated_by, class_name: 'User'
  61. belongs_to :create_article_type, class_name: 'Ticket::Article::Type'
  62. belongs_to :create_article_sender, class_name: 'Ticket::Article::Sender'
  63. self.inheritance_column = nil
  64. attr_accessor :callback_loop
  65. =begin
  66. get user access conditions
  67. conditions = Ticket.access_condition( User.find(1) , 'full')
  68. returns
  69. result = [user1, user2, ...]
  70. =end
  71. def self.access_condition(user, access)
  72. if user.permissions?('ticket.agent')
  73. ['group_id IN (?)', user.group_ids_access(access)]
  74. elsif !user.organization || ( !user.organization.shared || user.organization.shared == false )
  75. ['tickets.customer_id = ?', user.id]
  76. else
  77. ['(tickets.customer_id = ? OR tickets.organization_id = ?)', user.id, user.organization.id]
  78. end
  79. end
  80. =begin
  81. processes tickets which have reached their pending time and sets next state_id
  82. processed_tickets = Ticket.process_pending
  83. returns
  84. processed_tickets = [<Ticket>, ...]
  85. =end
  86. def self.process_pending
  87. result = []
  88. # process pending action tickets
  89. pending_action = Ticket::StateType.find_by(name: 'pending action')
  90. ticket_states_pending_action = Ticket::State.where(state_type_id: pending_action)
  91. .where.not(next_state_id: nil)
  92. if ticket_states_pending_action.present?
  93. next_state_map = {}
  94. ticket_states_pending_action.each { |state|
  95. next_state_map[state.id] = state.next_state_id
  96. }
  97. tickets = where(state_id: next_state_map.keys)
  98. .where('pending_time <= ?', Time.zone.now)
  99. tickets.each { |ticket|
  100. Transaction.execute do
  101. ticket.state_id = next_state_map[ticket.state_id]
  102. ticket.updated_at = Time.zone.now
  103. ticket.updated_by_id = 1
  104. ticket.save!
  105. end
  106. result.push ticket
  107. }
  108. end
  109. # process pending reminder tickets
  110. pending_reminder = Ticket::StateType.find_by(name: 'pending reminder')
  111. ticket_states_pending_reminder = Ticket::State.where(state_type_id: pending_reminder)
  112. if ticket_states_pending_reminder.present?
  113. reminder_state_map = {}
  114. ticket_states_pending_reminder.each { |state|
  115. reminder_state_map[state.id] = state.next_state_id
  116. }
  117. tickets = where(state_id: reminder_state_map.keys)
  118. .where('pending_time <= ?', Time.zone.now)
  119. tickets.each { |ticket|
  120. article_id = nil
  121. article = Ticket::Article.last_customer_agent_article(ticket.id)
  122. if article
  123. article_id = article.id
  124. end
  125. # send notification
  126. Transaction::BackgroundJob.run(
  127. object: 'Ticket',
  128. type: 'reminder_reached',
  129. object_id: ticket.id,
  130. article_id: article_id,
  131. user_id: 1,
  132. )
  133. result.push ticket
  134. }
  135. end
  136. result
  137. end
  138. =begin
  139. processes escalated tickets
  140. processed_tickets = Ticket.process_escalation
  141. returns
  142. processed_tickets = [<Ticket>, ...]
  143. =end
  144. def self.process_escalation
  145. result = []
  146. # get max warning diff
  147. tickets = where('escalation_at <= ?', Time.zone.now + 15.minutes)
  148. tickets.each { |ticket|
  149. # get sla
  150. sla = ticket.escalation_calculation_get_sla
  151. article_id = nil
  152. article = Ticket::Article.last_customer_agent_article(ticket.id)
  153. if article
  154. article_id = article.id
  155. end
  156. # send escalation
  157. if ticket.escalation_at < Time.zone.now
  158. Transaction::BackgroundJob.run(
  159. object: 'Ticket',
  160. type: 'escalation',
  161. object_id: ticket.id,
  162. article_id: article_id,
  163. user_id: 1,
  164. )
  165. result.push ticket
  166. next
  167. end
  168. # check if warning need to be sent
  169. Transaction::BackgroundJob.run(
  170. object: 'Ticket',
  171. type: 'escalation_warning',
  172. object_id: ticket.id,
  173. article_id: article_id,
  174. user_id: 1,
  175. )
  176. result.push ticket
  177. }
  178. result
  179. end
  180. =begin
  181. processes tickets which auto unassign time has reached
  182. processed_tickets = Ticket.process_auto_unassign
  183. returns
  184. processed_tickets = [<Ticket>, ...]
  185. =end
  186. def self.process_auto_unassign
  187. # process pending action tickets
  188. state_ids = Ticket::State.by_category(:work_on).pluck(:id)
  189. return [] if state_ids.blank?
  190. result = []
  191. groups = Group.where(active: true).where('assignment_timeout IS NOT NULL AND groups.assignment_timeout != 0')
  192. return [] if groups.blank?
  193. groups.each { |group|
  194. next if group.assignment_timeout.blank?
  195. ticket_ids = Ticket.where('state_id IN (?) AND owner_id != 1 AND group_id = ?', state_ids, group.id).limit(600).pluck(:id)
  196. ticket_ids.each { |ticket_id|
  197. ticket = Ticket.find_by(id: ticket_id)
  198. next if !ticket
  199. minutes_since_last_assignment = Time.zone.now - ticket.last_owner_update_at
  200. next if (minutes_since_last_assignment / 60) <= group.assignment_timeout
  201. Transaction.execute do
  202. ticket.owner_id = 1
  203. ticket.updated_at = Time.zone.now
  204. ticket.updated_by_id = 1
  205. ticket.save!
  206. end
  207. result.push ticket
  208. }
  209. }
  210. result
  211. end
  212. =begin
  213. merge tickets
  214. ticket = Ticket.find(123)
  215. result = ticket.merge_to(
  216. ticket_id: 123,
  217. user_id: 123,
  218. )
  219. returns
  220. result = true|false
  221. =end
  222. def merge_to(data)
  223. # prevent cross merging tickets
  224. target_ticket = Ticket.find_by(id: data[:ticket_id])
  225. raise 'no target ticket given' if !target_ticket
  226. raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged'
  227. # check different ticket ids
  228. raise Exceptions::UnprocessableEntity, 'Can\'t merge ticket with it self!' if id == target_ticket.id
  229. # update articles
  230. Transaction.execute do
  231. Ticket::Article.where(ticket_id: id).each(&:touch)
  232. # quiet update of reassign of articles
  233. Ticket::Article.where(ticket_id: id).update_all(['ticket_id = ?', data[:ticket_id]])
  234. # update history
  235. # create new merge article
  236. Ticket::Article.create(
  237. ticket_id: id,
  238. type_id: Ticket::Article::Type.lookup(name: 'note').id,
  239. sender_id: Ticket::Article::Sender.lookup(name: 'Agent').id,
  240. body: 'merged',
  241. internal: false,
  242. created_by_id: data[:user_id],
  243. updated_by_id: data[:user_id],
  244. )
  245. # add history to both
  246. # reassign links to the new ticket
  247. Link.where(
  248. link_object_source_id: Link::Object.find_by(name: 'Ticket').id,
  249. link_object_source_value: id,
  250. ).update_all(link_object_source_value: data[:ticket_id])
  251. Link.where(
  252. link_object_target_id: Link::Object.find_by(name: 'Ticket').id,
  253. link_object_target_value: id,
  254. ).update_all(link_object_target_value: data[:ticket_id])
  255. # link tickets
  256. Link.add(
  257. link_type: 'parent',
  258. link_object_source: 'Ticket',
  259. link_object_source_value: data[:ticket_id],
  260. link_object_target: 'Ticket',
  261. link_object_target_value: id
  262. )
  263. # set state to 'merged'
  264. self.state_id = Ticket::State.lookup(name: 'merged').id
  265. # rest owner
  266. self.owner_id = 1
  267. # save ticket
  268. save!
  269. # touch new ticket (to broadcast change)
  270. target_ticket.touch
  271. end
  272. true
  273. end
  274. =begin
  275. check if online notifcation should be shown in general as already seen with current state
  276. ticket = Ticket.find(1)
  277. seen = ticket.online_notification_seen_state(user_id_check)
  278. returns
  279. result = true # or false
  280. check if online notifcation should be shown for this user as already seen with current state
  281. ticket = Ticket.find(1)
  282. seen = ticket.online_notification_seen_state(check_user_id)
  283. returns
  284. result = true # or false
  285. =end
  286. def online_notification_seen_state(user_id_check = nil)
  287. state = Ticket::State.lookup(id: state_id)
  288. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  289. # always to set unseen for ticket owner and users which did not the update
  290. if state_type.name != 'merged'
  291. if user_id_check
  292. return false if user_id_check == owner_id && user_id_check != updated_by_id
  293. end
  294. end
  295. # set all to seen if pending action state is a closed or merged state
  296. if state_type.name == 'pending action' && state.next_state_id
  297. state = Ticket::State.lookup(id: state.next_state_id)
  298. state_type = Ticket::StateType.lookup(id: state.state_type_id)
  299. end
  300. # set all to seen if new state is pending reminder state
  301. if state_type.name == 'pending reminder'
  302. if user_id_check
  303. return false if owner_id == 1
  304. return false if updated_by_id != owner_id && user_id_check == owner_id
  305. return true
  306. end
  307. return true
  308. end
  309. # set all to seen if new state is a closed or merged state
  310. return true if state_type.name == 'closed'
  311. return true if state_type.name == 'merged'
  312. false
  313. end
  314. =begin
  315. get count of tickets and tickets which match on selector
  316. ticket_count, tickets = Ticket.selectors(params[:condition], limit, current_user, 'full')
  317. =end
  318. def self.selectors(selectors, limit = 10, current_user = nil, access = 'full')
  319. raise 'no selectors given' if !selectors
  320. query, bind_params, tables = selector2sql(selectors, current_user)
  321. return [] if !query
  322. ActiveRecord::Base.transaction(requires_new: true) do
  323. begin
  324. if !current_user
  325. ticket_count = Ticket.where(query, *bind_params).joins(tables).count
  326. tickets = Ticket.where(query, *bind_params).joins(tables).limit(limit)
  327. return [ticket_count, tickets]
  328. end
  329. access_condition = Ticket.access_condition(current_user, access)
  330. ticket_count = Ticket.where(access_condition).where(query, *bind_params).joins(tables).count
  331. tickets = Ticket.where(access_condition).where(query, *bind_params).joins(tables).limit(limit)
  332. return [ticket_count, tickets]
  333. rescue ActiveRecord::StatementInvalid => e
  334. Rails.logger.error e.inspect
  335. Rails.logger.error e.backtrace
  336. raise ActiveRecord::Rollback
  337. end
  338. end
  339. []
  340. end
  341. =begin
  342. generate condition query to search for tickets based on condition
  343. query_condition, bind_condition, tables = selector2sql(params[:condition], current_user)
  344. condition example
  345. {
  346. 'ticket.title' => {
  347. operator: 'contains', # contains not
  348. value: 'some value',
  349. },
  350. 'ticket.state_id' => {
  351. operator: 'is',
  352. value: [1,2,5]
  353. },
  354. 'ticket.created_at' => {
  355. operator: 'after (absolute)', # after,before
  356. value: '2015-10-17T06:00:00.000Z',
  357. },
  358. 'ticket.created_at' => {
  359. operator: 'within next (relative)', # before,within,in,after
  360. range: 'day', # minute|hour|day|month|year
  361. value: '25',
  362. },
  363. 'ticket.owner_id' => {
  364. operator: 'is', # is not
  365. pre_condition: 'current_user.id',
  366. },
  367. 'ticket.owner_id' => {
  368. operator: 'is', # is not
  369. pre_condition: 'specific',
  370. value: 4711,
  371. },
  372. 'ticket.escalation_at' => {
  373. operator: 'is not', # not
  374. value: nil,
  375. },
  376. 'ticket.tags' => {
  377. operator: 'contains all', # contains all|contains one|contains all not|contains one not
  378. value: 'tag1, tag2',
  379. },
  380. }
  381. =end
  382. def self.selector2sql(selectors, current_user = nil)
  383. current_user_id = UserInfo.current_user_id
  384. if current_user
  385. current_user_id = current_user.id
  386. end
  387. return if !selectors
  388. # remember query and bind params
  389. query = ''
  390. bind_params = []
  391. like = Rails.application.config.db_like
  392. # get tables to join
  393. tables = ''
  394. selectors.each { |attribute, selector|
  395. selector = attribute.split(/\./)
  396. next if !selector[1]
  397. next if selector[0] == 'ticket'
  398. next if tables.include?(selector[0])
  399. if query != ''
  400. query += ' AND '
  401. end
  402. if selector[0] == 'customer'
  403. tables += ', users customers'
  404. query += 'tickets.customer_id = customers.id'
  405. elsif selector[0] == 'organization'
  406. tables += ', organizations'
  407. query += 'tickets.organization_id = organizations.id'
  408. elsif selector[0] == 'owner'
  409. tables += ', users owners'
  410. query += 'tickets.owner_id = owners.id'
  411. elsif selector[0] == 'article'
  412. tables += ', ticket_articles articles'
  413. query += 'tickets.id = articles.ticket_id'
  414. else
  415. raise "invalid selector #{attribute.inspect}->#{selector.inspect}"
  416. end
  417. }
  418. # add conditions
  419. selectors.each { |attribute, selector_raw|
  420. # validation
  421. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw
  422. raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?)
  423. selector = selector_raw.stringify_keys
  424. raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator']
  425. # validate value / allow empty but only if pre_condition exists and is not specific
  426. if !selector.key?('value') || ((selector['value'].class == String || selector['value'].class == Array) && (selector['value'].respond_to?(:empty?) && selector['value'].empty?))
  427. return nil if selector['pre_condition'].nil?
  428. return nil if selector['pre_condition'].respond_to?(:empty?) && selector['pre_condition'].empty?
  429. return nil if selector['pre_condition'] == 'specific'
  430. end
  431. # validate pre_condition values
  432. return nil if selector['pre_condition'] && selector['pre_condition'] !~ /^(not_set|current_user\.|specific)/
  433. # get attributes
  434. attributes = attribute.split(/\./)
  435. attribute = "#{attributes[0]}s.#{attributes[1]}"
  436. if attributes[0] == 'ticket' && attributes[1] == 'tags'
  437. selector['value'] = selector['value'].split(/,/).collect(&:strip)
  438. end
  439. if query != ''
  440. query += ' AND '
  441. end
  442. if selector['operator'] == 'is'
  443. if selector['pre_condition'] == 'not_set'
  444. if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/
  445. query += "#{attribute} IN (?)"
  446. bind_params.push 1
  447. else
  448. query += "#{attribute} IS NULL"
  449. end
  450. elsif selector['pre_condition'] == 'current_user.id'
  451. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  452. query += "#{attribute} IN (?)"
  453. bind_params.push current_user_id
  454. elsif selector['pre_condition'] == 'current_user.organization_id'
  455. raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id
  456. query += "#{attribute} IN (?)"
  457. user = User.lookup(id: current_user_id)
  458. bind_params.push user.organization_id
  459. else
  460. # rubocop:disable Style/IfInsideElse
  461. if selector['value'].nil?
  462. query += "#{attribute} IS NULL"
  463. else
  464. query += "#{attribute} IN (?)"
  465. bind_params.push selector['value']
  466. end
  467. # rubocop:enable Style/IfInsideElse
  468. end
  469. elsif selector['operator'] == 'is not'
  470. if selector['pre_condition'] == 'not_set'
  471. if attributes[1] =~ /^(created_by|updated_by|owner|customer|user)_id/
  472. query += "#{attribute} NOT IN (?)"
  473. bind_params.push 1
  474. else
  475. query += "#{attribute} IS NOT NULL"
  476. end
  477. elsif selector['pre_condition'] == 'current_user.id'
  478. query += "#{attribute} NOT IN (?)"
  479. bind_params.push current_user_id
  480. elsif selector['pre_condition'] == 'current_user.organization_id'
  481. query += "#{attribute} NOT IN (?)"
  482. user = User.lookup(id: current_user_id)
  483. bind_params.push user.organization_id
  484. else
  485. # rubocop:disable Style/IfInsideElse
  486. if selector['value'].nil?
  487. query += "#{attribute} IS NOT NULL"
  488. else
  489. query += "#{attribute} NOT IN (?)"
  490. bind_params.push selector['value']
  491. end
  492. # rubocop:enable Style/IfInsideElse
  493. end
  494. elsif selector['operator'] == 'contains'
  495. query += "#{attribute} #{like} (?)"
  496. value = "%#{selector['value']}%"
  497. bind_params.push value
  498. elsif selector['operator'] == 'contains not'
  499. query += "#{attribute} NOT #{like} (?)"
  500. value = "%#{selector['value']}%"
  501. bind_params.push value
  502. elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
  503. query += "? = (
  504. SELECT
  505. COUNT(*)
  506. FROM
  507. tag_objects,
  508. tag_items,
  509. tags
  510. WHERE
  511. tickets.id = tags.o_id AND
  512. tag_objects.id = tags.tag_object_id AND
  513. tag_objects.name = 'Ticket' AND
  514. tag_items.id = tags.tag_item_id AND
  515. tag_items.name IN (?)
  516. )"
  517. bind_params.push selector['value'].count
  518. bind_params.push selector['value']
  519. elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
  520. query += "1 <= (
  521. SELECT
  522. COUNT(*)
  523. FROM
  524. tag_objects,
  525. tag_items,
  526. tags
  527. WHERE
  528. tickets.id = tags.o_id AND
  529. tag_objects.id = tags.tag_object_id AND
  530. tag_objects.name = 'Ticket' AND
  531. tag_items.id = tags.tag_item_id AND
  532. tag_items.name IN (?)
  533. )"
  534. bind_params.push selector['value']
  535. elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
  536. query += "0 = (
  537. SELECT
  538. COUNT(*)
  539. FROM
  540. tag_objects,
  541. tag_items,
  542. tags
  543. WHERE
  544. tickets.id = tags.o_id AND
  545. tag_objects.id = tags.tag_object_id AND
  546. tag_objects.name = 'Ticket' AND
  547. tag_items.id = tags.tag_item_id AND
  548. tag_items.name IN (?)
  549. )"
  550. bind_params.push selector['value']
  551. elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
  552. query += "(
  553. SELECT
  554. COUNT(*)
  555. FROM
  556. tag_objects,
  557. tag_items,
  558. tags
  559. WHERE
  560. tickets.id = tags.o_id AND
  561. tag_objects.id = tags.tag_object_id AND
  562. tag_objects.name = 'Ticket' AND
  563. tag_items.id = tags.tag_item_id AND
  564. tag_items.name IN (?)
  565. ) BETWEEN ? AND ?"
  566. bind_params.push selector['value']
  567. bind_params.push selector['value'].count - 1
  568. bind_params.push selector['value'].count
  569. elsif selector['operator'] == 'before (absolute)'
  570. query += "#{attribute} <= ?"
  571. bind_params.push selector['value']
  572. elsif selector['operator'] == 'after (absolute)'
  573. query += "#{attribute} >= ?"
  574. bind_params.push selector['value']
  575. elsif selector['operator'] == 'within last (relative)'
  576. query += "#{attribute} >= ?"
  577. time = nil
  578. if selector['range'] == 'minute'
  579. time = Time.zone.now - selector['value'].to_i.minutes
  580. elsif selector['range'] == 'hour'
  581. time = Time.zone.now - selector['value'].to_i.hours
  582. elsif selector['range'] == 'day'
  583. time = Time.zone.now - selector['value'].to_i.days
  584. elsif selector['range'] == 'month'
  585. time = Time.zone.now - selector['value'].to_i.months
  586. elsif selector['range'] == 'year'
  587. time = Time.zone.now - selector['value'].to_i.years
  588. else
  589. raise "Unknown selector attributes '#{selector.inspect}'"
  590. end
  591. bind_params.push time
  592. elsif selector['operator'] == 'within next (relative)'
  593. query += "#{attribute} <= ?"
  594. time = nil
  595. if selector['range'] == 'minute'
  596. time = Time.zone.now + selector['value'].to_i.minutes
  597. elsif selector['range'] == 'hour'
  598. time = Time.zone.now + selector['value'].to_i.hours
  599. elsif selector['range'] == 'day'
  600. time = Time.zone.now + selector['value'].to_i.days
  601. elsif selector['range'] == 'month'
  602. time = Time.zone.now + selector['value'].to_i.months
  603. elsif selector['range'] == 'year'
  604. time = Time.zone.now + selector['value'].to_i.years
  605. else
  606. raise "Unknown selector attributes '#{selector.inspect}'"
  607. end
  608. bind_params.push time
  609. elsif selector['operator'] == 'before (relative)'
  610. query += "#{attribute} <= ?"
  611. time = nil
  612. if selector['range'] == 'minute'
  613. time = Time.zone.now - selector['value'].to_i.minutes
  614. elsif selector['range'] == 'hour'
  615. time = Time.zone.now - selector['value'].to_i.hours
  616. elsif selector['range'] == 'day'
  617. time = Time.zone.now - selector['value'].to_i.days
  618. elsif selector['range'] == 'month'
  619. time = Time.zone.now - selector['value'].to_i.months
  620. elsif selector['range'] == 'year'
  621. time = Time.zone.now - selector['value'].to_i.years
  622. else
  623. raise "Unknown selector attributes '#{selector.inspect}'"
  624. end
  625. bind_params.push time
  626. elsif selector['operator'] == 'after (relative)'
  627. query += "#{attribute} >= ?"
  628. time = nil
  629. if selector['range'] == 'minute'
  630. time = Time.zone.now + selector['value'].to_i.minutes
  631. elsif selector['range'] == 'hour'
  632. time = Time.zone.now + selector['value'].to_i.hours
  633. elsif selector['range'] == 'day'
  634. time = Time.zone.now + selector['value'].to_i.days
  635. elsif selector['range'] == 'month'
  636. time = Time.zone.now + selector['value'].to_i.months
  637. elsif selector['range'] == 'year'
  638. time = Time.zone.now + selector['value'].to_i.years
  639. else
  640. raise "Unknown selector attributes '#{selector.inspect}'"
  641. end
  642. bind_params.push time
  643. else
  644. raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'"
  645. end
  646. }
  647. [query, bind_params, tables]
  648. end
  649. =begin
  650. perform changes on ticket
  651. ticket.perform_changes({}, 'trigger', item, current_user_id)
  652. =end
  653. def perform_changes(perform, perform_origin, item = nil, current_user_id = nil)
  654. logger.debug "Perform #{perform_origin} #{perform.inspect} on Ticket.find(#{id})"
  655. # if the configuration contains the deletion of the ticket then
  656. # we skip all other ticket changes because they does not matter
  657. if perform['ticket.action'].present? && perform['ticket.action']['value'] == 'delete'
  658. perform.each do |key, _value|
  659. (object_name, attribute) = key.split('.', 2)
  660. next if object_name != 'ticket'
  661. next if attribute == 'action'
  662. perform.delete(key)
  663. end
  664. end
  665. changed = false
  666. perform.each do |key, value|
  667. (object_name, attribute) = key.split('.', 2)
  668. raise "Unable to update object #{object_name}.#{attribute}, only can update tickets and send notifications!" if object_name != 'ticket' && object_name != 'notification'
  669. # send notification
  670. if object_name == 'notification'
  671. # value['recipient'] was a string in the past (single-select) so we convert it to array if needed
  672. value_recipient = value['recipient']
  673. if !value_recipient.is_a?(Array)
  674. value_recipient = [value_recipient]
  675. end
  676. recipients_raw = []
  677. value_recipient.each { |recipient|
  678. if recipient == 'article_last_sender'
  679. if item && item[:article_id]
  680. article = Ticket::Article.lookup(id: item[:article_id])
  681. if article.reply_to.present?
  682. recipients_raw.push(article.reply_to)
  683. elsif article.from.present?
  684. recipients_raw.push(article.from)
  685. elsif article.origin_by_id
  686. email = User.lookup(id: article.origin_by_id).email
  687. recipients_raw.push(email)
  688. elsif article.created_by_id
  689. email = User.lookup(id: article.created_by_id).email
  690. recipients_raw.push(email)
  691. end
  692. end
  693. elsif recipient == 'ticket_customer'
  694. email = User.lookup(id: customer_id).email
  695. recipients_raw.push(email)
  696. elsif recipient == 'ticket_owner'
  697. email = User.lookup(id: owner_id).email
  698. recipients_raw.push(email)
  699. elsif recipient == 'ticket_agents'
  700. User.group_access(group_id, 'full').sort_by(&:login).each do |user|
  701. recipients_raw.push(user.email)
  702. end
  703. else
  704. logger.error "Unknown email notification recipient '#{recipient}'"
  705. next
  706. end
  707. }
  708. recipients_checked = []
  709. recipients_raw.each { |recipient_email|
  710. skip_user = false
  711. users = User.where(email: recipient_email)
  712. users.each { |user|
  713. next if user.preferences[:mail_delivery_failed] != true
  714. next if !user.preferences[:mail_delivery_failed_data]
  715. till_blocked = ((user.preferences[:mail_delivery_failed_data] - Time.zone.now - 60.days) / 60 / 60 / 24).round
  716. next if till_blocked.positive?
  717. logger.info "Send no trigger based notification to #{recipient_email} because email is marked as mail_delivery_failed for #{till_blocked} days"
  718. skip_user = true
  719. break
  720. }
  721. next if skip_user
  722. # send notifications only to email adresses
  723. next if !recipient_email
  724. next if recipient_email !~ /@/
  725. # check if address is valid
  726. begin
  727. recipient_email = Mail::Address.new(recipient_email).address
  728. rescue
  729. next # because unable to parse
  730. end
  731. # do not sent notifications to this recipients
  732. send_no_auto_response_reg_exp = Setting.get('send_no_auto_response_reg_exp')
  733. begin
  734. next if recipient_email =~ /#{send_no_auto_response_reg_exp}/i
  735. rescue => e
  736. logger.error "ERROR: Invalid regex '#{send_no_auto_response_reg_exp}' in setting send_no_auto_response_reg_exp"
  737. logger.error 'ERROR: ' + e.inspect
  738. next if recipient_email =~ /(mailer-daemon|postmaster|abuse|root)@.+?\..+?/i
  739. end
  740. # check if notification should be send because of customer emails
  741. if item && item[:article_id]
  742. article = Ticket::Article.lookup(id: item[:article_id])
  743. if article && article.preferences['is-auto-response'] == true && article.from && article.from =~ /#{Regexp.quote(recipient_email)}/i
  744. logger.info "Send no trigger based notification to #{recipient_email} because of auto response tagged incoming email"
  745. next
  746. end
  747. end
  748. # loop protection / check if maximal count of trigger mail has reached
  749. map = {
  750. 10 => 10,
  751. 30 => 15,
  752. 60 => 25,
  753. 180 => 50,
  754. 600 => 100,
  755. }
  756. skip = false
  757. map.each { |minutes, count|
  758. already_sent = Ticket::Article.where(
  759. ticket_id: id,
  760. sender: Ticket::Article::Sender.find_by(name: 'System'),
  761. type: Ticket::Article::Type.find_by(name: 'email'),
  762. ).where("ticket_articles.created_at > ? AND ticket_articles.to LIKE '%#{recipient_email.strip}%'", Time.zone.now - minutes.minutes).count
  763. next if already_sent < count
  764. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} for this ticket within last #{minutes} minutes (loop protection)"
  765. skip = true
  766. break
  767. }
  768. next if skip
  769. map = {
  770. 10 => 30,
  771. 30 => 60,
  772. 60 => 120,
  773. 180 => 240,
  774. 600 => 360,
  775. }
  776. skip = false
  777. map.each { |minutes, count|
  778. already_sent = Ticket::Article.where(
  779. sender: Ticket::Article::Sender.find_by(name: 'System'),
  780. type: Ticket::Article::Type.find_by(name: 'email'),
  781. ).where("ticket_articles.created_at > ? AND ticket_articles.to LIKE '%#{recipient_email.strip}%'", Time.zone.now - minutes.minutes).count
  782. next if already_sent < count
  783. logger.info "Send no trigger based notification to #{recipient_email} because already sent #{count} in total within last #{minutes} minutes (loop protection)"
  784. skip = true
  785. break
  786. }
  787. next if skip
  788. email = recipient_email.downcase.strip
  789. next if recipients_checked.include?(email)
  790. recipients_checked.push(email)
  791. }
  792. next if recipients_checked.blank?
  793. recipient_string = recipients_checked.join(', ')
  794. group = self.group
  795. next if !group
  796. email_address = group.email_address
  797. if !email_address
  798. logger.info "Unable to send trigger based notification to #{recipient_string} because no email address is set for group '#{group.name}'"
  799. next
  800. end
  801. if !email_address.channel_id
  802. logger.info "Unable to send trigger based notification to #{recipient_string} because no channel is set for email address '#{email_address.email}' (id: #{email_address.id})"
  803. next
  804. end
  805. objects = {
  806. ticket: self,
  807. article: articles.last,
  808. }
  809. # get subject
  810. subject = NotificationFactory::Mailer.template(
  811. templateInline: value['subject'],
  812. locale: 'en-en',
  813. objects: objects,
  814. quote: false,
  815. )
  816. subject = subject_build(subject)
  817. body = NotificationFactory::Mailer.template(
  818. templateInline: value['body'],
  819. locale: 'en-en',
  820. objects: objects,
  821. quote: true,
  822. )
  823. Ticket::Article.create(
  824. ticket_id: id,
  825. to: recipient_string,
  826. subject: subject,
  827. content_type: 'text/html',
  828. body: body,
  829. internal: false,
  830. sender: Ticket::Article::Sender.find_by(name: 'System'),
  831. type: Ticket::Article::Type.find_by(name: 'email'),
  832. preferences: {
  833. perform_origin: perform_origin,
  834. },
  835. updated_by_id: 1,
  836. created_by_id: 1,
  837. )
  838. next
  839. end
  840. # update tags
  841. if key == 'ticket.tags'
  842. next if value['value'].blank?
  843. tags = value['value'].split(/,/)
  844. if value['operator'] == 'add'
  845. tags.each { |tag|
  846. tag_add(tag)
  847. }
  848. elsif value['operator'] == 'remove'
  849. tags.each { |tag|
  850. tag_remove(tag)
  851. }
  852. else
  853. logger.error "Unknown #{attribute} operator #{value['operator']}"
  854. end
  855. next
  856. end
  857. # delete ticket
  858. if key == 'ticket.action'
  859. next if value['value'].blank?
  860. next if value['value'] != 'delete'
  861. destroy
  862. next
  863. end
  864. # lookup pre_condition
  865. if value['pre_condition']
  866. if value['pre_condition'] =~ /^not_set/
  867. value['value'] = 1
  868. elsif value['pre_condition'] =~ /^current_user\./
  869. raise 'Unable to use current_user, got no current_user_id for ticket.perform_changes' if !current_user_id
  870. value['value'] = current_user_id
  871. end
  872. end
  873. # update ticket
  874. next if self[attribute].to_s == value['value'].to_s
  875. changed = true
  876. self[attribute] = value['value']
  877. logger.debug "set #{object_name}.#{attribute} = #{value['value'].inspect}"
  878. end
  879. return if !changed
  880. save
  881. end
  882. =begin
  883. get all email references headers of a ticket, to exclude some, parse it as array into method
  884. references = ticket.get_references
  885. result
  886. ['message-id-1234', 'message-id-5678']
  887. ignore references header(s)
  888. references = ticket.get_references(['message-id-5678'])
  889. result
  890. ['message-id-1234']
  891. =end
  892. def get_references(ignore = [])
  893. references = []
  894. Ticket::Article.select('in_reply_to, message_id').where(ticket_id: id).each { |article|
  895. if !article.in_reply_to.empty?
  896. references.push article.in_reply_to
  897. end
  898. next if !article.message_id
  899. next if article.message_id.empty?
  900. references.push article.message_id
  901. }
  902. ignore.each { |item|
  903. references.delete(item)
  904. }
  905. references
  906. end
  907. =begin
  908. get all articles of a ticket in correct order (overwrite active record default method)
  909. artilces = ticket.articles
  910. result
  911. [article1, articl2]
  912. =end
  913. def articles
  914. Ticket::Article.where(ticket_id: id).order(:created_at, :id)
  915. end
  916. def history_get(fulldata = false)
  917. list = History.list(self.class.name, self['id'], 'Ticket::Article')
  918. return list if !fulldata
  919. # get related objects
  920. assets = {}
  921. list.each { |item|
  922. record = Kernel.const_get(item['object']).find(item['o_id'])
  923. assets = record.assets(assets)
  924. if item['related_object']
  925. record = Kernel.const_get(item['related_object']).find( item['related_o_id'])
  926. assets = record.assets(assets)
  927. end
  928. }
  929. {
  930. history: list,
  931. assets: assets,
  932. }
  933. end
  934. private
  935. def check_generate
  936. return true if number
  937. self.number = Ticket::Number.generate
  938. true
  939. end
  940. def check_title
  941. return true if !title
  942. title.gsub!(/\s|\t|\r/, ' ')
  943. true
  944. end
  945. def check_defaults
  946. if !owner_id
  947. self.owner_id = 1
  948. end
  949. return true if !customer_id
  950. customer = User.find_by(id: customer_id)
  951. return true if !customer
  952. return true if organization_id == customer.organization_id
  953. self.organization_id = customer.organization_id
  954. true
  955. end
  956. def reset_pending_time
  957. # ignore if no state has changed
  958. return true if !changes['state_id']
  959. # ignore if new state is blank and
  960. # let handle ActiveRecord the error
  961. return if state_id.blank?
  962. # check if new state isn't pending*
  963. current_state = Ticket::State.lookup(id: state_id)
  964. current_state_type = Ticket::StateType.lookup(id: current_state.state_type_id)
  965. # in case, set pending_time to nil
  966. return true if current_state_type.name =~ /^pending/i
  967. self.pending_time = nil
  968. true
  969. end
  970. def check_escalation_update
  971. escalation_calculation
  972. true
  973. end
  974. def set_default_state
  975. return true if state_id
  976. default_ticket_state = Ticket::State.find_by(default_create: true)
  977. return true if !default_ticket_state
  978. self.state_id = default_ticket_state.id
  979. true
  980. end
  981. def set_default_priority
  982. return true if priority_id
  983. default_ticket_priority = Ticket::Priority.find_by(default_create: true)
  984. return true if !default_ticket_priority
  985. self.priority_id = default_ticket_priority.id
  986. true
  987. end
  988. end