Skip to content

Commit

Permalink
Merge branch 'develop' into develop-Y24-190
Browse files Browse the repository at this point in the history
  • Loading branch information
sdjmchattie committed Aug 12, 2024
2 parents 4fc7eeb + f9d884b commit 33f7d05
Show file tree
Hide file tree
Showing 23 changed files with 254 additions and 81 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module.exports = {
parser: '@babel/eslint-parser',
sourceType: 'module',
requireConfigFile: false,
ecmaVersion: 2018,
},
rules: {
'linebreak-style': ['error', 'unix'],
Expand Down
2 changes: 1 addition & 1 deletion .release-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.56.0
3.56.1
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

## Description

A flexible front end to plate bases pipelines in Sequencescape.
A flexible front end to pipelines in Sequencescape.

## User Requirements

- Used on laboratory instrument machines often running older browser versions due to vendor and network limitations.
The user-agent strings extracted from the nginx access logs of August 2024 indicate that the the oldest browser suspected of using Limber is Chrome 65. This means that the minified code served to browsers should be compatible with [ECMAScript 2018](https://www.w3schools.com/js/js_2018.asp).
Please see the [Limber page on Confluence](https://ssg-confluence.internal.sanger.ac.uk/display/PSDPUB/LIMBer) for more information.

## Initial Setup (using Docker)

Expand Down
1 change: 1 addition & 0 deletions app/frontend/javascript/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
parser: '@babel/eslint-parser',
sourceType: 'module',
requireConfigFile: false,
ecmaVersion: 2018,
},
rules: {
'linebreak-style': ['error', 'unix'],
Expand Down
20 changes: 18 additions & 2 deletions app/frontend/javascript/multi-stamp/components/MultiStamp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@
import LabwareScan from '@/javascript/shared/components/LabwareScan.vue'
import LoadingModal from '@/javascript/shared/components/LoadingModal.vue'
import Plate from '@/javascript/shared/components/Plate.vue'
import { checkDuplicates, checkSize } from '@/javascript/shared/components/plateScanValidators.js'
import {
checkDuplicates,
checkSize,
checkForUnacceptablePlatePurpose,
} from '@/javascript/shared/components/plateScanValidators.js'
import devourApi from '@/javascript/shared/devourApi.js'
import buildPlateObjs from '@/javascript/shared/plateHelpers.js'
import { handleFailedRequest, requestIsActive, requestsFromPlates } from '@/javascript/shared/requestHelpers.js'
Expand Down Expand Up @@ -127,6 +131,15 @@ export default {
// Default volume to define in the UI for the volume control
defaultVolume: { type: String, required: false, default: null },
// Acceptable plate purpose names that can be used as source plates.
// Defines a prop `acceptablePurposes` that accepts a string. It is optional and
// defaults to a string representation of an array '[]' if not provided.
// e.g. "['PurposeA', 'PurposeB']"
// See computed method acceptablePurposesArray for conversion to array.
// If present is used in scanValidation to check if the user has scanned an
// a plate of the correct type.
acceptablePurposes: { type: String, required: false, default: '[]' },
},
data() {
return {
Expand Down Expand Up @@ -175,6 +188,9 @@ export default {
targetColumnsNumber() {
return Number.parseInt(this.targetColumns)
},
acceptablePurposesArray() {
return JSON.parse(this.acceptablePurposes)
},
valid() {
return (
this.unsuitablePlates.length === 0 && // None of the plates are invalid
Expand Down Expand Up @@ -257,7 +273,7 @@ export default {
return [
checkSize(12, 8),
checkDuplicates(currPlates),
// checkExcess(this.excessTransfers)
checkForUnacceptablePlatePurpose(this.acceptablePurposesArray),
]
},
},
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/javascript/multi-stamp/components/filterProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ const filterProps = {
},
null: {
plateFields: {
plates: 'labware_barcode,wells,uuid,number_of_rows,number_of_columns',
plates: 'labware_barcode,purpose,wells,uuid,number_of_rows,number_of_columns',
requests: 'uuid,state',
wells: 'position,requests_as_source,aliquots,uuid',
aliquots: 'request',
},
plateIncludes: 'wells,wells.requests_as_source,wells.aliquots.request',
plateIncludes: 'purpose,wells,wells.requests_as_source,wells.aliquots.request',
requestsFilter: 'lb-null-filter',
},
}
Expand Down
2 changes: 1 addition & 1 deletion app/frontend/javascript/shared/components/LabwareScan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default {
uid += 1
return {
labwareBarcode: '', // The scanned barcode
labware: null, // The tube object
labware: null, // The labware object
uid: `labware-scan-${uid}`, // Unique id to ensure label identifies the correct field
apiActivity: { state: null, message: '' }, // API status
}
Expand Down
59 changes: 33 additions & 26 deletions app/frontend/javascript/shared/components/plateScanValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,31 +84,6 @@ const checkDuplicates = (plateList) => {
}
}

// Returns a validator that checks if there are wells in the scanned plate that
// are in excess (i.e. the sum of valid transfers across scanned
// plates is greater than the number of wells in the target plate).
// It also returns the excess wells' position.
// excessTransfers: An array of transfers that cannot be included in the
// target plate as all the wells are already occupied.
const checkExcess = (excessTransfers) => {
return (plate) => {
const excessWells = []
for (let i = 0; i < excessTransfers.length; i++) {
if (plate && excessTransfers[i].plateObj.plate.uuid === plate.uuid) {
excessWells.push(excessTransfers[i].well.position.name)
}
}
if (excessWells.length > 0) {
return {
valid: false,
message: 'Wells in excess: ' + excessWells.join(', '),
}
} else {
return validScanMessage()
}
}
}

// Returns a validator that ensures the plate has a state that matches to the
// supplied list of states. e.g. to check a plate has a state of 'available'
// or 'exhausted':
Expand Down Expand Up @@ -375,10 +350,41 @@ const checkPlateWithSameReadyLibrarySubmissions = (cached_submission_ids) => {
}
}

// Checks that the scanned plate's purpose matches one of those in the provided list.
// Args:
// acceptable_purposes - An array of acceptable plate purpose name strings e.g. ['Purpose1', 'Purpose2']
// plate - Plate object that contains the plate purpose
// Returns:
// Validation object indicating if the plate has passed the condition
const checkForUnacceptablePlatePurpose = (acceptable_purposes) => {
return (plate) => {
if (!acceptable_purposes || acceptable_purposes.length == 0) {
// return valid if no acceptable purposes are provided
return validScanMessage()
} else if (!plate.purpose) {
// guard for plate not having a purpose (should not happen)
return {
valid: false,
message: 'The scanned plate does not have a plate purpose',
}
} else if (acceptable_purposes.includes(plate.purpose.name)) {
// if we find a matching purpose, return valid
return validScanMessage()
} else {
return {
valid: false,
message:
'The scanned plate has an unacceptable plate purpose type (should be ' +
acceptable_purposes.join(' or ') +
')',
}
}
}
}

export {
checkSize,
checkDuplicates,
checkExcess,
checkLibraryTypesInAllWells,
getAllLibrarySubmissionsWithMatchingStateForPlate,
checkAllLibraryRequestsWithSameReadySubmissions,
Expand All @@ -389,4 +395,5 @@ export {
checkMaxCountRequests,
checkMinCountRequests,
checkAllSamplesInColumnsList,
checkForUnacceptablePlatePurpose,
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
checkLibraryTypesInAllWells,
checkSize,
checkDuplicates,
checkExcess,
checkState,
checkQCableWalkingBy,
checkForUnacceptablePlatePurpose,
} from '@/javascript/shared/components/plateScanValidators'

describe('checkSize', () => {
Expand Down Expand Up @@ -65,39 +65,6 @@ describe('checkDuplicates', () => {
})
})

describe('checkExcess', () => {
it('passes when the plate is not the source of excess transfers', () => {
const plate = { uuid: 'plate-uuid-1' }
const other_plate = { uuid: 'plate-uuid-2' }
const excessTransfers = [
{
plateObj: { plate: other_plate },
},
]

expect(checkExcess(excessTransfers)(plate)).toEqual({ valid: true })
})

it('fails when the plate is the source of excess transfers', () => {
const plate = { uuid: 'plate-uuid-1' }
const excessTransfers = [
{
plateObj: { plate: plate },
well: { position: { name: 'D11' } },
},
{
plateObj: { plate: plate },
well: { position: { name: 'D12' } },
},
]

expect(checkExcess(excessTransfers)(plate)).toEqual({
valid: false,
message: 'Wells in excess: D11, D12',
})
})
})

describe('checkState', () => {
it('passes if the state is in the allowed list', () => {
const plate = { state: 'available' }
Expand Down Expand Up @@ -686,3 +653,45 @@ describe('checkPlateWithSameReadyLibrarySubmissions', () => {
})
})
})

describe('checkForUnacceptablePlatePurpose', () => {
const plate1 = {
purpose: { name: 'PurposeA' },
}

const plate2 = {
purpose: { name: 'PurposeB' },
}

const plate3 = {}

describe('when there is not a list of acceptable purposes', () => {
const validator = checkForUnacceptablePlatePurpose([])

it('validates when there is no list of acceptable purposes', () => {
expect(validator(plate1)).toEqual({ valid: true })
})
})

describe('when there is a list of acceptable purposes', () => {
const validator = checkForUnacceptablePlatePurpose(['PurposeA', 'PurposeC'])

it('validates when the plate has an acceptable purpose', () => {
expect(validator(plate1)).toEqual({ valid: true })
})

it('fails when the plate has an unacceptable purpose', () => {
expect(validator(plate2)).toEqual({
valid: false,
message: 'The scanned plate has an unacceptable plate purpose type (should be PurposeA or PurposeC)',
})
})

it('fails when the plate does not have a purpose', () => {
expect(validator(plate3)).toEqual({
valid: false,
message: 'The scanned plate does not have a plate purpose',
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ describe('TransferVolumes', () => {
wrapper.vm.destinationTubeValidators // Refresh the evaluation to cause more calls to checkId

// After setting a source tube
console.log(checkId.mock.calls)
expect(checkId.mock.calls[1][0]).toEqual(['test1', 'test2'])
expect(checkId.mock.calls[1][1]).toEqual('Does not match the source tube')
})
Expand Down
4 changes: 3 additions & 1 deletion app/models/labware_creators/multi_stamp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class MultiStamp < Base # rubocop:todo Style/Documentation
:transfers_creator,
:target_rows,
:target_columns,
:source_plates
:source_plates,
:acceptable_purposes

self.page = 'multi_stamp'
self.aliquot_partial = 'standard_aliquot'
Expand All @@ -22,6 +23,7 @@ class MultiStamp < Base # rubocop:todo Style/Documentation
self.target_rows = 0
self.target_columns = 0
self.source_plates = 0
self.acceptable_purposes = []

validates :transfers, presence: true

Expand Down
9 changes: 9 additions & 0 deletions app/models/labware_creators/ten_stamp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ class TenStamp < MultiStamp # rubocop:todo Style/Documentation
self.target_columns = 12
self.source_plates = 10

def acceptable_purposes
# catch for older uses of tenstamp in purpose_config where creator_class is a string
if purpose_config[:creator_class].is_a?(Hash) && purpose_config.dig(:creator_class, :args, :acceptable_purposes)
Array(purpose_config.dig(:creator_class, :args, :acceptable_purposes))
else
[]
end
end

private

def request_hash(transfer, *args)
Expand Down
4 changes: 2 additions & 2 deletions app/models/presenters/minimal_pcr_plate_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class MinimalPcrPlatePresenter < MinimalPlatePresenter # rubocop:todo Style/Docu
self.summary_partial = 'labware/plates/pcr_summary'
self.state_transition_name_scope = :pcr

# summary_items is a hash of a label label, and a symbol representing the
# method to call to get the value
# Initializes `summary_items` with a hash mapping display names to their corresponding plate attributes.
# Used by the summary panel to display information about the plate in the GUI.
self.summary_items = {
'Barcode' => :barcode,
'Number of wells' => :number_of_wells,
Expand Down
29 changes: 29 additions & 0 deletions app/models/presenters/pcr_with_primer_panel_plate_presenter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Presenters
# This Presenter shows the PCR primer panel on the left along with the standard plate layout
# view and summary information. This version allows you to fail wells on the plate.
#
# This was specifically requested by the lab team for the GBS pipleine PCR1 plate so they
# could fail wells. If well failing functionality was allowed on stock plates in future, that would be a
# preferable option for them and they could fail the wells on the parent plate, and the PCR1 plate could
# be returned to using the MinimalPcrPlatePresenter.
class PcrWithPrimerPanelPlatePresenter < StandardPresenter
include HasPrimerPanel
self.summary_partial = 'labware/plates/pcr_with_primer_panel_summary'
self.state_transition_name_scope = :pcr

# Initializes `summary_items` with a hash mapping display names to their corresponding plate attributes.
# Used by the summary panel to display information about the plate in the GUI.
self.summary_items = {
'Barcode' => :barcode,
'Number of wells' => :number_of_wells,
'Plate type' => :purpose_name,
'Primer panel' => :panel_name,
'Current plate state' => :state,
'Input plate barcode' => :input_barcode,
'PCR Cycles' => :requested_pcr_cycles,
'Created on' => :created_on
}
end
end
4 changes: 2 additions & 2 deletions app/models/presenters/plate_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ class PlatePresenter
self.pooling_tab = 'plates/pooling_tab'
self.samples_partial = 'plates/samples_tab'

# summary_items is a hash of a label label, and a symbol representing the
# method to call to get the value
# Initializes `summary_items` with a hash mapping display names to their corresponding plate attributes.
# Used by the summary panel to display information about the plate in the GUI.
self.summary_items = {
'Barcode' => :barcode,
'Number of wells' => :number_of_wells,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div id="labware-summary-div" class="card-body text-white bg-info">
<dl>
<dt>Primer Panel</dt><dd><%= presenter.panel_name %></dd>
<dt>PCR Program</dt><dd><%= presenter.pcr_program %></dd>
<dt>PCR Duration</dt><dd><%= presenter.pcr_duration %></dd>
</dl>
</div>

<%= render partial: 'labware/plates/standard_summary', locals: { presenter: presenter } %>
1 change: 1 addition & 0 deletions app/views/plate_creation/multi_stamp.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
data-target-rows="<%= @labware_creator.target_rows %>"
data-target-columns="<%= @labware_creator.target_columns %>"
data-source-plates="<%= @labware_creator.source_plates %>"
data-acceptable-purposes="<%= @labware_creator.acceptable_purposes %>"
>
<div class="spinner-dark">Loading...</div>
</div>
Loading

0 comments on commit 33f7d05

Please sign in to comment.