Class: SampleRegistrar

Inherits:
ApplicationRecord show all
Defined in:
app/models/sample_registrar.rb

Overview

An instance of this class is responsible for the registration of a sample and its sample tube. You can think of this as a binding between those two, within the context of a user, study and asset group.

– NOTE: This is very much a temporary object: after creation the instance will instantly destroy itself. This is primarily done because Rails 2.3 doesn't have the ActiveModel features of Rails 3, and we need some of those above-and-beyond just validation. If required, the after_create callback could be removed to keep track of sample registrations. ++

Defined Under Namespace

Classes: AssetGroupHelper, RegistrationError

Constant Summary collapse

NoSamplesError =
Class.new(RegistrationError)
SpreadsheetError =
Class.new(StandardError)
TooManySamplesError =
Class.new(SpreadsheetError)
REMAPPED_COLUMN_NAMES =

Column names from old spreadsheets that need mapping to new names.

{ 'Asset group name' => 'Asset group' }.freeze
REQUIRED_COLUMNS =

Columns that are required for the spreadsheet to be considered valid.

['Asset group', 'Sample name'].freeze
REQUIRED_COLUMNS_SENTENCE =
REQUIRED_COLUMNS.map { |w| "'#{w}'" }.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ')

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

convert_labware_to_receptacle_for, find_by_id_or_name, find_by_id_or_name!

Methods included from Warren::BroadcastMessages

#broadcast, included, #queue_associated_for_broadcast, #queue_for_broadcast, #warren

Instance Attribute Details

#asset_group_helperObject


139
140
141
# File 'app/models/sample_registrar.rb', line 139

def asset_group_helper
  @asset_group_helper ||= SampleRegistrar::AssetGroupHelper.new
end

#asset_group_nameObject

Returns the value of attribute asset_group_name


114
115
116
# File 'app/models/sample_registrar.rb', line 114

def asset_group_name
  @asset_group_name
end

Class Method Details

.create_asset_group_by_name(name, study) ⇒ Object


128
129
130
131
132
# File 'app/models/sample_registrar.rb', line 128

def self.create_asset_group_by_name(name, study)
  return nil if name.blank?

  AssetGroup.find_by(name: name) || AssetGroup.create!(name: name, study: study)
end

.from_spreadsheet(file, study, user) ⇒ Object


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

def self.from_spreadsheet(file, study, user)
  (workbook = Spreadsheet.open(file.path)) || raise(SpreadsheetError, 'Problems processing your file. Only Excel spreadsheets accepted')
  worksheet = workbook.worksheet(0)

  # Assume there is always 1 header row
  num_samples = worksheet.count - 1

  raise TooManySamplesError, "You can only load #{configatron.uploaded_spreadsheet.max_number_of_samples} samples at a time. Please split the file into smaller groups of samples." if num_samples > configatron.uploaded_spreadsheet.max_number_of_samples

  # Map the header from the spreadsheet (the first row) to the attributes of the sample registrar.  Each column
  # has the same text as the label for the attribute, once it has been HTML unescaped.
  #
  # NOTE: There are two different versions of the spreadsheet in the wild.  One has a 'Volume' column name that
  # needs to be decoded using CGI HTML unescaping (the old format), and the other needs the column decoded
  # using the XML encoding (the new format).  Every column is mapped using both encodings, with the XML version
  # being the preferred decoding.
  definitions = Sample::Metadata.attribute_details.inject({}) do |hash, attribute|
    label   = attribute.to_field_info.display_name
    handler = ->(attributes, value) { attributes[:sample_attributes][:sample_metadata_attributes][attribute.name] = value }
    hash.tap do
      hash[CGI.unescapeHTML(label)]        = handler   # For the old spreadsheets
      hash[REXML::Text.unnormalize(label)] = handler   # For the new spreadsheets
    end
  end.merge(
    'Asset group' => ->(attributes, value) { attributes[:asset_group_name] = value },
    'Sample name' => ->(attributes, value) { attributes[:sample_attributes][:name] = value },
    '2D barcode' => ->(attributes, value) { attributes[:sample_tube_attributes][:two_dimensional_barcode] = value },
    'Reference Genome' => ->(attributes, value) { attributes[:sample_attributes][:sample_metadata_attributes][:reference_genome_id] = ReferenceGenome.find_by(name: value).try(:id) || 0 }
  )

  # Map the headers to their attribute handlers.  Ensure that the required headers are present.
  used_definitions = []
  headers = []
  column_index = 0
  column_name = worksheet.cell(0, 0).to_s.gsub(/\000/, '').gsub(/\.0/, '').strip
  until column_name.empty?
    column_name = REMAPPED_COLUMN_NAMES.fetch(column_name, column_name)
    handler     = definitions[column_name]
    unless handler.nil?
      used_definitions[column_index] = handler
      headers << column_name
    end

    column_index += 1
    column_name = worksheet.cell(0, column_index).to_s.gsub(/\000/, '').gsub(/\.0/, '').strip
  end

  raise SpreadsheetError, "Please check that your spreadsheet is in the latest format: one of #{REQUIRED_COLUMNS_SENTENCE} is missing or in the wrong column." if (headers & REQUIRED_COLUMNS) != REQUIRED_COLUMNS

  # Build a SampleRegistrar instance for each row of the spreadsheet, mapping the cells of the
  # spreadsheet to their appropriate attribute.
  sample_registrars = []
  1.upto(num_samples) do |row|
    attributes = {
      asset_group_helper: SampleRegistrar::AssetGroupHelper.new,
      sample_attributes: {
        sample_metadata_attributes: {}
      },
      sample_tube_attributes: {}
    }

    used_definitions.each_with_index do |column_handler, index|
      next if column_handler.nil?

      value = worksheet.cell(row, index).to_s.gsub(/\000/, '').gsub(/\.0/, '').strip
      column_handler.call(attributes, value) if value.present?
    end
    next if attributes[:sample_attributes][:name].blank?

    # Store the sample registration and check that it is valid.  This will mean that the
    # UI will display any errors without the user having to submit the form to find out.

    SampleRegistrar.new(attributes.merge(study: study, user: user)).tap do |sample_registrar|
      sample_registrars.push(sample_registrar)
      sample_registrar.valid?
    end
  end

  return sample_registrars
rescue Ole::Storage::FormatError
  raise SpreadsheetError, 'Problems processing your file. Only Excel spreadsheets accepted'
end

.register!(registration_attributes) ⇒ Object

This method is the main registration interface, taking a list of attributes and registering the associated sample and sample tubes. You get back a list of SampleRegistrar instances. If anything goes wrong you get a RegistrationError raised.

Raises:


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

def self.register!(registration_attributes)
  # Note that we're explicitly ignoring the ignored records here!

  helper     = AssetGroupHelper.new
  registrars = registration_attributes.map { |attributes| new(attributes.merge(asset_group_helper: helper)) }.reject(&:ignore?)
  raise NoSamplesError, registrars if registrars.empty?

  begin
    # We perform this in a database wide transaction because it is altering several tables.  It also locks
    # the tables from change whilst we validate our instances.
    transaction do
      # We don't use all? here as we don't want to lazily validate our registrars, otherwise we only return one
      # problem at a time, and annoy our users no-end
      all_valid = registrars.inject(true) { |all_valid_so_far, registrar| registrar.valid? && all_valid_so_far }
      raise RegistrationError, registrars unless all_valid

      registrars.each(&:save!)
    end

    registrars
  rescue ActiveRecord::RecordInvalid
    # NOTE: this shouldn't ever happen but you never know!
    raise RegistrationError, registrars
  end
end

Instance Method Details

#ignore=(ignore) ⇒ Object


149
150
151
# File 'app/models/sample_registrar.rb', line 149

def ignore=(ignore)
  @ignore = (ignore == '1')
end

#ignore?Boolean Also known as: ignore

Is this instance to be ignored?

Returns:

  • (Boolean)

144
145
146
# File 'app/models/sample_registrar.rb', line 144

def ignore?
  @ignore
end

#sampleObject

Build by default


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

def sample
  super || build_sample
end

#sample_tubeObject


91
92
93
# File 'app/models/sample_registrar.rb', line 91

def sample_tube
  super || build_sample_tube
end