Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate C Elegans Sex Correctly #705

Merged
merged 4 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions schemas/base-metadata.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,12 @@ export const preprocessMetadataSchema = (schema: any = baseMetadataSchema, globa
M: 'Male',
F: 'Female',
U: 'Unknown',
O: 'Other'
O: 'Other',
XX: 'Hermaphrodite — C. elegans',
XO: 'Male — C. elegans'
}


subjectProps.species = {
subjectProps.species = {
type: 'string',
strict: false,
description: 'The species of your subject.'
Expand Down
4 changes: 3 additions & 1 deletion schemas/json/base_metadata_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,9 @@
"M",
"F",
"U",
"O"
"O",
"XX",
"XO"
]
},
"species": {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/stories/SimpleTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ export class SimpleTable extends LitElement {
Object.keys(cols).map((k) => (cols[k] = ""));
if (this.validateOnChange)
Object.keys(cols).map((k) => {
const res = this.validateOnChange([k], { ...cols }, cols[k]);
const res = this.validateOnChange([k], { ...cols }, cols[k]); // NOTE: This is likely incorrect
if (typeof res === "function") res();
});

Expand Down
164 changes: 94 additions & 70 deletions src/renderer/src/stories/Table.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class Table extends LitElement {
onThrow,
contextMenu,
ignore,
groups, // NOTE: All groups must be non-overlapping
} = {}) {
super();
this.schema = schema ?? {};
Expand All @@ -128,6 +129,7 @@ export class Table extends LitElement {
this.validateEmptyCells = validateEmptyCells ?? true;
this.contextMenu = contextMenu ?? {};
this.ignore = ignore ?? {};
this.groups = groups ?? [];

if (onThrow) this.onThrow = onThrow;
if (onUpdate) this.onUpdate = onUpdate;
Expand Down Expand Up @@ -229,6 +231,8 @@ export class Table extends LitElement {
});
};

#info = {};

updated() {
const div = (this.shadowRoot ?? this).querySelector("div");

Expand Down Expand Up @@ -277,7 +281,7 @@ export class Table extends LitElement {
const displayHeaders = [...colHeaders].map(header);

const columns = colHeaders.map((k, i) => {
const info = { type: "text" };
const info = (this.#info[k] = { type: "text", stopGroupUpdates: {} });

const colInfo = entries[k];
if (colInfo.unit) displayHeaders[i] = `${displayHeaders[i]} (${colInfo.unit})`;
Expand Down Expand Up @@ -309,9 +313,10 @@ export class Table extends LitElement {

const runThisValidator = async (value, row, prop) => {
try {
const path = [row, k];
const valid = this.validateOnChange
? await this.validateOnChange(
[k],
path,
{ ...this.data[this.getRowName(row)] }, // Validate on a copy of the parent
value,
info
Expand All @@ -328,6 +333,8 @@ export class Table extends LitElement {
const required = isRequired(k, this.#itemSchema);

const validator = async function (value, callback) {
const row = this.row;

const validateEmptyCells = instanceThis.validateEmptyCells;
const willValidate =
validateEmptyCells === true ||
Expand All @@ -339,55 +346,42 @@ export class Table extends LitElement {
if (!value && !willValidate) {
instanceThis.#handleValidationResult(
[], // Clear errors
this.row,
row,
this.col
);
callback(true); // Allow empty value
return;
}

if (value && k === instanceThis.keyColumn) {
if (value in instanceThis.data && instanceThis.data[value]?.[rowSymbol] !== this.row) {
if (value in instanceThis.data && instanceThis.data[value]?.[rowSymbol] !== row) {
// Convert previously valid value to unresolved
const previousKey = instanceThis.getRowName(this.row);
const previousKey = instanceThis.getRowName(rrow);
if (previousKey) {
unresolved[this.row] = instanceThis.data[previousKey];
unresolved[row] = instanceThis.data[previousKey];
delete instanceThis.data[previousKey];
}

// Throw error
instanceThis.#handleValidationResult(
[{ message: `${header(k)} already exists`, type: "error" }],
this.row,
row,
this.col
);
callback(false);
return;
}
}

if (name === "subject_id") {
if (v) {
if (Object.values(this.data).filter((s) => s.subject_id === v).length > 1) {
return [
{
message: "Subject ID must be unique",
type: "error",
},
];
}
}
}

if (!(await runThisValidator(value, this.row, this.col))) {
if (!(await runThisValidator(value, row, this.col))) {
callback(false);
return;
}

if (!value && required) {
instanceThis.#handleValidationResult(
[{ message: `${header(k)} is a required property.`, type: "error" }],
this.row,
row,
this.col
);
callback(false);
Expand Down Expand Up @@ -508,70 +502,100 @@ export class Table extends LitElement {
if (menu) this.#root.appendChild(menu); // Move to style root

let validated = 0;
const initialCellsToUpdate = data.reduce((acc, v) => acc + v.length, 0);

const initialCellsToUpdate = data.reduce((acc, arr) => acc + arr.length, 0);

table.addHook("afterValidate", (isValid, value, row, prop) => {
const isUserUpdate = initialCellsToUpdate <= validated;
const header = typeof prop === "number" ? colHeaders[prop] : prop;
const info = this.#info[header];

let rowName = this.getRowName(row);
// Update other columns in the group

if (isUserUpdate) {
const header = typeof prop === "number" ? colHeaders[prop] : prop;
const skipUpdate = info.stopGroupUpdates[row];

// NOTE: We would like to allow invalid values to mutate the results
// if (isValid) {
const isResolved = rowName in this.data;
let target = this.data;
// Decrement counters
if (skipUpdate) info.stopGroupUpdates[row]--;

if (!isResolved) {
if (!this.keyColumn)
this.data[rowName] = {}; // Add new row to array
else {
rowName = row;
if (!unresolved[rowName]) unresolved[rowName] = {}; // Ensure row exists
target = unresolved;
}
}
if (!skipUpdate) {
const isUserUpdate = initialCellsToUpdate <= validated;

value = this.#getValue(value, entries[header]);

// Transfer data to object (if valid)
if (header === this.keyColumn) {
if (isValid && value && value !== rowName) {
const old = target[rowName] ?? {};
this.data[value] = old;
delete target[rowName];
delete unresolved[row];
Object.defineProperty(this.data[value], rowSymbol, { value: row, configurable: true }); // Setting row tracker
this.revalidate([{ row, prop }]);
}
}
let rowName = this.getRowName(row);

// Update data on passed object
else {
const globalValue = this.globals[header];
if (isUserUpdate) {
// NOTE: We would like to allow invalid values to mutate the results
// if (isValid) {
const isResolved = rowName in this.data;
let target = this.data;

if (value == undefined || value === "") {
if (globalValue) {
value = target[rowName][header] = globalValue;
table.setDataAtCell(row, prop, value);
this.onOverride(header, value, rowName);
if (!isResolved) {
if (!this.keyColumn)
this.data[rowName] = {}; // Add new row to array
else {
rowName = row;
if (!unresolved[rowName]) unresolved[rowName] = {}; // Ensure row exists
target = unresolved;
}
target[rowName][header] = undefined;
} else {
// Correct for expected arrays (copy-paste issue)
if (entries[header]?.type === "array") {
if (value && !Array.isArray(value)) value = value.split(",").map((v) => v.trim());
}

value = this.#getValue(value, entries[header]);

// Transfer data to object (if valid)
if (header === this.keyColumn) {
if (isValid && value && value !== rowName) {
const old = target[rowName] ?? {};
this.data[value] = old;
delete target[rowName];
delete unresolved[row];
Object.defineProperty(this.data[value], rowSymbol, { value: row, configurable: true }); // Setting row tracker
this.revalidate([{ row, prop }]);
}
}

target[rowName][header] = value === globalValue ? undefined : value;
// Update data on passed object
else {
const globalValue = this.globals[header];

if (value == undefined || value === "") {
if (globalValue) {
value = target[rowName][header] = globalValue;
table.setDataAtCell(row, prop, value);
this.onOverride(header, value, rowName);
}
target[rowName][header] = undefined;
} else {
// Correct for expected arrays (copy-paste issue)
if (entries[header]?.type === "array") {
if (value && !Array.isArray(value)) value = value.split(",").map((v) => v.trim());
}

target[rowName][header] = value === globalValue ? undefined : value;
}
}

this.onUpdate(rowName, header, value);
}

this.onUpdate(rowName, header, value);
}
validated++;

validated++;
// Check associated groups for validity
for (let group of this.groups) {
const table = this.table;
if (group.includes(header)) {
const otherGroup = group.filter((col) => col !== header);

otherGroup.forEach((col) => {
const j = colHeaders.indexOf(col);
const value = table.getDataAtCell(row, j);
const depInfo = this.#info[col];
const otherGroups = group.filter((c) => c !== col);
depInfo.stopGroupUpdates[row] = otherGroups.length; // Expecting this many updates from other members of the group
table.setDataAtCell(row, j, value);
});

return;
}
}
}

if (typeof isValid === "function") isValid();
});
Expand Down
12 changes: 10 additions & 2 deletions src/renderer/src/stories/pages/guided-mode/setup/GuidedSubjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,23 @@ export class GuidedSubjectsPage extends Page {
keyColumn: "subject_id",
validateEmptyCells: ["subject_id", "sessions"],
contextMenu: contextMenuConfig,
groups: [
[
"sex",
"species",
// 'age'
], // Validate both when one is changed
],
onThrow: (message, type) => this.notify(message, type),
onOverride: (name) => {
this.notify(`<b>${header(name)}</b> has been overridden with a global value.`, "warning", 3000);
},
onUpdate: () => {
this.unsavedUpdates = "conversions";
},
validateOnChange: (localPath, parent, v) => {
validateOnChange: function (localPath, parent, v) {
const name = localPath[localPath.length - 1];

if (name === "sessions") {
if (v?.length) return true;
else {
Expand All @@ -173,7 +181,7 @@ export class GuidedSubjectsPage extends Page {
}
} else {
delete parent.sessions; // Delete sessions from parent copy
return validateOnChange(localPath, parent, ["Subject"], v);
return validateOnChange.call(this, name, parent, ["Subject", ...localPath.slice(0, -1)], v);
}
},
});
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/src/validation/validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"conversion_output_folder": false,

"NWBFile": {
"*": "check_{*}",
"description": "check_description",
"experiment_description": "check_experiment_description",
"identifier": false,
"session_description": false,
"lab": false,
Expand Down Expand Up @@ -42,12 +43,12 @@
"Behavior": false,

"Subject": {
"*": "check_subject_{*}",
"sessions": false,
"description": false,
"genotype": false,
"strain": false,
"weight": false,
"sex": ["check_subject_sex"],
"age__reference": false,
"subject_id": "check_subject_id_exists",
"species": ["check_subject_species_form", "check_subject_species_exists"],
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/validation/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ const getTablePathInfo = (path: string[]) => {
return { modality, table, row }
}

// ----------------- Subject Validation ----------------- //

// Validate the same in rows and tables
schema.Subject['*'] = { ...schema.Subject }


// ----------------- Joint Ophys and Ecephys Validation ----------------- //

Expand Down
Loading