Class: Item

Inherits:
ActiveRecord::Base
  • Object
show all
Extended by:
T::Sig
Includes:
DataAssociator
Defined in:
app/models/item.rb

Overview

Class that represents a physical object in the lab Has an ObjectType that declares what kind of physical thing it is, and may have a Sample defining the specimen that resides within.

Direct Known Subclasses

Collection

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DataAssociator

#append_notes, #associate, #associations, #data_associations, #get, #get_association, #lazy_associate, #modify, #notes, #notes=, #upload

Class Method Details

.items_for(sid, oid) ⇒ Object



437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'app/models/item.rb', line 437

def self.items_for(sid, oid)
  sample = Sample.find_by(id: sid)
  ot = ObjectType.find_by(id: oid)

  if sample
    return [] unless ot
    return Collection.parts(sample, ot) if ot.collection_type?

    return sample.items.reject { |i| i.deleted? || i.object_type_id != ot.id }
  end
  return ot&.items&.reject(&:deleted?) if ot&.sample?

  []
end

.make(params, opts = {}) ⇒ Object



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
# File 'app/models/item.rb', line 221

def self.make(params, opts = {})
  o = { object_type: nil, sample: nil, location: nil }.merge opts

  if o[:object_type]
    loc = params['location']
    params.delete 'location'
    item = new params.merge(object_type_id: o[:object_type].id)
    item.save
    item.location = loc if loc
  else
    item = new params
  end

  item.sample_id = o[:sample].id if o[:sample]

  if o[:object_type]
    item.object_type_id = o[:object_type].id
    wiz = Wizard.find_by(name: o[:object_type].prefix)
    locator = wiz.next if wiz
    item.set_primitive_location locator.to_s if wiz
  end

  if locator
    ActiveRecord::Base.transaction do
      item.save
      locator.item_id = item.id
      locator.save
      item.locator_id = locator.id
      item.save
      locator.save
    end
  else
    item.save
  end

  item.reload
  logger.info "Made new item #{item.id} with location #{item.location} and primitive location #{item.primitive_location}"

  item
end

.new_object(name) ⇒ Object

OLD



392
393
394
395
396
397
398
# File 'app/models/item.rb', line 392

def self.new_object(name)
  olist = ObjectType.where('name = ?', name)
  raise "Could not find object type named '#{spec[:object_type]}'." if olist.empty?

  Item.make({ quantity: 1, inuse: 0 }, object_type: olist.first)

end

.new_sample(name, spec) ⇒ Object



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
# File 'app/models/item.rb', line 400

def self.new_sample(name, spec)

  raise 'No Sample Type Specified (with :of)' unless spec[:of]
  raise 'No Container Specified (with :in)' unless spec[:as]

  olist = ObjectType.where('name = ?', spec[:as])
  raise "Could not find container named '#{spec[:as]}'." if olist.empty?

  sample_type_id = SampleType.find_by(name: spec[:of])
  raise "Could not find sample type named '#{spec[:of]}'." unless sample_type_id

  slist = Sample.where('name = ? AND sample_type_id = ?', name, sample_type_id)
  raise "Could not find sample named #{name}" if slist.empty?

  Item.make({ quantity: 1, inuse: 0 }, sample: slist.first, object_type: olist.first)

end

.with_sample(sample:) ⇒ Object

scopes for searching Items



380
381
382
# File 'app/models/item.rb', line 380

def self.with_sample(sample:)
  includes(:locator).includes(:object_type).where(sample_id: sample.id)
end

.with_type(object_type:) ⇒ Object



384
385
386
# File 'app/models/item.rb', line 384

def self.with_type(object_type:)
  includes(:locator).includes(:object_type).where(object_type: object_type)
end

Instance Method Details

#all_attributesObject



351
352
353
354
355
356
357
# File 'app/models/item.rb', line 351

def all_attributes
  temp = attributes.symbolize_keys
  temp[:object_type] = object_type.attributes.symbolize_keys
  temp[:sample] = sample.attributes.symbolize_keys if sample_id

  temp
end

#annotate(hash) ⇒ Object



341
342
343
# File 'app/models/item.rb', line 341

def annotate(hash)
  set_data(datum.merge(hash))
end

#collection?Bool

Indicates whether this Item is a Collection.

Returns:

  • (Bool)

    true if this Item is a Collection, false otherwise



305
306
307
# File 'app/models/item.rb', line 305

def collection?
  object_type&.collection_type?
end

#containing_collectionCollection

Returns the parent Collection of this item, if it is a part. Otherwise, returns nil

Returns:



311
312
313
314
# File 'app/models/item.rb', line 311

def containing_collection
  pas = PartAssociation.where(part_id: id)
  pas[0].collection if pas.length == 1
end

#datumObject

Deprecated.

Use DataAssociator methods instead of datum



330
331
332
333
334
# File 'app/models/item.rb', line 330

def datum
  JSON.parse(T.must(data), symbolize_names: true)
rescue StandardError
  {}
end

#datum=(d) ⇒ Object

Deprecated.

Use DataAssociator methods instead of datum



337
338
339
# File 'app/models/item.rb', line 337

def datum=(d)
  self.data = d.to_json
end

#deleted?Bool

Indicates whether this Item is deleted.

Returns:

  • (Bool)

    true if this Item is deleted, false otherwise



298
299
300
# File 'app/models/item.rb', line 298

def deleted?
  primitive_location == 'deleted'
end

#exportObject



422
423
424
425
426
427
428
429
430
431
# File 'app/models/item.rb', line 422

def export
  a = attributes
  a.delete 'inuse'
  a.delete 'locator_id'
  data = get_data
  a['data'] = data if data
  a[:sample] = sample.export if association(:sample).loaded?
  a[:object_type] = object_type.export if association(:object_type).loaded?
  a
end

#featuresObject



345
346
347
348
349
# File 'app/models/item.rb', line 345

def features
  f = { id: id, location: location, name: object_type.name }
  f = f.merge(sample: sample.name, type: sample.sample_type.name) if sample_id
  f
end

#get_dataObject



323
324
325
326
327
# File 'app/models/item.rb', line 323

def get_data
  JSON.parse T.must(data), symbolize_names: true
rescue JSON::ParserError
  nil
end

#inuse_less_than_quantityObject



56
57
58
59
# File 'app/models/item.rb', line 56

def inuse_less_than_quantity
  errors.add(:inuse, 'must non-negative and not greater than the quantity.') unless
    quantity && inuse && T.must(inuse) >= -1 && T.must(inuse) <= T.must(quantity)
end

#is_partBool

Returns true if the item is a part of a collection

Returns:

  • (Bool)


77
78
79
# File 'app/models/item.rb', line 77

def is_part
  object_type_id == part_type.id
end

#locationString

Returns the location of the Item

Returns:

  • (String)

    the description of the Item's physical location in the lab as a string



85
86
87
88
89
90
91
92
93
94
95
# File 'app/models/item.rb', line 85

def location
  if is_part
    'Part of Collection'
  elsif locator
    locator.to_s
  elsif primitive_location
    primitive_location
  else
    'Unknown'
  end
end

#location=(x) ⇒ Object

Sets the location of the Item.

Parameters:

  • x (String)

    the location string



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

def location=(x)
  move_to x
  self[:location] = x # just for consistency
end

#mark_as_deletedBool

Delete the Item (sets item's location to "deleted").

Returns:

  • (Bool)

    true if the location is set to 'deleted', false otherwise



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'app/models/item.rb', line 277

def mark_as_deleted
  self[:location] = 'deleted'
  self.quantity = -1
  self.inuse = -1
  self.locator_id = nil
  locator&.item_id = nil if locator

  item_saved = T.let(false, T.untyped)
  locator_saved = T.let(false, T.untyped)

  transaction do
    item_saved = save
    locator_saved = locator&.save if locator
  end

  item_saved && locator_saved
end

#move(locstr) ⇒ Item

Note:

for backwards compatibility

Sets item location to provided string or to string's associated location Wizard if it exists.

Parameters:

  • locstr (String)

    the location string

Returns:



217
218
219
# File 'app/models/item.rb', line 217

def move(locstr)
  move_to locstr
end

#move_to(location_name) ⇒ Item

Sets item location to provided string or to string's associated location Wizard if it exists.

Parameters:

  • locstr (String)

    the location string

Returns:



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
# File 'app/models/item.rb', line 127

def move_to(location_name)

  wiz = Wizard.find_by(name: object_type.prefix) if object_type

  if object_type && wiz && wiz.has_correct_form(location_name) # item and location managed by a wizard

    unless wiz.has_correct_form location_name
      errors.add(:wrong_form, "'#{location_name}'' is not in the form of a location for the #{wiz.name} wizard.")
      return nil
    end

    locs = Locator.where(wizard_id: wiz.id, number: (wiz.location_to_int location_name))

    case locs.length
    when 0
      newloc = wiz.addnew location_name
    when 1
      newloc = locs.first
    else
      errors.add(:too_many_locators, "There are multiple items at #{location_name}.")
      return nil
    end

    if newloc == locator
      errors.add(:already_there, "Item is already at #{location_name}.")
      return nil
    end

    if newloc.item_id.nil?

      oldloc = Locator.find_by(id: locator_id)
      oldloc.item_id = nil if oldloc
      self.locator_id = newloc.id
      self[:location] = location_name
      self.quantity = 1
      self.inuse = 0
      newloc.item_id = id

      transaction do
        save
        oldloc.save if oldloc
        newloc.save
      end

      reload
      oldloc.reload if oldloc
      puts "newloc = #{newloc.inspect}"
      newloc.reload

      errors.add(:locator_save_error, "Error: '#{errors.full_messages.join(',')}'") unless errors.empty?

    else

      errors.add(:locator_taken, "Location taken by item #{newloc.item_id}.")

    end

  else # location is not in the form managed by a wizard

    loc = Locator.find_by(id: locator_id)
    loc.item_id = nil if loc

    self[:location] = location_name
    self.locator_id = nil

    transaction do
      save
      loc.save if loc
    end

    raise errors.full_messages.join(',') unless errors.empty?

    reload

  end

  self

end

#non_wizard_location?Boolean

Returns:

  • (Boolean)


208
209
210
211
212
# File 'app/models/item.rb', line 208

def non_wizard_location?
  wiz = Wizard.find_by(name: object_type.prefix)

  !(wiz && locator.nil?)
end

#num_postsObject



418
419
420
# File 'app/models/item.rb', line 418

def num_posts
  post_associations.count
end

#object_typeObjectType

Gets the ObjectType of Item.

Returns:

  • (ObjectType)

    type of object that this Item represents a unique physical instantiation of



41
# File 'app/models/item.rb', line 41

accepts_nested_attributes_for :object_type

#part_typeObject



70
71
72
# File 'app/models/item.rb', line 70

def part_type
  @@part_type ||= ObjectType.part_type
end

#primitive_locationObject



64
65
66
# File 'app/models/item.rb', line 64

def primitive_location
  self[:location]
end

#put_at(locstr) ⇒ Object



262
263
264
265
266
267
268
269
270
271
# File 'app/models/item.rb', line 262

def put_at(locstr)
  loc = Wizard.find_locator locstr
  return nil unless loc && loc.item_id.nil?

  loc.item_id = id
  transaction do
    loc.save
    save
  end
end

#quantity_nonnegObject



51
52
53
54
# File 'app/models/item.rb', line 51

def quantity_nonneg
  errors.add(:quantity, 'Must be non-negative.') unless
    quantity && T.must(quantity) >= -1
end

#sampleSample

Gets the sample inside this Item.

Returns:

  • (Sample)

    kind of specimen contained in this Item, if any. Some Items correspond to Samples and some do not. For example, an Item whose object type is "1 L Bottle" does not correspond to a sample. An item whose ObjectType is "Plasmid Stock" will have a corresponding Sample, whose name might be something like "pLAB1".



35
# File 'app/models/item.rb', line 35

accepts_nested_attributes_for :sample

#set_data(d) ⇒ Object

other methods ############################################################################



318
319
320
321
# File 'app/models/item.rb', line 318

def set_data(d)
  self.data = d.to_json
  save
end

#set_primitive_location(locstr) ⇒ Object



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

def set_primitive_location(locstr)
  self[:location] = locstr
end

#storeItem

Sets item location to empty slot based on location Wizard. By default sets to "Bench".

Returns:



112
113
114
115
116
117
118
119
120
# File 'app/models/item.rb', line 112

def store
  wiz = Wizard.find_by(name: object_type.prefix)
  if wiz
    locator = wiz.next
    move_to(wiz.int_to_location(locator.number))
  else
    move_to 'Bench'
  end
end

#to_sObject



359
360
361
# File 'app/models/item.rb', line 359

def to_s
  "<a href='#' onclick='open_item_ui(#{id})'>#{id}</a>"
end

#upgrade(force = false) ⇒ Object

upgrades data field to data association (if no data associations exist)



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'app/models/item.rb', line 363

def upgrade(force = false) # upgrades data field to data association (if no data associations exist)
  if force || associations.empty?
    begin
      obj = JSON.parse T.must(data)

      obj.each do |k, v|
        associate k, v
      end
    rescue StandardError
      self.notes = data if data
    end
  else
    append_notes "\n#{Date.today}: Attempt to upgrade failed. Item already had associations."
  end
end

#weekObject



433
434
435
# File 'app/models/item.rb', line 433

def week
  created_at.strftime('%W')
end