Skip to content
This repository has been archived by the owner on Apr 18, 2024. It is now read-only.

fix: LEAP-447: Fix Number tag validation #1649

Merged
merged 19 commits into from
Jan 30, 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
29 changes: 29 additions & 0 deletions src/mixins/PerItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,35 @@ const PerItemMixin = types
},
}))
.actions(self => ({
/**
* Validates all values related to the current classification per item (Multi Items Segmentation).
*
* - This method should not be overridden.
* - It is used only in validate method of the ClassificationBase mixin.
*
* @returns {boolean}
* @private
*/
_validatePerItem() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the idea behind underscore here? we use underscores rarely and for purely internal things, and those are called from other models using this mixin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to prevent using this method outside of the bundle PerItemMixin <-> ClassificationBase. I'm not sure how to prevent it in better way.

const objectTag = self.toNameTag;

return self.annotation.regions
.every((reg) => {
const result = reg.results.find(s => s.from_name === self);

if (!result?.hasValue) {
return true;
}
const value = result.mainValue;
const isValid = self.validateValue(value);

if (!isValid) {
objectTag.setCurrentItem(reg.item_index);
return false;
}
return true;
});
},
createPerItemResult() {
self.createPerObjectResult({
item_index: self.toNameTag.currentItemIndex,
Expand Down
24 changes: 24 additions & 0 deletions src/mixins/PerRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,30 @@ const PerRegionMixin = types
},
}))
.actions(self => ({
/**
* Validates all values related to the current classification per region.
*
* - This method should not be overridden.
* - It is used only in validate method of the ClassificationBase mixin.
*
* @returns {boolean}
* @private
*/
_validatePerRegion() {
const objectTag = self.toNameTag;

for (const reg of objectTag.allRegs) {
const value = reg.results.find(s => s.from_name === self)?.mainValue;
const isValid = self.validateValue(value);

if (!isValid) {
self.annotation.selectArea(reg);
return false;
}
}

return true;
},
createPerRegionResult() {
self.perRegionArea?.setValue(self);
},
Expand Down
100 changes: 53 additions & 47 deletions src/mixins/Required.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,71 +6,77 @@ const RequiredMixin = types
required: types.optional(types.boolean, false),
requiredmessage: types.maybeNull(types.string),
})
.actions(self => ({
validate() {
if (!self.required) return true;
.actions(self => {
const Super = {
validate: self.validate,
};

if (self.perregion) {
return {
validate() {
if (!Super.validate()) return false;
if (!self.required) return true;

if (self.perregion) {
// validating when choices labeling is done per region,
// for example choice may be required to be selected for
// every bbox
const objectTag = self.toNameTag;
const objectTag = self.toNameTag;

// if regions don't meet visibility conditions skip validation
for (const reg of objectTag.allRegs) {
const s = reg.results.find(s => s.from_name === self);
// if regions don't meet visibility conditions skip validation
for (const reg of objectTag.allRegs) {
const s = reg.results.find(s => s.from_name === self);

if (self.visiblewhen === 'region-selected') {
if (self.whentagname) {
const label = reg.labeling?.from_name?.name;
if (self.visiblewhen === 'region-selected') {
if (self.whentagname) {
const label = reg.labeling?.from_name?.name;

if (label && label !== self.whentagname) continue;
if (label && label !== self.whentagname) continue;
}
}
}

if (self.whenlabelvalue && !reg.hasLabel(self.whenlabelvalue)) {
continue;
}
if (self.whenlabelvalue && !reg.hasLabel(self.whenlabelvalue)) {
continue;
}

if (!s?.hasValue) {
self.annotation.selectArea(reg);
self.requiredModal();
if (!s?.hasValue) {
self.annotation.selectArea(reg);
self.requiredModal();

return false;
return false;
}
}
}
} else if (isFF(FF_LSDV_4583) && self.peritem) {
} else if (isFF(FF_LSDV_4583) && self.peritem) {
// validating when choices labeling is done per item,
const objectTag = self.toNameTag;
const maxItemIndex = objectTag.maxItemIndex;
const existingResultsIndexes = self.annotation.regions
.reduce((existingResultsIndexes, reg) => {
const result = reg.results.find(s => s.from_name === self);
const objectTag = self.toNameTag;
const maxItemIndex = objectTag.maxItemIndex;
const existingResultsIndexes = self.annotation.regions
.reduce((existingResultsIndexes, reg) => {
const result = reg.results.find(s => s.from_name === self);

if (result?.hasValue) {
existingResultsIndexes.add(reg.item_index);
}
return existingResultsIndexes;
}, new Set());
if (result?.hasValue) {
existingResultsIndexes.add(reg.item_index);
}
return existingResultsIndexes;
}, new Set());

for (let idx = 0; idx <= maxItemIndex; idx++) {
if (!existingResultsIndexes.has(idx)) {
objectTag.setCurrentItem(idx);
self.requiredModal();
return false;
for (let idx = 0; idx <= maxItemIndex; idx++) {
if (!existingResultsIndexes.has(idx)) {
objectTag.setCurrentItem(idx);
self.requiredModal();
return false;
}
}
}
} else {
} else {
// validation when its classifying the whole object
// isVisible can be undefined (so comparison is true) or boolean (so check for visibility)
if (!self.holdsState && self.isVisible !== false && getParent(self, 2)?.isVisible !== false) {
self.requiredModal();
return false;
if (!self.holdsState && self.isVisible !== false && getParent(self, 2)?.isVisible !== false) {
self.requiredModal();
return false;
}
}
}

return true;
},
}));
return true;
},
};
});

export default RequiredMixin;
60 changes: 60 additions & 0 deletions src/tags/control/ClassificationBase.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { types } from 'mobx-state-tree';
import { FF_LSDV_4583, isFF } from '../../utils/feature-flags';

/**
* This is a mixin for a control-tag that is a base of creating classification-like tags.
Expand Down Expand Up @@ -40,6 +41,65 @@ const ClassificationBase = types.model('ClassificationBase', {
};
}).actions(self => {
return {
/**
* Validates the input based on certain conditions.
*
* Generally, this method does not need to be overridden. And you need to override the validateValue method instead.
* However, there are exceptions. For example, RequiredMixin, Choices, and
* Taxonomy have their own additional logic, for which a broader context is needed.
* In this case, the parent method call is added at the beginning or end
* of the method to maintain all functionality in a predictable manner.
*
* @returns {boolean}
*/
validate() {
if (self.perregion) {
return self._validatePerRegion();
} else if (self.peritem && isFF(FF_LSDV_4583)) {
return self._validatePerItem();
} else {
return self._validatePerObject();
}
},
/**
* Validates the value.
*
* Override to add your custom validation logic specific for the tag.
* Per-item, per-region and per-object validation will be applied automatically.
*
* @example
* SomeModel.actions(self => {
* const Super = { validateValue: self.validateValue };
*
* return {
* validateValue(value) {
* if (!Super.validateValue(value)) return false;
* // your validation logic
* }
* // other actions
* }
* });
*
* @param {*} value - The value to be validated.
* @returns {boolean}
*
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validateValue(value) {
return true;
},
/**
* Validates all values related to the current classification per object.
*
* - This method should not be overridden.
* - It is used only in validate method of the ClassificationBase mixin.
*
* @returns {boolean}
* @private
*/
_validatePerObject() {
return self.validateValue(self.selectedValues());
},
createPerObjectResult(areaValues = {}) {
self.annotation.createResult(
areaValues,
Expand Down
51 changes: 14 additions & 37 deletions src/tags/control/DateTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,53 +293,30 @@ const Model = types
},
}))
.actions(self => {
const Super = { validate: self.validate };
const Super = { validateValue: self.validateValue };

return {
validate() {
if (!Super.validate()) return false;
validateValue(value) {
if (!Super.validateValue(value)) return false;

const { min, max } = self;

if (!min && !max) return true;

function validateValue(value) {
const errors = [];

if (!value) return true;
const errors = [];

let date = self.getISODate(value);
if (!value) return true;

if (self.only?.includes('year')) date = date.slice(0, 4);
let date = self.getISODate(value);

if (min && date < min) errors.push(`min date is ${min}`);
if (max && date > max) errors.push(`max date is ${max}`);
if (self.only?.includes('year')) date = date.slice(0, 4);

if (errors.length) {
InfoModal.warning(`Date "${date}" is not valid: ${errors.join(', ')}.`);
return false;
}
return true;
}

// per-region results are not visible, so we have to check their values
if (self.perregion) {
const objectTag = self.toNameTag;

for (const reg of objectTag.allRegs) {
const date = reg.results.find(s => s.from_name === self)?.mainValue;
const isValid = validateValue(date);
const { min, max } = self;

if (!isValid) {
self.annotation.selectArea(reg);
return false;
}
}
if (min && date < min) errors.push(`min date is ${min}`);
if (max && date > max) errors.push(`max date is ${max}`);

return true;
} else {
return validateValue(self.datetime);
if (errors.length) {
InfoModal.warning(`Date "${date}" is not valid: ${errors.join(', ')}.`);
return false;
}
return true;
},
};
});
Expand Down
Loading
Loading