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

Prefill annotation classes with values #140

Merged
merged 15 commits into from
Dec 11, 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
11 changes: 10 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- image: public.ecr.aws/dissco/${{ github.event.repository.name }}-production
keycloak_client: production-disscover-fe
- image: public.ecr.aws/dissco/${{ github.event.repository.name }}
keycloak_client: orchestration-service
steps:
- uses: actions/checkout@v2
with:
Expand Down Expand Up @@ -36,7 +43,7 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
images: public.ecr.aws/dissco/${{ github.event.repository.name }}
images: ${{ matrix.image }}
tags: |
type=sha
type=raw,value=latest
Expand All @@ -48,6 +55,8 @@ jobs:
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
"VITE_KEYCLOAK_CLIENT=${{ matrix.keycloak_client }}"
- name: Set outputs
id: vars
Expand Down
4 changes: 4 additions & 0 deletions dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ RUN cp 'src/app/GenerateTypes.js' 'src/app/GenerateTypes.cjs'
RUN rm 'src/app/GenerateTypes.js'
RUN node 'src/app/GenerateTypes.cjs'

# Set env variables
ARG VITE_KEYCLOAK_CLIENT
ENV VITE_KEYCLOAK_CLIENT ${VITE_KEYCLOAK_CLIENT}

# Setting app to production build
RUN npm run build

Expand Down
2 changes: 1 addition & 1 deletion src/app/Boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type Callback = (bootState: {

/**
* Function that runs the application's necessary processes before it can be booted
* @returns True or false depening on if is has booted
* @returns True or false depening on if it has booted
*/
const Boot = (callback: Callback) => {
/* Initiate keycloak which will render the root after finishing setting up */
Expand Down
2 changes: 1 addition & 1 deletion src/app/Hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ const usePagination = ({ pageSize, resultKey, params, allowSearchParams = false,
const result = await Method({ ...params, pageNumber: pageNumber, pageSize, ...(allowSearchParams && { searchFilters: searchFilters.GetSearchFilters() }) });

/* Set return data */
const records = resultKey ? result[resultKey] : result[Object.keys(result)[0]];
const records = resultKey ? result[resultKey] : result;

setReturnData({
records,
Expand Down
2 changes: 1 addition & 1 deletion src/app/Keycloak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type Callback = () => Function | void;
const keycloak = new Keycloak({
url: "https://login-demo.dissco.eu/auth",
realm: "dissco",
clientId: "orchestration-service",
clientId: import.meta.env.VITE_KEYCLOAK_CLIENT,
});

const InitKeyCloak = (callback?: Callback, token?: string) => {
Expand Down
49 changes: 40 additions & 9 deletions src/app/utilities/AnnotateUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jp from 'jsonpath';
import { cloneDeep, isEmpty, toLower } from 'lodash';

/* Import Utilities */
import { CheckForClassDefaultValues } from './ClassAnnotationUtilities';
import { ExtractLowestLevelSchema, ExtractClassesAndTermsFromSchema, MakeJsonPathReadableString } from 'app/utilities/SchemaUtilities';

/* Import Types */
Expand Down Expand Up @@ -42,7 +43,7 @@ const ConstructAnnotationObject = (params: {
}): AnnotationTemplate => {
const { digitalObjectId, digitalObjectType, motivation, annotationTargetType, jsonPath, annotationValues, fragments } = params;

let localJsonPath: string = jsonPath ?? '';
let localJsonPath: string = jsonPath?.replaceAll("'", '"') ?? '';

/* If motivation is adding, check for new index at end of JSON path and remove if it is there */
if (jsonPath && typeof (jp.parse(jsonPath).at(-1).expression.value) === 'number') {
Expand Down Expand Up @@ -182,7 +183,7 @@ const FormatJsonPathFromFieldName = (fieldName: string): string => {
* @param schemaName The name of the base schema
* @returns The annotation form field properties and their associated form values
*/
const GenerateAnnotationFormFieldProperties = async (jsonPath: string, superClass: SuperClass, schemaName: string) => {
const GenerateAnnotationFormFieldProperties = async (jsonPath: string, superClass: SuperClass, schemaName: string, motivation: string) => {
const annotationFormFieldProperties: {
[propertyName: string]: AnnotationFormProperty
} = {};
Expand All @@ -208,14 +209,33 @@ const GenerateAnnotationFormFieldProperties = async (jsonPath: string, superClas

/**
* Function to check for a default value for a term
*
* @param key The term key to check
*/
const CheckForTermDefaultValue = (key: string) => {
if (toLower(key).includes('date')) {
return format(new Date, 'yyyy-MM-dd');
}
};

/**
* Function to depict the values to be set for the class to be annotated
* @param fieldName The field name of the class (parsed from JSON path)
* @param classValues Possible existing class values from the super class
* @param isArray Boolean indicating if the class represents an array
* @param localClassValues Possible existing local class values of an annotation
*/
const DepictClassValues = (fieldName: string, classValues: Dict | Dict[] | undefined, isArray?: boolean, localClassValues?: Dict[]) => {
if (classValues) {
return classValues;
} else if (!isEmpty(localClassValues)) {
return localClassValues;
} else if (motivation === 'ods:adding' && isArray) {
return [CheckForClassDefaultValues(fieldName)];
} else if (motivation === 'ods:adding') {
return CheckForClassDefaultValues(fieldName) ?? {};
}
};

/* For each class, add it as a key property to the annotation form fields dictionary */
classesList.forEach(classProperty => {
if (!termValue) {
Expand All @@ -240,21 +260,32 @@ const GenerateAnnotationFormFieldProperties = async (jsonPath: string, superClas
}
});

formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = classFormValues ?? {};
formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = {};

/* Check if (default) values are present, if so, set form values property with them */
formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = DepictClassValues(
classProperty.value.replace(`$`, jsonPath), classFormValues
);

} else if (classProperty.value.includes('has') && classProperty.value.at(-3) === 's') {
const localClassValues: Dict[] | undefined = classValues ?? [];
let localClassValues: Dict[] = classValues ?? [];

if (!classValues) {
const parentFieldName: string = classProperty.value.replace(`$`, jsonPath).split('[').slice(0, -1).join('[');
const parentValues: Dict | undefined = jp.value(superClass, parentFieldName);
const childFieldName: string = classProperty.value.split('[').pop()?.replace(']', '').replaceAll("'", '') ?? '';

PushToLocalClassValues(parentValues, childFieldName, (value: Dict) => localClassValues.push(value));
PushToLocalClassValues(parentValues, childFieldName, (value: Dict) => localClassValues?.push(value));
}

formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = classValues ?? localClassValues ?? [];
formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = [];

/* Check if (default) values are present, if so, set form values property with them */
formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = DepictClassValues(
classProperty.value.replace(`$`, jsonPath), classValues, true, localClassValues
);
} else {
formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = {};
formValues[FormatFieldNameFromJsonPath(classProperty.value.replace(`$`, jsonPath))] = CheckForClassDefaultValues(classProperty.value.replace(`$`, jsonPath)) ?? {};
}

/* For each term of class, add it to the properties array of the corresponding class in the annotation form fields dictionary */
Expand All @@ -271,7 +302,7 @@ const GenerateAnnotationFormFieldProperties = async (jsonPath: string, superClas
});

/* Check if the term field needs to be filled with a default value */
if (CheckForTermDefaultValue(termOption.key)) {
if (CheckForTermDefaultValue(termOption.key) && !(termOption.key in classFormValues)) {
classFormValues[termOption.key] = CheckForTermDefaultValue(termOption.key);
}
});
Expand Down
101 changes: 101 additions & 0 deletions src/app/utilities/ClassAnnotationUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/* Import Dependencies */
import KeycloakService from "app/Keycloak";

/* Import Utilities */
import { FormatFieldNameFromJsonPath } from "./AnnotateUtilities";


/* Utilities associated with a class annotation */


/**
* Function to check for class default values and if present, return them
* @param jsonPath The JSON path of the class to check
* @returns Prefilled data array or object depending on provided class
*/
const CheckForClassDefaultValues = (jsonPath: string) => {
const className: string = FormatFieldNameFromJsonPath(jsonPath.replaceAll(/\[(\d+)\]/g, '')).split('_').slice(-1)[0].replace('$', '').replaceAll("'", '');

switch (className) {
case 'ods:hasAgents': {
return ClassAgents();
}
case 'ods:hasGeoreference': {
return ClassGeoreference();
}
case 'ods:hasIdentifiers': {
return ClassIdentifiers(jsonPath);
}
case 'ods:hasRoles': {
return ClassRoles();
}
};
};

/**
* Function to create a prefilled agent values object
* @returns Prefilled agents values array
*/
const ClassAgents = () => {
/* Construct agent values object */
const classValues = {
"@id": KeycloakService.GetParsedToken()?.orcid,
"@type": 'schema:Person',
"schema:identifier": KeycloakService.GetParsedToken()?.orcid,
"schema:name": `${KeycloakService.GetParsedToken()?.given_name ?? ''} ${KeycloakService.GetParsedToken()?.family_name ?? ''}`
};

return classValues;
};

/**
* Function to create a prefilled role values object
* @returns Prefilled georeferene values object
*/
const ClassGeoreference = () => {
/* Construct role values object */
const classValues = {
"@type": 'ods:Georeference'
};

return classValues;
};

/**
* Function to create a prefilled role values object
* @returns Prefilled identifiers values array
*/
const ClassIdentifiers = (jsonPath: string) => {
/* Extract parent class name from JSON path */
const parentClassName: string = FormatFieldNameFromJsonPath(jsonPath.replaceAll(/\[(\d+)\]/g, '')).split('_').slice(-2)[0].replace('$', '').replaceAll("'", '');

/* Set parent specific values */
const dctermsIdentifier: string = parentClassName === 'ods:hasAgents' ? KeycloakService.GetParsedToken()?.orcid : '';

/* Construct identifier values object */
const classValues = {
"@type": '',
"dcterms:title": '',
"dcterms:identifier": dctermsIdentifier
};

return classValues;
};

/**
* Function to create a prefilled role values object
* @returns Prefilled roles values array
*/
const ClassRoles = () => {
/* Construct role values object */
const classValues = {
"@type": '',
"schema:roleName": ''
};

return classValues;
};

export {
CheckForClassDefaultValues
};
11 changes: 11 additions & 0 deletions src/app/utilities/NomenclaturalUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ const GetSpecimenNameHTMLLabel = (digitalSpecimen: DigitalSpecimen): string => {
}
};

/**
* Function to get the scientific name identifier (that leads to COL) of the first accepted identification
* @param digitalSpecimen The provided digital specimen
*/
const GetSpecimenNameIdentifier = (digitalSpecimen: DigitalSpecimen) => {
const acceptedIdentification: Identification | undefined = digitalSpecimen['ods:hasIdentifications']?.find(identification => identification['ods:isVerifiedIdentification']);

return acceptedIdentification?.['ods:hasTaxonIdentifications']?.[0]["dwc:acceptedNameUsageID"];
};

/**
* Function to get the genus HTML label of the first accepted identification within the digital specimen, if not present, provide generic genus string
* @param digitalSpecimen The provided digital specimen
Expand All @@ -74,5 +84,6 @@ export {
DetermineScientificName,
DetermineTopicDisciplineIcon,
GetSpecimenNameHTMLLabel,
GetSpecimenNameIdentifier,
GetSpecimenGenusLabel
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useState } from 'react';
import { Row, Col, Card } from 'react-bootstrap';

/* Import Utiltiies */
import { GetSpecimenNameHTMLLabel } from 'app/utilities/NomenclaturalUtilities';
import { GetSpecimenNameHTMLLabel, GetSpecimenNameIdentifier } from 'app/utilities/NomenclaturalUtilities';

/* Import Types */
import { DigitalSpecimen } from 'app/types/DigitalSpecimen';
Expand Down Expand Up @@ -74,16 +74,68 @@ const DigitalSpecimenOverview = (props: Props) => {
* @returns Citation string
*/
const CraftCitation = (label?: boolean) => {
/**
* Function to determine the organisation name for the citation string
* @returns Organisation name with link
*/
const OrganisationName = () => {
return <a href={digitalSpecimen['ods:organisationID']}
target="_blank"
rel="noreferer"
className="tc-accent"
>
{digitalSpecimen['ods:organisationName'] ?? digitalSpecimen['ods:organisationID']}
</a>
};

/**
* Function to determine the scientific name for the citation string
* @returns Scientific name, with or without link/label
*/
const ScientificName = () => {
const scientificNameIdentifier = GetSpecimenNameIdentifier(digitalSpecimen);

if (scientificNameIdentifier) {
return (
<a href={scientificNameIdentifier}
target="_blank"
rel="noreferer"
className="tc-accent"
>
<span dangerouslySetInnerHTML={{ __html: GetSpecimenNameHTMLLabel(digitalSpecimen) }} />
</a>
);
} else {
return <span dangerouslySetInnerHTML={{ __html: (GetSpecimenNameHTMLLabel(digitalSpecimen) ?? digitalSpecimen['ods:specimenName']) }} />;
}
};

if (label) {
return (
<>
{`Distributed System of Scientific Collections (${new Date().getFullYear()}). `}
<span dangerouslySetInnerHTML={{ __html: GetSpecimenNameHTMLLabel(digitalSpecimen) }} />
{`. ${digitalSpecimen['ods:organisationName'] ? digitalSpecimen['ods:organisationName'] + '.' : ''} [Dataset]. ${digitalSpecimen['@id']}`}
{OrganisationName()}
{` (${new Date().getFullYear()}). `}
{ScientificName()}
{'. '}
<a href="https://ror.org/02wddde16"
target="_blank"
rel="noreferer"
className="tc-accent"
>
Distributed System of Scientific Collections
</a>
{`. [Dataset]. `}
<a href={digitalSpecimen['@id']}
target="_blank"
rel="noreferer"
className="tc-accent"
>
{digitalSpecimen['@id']}
</a>
</>
);
} else {
return `Distributed System of Scientific Collections (${new Date().getFullYear()}). ${digitalSpecimen['ods:specimenName']}. ${digitalSpecimen['ods:organisationName'] ? digitalSpecimen['ods:organisationName'] + '.' : ''} [Dataset]. ${digitalSpecimen['@id']}`;
return `${digitalSpecimen['ods:organisationName'] ? digitalSpecimen['ods:organisationName'] + '.' : digitalSpecimen['ods:organisationID'] + '.'} (${new Date().getFullYear()}). ${digitalSpecimen['ods:specimenName']}. Distributed System of Scientific Collections. [Dataset]. ${digitalSpecimen['@id']}`;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ const AnnotationsOverview = (props: Props) => {
if (jsonPath) {
filteredSortedAnnotations = filteredSortedAnnotations.filter(annotation => {
if (annotation["oa:hasTarget"]["oa:hasSelector"]?.["@type"] === 'ods:ClassSelector') {
return annotation["oa:hasTarget"]["oa:hasSelector"]["ods:class"] === jsonPath;
return annotation["oa:hasTarget"]["oa:hasSelector"]["ods:class"] === jsonPath.replaceAll("'", '"');
} else if (annotation["oa:hasTarget"]["oa:hasSelector"]?.["@type"] === 'ods:TermSelector') {
return annotation["oa:hasTarget"]["oa:hasSelector"]["ods:term"] === jsonPath;
return annotation["oa:hasTarget"]["oa:hasSelector"]["ods:term"] === jsonPath.replaceAll("'", '"');
}
});
}
Expand Down
Loading
Loading