Fixes #4026 - Knowledge Base on-the-fly updating does not work
This commit is contained in:
parent
2844b6d95d
commit
0a3e7628b4
8 changed files with 192 additions and 36 deletions
|
@ -58,6 +58,10 @@ class App.KnowledgeBaseContentController extends App.Controller
|
||||||
App.KnowledgeBaseFormController.compareParams(remoteParams, @startingParams)
|
App.KnowledgeBaseFormController.compareParams(remoteParams, @startingParams)
|
||||||
|
|
||||||
objectRefreshed: ->
|
objectRefreshed: ->
|
||||||
|
if !@object.exists()
|
||||||
|
@parentController.renderNotAvailableAnymore()
|
||||||
|
return
|
||||||
|
|
||||||
@renderAvailabilityWidgets()
|
@renderAvailabilityWidgets()
|
||||||
|
|
||||||
if @remoteDidntChangeSinceStart()
|
if @remoteDidntChangeSinceStart()
|
||||||
|
|
|
@ -6,7 +6,7 @@ class KnowledgeBasesController < KnowledgeBase::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def visible_ids
|
def visible_ids
|
||||||
render json: KnowledgeBase::InternalAssets.new(current_user).visible_ids
|
render json: calculate_visible_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -22,6 +22,20 @@ class KnowledgeBasesController < KnowledgeBase::BaseController
|
||||||
public_assets
|
public_assets
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def calculate_visible_ids
|
||||||
|
if KnowledgeBase.granular_permissions? && kb_permissions?
|
||||||
|
return KnowledgeBase::InternalAssets.new(current_user).visible_ids
|
||||||
|
end
|
||||||
|
|
||||||
|
if kb_permission_editor?
|
||||||
|
editor_assets_visible_ids
|
||||||
|
elsif kb_permission_reader?
|
||||||
|
reader_assets_visible_ids
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def kb_permissions?
|
def kb_permissions?
|
||||||
current_user&.permissions?(%w[knowledge_base.editor knowledge_base.reader])
|
current_user&.permissions?(%w[knowledge_base.editor knowledge_base.reader])
|
||||||
end
|
end
|
||||||
|
@ -63,6 +77,13 @@ class KnowledgeBasesController < KnowledgeBase::BaseController
|
||||||
assets
|
assets
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def editor_assets_visible_ids
|
||||||
|
{
|
||||||
|
answer_ids: KnowledgeBase::Answer.pluck(:id),
|
||||||
|
category_ids: KnowledgeBase::Category.pluck(:id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def reader_assets(answer_translation_content_ids)
|
def reader_assets(answer_translation_content_ids)
|
||||||
assets = [
|
assets = [
|
||||||
KnowledgeBase,
|
KnowledgeBase,
|
||||||
|
@ -96,6 +117,13 @@ class KnowledgeBasesController < KnowledgeBase::BaseController
|
||||||
assets
|
assets
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reader_assets_visible_ids
|
||||||
|
{
|
||||||
|
answer_ids: KnowledgeBase::Answer.internal.pluck(:id),
|
||||||
|
category_ids: KnowledgeBase::Category.pluck(:id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# assets for users who don't have KB permissions
|
# assets for users who don't have KB permissions
|
||||||
def public_assets
|
def public_assets
|
||||||
return [] if !Setting.get('kb_active_publicly')
|
return [] if !Setting.get('kb_active_publicly')
|
||||||
|
|
|
@ -12,49 +12,39 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
||||||
object = klass_name.constantize.find_by(id: object_id)
|
object = klass_name.constantize.find_by(id: object_id)
|
||||||
return if object.blank?
|
return if object.blank?
|
||||||
|
|
||||||
level = needs_editor?(object) ? 'editor' : '*'
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
event: 'kb_data_changed',
|
event: 'kb_data_changed',
|
||||||
data: build_data(object, event)
|
data: build_data(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
users_for(level).each { |user| notify(user, payload) }
|
active_users.each do |user|
|
||||||
|
notify(user, object, payload)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_data(object, event)
|
def build_data(object)
|
||||||
timestamp = event == :destroy ? Time.zone.now : object.updated_at
|
|
||||||
url = event == :destroy ? nil : object.try(:api_url)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
class: object.class.to_s,
|
class: object.class.name,
|
||||||
event: event,
|
|
||||||
id: object.id,
|
id: object.id,
|
||||||
timestamp: timestamp,
|
timestamp: object.updated_at,
|
||||||
url: url
|
url: object.try(:api_url)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def needs_editor?(object)
|
def notify(user, object, payload)
|
||||||
case object
|
return if !user.permissions? 'knowledge_base.*'
|
||||||
when KnowledgeBase::Answer
|
|
||||||
object.can_be_published_aasm.draft?
|
Pundit.authorize user, object, :show?
|
||||||
when KnowledgeBase::Category
|
|
||||||
!object.internal_content?
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def notify(user, payload)
|
|
||||||
PushMessages.send_to(user.id, payload)
|
PushMessages.send_to(user.id, payload)
|
||||||
|
rescue Pundit::NotAuthorizedError
|
||||||
|
# do nothing if user is not authorized to access
|
||||||
end
|
end
|
||||||
|
|
||||||
def users_for(permission_suffix)
|
def active_users
|
||||||
Sessions
|
Sessions
|
||||||
.sessions
|
.sessions
|
||||||
.filter_map { |client_id| Sessions.get(client_id)&.dig(:user, 'id') }
|
.filter_map { |client_id| Sessions.get(client_id)&.dig(:user, 'id') }
|
||||||
.filter_map { |user_id| User.find_by(id: user_id) }
|
.filter_map { |user_id| User.find_by(id: user_id) }
|
||||||
.select { |user| user.permissions? "knowledge_base.#{permission_suffix}" }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,6 +30,10 @@ module ChecksKbClientNotification
|
||||||
def notify_kb_clients_after
|
def notify_kb_clients_after
|
||||||
return if self.class.notify_kb_clients_suspend?
|
return if self.class.notify_kb_clients_suspend?
|
||||||
|
|
||||||
|
# do not leak details about deleted items. See ChecksKbClientVisibilityJob
|
||||||
|
# after_commit does not allow on: :touch
|
||||||
|
return if destroyed?
|
||||||
|
|
||||||
ChecksKbClientNotificationJob.perform_later(self.class.name, id)
|
ChecksKbClientNotificationJob.perform_later(self.class.name, id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
40
spec/jobs/checks_kb_client_notification_job_spec.rb
Normal file
40
spec/jobs/checks_kb_client_notification_job_spec.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ChecksKbClientNotificationJob, type: :job, performs_jobs: true do
|
||||||
|
include_context 'basic Knowledge Base'
|
||||||
|
|
||||||
|
describe 'pushes to users who have access' do
|
||||||
|
let(:admin) { create(:admin) }
|
||||||
|
let(:agent) { create(:agent) }
|
||||||
|
let(:customer) { create(:customer) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(described_class) # rubocop:disable RSpec/AnyInstance
|
||||||
|
.to receive(:active_users)
|
||||||
|
.and_return([admin, agent, customer])
|
||||||
|
|
||||||
|
allow(PushMessages).to receive(:send_to)
|
||||||
|
|
||||||
|
described_class.perform_now 'KnowledgeBase::Answer', answer.id
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'message pushed to given users' do |prefix, **args|
|
||||||
|
context "when answer is #{prefix}" do
|
||||||
|
let(:answer) { send("#{prefix}_answer") }
|
||||||
|
|
||||||
|
args.each do |key, value|
|
||||||
|
it "#{key} #{value ? 'is' : 'not'} notified" do
|
||||||
|
expect(PushMessages).send(value ? :to : :not_to, have_received(:send_to).with(send(key).id, any_args))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'message pushed to given users', 'published', admin: true, agent: true, customer: false
|
||||||
|
include_examples 'message pushed to given users', 'internal', admin: true, agent: true, customer: false
|
||||||
|
include_examples 'message pushed to given users', 'draft', admin: true, agent: false, customer: false
|
||||||
|
include_examples 'message pushed to given users', 'archived', admin: true, agent: false, customer: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ RSpec.shared_examples 'ChecksKbClientNotification' do
|
||||||
before { subject }
|
before { subject }
|
||||||
|
|
||||||
it 'on creation' do
|
it 'on creation' do
|
||||||
expect(ChecksKbClientNotificationJob).to have_been_enqueued.at_least(:once) # some object have associations that triggers touch job after creation
|
expect(ChecksKbClientNotificationJob).to have_been_enqueued.with(subject.class.name, subject.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'after initial notifications are cleared' do
|
context 'after initial notifications are cleared' do
|
||||||
|
@ -13,17 +13,17 @@ RSpec.shared_examples 'ChecksKbClientNotification' do
|
||||||
|
|
||||||
it 'on update' do
|
it 'on update' do
|
||||||
subject.update(updated_at: Time.zone.now)
|
subject.update(updated_at: Time.zone.now)
|
||||||
expect(ChecksKbClientNotificationJob).to have_been_enqueued.at_least(:once) # some object have associations that triggers touch job after creation
|
expect(ChecksKbClientNotificationJob).to have_been_enqueued.with(subject.class.name, subject.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'on touch' do
|
it 'on touch' do
|
||||||
subject.touch
|
subject.touch
|
||||||
expect(ChecksKbClientNotificationJob).to have_been_enqueued.at_least(:once) # some object have associations that triggers touch job after creation
|
expect(ChecksKbClientNotificationJob).to have_been_enqueued.with(subject.class.name, subject.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'on destroy' do
|
it 'on destroy' do
|
||||||
subject.destroy
|
subject.destroy
|
||||||
expect(ChecksKbClientNotificationJob).to have_been_enqueued.at_least(:once) # some object have associations that triggers touch job after creation
|
expect(ChecksKbClientNotificationJob).not_to have_been_enqueued.with(subject.class.name, subject.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'notifications be disabled' do
|
it 'notifications be disabled' do
|
||||||
|
|
|
@ -124,4 +124,50 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'deleted by another user' do
|
||||||
|
before do
|
||||||
|
visit "#knowledge_base/#{knowledge_base.id}/locale/#{primary_locale.system_locale.locale}/answer/#{published_answer.id}/edit"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows not available', performs_jobs: true do
|
||||||
|
find(:active_content, text: published_answer.translations.first.title)
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
published_answer.destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_text('The page is not available anymore')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updated by another user' do
|
||||||
|
before do
|
||||||
|
ensure_websocket do
|
||||||
|
visit "#knowledge_base/#{knowledge_base.id}/locale/#{primary_locale.system_locale.locale}/answer/#{published_answer.id}/edit"
|
||||||
|
end
|
||||||
|
|
||||||
|
travel 1.minute
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows new content', performs_jobs: true do
|
||||||
|
find(:active_content, text: published_answer.translations.first.title)
|
||||||
|
|
||||||
|
accept_prompt do
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
Transaction.execute do
|
||||||
|
published_answer.translations.first.update! title: 'new title'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_text('new title')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -48,4 +48,48 @@ RSpec.describe 'Knowledge Base Locale Answer Read', type: :system, authenticated
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'deleted by another user' do
|
||||||
|
before do
|
||||||
|
visit "#knowledge_base/#{knowledge_base.id}/locale/#{locale_name}/answer/#{published_answer.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows not available', performs_jobs: true do
|
||||||
|
find(:active_content, text: published_answer.translations.first.title)
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
published_answer.destroy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_text('The page is not available anymore')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'updated by another user' do
|
||||||
|
before do
|
||||||
|
ensure_websocket do
|
||||||
|
visit "#knowledge_base/#{knowledge_base.id}/locale/#{locale_name}/answer/#{published_answer.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
travel 1.minute
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows new content', performs_jobs: true do
|
||||||
|
find(:active_content, text: published_answer.translations.first.title)
|
||||||
|
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
Transaction.execute do
|
||||||
|
published_answer.translations.first.update! title: 'new title'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
within :active_content do
|
||||||
|
expect(page).to have_text('new title')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue