Fixes #4026 - Knowledge Base on-the-fly updating does not work

This commit is contained in:
Mantas Masalskis 2022-04-01 11:41:19 +02:00
parent 2844b6d95d
commit 0a3e7628b4
8 changed files with 192 additions and 36 deletions

View file

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

View file

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

View file

@ -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|
end notify(user, object, payload)
def build_data(object, event)
timestamp = event == :destroy ? Time.zone.now : object.updated_at
url = event == :destroy ? nil : object.try(:api_url)
{
class: object.class.to_s,
event: event,
id: object.id,
timestamp: timestamp,
url: url
}
end
def needs_editor?(object)
case object
when KnowledgeBase::Answer
object.can_be_published_aasm.draft?
when KnowledgeBase::Category
!object.internal_content?
else
false
end end
end end
def notify(user, payload) def build_data(object)
PushMessages.send_to(user.id, payload) {
class: object.class.name,
id: object.id,
timestamp: object.updated_at,
url: object.try(:api_url)
}
end end
def users_for(permission_suffix) def notify(user, object, payload)
return if !user.permissions? 'knowledge_base.*'
Pundit.authorize user, object, :show?
PushMessages.send_to(user.id, payload)
rescue Pundit::NotAuthorizedError
# do nothing if user is not authorized to access
end
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

View file

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

View 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

View file

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

View file

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

View file

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