Fixed bug: Regular Model list imports (e.g. Tickets) sequences fail import because of attribute that is not required and can't be provided.
This commit is contained in:
parent
022efe7921
commit
1287e9968e
4 changed files with 159 additions and 4 deletions
61
lib/assets_set.rb
Normal file
61
lib/assets_set.rb
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
require_dependency 'assets_set/proxy'
|
||||||
|
|
||||||
|
# The name AssetsSet combines the Assets term in Zammad with the Set class from the Ruby StdLib.
|
||||||
|
# Zammad Assets serve the purpose to limit requests to the REST API. For a requested object all
|
||||||
|
# related and relevant objects are collected recursively and then send to the client in one go.
|
||||||
|
# A Ruby Set implements a collection of unordered values with no duplicates.
|
||||||
|
#
|
||||||
|
# This class implements a collection of Zammad Assets with no duplicates.
|
||||||
|
# This is done by having two structures:
|
||||||
|
#
|
||||||
|
# 1st: The actual Assets Hash (e.g. `assets[model_name][object_id] = object_attributes`)
|
||||||
|
# 2nd: A lookup table for keeping track which elements were added to the actual Assets Hash
|
||||||
|
#
|
||||||
|
# The actual Assets Hash should be flushed after sending it to the client. This will keep the
|
||||||
|
# lookup table untouched. The next time a object that was already send to the client
|
||||||
|
# should be added to the actual Assets Hash the lookup table will recognize the object
|
||||||
|
# and will prevent the addition to the actual Assets Hash.
|
||||||
|
# This way Assets will be send only once to the client for the lifetime of a AssetsSet instance.
|
||||||
|
class AssetsSet < SimpleDelegator
|
||||||
|
|
||||||
|
# This method overwrites the SimpleDelegator initializer
|
||||||
|
# to be able to have the actual Assets Hash as an optional argument.
|
||||||
|
def initialize(assets = {})
|
||||||
|
super(assets)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method initializes the the global lookup table.
|
||||||
|
# Each (accessed) Model gets it's own sub structure in it.
|
||||||
|
def lookup_table
|
||||||
|
@lookup_table ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method flushes the actual Assets Hash.
|
||||||
|
def flush
|
||||||
|
__setobj__({})
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method intercepts `assets[model_name]` calls by registering a AssetsSet::Proxy.
|
||||||
|
# Instead of creating an entry in the actual Assets Hash a AssetsSet::Proxy.
|
||||||
|
# See AssetsSet::Proxy for further information.
|
||||||
|
# Existing proxies will be reused.
|
||||||
|
def [](model)
|
||||||
|
__getobj__[model] ||= proxy(model)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method is used to convert the AssetsSet into a regular Hash which then can be send to the client.
|
||||||
|
# It cleans up empty Model sub structures in the internal structure.
|
||||||
|
def to_h
|
||||||
|
super.delete_if { |_model, assets| assets.blank? }.transform_values!(&:to_h)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def proxy(model)
|
||||||
|
lookup_table[model] ||= {}
|
||||||
|
|
||||||
|
::AssetsSet::Proxy.new.tap do |proxy|
|
||||||
|
proxy.lookup_table = lookup_table[model]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
50
lib/assets_set/proxy.rb
Normal file
50
lib/assets_set/proxy.rb
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# This class defines a Proxy for accessing objects inside of a AssetsSet Model sub structure.
|
||||||
|
#
|
||||||
|
# The Zammad Assets logic works by having an Assets Hash that contains a sub structure for
|
||||||
|
# each model that is relevant. Before an object gets added to the Model sub structure the
|
||||||
|
# Model sub structure is checked for the presence of the object by its id. If the object is
|
||||||
|
# already present it will be skipped. However, if the model is not yet present in the matching
|
||||||
|
# Model sub structure the Zammad Assets will be collected and added.
|
||||||
|
#
|
||||||
|
# We make use of this lookup / add if not present functionality by intercepting calls to the
|
||||||
|
# actual Assets Hash.
|
||||||
|
#
|
||||||
|
# If an object is not yet present in the Model sub structure and should be added
|
||||||
|
# it will added to the lookup table of the AssetsSet first. After that the object will
|
||||||
|
# be stored to the actual Assets Hash.
|
||||||
|
#
|
||||||
|
# The next time a lookup for an object in the Model sub structure is performed it will find the
|
||||||
|
# actual object data. However, if the actual Assets Hash is send to the client and the AssetsSet
|
||||||
|
# is flushed the lookup table is still present. The next time a lookup for an object in the
|
||||||
|
# Model sub is performed it will NOT find the actual object data. In this case a fall back
|
||||||
|
# to the lookup table will be performed which will will just return `true` to satisfy the
|
||||||
|
# "is present" condition
|
||||||
|
class AssetsSet < SimpleDelegator
|
||||||
|
class Proxy < SimpleDelegator
|
||||||
|
|
||||||
|
attr_accessor :lookup_table
|
||||||
|
|
||||||
|
# This method overwrites the SimpleDelegator initializer
|
||||||
|
# to be able to have the actual Assets Hash as an optional argument.
|
||||||
|
def initialize(assets = {})
|
||||||
|
super(assets)
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method intercepts `assets[model_name][object_id]` calls and return the actual objects data.
|
||||||
|
# If the object it not present the lookup table of the AssetsSet will be queried.
|
||||||
|
# If the object was present before a previously performed `flush` it will return true and
|
||||||
|
# satisfy the "is present" condition in the `assets` of the given model.
|
||||||
|
# If the object is not and never was present the `assets` logic will be performed as normal.
|
||||||
|
def [](id)
|
||||||
|
__getobj__[id] || lookup_table[id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# This method intercepts `assets[model_name][object_id] = object_attributes` calls.
|
||||||
|
# It stores an entry in the lookup the of the AssetsSet and then performs the regular call
|
||||||
|
# to store the data in the actual Assets Hash Model sub structure.
|
||||||
|
def []=(id, _value)
|
||||||
|
lookup_table[id] = true
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -86,6 +86,7 @@ class Sessions::Backend::TicketOverviewList < Sessions::Backend::Base
|
||||||
|
|
||||||
# push overviews
|
# push overviews
|
||||||
results = []
|
results = []
|
||||||
|
assets = AssetsSet.new
|
||||||
index_and_lists.each do |data|
|
index_and_lists.each do |data|
|
||||||
|
|
||||||
# do not deliver unchanged lists
|
# do not deliver unchanged lists
|
||||||
|
@ -93,7 +94,6 @@ class Sessions::Backend::TicketOverviewList < Sessions::Backend::Base
|
||||||
|
|
||||||
@last_overview[data[:overview][:id]] = [data[:tickets], data[:overview]]
|
@last_overview[data[:overview][:id]] = [data[:tickets], data[:overview]]
|
||||||
|
|
||||||
assets = {}
|
|
||||||
overview = Overview.lookup(id: data[:overview][:id])
|
overview = Overview.lookup(id: data[:overview][:id])
|
||||||
next if !overview
|
next if !overview
|
||||||
|
|
||||||
|
@ -108,7 +108,8 @@ class Sessions::Backend::TicketOverviewList < Sessions::Backend::Base
|
||||||
|
|
||||||
assets = asset_push(ticket, assets)
|
assets = asset_push(ticket, assets)
|
||||||
end
|
end
|
||||||
data[:assets] = assets
|
|
||||||
|
data[:assets] = assets.to_h
|
||||||
|
|
||||||
if !@client
|
if !@client
|
||||||
result = {
|
result = {
|
||||||
|
@ -125,6 +126,8 @@ class Sessions::Backend::TicketOverviewList < Sessions::Backend::Base
|
||||||
data: data,
|
data: data,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
assets.flush
|
||||||
end
|
end
|
||||||
return results if !@client
|
return results if !@client
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,50 @@ RSpec.describe Sessions::Backend::TicketOverviewList do
|
||||||
.to match_array(Ticket::Overviews.all(current_user: admin).map(&:name))
|
.to match_array(Ticket::Overviews.all(current_user: admin).map(&:name))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'is optimized to not send duplicate asset entries over all events' do
|
||||||
|
collection_assets = collection.push.map { |hash| hash[:data][:assets] }
|
||||||
|
|
||||||
|
# match all event assets against the assets of the other events
|
||||||
|
# and make sure that each asset entry is unique over all events assets
|
||||||
|
unique_asssets = collection_assets.each_with_index.all? do |lookup_assets, lookup_index|
|
||||||
|
|
||||||
|
collection_assets.each_with_index.none? do |comparison_assets, comparison_index|
|
||||||
|
|
||||||
|
# skip assets comparison for same event
|
||||||
|
next if comparison_index == lookup_index
|
||||||
|
|
||||||
|
# check that none of the lookup_assets assets is present
|
||||||
|
# in the comparison_assets
|
||||||
|
lookup_assets.keys.any? do |model|
|
||||||
|
# skip Models that are only present in our lookup_assets entry
|
||||||
|
next if !comparison_assets.key?(model)
|
||||||
|
|
||||||
|
# check if there are no intersect Model record IDs
|
||||||
|
# aka IDs present in both hashes
|
||||||
|
intersection_ids = lookup_assets[model].keys & comparison_assets[model].keys
|
||||||
|
intersection_ids.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(unique_asssets).to be(true)
|
||||||
|
end
|
||||||
|
|
||||||
it 'includes FE assets for all overviews and tickets not pushed in the last two hours' do
|
it 'includes FE assets for all overviews and tickets not pushed in the last two hours' do
|
||||||
expect(collection.push.map { |hash| hash[:data][:assets] })
|
|
||||||
.to match_array(Ticket::Overviews.all(current_user: admin).map { |overview| overview.assets({}) })
|
# ATTENTION: we can't compare the arrays of assets directly
|
||||||
|
# because the Ticket::Overviews backend contain an optimization logic that sends an asset entry only once
|
||||||
|
# while the Sessions::Backend::* classes results contain all assets for each entry.
|
||||||
|
# Therefore we merge all assets for each of the both arrays to have two big Hashes that contains all assets.
|
||||||
|
# See previous example for the matching spec.
|
||||||
|
|
||||||
|
collection_assets = collection.push.map { |hash| hash[:data][:assets] }
|
||||||
|
collection_assets_merged = collection_assets.each_with_object({}) { |assets, result| result.deep_merge!(assets) }
|
||||||
|
|
||||||
|
overviews_all_assets = Ticket::Overviews.all(current_user: admin).map { |overview| overview.assets({}) }
|
||||||
|
overviews_all_assets_merged = overviews_all_assets.each_with_object({}) { |assets, result| result.deep_merge!(assets) }
|
||||||
|
|
||||||
|
expect(collection_assets_merged).to eq(overviews_all_assets_merged)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when called twice, with no changes to Ticket and Overview tables' do
|
context 'when called twice, with no changes to Ticket and Overview tables' do
|
||||||
|
|
Loading…
Reference in a new issue