From 052a8ca07765d4f3cbad76de95b89a897e62a77a Mon Sep 17 00:00:00 2001 From: pkdash Date: Sun, 12 May 2024 22:04:16 -0400 Subject: [PATCH 01/20] [#134] limiting search results with minimum relevance score --- api/routes/discovery.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/routes/discovery.py b/api/routes/discovery.py index e7c0ae8..d4bb47e 100644 --- a/api/routes/discovery.py +++ b/api/routes/discovery.py @@ -153,6 +153,9 @@ def stages(self): stages.append( {'$set': {'score': {'$meta': 'searchScore'}, 'highlights': {'$meta': 'searchHighlights'}}}, ) + if self.term: + # get only results with relevance score > 1.5 (which seems to be a good threshold) + stages.append({'$match': {'score': {'$gt': 1.5}}}) return stages From b3e75e903fc9093adea74b8f32706f7b66c2b0f7 Mon Sep 17 00:00:00 2001 From: pkdash Date: Mon, 13 May 2024 17:34:17 -0400 Subject: [PATCH 02/20] [#134] using env variable for relevance score threshold --- .github/workflows/deploy-dev.yaml | 2 +- .github/workflows/deploy.yaml | 2 +- api/config/__init__.py | 1 + api/routes/discovery.py | 7 +++++-- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index 7cb8c34..94f7af6 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -44,7 +44,7 @@ jobs: DB_USERNAME: ${{ secrets.DB_USERNAME_BETA }} DB_PASSWORD: ${{ secrets.DB_PASSWORD_BETA }} run: | - variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL") + variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL", "SEARCH_RELEVANCE_SCORE_THRESHOLD") # Empty the .env file > .env diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 126717e..9e2e1af 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -46,7 +46,7 @@ jobs: DB_USERNAME: ${{ secrets.DB_USERNAME }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} run: | - variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL") + variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL", "SEARCH_RELEVANCE_SCORE_THRESHOLD") # Empty the .env file > .env diff --git a/api/config/__init__.py b/api/config/__init__.py index cad45f3..adb3ba3 100644 --- a/api/config/__init__.py +++ b/api/config/__init__.py @@ -19,6 +19,7 @@ class Settings(BaseSettings): oidc_issuer: str hydroshare_meta_read_url: HttpUrl hydroshare_file_read_url: HttpUrl + search_relevance_score_threshold: float = 1.4 def __init__(self, **data: Any) -> None: super().__init__(**data) diff --git a/api/routes/discovery.py b/api/routes/discovery.py index d4bb47e..6becfe6 100644 --- a/api/routes/discovery.py +++ b/api/routes/discovery.py @@ -3,6 +3,8 @@ from fastapi import APIRouter, Request, Depends from pydantic import BaseModel, validator +from api.config import get_settings + router = APIRouter() @@ -154,8 +156,9 @@ def stages(self): {'$set': {'score': {'$meta': 'searchScore'}, 'highlights': {'$meta': 'searchHighlights'}}}, ) if self.term: - # get only results with relevance score > 1.5 (which seems to be a good threshold) - stages.append({'$match': {'score': {'$gt': 1.5}}}) + # get only results which meet minimum relevance score threshold + score_threshold = get_settings().search_relevance_score_threshold + stages.append({'$match': {'score': {'$gt': score_threshold}}}) return stages From 7a0f9ffc99a5096c8fe36fbc2886fcbf7328b02d Mon Sep 17 00:00:00 2001 From: Jeff Horsburgh Date: Tue, 14 May 2024 08:58:07 -0600 Subject: [PATCH 03/20] Update README.md Adding information about the catalog to the main readme file. --- README.md | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d6cce32..4be6d7f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,30 @@ -# I-GUIDE catalog API -I-GUIDE Catalog API +# I-GUIDE Catalog + +The I-GUIDE Catalog is part of of the [I-GUIDE Cyberinfrastructure Platform](https://i-guide.io/platform/). The Platform supports collaborative research along with computation and data-intensive geospatial problem solving. Within the context of the I-GUIDE Platform, the goal of the Catalog is to allow users to find, explore, and share data, models, code, software, hosted services, computational resources, and learning materials. A major goal of the catalog is to make these resources "actionable" - e.g., once a user finds a resource, they should be able to interact with the content of the resource and/or launch it into an appropriate analysis or computational environment for execution and exploration. + +## Deployment + +The I-GUIDE Catalog is currently deployed at [https://iguide.cuahsi.io/](https://iguide.cuahsi.io/). + +## Issue Tracker + +Please report any bugs or ideas for enhancements to the I-GUIDE Catalog issue tracker: + +[https://github.com/I-GUIDE/catalog/issues](https://github.com/I-GUIDE/catalog/issues) + +## License + +The I-GUIDE Catalog is released under the BSD 3-Clause License. This means that you can do what you want with the code [provided that you inlude the BSD copyright and license notice in it](https://www.tldrlegal.com/license/bsd-3-clause-license-revised). + +©2024 I-GUIDE Developers. + +## Sponsors and Credits + +[![NSF-2118329](https://img.shields.io/badge/NSF-2118329-blue.svg)](https://nsf.gov/awardsearch/showAward?AWD_ID=2118329) + +This material is based upon work supported by the National Science Foundation (NSF) under award [2118329](https://www.nsf.gov/awardsearch/showAward?AWD_ID=2118329). Any opinions, findings, conclusions, or recommendations expressed in this material are those of the authors and do not necessarily reflect the views of the NSF. + +## Developer Information ### Getting Started ```console From eea19682d27206959b4a3e41de3f1ef4cf458aca Mon Sep 17 00:00:00 2001 From: Maurier Date: Tue, 14 May 2024 10:18:02 -0600 Subject: [PATCH 04/20] update footer with new repo url and copyright notice --- .github/workflows/dependabot.yml | 2 +- frontend/src/assets/css/theme.scss | 4 ++++ frontend/src/components/base/cd.footer.vue | 10 ++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 3a3cce5..bc4ec88 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -6,6 +6,6 @@ version: 2 updates: - package-ecosystem: "npm" # See documentation for possible values - directory: "/" # Location of package manifests + directory: "/frontend" # Location of package manifests schedule: interval: "weekly" diff --git a/frontend/src/assets/css/theme.scss b/frontend/src/assets/css/theme.scss index 44d4f8b..0bbd4ca 100644 --- a/frontend/src/assets/css/theme.scss +++ b/frontend/src/assets/css/theme.scss @@ -94,4 +94,8 @@ mark { -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; +} + +.v-container { + max-width: 1920px; } \ No newline at end of file diff --git a/frontend/src/components/base/cd.footer.vue b/frontend/src/components/base/cd.footer.vue index 4adbfea..e178c0d 100644 --- a/frontend/src/components/base/cd.footer.vue +++ b/frontend/src/components/base/cd.footer.vue @@ -24,13 +24,12 @@
Open Source

The I-GUIDE Catalog and Discover system are Open Source. Find us on - GitHubGitHub.

Report a bug - here

@@ -42,9 +41,8 @@

- (c) {{ year }} CUAHSI. This material is based upon work supported by - the National Science Foundation (NSF) under awards 2012893, 2012593, and - 2012748.
+ © {{ year }} I-GUIDE Developers. This material is based upon work + supported by the National Science Foundation (NSF) under award 2118329. Any opinions, findings, conclusions, or recommendations expressed in this material are those of the authors and do not necessarily reflect the views of the NSF. From 5d754ef305fc156fbfcbecd3e3223b8ecd91cc20 Mon Sep 17 00:00:00 2001 From: Maurier Date: Tue, 14 May 2024 10:33:53 -0600 Subject: [PATCH 05/20] bump version --- frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/package.json b/frontend/package.json index 6c8c431..9c6eea4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "i-guide-catalog", - "version": "1.1.0", + "version": "1.1.1", "private": true, "scripts": { "serve": "vite --open", From 475e5cbe30739b0952cac92875cca35bb4aa925d Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 15 May 2024 13:19:11 -0600 Subject: [PATCH 06/20] download resource content over https --- .../src/components/dataset/cd.dataset.vue | 89 ++++++++++--------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/dataset/cd.dataset.vue b/frontend/src/components/dataset/cd.dataset.vue index 16507cd..a270f24 100644 --- a/frontend/src/components/dataset/cd.dataset.vue +++ b/frontend/src/components/dataset/cd.dataset.vue @@ -800,17 +800,19 @@

- + - + Content URL - {{ - selectedMetadata.metadata?.contentUrl - }} + {{ selectedMetadata.metadata?.contentUrl }} @@ -821,7 +823,7 @@ - + @@ -839,6 +841,10 @@ import { CzFileExplorer, Notifications } from "@cznethub/cznet-vue-core"; import { Loader, LoaderOptions } from "google-maps"; import CdSpatialCoverageMap from "@/components/search-results/cd.spatial-coverage-map.vue"; import User from "@/models/user.model"; +import markdownit from "markdown-it"; +import { Component, Vue, toNative } from "vue-facing-decorator"; +import { useGoTo } from "vuetify/lib/framework.mjs"; +import { useRoute, useRouter } from "vue-router"; const options: LoaderOptions = { libraries: ["drawing"] }; const loader: Loader = new Loader( @@ -846,13 +852,8 @@ const loader: Loader = new Loader( options, ); -import markdownit from "markdown-it"; const md = markdownit(); -import { Component, Vue, toNative } from "vue-facing-decorator"; -import { useGoTo } from "vuetify/lib/framework.mjs"; -import { useRoute, useRouter } from "vue-router"; - @Component({ name: "cd-dataset", components: { CdSpatialCoverageMap, CzFileExplorer }, @@ -868,11 +869,9 @@ class CdDataset extends Vue { tab = 0; selectedMetadata: any = false; readmeMd = ""; - // marked = marked; showCoordinateSystem = false; showExtent = false; - /** Example folder/file tree structure */ rootDirectory = { name: "root", children: [] as any[], @@ -919,6 +918,32 @@ class CdDataset extends Vue { route = useRoute(); router = useRouter(); + get hasSpatialFeatures(): boolean { + const feat = this.data.spatialCoverage?.["@type"]; + return feat === "GeoShape" || feat === "GeoCoordinates" || feat === "Place"; + } + + get schema() { + return User.$state.schema; + } + + get uiSchema() { + return User.$state.uiSchema; + } + + get boxCoordinates() { + const extents = this.data.spatialCoverage.geo.box + .trim() + .split(" ") + .map((n: string) => +n); + return { + north: extents[0], + east: extents[1], + south: extents[2], + west: extents[3], + }; + } + async created() { await this.loadDataset(); @@ -1089,6 +1114,14 @@ class CdDataset extends Vue { } } + getFileURL(fileMetadata: any): string { + const url = fileMetadata.metadata?.contentUrl; + if (url) { + return url.startsWith("http:") ? url.replace("http:", "https:") : url; + } + return ""; + } + parseDate(date: string): string { const parsed = new Date(Date.parse(date)); return parsed.toLocaleString("default", { @@ -1097,36 +1130,6 @@ class CdDataset extends Vue { year: "numeric", }); } - - // getTransformedSpatialCoverage() { - // return { ...data.spatialCoverage, } - // } - - get hasSpatialFeatures(): boolean { - const feat = this.data.spatialCoverage?.["@type"]; - return feat === "GeoShape" || feat === "GeoCoordinates" || feat === "Place"; - } - - get schema() { - return User.$state.schema; - } - - get uiSchema() { - return User.$state.uiSchema; - } - - get boxCoordinates() { - const extents = this.data.spatialCoverage.geo.box - .trim() - .split(" ") - .map((n: string) => +n); - return { - north: extents[0], - east: extents[1], - south: extents[2], - west: extents[3], - }; - } } export default toNative(CdDataset); From 45d64e06d2e9b0d7242826847f34e791f9bab605 Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 15 May 2024 13:30:16 -0600 Subject: [PATCH 07/20] use break word --- frontend/src/components/dataset/cd.dataset.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/dataset/cd.dataset.vue b/frontend/src/components/dataset/cd.dataset.vue index a270f24..5bf9480 100644 --- a/frontend/src/components/dataset/cd.dataset.vue +++ b/frontend/src/components/dataset/cd.dataset.vue @@ -1175,7 +1175,7 @@ export default toNative(CdDataset); .citation-text { min-width: 0; - word-break: break-all; + word-break: break-word; } #graph-container { From bef103f3b1296b469c88ecd72a84c979cacf92e0 Mon Sep 17 00:00:00 2001 From: pkdash Date: Wed, 15 May 2024 16:04:28 -0400 Subject: [PATCH 08/20] [#141] removing duplicate management script file --- README.md | 2 +- .../management/change_streams_pre_and_post.py | 5 +++-- .../management/change_streams_pre_and_post.py | 20 ------------------- 3 files changed, 4 insertions(+), 23 deletions(-) delete mode 100755 triggers/management/change_streams_pre_and_post.py diff --git a/README.md b/README.md index 4be6d7f..1f4a0d2 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ TESTING=True ``` 2. Login and submit a record to create all the collections -3. Run `triggers/management/change_streams_pre_and_post.py` +3. Run `api/models/management/change_streams_pre_and_post.py` 4. Create the catalog and typeahead indexes from `atlas/` (TODO detailed instructions) ### Triggers diff --git a/api/models/management/change_streams_pre_and_post.py b/api/models/management/change_streams_pre_and_post.py index aec4505..49daecd 100644 --- a/api/models/management/change_streams_pre_and_post.py +++ b/api/models/management/change_streams_pre_and_post.py @@ -1,10 +1,11 @@ import asyncio -from api.config import get_settings -from api.models.catalog import DatasetMetadataDOC from beanie import init_beanie from motor.motor_asyncio import AsyncIOMotorClient +from api.config import get_settings +from api.models.catalog import DatasetMetadataDOC + async def main(): db = AsyncIOMotorClient(get_settings().db_connection_string) diff --git a/triggers/management/change_streams_pre_and_post.py b/triggers/management/change_streams_pre_and_post.py deleted file mode 100755 index ebfaef1..0000000 --- a/triggers/management/change_streams_pre_and_post.py +++ /dev/null @@ -1,20 +0,0 @@ -import asyncio - -from beanie import init_beanie -from motor.motor_asyncio import AsyncIOMotorClient -from api.config import get_settings -from api.models.catalog import DatasetMetadataDOC - - -async def main(): - db = AsyncIOMotorClient(get_settings().db_connection_string) - await init_beanie(database=db[get_settings().database_name], document_models=[DatasetMetadataDOC]) - # https://www.mongodb.com/docs/manual/reference/command/collMod/#change-streams-with-document-pre--and-post-images - # This enables us to get the record id of a deleted submission within a trigger. We need this for removing - # discovery records when a submission is deleted - await db[get_settings().database_name].command( - ({'collMod': DatasetMetadataDOC.get_collection_name(), "changeStreamPreAndPostImages": {'enabled': True}})) - db.close() - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file From dd834a64d93ad76eef454541f3c64b310e815232 Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 15 May 2024 14:10:45 -0600 Subject: [PATCH 09/20] enable temporal coverage validation --- api/models/schema.py | 925 +++++++++++++++++---------------- api/models/schemas/schema.json | 24 + 2 files changed, 502 insertions(+), 447 deletions(-) diff --git a/api/models/schema.py b/api/models/schema.py index 8305b9c..f4f9a9e 100644 --- a/api/models/schema.py +++ b/api/models/schema.py @@ -12,549 +12,580 @@ class SchemaBaseModel(BaseModel): - class Config: - @staticmethod - def schema_extra(schema: dict[str, Any], model) -> None: - # json schema modification for jsonforms - for prop in schema.get('properties', {}).values(): - if 'format' in prop and prop['format'] == 'uri': - # using a regex for url matching - prop.pop('format') - prop[ - 'pattern' - ] = "^(http:\\/\\/www\\.|https:\\/\\/www\\.|http:\\/\\/|https:\\/\\/)?[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(\\/.*)?$" - prop['errorMessage'] = {"pattern": "must match format \"url\""} + class Config: + @staticmethod + def schema_extra(schema: dict[str, Any], model) -> None: + # json schema modification for jsonforms + for prop in schema.get('properties', {}).values(): + if 'format' in prop and prop['format'] == 'uri': + # using a regex for url matching + prop.pop('format') + prop[ + 'pattern' + ] = "^(http:\\/\\/www\\.|https:\\/\\/www\\.|http:\\/\\/|https:\\/\\/)?[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(\\/.*)?$" + prop['errorMessage'] = {"pattern": "must match format \"url\""} class CreativeWork(SchemaBaseModel): - type: str = Field( - alias="@type", - default="CreativeWork", - description="Submission type can include various forms of content, such as datasets, " - "software source code, digital documents, etc.", - ) - name: str = Field(description="Submission's name or title", title="Name or title") + type: str = Field( + alias="@type", + default="CreativeWork", + description="Submission type can include various forms of content, such as datasets, " + "software source code, digital documents, etc.", + ) + name: str = Field(description="Submission's name or title", + title="Name or title") class Person(SchemaBaseModel): - type: str = Field( - alias="@type", - default="Person", - const=True, - description="A person." - ) - name: str = Field( - description="A string containing the full name of the person. Personal name format: Family Name, Given Name." - ) - email: Optional[EmailStr] = Field(description="A string containing an email address for the person.") - identifier: Optional[List[str]] = Field( - description="Unique identifiers for the person. Where identifiers can be encoded as URLs, enter URLs here.") + type: str = Field( + alias="@type", + default="Person", + const=True, + description="A person." + ) + name: str = Field( + description="A string containing the full name of the person. Personal name format: Family Name, Given Name." + ) + email: Optional[EmailStr] = Field( + description="A string containing an email address for the person.") + identifier: Optional[List[str]] = Field( + description="Unique identifiers for the person. Where identifiers can be encoded as URLs, enter URLs here.") class Organization(SchemaBaseModel): - type: str = Field( - alias="@type", - default="Organization", - const=True - ) - name: str = Field(description="Name of the provider organization or repository.") - url: Optional[HttpUrl] = Field(title="URL", - description="A URL to the homepage for the organization." - ) - address: Optional[str] = Field( - description="Full address for the organization - e.g., “8200 Old Main Hill, Logan, UT 84322-8200”." - ) # Should address be a string or another constrained type? + type: str = Field( + alias="@type", + default="Organization", + const=True + ) + name: str = Field( + description="Name of the provider organization or repository.") + url: Optional[HttpUrl] = Field(title="URL", + description="A URL to the homepage for the organization." + ) + address: Optional[str] = Field( + description="Full address for the organization - e.g., “8200 Old Main Hill, Logan, UT 84322-8200”." + ) # Should address be a string or another constrained type? class Affiliation(Organization): - name: str = Field(description="Name of the organization the creator is affiliated with.") + name: str = Field( + description="Name of the organization the creator is affiliated with.") class Provider(Person): - identifier: Optional[str] = Field( - description="ORCID identifier for the person.", - pattern=orcid_pattern, - options={"placeholder": orcid_pattern_placeholder}, errorMessage={"pattern": orcid_pattern_error} - ) - email: Optional[EmailStr] = Field(description="A string containing an email address for the provider.") - affiliation: Optional[Affiliation] = Field(description="The affiliation of the creator with the organization.") + identifier: Optional[str] = Field( + description="ORCID identifier for the person.", + pattern=orcid_pattern, + options={"placeholder": orcid_pattern_placeholder}, errorMessage={"pattern": orcid_pattern_error} + ) + email: Optional[EmailStr] = Field( + description="A string containing an email address for the provider.") + affiliation: Optional[Affiliation] = Field( + description="The affiliation of the creator with the organization.") class Creator(Person): - identifier: Optional[str] = Field(description="ORCID identifier for creator.", - pattern=orcid_pattern, - options={"placeholder": orcid_pattern_placeholder}, - errorMessage={"pattern": orcid_pattern_error} - ) - email: Optional[EmailStr] = Field(description="A string containing an email address for the creator.") - affiliation: Optional[Affiliation] = Field(description="The affiliation of the creator with the organization.") + identifier: Optional[str] = Field(description="ORCID identifier for creator.", + pattern=orcid_pattern, + options={ + "placeholder": orcid_pattern_placeholder}, + errorMessage={ + "pattern": orcid_pattern_error} + ) + email: Optional[EmailStr] = Field( + description="A string containing an email address for the creator.") + affiliation: Optional[Affiliation] = Field( + description="The affiliation of the creator with the organization.") class FunderOrganization(Organization): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - schema = json.loads(FunderOrganization.schema_json()) - field_schema.update(schema, title="Funding Organization") + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + schema = json.loads(FunderOrganization.schema_json()) + field_schema.update(schema, title="Funding Organization") - name: str = Field(description="Name of the organization.") + name: str = Field(description="Name of the organization.") class PublisherOrganization(Organization): - name: str = Field(description="Name of the publishing organization.") - url: Optional[HttpUrl] = Field( - title="URL", - description="A URL to the homepage for the publisher organization or repository." - ) + name: str = Field(description="Name of the publishing organization.") + url: Optional[HttpUrl] = Field( + title="URL", + description="A URL to the homepage for the publisher organization or repository." + ) class SourceOrganization(Organization): - name: str = Field(description="Name of the organization that created the data.") + name: str = Field( + description="Name of the organization that created the data.") class DefinedTerm(SchemaBaseModel): - type: str = Field(alias="@type", default="DefinedTerm") - name: str = Field(description="The name of the term or item being defined.") - description: str = Field(description="The description of the item being defined.") + type: str = Field(alias="@type", default="DefinedTerm") + name: str = Field(description="The name of the term or item being defined.") + description: str = Field( + description="The description of the item being defined.") class Draft(DefinedTerm): - name: str = Field(default="Draft") - description: str = Field( - default="The resource is in draft state and should not be considered final. Content and metadata may change", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Draft") + description: str = Field( + default="The resource is in draft state and should not be considered final. Content and metadata may change", + readOnly=True, description="The description of the item being defined.") class Incomplete(DefinedTerm): - name: str = Field(default="Incomplete") - description: str = Field( - default="Data collection is ongoing or the resource is not completed", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Incomplete") + description: str = Field( + default="Data collection is ongoing or the resource is not completed", + readOnly=True, description="The description of the item being defined.") class Obsolete(DefinedTerm): - name: str = Field(default="Obsolete") - description: str = Field( - default="The resource has been replaced by a newer version, or the resource is no longer considered applicable", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Obsolete") + description: str = Field( + default="The resource has been replaced by a newer version, or the resource is no longer considered applicable", + readOnly=True, description="The description of the item being defined.") class Published(DefinedTerm): - name: str = Field(default="Published") - description: str = Field( - default="The resource has been permanently published and should be considered final and complete", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Published") + description: str = Field( + default="The resource has been permanently published and should be considered final and complete", + readOnly=True, description="The description of the item being defined.") class HasPart(CreativeWork): - url: Optional[HttpUrl] = Field(title="URL", description="The URL address to the data resource.") - description: Optional[str] = Field( - description="Information about a related resource that is part of this resource." - ) + url: Optional[HttpUrl] = Field( + title="URL", description="The URL address to the data resource.") + description: Optional[str] = Field( + description="Information about a related resource that is part of this resource." + ) class IsPartOf(CreativeWork): - url: Optional[HttpUrl] = Field(title="URL", description="The URL address to the data resource.") - description: Optional[str] = Field( - description="Information about a related resource that this resource is a " - "part of - e.g., a related collection." - ) + url: Optional[HttpUrl] = Field( + title="URL", description="The URL address to the data resource.") + description: Optional[str] = Field( + description="Information about a related resource that this resource is a " + "part of - e.g., a related collection." + ) class MediaObjectPartOf(CreativeWork): - url: Optional[HttpUrl] = Field(title="URL", description="The URL address to the related metadata document.") - description: Optional[str] = Field( - description="Information about a related metadata document." - ) + url: Optional[HttpUrl] = Field( + title="URL", description="The URL address to the related metadata document.") + description: Optional[str] = Field( + description="Information about a related metadata document." + ) class SubjectOf(CreativeWork): - url: Optional[HttpUrl] = Field( - title="URL", - description="The URL address that serves as a reference to access additional details related to the record. " - "It is important to note that this type of metadata solely pertains to the record itself and " - "may not necessarily be an integral component of the record, unlike the HasPart metadata." - ) - description: Optional[str] = Field( - description="Information about a related resource that is about or describes this " - "resource - e.g., a related metadata document describing the resource." - ) + url: Optional[HttpUrl] = Field( + title="URL", + description="The URL address that serves as a reference to access additional details related to the record. " + "It is important to note that this type of metadata solely pertains to the record itself and " + "may not necessarily be an integral component of the record, unlike the HasPart metadata." + ) + description: Optional[str] = Field( + description="Information about a related resource that is about or describes this " + "resource - e.g., a related metadata document describing the resource." + ) class License(CreativeWork): - name: str = Field( - description="A text string indicating the name of the license under which the resource is shared." - ) - url: Optional[HttpUrl] = Field(title="URL", description="A URL for a web page that describes the license.") - description: Optional[str] = Field( - description="A text string describing the license or containing the text of the license itself." - ) + name: str = Field( + description="A text string indicating the name of the license under which the resource is shared." + ) + url: Optional[HttpUrl] = Field( + title="URL", description="A URL for a web page that describes the license.") + description: Optional[str] = Field( + description="A text string describing the license or containing the text of the license itself." + ) class LanguageEnum(str, Enum): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update(type='string', title='Language', description='') + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', title='Language', description='') - eng = 'eng' - esp = 'esp' + eng = 'eng' + esp = 'esp' class InLanguageStr(str): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update(type='string', title='Other', description="Please specify another language.") + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', title='Other', + description="Please specify another language.") class IdentifierStr(str): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update(type='string', title='Identifier') + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', title='Identifier') class Grant(SchemaBaseModel): - type: str = Field( - alias="@type", - default="MonetaryGrant", - description="This metadata represents details about a grant or financial assistance provided to an " - "individual(s) or organization(s) for supporting the work related to the record." - ) - name: str = Field( - title="Name or title", - description="A text string indicating the name or title of the grant or financial assistance.") - description: Optional[str] = Field(description="A text string describing the grant or financial assistance.") - identifier: Optional[str] = Field( - title="Funding identifier", - description="Grant award number or other identifier." - ) - funder: Optional[FunderOrganization] = Field( - description="The organization that provided the funding or sponsorship." - ) + type: str = Field( + alias="@type", + default="MonetaryGrant", + description="This metadata represents details about a grant or financial assistance provided to an " + "individual(s) or organization(s) for supporting the work related to the record." + ) + name: str = Field( + title="Name or title", + description="A text string indicating the name or title of the grant or financial assistance.") + description: Optional[str] = Field( + description="A text string describing the grant or financial assistance.") + identifier: Optional[str] = Field( + title="Funding identifier", + description="Grant award number or other identifier." + ) + funder: Optional[FunderOrganization] = Field( + description="The organization that provided the funding or sponsorship." + ) class TemporalCoverage(SchemaBaseModel): - startDate: datetime = Field( - title="Start date", - description="A date/time object containing the instant corresponding to the commencement of the time " - "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM).", - # TODO: these are failing due to a problem with transpiled dependencies inside cznet-vue-core - # formatMaximum={"$data": "1/endDate"}, - # errorMessage= { "formatMaximum": "must be lesser than or equal to End date" } + startDate: datetime = Field( + title="Start date", + description="A date/time object containing the instant corresponding to the commencement of the time " + "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM).", + formatMaximum={"$data": "1/endDate"}, + errorMessage={ + "formatMaximum": "must be lesser than or equal to End date"} + ) + endDate: Optional[datetime] = Field( + title="End date", + description="A date/time object containing the instant corresponding to the termination of the time " + "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM). If the ending date is left off, " + "that means the temporal coverage is ongoing.", + formatMinimum={"$data": "1/startDate"}, + errorMessage={ + "formatMinimum": "must be greater than or equal to Start date"} ) - endDate: Optional[datetime] = Field( - title="End date", - description="A date/time object containing the instant corresponding to the termination of the time " - "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM). If the ending date is left off, " - "that means the temporal coverage is ongoing.", - # formatMinimum={"$data": "1/startDate"}, - # errorMessage= { "formatMinimum": "must be greater than or equal to Start date" } - ) class GeoCoordinates(SchemaBaseModel): - type: str = Field( - alias="@type", - default="GeoCoordinates", - description="Geographic coordinates that represent a specific location on the Earth's surface. " - "GeoCoordinates typically consists of two components: latitude and longitude." - ) - latitude: float = Field( - description="Represents the angular distance of a location north or south of the equator, " - "measured in degrees and ranges from -90 to +90 degrees." - ) - longitude: float = Field( - description="Represents the angular distance of a location east or west of the Prime Meridian, " - "measured in degrees and ranges from -180 to +180 degrees." - ) - - @validator('latitude') - def validate_latitude(cls, v): - if not -90 <= v <= 90: - raise ValueError('Latitude must be between -90 and 90') - return v - - @validator('longitude') - def validate_longitude(cls, v): - if not -180 <= v <= 180: - raise ValueError('Longitude must be between -180 and 180') - return v + type: str = Field( + alias="@type", + default="GeoCoordinates", + description="Geographic coordinates that represent a specific location on the Earth's surface. " + "GeoCoordinates typically consists of two components: latitude and longitude." + ) + latitude: float = Field( + description="Represents the angular distance of a location north or south of the equator, " + "measured in degrees and ranges from -90 to +90 degrees." + ) + longitude: float = Field( + description="Represents the angular distance of a location east or west of the Prime Meridian, " + "measured in degrees and ranges from -180 to +180 degrees." + ) + + @validator('latitude') + def validate_latitude(cls, v): + if not -90 <= v <= 90: + raise ValueError('Latitude must be between -90 and 90') + return v + + @validator('longitude') + def validate_longitude(cls, v): + if not -180 <= v <= 180: + raise ValueError('Longitude must be between -180 and 180') + return v class GeoShape(SchemaBaseModel): - type: str = Field( - alias="@type", - default="GeoShape", - description="A structured representation that describes the coordinates of a geographic feature." - ) - box: str = Field( - description="A box is a rectangular region defined by a pair of coordinates representing the " - "southwest and northeast corners of the box." - ) - - @validator('box') - def validate_box(cls, v): - if not isinstance(v, str): - raise TypeError('string required') - v = v.strip() - if not v: - raise ValueError('empty string') - v_parts = v.split(' ') - if len(v_parts) != 4: - raise ValueError('Bounding box must have 4 coordinate points') - for index, item in enumerate(v_parts, start=1): - try: - item = float(item) - except ValueError: - raise ValueError('Bounding box coordinate value is not a number') - item = abs(item) - if index % 2 == 0: - if item > 180: - raise ValueError('Bounding box coordinate east/west must be between -180 and 180') - elif item > 90: - raise ValueError('Bounding box coordinate north/south must be between -90 and 90') - - return v + type: str = Field( + alias="@type", + default="GeoShape", + description="A structured representation that describes the coordinates of a geographic feature." + ) + box: str = Field( + description="A box is a rectangular region defined by a pair of coordinates representing the " + "southwest and northeast corners of the box." + ) + + @validator('box') + def validate_box(cls, v): + if not isinstance(v, str): + raise TypeError('string required') + v = v.strip() + if not v: + raise ValueError('empty string') + v_parts = v.split(' ') + if len(v_parts) != 4: + raise ValueError('Bounding box must have 4 coordinate points') + for index, item in enumerate(v_parts, start=1): + try: + item = float(item) + except ValueError: + raise ValueError('Bounding box coordinate value is not a number') + item = abs(item) + if index % 2 == 0: + if item > 180: + raise ValueError( + 'Bounding box coordinate east/west must be between -180 and 180') + elif item > 90: + raise ValueError( + 'Bounding box coordinate north/south must be between -90 and 90') + + return v class PropertyValueBase(SchemaBaseModel): - type: str = Field( - alias="@type", - default="PropertyValue", - const="PropertyValue", - description="A property-value pair.", - ) - propertyID: Optional[str] = Field( - title="Property ID", description="The ID of the property." - ) - name: str = Field(description="The name of the property.") - value: str = Field(description="The value of the property.") - unitCode: Optional[str] = Field( - title="Measurement unit", description="The unit of measurement for the value." - ) - description: Optional[str] = Field(description="A description of the property.") - minValue: Optional[float] = Field( - title="Minimum value", description="The minimum allowed value for the property." - ) - maxValue: Optional[float] = Field( - title="Maximum value", description="The maximum allowed value for the property." - ) - measurementTechnique: Optional[str] = Field( - title="Measurement technique", description="A technique or technology used in a measurement." - ) - - class Config: - title = "PropertyValue" - - @root_validator - def validate_min_max_values(cls, values): - min_value = values.get("minValue", None) - max_value = values.get("maxValue", None) - if min_value is not None and max_value is not None: - if min_value > max_value: - raise ValueError("Minimum value must be less than or equal to maximum value") - - return values + type: str = Field( + alias="@type", + default="PropertyValue", + const="PropertyValue", + description="A property-value pair.", + ) + propertyID: Optional[str] = Field( + title="Property ID", description="The ID of the property." + ) + name: str = Field(description="The name of the property.") + value: str = Field(description="The value of the property.") + unitCode: Optional[str] = Field( + title="Measurement unit", description="The unit of measurement for the value." + ) + description: Optional[str] = Field( + description="A description of the property.") + minValue: Optional[float] = Field( + title="Minimum value", description="The minimum allowed value for the property." + ) + maxValue: Optional[float] = Field( + title="Maximum value", description="The maximum allowed value for the property." + ) + measurementTechnique: Optional[str] = Field( + title="Measurement technique", description="A technique or technology used in a measurement." + ) + + class Config: + title = "PropertyValue" + + @root_validator + def validate_min_max_values(cls, values): + min_value = values.get("minValue", None) + max_value = values.get("maxValue", None) + if min_value is not None and max_value is not None: + if min_value > max_value: + raise ValueError( + "Minimum value must be less than or equal to maximum value") + + return values class PropertyValue(PropertyValueBase): - # using PropertyValueBase model instead of PropertyValue model as one of the types for the value field - # in order for the schema generation (schema.json) to work. Self referencing nested models leads to - # infinite loop in our custom schema generation code when trying to replace dict with key '$ref' - value: Union[str, PropertyValueBase, List[PropertyValueBase]] = Field(description="The value of the property.") + # using PropertyValueBase model instead of PropertyValue model as one of the types for the value field + # in order for the schema generation (schema.json) to work. Self referencing nested models leads to + # infinite loop in our custom schema generation code when trying to replace dict with key '$ref' + value: Union[str, PropertyValueBase, List[PropertyValueBase] + ] = Field(description="The value of the property.") class Place(SchemaBaseModel): - type: str = Field(alias="@type", default="Place", description="Represents the focus area of the record's content.") - name: Optional[str] = Field(description="Name of the place.") - geo: Optional[Union[GeoCoordinates, GeoShape]] = Field( - description="Specifies the geographic coordinates of the place in the form of a point location, line, " - "or area coverage extent." - ) - - additionalProperty: Optional[List[PropertyValue]] = Field( - title="Additional properties", - default=[], - description="Additional properties of the place." - ) - - @root_validator - def validate_geo_or_name_required(cls, values): - name = values.get('name', None) - geo = values.get('geo', None) - if not name and not geo: - raise ValueError('Either place name or geo location of the place must be provided') - return values + type: str = Field(alias="@type", default="Place", + description="Represents the focus area of the record's content.") + name: Optional[str] = Field(description="Name of the place.") + geo: Optional[Union[GeoCoordinates, GeoShape]] = Field( + description="Specifies the geographic coordinates of the place in the form of a point location, line, " + "or area coverage extent." + ) + + additionalProperty: Optional[List[PropertyValue]] = Field( + title="Additional properties", + default=[], + description="Additional properties of the place." + ) + + @root_validator + def validate_geo_or_name_required(cls, values): + name = values.get('name', None) + geo = values.get('geo', None) + if not name and not geo: + raise ValueError( + 'Either place name or geo location of the place must be provided') + return values class MediaObject(SchemaBaseModel): - type: str = Field(alias="@type", default="MediaObject", description="An item that encodes the record.") - contentUrl: HttpUrl = Field( - title="Content URL", - description="The direct URL link to access or download the actual content of the media object.") - encodingFormat: Optional[str] = Field( - title="Encoding format", - description="Represents the specific file format in which the media is encoded." - ) # TODO enum for encoding formats - contentSize: str = Field( - title="Content size", - description="Represents the file size, expressed in bytes, kilobytes, megabytes, or another " - "unit of measurement." - ) - name: str = Field(description="The name of the media object (file).") - sha256: Optional[str] = Field(title="SHA-256", description="The SHA-256 hash of the media object.") - isPartOf: Optional[List[MediaObjectPartOf]] = Field( - title="Is part of", - description="Link to or citation for a related metadata document that this media object is a part of", - ) - - @validator('contentSize') - def validate_content_size(cls, v): - v = v.strip() - if not v: - raise ValueError('empty string') - - match = re.match(r"([0-9.]+)([a-zA-Z]+$)", v.replace(" ", "")) - if not match: - raise ValueError('invalid format') - - size_unit = match.group(2) - if size_unit.upper() not in [ - 'KB', - 'MB', - 'GB', - 'TB', - 'PB', - 'KILOBYTES', - 'MEGABYTES', - 'GIGABYTES', - 'TERABYTES', - 'PETABYTES', - ]: - raise ValueError('invalid unit') - - return v - - # TODO: not validating the SHA-256 hash for now as the hydroshare content file hash is in md5 format - # @validator('sha256') - # def validate_sha256_string_format(cls, v): - # if v: - # v = v.strip() - # if v and not re.match(r"^[a-fA-F0-9]{64}$", v): - # raise ValueError('invalid SHA-256 format') - # return v + type: str = Field(alias="@type", default="MediaObject", + description="An item that encodes the record.") + contentUrl: HttpUrl = Field( + title="Content URL", + description="The direct URL link to access or download the actual content of the media object.") + encodingFormat: Optional[str] = Field( + title="Encoding format", + description="Represents the specific file format in which the media is encoded." + ) # TODO enum for encoding formats + contentSize: str = Field( + title="Content size", + description="Represents the file size, expressed in bytes, kilobytes, megabytes, or another " + "unit of measurement." + ) + name: str = Field(description="The name of the media object (file).") + sha256: Optional[str] = Field( + title="SHA-256", description="The SHA-256 hash of the media object.") + isPartOf: Optional[List[MediaObjectPartOf]] = Field( + title="Is part of", + description="Link to or citation for a related metadata document that this media object is a part of", + ) + + @validator('contentSize') + def validate_content_size(cls, v): + v = v.strip() + if not v: + raise ValueError('empty string') + + match = re.match(r"([0-9.]+)([a-zA-Z]+$)", v.replace(" ", "")) + if not match: + raise ValueError('invalid format') + + size_unit = match.group(2) + if size_unit.upper() not in [ + 'KB', + 'MB', + 'GB', + 'TB', + 'PB', + 'KILOBYTES', + 'MEGABYTES', + 'GIGABYTES', + 'TERABYTES', + 'PETABYTES', + ]: + raise ValueError('invalid unit') + + return v + + # TODO: not validating the SHA-256 hash for now as the hydroshare content file hash is in md5 format + # @validator('sha256') + # def validate_sha256_string_format(cls, v): + # if v: + # v = v.strip() + # if v and not re.match(r"^[a-fA-F0-9]{64}$", v): + # raise ValueError('invalid SHA-256 format') + # return v class CoreMetadata(SchemaBaseModel): - context: HttpUrl = Field( - alias='@context', - default='https://schema.org', - description="Specifies the vocabulary employed for understanding the structured data markup.") - type: str = Field(alias="@type", title="Submission type", default="Dataset", - description="Submission type can include various forms of content, such as datasets," - " software source code, digital documents, etc.", - enum=["Dataset", "Notebook", "Software Source Code"] - ) - name: str = Field(title="Name or title", - description="A text string with a descriptive name or title for the resource." - ) - description: str = Field(title="Description or abstract", - description="A text string containing a description/abstract for the resource." - ) - url: HttpUrl = Field( - title="URL", - description="A URL for the landing page that describes the resource and where the content " - "of the resource can be accessed. If there is no landing page," - " provide the URL of the content." - ) - identifier: Optional[List[IdentifierStr]] = Field( - title="Identifiers", - description="Any kind of identifier for the resource. Identifiers may be DOIs or unique strings " - "assigned by a repository. Multiple identifiers can be entered. Where identifiers can be " - "encoded as URLs, enter URLs here." - ) - creator: List[Union[Creator, Organization]] = Field(description="Person or Organization that created the resource.") - dateCreated: datetime = Field(title="Date created", description="The date on which the resource was created.") - keywords: List[str] = Field( - min_items=1, - description="Keywords or tags used to describe the dataset, delimited by commas." - ) - license: License = Field( - description="A license document that applies to the resource." - ) - provider: Union[Organization, Provider] = Field( - description="The repository, service provider, organization, person, or service performer that provides" - " access to the resource." - ) - publisher: Optional[PublisherOrganization] = Field( - title="Publisher", - description="Where the resource is permanently published, indicated the repository, service provider," - " or organization that published the resource - e.g., CUAHSI HydroShare." - " This may be the same as Provider." - ) - datePublished: Optional[datetime] = Field(title="Date published", - description="Date of first publication for the resource.") - subjectOf: Optional[List[SubjectOf]] = Field( - title="Subject of", - description="Link to or citation for a related resource that is about or describes this resource" - " - e.g., a journal paper that describes this resource or a related metadata document " - "describing the resource.", - ) - version: Optional[str] = Field( - description="A text string indicating the version of the resource." - ) # TODO find something better than float for number - inLanguage: Optional[Union[LanguageEnum, InLanguageStr]] = Field( - title="Language", - description="The language of the content of the resource." - ) - creativeWorkStatus: Optional[Union[Draft, Incomplete, Obsolete, Published]] = Field( - title="Resource status", - description="The status of this resource in terms of its stage in a lifecycle. " - "Example terms include Incomplete, Draft, Published, and Obsolete.", - ) - dateModified: Optional[datetime] = Field( - title="Date modified", - description="The date on which the resource was most recently modified or updated." - ) - funding: Optional[List[Grant]] = Field( - description="A Grant or monetary assistance that directly or indirectly provided funding or sponsorship " - "for creation of the resource.", - ) - temporalCoverage: Optional[TemporalCoverage] = Field( - title="Temporal coverage", - description="The time period that applies to all of the content within the resource.", - ) - spatialCoverage: Optional[Place] = Field( - description="The spatialCoverage of a CreativeWork indicates the place(s) which are the focus of the content. " - "It is a sub property of contentLocation intended primarily for more technical and " - "detailed materials. For example with a Dataset, it indicates areas that the dataset " - "describes: a dataset of New York weather would have spatialCoverage which was the " - "place: the state of New York.", - ) - hasPart: Optional[List[HasPart]] = Field( - title="Has part", - description="Link to or citation for a related resource that is part of this resource." - ) - isPartOf: Optional[List[IsPartOf]] = Field( - title="Is part of", - description="Link to or citation for a related resource that this resource is a " - "part of - e.g., a related collection.", - ) - associatedMedia: Optional[List[MediaObject]] = Field( - title="Resource content", - description="A media object that encodes this CreativeWork. This property is a synonym for encoding.", - ) - citation: Optional[List[str]] = Field(title="Citation", description="A bibliographic citation for the resource.") + context: HttpUrl = Field( + alias='@context', + default='https://schema.org', + description="Specifies the vocabulary employed for understanding the structured data markup.") + type: str = Field(alias="@type", title="Submission type", default="Dataset", + description="Submission type can include various forms of content, such as datasets," + " software source code, digital documents, etc.", + enum=["Dataset", "Notebook", "Software Source Code"] + ) + name: str = Field(title="Name or title", + description="A text string with a descriptive name or title for the resource." + ) + description: str = Field(title="Description or abstract", + description="A text string containing a description/abstract for the resource." + ) + url: HttpUrl = Field( + title="URL", + description="A URL for the landing page that describes the resource and where the content " + "of the resource can be accessed. If there is no landing page," + " provide the URL of the content." + ) + identifier: Optional[List[IdentifierStr]] = Field( + title="Identifiers", + description="Any kind of identifier for the resource. Identifiers may be DOIs or unique strings " + "assigned by a repository. Multiple identifiers can be entered. Where identifiers can be " + "encoded as URLs, enter URLs here." + ) + creator: List[Union[Creator, Organization]] = Field( + description="Person or Organization that created the resource.") + dateCreated: datetime = Field( + title="Date created", description="The date on which the resource was created.") + keywords: List[str] = Field( + min_items=1, + description="Keywords or tags used to describe the dataset, delimited by commas." + ) + license: License = Field( + description="A license document that applies to the resource." + ) + provider: Union[Organization, Provider] = Field( + description="The repository, service provider, organization, person, or service performer that provides" + " access to the resource." + ) + publisher: Optional[PublisherOrganization] = Field( + title="Publisher", + description="Where the resource is permanently published, indicated the repository, service provider," + " or organization that published the resource - e.g., CUAHSI HydroShare." + " This may be the same as Provider." + ) + datePublished: Optional[datetime] = Field(title="Date published", + description="Date of first publication for the resource.") + subjectOf: Optional[List[SubjectOf]] = Field( + title="Subject of", + description="Link to or citation for a related resource that is about or describes this resource" + " - e.g., a journal paper that describes this resource or a related metadata document " + "describing the resource.", + ) + version: Optional[str] = Field( + description="A text string indicating the version of the resource." + ) # TODO find something better than float for number + inLanguage: Optional[Union[LanguageEnum, InLanguageStr]] = Field( + title="Language", + description="The language of the content of the resource." + ) + creativeWorkStatus: Optional[Union[Draft, Incomplete, Obsolete, Published]] = Field( + title="Resource status", + description="The status of this resource in terms of its stage in a lifecycle. " + "Example terms include Incomplete, Draft, Published, and Obsolete.", + ) + dateModified: Optional[datetime] = Field( + title="Date modified", + description="The date on which the resource was most recently modified or updated." + ) + funding: Optional[List[Grant]] = Field( + description="A Grant or monetary assistance that directly or indirectly provided funding or sponsorship " + "for creation of the resource.", + ) + temporalCoverage: Optional[TemporalCoverage] = Field( + title="Temporal coverage", + description="The time period that applies to all of the content within the resource.", + ) + spatialCoverage: Optional[Place] = Field( + description="The spatialCoverage of a CreativeWork indicates the place(s) which are the focus of the content. " + "It is a sub property of contentLocation intended primarily for more technical and " + "detailed materials. For example with a Dataset, it indicates areas that the dataset " + "describes: a dataset of New York weather would have spatialCoverage which was the " + "place: the state of New York.", + ) + hasPart: Optional[List[HasPart]] = Field( + title="Has part", + description="Link to or citation for a related resource that is part of this resource." + ) + isPartOf: Optional[List[IsPartOf]] = Field( + title="Is part of", + description="Link to or citation for a related resource that this resource is a " + "part of - e.g., a related collection.", + ) + associatedMedia: Optional[List[MediaObject]] = Field( + title="Resource content", + description="A media object that encodes this CreativeWork. This property is a synonym for encoding.", + ) + citation: Optional[List[str]] = Field( + title="Citation", description="A bibliographic citation for the resource.") class DatasetMetadata(CoreMetadata): - variableMeasured: Optional[List[Union[str, PropertyValue]]] = Field( - title="Variables measured", description="Measured variables." - ) - additionalProperty: Optional[List[PropertyValue]] = Field( - title="Additional properties", - default=[], - description="Additional properties of the dataset." - ) - sourceOrganization: Optional[SourceOrganization] = Field( - title="Source organization", - description="The organization that provided the data for this dataset." - ) + variableMeasured: Optional[List[Union[str, PropertyValue]]] = Field( + title="Variables measured", description="Measured variables." + ) + additionalProperty: Optional[List[PropertyValue]] = Field( + title="Additional properties", + default=[], + description="Additional properties of the dataset." + ) + sourceOrganization: Optional[SourceOrganization] = Field( + title="Source organization", + description="The organization that provided the data for this dataset." + ) diff --git a/api/models/schemas/schema.json b/api/models/schemas/schema.json index 779792b..06550c8 100644 --- a/api/models/schemas/schema.json +++ b/api/models/schemas/schema.json @@ -639,12 +639,24 @@ "startDate": { "title": "Start date", "description": "A date/time object containing the instant corresponding to the commencement of the time interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM).", + "formatMaximum": { + "$data": "1/endDate" + }, + "errorMessage": { + "formatMaximum": "must be lesser than or equal to End date" + }, "type": "string", "format": "date-time" }, "endDate": { "title": "End date", "description": "A date/time object containing the instant corresponding to the termination of the time interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM). If the ending date is left off, that means the temporal coverage is ongoing.", + "formatMinimum": { + "$data": "1/startDate" + }, + "errorMessage": { + "formatMinimum": "must be greater than or equal to Start date" + }, "type": "string", "format": "date-time" } @@ -2021,12 +2033,24 @@ "startDate": { "title": "Start date", "description": "A date/time object containing the instant corresponding to the commencement of the time interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM).", + "formatMaximum": { + "$data": "1/endDate" + }, + "errorMessage": { + "formatMaximum": "must be lesser than or equal to End date" + }, "type": "string", "format": "date-time" }, "endDate": { "title": "End date", "description": "A date/time object containing the instant corresponding to the termination of the time interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM). If the ending date is left off, that means the temporal coverage is ongoing.", + "formatMinimum": { + "$data": "1/startDate" + }, + "errorMessage": { + "formatMinimum": "must be greater than or equal to Start date" + }, "type": "string", "format": "date-time" } From 1702a8ff7ec851fde90f676b27a15376ca20457e Mon Sep 17 00:00:00 2001 From: pkdash Date: Wed, 15 May 2024 17:06:18 -0400 Subject: [PATCH 10/20] [#145] adding author name as search term --- api/routes/discovery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/routes/discovery.py b/api/routes/discovery.py index 6becfe6..6a95617 100644 --- a/api/routes/discovery.py +++ b/api/routes/discovery.py @@ -93,7 +93,7 @@ def _filters(self): @property def _should(self): - search_paths = ['name', 'description', 'keywords', 'keywords.name'] + search_paths = ['name', 'description', 'keywords', 'keywords.name', 'creator.name'] should = [ {'autocomplete': {'query': self.term, 'path': key, 'fuzzy': {'maxEdits': 1}}} for key in search_paths ] @@ -127,7 +127,7 @@ def _must(self): @property def stages(self): - highlightPaths = ['name', 'description', 'keywords', 'keywords.name'] + highlightPaths = ['name', 'description', 'keywords', 'keywords.name', 'creator.name'] stages = [] compound = {'filter': self._filters, 'must': self._must} if self.term: From b62d1124db51d59b7fdc5a7067d0ebf51adfe960 Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 15 May 2024 16:19:08 -0600 Subject: [PATCH 11/20] undo indentations --- api/models/schema.py | 924 +++++++++++++++++++++---------------------- 1 file changed, 446 insertions(+), 478 deletions(-) diff --git a/api/models/schema.py b/api/models/schema.py index f4f9a9e..0fdb84d 100644 --- a/api/models/schema.py +++ b/api/models/schema.py @@ -12,580 +12,548 @@ class SchemaBaseModel(BaseModel): - class Config: - @staticmethod - def schema_extra(schema: dict[str, Any], model) -> None: - # json schema modification for jsonforms - for prop in schema.get('properties', {}).values(): - if 'format' in prop and prop['format'] == 'uri': - # using a regex for url matching - prop.pop('format') - prop[ - 'pattern' - ] = "^(http:\\/\\/www\\.|https:\\/\\/www\\.|http:\\/\\/|https:\\/\\/)?[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(\\/.*)?$" - prop['errorMessage'] = {"pattern": "must match format \"url\""} + class Config: + @staticmethod + def schema_extra(schema: dict[str, Any], model) -> None: + # json schema modification for jsonforms + for prop in schema.get('properties', {}).values(): + if 'format' in prop and prop['format'] == 'uri': + # using a regex for url matching + prop.pop('format') + prop[ + 'pattern' + ] = "^(http:\\/\\/www\\.|https:\\/\\/www\\.|http:\\/\\/|https:\\/\\/)?[a-z0-9]+([\\-\\.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(\\/.*)?$" + prop['errorMessage'] = {"pattern": "must match format \"url\""} class CreativeWork(SchemaBaseModel): - type: str = Field( - alias="@type", - default="CreativeWork", - description="Submission type can include various forms of content, such as datasets, " - "software source code, digital documents, etc.", - ) - name: str = Field(description="Submission's name or title", - title="Name or title") + type: str = Field( + alias="@type", + default="CreativeWork", + description="Submission type can include various forms of content, such as datasets, " + "software source code, digital documents, etc.", + ) + name: str = Field(description="Submission's name or title", title="Name or title") class Person(SchemaBaseModel): - type: str = Field( - alias="@type", - default="Person", - const=True, - description="A person." - ) - name: str = Field( - description="A string containing the full name of the person. Personal name format: Family Name, Given Name." - ) - email: Optional[EmailStr] = Field( - description="A string containing an email address for the person.") - identifier: Optional[List[str]] = Field( - description="Unique identifiers for the person. Where identifiers can be encoded as URLs, enter URLs here.") + type: str = Field( + alias="@type", + default="Person", + const=True, + description="A person." + ) + name: str = Field( + description="A string containing the full name of the person. Personal name format: Family Name, Given Name." + ) + email: Optional[EmailStr] = Field(description="A string containing an email address for the person.") + identifier: Optional[List[str]] = Field( + description="Unique identifiers for the person. Where identifiers can be encoded as URLs, enter URLs here.") class Organization(SchemaBaseModel): - type: str = Field( - alias="@type", - default="Organization", - const=True - ) - name: str = Field( - description="Name of the provider organization or repository.") - url: Optional[HttpUrl] = Field(title="URL", - description="A URL to the homepage for the organization." - ) - address: Optional[str] = Field( - description="Full address for the organization - e.g., “8200 Old Main Hill, Logan, UT 84322-8200”." - ) # Should address be a string or another constrained type? + type: str = Field( + alias="@type", + default="Organization", + const=True + ) + name: str = Field(description="Name of the provider organization or repository.") + url: Optional[HttpUrl] = Field(title="URL", + description="A URL to the homepage for the organization." + ) + address: Optional[str] = Field( + description="Full address for the organization - e.g., “8200 Old Main Hill, Logan, UT 84322-8200”." + ) # Should address be a string or another constrained type? class Affiliation(Organization): - name: str = Field( - description="Name of the organization the creator is affiliated with.") + name: str = Field(description="Name of the organization the creator is affiliated with.") class Provider(Person): - identifier: Optional[str] = Field( - description="ORCID identifier for the person.", - pattern=orcid_pattern, - options={"placeholder": orcid_pattern_placeholder}, errorMessage={"pattern": orcid_pattern_error} - ) - email: Optional[EmailStr] = Field( - description="A string containing an email address for the provider.") - affiliation: Optional[Affiliation] = Field( - description="The affiliation of the creator with the organization.") + identifier: Optional[str] = Field( + description="ORCID identifier for the person.", + pattern=orcid_pattern, + options={"placeholder": orcid_pattern_placeholder}, errorMessage={"pattern": orcid_pattern_error} + ) + email: Optional[EmailStr] = Field(description="A string containing an email address for the provider.") + affiliation: Optional[Affiliation] = Field(description="The affiliation of the creator with the organization.") class Creator(Person): - identifier: Optional[str] = Field(description="ORCID identifier for creator.", - pattern=orcid_pattern, - options={ - "placeholder": orcid_pattern_placeholder}, - errorMessage={ - "pattern": orcid_pattern_error} - ) - email: Optional[EmailStr] = Field( - description="A string containing an email address for the creator.") - affiliation: Optional[Affiliation] = Field( - description="The affiliation of the creator with the organization.") + identifier: Optional[str] = Field(description="ORCID identifier for creator.", + pattern=orcid_pattern, + options={"placeholder": orcid_pattern_placeholder}, + errorMessage={"pattern": orcid_pattern_error} + ) + email: Optional[EmailStr] = Field(description="A string containing an email address for the creator.") + affiliation: Optional[Affiliation] = Field(description="The affiliation of the creator with the organization.") class FunderOrganization(Organization): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - schema = json.loads(FunderOrganization.schema_json()) - field_schema.update(schema, title="Funding Organization") + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + schema = json.loads(FunderOrganization.schema_json()) + field_schema.update(schema, title="Funding Organization") - name: str = Field(description="Name of the organization.") + name: str = Field(description="Name of the organization.") class PublisherOrganization(Organization): - name: str = Field(description="Name of the publishing organization.") - url: Optional[HttpUrl] = Field( - title="URL", - description="A URL to the homepage for the publisher organization or repository." - ) + name: str = Field(description="Name of the publishing organization.") + url: Optional[HttpUrl] = Field( + title="URL", + description="A URL to the homepage for the publisher organization or repository." + ) class SourceOrganization(Organization): - name: str = Field( - description="Name of the organization that created the data.") + name: str = Field(description="Name of the organization that created the data.") class DefinedTerm(SchemaBaseModel): - type: str = Field(alias="@type", default="DefinedTerm") - name: str = Field(description="The name of the term or item being defined.") - description: str = Field( - description="The description of the item being defined.") + type: str = Field(alias="@type", default="DefinedTerm") + name: str = Field(description="The name of the term or item being defined.") + description: str = Field(description="The description of the item being defined.") class Draft(DefinedTerm): - name: str = Field(default="Draft") - description: str = Field( - default="The resource is in draft state and should not be considered final. Content and metadata may change", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Draft") + description: str = Field( + default="The resource is in draft state and should not be considered final. Content and metadata may change", + readOnly=True, description="The description of the item being defined.") class Incomplete(DefinedTerm): - name: str = Field(default="Incomplete") - description: str = Field( - default="Data collection is ongoing or the resource is not completed", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Incomplete") + description: str = Field( + default="Data collection is ongoing or the resource is not completed", + readOnly=True, description="The description of the item being defined.") class Obsolete(DefinedTerm): - name: str = Field(default="Obsolete") - description: str = Field( - default="The resource has been replaced by a newer version, or the resource is no longer considered applicable", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Obsolete") + description: str = Field( + default="The resource has been replaced by a newer version, or the resource is no longer considered applicable", + readOnly=True, description="The description of the item being defined.") class Published(DefinedTerm): - name: str = Field(default="Published") - description: str = Field( - default="The resource has been permanently published and should be considered final and complete", - readOnly=True, description="The description of the item being defined.") + name: str = Field(default="Published") + description: str = Field( + default="The resource has been permanently published and should be considered final and complete", + readOnly=True, description="The description of the item being defined.") class HasPart(CreativeWork): - url: Optional[HttpUrl] = Field( - title="URL", description="The URL address to the data resource.") - description: Optional[str] = Field( - description="Information about a related resource that is part of this resource." - ) + url: Optional[HttpUrl] = Field(title="URL", description="The URL address to the data resource.") + description: Optional[str] = Field( + description="Information about a related resource that is part of this resource." + ) class IsPartOf(CreativeWork): - url: Optional[HttpUrl] = Field( - title="URL", description="The URL address to the data resource.") - description: Optional[str] = Field( - description="Information about a related resource that this resource is a " - "part of - e.g., a related collection." - ) + url: Optional[HttpUrl] = Field(title="URL", description="The URL address to the data resource.") + description: Optional[str] = Field( + description="Information about a related resource that this resource is a " + "part of - e.g., a related collection." + ) class MediaObjectPartOf(CreativeWork): - url: Optional[HttpUrl] = Field( - title="URL", description="The URL address to the related metadata document.") - description: Optional[str] = Field( - description="Information about a related metadata document." - ) + url: Optional[HttpUrl] = Field(title="URL", description="The URL address to the related metadata document.") + description: Optional[str] = Field( + description="Information about a related metadata document." + ) class SubjectOf(CreativeWork): - url: Optional[HttpUrl] = Field( - title="URL", - description="The URL address that serves as a reference to access additional details related to the record. " - "It is important to note that this type of metadata solely pertains to the record itself and " - "may not necessarily be an integral component of the record, unlike the HasPart metadata." - ) - description: Optional[str] = Field( - description="Information about a related resource that is about or describes this " - "resource - e.g., a related metadata document describing the resource." - ) + url: Optional[HttpUrl] = Field( + title="URL", + description="The URL address that serves as a reference to access additional details related to the record. " + "It is important to note that this type of metadata solely pertains to the record itself and " + "may not necessarily be an integral component of the record, unlike the HasPart metadata." + ) + description: Optional[str] = Field( + description="Information about a related resource that is about or describes this " + "resource - e.g., a related metadata document describing the resource." + ) class License(CreativeWork): - name: str = Field( - description="A text string indicating the name of the license under which the resource is shared." - ) - url: Optional[HttpUrl] = Field( - title="URL", description="A URL for a web page that describes the license.") - description: Optional[str] = Field( - description="A text string describing the license or containing the text of the license itself." - ) + name: str = Field( + description="A text string indicating the name of the license under which the resource is shared." + ) + url: Optional[HttpUrl] = Field(title="URL", description="A URL for a web page that describes the license.") + description: Optional[str] = Field( + description="A text string describing the license or containing the text of the license itself." + ) class LanguageEnum(str, Enum): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update(type='string', title='Language', description='') + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', title='Language', description='') - eng = 'eng' - esp = 'esp' + eng = 'eng' + esp = 'esp' class InLanguageStr(str): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update(type='string', title='Other', - description="Please specify another language.") + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', title='Other', description="Please specify another language.") class IdentifierStr(str): - @classmethod - def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: - field_schema.update(type='string', title='Identifier') + @classmethod + def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None: + field_schema.update(type='string', title='Identifier') class Grant(SchemaBaseModel): - type: str = Field( - alias="@type", - default="MonetaryGrant", - description="This metadata represents details about a grant or financial assistance provided to an " - "individual(s) or organization(s) for supporting the work related to the record." - ) - name: str = Field( - title="Name or title", - description="A text string indicating the name or title of the grant or financial assistance.") - description: Optional[str] = Field( - description="A text string describing the grant or financial assistance.") - identifier: Optional[str] = Field( - title="Funding identifier", - description="Grant award number or other identifier." - ) - funder: Optional[FunderOrganization] = Field( - description="The organization that provided the funding or sponsorship." - ) + type: str = Field( + alias="@type", + default="MonetaryGrant", + description="This metadata represents details about a grant or financial assistance provided to an " + "individual(s) or organization(s) for supporting the work related to the record." + ) + name: str = Field( + title="Name or title", + description="A text string indicating the name or title of the grant or financial assistance.") + description: Optional[str] = Field(description="A text string describing the grant or financial assistance.") + identifier: Optional[str] = Field( + title="Funding identifier", + description="Grant award number or other identifier." + ) + funder: Optional[FunderOrganization] = Field( + description="The organization that provided the funding or sponsorship." + ) class TemporalCoverage(SchemaBaseModel): - startDate: datetime = Field( - title="Start date", - description="A date/time object containing the instant corresponding to the commencement of the time " - "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM).", - formatMaximum={"$data": "1/endDate"}, - errorMessage={ - "formatMaximum": "must be lesser than or equal to End date"} - ) - endDate: Optional[datetime] = Field( - title="End date", - description="A date/time object containing the instant corresponding to the termination of the time " - "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM). If the ending date is left off, " - "that means the temporal coverage is ongoing.", - formatMinimum={"$data": "1/startDate"}, - errorMessage={ - "formatMinimum": "must be greater than or equal to Start date"} + startDate: datetime = Field( + title="Start date", + description="A date/time object containing the instant corresponding to the commencement of the time " + "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM).", + formatMaximum={"$data": "1/endDate"}, + errorMessage= { "formatMaximum": "must be lesser than or equal to End date" } ) + endDate: Optional[datetime] = Field( + title="End date", + description="A date/time object containing the instant corresponding to the termination of the time " + "interval (ISO8601 formatted date - YYYY-MM-DDTHH:MM). If the ending date is left off, " + "that means the temporal coverage is ongoing.", + formatMinimum={"$data": "1/startDate"}, + errorMessage= { "formatMinimum": "must be greater than or equal to Start date" } + ) class GeoCoordinates(SchemaBaseModel): - type: str = Field( - alias="@type", - default="GeoCoordinates", - description="Geographic coordinates that represent a specific location on the Earth's surface. " - "GeoCoordinates typically consists of two components: latitude and longitude." - ) - latitude: float = Field( - description="Represents the angular distance of a location north or south of the equator, " - "measured in degrees and ranges from -90 to +90 degrees." - ) - longitude: float = Field( - description="Represents the angular distance of a location east or west of the Prime Meridian, " - "measured in degrees and ranges from -180 to +180 degrees." - ) - - @validator('latitude') - def validate_latitude(cls, v): - if not -90 <= v <= 90: - raise ValueError('Latitude must be between -90 and 90') - return v - - @validator('longitude') - def validate_longitude(cls, v): - if not -180 <= v <= 180: - raise ValueError('Longitude must be between -180 and 180') - return v + type: str = Field( + alias="@type", + default="GeoCoordinates", + description="Geographic coordinates that represent a specific location on the Earth's surface. " + "GeoCoordinates typically consists of two components: latitude and longitude." + ) + latitude: float = Field( + description="Represents the angular distance of a location north or south of the equator, " + "measured in degrees and ranges from -90 to +90 degrees." + ) + longitude: float = Field( + description="Represents the angular distance of a location east or west of the Prime Meridian, " + "measured in degrees and ranges from -180 to +180 degrees." + ) + + @validator('latitude') + def validate_latitude(cls, v): + if not -90 <= v <= 90: + raise ValueError('Latitude must be between -90 and 90') + return v + + @validator('longitude') + def validate_longitude(cls, v): + if not -180 <= v <= 180: + raise ValueError('Longitude must be between -180 and 180') + return v class GeoShape(SchemaBaseModel): - type: str = Field( - alias="@type", - default="GeoShape", - description="A structured representation that describes the coordinates of a geographic feature." - ) - box: str = Field( - description="A box is a rectangular region defined by a pair of coordinates representing the " - "southwest and northeast corners of the box." - ) - - @validator('box') - def validate_box(cls, v): - if not isinstance(v, str): - raise TypeError('string required') - v = v.strip() - if not v: - raise ValueError('empty string') - v_parts = v.split(' ') - if len(v_parts) != 4: - raise ValueError('Bounding box must have 4 coordinate points') - for index, item in enumerate(v_parts, start=1): - try: - item = float(item) - except ValueError: - raise ValueError('Bounding box coordinate value is not a number') - item = abs(item) - if index % 2 == 0: - if item > 180: - raise ValueError( - 'Bounding box coordinate east/west must be between -180 and 180') - elif item > 90: - raise ValueError( - 'Bounding box coordinate north/south must be between -90 and 90') - - return v + type: str = Field( + alias="@type", + default="GeoShape", + description="A structured representation that describes the coordinates of a geographic feature." + ) + box: str = Field( + description="A box is a rectangular region defined by a pair of coordinates representing the " + "southwest and northeast corners of the box." + ) + + @validator('box') + def validate_box(cls, v): + if not isinstance(v, str): + raise TypeError('string required') + v = v.strip() + if not v: + raise ValueError('empty string') + v_parts = v.split(' ') + if len(v_parts) != 4: + raise ValueError('Bounding box must have 4 coordinate points') + for index, item in enumerate(v_parts, start=1): + try: + item = float(item) + except ValueError: + raise ValueError('Bounding box coordinate value is not a number') + item = abs(item) + if index % 2 == 0: + if item > 180: + raise ValueError('Bounding box coordinate east/west must be between -180 and 180') + elif item > 90: + raise ValueError('Bounding box coordinate north/south must be between -90 and 90') + + return v class PropertyValueBase(SchemaBaseModel): - type: str = Field( - alias="@type", - default="PropertyValue", - const="PropertyValue", - description="A property-value pair.", - ) - propertyID: Optional[str] = Field( - title="Property ID", description="The ID of the property." - ) - name: str = Field(description="The name of the property.") - value: str = Field(description="The value of the property.") - unitCode: Optional[str] = Field( - title="Measurement unit", description="The unit of measurement for the value." - ) - description: Optional[str] = Field( - description="A description of the property.") - minValue: Optional[float] = Field( - title="Minimum value", description="The minimum allowed value for the property." - ) - maxValue: Optional[float] = Field( - title="Maximum value", description="The maximum allowed value for the property." - ) - measurementTechnique: Optional[str] = Field( - title="Measurement technique", description="A technique or technology used in a measurement." - ) - - class Config: - title = "PropertyValue" - - @root_validator - def validate_min_max_values(cls, values): - min_value = values.get("minValue", None) - max_value = values.get("maxValue", None) - if min_value is not None and max_value is not None: - if min_value > max_value: - raise ValueError( - "Minimum value must be less than or equal to maximum value") - - return values + type: str = Field( + alias="@type", + default="PropertyValue", + const="PropertyValue", + description="A property-value pair.", + ) + propertyID: Optional[str] = Field( + title="Property ID", description="The ID of the property." + ) + name: str = Field(description="The name of the property.") + value: str = Field(description="The value of the property.") + unitCode: Optional[str] = Field( + title="Measurement unit", description="The unit of measurement for the value." + ) + description: Optional[str] = Field(description="A description of the property.") + minValue: Optional[float] = Field( + title="Minimum value", description="The minimum allowed value for the property." + ) + maxValue: Optional[float] = Field( + title="Maximum value", description="The maximum allowed value for the property." + ) + measurementTechnique: Optional[str] = Field( + title="Measurement technique", description="A technique or technology used in a measurement." + ) + + class Config: + title = "PropertyValue" + + @root_validator + def validate_min_max_values(cls, values): + min_value = values.get("minValue", None) + max_value = values.get("maxValue", None) + if min_value is not None and max_value is not None: + if min_value > max_value: + raise ValueError("Minimum value must be less than or equal to maximum value") + + return values class PropertyValue(PropertyValueBase): - # using PropertyValueBase model instead of PropertyValue model as one of the types for the value field - # in order for the schema generation (schema.json) to work. Self referencing nested models leads to - # infinite loop in our custom schema generation code when trying to replace dict with key '$ref' - value: Union[str, PropertyValueBase, List[PropertyValueBase] - ] = Field(description="The value of the property.") + # using PropertyValueBase model instead of PropertyValue model as one of the types for the value field + # in order for the schema generation (schema.json) to work. Self referencing nested models leads to + # infinite loop in our custom schema generation code when trying to replace dict with key '$ref' + value: Union[str, PropertyValueBase, List[PropertyValueBase]] = Field(description="The value of the property.") class Place(SchemaBaseModel): - type: str = Field(alias="@type", default="Place", - description="Represents the focus area of the record's content.") - name: Optional[str] = Field(description="Name of the place.") - geo: Optional[Union[GeoCoordinates, GeoShape]] = Field( - description="Specifies the geographic coordinates of the place in the form of a point location, line, " - "or area coverage extent." - ) - - additionalProperty: Optional[List[PropertyValue]] = Field( - title="Additional properties", - default=[], - description="Additional properties of the place." - ) - - @root_validator - def validate_geo_or_name_required(cls, values): - name = values.get('name', None) - geo = values.get('geo', None) - if not name and not geo: - raise ValueError( - 'Either place name or geo location of the place must be provided') - return values + type: str = Field(alias="@type", default="Place", description="Represents the focus area of the record's content.") + name: Optional[str] = Field(description="Name of the place.") + geo: Optional[Union[GeoCoordinates, GeoShape]] = Field( + description="Specifies the geographic coordinates of the place in the form of a point location, line, " + "or area coverage extent." + ) + + additionalProperty: Optional[List[PropertyValue]] = Field( + title="Additional properties", + default=[], + description="Additional properties of the place." + ) + + @root_validator + def validate_geo_or_name_required(cls, values): + name = values.get('name', None) + geo = values.get('geo', None) + if not name and not geo: + raise ValueError('Either place name or geo location of the place must be provided') + return values class MediaObject(SchemaBaseModel): - type: str = Field(alias="@type", default="MediaObject", - description="An item that encodes the record.") - contentUrl: HttpUrl = Field( - title="Content URL", - description="The direct URL link to access or download the actual content of the media object.") - encodingFormat: Optional[str] = Field( - title="Encoding format", - description="Represents the specific file format in which the media is encoded." - ) # TODO enum for encoding formats - contentSize: str = Field( - title="Content size", - description="Represents the file size, expressed in bytes, kilobytes, megabytes, or another " - "unit of measurement." - ) - name: str = Field(description="The name of the media object (file).") - sha256: Optional[str] = Field( - title="SHA-256", description="The SHA-256 hash of the media object.") - isPartOf: Optional[List[MediaObjectPartOf]] = Field( - title="Is part of", - description="Link to or citation for a related metadata document that this media object is a part of", - ) - - @validator('contentSize') - def validate_content_size(cls, v): - v = v.strip() - if not v: - raise ValueError('empty string') - - match = re.match(r"([0-9.]+)([a-zA-Z]+$)", v.replace(" ", "")) - if not match: - raise ValueError('invalid format') - - size_unit = match.group(2) - if size_unit.upper() not in [ - 'KB', - 'MB', - 'GB', - 'TB', - 'PB', - 'KILOBYTES', - 'MEGABYTES', - 'GIGABYTES', - 'TERABYTES', - 'PETABYTES', - ]: - raise ValueError('invalid unit') - - return v - - # TODO: not validating the SHA-256 hash for now as the hydroshare content file hash is in md5 format - # @validator('sha256') - # def validate_sha256_string_format(cls, v): - # if v: - # v = v.strip() - # if v and not re.match(r"^[a-fA-F0-9]{64}$", v): - # raise ValueError('invalid SHA-256 format') - # return v + type: str = Field(alias="@type", default="MediaObject", description="An item that encodes the record.") + contentUrl: HttpUrl = Field( + title="Content URL", + description="The direct URL link to access or download the actual content of the media object.") + encodingFormat: Optional[str] = Field( + title="Encoding format", + description="Represents the specific file format in which the media is encoded." + ) # TODO enum for encoding formats + contentSize: str = Field( + title="Content size", + description="Represents the file size, expressed in bytes, kilobytes, megabytes, or another " + "unit of measurement." + ) + name: str = Field(description="The name of the media object (file).") + sha256: Optional[str] = Field(title="SHA-256", description="The SHA-256 hash of the media object.") + isPartOf: Optional[List[MediaObjectPartOf]] = Field( + title="Is part of", + description="Link to or citation for a related metadata document that this media object is a part of", + ) + + @validator('contentSize') + def validate_content_size(cls, v): + v = v.strip() + if not v: + raise ValueError('empty string') + + match = re.match(r"([0-9.]+)([a-zA-Z]+$)", v.replace(" ", "")) + if not match: + raise ValueError('invalid format') + + size_unit = match.group(2) + if size_unit.upper() not in [ + 'KB', + 'MB', + 'GB', + 'TB', + 'PB', + 'KILOBYTES', + 'MEGABYTES', + 'GIGABYTES', + 'TERABYTES', + 'PETABYTES', + ]: + raise ValueError('invalid unit') + + return v + + # TODO: not validating the SHA-256 hash for now as the hydroshare content file hash is in md5 format + # @validator('sha256') + # def validate_sha256_string_format(cls, v): + # if v: + # v = v.strip() + # if v and not re.match(r"^[a-fA-F0-9]{64}$", v): + # raise ValueError('invalid SHA-256 format') + # return v class CoreMetadata(SchemaBaseModel): - context: HttpUrl = Field( - alias='@context', - default='https://schema.org', - description="Specifies the vocabulary employed for understanding the structured data markup.") - type: str = Field(alias="@type", title="Submission type", default="Dataset", - description="Submission type can include various forms of content, such as datasets," - " software source code, digital documents, etc.", - enum=["Dataset", "Notebook", "Software Source Code"] - ) - name: str = Field(title="Name or title", - description="A text string with a descriptive name or title for the resource." - ) - description: str = Field(title="Description or abstract", - description="A text string containing a description/abstract for the resource." - ) - url: HttpUrl = Field( - title="URL", - description="A URL for the landing page that describes the resource and where the content " - "of the resource can be accessed. If there is no landing page," - " provide the URL of the content." - ) - identifier: Optional[List[IdentifierStr]] = Field( - title="Identifiers", - description="Any kind of identifier for the resource. Identifiers may be DOIs or unique strings " - "assigned by a repository. Multiple identifiers can be entered. Where identifiers can be " - "encoded as URLs, enter URLs here." - ) - creator: List[Union[Creator, Organization]] = Field( - description="Person or Organization that created the resource.") - dateCreated: datetime = Field( - title="Date created", description="The date on which the resource was created.") - keywords: List[str] = Field( - min_items=1, - description="Keywords or tags used to describe the dataset, delimited by commas." - ) - license: License = Field( - description="A license document that applies to the resource." - ) - provider: Union[Organization, Provider] = Field( - description="The repository, service provider, organization, person, or service performer that provides" - " access to the resource." - ) - publisher: Optional[PublisherOrganization] = Field( - title="Publisher", - description="Where the resource is permanently published, indicated the repository, service provider," - " or organization that published the resource - e.g., CUAHSI HydroShare." - " This may be the same as Provider." - ) - datePublished: Optional[datetime] = Field(title="Date published", - description="Date of first publication for the resource.") - subjectOf: Optional[List[SubjectOf]] = Field( - title="Subject of", - description="Link to or citation for a related resource that is about or describes this resource" - " - e.g., a journal paper that describes this resource or a related metadata document " - "describing the resource.", - ) - version: Optional[str] = Field( - description="A text string indicating the version of the resource." - ) # TODO find something better than float for number - inLanguage: Optional[Union[LanguageEnum, InLanguageStr]] = Field( - title="Language", - description="The language of the content of the resource." - ) - creativeWorkStatus: Optional[Union[Draft, Incomplete, Obsolete, Published]] = Field( - title="Resource status", - description="The status of this resource in terms of its stage in a lifecycle. " - "Example terms include Incomplete, Draft, Published, and Obsolete.", - ) - dateModified: Optional[datetime] = Field( - title="Date modified", - description="The date on which the resource was most recently modified or updated." - ) - funding: Optional[List[Grant]] = Field( - description="A Grant or monetary assistance that directly or indirectly provided funding or sponsorship " - "for creation of the resource.", - ) - temporalCoverage: Optional[TemporalCoverage] = Field( - title="Temporal coverage", - description="The time period that applies to all of the content within the resource.", - ) - spatialCoverage: Optional[Place] = Field( - description="The spatialCoverage of a CreativeWork indicates the place(s) which are the focus of the content. " - "It is a sub property of contentLocation intended primarily for more technical and " - "detailed materials. For example with a Dataset, it indicates areas that the dataset " - "describes: a dataset of New York weather would have spatialCoverage which was the " - "place: the state of New York.", - ) - hasPart: Optional[List[HasPart]] = Field( - title="Has part", - description="Link to or citation for a related resource that is part of this resource." - ) - isPartOf: Optional[List[IsPartOf]] = Field( - title="Is part of", - description="Link to or citation for a related resource that this resource is a " - "part of - e.g., a related collection.", - ) - associatedMedia: Optional[List[MediaObject]] = Field( - title="Resource content", - description="A media object that encodes this CreativeWork. This property is a synonym for encoding.", - ) - citation: Optional[List[str]] = Field( - title="Citation", description="A bibliographic citation for the resource.") + context: HttpUrl = Field( + alias='@context', + default='https://schema.org', + description="Specifies the vocabulary employed for understanding the structured data markup.") + type: str = Field(alias="@type", title="Submission type", default="Dataset", + description="Submission type can include various forms of content, such as datasets," + " software source code, digital documents, etc.", + enum=["Dataset", "Notebook", "Software Source Code"] + ) + name: str = Field(title="Name or title", + description="A text string with a descriptive name or title for the resource." + ) + description: str = Field(title="Description or abstract", + description="A text string containing a description/abstract for the resource." + ) + url: HttpUrl = Field( + title="URL", + description="A URL for the landing page that describes the resource and where the content " + "of the resource can be accessed. If there is no landing page," + " provide the URL of the content." + ) + identifier: Optional[List[IdentifierStr]] = Field( + title="Identifiers", + description="Any kind of identifier for the resource. Identifiers may be DOIs or unique strings " + "assigned by a repository. Multiple identifiers can be entered. Where identifiers can be " + "encoded as URLs, enter URLs here." + ) + creator: List[Union[Creator, Organization]] = Field(description="Person or Organization that created the resource.") + dateCreated: datetime = Field(title="Date created", description="The date on which the resource was created.") + keywords: List[str] = Field( + min_items=1, + description="Keywords or tags used to describe the dataset, delimited by commas." + ) + license: License = Field( + description="A license document that applies to the resource." + ) + provider: Union[Organization, Provider] = Field( + description="The repository, service provider, organization, person, or service performer that provides" + " access to the resource." + ) + publisher: Optional[PublisherOrganization] = Field( + title="Publisher", + description="Where the resource is permanently published, indicated the repository, service provider," + " or organization that published the resource - e.g., CUAHSI HydroShare." + " This may be the same as Provider." + ) + datePublished: Optional[datetime] = Field(title="Date published", + description="Date of first publication for the resource.") + subjectOf: Optional[List[SubjectOf]] = Field( + title="Subject of", + description="Link to or citation for a related resource that is about or describes this resource" + " - e.g., a journal paper that describes this resource or a related metadata document " + "describing the resource.", + ) + version: Optional[str] = Field( + description="A text string indicating the version of the resource." + ) # TODO find something better than float for number + inLanguage: Optional[Union[LanguageEnum, InLanguageStr]] = Field( + title="Language", + description="The language of the content of the resource." + ) + creativeWorkStatus: Optional[Union[Draft, Incomplete, Obsolete, Published]] = Field( + title="Resource status", + description="The status of this resource in terms of its stage in a lifecycle. " + "Example terms include Incomplete, Draft, Published, and Obsolete.", + ) + dateModified: Optional[datetime] = Field( + title="Date modified", + description="The date on which the resource was most recently modified or updated." + ) + funding: Optional[List[Grant]] = Field( + description="A Grant or monetary assistance that directly or indirectly provided funding or sponsorship " + "for creation of the resource.", + ) + temporalCoverage: Optional[TemporalCoverage] = Field( + title="Temporal coverage", + description="The time period that applies to all of the content within the resource.", + ) + spatialCoverage: Optional[Place] = Field( + description="The spatialCoverage of a CreativeWork indicates the place(s) which are the focus of the content. " + "It is a sub property of contentLocation intended primarily for more technical and " + "detailed materials. For example with a Dataset, it indicates areas that the dataset " + "describes: a dataset of New York weather would have spatialCoverage which was the " + "place: the state of New York.", + ) + hasPart: Optional[List[HasPart]] = Field( + title="Has part", + description="Link to or citation for a related resource that is part of this resource." + ) + isPartOf: Optional[List[IsPartOf]] = Field( + title="Is part of", + description="Link to or citation for a related resource that this resource is a " + "part of - e.g., a related collection.", + ) + associatedMedia: Optional[List[MediaObject]] = Field( + title="Resource content", + description="A media object that encodes this CreativeWork. This property is a synonym for encoding.", + ) + citation: Optional[List[str]] = Field(title="Citation", description="A bibliographic citation for the resource.") class DatasetMetadata(CoreMetadata): - variableMeasured: Optional[List[Union[str, PropertyValue]]] = Field( - title="Variables measured", description="Measured variables." - ) - additionalProperty: Optional[List[PropertyValue]] = Field( - title="Additional properties", - default=[], - description="Additional properties of the dataset." - ) - sourceOrganization: Optional[SourceOrganization] = Field( - title="Source organization", - description="The organization that provided the data for this dataset." - ) + variableMeasured: Optional[List[Union[str, PropertyValue]]] = Field( + title="Variables measured", description="Measured variables." + ) + additionalProperty: Optional[List[PropertyValue]] = Field( + title="Additional properties", + default=[], + description="Additional properties of the dataset." + ) + sourceOrganization: Optional[SourceOrganization] = Field( + title="Source organization", + description="The organization that provided the data for this dataset." + ) From bb51b1e596c71c29fe0086319e5c36812e1e8a30 Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 15 May 2024 16:37:38 -0600 Subject: [PATCH 12/20] vscode sttings --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0bad23b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.detectIndentation": true +} \ No newline at end of file From e25f3b2fb9b489db4b2c0f033b6064ff1f5b6397 Mon Sep 17 00:00:00 2001 From: Maurier Date: Thu, 16 May 2024 11:24:41 -0600 Subject: [PATCH 13/20] remove comma --- .github/workflows/deploy-dev.yaml | 2 +- .github/workflows/deploy.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index 94f7af6..17050eb 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -44,7 +44,7 @@ jobs: DB_USERNAME: ${{ secrets.DB_USERNAME_BETA }} DB_PASSWORD: ${{ secrets.DB_PASSWORD_BETA }} run: | - variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL", "SEARCH_RELEVANCE_SCORE_THRESHOLD") + variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL" "SEARCH_RELEVANCE_SCORE_THRESHOLD") # Empty the .env file > .env diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9e2e1af..3c0e944 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -46,7 +46,7 @@ jobs: DB_USERNAME: ${{ secrets.DB_USERNAME }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} run: | - variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL", "SEARCH_RELEVANCE_SCORE_THRESHOLD") + variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL" "SEARCH_RELEVANCE_SCORE_THRESHOLD") # Empty the .env file > .env From 0db790dc4798f313ca851d1f9cd1dec2503780e4 Mon Sep 17 00:00:00 2001 From: Maurier Date: Thu, 16 May 2024 11:39:41 -0600 Subject: [PATCH 14/20] add missing env variable to action script --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6a4d539..8f7bc67 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,7 +36,7 @@ jobs: DB_USERNAME: ${{ secrets.DB_USERNAME }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} run: | - variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL") + variables=("OIDC_ISSUER" "DB_USERNAME" "DB_PASSWORD" "DB_HOST" "DATABASE_NAME" "DB_PROTOCOL" "TESTING" "VITE_APP_LOGIN_URL" "HYDROSHARE_META_READ_URL" "HYDROSHARE_FILE_READ_URL" "SEARCH_RELEVANCE_SCORE_THRESHOLD") # Empty the .env file > .env From 966e870cdd7858a14e25be37eced307769e45319 Mon Sep 17 00:00:00 2001 From: Maurier Date: Thu, 16 May 2024 12:08:41 -0600 Subject: [PATCH 15/20] code cleanup --- frontend/src/App.vue | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 342728c..793c7a5 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -240,11 +240,6 @@ class App extends Vue { label: "Register", icon: "mdi-link-plus", }, - // { - // attrs: { href: "https://dsp.criticalzone.org/" }, - // label: "Contribute Data", - // icon: "mdi-book-plus", - // }, ]; User.fetchSchemas(); From 2b78ce2acfe4c88ef280bf14297991f7ff97b386 Mon Sep 17 00:00:00 2001 From: pkdash Date: Thu, 16 May 2024 14:55:21 -0400 Subject: [PATCH 16/20] [#154] adding new env variable to workflow files --- .github/workflows/ci.yaml | 1 + .github/workflows/deploy-dev.yaml | 1 + .github/workflows/deploy.yaml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8f7bc67..a78d077 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,7 @@ env: VITE_APP_GOOGLE_MAPS_API_KEY: "" VITE_APP_SUPPORT_EMAIL: help@example.com VITE_APP_CLIENT_ID: APP-4ZA8C8BYAH3QHNE9 + SEARCH_RELEVANCE_SCORE_THRESHOLD: 1.4 jobs: diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index 17050eb..c764159 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -21,6 +21,7 @@ env: VITE_APP_LOGIN_URL: https://orcid.org/oauth/authorize VITE_APP_SUPPORT_EMAIL: help@example.com VITE_APP_CLIENT_ID: APP-4ZA8C8BYAH3QHNE9 + SEARCH_RELEVANCE_SCORE_THRESHOLD: 1.4 jobs: diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 3c0e944..063bf22 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -23,6 +23,7 @@ env: VITE_APP_GOOGLE_MAPS_API_KEY: "" VITE_APP_SUPPORT_EMAIL: help@example.com VITE_APP_CLIENT_ID: APP-4ZA8C8BYAH3QHNE9 + SEARCH_RELEVANCE_SCORE_THRESHOLD: 1.4 jobs: From cd7853cc1c7bfa47b2720e4b16da974ce9e0add4 Mon Sep 17 00:00:00 2001 From: Maurier Date: Thu, 16 May 2024 16:19:10 -0600 Subject: [PATCH 17/20] enable status and last updated timeago --- frontend/package-lock.json | 39 +++++++++++++--- frontend/package.json | 3 +- .../src/components/dataset/cd.dataset.vue | 45 +++++++++++++------ frontend/src/modules/timeago.ts | 6 +++ frontend/src/modules/vuex.ts | 2 - frontend/src/types.ts | 7 +++ 6 files changed, 79 insertions(+), 23 deletions(-) create mode 100644 frontend/src/modules/timeago.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7ab3c51..7adfc8c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { - "name": "cznet-discovery", - "version": "2.0.0", + "name": "i-guide-catalog", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "cznet-discovery", - "version": "2.0.0", + "name": "i-guide-catalog", + "version": "1.1.1", "dependencies": { "@cznethub/cznet-vue-core": "^0.2.6", "@unhead/vue": "^1.9.9", @@ -30,6 +30,7 @@ "vue-facing-decorator": "^3.0.4", "vue-i18n": "^9.13.1", "vue-router": "^4.3.2", + "vue-timeago3": "^2.3.2", "vuetify": "^3.6.3", "vuex": "^4.1.0", "vuex-persistedstate": "^4.1.0" @@ -2001,7 +2002,6 @@ "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -6297,6 +6297,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dayjs": { "version": "1.11.11", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", @@ -11158,8 +11173,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -13668,6 +13682,17 @@ "he": "^1.2.0" } }, + "node_modules/vue-timeago3": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/vue-timeago3/-/vue-timeago3-2.3.2.tgz", + "integrity": "sha512-yxb++H9ekLS8bSt3in7fMwIfW1ex9ceMYVCcfLiOppBYpp8hatlOTLqQyCmLXeDYB098uwgfnCewEPKVh/gi6A==", + "dependencies": { + "date-fns": "^2.28.0" + }, + "peerDependencies": { + "vue": "^3.3.6" + } + }, "node_modules/vue-tsc": { "version": "2.0.16", "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.16.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9c6eea4..15b07ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "vue-facing-decorator": "^3.0.4", "vue-i18n": "^9.13.1", "vue-router": "^4.3.2", + "vue-timeago3": "^2.3.2", "vuetify": "^3.6.3", "vuex": "^4.1.0", "vuex-persistedstate": "^4.1.0" @@ -74,4 +75,4 @@ "lint-staged": { "*": "eslint --fix" } -} \ No newline at end of file +} diff --git a/frontend/src/components/dataset/cd.dataset.vue b/frontend/src/components/dataset/cd.dataset.vue index 5bf9480..8f11987 100644 --- a/frontend/src/components/dataset/cd.dataset.vue +++ b/frontend/src/components/dataset/cd.dataset.vue @@ -198,21 +198,24 @@ class="page-content" :class="{ 'is-sm': $vuetify.display.mdAndDown }" > -

{{ data.name }}

- - + + --> + { + app.use(timeago); +}; diff --git a/frontend/src/modules/vuex.ts b/frontend/src/modules/vuex.ts index 30fb07e..17b3185 100644 --- a/frontend/src/modules/vuex.ts +++ b/frontend/src/modules/vuex.ts @@ -5,8 +5,6 @@ import { orm } from "@/models/orm"; import { persistedPaths } from "@/models/persistedPaths"; import type { UserModule } from "@/types"; -// Setup Pinia -// https://pinia.vuejs.org/ export const install: UserModule = ({ app }) => { // Create Vuex Store and register database through Vuex ORM. diff --git a/frontend/src/types.ts b/frontend/src/types.ts index b298e84..aee3dd9 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -73,3 +73,10 @@ export interface ISearchParams { export interface ITypeaheadParams { term: string; } + +export enum EnumCreativeWorkStatus { + Draft = "Draft", + Incomplete = "Incomplete", + Obsolete = "Obsolete", + Published = "Published", +} From 74c006a3819ab6d0b16d55b86148abaad9a21a7d Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 22 May 2024 11:44:27 -0600 Subject: [PATCH 18/20] load readme file over https and add loading state --- .../src/components/dataset/cd.dataset.vue | 26 +++++++++++++++---- frontend/src/modules/vuex.ts | 3 ++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/dataset/cd.dataset.vue b/frontend/src/components/dataset/cd.dataset.vue index 8f11987..4fc9819 100644 --- a/frontend/src/components/dataset/cd.dataset.vue +++ b/frontend/src/components/dataset/cd.dataset.vue @@ -533,7 +533,7 @@ /> README -
+
+ +
+
@@ -856,7 +863,11 @@ const loader: Loader = new Loader( options, ); -const md = markdownit(); +const md = markdownit({ + linkify: true, + typographer: true, + breaks: true, +}); @Component({ name: "cd-dataset", @@ -875,6 +886,7 @@ class CdDataset extends Vue { readmeMd = ""; showCoordinateSystem = false; showExtent = false; + isLoadingMD = false; rootDirectory = { name: "root", @@ -1027,7 +1039,7 @@ class CdDataset extends Vue { loadFileExporer() { // Load file explorer if (this.data.associatedMedia?.length) { - this.data.associatedMedia.map((m: any, index: number) => { + this.data.associatedMedia.map((m: any, _index: number) => { let fileSizeBytes; if (typeof m.contentSize === "string") { @@ -1104,12 +1116,16 @@ class CdDataset extends Vue { ); if (readmeFile?.contentUrl) { + const url = readmeFile.contentUrl.replace("http:", "https:"); try { - const response = await fetch(readmeFile.contentUrl); + this.isLoadingMD = true; + const response = await fetch(url); const rawMd = await response.text(); this.readmeMd = md.render(rawMd); } catch (e) { console.log(e); + } finally { + this.isLoadingMD = false; } } } diff --git a/frontend/src/modules/vuex.ts b/frontend/src/modules/vuex.ts index 17b3185..f931f15 100644 --- a/frontend/src/modules/vuex.ts +++ b/frontend/src/modules/vuex.ts @@ -4,6 +4,7 @@ import createPersistedState from "vuex-persistedstate"; import { orm } from "@/models/orm"; import { persistedPaths } from "@/models/persistedPaths"; import type { UserModule } from "@/types"; +import { APP_NAME } from "@/constants"; export const install: UserModule = ({ app }) => { // Create Vuex Store and register database through Vuex ORM. @@ -13,7 +14,7 @@ export const install: UserModule = ({ app }) => { VuexORM.install(orm), createPersistedState({ paths: persistedPaths, - key: `CZ Hub`, + key: APP_NAME || 'iguide-catalog', }), ], // state() { From c480668ca8288bd3f79af4d4f0f2aabefee1a7b8 Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 22 May 2024 12:05:17 -0600 Subject: [PATCH 19/20] add markdown css --- frontend/package-lock.json | 12 ++++++++++++ frontend/package.json | 1 + frontend/src/assets/css/theme.scss | 1 + frontend/src/components/dataset/cd.dataset.vue | 15 ++++++++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7adfc8c..7ac5560 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,7 @@ "@vuex-orm/core": "^0.36.4", "deepmerge": "^4.3.1", "dompurify": "^3.1.2", + "github-markdown-css": "^5.5.1", "google-maps": "^4.3.3", "lodash.isequal": "^4.5.0", "markdown-it": "^14.1.0", @@ -8225,6 +8226,17 @@ "assert-plus": "^1.0.0" } }, + "node_modules/github-markdown-css": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.5.1.tgz", + "integrity": "sha512-2osyhNgFt7DEHnGHbgIifWawAqlc68gjJiGwO1xNw/S48jivj8kVaocsVkyJqUi3fm7fdYIDi4C6yOtcqR/aEQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "10.3.12", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", diff --git a/frontend/package.json b/frontend/package.json index 15b07ac..a1993a9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@vuex-orm/core": "^0.36.4", "deepmerge": "^4.3.1", "dompurify": "^3.1.2", + "github-markdown-css": "^5.5.1", "google-maps": "^4.3.3", "lodash.isequal": "^4.5.0", "markdown-it": "^14.1.0", diff --git a/frontend/src/assets/css/theme.scss b/frontend/src/assets/css/theme.scss index 0bbd4ca..6243dff 100644 --- a/frontend/src/assets/css/theme.scss +++ b/frontend/src/assets/css/theme.scss @@ -3,6 +3,7 @@ // } @import '@fortawesome/fontawesome-free/css/all.min.css'; @import '@cznethub/cznet-vue-core/styles'; +@import 'github-markdown-css/github-markdown-light.css'; html { diff --git a/frontend/src/components/dataset/cd.dataset.vue b/frontend/src/components/dataset/cd.dataset.vue index 4fc9819..8444c02 100644 --- a/frontend/src/components/dataset/cd.dataset.vue +++ b/frontend/src/components/dataset/cd.dataset.vue @@ -548,7 +548,7 @@ color="primary" /> -
+
@@ -1235,4 +1235,17 @@ export default toNative(CdDataset); :deep(#fileExplorer .v-sheet) { background-color: #f6f6f6 !important; } + +.markdown-body { + box-sizing: border-box; + min-width: 200px; + max-width: 980px; + padding: 45px; +} + +@media (max-width: 767px) { + .markdown-body { + padding: 15px; + } +} From e3641b1e3184c32418abea06c009597f793fd847 Mon Sep 17 00:00:00 2001 From: Maurier Date: Wed, 22 May 2024 12:16:26 -0600 Subject: [PATCH 20/20] use current font in markdown body --- .../src/components/dataset/cd.dataset.vue | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/dataset/cd.dataset.vue b/frontend/src/components/dataset/cd.dataset.vue index 8444c02..f0f852f 100644 --- a/frontend/src/components/dataset/cd.dataset.vue +++ b/frontend/src/components/dataset/cd.dataset.vue @@ -1189,6 +1189,20 @@ export default toNative(CdDataset); overflow: auto; resize: vertical; } + + .markdown-body { + box-sizing: border-box; + min-width: 200px; + max-width: 980px; + padding: 45px; + font-family: inherit; + } + + @media (max-width: 767px) { + .markdown-body { + padding: 15px; + } + } } .page-content { @@ -1235,17 +1249,4 @@ export default toNative(CdDataset); :deep(#fileExplorer .v-sheet) { background-color: #f6f6f6 !important; } - -.markdown-body { - box-sizing: border-box; - min-width: 200px; - max-width: 980px; - padding: 45px; -} - -@media (max-width: 767px) { - .markdown-body { - padding: 15px; - } -}