Skip to content

Commit

Permalink
Merge pull request #179 from wtsi-npg/devel
Browse files Browse the repository at this point in the history
Release 1.4.0
  • Loading branch information
nerdstrike authored Aug 16, 2023
2 parents aaa9118 + 83f4988 commit 804c5a2
Show file tree
Hide file tree
Showing 43 changed files with 1,093 additions and 626 deletions.
17 changes: 16 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,22 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
## [1.4.0] - 2023-08-16

### Added

* Tests for bulk retrieval of QC states
* Third dimension for describing wells added to QC Schema to support multi-plate runs

### Changed

* Browser URLs now use id_product to identify items to show in the QC Viewer instead of the combination of run, label (and plate number)
* More strict validation of checksums (id_product) when sent to backend API

### Fixed

* A bug that disregarded the value of the `sequencing_outcomes_only`
argument in what is now the `get_qc_states_by_id_product_list` function

## [1.3.0] - 2023-08-02

Expand Down
3 changes: 2 additions & 1 deletion alembic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ What you need:
- the URL for that database set in `ALEMBIC_DB_URL`

Set `ALEMBIC_DB_URL` env. variable to the SQLAlchemy URL for the database you
want to operate on. The format is `driver://user:pass@host:port/dbname`.
want to operate on. The format is `driver://user:pass@host:port/dbname`, where
for MySQL and our software stack the driver is `mysql+pymysql`.

Ensure `alembic` in on your PATH. Create a revision:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Add ability to record plate number
Revision ID: 36952df5b8ba
Revises: dd60c67ad3e5
Create Date: 2023-08-03 13:32:06.820978
"""
from alembic import op

# revision identifiers, used by Alembic.
revision = "36952df5b8ba"
down_revision = "dd60c67ad3e5"
branch_labels = None
depends_on = None


def upgrade() -> None:
sql = """
INSERT INTO sub_product_attr (`attr_name`, `description`)
VALUES ('plate_number', 'An optional PacBio plate number')
"""
op.execute(sql)

sql = """
ALTER TABLE sub_product
ADD COLUMN `id_attr_three` INT DEFAULT NULL AFTER `value_attr_two`,
ADD COLUMN `value_attr_three` VARCHAR(20) DEFAULT NULL AFTER `id_attr_three`,
ADD KEY `ix_sub_product_id_attr_three` (`id_attr_three`),
ADD KEY `ix_sub_product_value_attr_three` (`value_attr_three`),
ADD CONSTRAINT `fk_subproduct_attr3` FOREIGN KEY (`id_attr_three`)
REFERENCES `sub_product_attr` (`id_attr`);
"""
op.execute(sql)


def downgrade() -> None:
sql = """
ALTER TABLE sub_product
DROP KEY `ix_sub_product_id_attr_three`,
DROP KEY `ix_sub_product_value_attr_three`,
DROP CONSTRAINT `fk_subproduct_attr3`,
DROP COLUMN `value_attr_three`,
DROP COLUMN `id_attr_three`;
"""
op.execute(sql)

sql = """
DELETE FROM sub_product_attr WHERE attr_name='plate_number';
"""
op.execute(sql)
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "npg-longue-vue",
"version": "1.3.0",
"version": "1.4.0",
"description": "UI for LangQC",
"author": "Kieron Taylor <kt19@sanger.ac.uk>",
"license": "GPL-3.0-or-later",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/ClaimWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ const props = defineProps({
let client = new LangQc();
function claimHandler() {
const [runName, wellLabel] = focusWell.getRunAndLabel;
if (!(runName && wellLabel)) {
throw new Error('Claim environment misconfigured, no run name or well label');
const id = focusWell.getIdProduct;
if (!id) {
throw new Error('Claim environment misconfigured, needs an id_product');
}
client.claimWell(runName, wellLabel)
client.claimWell(id)
.then(
(response) => {
focusWell.updateWellQcState(response);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/QcWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ function syncWidgetToQcState() {
}
function submitQcState() {
let [name, well] = focusWell.getRunAndLabel;
const id_product = focusWell.getIdProduct;
client.setWellQcState(name, well, widgetQcSetting.value, widgetFinality.value)
client.setWellQcState(id_product, widgetQcSetting.value, widgetFinality.value)
.then(
(response) => {
focusWell.updateWellQcState(response);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/WellTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ defineEmits(['wellSelected'])
<tr :key="wellObj.run_name + ':' + wellObj.label" v-for="wellObj in wellCollection">
<td>{{ wellObj.run_name }}</td>
<td class="well_selector">
<button v-on:click="$emit('wellSelected', { qcRun: wellObj.run_name, qcLabel: wellObj.label })">
<button v-on:click="$emit('wellSelected', { idProduct: wellObj.id_product })">
{{ combineLabelWithPlate(wellObj.label, wellObj.plate_number) }}
</button>
</td>
Expand All @@ -48,4 +48,4 @@ defineEmits(['wellSelected'])
#run_wells {
width: 100%;
}
</style>
</style>
4 changes: 3 additions & 1 deletion frontend/src/components/__tests__/ClaimWidget.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('Clicking triggers POST and side-effects', () => {
// A typical claim success
fetch.mockResponse(
JSON.stringify({
id_product: 'ABCDEF',
qc_state: 'Claimed',
outcome: null,
is_preliminary: true,
Expand All @@ -31,6 +32,7 @@ describe('Clicking triggers POST and side-effects', () => {
initialState: {
focusWell: {
well: {
id_product: 'ABCDEF',
run_name: 'TEST',
label: 'A1',
}
Expand All @@ -56,7 +58,7 @@ describe('Clicking triggers POST and side-effects', () => {
expect(wellStore.getQcValue).toEqual('Claimed');

let request = fetch.mock.calls[0];
expect(request[0]).toEqual('/api/pacbio/run/TEST/well/A1/qc_claim');
expect(request[0]).toEqual('/api/pacbio/products/ABCDEF/qc_claim');

let emits = widget.emitted();
expect(emits.wellClaimed).toBeTruthy();
Expand Down
14 changes: 6 additions & 8 deletions frontend/src/components/__tests__/QcWidget.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ describe('QC widgets render with no prior QC state, i.e. pending/ready', () => {
initialState: {
focusWell: {
well: {
run_info: {
pac_bio_run_name: 'TEST',
well_label: 'A1',
}
id_product: 'ABCDEF',
run_name: 'TEST',
label: 'A1',
}
},
},
Expand Down Expand Up @@ -107,10 +106,9 @@ describe('QC widget acquires state from prior outcome in run-table', () => {
initialState: {
focusWell: {
well: {
run_info: {
pac_bio_run_name: 'TEST',
well_label: 'A1',
},
id_product: 'ABCDEF',
run_name: 'TEST',
label: 'A1',
qc_state: {
qc_state: 'Claimed',
is_preliminary: true,
Expand Down
23 changes: 11 additions & 12 deletions frontend/src/components/__tests__/WellTable.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@ 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'},
{run_name: 'TEST1', label: 'B1', plate_number: null, instrument_name: '1234', instrument_type: 'Revio'},
{run_name: 'TEST2', label: 'A1', plate_number: 1, instrument_name: '1234', instrument_type: 'Revio'},
{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'},
]
}
})

test('There are three rows plus a header in the table', () => {
const rows = table.findAll('tr')
expect(rows.length).toEqual(4)

const columns = rows[1].findAll('td')
let columns = rows[1].findAll('td')
expect(columns[0].text()).toEqual('TEST1')
expect(columns[1].text()).toEqual('A1')
expect(columns[2].text()).toEqual('Revio 1234')
for (let col of columns.splice(3)) {
expect(col.text()).toEqual('')
}
columns = rows[3].findAll('td')
expect(columns[1].text()).toEqual('1-A1')
})

test('Non-null plate_number gives modified well labels', () => {
Expand All @@ -35,13 +36,11 @@ describe('Rows of data give rows in the table', () => {
test('Clicking triggers event emits containing required data', async () => {
const rows = table.findAll('tr')
await rows[1].find('button').trigger('click')
expect(table.emitted().wellSelected[0][0]).toHaveProperty('qcLabel')
expect(table.emitted().wellSelected[0][0]).toHaveProperty('qcRun')
expect(table.emitted().wellSelected[0][0].qcRun).toEqual('TEST1')
expect(table.emitted().wellSelected[0][0].qcLabel).toEqual('A1')

expect(table.emitted().wellSelected[0][0]).toHaveProperty('idProduct')
expect(table.emitted().wellSelected[0][0].idProduct).toEqual('ABCDEF')

await rows[2].find('button').trigger('click')
expect(table.emitted().wellSelected[1][0].qcRun).toEqual('TEST1')
expect(table.emitted().wellSelected[1][0].qcLabel).toEqual('B1')
expect(table.emitted().wellSelected[1][0].idProduct).toEqual('123456')
})
})
})
13 changes: 6 additions & 7 deletions frontend/src/stores/__tests__/focusWell.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ describe('Check the getters', () => {
wellStore = useWellStore();
})

test('Get run name name and well label', () => {
expect(wellStore.getRunAndLabel).toStrictEqual([null, null]);
test('Get id_product', () => {
expect(wellStore.getIdProduct).toBeNull()

wellStore.setFocusWell({
run_name: 'Whatever',
label: 'A1'
});
id_product: 'ABCDEF'
})

expect(wellStore.getRunAndLabel).toStrictEqual(['Whatever', 'A1']);
});
expect(wellStore.getIdProduct).toEqual('ABCDEF')
})

test('getQcState', () => {
expect(wellStore.getQcState).toBeNull();
Expand Down
15 changes: 6 additions & 9 deletions frontend/src/stores/focusWell.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,11 @@ export const useWellStore = defineStore('focusWell', {
*/
}),
getters: {
getRunAndLabel(state) {
if (!isNull(state.well)) {
return [
state.well.run_name,
state.well.label
];
getIdProduct(state) {
if (state.well && !isNull(state.well.id_product)) {
return state.well.id_product
} else {
return [null, null]
return null
}
},
getQcState(state) {
Expand Down Expand Up @@ -71,8 +68,8 @@ export const useWellStore = defineStore('focusWell', {
setFocusWell(well) {
this.well = well;
},
loadWellDetail(runName, label) {
apiClient.getWellPromise(runName, label).then(
loadWellDetail(id_product) {
apiClient.getWellPromise(id_product).then(
(well) => this.well = well
).catch(
(error) => {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/utils/__tests__/langqc.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ describe('Example fake remote api call', () => {
})
)

client.claimWell('TRACTION-RUN-299', 'B1');
client.claimWell('ABCDEF');

let request = fetch.mock.calls[3];
expect(
request[0]
).toEqual(
'/api/pacbio/run/TRACTION-RUN-299/well/B1/qc_claim'
'/api/pacbio/products/ABCDEF/qc_claim'
);

expect(
Expand All @@ -90,8 +90,8 @@ describe('Example fake remote api call', () => {
client.getClientConfig();
expect(fetch.mock.calls[4][0]).toEqual('/api/config');

client.getWellPromise('blah', 'A2');
expect(fetch.mock.calls[5][0]).toEqual('/api/pacbio/run/blah/well/A2');
client.getWellPromise('A12345');
expect(fetch.mock.calls[5][0]).toEqual('/api/pacbio/products/A12345/seq_level');

client.getWellsForRunPromise('blah')
expect(fetch.mock.calls[6][0]).toEqual('/api/pacbio/run/blah?page_size=100&page=1')
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/utils/__tests__/url.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ describe('Test query setting in URL', () => {
})

describe('Test QC param check', () => {
const testRun = {qcRun: 'test-run', qcLabel: 'A1'}
const alteredTestRun = {qcRun: 'test-run', qcLabel: 'B1'}
const testRun = {idProduct: 'AAAAAAAA'}
const alteredTestRun = {idProduct: 'BBBBBBBB'}

test('No before, some after', () => {
expect(qcQueryChanged(undefined, testRun)).toBe(true)
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/utils/langqc.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ export default class LangQc {
);
}

getWellPromise(name, well) {
return this.fetchWrapper(this.buildUrl(['run', name, 'well', well]));
getWellPromise(id_product) {
return this.fetchWrapper(this.buildUrl(['products', id_product, 'seq_level']));
}

getWellsForRunPromise(name) {
Expand All @@ -100,16 +100,16 @@ export default class LangQc {
return this.fetchWrapper('/api/config');
}

claimWell(name, well) {
claimWell(id_product) {
return this.fetchWrapper(
this.buildUrl(['run', name, 'well', well, 'qc_claim']),
this.buildUrl(['products', id_product, 'qc_claim']),
'POST'
)
}

setWellQcState(name, well, state, final = false) {
setWellQcState(id_product, state, final = false) {
return this.fetchWrapper(
this.buildUrl(['run', name, 'well', well, 'qc_assign']),
this.buildUrl(['products', id_product, 'qc_assign']),
'PUT',
{
qc_state: state,
Expand Down
16 changes: 10 additions & 6 deletions frontend/src/utils/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,22 +37,26 @@ export function generateUrl(existingSettings, newSettings, path) {
}

export function qcQueryChanged(before, after) {
// Compares ?qcLabel and ?qcRun in the URL to tell when new data will be needed
// Invoke while watching vue-router.route.query
// Compares ?idProduct in the URL to tell when new data will
// be needed. Invoke while watching vue-router.route.query
if (
(after.qcLabel || after.qcRun)
(after.idProduct)
&& (
// New page load
before === undefined
|| (
!before['qcLabel'] && !before['qcRun']
// Focus removed in previous nav, e.g. go home
!before['idProduct']
)
|| (
before.qcLabel && before.qcRun && (after.qcLabel != before.qcLabel || after.qcRun != before.qcRun)
// Change of focus within page
before.idProduct
&& (after.idProduct != before.idProduct)
)
)
) {
return true
} else if (before !== undefined && (before['qcLabel'] || before['qcRun'])) {
} else if (before !== undefined && (before['idProduct'])) {
// In case all arguments are removed
return true
}
Expand Down
Loading

0 comments on commit 804c5a2

Please sign in to comment.