tickets_controller.rb 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class TicketsController < ApplicationController
  3. include CreatesTicketArticles
  4. include ClonesTicketArticleAttachments
  5. include ChecksUserAttributesByCurrentUserPermission
  6. include TicketStats
  7. include CanPaginate
  8. prepend_before_action -> { authorize! }, only: %i[create import_example import_start ticket_customer ticket_history ticket_related ticket_recent ticket_merge ticket_split]
  9. prepend_before_action :authentication_check
  10. # GET /api/v1/tickets
  11. def index
  12. paginate_with(max: 100)
  13. tickets = TicketPolicy::ReadScope.new(current_user).resolve
  14. .reorder(id: :asc)
  15. .offset(pagination.offset)
  16. .limit(pagination.limit)
  17. if response_expand?
  18. list = tickets.map(&:attributes_with_association_names)
  19. render json: list, status: :ok
  20. return
  21. end
  22. if response_full?
  23. assets = {}
  24. item_ids = []
  25. tickets.each do |item|
  26. item_ids.push item.id
  27. assets = item.assets(assets)
  28. end
  29. render json: {
  30. record_ids: item_ids,
  31. assets: assets,
  32. }, status: :ok
  33. return
  34. end
  35. render json: tickets
  36. end
  37. # GET /api/v1/tickets/1
  38. def show
  39. ticket = Ticket.find(params[:id])
  40. authorize!(ticket)
  41. auto_assign_ticket(ticket)
  42. if response_expand?
  43. result = ticket.attributes_with_association_names
  44. render json: result, status: :ok
  45. return
  46. end
  47. if response_full?
  48. full = Ticket.full(params[:id])
  49. render json: full
  50. return
  51. end
  52. if response_all?
  53. render json: ticket_all(ticket)
  54. return
  55. end
  56. render json: ticket
  57. end
  58. def auto_assign_ticket(ticket)
  59. return if params[:auto_assign].blank?
  60. ticket.auto_assign(current_user)
  61. end
  62. # POST /api/v1/tickets
  63. def create
  64. ticket = nil
  65. Transaction.execute do # rubocop:disable Metrics/BlockLength
  66. customer = {}
  67. if params[:customer].instance_of?(ActionController::Parameters)
  68. customer = params[:customer]
  69. params.delete(:customer)
  70. end
  71. if (shared_draft_id = params[:shared_draft_id])
  72. shared_draft = Ticket::SharedDraftStart.find_by id: shared_draft_id
  73. if shared_draft && (shared_draft.group_id.to_s != params[:group_id]&.to_s || !shared_draft.group.shared_drafts?)
  74. raise Exceptions::UnprocessableEntity, __('Shared draft cannot be selected for this ticket.')
  75. end
  76. shared_draft&.destroy
  77. end
  78. clean_params = Ticket.association_name_to_id_convert(params)
  79. # overwrite params
  80. if !current_user.permissions?('ticket.agent')
  81. %i[owner owner_id customer customer_id preferences].each do |key|
  82. clean_params.delete(key)
  83. end
  84. clean_params[:customer_id] = current_user.id
  85. end
  86. # The parameter :customer_id is 'abused' in cases where it is not an integer, but a string like
  87. # 'guess:customers.email@domain.cm' which implies that the customer should be looked up.
  88. if clean_params[:customer_id].is_a?(String) && clean_params[:customer_id] =~ %r{^guess:(.+?)$}
  89. email_address = $1
  90. email_address_validation = EmailAddressValidation.new(email_address)
  91. if !email_address_validation.valid?
  92. render json: { error: "Invalid email '#{email_address}' of customer" }, status: :unprocessable_entity
  93. return
  94. end
  95. local_customer = User.find_by(email: email_address.downcase)
  96. if !local_customer
  97. role_ids = Role.signup_role_ids
  98. local_customer = User.create(
  99. firstname: '',
  100. lastname: '',
  101. email: email_address,
  102. password: '',
  103. active: true,
  104. role_ids: role_ids,
  105. )
  106. end
  107. clean_params[:customer_id] = local_customer.id
  108. end
  109. # try to create customer if needed
  110. if clean_params[:customer_id].blank? && customer.present?
  111. check_attributes_by_current_user_permission(customer)
  112. clean_customer = User.association_name_to_id_convert(customer)
  113. local_customer = nil
  114. if !local_customer && clean_customer[:id].present?
  115. local_customer = User.find_by(id: clean_customer[:id])
  116. end
  117. if !local_customer && clean_customer[:email].present?
  118. local_customer = User.find_by(email: clean_customer[:email].downcase)
  119. end
  120. if !local_customer && clean_customer[:login].present?
  121. local_customer = User.find_by(login: clean_customer[:login].downcase)
  122. end
  123. if !local_customer
  124. role_ids = Role.signup_role_ids
  125. local_customer = User.new(clean_customer)
  126. local_customer.role_ids = role_ids
  127. local_customer.save!
  128. end
  129. clean_params[:customer_id] = local_customer.id
  130. end
  131. clean_params = Ticket.param_cleanup(clean_params, true)
  132. clean_params[:screen] = 'create_middle'
  133. ticket = Ticket.new(clean_params)
  134. authorize!(ticket, :create?)
  135. # create ticket
  136. ticket.save!
  137. # create tags if given
  138. if params[:tags].present?
  139. tags = params[:tags].split(',').map(&:strip)
  140. tags.each do |tag|
  141. next if !::Tag.tag_allowed?(name: tag, user_id: UserInfo.current_user_id)
  142. ticket.tag_add(tag)
  143. end
  144. end
  145. # This mentions handling is used by custom API calls only
  146. # Mentions created in UI are handled by Ticket::Article#check_mentions
  147. if params[:mentions].present?
  148. authorize!(ticket, :create_mentions?)
  149. Array(params[:mentions]).each do |user_id|
  150. Mention.subscribe! ticket, User.find(user_id)
  151. end
  152. end
  153. # create article if given
  154. if params[:article]
  155. article_create(ticket, params[:article])
  156. end
  157. # create links (e. g. in case of ticket split)
  158. # links: {
  159. # Ticket: {
  160. # parent: [ticket_id1, ticket_id2, ...]
  161. # normal: [ticket_id1, ticket_id2, ...]
  162. # child: [ticket_id1, ticket_id2, ...]
  163. # },
  164. # }
  165. if params[:links].present?
  166. link = params[:links].permit!.to_h
  167. raise Exceptions::UnprocessableEntity, __('Invalid link structure') if !link.is_a? Hash
  168. link.each do |target_object, link_types_with_object_ids|
  169. raise Exceptions::UnprocessableEntity, __('Invalid link structure (Object)') if !link_types_with_object_ids.is_a? Hash
  170. link_types_with_object_ids.each do |link_type, object_ids|
  171. raise Exceptions::UnprocessableEntity, __('Invalid link structure (Object → LinkType)') if !object_ids.is_a? Array
  172. object_ids.each do |local_object_id|
  173. link = Link.add(
  174. link_type: link_type,
  175. link_object_target: target_object,
  176. link_object_target_value: local_object_id,
  177. link_object_source: 'Ticket',
  178. link_object_source_value: ticket.id,
  179. )
  180. end
  181. end
  182. end
  183. end
  184. end
  185. if response_expand?
  186. result = ticket.reload.attributes_with_association_names
  187. render json: result, status: :created
  188. return
  189. end
  190. if response_full?
  191. full = Ticket.full(ticket.id)
  192. render json: full, status: :created
  193. return
  194. end
  195. if response_all?
  196. render json: ticket_all(ticket.reload), status: :created
  197. return
  198. end
  199. render json: ticket.reload.attributes_with_association_ids, status: :created
  200. end
  201. # PUT /api/v1/tickets/1
  202. def update
  203. ticket = Ticket.find(params[:id])
  204. authorize!(ticket, :follow_up?)
  205. clean_params = Ticket.association_name_to_id_convert(params)
  206. clean_params = Ticket.param_cleanup(clean_params, true)
  207. # only apply preferences changes (keep not updated keys/values)
  208. clean_params = ticket.param_preferences_merge(clean_params)
  209. clean_params[:screen] = 'edit'
  210. # disable changes on ticket number
  211. clean_params.delete('number')
  212. # overwrite params
  213. if !current_user.permissions?('ticket.agent')
  214. %i[owner owner_id customer customer_id organization organization_id preferences].each do |key|
  215. clean_params.delete(key)
  216. end
  217. end
  218. ticket.with_lock do
  219. ticket.update!(clean_params)
  220. if params[:article].present?
  221. if (shared_draft_id = params[:article][:shared_draft_id])
  222. shared_draft = Ticket::SharedDraftZoom.find_by id: shared_draft_id
  223. if shared_draft && shared_draft.ticket != ticket
  224. raise Exceptions::UnprocessableEntity, __('Shared draft cannot be selected for this ticket.')
  225. end
  226. shared_draft&.destroy
  227. end
  228. article_create(ticket, params[:article])
  229. end
  230. end
  231. if response_expand?
  232. result = ticket.reload.attributes_with_association_names
  233. render json: result, status: :ok
  234. return
  235. end
  236. if response_full?
  237. full = Ticket.full(params[:id])
  238. render json: full, status: :ok
  239. return
  240. end
  241. if response_all?
  242. render json: ticket_all(ticket.reload), status: :ok
  243. return
  244. end
  245. render json: ticket.reload.attributes_with_association_ids, status: :ok
  246. end
  247. # DELETE /api/v1/tickets/1
  248. def destroy
  249. ticket = Ticket.find(params[:id])
  250. authorize!(ticket)
  251. ticket.destroy!
  252. head :ok
  253. end
  254. # GET /api/v1/ticket_customer
  255. # GET /api/v1/tickets_customer
  256. def ticket_customer
  257. # return result
  258. result = Ticket::ScreenOptions.list_by_customer(
  259. current_user: current_user,
  260. customer_id: params[:customer_id],
  261. limit: 15,
  262. )
  263. render json: result
  264. end
  265. # GET /api/v1/ticket_history/1
  266. def ticket_history
  267. # get ticket data
  268. ticket = Ticket.find(params[:id])
  269. authorize!(ticket, :show?)
  270. # get history of ticket
  271. render json: ticket.history_get(true)
  272. end
  273. # GET /api/v1/ticket_related/1
  274. def ticket_related
  275. ticket = Ticket.find(params[:ticket_id])
  276. assets = ticket.assets({})
  277. tickets = TicketPolicy::ReadScope.new(current_user).resolve
  278. .where(
  279. customer_id: ticket.customer_id,
  280. state_id: Ticket::State.by_category(:open).select(:id),
  281. )
  282. .where.not(id: ticket.id)
  283. .reorder(created_at: :desc)
  284. .limit(6)
  285. # if we do not have open related tickets, search for any tickets
  286. tickets ||= TicketPolicy::ReadScope.new(current_user).resolve
  287. .where(customer_id: ticket.customer_id)
  288. .where.not(state_id: Ticket::State.by_category_ids(:merged))
  289. .where.not(id: ticket.id)
  290. .reorder(created_at: :desc)
  291. .limit(6)
  292. # get related assets
  293. ticket_ids_by_customer = []
  294. tickets.each do |ticket_list|
  295. ticket_ids_by_customer.push ticket_list.id
  296. assets = ticket_list.assets(assets)
  297. end
  298. ticket_ids_recent_viewed = []
  299. recent_views = RecentView.list(current_user, 8, 'Ticket')
  300. recent_views.each do |recent_view|
  301. next if recent_view.object.name != 'Ticket'
  302. next if recent_view.o_id == ticket.id
  303. ticket_ids_recent_viewed.push recent_view.o_id
  304. recent_view_ticket = Ticket.find(recent_view.o_id)
  305. assets = recent_view_ticket.assets(assets)
  306. end
  307. # return result
  308. render json: {
  309. assets: assets,
  310. ticket_ids_by_customer: ticket_ids_by_customer,
  311. ticket_ids_recent_viewed: ticket_ids_recent_viewed,
  312. }
  313. end
  314. # GET /api/v1/ticket_recent
  315. def ticket_recent
  316. ticket_ids = RecentView.list(current_user, 10, Ticket.name).map(&:o_id)
  317. tickets = ticket_ids.map { |elem| Ticket.lookup(id: elem) }
  318. assets = ApplicationModel::CanAssets.reduce(tickets)
  319. render json: {
  320. assets: assets,
  321. ticket_ids_recent_viewed: ticket_ids
  322. }
  323. end
  324. # PUT /api/v1/ticket_merge/1/1
  325. def ticket_merge
  326. # check target ticket
  327. target_ticket = Ticket.find_by(number: params[:target_ticket_number])
  328. if !target_ticket
  329. render json: {
  330. result: 'failed',
  331. message: __('The target ticket number could not be found.'),
  332. }
  333. return
  334. end
  335. # check source ticket
  336. source_ticket = Ticket.find_by(id: params[:source_ticket_id])
  337. if !source_ticket
  338. render json: {
  339. result: 'failed',
  340. message: __('The source ticket could not be found.'),
  341. }
  342. return
  343. end
  344. # merge ticket
  345. Service::Ticket::Merge.new(current_user:).execute(source_ticket:, target_ticket:)
  346. # return result
  347. render json: {
  348. result: 'success',
  349. target_ticket: target_ticket.attributes,
  350. source_ticket: source_ticket.attributes,
  351. }
  352. end
  353. # GET /api/v1/ticket_split
  354. def ticket_split
  355. ticket = Ticket.find(params[:ticket_id])
  356. authorize!(ticket, :show?)
  357. assets = ticket.assets({})
  358. article = Ticket::Article.find(params[:article_id])
  359. authorize!(article.ticket, :show?)
  360. assets = article.assets(assets)
  361. render json: {
  362. assets: assets,
  363. attachments: article_attachments_clone(article),
  364. }
  365. end
  366. # GET /api/v1/ticket_create
  367. def ticket_create
  368. # get attributes to update
  369. attributes_to_change = Ticket::ScreenOptions.attributes_to_change(
  370. view: 'ticket_create',
  371. screen: 'create_middle',
  372. current_user: current_user,
  373. )
  374. render json: attributes_to_change
  375. end
  376. # GET /api/v1/tickets/search
  377. def search
  378. # permit nested conditions
  379. if params[:condition]
  380. params.require(:condition).permit!
  381. end
  382. paginate_with(max: 200, default: 50)
  383. query = params[:query]
  384. if query.respond_to?(:permit!)
  385. query = query.permit!.to_h
  386. end
  387. # build result list
  388. tickets = Ticket.search(
  389. query: query,
  390. condition: params[:condition].to_h,
  391. limit: pagination.limit,
  392. offset: pagination.offset,
  393. order_by: params[:order_by],
  394. sort_by: params[:sort_by],
  395. current_user: current_user,
  396. )
  397. if response_expand?
  398. list = tickets.map(&:attributes_with_association_names)
  399. render json: list, status: :ok
  400. return
  401. end
  402. assets = {}
  403. ticket_result = []
  404. tickets.each do |ticket|
  405. ticket_result.push ticket.id
  406. assets = ticket.assets(assets)
  407. end
  408. # return result
  409. render json: {
  410. tickets: ticket_result,
  411. tickets_count: tickets.count,
  412. assets: assets,
  413. }
  414. end
  415. # GET /api/v1/ticket_stats
  416. def stats
  417. if !params[:user_id] && !params[:organization_id]
  418. raise __('Need user_id or organization_id as param')
  419. end
  420. # lookup open user tickets
  421. limit = 100
  422. assets = {}
  423. user_tickets = {}
  424. if params[:user_id]
  425. user = User.lookup(id: params[:user_id])
  426. if !user
  427. raise "No such user with id #{params[:user_id]}"
  428. end
  429. conditions = {
  430. closed_ids: {
  431. 'ticket.state_id' => {
  432. operator: 'is',
  433. value: Ticket::State.by_category_ids(:closed),
  434. },
  435. 'ticket.customer_id' => {
  436. operator: 'is',
  437. value: user.id,
  438. },
  439. },
  440. open_ids: {
  441. 'ticket.state_id' => {
  442. operator: 'is',
  443. value: Ticket::State.by_category_ids(:open),
  444. },
  445. 'ticket.customer_id' => {
  446. operator: 'is',
  447. value: user.id,
  448. },
  449. },
  450. }
  451. conditions.each do |key, local_condition|
  452. user_tickets[key] = ticket_ids_and_assets(local_condition, current_user, limit, assets)
  453. end
  454. # generate stats by user
  455. condition = {
  456. 'tickets.customer_id' => user.id,
  457. }
  458. user_tickets[:volume_by_year] = ticket_stats_last_year(condition)
  459. end
  460. # lookup open org tickets
  461. org_tickets = {}
  462. organization_ids = Array(params[:organization_id])
  463. if organization_ids.present?
  464. organization_ids.each do |organization_id|
  465. organization = Organization.lookup(id: organization_id)
  466. if !organization
  467. raise "No such organization with id #{organization_id}"
  468. end
  469. end
  470. conditions = {
  471. closed_ids: {
  472. 'ticket.state_id' => {
  473. operator: 'is',
  474. value: Ticket::State.by_category_ids(:closed),
  475. },
  476. 'ticket.organization_id' => {
  477. operator: 'is',
  478. value: organization_ids,
  479. },
  480. },
  481. open_ids: {
  482. 'ticket.state_id' => {
  483. operator: 'is',
  484. value: Ticket::State.by_category_ids(:open),
  485. },
  486. 'ticket.organization_id' => {
  487. operator: 'is',
  488. value: organization_ids,
  489. },
  490. },
  491. }
  492. conditions.each do |key, local_condition|
  493. org_tickets[key] = ticket_ids_and_assets(local_condition, current_user, limit, assets)
  494. end
  495. # generate stats by org
  496. condition = {
  497. 'tickets.organization_id' => organization_ids,
  498. }
  499. org_tickets[:volume_by_year] = ticket_stats_last_year(condition)
  500. end
  501. # return result
  502. render json: {
  503. user: user_tickets,
  504. organization: org_tickets,
  505. assets: assets,
  506. }
  507. end
  508. # @path [GET] /tickets/import_example
  509. #
  510. # @summary Download of example CSV file.
  511. # @notes The requester have 'admin' permissions to be able to download it.
  512. # @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/tickets/import_example
  513. #
  514. # @response_message 200 File download.
  515. # @response_message 403 Forbidden / Invalid session.
  516. def import_example
  517. csv_string = Ticket.csv_example(
  518. col_sep: ',',
  519. )
  520. send_data(
  521. csv_string,
  522. filename: 'example.csv',
  523. type: 'text/csv',
  524. disposition: 'attachment'
  525. )
  526. end
  527. # @path [POST] /tickets/import
  528. #
  529. # @summary Starts import.
  530. # @notes The requester have 'admin' permissions to be create a new import.
  531. # @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/tickets.csv' 'https://your.zammad/api/v1/tickets/import?try=true'
  532. # @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/tickets.csv' 'https://your.zammad/api/v1/tickets/import'
  533. #
  534. # @response_message 201 Import started.
  535. # @response_message 403 Forbidden / Invalid session.
  536. def import_start
  537. if Setting.get('import_mode') != true
  538. raise __('Tickets can only be imported if system is in import mode.')
  539. end
  540. string = params[:data]
  541. if string.blank? && params[:file].present?
  542. string = params[:file].read.force_encoding('utf-8')
  543. end
  544. raise Exceptions::UnprocessableEntity, __('No source data submitted!') if string.blank?
  545. result = Ticket.csv_import(
  546. string: string,
  547. parse_params: {
  548. col_sep: params[:col_sep] || ',',
  549. },
  550. try: params[:try],
  551. )
  552. render json: result, status: :ok
  553. end
  554. private
  555. def ticket_all(ticket)
  556. # get attributes to update
  557. attributes_to_change = Ticket::ScreenOptions.attributes_to_change(
  558. current_user: current_user,
  559. ticket: ticket,
  560. screen: 'edit',
  561. )
  562. # get related users
  563. assets = attributes_to_change[:assets]
  564. assets = ticket.assets(assets)
  565. # get related users
  566. article_ids = []
  567. ticket.articles.each do |article|
  568. next if !authorized?(article, :show?)
  569. article_ids.push article.id
  570. assets = article.assets(assets)
  571. end
  572. # get links
  573. links = Link.list(
  574. link_object: 'Ticket',
  575. link_object_value: ticket.id,
  576. user: current_user,
  577. )
  578. assets = Link.reduce_assets(assets, links)
  579. # get tags
  580. tags = ticket.tag_list
  581. # get time units
  582. time_accountings = ticket.ticket_time_accounting.map { |row| row.slice(:id, :ticket_id, :ticket_article_id, :time_unit, :type_id) }
  583. # get mentions
  584. mentions = Mention.where(mentionable: ticket).reorder(created_at: :desc)
  585. mentions.each do |mention|
  586. assets = mention.assets(assets)
  587. end
  588. if (draft = ticket.shared_draft) && authorized?(draft, :show?)
  589. assets = draft.assets(assets)
  590. end
  591. # return result
  592. {
  593. ticket_id: ticket.id,
  594. ticket_article_ids: article_ids,
  595. assets: assets,
  596. links: links,
  597. tags: tags,
  598. mentions: mentions.pluck(:id),
  599. time_accountings: time_accountings,
  600. form_meta: attributes_to_change[:form_meta],
  601. }
  602. end
  603. end