Skip to content

Commit

Permalink
User extra properties (#3589)
Browse files Browse the repository at this point in the history
* layout adjustments (user edit form)

* user extra props editor

* editing user extra props

* code cleanup

* extra props save

* layout adjutments

* pass user to expression evaluator (WIP)

* pass user to expression evaluation functions

* make extra properties experimental

* set user functions as experimental

* added functions descriptions

* use latest arena-core version

* fixing tests

* fixing tests

* added userProp function editor

* fixing tests

---------

Co-authored-by: Stefano Ricci <[email protected]>
  • Loading branch information
SteRiccio and SteRiccio authored Oct 7, 2024
1 parent 6f57569 commit e5833a0
Show file tree
Hide file tree
Showing 35 changed files with 549 additions and 169 deletions.
3 changes: 3 additions & 0 deletions core/expressionParser/helpers/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ export const functionNames = {
pow: 'pow',
sum: 'sum',
taxonProp: 'taxonProp',
userEmail: 'userEmail',
userName: 'userName',
userProp: 'userProp',
uuid: 'uuid',
}
13 changes: 13 additions & 0 deletions core/i18n/resources/en/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,9 @@ $t(common.appNameFull)
pow: 'Returns the value of a base raised to a power',
rowIndex: 'Returns the current table row (or form) index',
taxonProp: 'Returns the value of the specified $t(extraProp.label) of a taxon having the specified code',
userEmail: 'Returns the email of the logged in user',
userName: 'Returns the name of the logged in user',
userProp: 'Returns the value of the specified $t(extraProp.label) of the logged in user',
uuid: 'Generates a UUID (universally unique identifier) that can be used as identifier (e.g. as a key attribute of an enity)',
// SQL functions
avg: 'Returns the average value of a numeric variable',
Expand Down Expand Up @@ -1566,6 +1569,7 @@ Levels will be renamed into level_1, level_2... level_N and an extra 'area' prop
extraProp: {
label: 'Extra property',
label_plural: 'Extra properties',
addExtraProp: 'Add extra property',
dataTypes: {
geometryPoint: 'Geometry Point',
number: 'Number',
Expand All @@ -1584,6 +1588,8 @@ Levels will be renamed into level_1, level_2... level_N and an extra 'area' prop
dataTypeChanged: 'Data type changed from {{dataTypeOld}} to {{dataTypeNew}}',
},
},
name: 'Property {{position}} name',
value: 'Value',
},

// ===== All validation errors
Expand Down Expand Up @@ -1671,6 +1677,13 @@ Levels will be renamed into level_1, level_2... level_N and an extra 'area' prop
unableToFindNodeSibling: 'unable to find sibling node: {{name}}',
},

extraPropEdit: {
nameInvalid: 'Invalid name',
nameRequired: 'Name required',
dataTypeRequired: 'Data type required',
valueRequired: 'Value required',
},

nodeDefEdit: {
analysisParentEntityRequired: 'Entity is required',
applyIfDuplicate: '"$t(nodeDefEdit.expressionsProp.applyIf)" condition is duplicate',
Expand Down
5 changes: 5 additions & 0 deletions core/objectUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const keys = {
dateCreated: 'dateCreated',
dateModified: 'dateModified',
draft: 'draft',
extra: 'extra',
id: 'id',
index: 'index',
name: 'name',
Expand All @@ -25,6 +26,7 @@ export const keysProps = {
descriptions: 'descriptions',
labels: 'labels',
cycles: 'cycles',
extra: 'extra',
}

// ====== READ
Expand All @@ -45,6 +47,9 @@ export const getLabel = (lang, defaultTo = null) => R.pipe(getLabels, R.propOr(d
export const getDescriptions = getProp(keysProps.descriptions, {})
export const getDescription = (lang, defaultTo = null) => R.pipe(getDescriptions, R.propOr(defaultTo, lang))

export const getExtra = getProp(keysProps.extra, {})
export const getExtraProp = (extraPropKey) => R.pipe(getExtra, R.propOr(null, extraPropKey))

export const getDate = (prop) => R.pipe(R.propOr(null, prop), R.unless(R.isNil, DateUtils.parseISO))
export const getDateCreated = getDate(keys.dateCreated)
export const getDateModified = getDate(keys.dateModified)
Expand Down
26 changes: 19 additions & 7 deletions core/record/_record/recordNodesUpdater.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ const _addOrUpdateAttribute =
}

const _addEntityAndKeyValues =
({ survey, entityDef, parentNode, keyValuesByDefUuid, sideEffect = false }) =>
({ user, survey, entityDef, parentNode, keyValuesByDefUuid, sideEffect = false }) =>
(record) => {
const updateResult = new RecordUpdateResult({ record })
const updateResultDescendants = CoreRecordNodesUpdater.createNodeAndDescendants({
user,
survey,
record,
parentNode,
Expand Down Expand Up @@ -107,7 +108,7 @@ const _addEntityAndKeyValues =
}

const _getOrCreateEntityByKeys =
({ survey, entityDefUuid, valuesByDefUuid, insertMissingNodes, sideEffect = false }) =>
({ user, survey, entityDefUuid, valuesByDefUuid, insertMissingNodes, sideEffect = false }) =>
(record) => {
if (NodeDef.getUuid(Survey.getNodeDefRoot(survey)) === entityDefUuid) {
return { entity: RecordReader.getRootNode(record), updateResult: null }
Expand Down Expand Up @@ -158,6 +159,7 @@ const _getOrCreateEntityByKeys =
})
}
const { entity: entityInserted, updateResult } = _addEntityAndKeyValues({
user,
survey,
entityDef,
parentNode: entityParent,
Expand All @@ -169,11 +171,12 @@ const _getOrCreateEntityByKeys =
}

const getOrCreateEntityByKeys =
({ survey, entityDefUuid, valuesByDefUuid, timezoneOffset, insertMissingNodes = false, sideEffect = false }) =>
({ user, survey, entityDefUuid, valuesByDefUuid, timezoneOffset, insertMissingNodes = false, sideEffect = false }) =>
async (record) => {
const updateResult = new RecordUpdateResult({ record })

const { entity, updateResult: updateResultEntity } = _getOrCreateEntityByKeys({
user,
survey,
entityDefUuid,
valuesByDefUuid,
Expand All @@ -185,6 +188,7 @@ const getOrCreateEntityByKeys =
updateResult.merge(updateResultEntity)

const dependentsUpdateResult = await afterNodesUpdate({
user,
survey,
record: updateResultEntity.record,
nodes: updateResultEntity.nodes,
Expand All @@ -197,7 +201,7 @@ const getOrCreateEntityByKeys =
}

const updateAttributesInEntityWithValues =
({ survey, entity, valuesByDefUuid, timezoneOffset, sideEffect = false }) =>
({ user, survey, entity, valuesByDefUuid, timezoneOffset, sideEffect = false }) =>
async (record) => {
const updateResult = new RecordUpdateResult({ record })

Expand All @@ -207,6 +211,7 @@ const updateAttributesInEntityWithValues =
updateResult.merge(nodeUpdateResult)

const dependentsUpdateResult = await afterNodesUpdate({
user,
survey,
record: nodeUpdateResult.record,
nodes: nodeUpdateResult.nodes,
Expand Down Expand Up @@ -258,12 +263,13 @@ const updateAttributesInEntityWithValues =
}

const updateAttributesWithValues =
({ survey, entityDefUuid, valuesByDefUuid, timezoneOffset, insertMissingNodes = false, sideEffect = false }) =>
({ user, survey, entityDefUuid, valuesByDefUuid, timezoneOffset, insertMissingNodes = false, sideEffect = false }) =>
async (record) => {
const updateResult = new RecordUpdateResult({ record })

// 1. get or create context entity
const { entity, updateResult: updateResultEntity } = await getOrCreateEntityByKeys({
user,
survey,
entityDefUuid,
valuesByDefUuid,
Expand All @@ -289,7 +295,7 @@ const updateAttributesWithValues =
}

const deleteNodesInEntityByNodeDefUuid =
({ survey, entity, nodeDefUuids, sideEffect = false }) =>
({ user, survey, entity, nodeDefUuids, sideEffect = false }) =>
async (record) => {
const updateResult = new RecordUpdateResult({ record })

Expand All @@ -299,7 +305,13 @@ const deleteNodesInEntityByNodeDefUuid =
nodeUuidsToDelete.push(...children.map(Node.getUuid))
})

const nodesDeleteUpdateResult = await deleteNodes({ survey, record, nodeUuids: nodeUuidsToDelete, sideEffect })
const nodesDeleteUpdateResult = await deleteNodes({
user,
survey,
record,
nodeUuids: nodeUuidsToDelete,
sideEffect,
})
return updateResult.merge(nodesDeleteUpdateResult)
}

Expand Down
3 changes: 2 additions & 1 deletion core/record/_record/recordNodesUpdaterCommon.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import * as Validation from '@core/validation/validation'

const { updateNodesDependents } = CoreRecordNodesUpdater

export const afterNodesUpdate = async ({ survey, record, nodes, timezoneOffset, sideEffect = false }) => {
export const afterNodesUpdate = async ({ user, survey, record, nodes, timezoneOffset, sideEffect = false }) => {
// output
const updateResult = new RecordUpdateResult({ record, nodes })

// 1. update dependent nodes
const updateResultDependents = updateNodesDependents({
user,
survey,
record,
nodes,
Expand Down
6 changes: 4 additions & 2 deletions core/record/_record/recordsCombiner.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ const _replaceUpdatedNodesInEntities = ({
}

export const replaceUpdatedNodes =
({ survey, recordSource, timezoneOffset, sideEffect = false }) =>
({ user, survey, recordSource, timezoneOffset, sideEffect = false }) =>
async (recordTarget) => {
const rootSource = RecordReader.getRootNode(recordSource)
const rootTarget = RecordReader.getRootNode(recordTarget)
Expand All @@ -156,6 +156,7 @@ export const replaceUpdatedNodes =
sideEffect,
})
return afterNodesUpdate({
user,
survey,
record: updateResult.record,
nodes: updateResult.nodes,
Expand Down Expand Up @@ -362,7 +363,7 @@ const _mergeRecordsNodes = ({
}

export const mergeRecords =
({ survey, recordSource, timezoneOffset, sideEffect = false }) =>
({ user, survey, recordSource, timezoneOffset, sideEffect = false }) =>
async (recordTarget) => {
const { cycle } = recordTarget
const rootSource = RecordReader.getRootNode(recordSource)
Expand Down Expand Up @@ -390,6 +391,7 @@ export const mergeRecords =
})
}
return afterNodesUpdate({
user,
survey,
record: updateResult.record,
nodes: updateResult.nodes,
Expand Down
6 changes: 2 additions & 4 deletions core/survey/categoryItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const newItem = (levelUuid, parentItemUuid = null, props = {}) => ({
export const {
getDescription,
getDescriptions,
getExtra,
getExtraProp,
getLabels,
getProps,
getPropsDraft,
Expand All @@ -63,10 +65,6 @@ export const getLabel =
export const getLabelWithCode = (language) => (item) => CategoryItems.getLabelWithCode(item, language)
export const getIndex = ObjectUtils.getProp(keysProps.index)

// ====== READ - Extra Props
export const getExtra = ObjectUtils.getProp(keysProps.extra)
export const getExtraProp = (prop) => R.pipe(getExtra, R.prop(prop))

// ====== UPDATE
export const assocProp = ({ key, value }) => ObjectUtils.setProp(key, value)
// ====== UPDATE - Extra Props
Expand Down
13 changes: 3 additions & 10 deletions core/survey/taxon.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const propKeys = {
family: 'family',
genus: 'genus',
scientificName: 'scientificName',
extra: 'extra',
index: 'index',
extra: ObjectUtils.keysProps.extra,
}

export const unlistedCode = 'UNL'
Expand All @@ -41,7 +41,8 @@ export const newTaxon = ({ taxonomyUuid, code, family, genus, scientificName, ve
})

// ====== READ
export const { getUuid, getProps, getPropsDraft, getPropsAndPropsDraft, setProp } = ObjectUtils
export const { getExtra, getExtraProp, getProps, getPropsDraft, getPropsAndPropsDraft, getUuid, isEqual, setProp } =
ObjectUtils
export const getTaxonomyUuid = R.prop(keys.taxonomyUuid)
export const getCode = ObjectUtils.getProp(propKeys.code, '')
export const getFamily = ObjectUtils.getProp(propKeys.family, '')
Expand All @@ -56,18 +57,10 @@ export const getVernacularLanguage = R.propOr('', keys.vernacularLanguage)
export const getVernacularNameUuid = R.prop(keys.vernacularNameUuid)
export const getVernacularName = R.propOr('', keys.vernacularName)

export const getExtra = ObjectUtils.getProp(propKeys.extra, {})
export const getExtraProp = (extraPropKey) => (taxon) => {
const extra = getExtra(taxon)
return R.propOr(null, extraPropKey)(extra)
}

export const isUnlistedTaxon = R.pipe(getCode, R.equals(unlistedCode))
export const isUnknownTaxon = R.pipe(getCode, R.equals(unknownCode))
export const isUnkOrUnlTaxon = (taxon) => isUnlistedTaxon(taxon) || isUnknownTaxon(taxon)

export const { isEqual } = ObjectUtils

// ==== UPDATE
export const assocVernacularNames = (lang, vernacularNames) =>
R.assocPath([keys.vernacularNames, lang], vernacularNames)
Expand Down
3 changes: 3 additions & 0 deletions core/user/_user/userProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const keysProps = {
mapApiKeyByProvider: 'mapApiKeyByProvider',
// restricted props (editable only by system admins)
maxSurveys: 'maxSurveys',
// custom extra properties
extra: ObjectUtils.keysProps.extra,
}

const privateProps = [keysProps.mapApiKeyByProvider]
Expand Down Expand Up @@ -55,6 +57,7 @@ export const assocMapApiKey =
return assocProp(keysProps.mapApiKeyByProvider)(mapApiKeyByProviderUpdated)(user)
}
export const assocMaxSurveys = assocProp(keysProps.maxSurveys)
export const assocExtra = assocProp(keysProps.extra)

const dissocListOfProps = (propsArray) => (user) =>
propsArray.reduce((acc, propKey) => R.dissocPath([userKeys.props, propKey])(acc), user)
Expand Down
7 changes: 3 additions & 4 deletions core/user/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,21 @@ import * as UserProps from './_user/userProps'
import { userStatus } from './_user/userStatus'

export { keys } from './_user/userKeys'
export { keysProps } from './_user/userProps'
export { userStatus } from './_user/userStatus'

export const nameMaxLength = 128

export const { keysPrefs, keysSurveyPrefs } = UserPrefs
export const { keysProps } = UserProps

// ====== READ
export const { isEqual } = ObjectUtils
export const { getUuid } = ObjectUtils
export const { getAuthGroups, getExtra, getExtraProp, getUuid, isEqual } = ObjectUtils
export const getName = R.propOr('', keys.name)
export const getEmail = R.prop(keys.email)
export const getInvitedBy = R.prop(keys.invitedBy)
export const getInvitedDate = R.prop(keys.invitedDate)
export const getPassword = R.prop(keys.password)
export const getLang = R.propOr('en', keys.lang)
export const { getAuthGroups } = ObjectUtils
export const getPrefs = R.propOr({}, keys.prefs)
export const getProps = R.propOr({}, keys.props)
export const getProfilePicture = R.prop(keys.profilePicture)
Expand Down Expand Up @@ -111,6 +109,7 @@ export const {
assocTitle,
assocMapApiKey,
assocMaxSurveys,
assocExtra,
titleKeys,
titleKeysArray,
newProps,
Expand Down
4 changes: 3 additions & 1 deletion core/validation/_validator/validatorErrorKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ export const ValidatorErrorKeys = {
},

extraPropEdit: {
nameInvalid: 'validationErrors.extraPropEdit.nameInvalid',
dataTypeRequired: 'validationErrors.extraPropEdit.dataTypeRequired',
nameInvalid: 'validationErrors.extraPropEdit.nameInvalid',
nameRequired: 'validationErrors.extraPropEdit.nameRequired',
valueRequired: 'validationErrors.extraPropEdit.valueRequired',
},
nodeDefEdit: {
analysisParentEntityRequired: 'validationErrors.nodeDefEdit.analysisParentEntityRequired',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
"@mui/x-data-grid": "^7.14.0",
"@mui/x-date-pickers": "^7.14.0",
"@mui/x-tree-view": "^7.7.1",
"@openforis/arena-core": "^0.0.206",
"@openforis/arena-core": "^0.0.207",
"@openforis/arena-server": "^0.1.35",
"@reduxjs/toolkit": "^2.2.5",
"@sendgrid/mail": "^8.1.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export default class CsvDataImportJob extends DataImportBaseJob {

async onRowItem({ valuesByDefUuid, errors }) {
const { context, tx } = this
const { survey, nodeDefUuid, includeFiles, insertMissingNodes } = context
const { survey, nodeDefUuid, includeFiles, insertMissingNodes, user } = context

if (this.isCanceled()) {
return
Expand Down Expand Up @@ -186,6 +186,7 @@ export default class CsvDataImportJob extends DataImportBaseJob {
const updateResult = new RecordUpdateResult({ record: this.currentRecord })

const { entity, updateResult: entityUpdateResult } = await Record.getOrCreateEntityByKeys({
user,
survey,
entityDefUuid,
valuesByDefUuid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const fetchOrCreateRecord = async ({ valuesByDefUuid, currentRecord, context, tx
let record = null
if (dryRun) {
const { record: recordCreated } = await RecordUpdater.createRootEntity({
user,
survey,
record: recordToInsert,
sideEffect: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const insertNode = async (

if (NodeDef.isEntity(nodeDef)) {
const descendantsCreateResult = RecordNodesUpdater.createDescendants({
user,
survey,
record,
parentNode: node,
Expand Down
Loading

0 comments on commit e5833a0

Please sign in to comment.