Skip to content

Commit

Permalink
Merge branch 'update_toolchain' of https://github.com/nerdstrike/npg_…
Browse files Browse the repository at this point in the history
…langqc into update_toolchain
  • Loading branch information
nerdstrike committed Mar 26, 2024
2 parents f247412 + 25ce043 commit 938f879
Show file tree
Hide file tree
Showing 38 changed files with 3,125 additions and 850 deletions.
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Changed

* To simplify object instantiation and fields' assignment for some
of the response models, converted `PacBioWell` and `PacBioWellFull`
models to pydantic dataclasses.
* Changed the response model for filtered by either QC status or run wells from
`PacBioWell` to `PacBioWellSummary`, the latter initially being identical
the former. In order to propagate information about a study to the tabbed
well summary view, added a new field, study_names, to the `PacBioWellSummary`
model.
* Added a new event to the tabbed well summary view, to the button with the well
name. Mouse hover over this button displays study names associated with the
well.
* Changed the colour scheme of the above mentioned button from grey to orange
if one of the studies associated with the well is the BIOSCAN study, which
the QC team needs to deal with slightly differently.
* Added a new QC state 'On hold external'. Semantically the new state is similar
to the existing 'On hold' state. The intended purpose of the new QC state - to
highlight the wells, which are waiting for a completion of some off-site
process (example - deplexing at http://mbrave.net/).

### Added

* A new response model `PacBioWellSummary`, which replaces `PacBioWell`
in the latest's capacity of the response model for a PacBio well
summary.
* A new field, `study_names`, a potentially empty sorted array of
unique study names, is added to the response model for a PacBio
well summary.

## [2.0.0] - 2024-02-20

### Changed
Expand Down
22 changes: 22 additions & 0 deletions alembic/versions/2.1.0_extend_qc_state_dict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""extend_qc_state_dict
Revision ID: 2.1.0
Revises: 2.0.0
Create Date: 2024-03-19 12:31:26.359652
"""
from alembic import op

# revision identifiers, used by Alembic.
revision = "2.1.0"
down_revision = "2.0.0"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute("INSERT INTO qc_state_dict VALUES ('On hold external', NULL)")


def downgrade() -> None:
op.execute("DELETE FROM qc_state_dict WHERE state='On hold external'")
2 changes: 1 addition & 1 deletion frontend/src/components/QcView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { computed } from "vue";
import groupMetrics from "../utils/metrics.js";
import combineLabelWithPlate from "../utils/text.js"
import { combineLabelWithPlate } from "../utils/text.js"
const props = defineProps({
// Well object representing one prepared input for the instrument
Expand Down
20 changes: 16 additions & 4 deletions frontend/src/components/WellTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
/*
* Renders a table for a list of wells and generates buttons for selecting wells
*/
import combineLabelWithPlate from "../utils/text.js"
import { combineLabelWithPlate, listStudiesForTooltip } from "../utils/text.js"
import { ElTooltip, ElButton } from "element-plus";
const tooltipDelay = 500
const studyNameHighlight = 'BIOSCAN UK for flying insects'
defineProps({
wellCollection: Object
Expand All @@ -28,9 +32,17 @@ defineEmits(['wellSelected'])
<tr :key="wellObj.id_product" v-for="wellObj in wellCollection">
<td>{{ wellObj.run_name }}</td>
<td class="well_selector">
<button v-on:click="$emit('wellSelected', { idProduct: wellObj.id_product })">
{{ combineLabelWithPlate(wellObj.label, wellObj.plate_number) }}
</button>
<el-tooltip placement="top" effect="light" :show-after="tooltipDelay"
:content="'<span>'.concat(listStudiesForTooltip(wellObj.study_names)).concat('</span>')"
raw-content
>
<el-button
:type="wellObj.study_names.includes(studyNameHighlight) ? 'warning' : 'info'"
v-on:click="$emit('wellSelected', { idProduct: wellObj.id_product })"
>
{{ combineLabelWithPlate(wellObj.label, wellObj.plate_number) }}
</el-button>
</el-tooltip>
</td>
<td>{{ wellObj.instrument_type }} {{ wellObj.instrument_name }}</td>
<td>{{ wellObj.qc_state ? wellObj.qc_state.qc_state : '&nbsp;' }}</td>
Expand Down
39 changes: 35 additions & 4 deletions frontend/src/components/__tests__/WellTable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,33 @@ describe('Rows of data give rows in the table', () => {
const table = mount(WellTable, {
props: {
wellCollection: [
{run_name: 'TEST1', label: 'A1', plate_number: null, instrument_name: '1234', instrument_type: 'Revio', id_product: 'ABCDEF'},
{run_name: 'TEST1', label: 'B1', plate_number: null, instrument_name: '1234', instrument_type: 'Revio', id_product: '123456'},
{run_name: 'TEST2', label: 'A1', plate_number: 1, instrument_name: '1234', instrument_type: 'Revio', id_product: '123457'},
{
run_name: 'TEST1',
label: 'A1',
plate_number: null,
instrument_name: '1234',
instrument_type: 'Revio',
id_product: 'ABCDEF',
study_names: []
},
{
run_name: 'TEST1',
label: 'B1',
plate_number: null,
instrument_name: '1234',
instrument_type: 'Revio',
id_product: '123456',
study_names: ['Study name 1', 'Study name 2']
},
{
run_name: 'TEST2',
label: 'A1',
plate_number: 1,
instrument_name: '1234',
instrument_type: 'Revio',
id_product: '123457',
study_names: ['BIOSCAN UK for flying insects']
},
]
}
})
Expand Down Expand Up @@ -40,7 +64,14 @@ describe('Rows of data give rows in the table', () => {
expect(table.emitted().wellSelected[0][0]).toHaveProperty('idProduct')
expect(table.emitted().wellSelected[0][0].idProduct).toEqual('ABCDEF')

await rows[2].find('button').trigger('click')
let wellButton = rows[2].find('button')
await wellButton.trigger('click')
expect(table.emitted().wellSelected[1][0].idProduct).toEqual('123456')
expect(wellButton.classes('el-tooltip__trigger')).toBeTruthy()
expect(wellButton.classes('el-button--info')).toBeTruthy()

wellButton = rows[3].find('button')
expect(wellButton.classes('el-tooltip__trigger')).toBeTruthy()
expect(wellButton.classes('el-button--warning')).toBeTruthy()
})
})
19 changes: 17 additions & 2 deletions frontend/src/utils/__tests__/text.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, test, expect } from 'vitest'
import combineLabelWithPlate from '../text'
import { combineLabelWithPlate, listStudiesForTooltip } from '../text'

describe('Text processing', () => {
describe('Well label and plate display', () => {
test('Print well label without plate number', () => {
expect(combineLabelWithPlate('A1', undefined)).toEqual('A1')
expect(combineLabelWithPlate('A1', null)).toEqual('A1')
Expand All @@ -11,3 +11,18 @@ describe('Text processing', () => {
expect(combineLabelWithPlate('A1', 1)).toEqual('1-A1')
})
})

describe('Study names tooltip', () => {
test('Display a warning in case of an empty study array', () => {
expect(listStudiesForTooltip([])).toEqual('No study info')
})

test('Display a single study name for an array of one study', () => {
expect(listStudiesForTooltip(['My single study'])).toEqual('My single study')
})

test('Display multiple lines for multiple studies', () => {
expect(listStudiesForTooltip(['Study One', 'Study two', 'Study 3'])).toEqual(
'Study One<br />Study two<br />Study 3')
})
})
9 changes: 8 additions & 1 deletion frontend/src/utils/text.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
// Reusable text-mangling for the interface

export default function combineLabelWithPlate(well, plate) {
export { combineLabelWithPlate, listStudiesForTooltip }

function combineLabelWithPlate(well, plate) {
if (!plate) {
return well
} else {
return `${plate}-${well}`
}
}

function listStudiesForTooltip(study_names) {
let names = study_names.length == 0 ? ['No study info'] : study_names
return names.join('<br />')
}
3 changes: 2 additions & 1 deletion frontend/src/views/__tests__/WellsByRun.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ for (let index = 0; index < 2; index++) {
instrument_name: "1234",
instrument_type: "Revio",
id_product: `${index}23456`,
study_names: [`Study ${index}`, 'Another study'],
...someLinkGeneration
})
}
Expand All @@ -48,6 +49,7 @@ const secondaryRun = {
instrument_name: '1234',
instrument_type: 'Revio',
id_product: 'ABCDEF',
study_names: [],
...someLinkGeneration
}

Expand Down Expand Up @@ -141,7 +143,6 @@ describe('Does it work?', async () => {
let buttons = wrapper.findAll('button')
buttons[1].trigger('click')
await flushPromises()

expect(wrapper.get('#well_summary').exists()).toBe(true)
})

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/views/__tests__/WellsByStatus.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ for (let index = 0; index < 10; index++) {
well_status: "Complete",
qc_state: null,
instrument_name: 1234,
instrument_type: 'Revio'
instrument_type: 'Revio',
study_names: ['Some study'],
})
}

Expand Down Expand Up @@ -140,6 +141,7 @@ describe('View loads configuration on mount', async () => {
qc_state: null,
instrument_name: '1234',
instrument_type: 'Revio',
study_names: [],
}]
})
)
Expand Down
16 changes: 8 additions & 8 deletions lang_qc/db/helper/wells.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
)
from lang_qc.db.mlwh_schema import PacBioRunWellMetrics
from lang_qc.db.qc_schema import QcState, QcStateDict, QcType
from lang_qc.models.pacbio.well import PacBioPagedWells, PacBioWell
from lang_qc.models.pacbio.well import PacBioPagedWells, PacBioWellSummary
from lang_qc.models.pager import PagedResponse
from lang_qc.models.qc_flow_status import QcFlowStatusEnum
from lang_qc.models.qc_state import QcState as QcStateModel
Expand Down Expand Up @@ -169,10 +169,10 @@ class PacBioPagedWellsFactory(WellWh, PagedResponse):
# For MySQL it's OK to use case-sensitive comparison operators since
# its string comparisons for the collation we use are case-insensitive.
FILTERS: ClassVar = {
QcFlowStatusEnum.ON_HOLD.name: (QcStateDict.state == "On hold"),
QcFlowStatusEnum.ON_HOLD.name: (QcStateDict.state.ilike("On hold%")),
QcFlowStatusEnum.QC_COMPLETE.name: (QcState.is_preliminary == 0),
QcFlowStatusEnum.IN_PROGRESS.name: and_(
QcState.is_preliminary == 1, QcStateDict.state != "On hold"
QcState.is_preliminary == 1, QcStateDict.state.notilike("On hold%")
),
QcFlowStatusEnum.ABORTED.name: or_(
PacBioRunWellMetrics.well_status.like("Abort%"),
Expand All @@ -195,7 +195,7 @@ def create_for_qc_status(
specified by the `page_size`, `page_number` object's attributes and
`qc_flow_status` argument of this function..
The `PacBioWell` objects in `wells` attribute of the returned object
The `PacBioWellPacBioWell` objects in `wells` attribute of the returned object
are sorted in a way appropriate for the requested `qc_flow_status`.
For the 'in progress' and 'on hold' requests the wells with most recently
assigned QC states come first. For inbox requests the wells with least
Expand Down Expand Up @@ -230,7 +230,7 @@ def create_for_run(self, run_name: str) -> PacBioPagedWells:
"""
Returns `PacBioPagedWells` object that corresponds to the criteria
specified by the `page_size` and `page_number` attributes.
The `PacBioWell` objects in `wells` attribute of the returned object
The `PacBioWellSummary` objects in `wells` attribute of the returned object
belong to runs specified by the `run_name` argument and are sorted
by the run name and well label.
"""
Expand Down Expand Up @@ -281,7 +281,7 @@ def _retrieve_paged_qc_states(

def _get_wells_for_status(
self, qc_flow_status: QcFlowStatusEnum
) -> List[PacBioWell]:
) -> List[PacBioWellSummary]:

wells = []

Expand All @@ -290,7 +290,7 @@ def _get_wells_for_status(
id_product = qc_state_model.id_product
mlwh_well = self.get_mlwh_well_by_product_id(id_product=id_product)
if mlwh_well is not None:
pbw = PacBioWell(db_well=mlwh_well, qc_state=qc_state_model)
pbw = PacBioWellSummary(db_well=mlwh_well, qc_state=qc_state_model)
wells.append(pbw)
else:
"""
Expand Down Expand Up @@ -394,7 +394,7 @@ def _well_models(
qc_state = None
if id_product in qced_products:
qc_state = qced_products[id_product][0]
pb_well = PacBioWell(db_well=db_well, qc_state=qc_state)
pb_well = PacBioWellSummary(db_well=db_well, qc_state=qc_state)
pb_wells.append(pb_well)

return pb_wells
Expand Down
21 changes: 21 additions & 0 deletions lang_qc/db/mlwh_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,27 @@ class PacBioRunWellMetrics(Base):
"PacBioProductMetrics", back_populates="pac_bio_run_well_metrics"
)

def get_experiment_info(self):
"""Returns a list of PacBioRun mlwh database rows.
Returns LIMS information about the PacBio experiment
for this well, one pac_bio_run table row per sample
(product) in the well.
If any or all of the pac_bio_product_metrics rows linked
to this well record are not linked to the pac_bio_run
table, and empty array is returned, thus preventing incomplete
data being supplied to the client.
"""
product_metrics = self.pac_bio_product_metrics
experiment_info = [
pbr for pbr in [pm.pac_bio_run for pm in product_metrics] if pbr is not None
]
if len(experiment_info) != len(product_metrics):
experiment_info = []

return experiment_info


class PacBioProductMetrics(Base):
__tablename__ = "pac_bio_product_metrics"
Expand Down
4 changes: 3 additions & 1 deletion lang_qc/endpoints/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def _states_for_update(session) -> List:
states = []
for (name, row) in qc_state_dict(session).items():
if name not in ["Aborted", "Claimed"]:
states.append({"description": name, "only_prelim": row.state == "On hold"})
states.append(
{"description": name, "only_prelim": "on hold" in row.state.lower()}
)

return states
Loading

0 comments on commit 938f879

Please sign in to comment.