Fixes #2644 - Including Knowledge Base Answers into Ticket Articles doesn't attach Attachments
This commit is contained in:
parent
3306de2d1f
commit
880f1d5e3a
14 changed files with 226 additions and 85 deletions
|
@ -36,6 +36,15 @@ class App.UiElement.richtext
|
||||||
for file in attribute.attachments
|
for file in attribute.attachments
|
||||||
renderFile(file)
|
renderFile(file)
|
||||||
|
|
||||||
|
App.Event.bind('ui::ticket::addArticleAttachent', (data) ->
|
||||||
|
form_id = item.closest('form').find('[name=form_id]').val()
|
||||||
|
|
||||||
|
return if data.form_id isnt form_id
|
||||||
|
return if _.isEmpty(data.attachments)
|
||||||
|
for file in data.attachments
|
||||||
|
renderFile(file)
|
||||||
|
, form.form_id)
|
||||||
|
|
||||||
# remove items
|
# remove items
|
||||||
item.find('.attachments').on('click', '.js-delete', (e) =>
|
item.find('.attachments').on('click', '.js-delete', (e) =>
|
||||||
id = $(e.currentTarget).data('id')
|
id = $(e.currentTarget).data('id')
|
||||||
|
|
|
@ -81,7 +81,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
|
|
||||||
# add article attachment
|
# add article attachment
|
||||||
@bind('ui::ticket::addArticleAttachent', (data) =>
|
@bind('ui::ticket::addArticleAttachent', (data) =>
|
||||||
return if data.ticket.id.toString() isnt @ticket_id.toString()
|
return if data.ticket?.id?.toString() isnt @ticket_id.toString() && data.form_id isnt @form_id
|
||||||
return if _.isEmpty(data.attachments)
|
return if _.isEmpty(data.attachments)
|
||||||
for file in data.attachments
|
for file in data.attachments
|
||||||
@renderAttachment(file)
|
@renderAttachment(file)
|
||||||
|
|
|
@ -375,10 +375,17 @@
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
|
|
||||||
trigger.renderValue(this, elem, function(text) {
|
var form_id = this.$element.closest('form').find('[name=form_id]').val()
|
||||||
|
|
||||||
|
trigger.renderValue(this, elem, function(text, attachments) {
|
||||||
_this.cutInput()
|
_this.cutInput()
|
||||||
_this.paste(text)
|
_this.paste(text)
|
||||||
_this.close(true)
|
_this.close(true)
|
||||||
|
|
||||||
|
App.Event.trigger('ui::ticket::addArticleAttachent', {
|
||||||
|
attachments: attachments,
|
||||||
|
form_id: form_id
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -455,7 +462,7 @@
|
||||||
|
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
|
||||||
callback(item.content)
|
callback(item.content, [])
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection.renderResults = function(textmodule, term) {
|
Collection.renderResults = function(textmodule, term) {
|
||||||
|
@ -497,6 +504,8 @@
|
||||||
var element = $('<li>').text(App.i18n.translateInline('Please wait...'))
|
var element = $('<li>').text(App.i18n.translateInline('Please wait...'))
|
||||||
textmodule.appendResults(element)
|
textmodule.appendResults(element)
|
||||||
|
|
||||||
|
var form_id = textmodule.$element.closest('form').find('[name=form_id]').val()
|
||||||
|
|
||||||
App.Ajax.request({
|
App.Ajax.request({
|
||||||
id: 'textmoduleKbAnswer',
|
id: 'textmoduleKbAnswer',
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
|
@ -508,8 +517,23 @@
|
||||||
|
|
||||||
var body = translation.content().bodyWithPublicURLs()
|
var body = translation.content().bodyWithPublicURLs()
|
||||||
|
|
||||||
App.Utils.htmlImage2DataUrlAsync(body, function(output){
|
App.Ajax.request({
|
||||||
callback(output)
|
id: 'textmoduleKbAnswerAttachments',
|
||||||
|
type: 'POST',
|
||||||
|
data: JSON.stringify({
|
||||||
|
form_id: form_id
|
||||||
|
}),
|
||||||
|
url: translation.parent().generateURL('/attachments/clone_to_form'),
|
||||||
|
success: function(data, status, xhr) {
|
||||||
|
translation.parent().attachments += data.attachments
|
||||||
|
|
||||||
|
App.Utils.htmlImage2DataUrlAsync(body, function(output){
|
||||||
|
callback(output, translation.parent().attachments)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
callback('')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
error: function(xhr) {
|
error: function(xhr) {
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
class KnowledgeBase::Answer::AttachmentsController < ApplicationController
|
class KnowledgeBase::Answer::AttachmentsController < ApplicationController
|
||||||
prepend_before_action :authentication_check
|
prepend_before_action :authentication_check
|
||||||
prepend_before_action :authorize!
|
before_action :authorize!
|
||||||
|
|
||||||
before_action :fetch_answer
|
before_action :fetch_answer
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -18,6 +17,14 @@ class KnowledgeBase::Answer::AttachmentsController < ApplicationController
|
||||||
render json: @answer.assets({})
|
render json: @answer.assets({})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clone_to_form
|
||||||
|
new_attachments = @answer.clone_attachments('UploadCache', params[:form_id], only_attached_attachments: true)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
attachments: new_attachments
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_answer
|
def fetch_answer
|
||||||
|
|
79
app/models/concerns/can_clone_attachments.rb
Normal file
79
app/models/concerns/can_clone_attachments.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
module CanCloneAttachments
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
clone existing attachments of article to the target object
|
||||||
|
|
||||||
|
article_parent = Ticket::Article.find(123)
|
||||||
|
article_new = Ticket::Article.find(456)
|
||||||
|
|
||||||
|
attached_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
|
||||||
|
|
||||||
|
inline_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true)
|
||||||
|
|
||||||
|
returns
|
||||||
|
|
||||||
|
[attachment1, attachment2, ...]
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def clone_attachments(object_type, object_id, options = {})
|
||||||
|
existing_attachments = Store.list(
|
||||||
|
object: object_type,
|
||||||
|
o_id: object_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_html_content = false
|
||||||
|
if content_type.present? && content_type =~ %r{text/html}i
|
||||||
|
is_html_content = true
|
||||||
|
end
|
||||||
|
|
||||||
|
new_attachments = []
|
||||||
|
attachments.each do |new_attachment|
|
||||||
|
next if new_attachment.preferences['content-alternative'] == true
|
||||||
|
|
||||||
|
# only_attached_attachments mode is used by apply attached attachments to forwared article
|
||||||
|
if options[:only_attached_attachments] == true
|
||||||
|
if is_html_content == true
|
||||||
|
|
||||||
|
content_id = new_attachment.preferences['Content-ID'] || new_attachment.preferences['content_id']
|
||||||
|
next if content_id.present? && body.present? && body.match?(/#{Regexp.quote(content_id)}/i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# only_inline_attachments mode is used when quoting HTML mail with #{article.body_as_html}
|
||||||
|
if options[:only_inline_attachments] == true
|
||||||
|
next if is_html_content == false
|
||||||
|
next if body.blank?
|
||||||
|
|
||||||
|
content_disposition = new_attachment.preferences['Content-Disposition'] || new_attachment.preferences['content_disposition']
|
||||||
|
next if content_disposition.present? && content_disposition !~ /inline/
|
||||||
|
|
||||||
|
content_id = new_attachment.preferences['Content-ID'] || new_attachment.preferences['content_id']
|
||||||
|
next if content_id.blank?
|
||||||
|
next if !body.match?(/#{Regexp.quote(content_id)}/i)
|
||||||
|
end
|
||||||
|
|
||||||
|
already_added = false
|
||||||
|
existing_attachments.each do |existing_attachment|
|
||||||
|
next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size
|
||||||
|
|
||||||
|
already_added = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
next if already_added == true
|
||||||
|
|
||||||
|
file = Store.add(
|
||||||
|
object: object_type,
|
||||||
|
o_id: object_id,
|
||||||
|
data: new_attachment.content,
|
||||||
|
filename: new_attachment.filename,
|
||||||
|
preferences: new_attachment.preferences,
|
||||||
|
)
|
||||||
|
new_attachments.push file
|
||||||
|
end
|
||||||
|
|
||||||
|
new_attachments
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,6 +5,7 @@ class KnowledgeBase::Answer < ApplicationModel
|
||||||
include CanBePublished
|
include CanBePublished
|
||||||
include HasKnowledgeBaseAttachmentPermissions
|
include HasKnowledgeBaseAttachmentPermissions
|
||||||
include ChecksKbClientNotification
|
include ChecksKbClientNotification
|
||||||
|
include CanCloneAttachments
|
||||||
|
|
||||||
AGENT_ALLOWED_ATTRIBUTES = %i[category_id promoted internal_note].freeze
|
AGENT_ALLOWED_ATTRIBUTES = %i[category_id promoted internal_note].freeze
|
||||||
AGENT_ALLOWED_NESTED_RELATIONS = %i[translations].freeze
|
AGENT_ALLOWED_NESTED_RELATIONS = %i[translations].freeze
|
||||||
|
@ -96,6 +97,11 @@ class KnowledgeBase::Answer < ApplicationModel
|
||||||
Rails.application.routes.url_helpers.knowledge_base_answer_path(category.knowledge_base, self)
|
Rails.application.routes.url_helpers.knowledge_base_answer_path(category.knowledge_base, self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# required by CanCloneAttachments
|
||||||
|
def content_type
|
||||||
|
'text/html'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def reordering_callback
|
def reordering_callback
|
||||||
|
|
|
@ -6,6 +6,7 @@ class Ticket::Article < ApplicationModel
|
||||||
include HasHistory
|
include HasHistory
|
||||||
include ChecksHtmlSanitized
|
include ChecksHtmlSanitized
|
||||||
include CanCsvImport
|
include CanCsvImport
|
||||||
|
include CanCloneAttachments
|
||||||
include HasObjectManagerAttributesValidation
|
include HasObjectManagerAttributesValidation
|
||||||
|
|
||||||
include Ticket::Article::Assets
|
include Ticket::Article::Assets
|
||||||
|
@ -134,82 +135,6 @@ returns
|
||||||
new_attachments
|
new_attachments
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
clone existing attachments of article to the target object
|
|
||||||
|
|
||||||
article_parent = Ticket::Article.find(123)
|
|
||||||
article_new = Ticket::Article.find(456)
|
|
||||||
|
|
||||||
attached_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
|
|
||||||
|
|
||||||
inline_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true)
|
|
||||||
|
|
||||||
returns
|
|
||||||
|
|
||||||
[attachment1, attachment2, ...]
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def clone_attachments(object_type, object_id, options = {})
|
|
||||||
existing_attachments = Store.list(
|
|
||||||
object: object_type,
|
|
||||||
o_id: object_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
is_html_content = false
|
|
||||||
if content_type.present? && content_type =~ %r{text/html}i
|
|
||||||
is_html_content = true
|
|
||||||
end
|
|
||||||
|
|
||||||
new_attachments = []
|
|
||||||
attachments.each do |new_attachment|
|
|
||||||
next if new_attachment.preferences['content-alternative'] == true
|
|
||||||
|
|
||||||
# only_attached_attachments mode is used by apply attached attachments to forwared article
|
|
||||||
if options[:only_attached_attachments] == true
|
|
||||||
if is_html_content == true
|
|
||||||
|
|
||||||
content_id = new_attachment.preferences['Content-ID'] || new_attachment.preferences['content_id']
|
|
||||||
next if content_id.present? && body.present? && body.match?(/#{Regexp.quote(content_id)}/i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# only_inline_attachments mode is used when quoting HTML mail with #{article.body_as_html}
|
|
||||||
if options[:only_inline_attachments] == true
|
|
||||||
next if is_html_content == false
|
|
||||||
next if body.blank?
|
|
||||||
|
|
||||||
content_disposition = new_attachment.preferences['Content-Disposition'] || new_attachment.preferences['content_disposition']
|
|
||||||
next if content_disposition.present? && content_disposition !~ /inline/
|
|
||||||
|
|
||||||
content_id = new_attachment.preferences['Content-ID'] || new_attachment.preferences['content_id']
|
|
||||||
next if content_id.blank?
|
|
||||||
next if !body.match?(/#{Regexp.quote(content_id)}/i)
|
|
||||||
end
|
|
||||||
|
|
||||||
already_added = false
|
|
||||||
existing_attachments.each do |existing_attachment|
|
|
||||||
next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size
|
|
||||||
|
|
||||||
already_added = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
next if already_added == true
|
|
||||||
|
|
||||||
file = Store.add(
|
|
||||||
object: object_type,
|
|
||||||
o_id: object_id,
|
|
||||||
data: new_attachment.content,
|
|
||||||
filename: new_attachment.filename,
|
|
||||||
preferences: new_attachment.preferences,
|
|
||||||
)
|
|
||||||
new_attachments.push file
|
|
||||||
end
|
|
||||||
|
|
||||||
new_attachments
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.last_customer_agent_article(ticket_id)
|
def self.last_customer_agent_article(ticket_id)
|
||||||
sender = Ticket::Article::Sender.lookup(name: 'System')
|
sender = Ticket::Article::Sender.lookup(name: 'System')
|
||||||
Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order(created_at: :desc).first
|
Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order(created_at: :desc).first
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
class Controllers::KnowledgeBase::Answer::AttachmentsControllerPolicy < Controllers::ApplicationControllerPolicy
|
class Controllers::KnowledgeBase::Answer::AttachmentsControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||||
|
permit! :clone_to_form, to: 'knowledge_base.*'
|
||||||
default_permit!('knowledge_base.editor')
|
default_permit!('knowledge_base.editor')
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,7 +49,11 @@ Zammad::Application.routes.draw do
|
||||||
only: %i[create update show destroy],
|
only: %i[create update show destroy],
|
||||||
concerns: :has_publishing do
|
concerns: :has_publishing do
|
||||||
|
|
||||||
resources :attachments, controller: 'knowledge_base/answer/attachments', only: %i[create destroy]
|
resources :attachments, controller: 'knowledge_base/answer/attachments', only: %i[create destroy] do
|
||||||
|
collection do
|
||||||
|
post :clone_to_form
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,5 +18,21 @@ FactoryBot.define do
|
||||||
translation_traits { [:with_video] }
|
translation_traits { [:with_video] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :with_attachment do
|
||||||
|
transient do
|
||||||
|
attachment { File.open('spec/fixtures/upload/hello_world.txt') }
|
||||||
|
end
|
||||||
|
|
||||||
|
after(:create) do |answer, context|
|
||||||
|
Store.add(
|
||||||
|
object: answer.class.name,
|
||||||
|
o_id: answer.id,
|
||||||
|
data: context.attachment.read,
|
||||||
|
filename: File.basename(context.attachment.path),
|
||||||
|
preferences: {}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,4 +11,11 @@ RSpec.describe KnowledgeBase::Answer, type: :model, current_user_id: 1 do
|
||||||
|
|
||||||
it { is_expected.not_to validate_presence_of(:category_id) }
|
it { is_expected.not_to validate_presence_of(:category_id) }
|
||||||
it { is_expected.to belong_to(:category) }
|
it { is_expected.to belong_to(:category) }
|
||||||
|
it { expect(kb_answer.attachments).to be_blank }
|
||||||
|
|
||||||
|
context 'with attachment' do
|
||||||
|
subject(:kb_answer) { create(:knowledge_base_answer, :with_attachment) }
|
||||||
|
|
||||||
|
it { expect(kb_answer.attachments).to be_present }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'KnowledgeBase answer attachments cloning', type: :request, authenticated_as: :agent_user do
|
||||||
|
include_context 'basic Knowledge Base' do
|
||||||
|
before do
|
||||||
|
published_answer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'copies to given UploadCache' do
|
||||||
|
form_id = Random.rand(999..9999)
|
||||||
|
endpoint = "/api/v1/knowledge_bases/#{knowledge_base.id}/answers/#{published_answer.id}/attachments/clone_to_form"
|
||||||
|
params = { form_id: form_id }
|
||||||
|
|
||||||
|
expect { post endpoint, params: params }
|
||||||
|
.to change { Store.list(object: 'UploadCache', o_id: form_id).count }
|
||||||
|
.from(0)
|
||||||
|
.to(1)
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,7 +20,7 @@ RSpec.shared_context 'basic Knowledge Base', current_user_id: 1 do
|
||||||
end
|
end
|
||||||
|
|
||||||
let :published_answer do
|
let :published_answer do
|
||||||
create(:knowledge_base_answer, category: category, published_at: 1.week.ago)
|
create(:knowledge_base_answer, :with_attachment, category: category, published_at: 1.week.ago)
|
||||||
end
|
end
|
||||||
|
|
||||||
let :published_answer_with_video do
|
let :published_answer_with_video do
|
||||||
|
|
43
spec/system/ticket/inserting_knowledge_base_answer_spec.rb
Normal file
43
spec/system/ticket/inserting_knowledge_base_answer_spec.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'inserting Knowledge Base answer', type: :system, authenticated: true, searchindex: true do
|
||||||
|
include_context 'basic Knowledge Base'
|
||||||
|
|
||||||
|
let(:field) { find(:richtext) }
|
||||||
|
let(:target_translation) { published_answer.translations.first }
|
||||||
|
|
||||||
|
before do
|
||||||
|
configure_elasticsearch(required: true, rebuild: true) do
|
||||||
|
published_answer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'adds text' do
|
||||||
|
open_page
|
||||||
|
insert_kb_answer(target_translation, field)
|
||||||
|
|
||||||
|
expect(field).to have_text target_translation.content.body
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'attaches file' do
|
||||||
|
open_page
|
||||||
|
insert_kb_answer(target_translation, field)
|
||||||
|
|
||||||
|
within(:active_content) do
|
||||||
|
expect(page).to have_css '.attachments .attachment--row'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def open_page
|
||||||
|
visit 'ticket/create'
|
||||||
|
end
|
||||||
|
|
||||||
|
def insert_kb_answer(translation, target_field)
|
||||||
|
target_field.send_keys('??')
|
||||||
|
translation.title.slice(0, 3).split('').each { |letter| target_field.send_keys(letter) }
|
||||||
|
|
||||||
|
find(:text_module, translation.id).click
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue