Class: Container

Overview

A container localizes the proximity of one ore more physical things, at this point in the TW UI this is restricted to a number of collection objects.

Objects are placed in containers by reference to a ContainerItem.

Defined Under Namespace

Classes: Aisle, Box, Building, Cabinet, Collection, Drawer, Envelope, Folder, Jar, PillBox, Pin, Room, Shelf, Site, Slide, SlideBox, UnitTray, Vial, VialRack, Virtual, WellPlate

Constant Summary

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from SoftValidation

#clear_soft_validations, #fix_for, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations, #soft_validators

Methods included from Shared::IsData

#errors_excepting, #full_error_messages_excepting, #identical, #is_community?, #is_in_use?, #similar

Methods included from Shared::Tags

#reject_tags, #tag_with, #tagged?, #tagged_with?

Methods included from Shared::Loanable

#all_loan_items, #all_loans, #container_loan_items, #container_loaned?, #container_loans, #container_times_loaned, #current_loan, #current_loan_item, #has_been_loaned?, #is_loanable?, #loan_return_date, #loaned_in_container, #on_loan?, #times_loaned

Methods included from Shared::Labels

#labeled?

Methods included from Shared::Identifiers

#dwc_occurrence_id, #identified?, #next_by_identifier, #previous_by_identifier, #reject_identifiers, #uri, #uuid, #visible_identifiers_for

Methods included from Shared::Depictions

#has_depictions?, #image_array=, #reject_depictions, #reject_images

Methods included from Shared::Containable

#contain, #containable?, containable_types, #contained?, #contained_by?, #contained_siblings, #enclosing_containers, #put_in_container

Methods included from Housekeeping

#has_polymorphic_relationship?

Methods inherited from ApplicationRecord

transaction_with_retry

Instance Attribute Details

#dispositionString

Returns a free text description of the position of this container.

Returns:

  • (String)

    a free text description of the position of this container



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

#empty_containerObject

Returns the value of attribute empty_container.



38
39
40
# File 'app/models/container.rb', line 38

def empty_container
  @empty_container
end

#nameString

Returns abitrary name of this container.

Returns:

  • (String)

    abitrary name of this container



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

Returns text of a label to print for this container.

Returns:

  • (String)

    text of a label to print for this container



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

#project_idInteger

Returns:

  • (Integer)


36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

#size_xInt

Returns the number of slots in the x dimension.

Returns:

  • (Int)

    the number of slots in the x dimension



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

#size_yInt

Returns the number of slots in the y dimension.

Returns:

  • (Int)

    the number of slots in the y dimension



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

#size_zInt

Returns the number of slots in the z dimension.

Returns:

  • (Int)

    the number of slots in the z dimension



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

#typeString

Returns STI, the type of container.

Returns:

  • (String)

    STI, the type of container



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'app/models/container.rb', line 36

class Container < ApplicationRecord

  attr_accessor :empty_container

  include Housekeeping

  # !! Must come before Shared::Containable
  before_destroy :empty_contents, if: -> { empty_container }
  before_destroy :check_for_contents

  include Shared::Containable
  include Shared::Depictions
  include Shared::Identifiers
  include Shared::Labels
  include Shared::Loanable
  include Shared::Tags
  include Shared::IsData
  include SoftValidation

  has_many :collection_profiles, inverse_of: :container, dependent: :restrict_with_error

  validates :type, presence: true
  validate :type_is_valid

  validates :asserted_percent_empty,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validates :asserted_percent_earmarked,
    numericality: { greater_than_or_equal_to: 0.0, less_than_or_equal_to: 100.0 },
    allow_nil: true

  validate :earmarked_requires_sufficient_empty
  validate :size_does_not_exclude_placed_items, if: :persisted?

  # @return [ContainerItem Scope]
  #    return all ContainerItems contained in this container (non recursive)
  # TODO: fix Please call `reload_container_item` instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)
  #
  def container_items
    container_item&.children || ContainerItem.none
  end

  # @return [ContainerItem Scope]
  #   return all ContainerItems contained in this container (recursive)
  def all_container_items
    reload_container_item.try(:descendants) || ContainerItem.none
  end

  # @return [Array]
  #   return all #contained_object(s) (non-recursive)
  def contained_objects
    return [] if !reload_container_item
    container_item.children.map(&:contained_object)
  end

  # @return [Array]
  #   return all #contained_object(s) (recursive)
  def all_contained_objects
    return [] if !reload_container_item
    container_item.descendants.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's CollectionObjects only (with recursion)
  def collection_objects
    all_container_items.containing_collection_objects.map(&:contained_object)
  end

  # @return [Array] of CollectionObject#id of this container's contents (no recursion)
  def collection_object_ids
    container_items.containing_collection_objects.pluck(:id)
  end

  # @return [Array] of CollectionObject#id of this container's contents (recursive)
  def all_collection_object_ids
    # all_container_items.containing_collection_objects.pluck(:id)
    collection_objects.map(&:id)
  end

  # @return [Boolean]
  #   regardless whether size is defined, whether there is anything in this container (non-recursive)
  def is_empty?
    !container_items.any?
  end

  # @return [Boolean]
  #   whether this container is nested in other containers
  def is_nested?
    container_item && container_item.ancestors.any?
  end

  # @return [Boolean]
  #   true if size is defined, and there is no space left in this container (non-recursive)
  def is_full?
    available_space == 0
  end

  # @return [Integer]
  #   the free space in this container (non-recursive)
  def available_space
    in_container = container_items.count
    if size
      size - in_container
    else
      nil
    end
  end

  # @return [Integer, nil]
  #   the total number of "slots" or "spaces" this container has, it's size
  # TODO: reserved word?
  def size
    return nil if size_x.blank? && size_y.blank? && size_z.blank?
    if size_x
      if size_y
        if size_z
          size_x * size_y * size_z
        else
          size_x * size_y
        end
      else
        size_x
      end
    end
  end

  # @return [String]
  #   the "common name" of this class
  def self.class_name
    self.name.demodulize.underscore.humanize.downcase
  end

  # @return [Array of Strings]
  #   valid containers class names that this container can fit in, by default none
  def self.valid_parents
    []
  end

  def self.dimensions
    {}
  end

  # @return [Container]
  #   places all objects in a new, parent-less container, saves it off,
  #   None of the objects are permitted to be new_records.
  #   !! If an object is in another container it is moved to the new container created here.
  def self.containerize(objects, klass = Container::Virtual)
    new_container = nil
    begin
      Container.transaction do
        new_container = klass.create()
        ci_parent     = ContainerItem.create(contained_object: new_container)

        objects.each do |o|
          raise ActiveRecord::RecordInvalid if o.new_record?
          if o.container_item.nil? # contain an uncontained objet
            ContainerItem.create(parent: ci_parent, contained_object: o)
          else # move the object if it's in a container already
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end
    new_container
  end

  # @param params [ActionController::Parameters, Hash] with keys:
  #   building_id [Integer] id of the parent Container::Building
  #   drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy'
  #   rooms [Integer] number of Room containers to create
  #   cabinets [Integer] number of Cabinet containers per Room
  #   drawers [Integer] number of Drawer containers per Cabinet
  #   cabinet_size_x [Integer, nil] default size_x applied to each created cabinet
  #   cabinet_size_y [Integer, nil] default size_y applied to each created cabinet
  #   cabinet_size_z [Integer, nil] default size_z applied to each created cabinet
  #   asserted_percent_empty [Float, nil] default value applied to each created drawer
  #   asserted_percent_earmarked [Float, nil] default value applied to each created drawer
  # @return [Array<Container::Room>] the created Room containers, or false on failure
  def self.scaffold(params)
    building_id   = params[:building_id].to_i
    drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
    room_count    = params[:rooms].to_i
    cabinet_count = params[:cabinets].to_i
    drawer_count  = params[:drawers].to_i

    cabinet_defaults = {}
    cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
    cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
    cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

    drawer_defaults = {}
    drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
    drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

    return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

    # The drawer class must exist; if not, fail early.
    begin
      drawer_klass = drawer_type.constantize
    rescue NameError
      return false
    end

    # Not customizable yet
    cabinet_klass = Container::Cabinet

    created_rooms = []

    begin
      Container.transaction do
        building = Container.find(building_id)

        room_count.times do
          room = Container::Room.create!
          building.add_container_items([room])

          cabinet_count.times do
            cabinet = cabinet_klass.create!(cabinet_defaults)
            room.add_container_items([cabinet])

            drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
            cabinet.add_container_items(drawers)
          end

          created_rooms << room
        end
      end
    rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

      return false
    end



    created_rooms
  end

  # @return [Boolean]
  # add the objects to this container
  def add_container_items(objects)
    return false if new_record?

    # TODO: Figure out why this reload is required.
    self.reload # this seems to be required under some (as yet undefined) circumstances.
    begin
      Container.transaction do
        ci_parent = container_item
        ci_parent ||= ContainerItem.create!(contained_object: self)

        objects.each do |o|
          return false if o.new_record? || !o.containable? # does this roll back transaction
          if o.container_item.nil?
            ContainerItem.create!(parent: ci_parent, contained_object: o)
          else # move the object to a new container
            # this triggers the closure_tree parenting/re-parenting
            o.container_item.update(parent_id: ci_parent.id)
          end
        end
      end
    rescue ActiveRecord::RecordInvalid
      return false
    end

    true
  end

  protected

  def empty_contents
    container_items.delete_all
  end

  def type_is_valid
    raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
  end

  # Prevent shrinking a dimension when placed container items would fall
  # outside the new boundary on that axis.
  def size_does_not_exclude_placed_items
    {
      size_x: :disposition_x,
      size_y: :disposition_y,
      size_z: :disposition_z
    }.each do |size_attr, disp_attr|
      next unless send(:"#{size_attr}_changed?")
      new_val = send(size_attr)
      old_val = send(:"#{size_attr}_was")
      next unless new_val.present? && old_val.present? && new_val < old_val
      if container_items.where("#{disp_attr} > ?", new_val).exists?
        errors.add(:base, 'Resize would impact placed containers')
        return
      end
    end
  end

  def earmarked_requires_sufficient_empty
    return if asserted_percent_earmarked.nil?
    if asserted_percent_empty.nil?
      errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
    elsif asserted_percent_empty < asserted_percent_earmarked
      errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
    end
  end

  def check_for_contents
    if !is_empty?
      errors.add(:base, 'is not empty, empty it before destroying it')
      throw :abort
    end
  end

end

Class Method Details

.class_nameString

Returns the "common name" of this class.

Returns:

  • (String)

    the "common name" of this class



164
165
166
# File 'app/models/container.rb', line 164

def self.class_name
  self.name.demodulize.underscore.humanize.downcase
end

.containerize(objects, klass = Container::Virtual) ⇒ Container

Returns places all objects in a new, parent-less container, saves it off, None of the objects are permitted to be new_records. !! If an object is in another container it is moved to the new container created here.

Returns:

  • (Container)

    places all objects in a new, parent-less container, saves it off, None of the objects are permitted to be new_records. !! If an object is in another container it is moved to the new container created here.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'app/models/container.rb', line 182

def self.containerize(objects, klass = Container::Virtual)
  new_container = nil
  begin
    Container.transaction do
      new_container = klass.create()
      ci_parent     = ContainerItem.create(contained_object: new_container)

      objects.each do |o|
        raise ActiveRecord::RecordInvalid if o.new_record?
        if o.container_item.nil? # contain an uncontained objet
          ContainerItem.create(parent: ci_parent, contained_object: o)
        else # move the object if it's in a container already
          o.container_item.update(parent_id: ci_parent.id)
        end
      end
    end
  rescue ActiveRecord::RecordInvalid
    return false
  end
  new_container
end

.dimensionsObject



174
175
176
# File 'app/models/container.rb', line 174

def self.dimensions
  {}
end

.scaffold(params) ⇒ Array<Container::Room>

Returns the created Room containers, or false on failure.

Parameters:

  • params (ActionController::Parameters, Hash)

    with keys: building_id [Integer] id of the parent Container::Building drawer_type [String] full STI type name of the drawer, e.g. 'Container::Drawer::CalAcademy' rooms [Integer] number of Room containers to create cabinets [Integer] number of Cabinet containers per Room drawers [Integer] number of Drawer containers per Cabinet cabinet_size_x [Integer, nil] default size_x applied to each created cabinet cabinet_size_y [Integer, nil] default size_y applied to each created cabinet cabinet_size_z [Integer, nil] default size_z applied to each created cabinet asserted_percent_empty [Float, nil] default value applied to each created drawer asserted_percent_earmarked [Float, nil] default value applied to each created drawer

Returns:

  • (Array<Container::Room>)

    the created Room containers, or false on failure



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'app/models/container.rb', line 216

def self.scaffold(params)
  building_id   = params[:building_id].to_i
  drawer_type   = params[:drawer_type].to_s.presence || 'Container::Drawer'
  room_count    = params[:rooms].to_i
  cabinet_count = params[:cabinets].to_i
  drawer_count  = params[:drawers].to_i

  cabinet_defaults = {}
  cabinet_defaults[:size_x] = params[:cabinet_size_x].presence&.to_i unless params[:cabinet_size_x].nil?
  cabinet_defaults[:size_y] = params[:cabinet_size_y].presence&.to_i unless params[:cabinet_size_y].nil?
  cabinet_defaults[:size_z] = params[:cabinet_size_z].presence&.to_i unless params[:cabinet_size_z].nil?

  drawer_defaults = {}
  drawer_defaults[:asserted_percent_empty] = params[:asserted_percent_empty].presence&.to_f     unless params[:asserted_percent_empty].nil?
  drawer_defaults[:asserted_percent_earmarked] = params[:asserted_percent_earmarked].presence&.to_f unless params[:asserted_percent_earmarked].nil?

  return false if building_id == 0 || room_count < 1 || cabinet_count < 1 || drawer_count < 1

  # The drawer class must exist; if not, fail early.
  begin
    drawer_klass = drawer_type.constantize
  rescue NameError
    return false
  end

  # Not customizable yet
  cabinet_klass = Container::Cabinet

  created_rooms = []

  begin
    Container.transaction do
      building = Container.find(building_id)

      room_count.times do
        room = Container::Room.create!
        building.add_container_items([room])

        cabinet_count.times do
          cabinet = cabinet_klass.create!(cabinet_defaults)
          room.add_container_items([cabinet])

          drawers = drawer_count.times.map { drawer_klass.create!(drawer_defaults) }
          cabinet.add_container_items(drawers)
        end

        created_rooms << room
      end
    end
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotFound

    return false
  end



  created_rooms
end

.valid_parentsArray of Strings

Returns valid containers class names that this container can fit in, by default none.

Returns:

  • (Array of Strings)

    valid containers class names that this container can fit in, by default none



170
171
172
# File 'app/models/container.rb', line 170

def self.valid_parents
  []
end

Instance Method Details

#add_container_items(objects) ⇒ Boolean

add the objects to this container

Returns:

  • (Boolean)


277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'app/models/container.rb', line 277

def add_container_items(objects)
  return false if new_record?

  # TODO: Figure out why this reload is required.
  self.reload # this seems to be required under some (as yet undefined) circumstances.
  begin
    Container.transaction do
      ci_parent = container_item
      ci_parent ||= ContainerItem.create!(contained_object: self)

      objects.each do |o|
        return false if o.new_record? || !o.containable? # does this roll back transaction
        if o.container_item.nil?
          ContainerItem.create!(parent: ci_parent, contained_object: o)
        else # move the object to a new container
          # this triggers the closure_tree parenting/re-parenting
          o.container_item.update(parent_id: ci_parent.id)
        end
      end
    end
  rescue ActiveRecord::RecordInvalid
    return false
  end

  true
end

#all_collection_object_idsArray

Returns of CollectionObject#id of this container's contents (recursive).

Returns:

  • (Array)

    of CollectionObject#id of this container's contents (recursive)



110
111
112
113
# File 'app/models/container.rb', line 110

def all_collection_object_ids
  # all_container_items.containing_collection_objects.pluck(:id)
  collection_objects.map(&:id)
end

#all_contained_objectsArray

Return all #contained_object(s) (recursive)

Returns:

  • (Array)

    return all #contained_object(s) (recursive)



94
95
96
97
# File 'app/models/container.rb', line 94

def all_contained_objects
  return [] if !reload_container_item
  container_item.descendants.map(&:contained_object)
end

#all_container_itemsContainerItem Scope

Return all ContainerItems contained in this container (recursive)

Returns:

  • (ContainerItem Scope)

    return all ContainerItems contained in this container (recursive)



81
82
83
# File 'app/models/container.rb', line 81

def all_container_items
  reload_container_item.try(:descendants) || ContainerItem.none
end

#available_spaceInteger

Returns the free space in this container (non-recursive).

Returns:

  • (Integer)

    the free space in this container (non-recursive)



135
136
137
138
139
140
141
142
# File 'app/models/container.rb', line 135

def available_space
  in_container = container_items.count
  if size
    size - in_container
  else
    nil
  end
end

#check_for_contentsObject (protected)



342
343
344
345
346
347
# File 'app/models/container.rb', line 342

def check_for_contents
  if !is_empty?
    errors.add(:base, 'is not empty, empty it before destroying it')
    throw :abort
  end
end

#collection_object_idsArray

Returns of CollectionObject#id of this container's contents (no recursion).

Returns:

  • (Array)

    of CollectionObject#id of this container's contents (no recursion)



105
106
107
# File 'app/models/container.rb', line 105

def collection_object_ids
  container_items.containing_collection_objects.pluck(:id)
end

#collection_objectsArray

Returns of CollectionObject#id of this container's CollectionObjects only (with recursion).

Returns:

  • (Array)

    of CollectionObject#id of this container's CollectionObjects only (with recursion)



100
101
102
# File 'app/models/container.rb', line 100

def collection_objects
  all_container_items.containing_collection_objects.map(&:contained_object)
end

#contained_objectsArray

Return all #contained_object(s) (non-recursive)

Returns:

  • (Array)

    return all #contained_object(s) (non-recursive)



87
88
89
90
# File 'app/models/container.rb', line 87

def contained_objects
  return [] if !reload_container_item
  container_item.children.map(&:contained_object)
end

#container_itemsContainerItem Scope

TODO: fix Please call reload_container_item instead. (called from container_items at /Users/jrflood/src/taxonworks/app/models/container.rb:43)

Returns:

  • (ContainerItem Scope)

    return all ContainerItems contained in this container (non recursive)



75
76
77
# File 'app/models/container.rb', line 75

def container_items
  container_item&.children || ContainerItem.none
end

#earmarked_requires_sufficient_emptyObject (protected)



333
334
335
336
337
338
339
340
# File 'app/models/container.rb', line 333

def earmarked_requires_sufficient_empty
  return if asserted_percent_earmarked.nil?
  if asserted_percent_empty.nil?
    errors.add(:asserted_percent_empty, 'must be present when earmarked is set')
  elsif asserted_percent_empty < asserted_percent_earmarked
    errors.add(:asserted_percent_empty, 'must be greater than or equal to asserted percent earmarked')
  end
end

#empty_contentsObject (protected)



306
307
308
# File 'app/models/container.rb', line 306

def empty_contents
  container_items.delete_all
end

#is_empty?Boolean

Returns regardless whether size is defined, whether there is anything in this container (non-recursive).

Returns:

  • (Boolean)

    regardless whether size is defined, whether there is anything in this container (non-recursive)



117
118
119
# File 'app/models/container.rb', line 117

def is_empty?
  !container_items.any?
end

#is_full?Boolean

Returns true if size is defined, and there is no space left in this container (non-recursive).

Returns:

  • (Boolean)

    true if size is defined, and there is no space left in this container (non-recursive)



129
130
131
# File 'app/models/container.rb', line 129

def is_full?
  available_space == 0
end

#is_nested?Boolean

Returns whether this container is nested in other containers.

Returns:

  • (Boolean)

    whether this container is nested in other containers



123
124
125
# File 'app/models/container.rb', line 123

def is_nested?
  container_item && container_item.ancestors.any?
end

#sizeInteger?

TODO: reserved word?

Returns:

  • (Integer, nil)

    the total number of "slots" or "spaces" this container has, it's size



147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'app/models/container.rb', line 147

def size
  return nil if size_x.blank? && size_y.blank? && size_z.blank?
  if size_x
    if size_y
      if size_z
        size_x * size_y * size_z
      else
        size_x * size_y
      end
    else
      size_x
    end
  end
end

#size_does_not_exclude_placed_itemsObject (protected)

Prevent shrinking a dimension when placed container items would fall outside the new boundary on that axis.



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'app/models/container.rb', line 316

def size_does_not_exclude_placed_items
  {
    size_x: :disposition_x,
    size_y: :disposition_y,
    size_z: :disposition_z
  }.each do |size_attr, disp_attr|
    next unless send(:"#{size_attr}_changed?")
    new_val = send(size_attr)
    old_val = send(:"#{size_attr}_was")
    next unless new_val.present? && old_val.present? && new_val < old_val
    if container_items.where("#{disp_attr} > ?", new_val).exists?
      errors.add(:base, 'Resize would impact placed containers')
      return
    end
  end
end

#type_is_validObject (protected)

Raises:

  • (ActiveRecord::SubclassNotFound)


310
311
312
# File 'app/models/container.rb', line 310

def type_is_valid
  raise ActiveRecord::SubclassNotFound, 'Invalid subclass' if type && !CONTAINER_TYPES.include?(type)
end