mirror of
https://github.com/mistergibson/gxg-framework.git
synced 2026-08-15 03:46:00 -07:00
Added merge to DetachedHash/PersistedHas & finalized data sync for database
This commit is contained in:
parent
2a32efd9a7
commit
7631cd8c6c
5 changed files with 116 additions and 148 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
Gem::Specification.new do |s|
|
Gem::Specification.new do |s|
|
||||||
s.name = 'gxg-framework'
|
s.name = 'gxg-framework'
|
||||||
s.version = '0.0.69'
|
s.version = '0.0.70'
|
||||||
s.licenses = ['HIPPOCRATIC 2.1', 'https://firstdonoharm.dev/version/2/1/license.html']
|
s.licenses = ['HIPPOCRATIC 2.1', 'https://firstdonoharm.dev/version/2/1/license.html']
|
||||||
s.summary = "GxG Framework"
|
s.summary = "GxG Framework"
|
||||||
s.description = "GxG Framework"
|
s.description = "GxG Framework"
|
||||||
|
|
|
||||||
|
|
@ -3238,10 +3238,10 @@ class Struct
|
||||||
def self.process(the_struct=::Struct.new(nil),&block)
|
def self.process(the_struct=::Struct.new(nil),&block)
|
||||||
new_struct = ::Struct.new(nil)
|
new_struct = ::Struct.new(nil)
|
||||||
if block.respond_to?(:call)
|
if block.respond_to?(:call)
|
||||||
if the_struct.is_any?(::Array, ::Hash, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray)
|
if the_struct.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::PersistedHash, ::GxG::Database::DetachedArray, ::GxG::Database::PersistedArray)
|
||||||
new_struct = the_struct.process!(&block)
|
new_struct = the_struct.process!(&block)
|
||||||
else
|
else
|
||||||
raise ArgumentError, "you must pass a Hash, or an Array, or a ByteArray, or a Set, or a Struct"
|
raise ArgumentError, "you must pass a Hash, or an Array, or a ByteArray, or a Struct"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
new_struct
|
new_struct
|
||||||
|
|
@ -3250,7 +3250,7 @@ class Struct
|
||||||
def self.search(the_struct=::Struct.new(nil),&block)
|
def self.search(the_struct=::Struct.new(nil),&block)
|
||||||
new_struct = ::Struct.new(nil)
|
new_struct = ::Struct.new(nil)
|
||||||
if block.respond_to?(:call)
|
if block.respond_to?(:call)
|
||||||
if the_struct.is_any?(::Array, ::Hash, ::Set, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray)
|
if the_struct.is_any?(::Hash, ::Array, ::Set, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::PersistedHash, ::GxG::Database::DetachedArray, ::GxG::Database::PersistedArray)
|
||||||
new_struct = the_struct.search(&block)
|
new_struct = the_struct.search(&block)
|
||||||
else
|
else
|
||||||
raise ArgumentError, "you must pass a Hash, or an Array, or a ByteArray, or a Set, or a Struct"
|
raise ArgumentError, "you must pass a Hash, or an Array, or a ByteArray, or a Set, or a Struct"
|
||||||
|
|
@ -3502,7 +3502,7 @@ class Hash
|
||||||
last_container = origin
|
last_container = origin
|
||||||
found = false
|
found = false
|
||||||
# tester = {:a=>1, :b=>2, :c=>[0, 5], :testing=>{:d=>4.0, :e=>0.9, :f => nil}}
|
# tester = {:a=>1, :b=>2, :c=>[0, 5], :testing=>{:d=>4.0, :e=>0.9, :f => nil}}
|
||||||
if origin.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::PersistedHash, ::GxG::Database::PersistedArray)
|
if origin.is_any?(::Hash, ::Array, ::Struct, ::GxG::Database::DetachedHash, ::GxG::Database::PersistedHash, ::GxG::Database::DetachedArray, ::GxG::Database::PersistedArray)
|
||||||
origin.process! do |the_value, selector, container|
|
origin.process! do |the_value, selector, container|
|
||||||
if last_container.object_id != container.object_id
|
if last_container.object_id != container.object_id
|
||||||
container_record = find_container.call(container)
|
container_record = find_container.call(container)
|
||||||
|
|
@ -3514,14 +3514,13 @@ class Hash
|
||||||
end
|
end
|
||||||
last_container = container
|
last_container = container
|
||||||
end
|
end
|
||||||
safe_key = selector.to_s
|
# xxx --> not using colon in pathnames.
|
||||||
# xxx
|
|
||||||
# if selector.is_a?(Symbol)
|
# if selector.is_a?(Symbol)
|
||||||
# safe_key = (":" + selector.to_s)
|
# safe_key = (":" + selector.to_s)
|
||||||
# else
|
# else
|
||||||
# safe_key = selector.to_s
|
# safe_key = selector.to_s
|
||||||
# end
|
# end
|
||||||
# xxx
|
safe_key = selector.to_s
|
||||||
safe_key = safe_key.gsub("/","%2f")
|
safe_key = safe_key.gsub("/","%2f")
|
||||||
path_stack.unshift(safe_key)
|
path_stack.unshift(safe_key)
|
||||||
# compare the_value
|
# compare the_value
|
||||||
|
|
|
||||||
|
|
@ -4769,7 +4769,7 @@ module GxG
|
||||||
if existing.version > record[:version]
|
if existing.version > record[:version]
|
||||||
action = :provide
|
action = :provide
|
||||||
else
|
else
|
||||||
if existing.version < record[:version]
|
if existing.version <= record[:version]
|
||||||
action = :update
|
action = :update
|
||||||
else
|
else
|
||||||
action = :ignore
|
action = :ignore
|
||||||
|
|
@ -4790,152 +4790,107 @@ module GxG
|
||||||
formatting[:constraint] = record[:constraint].to_s
|
formatting[:constraint] = record[:constraint].to_s
|
||||||
end
|
end
|
||||||
clean_up_list << existing
|
clean_up_list << existing
|
||||||
existing.get_reservation()
|
existing.wait_for_reservation(100.0)
|
||||||
unless existing.write_permission?() && existing.write_reserved?()
|
unless existing.write_permission?() && existing.write_reserved?()
|
||||||
raise Exception, "Failed to secure write permission and write reservation for this credential."
|
raise Exception, "Failed to secure write permission and write reservation for this credential."
|
||||||
end
|
end
|
||||||
existing.title = (record[:title])
|
existing.title = (record[:title])
|
||||||
# ### Update Elements
|
# ### Update Elements
|
||||||
if record[:content].is_a?(::Hash)
|
# ???
|
||||||
# Review : What to do / how to handle formatted existing Hashes??
|
# Formatted/Constrained or Freeform?
|
||||||
unless existing.format
|
if formatting[:format] || formatting[:constraint]
|
||||||
# Eliminate keys that the imported record does not have. (tracking structural changes)
|
# Formatted
|
||||||
|
if existing.is_a?(::GxG::Database::PersistedHash)
|
||||||
|
if existing.format
|
||||||
|
the_format = existing.format()
|
||||||
|
if existing.format() != formatting[:format]
|
||||||
|
raise Exception, "Format Mismatch Error --> Aborting Merge for record uuid #{existing.uuid.inspect}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
the_format = formatting[:format]
|
||||||
|
end
|
||||||
|
existing.format = nil
|
||||||
existing.keys.each do |the_key|
|
existing.keys.each do |the_key|
|
||||||
unless record[:content].keys.include?(the_key)
|
unless record[:content].keys.include?(the_key)
|
||||||
temp_object = existing.delete(the_key)
|
temp_object = existing.delete(the_key)
|
||||||
if temp_object.is_any?(::GxG::Database::PersistedArray, ::GxG::Database::PersistedHash)
|
|
||||||
temp_object.destroy()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
record[:content].each_pair do |property, element_record|
|
|
||||||
if element_record[:uuid]
|
|
||||||
# ### Element is Hash or Array
|
|
||||||
element = self.retrieve_by_uuid(element_record[:uuid], credential)
|
|
||||||
unless element
|
|
||||||
# ### Create blank element record and retrieve
|
|
||||||
# Note: since there is no way to create a Persisted Object with a given UUID, this method is used to create the object: object will be updated later.
|
|
||||||
unless @connector[(element_record[:type].to_s.downcase.to_sym)].filter({:uuid => element_record[:uuid].to_s}).count > 0
|
|
||||||
# This prevents no-element-retrieved-due-to-permission issue causing an 'add duplicate uuid object' situation:
|
|
||||||
dbid = @connector[(element_record[:type].to_s.downcase.to_sym)].insert({:uuid => element_record[:uuid].to_s, :title => "Merged Persisted Object"})
|
|
||||||
self.assign_element_permission(element_record[:type].to_s.downcase.to_sym,dbid,credential,{:execute => false, :rename => true, :move => true, :destroy => true, :create => true, :write => true, :read => true})
|
|
||||||
#
|
|
||||||
end
|
|
||||||
element = self.retrieve_by_uuid(element_record[:uuid], credential)
|
|
||||||
end
|
|
||||||
if element
|
|
||||||
# ### Ensure Element Linking
|
|
||||||
# Review : Warning - absence of a :write permission can bugger this up.
|
|
||||||
clean_up_list << element
|
|
||||||
element.get_reservation()
|
|
||||||
unless element.write_permission?() && element.write_reserved?()
|
|
||||||
raise Exception, "Failed to secure write permission and write reservation for this credential."
|
|
||||||
end
|
|
||||||
existing[(property)] = element
|
|
||||||
# element.deactivate ??
|
|
||||||
end
|
|
||||||
# Note: return to processing queue to set its elements
|
|
||||||
queue << element_record
|
|
||||||
else
|
|
||||||
# ### Base Element
|
|
||||||
unless existing.version(property) > element_record[:version]
|
|
||||||
the_content = nil
|
|
||||||
case element_record[:type].to_s.downcase.to_sym
|
|
||||||
when :element_boolean, :element_integer, :element_float
|
|
||||||
the_content = element_record[:content]
|
|
||||||
when :element_bigdecimal
|
|
||||||
the_content = ::BigDecimal.new(element_record[:content].to_s)
|
|
||||||
when :element_datetime
|
|
||||||
the_content = ::DateTime::parse(element_record[:content].to_s)
|
|
||||||
when :element_text
|
|
||||||
the_content = element_record[:content].to_s
|
|
||||||
when :element_binary
|
|
||||||
the_content = ::GxG::ByteArray.new(element_record[:content].to_s.decode64)
|
|
||||||
end
|
|
||||||
existing[(property)] = the_content
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if record[:content].is_a?(::Array)
|
|
||||||
# Eliminate elements that the imported record does not have. (tracking structural changes)
|
|
||||||
if existing.size > record[:content].size
|
|
||||||
((record[:content].size)..(existing.size - 1)).each do |the_indexer|
|
|
||||||
temp_object = existing.delete_at(the_indexer)
|
|
||||||
if temp_object.is_any?(::GxG::Database::PersistedArray, ::GxG::Database::PersistedHash)
|
|
||||||
temp_object.destroy()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
record[:content].each_with_index do |element_record, indexer|
|
|
||||||
if element_record[:uuid]
|
|
||||||
# ### Element is Hash or Array
|
|
||||||
element = self.retrieve_by_uuid(element_record[:uuid], credential)
|
|
||||||
unless element
|
|
||||||
# ### Create blank element record and retrieve
|
|
||||||
# Note: since there is no way to create a Persisted Object with a given UUID, this method is used to create the object: object will be updated later.
|
|
||||||
unless @connector[(element_record[:type].to_s.downcase.to_sym)].filter({:uuid => element_record[:uuid].to_s}).count > 0
|
|
||||||
# This prevents no-element-retrieved-due-to-permission issue causing an 'add duplicate uuid object' situation:
|
|
||||||
dbid = @connector[(element_record[:type].to_s.downcase.to_sym)].insert({:uuid => element_record[:uuid].to_s, :title => "Merged Persisted Object"})
|
|
||||||
self.assign_element_permission(element_record[:type].to_s.downcase.to_sym,dbid,credential,{:execute => false, :rename => true, :move => true, :destroy => true, :create => true, :write => true, :read => true})
|
|
||||||
#
|
|
||||||
end
|
|
||||||
element = self.retrieve_by_uuid(element_record[:uuid], credential)
|
|
||||||
end
|
|
||||||
if element
|
|
||||||
# ### Ensure Element Linking
|
|
||||||
# Review : Warning - absence of a :write permission can bugger this up.
|
|
||||||
clean_up_list << element
|
|
||||||
element.get_reservation()
|
|
||||||
unless element.write_permission?() && element.write_reserved?()
|
|
||||||
raise Exception, "Failed to secure write permission and write reservation for this credential."
|
|
||||||
end
|
|
||||||
existing[(indexer)] = element
|
|
||||||
# element.deactivate ??
|
|
||||||
end
|
|
||||||
# Note: return to processing queue to set its elements
|
|
||||||
queue << element_record
|
|
||||||
else
|
|
||||||
# ### Base Element
|
|
||||||
unless existing.version(indexer) > element_record[:version]
|
|
||||||
the_content = nil
|
|
||||||
case element_record[:type].to_s.downcase.to_sym
|
|
||||||
when :element_boolean, :element_integer, :element_float
|
|
||||||
the_content = element_record[:content]
|
|
||||||
when :element_bigdecimal
|
|
||||||
the_content = ::BigDecimal.new(element_record[:content].to_s)
|
|
||||||
when :element_datetime
|
|
||||||
the_content = ::DateTime::parse(element_record[:content].to_s)
|
|
||||||
when :element_text
|
|
||||||
the_content = element_record[:content].to_s
|
|
||||||
when :element_binary
|
|
||||||
the_content = ::GxG::ByteArray.new(element_record[:content].to_s.decode64)
|
|
||||||
end
|
|
||||||
existing[(indexer)] = the_content
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
|
record[:content].keys.each do |the_key|
|
||||||
|
existing[(the_key)] = record[:content][(the_key)]
|
||||||
|
existing.set_property_version(the_key, record[:content].property_version(the_key))
|
||||||
end
|
end
|
||||||
|
#
|
||||||
|
if the_format
|
||||||
|
existing.format = the_format
|
||||||
end
|
end
|
||||||
end
|
existing.version = record[:content].version
|
||||||
# ### Set Fromatting / Constraints
|
|
||||||
if existing.is_a?(::GxG::Database::PersistedHash)
|
|
||||||
if formatting[:format].to_s.size > 0
|
|
||||||
existing.format = (formatting[:format])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if existing.is_a?(::GxG::Database::PersistedArray)
|
|
||||||
if formatting[:constraint].to_s.size > 0
|
|
||||||
existing.constraint = (formatting[:constraint])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# ### Conclude Existing
|
|
||||||
existing.version = (record[:version])
|
|
||||||
existing.save
|
existing.save
|
||||||
existing.deactivate
|
existing.deactivate
|
||||||
total_records_processed += 1
|
total_records_processed += 1
|
||||||
end
|
end
|
||||||
|
if existing.is_a?(::GxG::Database::PersistedArray)
|
||||||
|
if existing.constraint()
|
||||||
|
the_format = existing.constraint()
|
||||||
|
if existing.constraint() != formatting[:constraint]
|
||||||
|
raise Exception, "Format Constraint Mismatch Error --> Aborting Merge for record uuid #{existing.uuid.inspect}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
the_format = formatting[:constraint]
|
||||||
|
end
|
||||||
|
existing.constraint = nil
|
||||||
|
record[:content].each_with_index do |item, indexer|
|
||||||
|
existing[(indexer)] = record[:content][(indexer)]
|
||||||
|
existing.set_element_version(indexer, record[:content].element_version(indexer))
|
||||||
|
end
|
||||||
|
if existing.size > record[:content].size
|
||||||
|
((record[:content].size)..(existing.size - 1)).to_a.reverse.each do |indexer|
|
||||||
|
temp_object = existing.delete_at(indexer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
existing.constraint = the_format
|
||||||
|
existing.version = record[:content].version
|
||||||
|
existing.save
|
||||||
|
existing.deactivate
|
||||||
|
total_records_processed += 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Freeform (rsync)
|
||||||
|
if existing.is_a?(::GxG::Database::PersistedHash)
|
||||||
|
existing.format = nil
|
||||||
|
existing.keys.each do |the_key|
|
||||||
|
unless record[:content].keys.include?(the_key)
|
||||||
|
temp_object = existing.delete(the_key)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
#
|
||||||
|
record[:content].keys.each do |the_key|
|
||||||
|
existing[(the_key)] = record[:content][(the_key)]
|
||||||
|
existing.set_property_version(the_key, record[:content].property_version(the_key))
|
||||||
|
end
|
||||||
|
#
|
||||||
|
existing.version = record[:content].version
|
||||||
|
existing.save
|
||||||
|
existing.deactivate
|
||||||
|
total_records_processed += 1
|
||||||
|
end
|
||||||
|
if existing.is_a?(::GxG::Database::PersistedArray)
|
||||||
|
record[:content].each_with_index do |item, indexer|
|
||||||
|
existing[(indexer)] = record[:content][(indexer)]
|
||||||
|
existing.set_element_version(indexer, record[:content].element_version(indexer))
|
||||||
|
end
|
||||||
|
if existing.size > record[:content].size
|
||||||
|
((record[:content].size)..(existing.size - 1)).to_a.reverse.each do |indexer|
|
||||||
|
temp_object = existing.delete_at(indexer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
existing.version = record[:content].version
|
||||||
|
existing.save
|
||||||
|
existing.deactivate
|
||||||
|
total_records_processed += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1715,7 +1715,7 @@ module GxG
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
def element_version(index=nil)
|
def element_version(index=nil)
|
||||||
result = 0.0
|
result = ::BigDecimal.new("0.0")
|
||||||
if index.is_a?(::Numeric)
|
if index.is_a?(::Numeric)
|
||||||
if @element_links[(index)]
|
if @element_links[(index)]
|
||||||
result = @element_links[(index)][:record][:version]
|
result = @element_links[(index)][:record][:version]
|
||||||
|
|
@ -1810,12 +1810,12 @@ module GxG
|
||||||
if @constraint
|
if @constraint
|
||||||
record = @db_address[:database].format_load({:uuid => @constraint.to_s})
|
record = @db_address[:database].format_load({:uuid => @constraint.to_s})
|
||||||
if record
|
if record
|
||||||
record[:ufs]
|
{:ufs => record[:ufs], :version => record[:version]}
|
||||||
else
|
else
|
||||||
""
|
{}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
""
|
{}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ module GxG
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
def element_version(key=nil)
|
def property_version(key=nil)
|
||||||
result = 0.0
|
result = 0.0
|
||||||
if key.is_a?(::Symbol)
|
if key.is_a?(::Symbol)
|
||||||
if key.to_s.size > 256
|
if key.to_s.size > 256
|
||||||
|
|
@ -59,7 +59,7 @@ module GxG
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
def set_element_version(element_key, the_version=nil)
|
def set_property_version(element_key, the_version=nil)
|
||||||
result = false
|
result = false
|
||||||
if @property_links[(element_key)]
|
if @property_links[(element_key)]
|
||||||
if the_version.is_a?(::Numeric)
|
if the_version.is_a?(::Numeric)
|
||||||
|
|
@ -870,14 +870,13 @@ module GxG
|
||||||
end
|
end
|
||||||
last_container = container
|
last_container = container
|
||||||
end
|
end
|
||||||
safe_key = selector.to_s
|
# xxx --> not using colon in pathnames.
|
||||||
# xxx
|
|
||||||
# if selector.is_a?(Symbol)
|
# if selector.is_a?(Symbol)
|
||||||
# safe_key = (":" + selector.to_s)
|
# safe_key = (":" + selector.to_s)
|
||||||
# else
|
# else
|
||||||
# safe_key = selector.to_s
|
# safe_key = selector.to_s
|
||||||
# end
|
# end
|
||||||
# xxx
|
safe_key = selector.to_s
|
||||||
safe_key.gsub!("/","%2f")
|
safe_key.gsub!("/","%2f")
|
||||||
path_stack.unshift(safe_key)
|
path_stack.unshift(safe_key)
|
||||||
# compare the_value
|
# compare the_value
|
||||||
|
|
@ -1426,7 +1425,7 @@ module GxG
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
def element_version(key=nil)
|
def property_version(key=nil)
|
||||||
result = 0.0
|
result = 0.0
|
||||||
if key.is_a?(::Symbol)
|
if key.is_a?(::Symbol)
|
||||||
if key.to_s.size > 256
|
if key.to_s.size > 256
|
||||||
|
|
@ -1444,7 +1443,7 @@ module GxG
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
def set_element_version(element_key, the_version=nil)
|
def set_property_version(element_key, the_version=nil)
|
||||||
result = false
|
result = false
|
||||||
if @property_links[(element_key)]
|
if @property_links[(element_key)]
|
||||||
if the_version.is_a?(::Numeric)
|
if the_version.is_a?(::Numeric)
|
||||||
|
|
@ -1469,6 +1468,19 @@ module GxG
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#
|
#
|
||||||
|
def ufs()
|
||||||
|
if @format
|
||||||
|
record = @db_address[:database].format_load({:uuid => @format.to_s})
|
||||||
|
if record
|
||||||
|
{:ufs => record[:ufs], :version => record[:version]}
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
#
|
||||||
def parent()
|
def parent()
|
||||||
@parent
|
@parent
|
||||||
end
|
end
|
||||||
|
|
@ -2801,11 +2813,13 @@ module GxG
|
||||||
end
|
end
|
||||||
last_container = container
|
last_container = container
|
||||||
end
|
end
|
||||||
if selector.is_a?(Symbol)
|
# xxx --> not using colon in pathnames.
|
||||||
safe_key = (":" + selector.to_s)
|
# if selector.is_a?(Symbol)
|
||||||
else
|
# safe_key = (":" + selector.to_s)
|
||||||
|
# else
|
||||||
|
# safe_key = selector.to_s
|
||||||
|
# end
|
||||||
safe_key = selector.to_s
|
safe_key = selector.to_s
|
||||||
end
|
|
||||||
safe_key.gsub!("/","%2f")
|
safe_key.gsub!("/","%2f")
|
||||||
path_stack.unshift(safe_key)
|
path_stack.unshift(safe_key)
|
||||||
# compare the_value
|
# compare the_value
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue