Merge branch '215-flag-unflag-qc-results-as-accepted' into 'devel'

Resolve "Flag / unflag QC results as accepted"

Closes #215

See merge request wgp/dougal/software!28
This commit is contained in:
D. Berge
2022-05-04 16:32:48 +00:00
10 changed files with 399 additions and 94 deletions

View File

@@ -0,0 +1,135 @@
<template>
<v-hover v-slot:default="{hover}" v-if="!isEmpty(item)">
<span>
<v-btn v-if="!isAccepted(item)"
:class="{'text--disabled': !hover}"
icon
small
color="primary"
:title="isMultiple(item) ? 'Accept all' : 'Accept'"
@click.stop="accept(item)">
<v-icon small :color="isAccepted(item) ? 'green' : ''">
{{ isMultiple(item) ? 'mdi-check-all' : 'mdi-check' }}
</v-icon>
</v-btn>
<v-btn v-if="someAccepted(item)"
:class="{'text--disabled': !hover}"
icon
small
color="primary"
:title="isMultiple(item) ? 'Restore all' : 'Restore'"
@click.stop="unaccept(item)">
<v-icon small>
{{ isMultiple(item) ? 'mdi-restore' : 'mdi-restore' }}
</v-icon>
</v-btn>
</span>
</v-hover>
</template>
<script>
export default {
name: 'DougalQcAcceptance',
props: {
item: { type: Object }
},
methods: {
isAccepted (item) {
if (item._children) {
return item._children.every(child => this.isAccepted(child));
}
if (item.labels) {
return item.labels.includes("QCAccepted");
}
return false;
},
someAccepted (item) {
if (item._children) {
return item._children.some(child => this.someAccepted(child));
}
if (item.labels) {
return item.labels.includes("QCAccepted");
}
return false;
},
isEmpty (item) {
return item._children?.length === 0;
},
isMultiple (item) {
return item._children?.length;
},
action (action, item) {
const items = [];
const iterate = (item) => {
if (item._kind == "point") {
if (this.isAccepted(item)) {
if (action == "unaccept") {
items.push(item);
}
} else {
if (action == "accept") {
items.push(item);
}
}
} else if (item._kind == "sequence" || item._kind == "test") {
if (item._children) {
for (const child of item._children) {
iterate(child);
}
}
if (item._shots) {
for (const child of item._children) {
iterate(child);
}
}
}
}
iterate(item);
return items;
},
accept (item) {
const items = this.action('accept', item);
if (items.length) {
this.$emit('accept', items);
}
},
unaccept (item) {
const items = this.action('unaccept', item);
if (items.length) {
this.$emit('unaccept', items);
}
}
}
}
</script>

View File

@@ -50,16 +50,6 @@
<v-col col="12" sm="6">
<p>QC checks done on {{updatedOn}}.</p>
</v-col>
<v-col class="text-right">
<div v-if="isDirty">
<v-btn @click="saveLabels" small color="primary" class="mx-2">
Save <v-icon right>mdi-content-save</v-icon>
</v-btn>
<v-btn @click="getQCData" small color="warning" outlined class="mx-2">
Cancel <v-icon right>mdi-restore-alert</v-icon>
</v-btn>
</div>
</v-col>
</v-row>
<v-treeview
@@ -98,39 +88,11 @@
{{label}}
</v-chip>
<template v-if="!item.labels || !item.labels.includes('QCAccepted')">
<v-hover v-slot:default="{hover}" v-if="writeaccess">
<span v-if="item.children && item.children.length">
<v-btn
:class="{'text--disabled': !hover}"
icon
small
color="primary"
title="Accept all"
@click.stop="accept(item)">
<v-icon small :color="accepted(item) ? 'green' : ''">mdi-check-all</v-icon>
</v-btn>
<v-btn
:class="{'text--disabled': !hover}"
icon
small
color="primary"
title="Restore all"
@click.stop="unaccept(item)">
<v-icon small>mdi-restore</v-icon>
</v-btn>
</span>
<v-btn v-else
:class="{'text--disabled': !hover}"
icon
small
color="primary"
title="Accept this value"
@click="accept(item)">
<v-icon small :color="(item.children && item.children.length == 0)? 'green':''">mdi-check</v-icon>
</v-btn>
</v-hover>
</template>
<dougal-qc-acceptance v-if="writeaccess"
:item="item"
@accept="accept"
@unaccept="unaccept"
></dougal-qc-acceptance>
</div>
<div :title="item.remarks" @dblclick.stop.prevent="toggleChildren(item)" v-else-if="item._kind=='sequence'">
@@ -142,8 +104,21 @@
v-text="itemCount(item)"
>
</v-chip>
<dougal-qc-acceptance v-if="writeaccess"
:item="item"
@accept="accept"
@unaccept="unaccept"
></dougal-qc-acceptance>
</div>
<div class="text--secondary" v-else>
<dougal-qc-acceptance v-if="writeaccess"
:item="item"
@accept="accept"
@unaccept="unaccept"
></dougal-qc-acceptance>
{{item._text}}
</div>
</template>
@@ -164,10 +139,15 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { withParentProps } from '@/lib/utils';
import DougalQcAcceptance from '@/components/qc-acceptance';
export default {
name: "QC",
components: {
DougalQcAcceptance
},
data () {
return {
updatedOn: null,
@@ -179,8 +159,7 @@ export default {
selectedSequences: null,
multiple: false,
autoexpand: false,
itemIndex: 0,
isDirty: false
itemIndex: 0
}
},
@@ -283,44 +262,26 @@ export default {
return sum;
},
accepted (item) {
if (item._children) {
return item._children.every(child => this.accepted(child));
}
async accept (items) {
const url = `/project/${this.$route.params.project}/qc/results/accept`;
await this.api([url, {
method: "POST",
body: items.map(i => i.id)
}]);
if (item.labels) {
return item.labels.includes("QCAccepted");
}
return false;
// The open/closed state of the tree branches should stay the same, unless
// the tree structure itself has changed in the meanwhile.
await this.getQCData();
},
accept (item) {
if (item._children) {
for (const child of item._children) {
this.accept(child);
}
return;
}
async unaccept (items) {
const url = `/project/${this.$route.params.project}/qc/results/unaccept`;
await this.api([url, {
method: "POST",
body: items.map(i => i.id)
}]);
if (!item.labels) {
this.$set(item, "labels", []);
}
item.labels.includes("QCAccepted") || item.labels.push("QCAccepted");
this.isDirty = true;
},
unaccept (item) {
if (item._children) {
for (const child of item._children) {
this.unaccept(child);
}
return;
}
const i = item.labels.indexOf("QCAccepted");
if (i != -1) {
item.labels.splice(i, 1);
this.isDirty = true;
}
await this.getQCData();
},
async getQCLabels () {
@@ -375,16 +336,6 @@ export default {
await Promise.all(promises);
},
async saveLabels () {
const url = `/project/${this.$route.params.project}/meta`;
const res = await this.api([url, {
method: "PUT",
body: this.resultObjects.filter(r => typeof r.value !== "undefined")
}]);
this.isDirty = false;
},
filterByText(item, queryText) {
if (!queryText || !item) return item;
@@ -495,6 +446,7 @@ export default {
const res = await this.api([url]);
if (res) {
this.itemIndex = 0;
this.items = res.map(i => this.transform(i)) || [];
this.updatedOn = res.updatedOn;
await this.getQCLabels();
@@ -503,7 +455,6 @@ export default {
this.updatedOn = null;
}
this.isDirty = false;
},
...mapActions(["api"])

View File

@@ -8,7 +8,7 @@ const mw = require('./middleware');
const verbose = process.env.NODE_ENV != 'test';
const app = express();
app.locals.version = "0.3.0"; // API version
app.locals.version = "0.3.1"; // API version
app.map = function(a, route){
route = route || '';
@@ -187,6 +187,14 @@ app.map({
// Delete all QC results for :project
delete: [ mw.auth.access.write, mw.qc.results.delete ],
'/accept': {
post: [ mw.auth.access.write, mw.qc.results.accept ]
},
'/unaccept': {
post: [ mw.auth.access.write, mw.qc.results.unaccept ]
},
'/sequence/:sequence': {
// Get QC results for :project, :sequence
get: [ mw.qc.results.get ],

View File

@@ -0,0 +1,16 @@
const { qc } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
const payload = req.body;
await qc.results.accept(req.params.project, payload);
res.status(204).send();
next();
} catch (err) {
next(err);
}
};

View File

@@ -1,4 +1,6 @@
module.exports = {
get: require('./get'),
delete: require('./delete')
delete: require('./delete'),
accept: require('./accept'),
unaccept: require('./unaccept')
};

View File

@@ -0,0 +1,16 @@
const { qc } = require('../../../../lib/db');
module.exports = async function (req, res, next) {
try {
const payload = req.body;
await qc.results.unaccept(req.params.project, payload);
res.status(204).send();
next();
} catch (err) {
next(err);
}
};

View File

@@ -0,0 +1,23 @@
const { setSurvey } = require('../../connection');
async function accept (projectId, payload) {
const client = await setSurvey(projectId);
const text = `
UPDATE event_log_full
SET
labels = array_append(labels, 'QCAccepted')
WHERE
validity @> current_timestamp
AND id = ANY($1)
AND NOT ('QCAccepted' = ANY(labels));
`;
const values = [ payload ];
await client.query(text, values);
client.release();
return;
}
module.exports = accept;

View File

@@ -1,5 +1,7 @@
module.exports = {
get: require('./get'),
delete: require('./delete')
delete: require('./delete'),
accept: require('./accept'),
unaccept: require('./unaccept')
};

View File

@@ -0,0 +1,22 @@
const { setSurvey, transaction } = require('../../connection');
async function unaccept (projectId, payload) {
const client = await setSurvey(projectId);
const text = `
UPDATE event_log_full
SET
labels = array_remove(labels, 'QCAccepted')
WHERE
validity @> current_timestamp
AND id = ANY($1);
`;
const values = [ payload ];
await client.query(text, values);
client.release();
return;
}
module.exports = unaccept;

View File

@@ -1,6 +1,6 @@
openapi: 3.0.0
info:
version: 0.3.0
version: 0.3.1
title: Dougal API
description: >
Public API of the Dougal seismic production & data analysis tool.
@@ -55,6 +55,9 @@ tags:
-
name: log
description: Project events log
-
name: qc
description: Project quality control definition and results
-
name: metadata
description: Project items metadata
@@ -1401,6 +1404,133 @@ paths:
$ref: "#/components/responses/401"
/project/{project}/qc/results:
get:
summary: Get QC results.
tags: [ "qc" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
"200":
description: Project QC results.
content:
application/json:
schema:
type: object
description: |
The returned object is a tree structure of QC tests and their results.
"401":
$ref: "#/components/responses/401"
delete:
summary: Delete all QC results.
tags: [ "qc" ]
security:
- BearerAuthUser: []
- BearerAuthUser: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
"204":
description: |
All QC results for the project have been deleted.
Note that unless the project has been archived, the QCs will be regenerated in the next run of the deferred tasks process.
"401":
$ref: "#/components/responses/401"
/project/{project}/qc/results/accept:
post:
summary: Accept shotpoint QC results.
tags: [ "qc" ]
security:
- BearerAuthUser: []
- BearerAuthUser: []
parameters:
- $ref: "#/components/parameters/Project"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: integer
description: Event ID of the QC result to mark as accepted.
responses:
"204":
description: |
The QC events referenced in the request body have been marked as accepted. These events will not be exported to Seis+JSON files or derived human-readable reports.
"401":
$ref: "#/components/responses/401"
/project/{project}/qc/results/unaccept:
post:
summary: Unaccept shotpoint QC results.
tags: [ "qc" ]
security:
- BearerAuthUser: []
- BearerAuthUser: []
parameters:
- $ref: "#/components/parameters/Project"
responses:
"204":
description: |
The QC events referenced in the request body are no longer marked as accepted.
"401":
$ref: "#/components/responses/401"
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
type: integer
description: Event ID of the QC result to no longer mark accepted.
/project/{project}/qc/results/sequence/{sequence}:
get:
summary: Get sequence QC results.
tags: [ "qc" ]
security:
- BearerAuthGuest: []
- CookieAuthGuest: []
parameters:
- $ref: "#/components/parameters/Project"
- $ref: "#/components/parameters/SequenceNumber"
responses:
"200":
description: Sequence QC results.
content:
application/json:
schema:
type: object
description: |
The returned object is a tree structure of QC tests and their results for a specific sequence
"401":
$ref: "#/components/responses/401"
delete:
summary: Delete sequence QC results.
tags: [ "qc" ]
security:
- BearerAuthUser: []
- BearerAuthUser: []
parameters:
- $ref: "#/components/parameters/Project"
- $ref: "#/components/parameters/SequenceNumber"
responses:
"204":
description: |
All QC results for the sequence have been deleted.
Note that unless the project has been archived, the QCs will be regenerated in the next run of the deferred tasks process.
"401":
$ref: "#/components/responses/401"
/project/{project}/configuration/{path}:
get:
summary: Get project configuration data.