Fixes #2644 - Including Knowledge Base Answers into Ticket Articles doesn't attach Attachments

This commit is contained in:
Mantas Masalskis 2020-04-20 11:47:45 +02:00 committed by Thorsten Eckel
parent 3306de2d1f
commit 880f1d5e3a
14 changed files with 226 additions and 85 deletions

View file

@ -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')

View file

@ -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)

View 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.Ajax.request({
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){ App.Utils.htmlImage2DataUrlAsync(body, function(output){
callback(output) callback(output, translation.parent().attachments)
})
},
error: function(xhr) {
callback('')
}
}) })
}, },
error: function(xhr) { error: function(xhr) {

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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