diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dc1bae75e..3a7c90ac7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,6 +6,7 @@ on: # Trigger on push to main or tags matching v* push: branches: + - "dev" - "main" - "production" - "staging" @@ -54,6 +55,10 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | echo "RELEASE_VERSION=latest" >> $GITHUB_ENV + - name: Label dev + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + run: | + echo "RELEASE_VERSION=dev" >> $GITHUB_ENV - name: Label production if: github.event_name == 'push' && github.ref == 'refs/heads/production' run: | @@ -109,16 +114,7 @@ jobs: # key: local-docker-directory - name: Build AMD Image for production or main - if: env.RELEASE_VERSION == 'production' || env.RELEASE_VERSION == 'latest' - env: - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - run: | - docker build . --file Dockerfile --platform=linux/amd64 --tag $DOCKERHUB_REGISTRY/$DOCKERHUB_USERNAME/$IMAGE_NAME:$RELEASE_VERSION --tag $GITHUB_REGISTRY/$DOCKERHUB_USERNAME/$IMAGE_NAME:$RELEASE_VERSION --tag $ECR_REGISTRY/kcworks:$RELEASE_VERSION - docker image ls - - - name: Build AMD Image for staging - if: env.RELEASE_VERSION == 'staging' + if: env.RELEASE_VERSION == 'production' || env.RELEASE_VERSION == 'latest' || env.RELEASE_VERSION == 'dev' || env.RELEASE_VERSION == 'staging' env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} @@ -133,7 +129,7 @@ jobs: docker push $DOCKERHUB_REGISTRY/$DOCKERHUB_USERNAME/$IMAGE_NAME --all-tags - name: Push production or staging image to Amazon ECR - if: env.RELEASE_VERSION == 'production' || env.RELEASE_VERSION == 'staging' + if: env.RELEASE_VERSION == 'production' || env.RELEASE_VERSION == 'staging' || env.RELEASE_VERSION == 'dev' env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} RELEASE_VERSION: ${{ env.RELEASE_VERSION }} @@ -146,10 +142,6 @@ jobs: touch .env docker compose --file docker-compose.yml --file docker-compose.dev.yml up -d - # - name: Run unit tests - # run: | - # docker exec -it kcworks-ui bash -c "cd /opt/invenio/src/site && PIPENV_DOTENV_LOCATION=/Users/ianscott/Development/knowledge-commons-works/site/tests/.env pipenv run python -m pytest" - - name: Destroy containers if: always() run: | diff --git a/docs/build/.buildinfo b/docs/build/.buildinfo index f97579b82..d71fe766e 100644 --- a/docs/build/.buildinfo +++ b/docs/build/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: c98d14472b07a7a52cb283ce05317c0a +config: c534e3803c9cb7a80e2fd2d8105eeda9 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/.doctrees/README.doctree b/docs/build/.doctrees/README.doctree index 4ceb6542b..639fc2fbc 100644 Binary files a/docs/build/.doctrees/README.doctree and b/docs/build/.doctrees/README.doctree differ diff --git a/docs/build/.doctrees/api.doctree b/docs/build/.doctrees/api.doctree new file mode 100644 index 000000000..5372a3713 Binary files /dev/null and b/docs/build/.doctrees/api.doctree differ diff --git a/docs/build/.doctrees/architecture.doctree b/docs/build/.doctrees/architecture.doctree new file mode 100644 index 000000000..9fb9ead6f Binary files /dev/null and b/docs/build/.doctrees/architecture.doctree differ diff --git a/docs/build/.doctrees/changelog.doctree b/docs/build/.doctrees/changelog.doctree new file mode 100644 index 000000000..bb669879c Binary files /dev/null and b/docs/build/.doctrees/changelog.doctree differ diff --git a/docs/build/.doctrees/environment.pickle b/docs/build/.doctrees/environment.pickle index c5cb32e46..1e7308bfa 100644 Binary files a/docs/build/.doctrees/environment.pickle and b/docs/build/.doctrees/environment.pickle differ diff --git a/docs/build/.doctrees/index.doctree b/docs/build/.doctrees/index.doctree index 927abe393..df239795c 100644 Binary files a/docs/build/.doctrees/index.doctree and b/docs/build/.doctrees/index.doctree differ diff --git a/docs/build/.doctrees/metadata.doctree b/docs/build/.doctrees/metadata.doctree index 03f0c6793..0edefb9fb 100644 Binary files a/docs/build/.doctrees/metadata.doctree and b/docs/build/.doctrees/metadata.doctree differ diff --git a/docs/build/README.html b/docs/build/README.html index d2194b852..7df9e083e 100644 --- a/docs/build/README.html +++ b/docs/build/README.html @@ -3,10 +3,10 @@ - + - About - Knowledge Commons Works 0.3.3 documentation + About - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
-
Knowledge Commons Works 0.3.3 documentation
+
Knowledge Commons Works 0.3.5 documentation
@@ -190,7 +190,7 @@ diff --git a/docs/build/_sources/README.md.txt b/docs/build/_sources/README.md.txt index 54f1d04a1..c712c0776 100644 --- a/docs/build/_sources/README.md.txt +++ b/docs/build/_sources/README.md.txt @@ -2,7 +2,7 @@ Knowledge Commons Works is a collaborative tool for storing and sharing academic research. It is part of Knowledge Commons and is built on an instance of the InvenioRDM repository system. -Version 0.3.3-beta6 +Version 0.3.5-beta8 ## Copyright diff --git a/docs/build/_sources/api.md.txt b/docs/build/_sources/api.md.txt new file mode 100644 index 000000000..adac6b9b6 --- /dev/null +++ b/docs/build/_sources/api.md.txt @@ -0,0 +1,1228 @@ +# API + +KCWorks provides a robust REST API that allows clients to perform most operations on KCWorks records and collections. + +## The InvenioRDM REST API + +KCWorks is built on top of InvenioRDM, which provides a REST API for creating, managing, and querying records. This API is documented at https://inveniordm.docs.cern.ch/reference/rest_api_index/. + +> **Note:** "Collections" are referred to as "communities" in the InvenioRDM API and its documentation. To avoid confusion with the social groups that are part of the Knowledge Commons network, KCWorks uses the term "collections" in its documentation and user interface. But operations involving collections are handled via the "communities" endpoint in the InvenioRDM REST API. + +This REST API allows clients to retrieve and manage the following resources: + +| Resource | Supported Operations | Requires Authentication | Endpoint | InvenioRDM API documentation | +| -------- | ----------- | ------------- | -------- | ---------------------------- | +| draft works | read | yes | GET /api/records/{id}/draft | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-draft-record | +| | create | yes | POST /api/records/ | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#create-a-draft-record | +| | update | yes | PUT /api/records/{id} | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#update-a-draft-record | +| | publish | yes | POST /api/records/{id}/draft/actions/publish | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#publish-a-draft-record | +| | delete | yes | DELETE /api/records/{id}/draft | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#delete-a-record | +| | list files | yes | GET /api/records/{id}/draft/files | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#list-a-drafts-files | +| | upload files[^draft-file-upload] | yes | POST /api/records/{id}/draft/files | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#start-draft-file-uploads | +| | view file metadata | yes | GET /api/records/{id}/draft/files/{filename} | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-draft-files-metadata | +| | download file | yes | GET /api/records/{id}/draft/files/{filename}/content | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#download-a-draft-file | +| | delete file | yes | DELETE /api/records/{id}/draft/files/{filename} | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#delete-a-draft-file | +| published works | read[^published-work-read] | no | GET /api/records/ | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-record | +| | read all versions | no | GET /api/records/{id}/versions | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-all-versions | +| | read latest version | no | GET /api/records/{id}/versions/latest | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-latest-version | +| | search | no | GET /api/records/ | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#search-records | +| | update[^published-work-update] | yes | POST /api/records/{id}/draft | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#update-a-draft-record | +| | create new version | yes | POST /api/records/{id}/versions | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#create-a-new-version | +| | attach files from a previous version[^attach-files-from-previous-version] | yes | POST /api/records/{id}/draft/actions/files-import | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#link-files-from-previous-version | +| | list files | no | GET /api/records/{id}/files | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-all-files | +| collections | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_communities/ | +| collection memberships | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_members/ | +| reviews | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_reviews/ | +| requests | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_requests/ | +| users | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_users/ | +| groups[^groups] | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_groups/ | +| vocabularies | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_vocabularies/ | +| names | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_names/ | +| funders | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_funders/ | +| awards | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_awards/ | +| OAI-PMH sets | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_oaipmh_sets/ | +| statistics | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_statistics/ | + + +[^published-work-read]: Note that each version of a published work has its own `id`. The `read` operation retrieves the specific version whose `id` is provided. +[^published-work-update]: Published works cannot be updated directly. Rather, the update API call creates a new draft of the published work. The original version of the published work is not yet modified and remains discoverable via the search API. This draft may then be published, in which case it replaces the original published work. Note that this kind of update can only be made to the metadata of the published work, not to the files associated with it. In order to update the files associated with a published work, the client must create a new version of the work. +[^draft-file-upload]: The file upload process involves three separate API calls: + - `POST /api/records/{id}/draft/files` to start the upload process + - `POST /api/records/{id}/draft/files/{filename}/content` to upload the file's content + - `POST /api/records/{id}/draft/files/{filename}/commit` to finalize the upload and attach the file to the draft record +[^attach-files-from-previous-version]: The `files-import` operation attaches files from a previous version of a work to *the current draft* of that work. If the work already has multiple versions, clients must specify the `id` of the version whose files are to be attached. +[^groups]: Note that the `groups` managed by the `groups` API endpoint are not the same as KCWorks social groups or InvenioRDM collections. These are instead sets of users sharing a set of system permissions. (Under the hood these are wrappers around the invenio-accounts `Role` class.) This might include the "administration" group who are assigned certain admin permissions. It might also include a group who are all assigned "curator" permissions for a collection, etc. + +Note that for some operations where authentication is required, the client must also possess the appropriate permissions. (E.g., to edit a draft work, manage collection requests, etc.) + +Note that several operations are NOT possible via the REST API, including: + +- searching draft records + - Draft records are by definition not intended to be distributed, and so are not discoverable via the search API until they have been published. +- creating a published work directly + - Published works can only be created by first creating a draft work and then publishing it. +- deleting a published work + - Published works are generally considered to be permanent and so cannot be deleted. If desired, access to a published work, or a version of a published work, can be set to "restricted", in which case the work is no longer discoverable via the search API. +- modifying files for a published work + - Again, published works are generally considered to be permanent and so their files cannot be modified. The only way to update the files associated with a published work is to create a new version of the work. If desired, access to the original version of the published work can be set to "restricted", in which case only the new version and its files are discoverable. +- creating or modifying user accounts + - The REST API endpoint for users is currently read-only. It is not possible to create or modify user accounts, or change and user profile information, via the REST API. These operations are handled via the KCWorks admin interface or via CLI commands. +- creating or modifying groups (permissions) +- creating or modifying controlled vocabularies + +### Creating a new Work via the InvenioRDM REST API + +Creating a new Work via the REST API requires several steps. + +- Step 1: Create a draft record + +- Step 2: Initialize the file upload + +- Step 3: Upload the file content + - This step must be repeated for each file being added to the work. + +- Step 4: Commit the file upload + +- Step 5: Publish the draft record + +If you want the work to be included in a collection at publication time, you must submit a request for the work to be published in the collection. The first four steps are the same as above, but in place of Step 5 (publication), you must submit a request for the work to be published in the collection. + +- Step 5: Create a review request + +- Step 6: Submit review request + +If the collection in question requires review before publication, the request will not be published until the review is accepted. + +- Step 7: Accept and publish the record + +## Streamlined Import API + +In order to streamline the process of uploading works to KCWorks, particularly for works intended for publication in a collection, KCWorks provides a streamlined import API. This API allows clients to upload a work and its files in a single step, without the need to create a draft record, initialize file uploads, commit file uploads, or submit a review request. + +Why is this API needed? The InvenioRDM REST API can be fragile and difficult to use, particularly for clients who are not familiar with the system. The creation and acceptance of a review request is redundant where collection administrators are uploading works for a collection they administer. The file upload steps are also not truly stateless, introducing the possibility of a file upload being interrupted and left incomplete, even if the upload of the file's content was successful. + +### Who can use the import API? + +The import API is available to any registered active user who has obtained an OAuth token for API operations. + +If the import is to include placing the work directly in a collection, without passing through the review process, the user must have sufficient permissions to publish directly in the collection. Exactly what role they must have in the collection ("owner", "manager", "curator", "reader") depends on the collection's review policy. + +The exception to this rule is for collection owners, who may override the collection's review policy and import works directly into the collection without review. + +### The import request + +#### Request +``` +POST https://works.hcommons.org/api/import/ HTTP/1.1 +``` + +#### Required headers +``` +Content-Type: multipart/form-data +Accept: application/json +Authorization: Bearer \ +``` + +#### Request body + +This request must be made with a multipart/form-data request. The request body must include parts with following names: + +| Name | Required | Content Type | Description | +|-------|----------|--------------|-------------| +| `files` | yes | `application/octet-stream` | The (binary) file content to be uploaded. If multiple files are being uploaded, a body part with this same name ("files") must be provided for each file. If more than three or four files are being uploaded, it is recommended to provide a single zip archive containing all of the files. The files will be assigned to the appropriate work based on filename, so where multiple files are provided these **must be unique**. If a zip archive is provided, the files must be contained in a single compressed folder with no subfolders. | +| `metadata` | yes | `application/json` | An array of JSON metadata objects, each of which will be used to create a new work. Each must following the KCWorks implementation of the InvenioRDM metadata schema described {ref}`here `. In addition, an array of owners for the work may optionally be provided by adding an `access.owned_by` property to each metadata object. | +| `collection` | no | `text/plain` | The ID (either the url slug or the UUID) of the collection to which the work should be published. If this value is provided, the work will be submitted to the collection immediately after import. If the collection requires review, the work will be placed in the collection's review queue. | +| `review_required` | no | `text/plain` | A string representation of a boolean (either "true" or "false") indicating whether the work should be reviewed before publication. This setting is only relevant if the work is intended for publication in a collection that requires review. It will override the collection's usual review policy, since the work is being uploaded by a collection administrator. (Default: "true") | +| `strict_validation` | no | `text/plain` | A string representation of a boolean (either "true" or "false") indicating whether the import request should be rejected if any validation errors are encountered. If this value is "false", the imported work will be created in KCWorks even if some of the provided metadata does not conform to the KCWorks metadata schema, provided these are not required fields. If this value is "true", the import request will be rejected if any validation errors are encountered. (Default: "true") | +| `all_or_none` | no | `text/plain` | A string representation of a boolean (either "true" or "false") indicating whether the entire import request should be rejected if any of the works fail to be created (whether for validation errors, upload errors, or other reasons). If this value is "false", the import request will be accepted even if some of the works cannot be created. The response in this case will include a list of works that were successfully created and a list of errors for the works that failed to be created. (Default: "true") | + +The array of owners, if provided in a metadata object's `access.owned_by` property, must include at least the full name and email address of the users to be added as owners of the work. If the user already has a Knowledge Commons account, their username should also be provided. Additional identifiers (e.g., ORCID) may be provided as well to help avoid duplicate accounts, since a KCWorks account will be created for each user if they do not already have one. + +| key | required | type | description | +|-----|----------|------|-------------| +| `full_name` | yes | `string` | The full name of the user. | +| `email` | yes | `string` | The email address of the user. | +| `identifiers` | no | `array` | An array of identifiers for the user. Any identifier schemes supported by KCWorks will be accepted. | + +The resulting `owners` list should be shaped like this: + +```json +[ + { + "full_name": "John Doe", + "email": "john.doe@example.com", + "identifiers": [ + { + "identifier": "0000-0000-0000-0000", + "scheme": "orcid" + }, + { + "identifier": "jdoe", + "scheme": "kc_username" + } + ] + } +] +``` +Note that it is *not* assumed that the creators of a work should be the work's owners. The creators will only be added as owners if each of them is listed in the `access.owned_by` property of the work's metadata object. + +### A successful import response + +``` +HTTP/1.1 201 Created +Content-Type: application/json +``` + +This response will include a JSON object with the following fields: + +- `status`: The status of the import request, which will be "success" if the import request was successful. +- `data`: An array of JSON objects with the following fields: + +| key | type | description | +|-----|------|-------------| +| `record_id` | `string` | The ID of the new work. | +| `record_url` | `string` | The URL of the new work. | +| `files` | `array` | A list of the filenames for the files that were successfully uploaded. This is for convenience. Details about the files, including their size and checksum, are available in the `files` property of the `metadata` object. | +| `collection_id` | `string` | The ID of the collection to which the work was published, if any. This is provided for convenience. Details about the collection are available in the `parent.communities` property of the `metadata` object. | +| `errors` | `array` | A list of errors that occurred during the import process. These might include validation errors for certain fields in the provided metadata that did not prevent creation of the work. (Only provided if the request was made with `strict_validation` set to "false".) | +| `metadata` | `object` | The metadata for the created work, in JSON format, following the KCWorks implementation of the InvenioRDM metadata schema described {ref}`here `. | + +The response object will be shaped like this: + +```json +{ + "status": "success", + "data": [ + { + "item_index": 0, + "record_id": "1234567890", + "record_url": "https://works.hcommons.org/records/1234567890", + "files": ["file1.pdf", "file2.pdf"], + "collection_id": "1234567890", + "errors": [], + "metadata": { + /* ... */ + } + }, + { + "item_index": 1, + "record_id": "1234567891", + "record_url": "https://works.hcommons.org/records/1234567891", + "files": ["file1.pdf", "file2.pdf"], + "collection_id": "1234567890", + "errors": [], + "metadata": { + /* ... */ + } + } + ] +} +``` + +### An unsuccessful import response + +#### The token does not have the necessary permissions + +``` +HTTP/1.1 403 Forbidden +Content-Type: application/json +``` + +This response will include a JSON object: + +```json +{ + "status": "error", + "message": "The user does not have the necessary permissions." +} +``` + +#### The request metadata is malformed or invalid + +```http +HTTP/1.1 400 Bad Request +Content-Type: application/json +``` + +This response is returned when some of the provided metadata for all of the works to be imported is malformed or invalid. This indicates that *none of the works has been created* and a new request must be made with corrected metadata. This response will only be received if either +a. the `strict_validation` request parameter was set to "true" and all of the supplied metadata objects raise validation errors, or +b. the `strict_validation` parameter is set to "false", but the validation errors affected fields that are required for the works to be created. +c. the `all_or_none` request parameter is set to "true" and some of the supplied metadata objects raise validation errors. + +The response will include a JSON object with the following fields: + +```json +{ + "status": "error", + "message": "The request metadata is malformed or invalid.", + "errors": [ + { + "item_index": 0, + "errors": [ + { + "field": "title", + "message": "Required field missing." + } + ] + }, + { + "item_index": 1, + "errors": [ + { + "field": "metadata.creators.0.occupation", + "message": "Unknown field." + }, + { + "field": "metadata.publication_date", + "message": "Date is not in Extended Date Time Format (EDTF)." + } + ] + } + ] +} +``` + +If only some of the works to be imported are malformed or invalid, and the `all_or_none` request parameter is set to "false", the response will be `207 Multi-Status`. + +#### The request file upload failed + +```http +HTTP/1.1 400 Bad Request +Content-Type: application/json +``` + +If the file content is uploaded but for some reason is considered corrupted or invalid, a `400 Bad Request` response will be returned. This response will include a JSON object with the following fields: + +```json +{ + "status": "error", + "message": "The file content is corrupted or invalid.", + "errors": [ + { + "item_index": 0, + "errors": [ + { + "file": "file1.pdf", + "message": "The file size does not match the supplied metadata." + }, + { + "file": "file2.pdf", + "message": "The file checksum does not match the supplied metadata." + } + ] + }, + { + "item_index": 1, + "errors": [ + { + "file": "file3.pdf", + "message": "The file exceeds the maximum file size." + } + ] + } + ] +} +``` + +If an upload simply fails to complete and times out, the client will instead receive a `504 Gateway Timeout` response. + +#### Only some of the works to be imported failed + +```http +HTTP/1.1 207 Multi-Status +Content-Type: application/json +``` + +If the `all_or_none` request parameter is set to "false", it is possible that some of the works to be imported were successfully created and others were not. In this case, the response will be `207 Multi-Status` and will include a JSON object with the following fields: + +```json +{ + "status": "multi_status", + "data": { + "succeeded": [ + { + "item_index": 0, + "record_id": "1234567890", + "record_url": "https://works.hcommons.org/records/1234567890", + "files": ["file1.pdf", "file2.pdf"], + "collection_id": "1234567890", + "errors": [], + "metadata": { + /* ... */ + } + }, + ], + "failed": [ + { + "item_index": 1, + "message": "The request metadata is malformed or invalid.", + "errors": [ + { + "field": "title", + "message": "Required field missing.", + } + ] + }, + { + "item_index": 2, + "message": "The file content is corrupted or invalid.", + "errors": [ + { + "file": "file3.pdf", + "message": "The file exceeds the maximum file size." + } + ] + } + ] + } +} +``` + +### What happens to an import request that fails? + +If all steps of an import request do not complete successfully, the work will not be created. The files that were successfully uploaded will be deleted, and any draft record created as part of the import request will be deleted. The client may attempt the import request again. + +### Making duplicate import requests + +Note that it is possible to make duplicate import requests *unless* the work to be imported includes a pre-existing DOI identifier or some other unique identifier that has already been registered in KCWorks. In this case, the import request will be rejected with a `409 Conflict` response code and a `Location` header pointing to the existing work. + +In the absence of such a unique identifier, however, KCWorks will not try to detect duplicate works based on the metadata, file name, or file content. If the same work is imported multiple times without a pre-existing unique identifier, it will be created multiple times in KCWorks and each version will be assigned a newly minted DOI. + + +## Group Collections API + +``` +https://works.hcommons.org/api/group_collections +``` + +The `group_collections` REST API endpoint allows a client to create, read, modify, or delete a collection in KCWorks owned and administered by a Knowledge Commons group. GET requests to retrieve information about group collections are open to all clients. POST, PUT, and DELETE requests are secured by an oauth token that must be obtained from the Knowledge Commons Works administrator. + +This endpoint is not configured to receive all of the metadata required to create or modify group collections. Rather, the `group_collections` endpoint receives minimal signals from a Commons Instance and then obtains the full required metadata via an API callback to the Commons instance. + +> [!NOTE] +> KCWorks uses the term "collection" in place of the default term "community" employed in other InvenioRDM installations. This is partly to accommodate exactly the integration with Knowledge Commons groups that is discussed here. + + +### Group collection owner + +InvenioRDM does not allow groups to be owners of a collection (community). When a collection is created for a group, though, we do not know which of the group's administrators to assign as the individual owner. It is also awkward to change ownership of a collection later on if the group's administrativer personnel change. So the collection is owned by an administrative user who is assigned the role `group-collections-owner`. The group's administrators are then assigned privileges as "managers" of the group collection. This allows them to manage the collection's settings and membership, but not to delete the collection or change its ownership. + +Before the invenio_group_collections_kcworks module can be used, the administrator must create a role called `group-collections-owner` and assign membership in that role to one administrative user account. If multiple user accounts belong to that role, the first user account in the list will be assigned as the owner of group collections. If no user accounts belong to the role, the group collection creation will fail with a NoOwnerAvailable error. + +### Endpoint configuration + +The configuration variable `GROUP_COLLECTIONS_METADATA_ENDPOINTS` must be provided in the `invenio.cfg` file in order to use this endpoint. This variable should hold a dictionary whose keys are Commons instance names. The value for each key is a dictionary containing the following keys: + +| key | value type | required | value | +| --- | ---------- | ----- | ----- | +| `url` | str | Y | The url on the Commons instance where a GET request can retrieve the metadata for a group. The url should include the placeholder `{id}` where the Commons instance id for the requested group should be placed. | +| `token_name` | str (upper case) | Y | The name of the environment variable that will hold the authentication token for requests to the Commons instance url for retrieving group metadata. | +| `placeholder_avatar` | str | N | The filename or last url component that identifies a placeholder avatar in the avatar image url supplied for the Commons group avatar. | + +A typical configuration might look like the following: + +```python +GROUP_COLLECTIONS_METADATA_ENDPOINTS = { + "knowledgeCommons": { + "url": "https://hcommons-dev.org/wp-json/commons/v1/groups/{id}", + "token_name": "COMMONS_API_TOKEN", + "placeholder_avatar": "mystery-group.png", + }, +} +``` + +### Retrieving Group Collection Metadata (GET) + +A GET request to this endpoint will retrieve metadata on Invenio collections +that are owned by a Commons group. A request to the bare endpoint without a +group ID or collection slug will return a list of all collections owned by +all Commons groups. (Commons Works collections not linked to a Commons group will not be included. If you wish to query all groups, please use the `communities` API endpoint.) + +#### Query parameters + +Four optional query parameters can be used to filter the results: + +| Parameter name | Description | +| ---------------|------------ | +| `commons_instance` | the name of the Commons instance to which the group belongs. If this parameter is provided, the response will only include collections owned by groups in that instance. | +| `commons_group_id` | the ID of the Commons group. If this parameter is provided, the response will only include collections owned by that group. | +| `collection` | the slug of the collection. If this parameter is provided, the response will include only metadata for that collection. | +| `page` | the page number of the results | +| `size` | the number of results to include on each page | +| `sort` | the kind of sorting applied to the returned results | + +#### Sorting + +The `sort` parameter can be set to one of the following sort types: + +| Field name | Description | +| -----------|-------------| +| newest | Descending order based on `created` date | +| oldest | Ascending order based on `created` date | +| updated-desc | Descending order based on `updated` date | +| updated-asc | Ascending order based on `updated` date | + +By default the results are sorted by `updated-desc` + +#### Pagination + +Long result sets will be paginated. The response will include urls for the `first`, `last`, `previous`, and `next` pages of results in the `link` property of the response body. A url for the current page of results will also be included in the list as a `self` link. By default the page size is 25, but this can be changed by providing a value for the `size` query parameter. + +#### Requesting all collections + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections HTTP/1.1 +``` + +##### Successful Response Status Code + +`200 OK` + +##### Successful response body + +```json +{ + "aggregations": { + "type": { + "buckets": [ + { + "doc_count": 50, + "is_selected": false, + "key": "event", + "label": "Event", + }, + { + "doc_count": 50, + "is_selected": false, + "key": "organization", + "label": "Organization", + }, + ], + "label": "Type", + }, + "visibility": { + "buckets": [ + { + "doc_count": 100, + "is_selected": false, + "key": "public", + "label": "Public", + } + ], + "label": "Visibility", + }, + }, + "hits": { + "hits": [ + { + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "access": { + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + }, + "children": {"allow": false}, + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 100, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for panda research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } + }, + /* ... */ + ], + "total": 100, + }, + "links": { + "self": "https://works.hcommons.org/api/group_collections", + "first": "https://works.hcommons.org/api/group_collections?page=1", + "last": "https://works.hcommons.org/api/group_collections?page=10", + "prev": "https://works.hcommons.org/api/group_collections?page=1", + "next": "https://works.hcommons.org/api/group_collections?page=2", + } + "sortBy": "newest", + "order": "ascending", +} +``` + +##### Successful Response Headers + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | + +#### Requesting collections for a Commons instance + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&sort=updated-asc HTTP/1.1 +``` + +##### Successful response status code + +`200 OK` + +##### Successful Response Body: + +```json +{ + "aggregations": { + "type": { + "buckets": [ + { + "doc_count": 45, + "is_selected": false, + "key": "event", + "label": "Event", + }, + { + "doc_count": 45, + "is_selected": false, + "key": "organization", + "label": "Organization", + }, + ], + "label": "Type", + }, + "visibility": { + "buckets": [ + { + "doc_count": 90, + "is_selected": false, + "key": "public", + "label": "Public", + } + ], + "label": "Visibility", + }, + }, + "hits": { + "hits": [ + { + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "access": { + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + }, + "children": {"allow": false}, + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 100, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for panda research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } + }, + /* ... */ + ], + "total": 90, + }, + "links": { + "self": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons", + "first": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1", + "last": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=9", + "prev": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1", + "next": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=2", + } + "sortBy": "updated-asc", +} +``` + +##### Successful response headers + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Link | `; rel="first", ; rel="last", ; rel="prev", ; rel="next"` | + + +#### Requesting collections for a specific group + +Note that if you specify a `commons_group_id` value, you must *also* provide a `commons_instance` value. This is to avoid confusion if different Commons instances use the same internal id for groups. + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&commons_group_id=12345 HTTP/1.1 +``` + +##### Successful response status code + +`200 OK` + +##### Successful Response Body: + +```json +{ + "aggregations": { + "type": { + "buckets": [ + { + "doc_count": 2, + "is_selected": false, + "key": "event", + "label": "Event", + }, + { + "doc_count": 2, + "is_selected": false, + "key": "organization", + "label": "Organization", + }, + ], + "label": "Type", + }, + "visibility": { + "buckets": [ + { + "doc_count": 4, + "is_selected": false, + "key": "public", + "label": "Public", + } + ], + "label": "Visibility", + }, + }, + "hits": { + "hits": [ + { + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "access": { + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + }, + "children": {"allow": false}, + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 2, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for panda research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } + }, + /* ... */ + ], + "total": 4, + }, + "links": { + "self": "https://works.hcommons.org/api/group_collections", + "first": "https://works.hcommons.org/api/group_collections?page=1", + "last": "https://works.hcommons.org/api/group_collections?page=1", + "prev": "https://works.hcommons.org/api/group_collections?page=1", + "next": "https://works.hcommons.org/api/group_collections?page=1", + } + "sortBy": "newest", +} +``` + +##### Successful response headers + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | + +#### Requesting a specific collection + +While other kinds of requests require query parameters, a request for metadata on a specific Commons Works collection can be made by simply adding the community's slug to the end of the url path. Once again, this will only succeed for collections that are linked to a Commons instance group. Collections that exist independently on Knowledge Commons Works will not be found at the `group_collections` endpoint and should be requested at the `communities` endpoint instead. + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections/my-collection-slug HTTP/1.1 +``` + +##### Successful Response Status Code + +`200 OK` + +##### Successful Response Body: + +```json +{ + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 100, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for pandas research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } +} +``` + +### Creating a Collection for a Group (POST) + +A POST request to this endpoint creates a new collection in Invenio owned by the specified Commons group. If the collection is successfully created, the response status code will be 201 Created, and the response body will be a JSON object containing the URL slug for the newly created collection. + +The POST request will trigger a callback to the Commons instance to get the metadata for the specified group, using the configuration dictionary declared in the `GROUP_COLLECTIONS_METADATA_ENDPOINTS` config variable, under the key matching the Commons instance's SAML IDP provider name (declared in the `SSO_SAML_IDPS` config variable). This callback request will be sent to the "url" specified in the configuration dictionary (e.g., `GROUP_COLLECTIONS_METADATA_ENDPOINTS["knowledgeCommons"]["url"]`). This request will be authenticated using the environment variable whose name matches the `token_name` from the same configuration dictionary. The metadata from this callback request will then be used to populate the collection metadata in Invenio. + +If the metadata returned from the Commons instance includes a url for an avatar, that avatar will be downloaded and stored in the Invenio instance's file storage. Since we do not want to use a placeholder avatar for the group, the instance's configuration can include a `placeholder_avatar` key. If the file name or last segment of the supplied avatar url matches this `placeholder_avatar` value, it will be ignored. + +#### Permissions and access in newly created collections + +By default, the newly created collection will have the following access settings: + +- Visibility: "public" +- Member visibility: "public" +- Member policy: "closed" +- Record policy: "closed" +- Review policy: "closed" + +They will appear in search results and be visible to non-members of the collection. But users who are not group members will not be able to request membership, and all submissions to the group will be held for review by the collection curators. + +The collection's administrators can change these settings in the collection's settings page. + +#### Handling group name changes + +Note that when a collection is created for a group, the collection's slug will be generated from the group's name. If the group's name is changed in the Commons instance, the collection's slug will not be automatically updated. This is to avoid breaking links to the collection. If the group's name is changed, the collection's slug will remain the same, but the collection's metadata will be updated to reflect the new group name. + +#### Handling collection name collisions + +It is possible for two groups on Commons instances to share the same human readable name, even though their ids are different. Knowledge Commons Works *will* allow multiple collections to share identical human readable names, but group url *slugs* must be unique across all KC Works collections. So where group names collide, only the first of the identically-named collections will have its slug generated normally. Susequent collections with the same name will have a numerical disambiguator appended to the end of their slugs. So if we have three groups named "Panda Studies," the first collection created for one of the groups will have the slug `panda-studies`. The other collections created by these groups will be assigned the slugs `panda-studies-1` and `panda-studies-2`, in order of their creation in Knowledge Commons Works. + +#### Handling deleted group collections + +If a group collection is deleted, its slug will be reserved in the Invenio PID store and cannot be re-used for a new collection. If a new collection is created for the same group, the slug will have a numerical disambiguator appended to the end, exactly as in cases of group name collision. E.g., if the group `panda-studies` were deleted earlier, a request to create a new collection for the "Panda Studies" group would be assigned the URL slug `panda-studies-1`. This is to avoid breaking links to the deleted collection. + +In future it may be possible to restore deleted collections, but this is not currently implemented. + + +#### Request + +```http +POST https://works.hcommons.org/api/group_collections HTTP/1.1 +``` + +Required request headers: + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Authorization | `Bearer ` | + +#### Request body + +The request body must be a JSON object with the following fields: + +| Field name | Required | Description | +| -----------|----------|-------------| +| `commons_instance` | Y | The name of the Commons instance to which the group belongs. This must be the same string used to identify the instance in the `GROUP_COLLECTIONS_METADATA_ENDPOINTS` config variable. | +| `commons_group_id` | Y | The ID of the Commons group that will own the collection. | +| `collection_visibility` | N | The visibility setting for the collection to be created. Must be either "public" or "restricted". [default: "restricted"]| + +The resulting request body will be shaped like this: + +```json +{ + "commons_instance": "knowledgeCommons", + "commons_group_id": "12345", + "collection_visibility": "public", +} +``` + + +#### Successful response status code + +`201 Created` + +#### Successful response body + +```json +{ + "commons_group_id": "12345", + "collection_slug": "new-collection-slug" +} +``` + +#### Unsuccessful response codes + +- 400 Bad Request: The request body is missing required fields or contains + invalid data. +- 404 Not Found: The specified group could not be found by the callback to the Commons instance. +- 403 Forbidden: The request is not authorized to modify the collection. +- 409 Conflict: A collection already exists in Knowledge Commons Works linked to the specified group. + +### Changing the Group Ownership of a Collection (PATCH) + +[!WARNING] +PATCH requests to change group ownership of the collection are not yet implemented. + +A PATCH request to this endpoint modifies an existing collection in Invenio by changing the Commons group to which it belongs. This is the *only* modification that can be made to a collection via this endpoint. Other modifications to Commons group metadata should be handled by signalling the Invenio webhook for commons group metadata updates. Modifications to internal metadata or settings for the Invenio collection should be made view the Invenio "communities" API or the collection settings UI. + +Note that the collection memberships in Invenio will be automatically transferred to the new Commons group. The corporate roles for the old Commons group will be removed from the collection and corporate roles for the new Commons group will be added to its membership with appropriate permissions. But any individual memberships that have been granted through the Invenio UI will be left unchanged. If the new collection administrators wish to change these individual memberships, they will need to do so through the Invenio UI. + +#### Request + +```http +PATCH https://works.hcommons.org/api/group_collections/my-collection-slug HTTP/1.1 +``` + +Required request headers: + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Authorization | `Bearer ` | + +#### Request body + +```json +{ + "commons_instance": "knowledgeCommons", + "old_commons_group_id": "12345", + "new_commons_group_id": "67890", + "new_commons_group_name": "My Group", + "collection_visibility": "public", +} +``` + +#### Successful response status code + +`200 OK` + +#### Successful response body + +```json +{ + "collection": "my-collection-slug" + "old_commons_group_id": "12345", + "new_commons_group_id": "67890", +} +``` + +#### Unsuccessful response codes + +- 400 Bad Request: The request body is missing required fields or contains + invalid data. +- 404 Not Found: The collection does not exist. +- 403 Forbidden: The request is not authorized to modify the collection. +- 304 Not Modified: The collection is already owned by the specified + Commons group. + +### Deleting a Group's Collection (DELETE) + +A DELETE request to this endpoint deletes a collection in Invenio owned by the specified Commons group. Note that the request must include all of: + +- the collection slug as the url path parameter +- the identifier of the Commons instance to which the group belongs, in the `commons_instance` query parameter +- the Commons identifier of the group which owns the collection, in the `commons_group_id` query parameter + +If any of these is missing the request will fail with a `400 Bad Request` error. This is to ensure that collections are not deleted accidentally or by agents without authorization. + +If the collection is successfully deleted, the response status code will be 204 No Content. + +[!NOTE] +Once a group collection has been deleted, its former URL slug is still registered in Invenio's PID store and reserved for the (now deleted) collection. Subsequent requests to create a collection for the same group cannot re-use the same URL slug. Instead the new slug will have a numerical disambiguator added to the end, exactly as in cases of group name collision. E.g., if the group `panda-studies` were deleted earlier, a request to create a new collection for the "Panda Studies" group would be assigned the URL slug `panda-studies-1`. + +[!NOTE] +Group collections are soft deleted and can in principle be restored within a short period after the delete signal has been sent. Eventually, though, the soft deleted collection records will be +automatically purged entirely from the database. There is also no API mechanism for restoring them. So delete operations should be regarded as permanent and irrevocable. + +#### Request + +```http +DELETE https://works.hcommons.org/api/group_collections/my-collection-slug?commons_instance=knowledgeCommons&commons_group_id=12345 HTTP/1.1 +``` + +Required request headers: + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Authorization | `Bearer ` | + +#### Successful response status code + +`204 No Content` + +#### Unsuccessful response codes + +- 400 Bad Request: The request did not include the required parameters or the parameters are not well formed. +- 403 Forbidden: The requesting agent is not authorized to delete the collection. The collection may not belong to the Commons instance making the request, or it may not belong to the specified Commons group. +- 404 Not Found: The collection does not exist. +- 422 UnprocessableEntity: The deletion could not be performed because the + + +## User and Group Data Updates (Internal Only) + +``` +https://works.hcommons.org/api/webhooks/user_data_update +``` + +> [!WARNING] +> This API endpoint is intended for internal use only. It is not intended to be used by clients outside of the Knowledge Commons system. + +> [!NOTE] +> This API was implemented with a distributed network of independent Commons instances in mind. Currently, only the Knowledge Commons instance exists and is supported as a SAML IDP by KCWorks. + +The api endpoint `/api/webhooks/user_data_update` is provided for Knowledge Commons applications and instances to signal that user or group metadata has been changed. These endpoints do not receive the actual updated data. They only receive notices *that* the metadata for a user or group has changed. KCWorks will then query the Commons instance's endpoint to retrieve current metadata for the user or group. + +### User/Groups Metadata updates and SAML authentication + +It is assumed that Commons instances have registered a SAML authentication IDP with KCWorks. The Commons identifiers for users in metadata update signals must be the same identifiers provided by the instance's SAML IDP. This allows KCWorks to reliably identify the correct KCWorks user account, even if the same identifier happens to be used internally by multiple Commons instances. It also allows KCWorks to store Commons instance user ids in one central place within KCWorks, minimizing the chances of those links between a Commons instance user account and a KCWorks user account becoming corrupted. + +### GET requests + +A `GET` request to this endpoint can be used to check that the endpoint is available and receiving messages. The response should have a `200` status code and should carry the following JSON response body: + +```json +{ + "message": "Webhook receiver is active", + "status": 200, +} +``` + +### POST requests +#### Payload objects + +Update notices should be sent via a `POST` request with a JSON payload object shaped like this: + +```json +{ + "idp": "knowledgeCommons", + "updates": { + "users": [ + {"id": "myusername", "event": "updated"}, + {"id": "anotherusername", "event": "created"}, + ], + "groups": [{"id": "1234", "event": "updated"}], + }, +}, +``` + +Top level payload object properties: + +| Property | Type | Description | Required | +| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `idp` | string | The name used by KCWorks to identify the identity provider the Commons instance has registered with KCWorks. This id should have been provided by the KCWorks administrators when the Commons instance's IDP connection was established. For Knowledge Commons the value is `knowledgeCommons` | Y | +| `updates` | object | This object identifies the metadata updates that have taken place on the Commons instance. It allows updates of different kinds and for multiple entities to be signalled in a single request. Its properties are described below. | Y | + +`updates` object properties: + +| Property | Type | Description | Required | +| -------- | ----- | ----------------------------------------------------------------------------------- | -------- | +| `users` | array | An array of objects each representing one metadata change event for a single user. | N | +| `groups` | array | An array of objects each representing one metadata change for a single group. | N | + +NOTE: A valid payload *must* provide either a `users` array or a `groups` array with at least one member. Requests providing neither `users` nor `groups`, or providing only empty arrays, will result in an error response. + +`users` and `groups` object properties + +| Property | Type | Description | Required | +| -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `id` | number | The local identifier of the user or group on the Commons instance. This must be the same identifier that can be used to retrieve the entity's metadata at the corresponding endpoint on the Commons instance. | Y | +| `event` | string | The nature of the metadata change for the entity. Must be one of `updated`, `created`, or `deleted`. The `updated` and `deleted` event types should be sent when an entity is first created or is deleted entirely from the Commons instance. These will trigger the creation or deletion of corresponding entities (a user or a group) on KC Works. All other metadata changes are `updated` events. | Y | + +NOTE: A valid payload's user and/or group objects must each include *both* an `id` *and* an `event` value. + +#### Event timing + +There may be some delay between KC Works' receiving an update signal and the updating of the corresponding entity's metadata in KC Works. The actual updates are handled by background workers and in some cases there may be a slight delay before a worker is free. Usually this will only be a fraction of a second, but if intensive background tasks (like indexing) are ongoing it could be several minutes. The update also depends on a successful callback request from KC Works to the Commons instance's endpoint for serving user or group metadata. If that request fails, it is possible for an update to fail even though the webhook signal was received successfully. + +#### Success responses + +If a signal is received successfully, the response will have a status of `202` and carry a JSON response object shaped like this: + +```json +{ + "message": "Webhook received", + "status": 202, + "updates": { + "users": [ + {"id": "myusername", "event": "updated"}, + {"id": "anotherusername", "event": "created"}, + ], + "groups": [{"id": "1234", "event": "updated"}], + } +} +``` + +The `updates` object should be identical to the `updates` object provided in the `POST` request. This confirms that the correct events have all been received and are being sent for processing. + +#### Error responses + +If multiple update signals are received in one `POST` request, it is possible that only some of the updates can be processed. The request might, for example, provide `updated` event signals for a number of entities, some of whose ids do not exist in KC Works. In this case the response code will be `207 Multi-Status` and the response payload will be a JSON object + + diff --git a/docs/build/_sources/architecture.md.txt b/docs/build/_sources/architecture.md.txt new file mode 100644 index 000000000..872359139 --- /dev/null +++ b/docs/build/_sources/architecture.md.txt @@ -0,0 +1,342 @@ +# KCWorks Architecture + +## InvenioRDM's Layered Architecture + +InvenioRDM employs a layered architecture with: + +1. Data layer + - Low-level data storage and retrieval. + - Primarily SQLAlchemy model classes. + - High-level data API classes that provide a Pythonic interface to the data layer. + - Validate data before storing it. +2. Service layer + - Retrieves and modifies data from the data layer, either for a view or for another service. + - Providing abstract CRUD methods for operating on the data layer's API classes. + - Providing abstracted "result items" and "result lists" + - Enforces permission and access control policies. +3. View layer + - Consists of + - Flask views (registered as Blueprints) + - rendering either + - Jinja2 templates to produce HTML + - JSON to produce API responses + - in some cases, React components embedded in the Jinja2 templates + - These are rendered on the client side + - Data is passed from the Jinja2 templates to the React components via HTML data attributes + +## InvenioRDM Services + +An InvenioRDM service is a class that provides methods for interacting with the data layer. The business logic of the service is usually delegated to one or more component classes, which are called during the service's methods. + +### Service Classes + +#### BaseService + +The base Service class is defined in `invenio_records_resources.services.base.Service`. It defines methods for: + +- Getting the service ID + - `id(self)`: Return the id of the service from config. +- Permissions checking + - `permission_policy(self, action_name, **kwargs)`: Factory for a permission policy instance. + - `check_permission(self, identity, action_name, **kwargs)`: Check a permission against the identity. + - `require_permission(self, identity, action_name, **kwargs)`: Require a specific permission from the permission policy. +- Handling service components + - `components(self)`: Return initialized instances of the service's component classes. + - `run_components(self, action, *args, **kwargs)`: Run components for a given action. +- Producing result items and lists + - `result_item(self, *args, **kwargs)`: Create a new instance of the resource unit, i.e. whatever the service provides. + - `result_list(self, *args, **kwargs)`: Create a new list of resource units. In some cases this is a simple iterable of resource units, but in other cases it is a more complex object that includes additional data. + +#### RecordService + +Services dealing with InvenioRDM records of some kind (e.g. records, drafts, communities, etc.) inherit from the `RecordService` class defined in `invenio_records_resources.services.records.service`. This class adds: + +- properties and methods related to the service's related data-layer API class + - A `schema` property that returns a `ServiceSchemaWrapper` instance. + - A `record_cls` property that returns the record class for the service. + - A `links_item_tpl` property that returns a `LinksTemplate` instance for constructing links to a resource unit. + - An `expandable_fields` property that returns a list of expandable fields for the service's data-layer API class. +- Methods for creating searches + - `create_search(self, identity, record_cls, search_opts, permission_action="read", preference=None, extra_filter=None, versioning=True)`: Instantiate a search class. + - `search_records(self, identity, params, **kwargs)`: A low-level method to create an OpenSearch DSL instance for searching records. + - `search(self, identity, params=None, search_preference=None, expand=False, **kwargs)`: A high-level method to search for records matching the querystring. + - `scan(self, identity, params=None, search_preference=None, expand=False, **kwargs)`: A high-level method to perform a rolling "scroll" search for records matching the querystring. (This is used for searching through large numbers of records, since OpenSearch will not return more than 10,000 records at a time.) +- Methods for indexing records + - `reindex(self, identity, params=None, search_preference=None, search_query=None, extra_filter=None, **kwargs)`: A high-level method to reindex records matching the query parameters. + - `rebuild_index(self, identity, uow=None)`: A high-level method to reindex all records managed by this service. +- CRUD methods + - `create(self, identity, data, uow=None, expand=False)`: Create a record. + - `exists(self, identity, id_)`: Check if the record exists and user has permission. (Does *not* use the search index.) + - `read(self, identity, id_, expand=False, action="read")`: Retrieve a record. (Does *not* use the search index.) + - `read_many(self, identity, ids, expand=False, action="read")`: Retrieve multiple records using the search index. + - `read_all(self, identity, params=None, search_preference=None, expand=False, **kwargs)`: Retrieve all records matching the query parameters using the search index. + - `update(self, identity, id_, data, uow=None, expand=False)`: Update a record. + - `delete(self, identity, id_, uow=None)`: Delete a record. +- Helper methods for record management + - `check_revision_id(self, record, expected_revision_id)`: Validate the given revision_id with current record's one. + - `on_relation_update(self, identity, record_type, records_info, notif_time, limit=100)`: Handles the update of a related field record when the related field is updated. + +#### Augmented RecordService + +The `invenio_drafts_resources` package then overrides this with a `RecordService` class that adds (a) a distinction between published and draft records, (b) record versioning and a parent-child record relationship, and (c) file attachments to service records. This adds the following properties and methods to the `RecordService` class: + +- Properties and methods for draft records + - `draft_cls(self)`: Return the record class for the service. + - `draft_files(self)`: Return the draft files service for the service. + - `draft_indexer(self)`: A factory for creating an indexer instance. + - `search_drafts(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs)`: Search for draft records matching the querystring. + - `read_draft(self, identity, id_, expand=False)`: Retrieve a draft record. + - `update_draft(self, identity, id_, data, revision_id=None, uow=None, expand=False)`: Replace a draft. + - `edit(self, identity, id_, uow=None, expand=False)`: Creates a new revision of a draft or a draft for an existing published record. + - `publish(self, identity, id_, uow=None, expand=False)`: Publishes a draft record. + - `delete_draft(self, identity, id_, revision_id=None, uow=None)`: Deletes a draft record. (Defaults to a soft delete, so the record is not actually deleted from the database or search index until a later cleanup operation.) + - `validate_draft(self, identity, id_, ignore_field_permissions=False)`: Validate a draft. + - `cleanup_drafts(self, timedelta, uow=None, search_gc_deletes=60)`: Hard delete of soft deleted drafts. +- Properties and methods for files + - `files(self)`: Return the files service for the service. + - `import_files(self, identity, id_, uow=None)`: Import files from previous record version. +- Properties and methods for versions and parent records + - `schema_parent(self)`: Return the parent schema for the service. + - `search_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read", **kwargs)`: Search for record's versions. + - `read_latest(self, identity, id_, expand=False)`: Retrieve the latest version of a record. + - `new_version(self, identity, id_, uow=None, expand=False)`: Creates a new version of a record. +This overridden `RecordService` class also modifies the CRUD methods to enforce a workflow in which records are only modified via their draft records. This involves overriding: + +- `update(self, identity, id_, data, uow=None, expand=False)`: Now raises a `NotImplementedError` error. +- `create(self, identity, data, uow=None, expand=False)`: Now creates a draft record. +- `rebuild_index(self, identity)`: Now reindexes all draft records (instances of draft API class) as well as all published records (instances of record API class) and skips soft-deleted records. + +#### RDMRecordService + +The `invenio_rdm_records` package provides an `RDMRecordService` class that inherits from the `RecordService` class and adds: + +- Additional properties for accessing subservices + - `access`: Return the access service for the service. + - `pids`: Return the PIDs service for the service. + - `review`: Return the review service for the service. +- Methods for embargo handling + - `lift_embargo(self, identity, _id, uow=None)`: Lifts an embargo from the record and draft (if exists). + - `scan_expired_embargos(self, identity)`: Scan for records with an expired embargo. +- Properties and methods for file quota handling + - `schema_quota`: Return the schema for quota information. + - `set_quota(self, identity, id_, data, files_attr="files", uow=None)`: Set the quota values for a record. + - `set_user_quota(self, identity, id_, data, uow=None)`: Set the user files quota. +- Properties and methods for deletion of published records + - `schema_tombstone`: Return the schema for tombstone information. + - `delete_record(self, identity, id_, data, expand=False, uow=None, revision_id=None)`: Re-introduces soft-deletion of published records (which were previously removed by the `RecordService` class). + - `update_tombstone(self, identity, id_, data, expand=False, uow=None)`: Update the tombstone information for the (soft) deleted record. + - `cleanup_record(self, identity, id_, uow=None)`: Clean up a (soft) deleted record. + - `restore_record(self, identity, id_, expand=False, uow=None)`: Restore a record that has been (soft) deleted. + - `mark_record_for_purge(self, identity, id_, expand=False, uow=None)`: Mark a (soft) deleted record for purge. + - `unmark_record_for_purge(self, identity, id_, expand=False, uow=None)`: Remove the mark for deletion from a record, returning it to deleted state. + - `purge_record(self, identity, id_, uow=None)`: Purge a record that has been marked. +- Overridden methods to add deletion-related functionality + - `read(self, identity, id_, expand=False, action="read", include_deleted=False)`: Adds an `include_deleted` argument to the read method, and a check for the `read_deleted` permission if it is set to `True`. + - `read_draft(self, identity, id_, expand=False)`: Prevents reading a draft if there is a published deleted record. (410 response.) + - `search(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs)`: Adds a "read_deleted" permission action to the search method. + - `search_drafts(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs)`: Adds a filter to exclude soft-deleted records from the search results. + - `search_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read", **kwargs)`: Adds a "read_deleted" permission action to the search method. +- Additional overridden methods for other functionality + - `publish(self, identity, id_, uow=None, expand=False)`: Adds a check prior to the original publish method to allow enforcement of a config setting that requires a community to be present on a record before it can be published. + - `update_draft(self, identity, id_, data, revision_id=None, uow=None, expand=False)`: Adds a check prior to the original update_draft method to allow enforcement of a config setting that prevents a record from being restricted after the grace period. +- Additional new methods for other functionality + - `expandable_fields`: Expands the `communities` field to return community details. + - `oai_result_item(self, identity, oai_record_source)`: Get a result item from a record source in the OAI server. + - `scan_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read_deleted", **kwargs)`: Search for record's versions using a "scroll" search. + +### Service Configuration + +A service configuration is an object that provides the service with its configuration. It is passed to the service's constructor when it is instantiated during the Flask app initialization. + +The service configuration is defined in the service's `config` attribute. + +All service configurations inherit from the `ServiceConfig` class, which is defined in `invenio_records_resources.services.base.config`. They include at least: + +- `service_id`: The ID of the service. +- `permission_policy_cls`: The permission policy class to use for the service. +- `result_item_cls`: The result item class to use for the service. +- `result_list_cls`: The result list class to use for the service. + +This is expanded in a `RecordServiceConfig` class by the `invenio_records_resources` package to add: + +- `record_cls`: The record class to use for the service. +- `indexer_cls`: The indexer class to use for the service. +- `indexer_queue_name`: The name of the task queue to be used by the service's indexer. +- `index_dumper`: The dumper to be used for serializing records to be indexed by OpenSearch. +- `relations`: The inverse relation mapping for the service, defining which fields relate to which record type. +- `search`: The search configuration for the service. (This is a `SearchOptions` instance.) +- `schema`: The schema to be used when validating the service's records. +- `links_item`: The template for creating url links for the service's result items. +- `links_search`: The template for creating url links for the service's search endpoints. +- `components`: A list of components that will be used by the service. + +It is further expanded in an overridden `RecordServiceConfig` class by the `invenio_drafts_resources` package to add: + +- `draft_cls`: The draft record class to use for the service. +- `draft_indexer_cls`: The indexer class to use for the service's draft records. +- `draft_indexer_queue_name`: The name of the task queue to be used by the service's draft records indexer. +- `schema_parent`: The schema used to valid parent records for the service. +- `search_drafts`: A search class for searching for draft records. +- `search_versions`: A search class for searching for record versions. +- `default_files_enabled`: Whether files are enabled by default for the service. +- `default_media_files_enabled`: Whether media files are enabled by default for the service. +- `lock_edit_published_files`: Whether to lock editing of published files for the service. +- `links_search_drafts`: The template for creating url links for the service's search drafts endpoint. +- `links_search_versions`: The template for creating url links for the service's search versions endpoint. + +The `RDMRecordServiceConfig` class adds the following additional configuration attributes: + +- `max_files_count`: The maximum number of files that can be attached to a record. +- `file_links_list`: The list of file links for the service. +- `schema_access_settings`: The schema for access settings. +- `schema_secret_link`: The schema for secret links. +- `schema_grant`: The schema for grants. +- `schema_grants`: The schema for grants. +- `schema_request_access`: The schema for request access. +- `schema_tombstone`: The schema for tombstone. +- `schema_quota`: The schema for quota. + + +Additional common configration attributes are added by inheriting from additional mixin classes. + +#### Attaching configuration to the service + +The service config class is passed to the service's constructor when it is instantiated during the Flask app initialization: + +```python +service = MyService(config=MyServiceConfig) +``` + +#### File service configuration + +The `FileConfigMixin` class (defined in `invenio_records_resources.services.records.components.files`) adds config class attributes for: ???? + +- `_files_attr_key`: The attribute key for the files field. +- `_files_data_key`: The attribute key for the files data. +- `_files_bucket_attr_key`: The attribute key for the files bucket. +- `_files_bucket_id_attr_key`: The attribute key for the files bucket ID. + +#### Search configuration + +##### SearchOptionsMixin + +This mixin class (defined in `invenio_records_resources.services.base.config`) adds config class attributes for: + +- `facets`: The search facet definitions for searches on the service's resource. +- `sort_options`: The sort options for searches on the service's resource. +- `sort_default`: The default sort option for searches on the service's resource. +- `sort_default_no_query`: The default sort option for searches on the service's resource when no query is present. +- `available_sort_options`: The available sort options for searches on the service's resource. +- `query_parser_cls`: The query parser class to use in constructing searches on the service's resource. + +##### SearchConfig + +The SearchConfig class (defined in `invenio_records_resources.services.base.config`) defines the search configuration that will be used to interface with OpenSearch. + +##### FromConfigSearchOptions + +The `FromConfigSearchOptions` class (defined in `invenio_records_resources.services.base.config`) is used to load search configuration from app config variables. In the service's config class, it is used like this: + +#### Loading configuration from app config variables + +The `FromConfig` class (defined in `invenio_records_resources.services.base.config`) is used to load configuration from app config variables. In the service's config class, it is used like this: + +```python +class MyServiceConfig(ServiceConfig): + foo = FromConfig("FOO", default=1) +``` + +In the app config, the config variable is defined like this: + +```python +FOO = 2 +``` + +When the service is instantiated, the `FromConfig` class will load the config variable from the app config and assign it to the `foo` attribute. + +### Service Components + +A service component is a class that provides methods that shadow the service's methods. When a service method is called, it passes the call through each of the service's components (using the `Service.run_components()` method), allowing each component to perform additional processing before the result is returned. If the service component includes a method with the same name as the service method that is being called, its matching method will be called. During this call, the component method is passed the service method's arguments and keyword arguments, and the service method's modified versions of these arguments are passed on to the next component. Once all the service's components have been called, the result is returned to the service method, which returns the final result or performs the final action. + +#### BaseServiceComponent + +The `BaseServiceComponent` class (defined in `invenio_records_resources.services.base.components`) is the base class for all service components. It provides a `uow` property that returns the Unit of Work manager. + +This class is overridden by the `ServiceComponent` class (defined in `invenio_records_resources.services.base.components.base`), which adds the following methods: + +- `create(self, identity, **kwargs)`: Perform additional processing while creating an item of the service's resource. +- `read(self, identity, **kwargs)`: Perform additional processing while retrieving an item of the service's resource. +- `update(self, identity, **kwargs)`: Perform additional processing while updating an item of the service's resource. +- `delete(self, identity, **kwargs)`: Perform additional processing while deleting an item of the service's resource. +- `search(self, identity, search, params, **kwargs)`: Perform additional processing while searching for items of the service's resource. + +The `invenio_drafts_resources` package overrides the `ServiceComponent` class to add methods matching the overridden RecordService methods for draft records and versioning. + +- `read_draft(self, identity, draft=None)`: Retrieve a draft record. +- `update_draft(self, identity, data=None, record=None, errors=None)`: Update a draft record. +- `delete_draft(self, identity, draft=None, record=None, force=False)`: Delete a draft record. +- `edit(self, identity, draft=None, record=None)`: Edit a record. +- `new_version(self, identity, draft=None, record=None)`: Create a new version of a record. +- `publish(self, identity, draft=None, record=None)`: Publish a draft record. +- `import_files(self, identity, draft=None, record=None)`: Import files from previous record version. +- `post_publish(self, identity, record=None, is_published=False)`: Post publish handler. + +#### RecordService Components + +The `invenio_records_resources` package provides the following components for the `RecordService` class: + +- `DataServiceComponent` (create, update): Adds data to the record. +- `BaseRecordFilesComponent` (create, update): + - Handles enabling/disabling files for a record. + - Handles setting the default preview file for a record. +- `MetadataComponent` (create, update): Adds metadata to the new/updated record from the input data. +- `RelationsComponent` (read): Dereferences a record's related fields in order to provide the data from the related records in a read result. +- `ChangeNotificationsComponent` (update): Emits a change notification for the updated record. + +The `invenio_drafts_resources` package provides additional components for the `RecordService` class: + +- an overridden `BaseRecordFilesComponent` class that adds methods for ??? +- `DraftFilesComponent`: Handles files for draft records. +- `DraftMediaFilesComponent`: Handles media files for draft records. +- `DraftMetadataComponent`: Handles metadata for draft records. +- `PIDComponent` (create, delete_draft): Handles registration of PIDs for draft records. +- an overridden `RelationsComponent` class that adds a `read_drafts` method + +The `invenio_rdm_records` package provides additional components for the `RDMRecordService` class: + +- `AccessComponent`(create, update_draft, publish, edit, new_version): Handles access settings for records. +- an overridden `MetadataComponent` class (create, update_draft, publish, edit, new_version): Adds metadata to the new/updated record from the input data. (Removes the `update` method from the earlier `MetadataComponent` class.) +- `CustomFieldsComponent`(create, update_draft, publish, edit, new_version): Adds custom fields to the metadata of a record. +- `PIDsComponent`(create, update_draft, delete_draft, publish, edit, new_version, delete_record, restore_record): Handles PIDs for records. +- `ParentPIDsComponent`(create, publish, delete_record, restore_record): Handles parent PIDs for records. +- `RecordDeletionComponent`(delete_record, update_tombstone, restore_record, mark_record, unmark_record, purge_record): Handles deletion of records. +- `RecordFilesProcessorComponent`(publish, lift_embargo): Handles file processing for records. +- `ReviewComponent`(create, delete_draft, publish): Handles reviews for records. +- `SignalComponent`(publish): Triggers signals on publish. +- `ContentModerationComponent`(publish): Creates a moderation request if the user is not verified. + +#### RDMRecordService Components + +The `invenio_rdm_records` package draws its list of components from the `RDM_RECORDS_SERVICE_COMPONENTS` config variable. The default list is defined in the `DefaultRecordsComponents` class (defined in `invenio_rdm_records.services.config`) and currently includes:. + +```python +[ + MetadataComponent, + CustomFieldsComponent, + AccessComponent, + DraftFilesComponent, + DraftMediaFilesComponent, + RecordFilesProcessorComponent, + RecordDeletionComponent, + # for the internal `pid` field + PIDComponent, + # for the `pids` field (external PIDs) + PIDsComponent, + ParentPIDsComponent, + RelationsComponent, + ReviewComponent, + ContentModerationComponent, +] +``` + +Note that the order of the components in the list is important, since the components are called in the order they are listed and some components depend on the results of previous components. \ No newline at end of file diff --git a/docs/build/_sources/changelog.md.txt b/docs/build/_sources/changelog.md.txt new file mode 100644 index 000000000..6f876f44d --- /dev/null +++ b/docs/build/_sources/changelog.md.txt @@ -0,0 +1,85 @@ + + + + +# Changes + +## 0.3.5-beta8 (2025-01-10) + +- Dashboard works search + - Fixed the bug that broke works searching from the dashboard. + +## 0.3.4-beta7 (2025-01-09) + +- Upload form collection selector + - Fixed bug in collection selection modal where search results were always sorted by "newest" instead of "bestmatch" and so were useless for large result sets (original fix only worked in detail page) +- Documentation + - Moved documentation from README.md and site/CHANGES.md into a static documentation site to be served by Github Pages. + - Added more documentation for cli commands, metadata/identifiers/vocabularies, installation, and version control. +- Build system + - Pinned the version of invenio-logging to less than 2.1.2 to avoid a webpack build conflict. + +## 0.3.3-beta6 (2024-12-18) + +- Names + - Added the infrastructure to customize the division of users' names into parts so that it can be divided as desired when, e.g., the user's name is being auto-filled in the name fields of the upload form. This involves + - a new "name_parts_local" field to the user profile schema. This field contains the user's name parts if they have been modified within the KCWorks system. This is sometimes necessary when the user data synced from the remote user data service does not divide the user's name correctly. + - a cli command to update the user's name parts. + - a new "names" js module that contains functions to get the user's full name, full name in inverted order, family name, and given name from the user's name parts. + - updates to the CreatibutorsField component to use the new "names" js module and the customized name parts if they are present in a user's profile. +- Detail page + - Added missing aria-label properties for accessibility +- Collections + - Fixed wording of empty results message for collection members search + - Previously, the empty results message used "community" instead of "collection". + - Tweaks to layout of collection detail page header +- Remote user data service + - Fixed bug where user profile data was not being updated because comparison with initial data was not being made correctly. This means that, among other things, ORCID ids will now be added correctly when the user chooses "add self" on the upload form. + +## 0.3.2-beta5 (2024-12-11) + +- Added Bluesky sharing option to detail page +- Fixed line wrapping of long values in record sidebar details +- Added OpenGraph image metadata property to record detail page + - This allows social media platforms to display the KCWorks logo instead of a random image they might find on the page. + +## 0.3.1-beta4 (2024-12-10) + +- Added sort options for publication date to record search + - This allows users to sort records by the date they were published. + - It also allows publication-date sorting in API requests to the search API. Among other things, this allows users' KC profiles to display records in publication date order. +- Community selection modal bug fixes + - This affects the modal that appears both during record submission on the upload form and during collection management on the detail page. + - Fixed the sort order of search results in the modal. These were being sorted by record creation date, leading to a confusing sort order. It now sorts by "best match". This allows, e.g., "Knowledge Commons" to find the main KC collection. + - Also fixed the handling of '/' in the search query string. This allows, e.g., "ARLIS/NA" to find the ARLIS/NA collection, where previously it would produce an error. + +## 0.3.0-beta3 (2024-11-30) + +- Record detail page + - Added ui for collection management + - A new menu appears in the detail page sidebar when a user has permission to edit a record. This + allows users to manage the record's collections right from the detail page. + - With this menu users can now + - submit a request to have an existing published record added to a collection. + - add a record to multiple collections + - remove a record from some or all of its collections + - view pending collection submissions for the record + - change which collection appears as the primary collection for the record (i.e., the collection whose logo appears in the record's detail page sidebar) + - Refactored record management menu + - Refactored all sidebar menus (including the record management menu) to allow accessible + keyboard navigation + - Fixed display of event metadata + - Added display for work doi as well as version doi + - Each record has at least two DOIs: a work DOI and a version DOI. The work DOI is the DOI for the record as a whole. It always points to the most recent version of the work, even if the user creates new versions in the future. The other identifier is the version DOI, which will always point to the specific version of the work that the user is currently viewing. Previously, only the version DOI was displayed, which could be confusing if the user created a new version of the work. + +- Upload form + - Added proper messages to collections widget for published records + - since collections for published records are now managed from the detail page, the collections widget now displays messages to users pointing them to the detail page to manage collections. + - Added clearer titles to form when editing an existing record + or creating a new version + - Previously, the form would display "Editing Published Record" both when editing the metadata of an existing published version *and* when creating a new version. The header now displays "Creating New Version" when creating a new version, and "Editing Published Record" when editing the metadata of an existing published version. + - Changed default publisher from "unknown" to "Knowledge Commons" + - Previously, the default publisher was "unknown". This was especially confusing for resource types where the publisher field is hidden on the upload form. Now, the default publisher is "Knowledge Commons". + +- Solved collection links bug with custom routes + - This is a back-end technical fix that should not be visible to users. diff --git a/docs/build/_sources/index.rst.txt b/docs/build/_sources/index.rst.txt index 0f57de54e..4d71aac95 100644 --- a/docs/build/_sources/index.rst.txt +++ b/docs/build/_sources/index.rst.txt @@ -3,28 +3,30 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to Knowledge Commons Works's documentation! -=================================================== +Welcome to the Knowledge Commons Works technical documentation! +=============================================================== .. toctree:: :maxdepth: 2 :caption: Contents: README - CHANGES + changelog installation metadata customizations configuration + api cli_commands infrastructure developing + architecture in_depth reference -Indices and tables -================== +.. Indices and tables +.. ================== -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. * :ref:`genindex` +.. * :ref:`modindex` +.. * :ref:`search` diff --git a/docs/build/_sources/metadata.md.txt b/docs/build/_sources/metadata.md.txt index b653bf5cf..088f2e8e6 100644 --- a/docs/build/_sources/metadata.md.txt +++ b/docs/build/_sources/metadata.md.txt @@ -1,13 +1,17 @@ -# Metadata Schema and Vocabularies +(metadata-schema-vocabularies-and-identifiers)= +# Metadata Schema, Vocabularies, and Identifiers The default metadata schema for InvenioRDM records is defined in the `invenio-rdm-records` package and documented [here](https://inveniordm.docs.cern.ch/reference/metadata/). It also includes a number of optional metadata fields which have been enabled in KCWorks, documented [here](https://inveniordm.docs.cern.ch/reference/metadata/optional_metadata/). -Beyond these InvenioRDM fields, KCWorks adds a number of custom metadata fields to the schema using InvenioRDM's custom field mechanism. These are all located in the top-level `custom_fields` field of the record metadata. They are prefixed with two different namespaces: +In this documentation we provide +1. A full example of a KCWorks record metadata object +2. A list of the controlled vocabularies and identifier schemes supported by KCWorks +3. Discussion of how some of the standard InvenioRDM metadata fields are used in KCWorks +4. A list of the custom metadata fields KCWorks adds to the base InvenioRDM schema -- `kcr`: custom fields that are used to store data from the KC system. These fields **may** be used for new data, but are not required. -- `hclegacy`: custom fields that are used to store data from the legacy CORE repository. These fields **must not** be used for new data. +## Example metadata record -## Example JSON record +### JSON object for record creation What follows is an example of a complete metadata record (JSON object) used to create a KCWorks record. The various fields and their possible values are described in the sections below. @@ -247,26 +251,278 @@ Note that no single actual record would include all of these fields. The example } ``` +### JSON object retrieved from the record API + +The JSON object retrieved from the record API shares the same basic structure as the JSON object used to create the record, except that it includes a number of additional fields. Some properties are also filled out with additional details (e.g., readable titles for licenses, etc.) + ## Controlled Vocabularies ### Subject headings #### FAST -The FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) is used for the `subjects` field. +The FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) is used for the `subjects` field. See the [metadata.subjects](#metadata-subjects) section for more information about how to include FAST subjects in a KCWorks record. #### Homosaurus -The FAST vocabulary is augmented in KCWorks by the Homosaurus vocabulary (https://homosaurus.org/) for subjects related to sexuality and gender identity. +The FAST vocabulary is augmented in KCWorks by the Homosaurus vocabulary (https://homosaurus.org/) for subjects related to sexuality and gender identity. See the [metadata.subjects](#metadata-subjects) section for information about how to include Homosaurus subjects in a KCWorks record. + +## Resource types + + As an open repository that serves a multidisciplinary audience, KCWorks uses a custom vocabulary of resource types designed (a) to support the wide variety of scholarly materials we accept and (b) to facilitate ease of use for depositors. The terms in this vocabulary are mapped to DataCite's [resourceTypeGeneral](https://schema.datacite.org/meta/kernel-4.4/doc/DataCite_Schema_v4.4.pdf) vocabulary and a number of other resource type vocabularies (COAR, CSL, EUREPO, Schema.org). This allows correct export of metadata to DataCite and in other metadata formats. + +InvenioRDM employs a hierarchical structure of resource types, each of which has a number of subtypes. In KCWorks the 8 top-level resource types are: + +- audiovisual +- dataset +- image +- instructionalResource +- presentation +- software +- textDocument +- other + +We selected these top-level types in part to allow division of the many subtypes into manageable groups. This allows us to provide a wide range of resource types while also allowing users to easily find the resource type that best fits their deposit. + +Beneath these top-level types are a number of subtypes, which are listed below. Where the DataCite schema allows free-text, arbitrary subtypes, we have followed InvenioRDM's approach of using a controlled vocabulary of subtypes. Where our list of top-level types is short, we have erred on the side of including more subtypes. Again, this allows us to support a wide range of materials without forcing them to choose a subtype that does not fit. It also allows us to tailor the user interface of the upload form to the specific subtype of the record being deposited, preventing the confusion and overwhelm of users being presented with many metadata fields which are not relevant to their material. + +The following is the complete list of KCWorks resource types with their subtypes. This list may be expanded in the future. + +- audiovisual + - documentary + - interviewRecording + - musicalRecording + - other + - performance + - podcastEpisode + - audioRecording + - videoRecording +- dataset +- image + - chart + - diagram + - figure + - map + - visualArt + - photograph + - other +- instructionalResource + - curriculum + - lessonPlan + - syllabus + - other +- presentation + - conferencePaper + - conferencePoster + - presentationText + - slides + - other +- software + - 3DModel + - application + - computationalModel + - computationalNotebook + - service + - other +- textDocument + - abstract + - bibliography + - blogPost + - book + - bookSection + - conferenceProceeding + - dataManagementPlan + - documentation + - editorial + - essay + - interviewTranscript + - journalArticle + - legalComment + - legalResponse + - magazineArticle + - monograph + - newspaperArticle + - onlinePublication + - poeticWork + - preprint + - report + - workingPaper + - review + - technicalStandard + - thesis + - whitePaper + - other +- other + - catalog + - collection + - event + - interactiveResource + - notes + - patent + - peerReview + - physicalObject + - workflow + +Note that (like with the base InvenioRDM resource types), neither the list of top-level resource types nor the list of subtypes exactly matches the vocabulary provided by DataCite under [resourceTypeGeneral](https://datacite-metadata-schema.readthedocs.io/en/4.6/appendices/appendix-1/resourceTypeGeneral/). Those types are all included in the KCWorks vocabulary--some as top-level types, some as subtypes. But because we do not follow DataCite in allowing arbitrary free-text subtypes, we have needed to greatly expand the list of subtypes to support the wide variety of materials we accept. As mentioned above, however, each subtype is mapped to a DataCite resourceTypeGeneral value for correct export to DataCite and other metadata formats. + +You can compare the KCWorks resource types with the list from the original Humanities Commons CORE repository [here](https://works.hcommons.org/records/f9xww-xwr22). The KCWorks resource type vocabulary is not structured in the same way as the CORE vocabulary (which was a flat list), but the KCWorks subtypes encompass all of the original CORE types. + + +## Creator/contributor roles + +Keeping with our support for a wide variety of objects and disciplines, our creator roles are more diverse than just "author," "editor," or "translator." For contribuors we were influenced by the [CRediT Taxonomy](https://credit.niso.org/), finding ways of recognizing labor even when the contribution is not immediately visible. Included in both creator and contributor roles a selection of types taken from the [Variations Metadata](https://dlib.indiana.edu/projects/variations3/metadata/guide/controlVocabs/contributorRoles.html) taxonomy, providing the ability to credit those who engage in creative and musical works. + +The complete list of creator/contributor roles is: + +- actor +- adaptor +- annotator +- analyst +- arranger +- artisan +- artist +- attributedName +- author +- authorOfIntroduction +- authorOfForeword +- authorOfAfterword +- committeeChair +- choreographer +- cinematographer +- collaborator +- collector +- committeeMember +- composer +- conductor +- consultant +- contactperson +- correspondent +- datacollector +- datacurator +- datamanager +- dedicatee +- designer +- director +- distributor +- donor +- drafter +- editor +- examiner +- formerOwner +- hostinginstitution +- illustrator +- interviewee +- interviewer +- inventor +- juror +- licensee +- lyricist +- manufacturer +- organizer +- owner +- performer +- photographer +- printer +- producer +- projectOrTeamLeader +- projectOrTeamManager +- projectOrTeamMember +- recording engineer +- referee +- registrationagency +- registrationauthority +- relatedperson +- reporter +- researcher +- researchgroup +- researchParticipant +- rightsholder +- screenplayAuthor +- speaker +- supervisor +- transcriber +- translator +- witness +- workpackageleader +- writerOfAccompanying + +Note that where InvenioRDM provides distinct custom vocabularies for creators and contributors, KCWorks employs a single creator/contributor vocabulary. This is in keeping with our handling of the `creators` and `contributors` fields, discussed below. + +## Identifier Schemes + +### Works + +#### DOI (primary identifier) + +KCWorks (and InvenioRDM) supports the DOI identifier scheme to identify works in the repository. Note that two DOIs are minted for each KCWorks record: one for the current version of the record, and one for the work as a whole (including all versions). The version-specific DOI is stored in the `pids` property of the metadata record (`pids.identifiers.doi`). The work DOI is stored in the `parent.pids.doi` property of the `parent` object. + +These DOIs are minted by DataCite (https://datacite.org/) and the attached metadata is maintained automatically by KCWorks. + +Additional DOIs minted elsewhere can be attached to a KCWorks record. If provided at record creation such external DOIs can be used as the record's primary identifier (in `pids.doi`). Otherwise, they can be added using the `identifiers` property of the metadata record using the scheme `alternate-doi`. In both cases, these externally minted DOIs are **not** maintained automatically by KCWorks. + +#### OAI (secondary identifier) + +KCWorks also supports the OAI identifier scheme. The OAI identifier for a KCWorks record is stored in the `pids` property of the metadata record (`pids.identifiers.oai`). + +#### Handle (secondary identifier) + +KCWorks also supports the Handle identifier scheme (https://handle.net/). The Handle identifier for a KCWorks record is stored in the `identifiers` property of the metadata record (`identifiers[0].identifier`) using the scheme `handle`. + +#### ISSN (secondary identifier) + +An ISSN is an eight digit code that identifies a print or electronic newspaper, journal, magazine, or other periodical. More information on the ISSN can be found on [ISSN.org](https://www.issn.org/understanding-the-issn/what-is-an-issn/). + +#### ISBN (secondary identifier) + +An ISBN (International Standard Book Number) is a ten (pre-2007) or 13 digit (2007 to present) identifier used to identify both print and electronic published books. More information on the ISBN can be found on [ISBN-international.org](https://www.isbn-international.org/content/what-isbn/10). + +### People + +#### ORCID (recommended) + +KCWorks (and InvenioRDM) supports the ORCID identifier scheme. The ORCID of the submitter of the KCWorks record is stored in the `person_or_org.identifiers` property of the `creators` array (`creators[0].person_or_org.identifiers.identifier`). A KCWorks user's ORCID id is also drawn from their KC profile (if they have provided one) and stored in their system user profile (as `.user_profile.identifier_orcid`). + +For details on how to use ORCID identifiers in KCWorks, see the section on [Metadata.creators](#metadata-creators-metadata-contributors) below. + +#### KC Username (recommended) + +KCWorks also allows the use of Knowledge Commons usernames as identifiers. The KC username of the submitter of the KCWorks record is stored in the `person_or_org.identifiers` property of the `creators` array (`creators[0].person_or_org.identifiers.identifier`) using the scheme `kc_username`. + +For details on how to use KC usernames in KCWorks, see the section on [Metadata.creators](#metadata-creators-metadata-contributors) below. + +#### GND + +KCWorks also supports the Integrated Authority File (GND) identifier scheme (https://www.dnb.de/EN/Professionell/Standardisierung/GND/gnd_node.html). The GND identifier of the submitter of the KCWorks record is stored in the `person_or_org.identifiers` property of the `creators` array (`creators[0].person_or_org.identifiers.identifier`) using the scheme `gnd`. + +#### ISNI + +KCWorks also supports the ISNI identifier scheme (https://isni.org/). The ISNI of the submitter of the KCWorks record is stored in the `person_or_org.identifiers` property of the `creators` array (`creators[0].person_or_org.identifiers.identifier`) using the scheme `isni`. ### Organizations -#### ROR +#### ROR (recommended) -The Research Organization Registry (https://ror.org/) is used for the `organizations` field. +Organization identifiers can appear in the `creators` and `contributors` arrays, either for organizational creators/contributors or in the `affiliations` array of a personal creator/contributor. These fields *may* identify an organization using its id in Research Organization Registry (https://ror.org/) using the scheme `ror`, although free text names are also supported. +#### Grid (deprecated) -## Notes about Implementation of Core InvenioRDM Fields +KCWorks also supports the Grid identifier scheme (https://www.grid.ac/) for organizations using the scheme `grid`. This scheme is deprecated in favour of ROR, however, and should not be used for new identifiers. + +#### GND + +KCWorks also supports the Integrated Authority File (GND) identifier scheme (https://www.dnb.de/EN/Professionell/Standardisierung/GND/gnd_node.html) for organizations using the scheme `gnd`. + +### Funders + +#### DOI + +Funders in the `metadata.funding` array can be identified using DOIs formed with a FundRef id and the scheme `doi`. + +#### OFR + +Funders in the `metadata.funding` array can also be identified using the Open Funder Registry (https://openfunder.org/) identifiers and the scheme `ofr`. + +## KCWorks Implementation of Core InvenioRDM Fields ### metadata.subjects @@ -332,9 +588,14 @@ Example: } ``` -### KCWorks Custom Fields (kcworks/site/metadata_fields) +## KCWorks Custom Fields (kcworks/site/metadata_fields) + +Beyond the standard InvenioRDM metadata fields, KCWorks adds a number of custom metadata fields to the schema using InvenioRDM's custom field mechanism. These are all located in the top-level `custom_fields` field of the record metadata. They are prefixed with two different namespaces: + +- `kcr`: custom fields that are used to store data from the KC system. These fields **may** be used for new data, but are not required. +- `hclegacy`: custom fields that are used to store data from the legacy CORE repository. These fields **must not** be used for new data. -#### kcr:ai_usage +### kcr:ai_usage Type: `Object[boolean, string]` @@ -350,7 +611,7 @@ Example: } ``` -#### kcr:media +### kcr:media Type: `Array[string]` @@ -363,7 +624,7 @@ Example: } ``` -#### kcr:commons_domain +### kcr:commons_domain Type: `string` @@ -376,7 +637,7 @@ Example: } ``` -#### kcr:chapter_label +### kcr:chapter_label Type: `string` @@ -389,7 +650,7 @@ Example: } ``` -#### kcr:content_warning +### kcr:content_warning Type: `string` @@ -402,7 +663,7 @@ Example: } ``` -#### kcr:course_title +### kcr:course_title Type: `string` @@ -415,7 +676,7 @@ Example: } ``` -#### kcr:degree +### kcr:degree Type: `string` @@ -428,7 +689,7 @@ Example: } ``` -#### kcr:discipline +### kcr:discipline Type: `string` @@ -445,7 +706,7 @@ Example: } ``` -#### kcr:edition +### kcr:edition Type: `string` @@ -458,7 +719,7 @@ Example: } ``` -#### kcr:meeting_organization +### kcr:meeting_organization Type: `string` @@ -471,7 +732,7 @@ Example: } ``` -#### kcr:project_title +### kcr:project_title Type: `string` @@ -484,7 +745,7 @@ Example: } ``` -#### kcr:publication_url +### kcr:publication_url Type: `string` (URL) @@ -499,7 +760,7 @@ Example: } ``` -#### kcr:sponsoring_institution +### kcr:sponsoring_institution Type: `string` @@ -514,7 +775,7 @@ Example: } ``` -#### kcr:submitter_email +### kcr:submitter_email Type: `string` (email address) @@ -527,7 +788,7 @@ Example: } ``` -#### kcr:submitter_username +### kcr:submitter_username Type: `string` @@ -540,7 +801,7 @@ Example: } ``` -#### kcr:institution_department +### kcr:institution_department Type: `string` @@ -553,7 +814,7 @@ Example: } ``` -#### kcr:book_series +### kcr:book_series Type: `Object[string, string]` @@ -570,7 +831,7 @@ Example: } ``` -#### kcr:user_defined_tags +### kcr:user_defined_tags Type: `Array[string]` @@ -586,14 +847,14 @@ Example: } ``` -#### kcr:commons_search_recid (system field) +### kcr:commons_search_recid (system field) This field is used to store the persistent identifier for the KCWorks record in the KC central search index. > [!Warning] > This field is automatically generated by the `invenio-remote-api-provisioner` service when a KCWorks record is published. It *must not* be set by the user. -#### kcr:commons_search_updated (system field) +### kcr:commons_search_updated (system field) Type: `string` (ISO 8601 datetime string) @@ -602,11 +863,11 @@ This field stores the date and time when the KCWorks record was last updated in > [!Warning] > This field is automatically generated by the `invenio-remote-api-provisioner` service when a KCWorks record is published. It *must not* be set by the user. -### HC Legacy Custom Fields +## HC Legacy Custom Fields The `hclegacy` namespace is used for custom fields that are used to store data from the legacy CORE database. These fields should not be used for new data. -#### custom_fields.hclegacy:groups_for_deposit +### custom_fields.hclegacy:groups_for_deposit Type: `Array[Object[string, string]]` @@ -624,7 +885,7 @@ Example: } ``` -#### custom_fields.hclegacy:collection +### custom_fields.hclegacy:collection Type: `string` @@ -637,7 +898,7 @@ Example: } ``` -#### custom_fields.hclegacy:committee_deposit +### custom_fields.hclegacy:committee_deposit Type: `integer` @@ -650,7 +911,7 @@ Example: } ``` -#### custom_fields.hclegacy:file_location +### custom_fields.hclegacy:file_location Type: `string` @@ -663,7 +924,7 @@ Example: } ``` -#### custom_fields.hclegacy:file_pid +### custom_fields.hclegacy:file_pid Type: `string` @@ -676,7 +937,7 @@ Example: } ``` -#### custom_fields.hclegacy:previously_published +### custom_fields.hclegacy:previously_published Type: `string` @@ -689,7 +950,7 @@ Example: } ``` -#### custom_fields.hclegacy:publication_type +### custom_fields.hclegacy:publication_type Type: `string` @@ -702,7 +963,7 @@ Example: } ``` -#### custom_fields.hclegacy:record_change_date +### custom_fields.hclegacy:record_change_date Type: `string` (ISO 8601 datetime string) @@ -715,7 +976,7 @@ Example: } ``` -#### custom_fields.hclegacy:record_creation_date +### custom_fields.hclegacy:record_creation_date Type: `string` (ISO 8601 datetime string) @@ -728,7 +989,7 @@ Example: } ``` -#### custom_fields.hclegacy:record_identifier +### custom_fields.hclegacy:record_identifier Type: `string` @@ -741,7 +1002,7 @@ Example: } ``` -#### custom_fields.hclegacy:submitter_org_memberships +### custom_fields.hclegacy:submitter_org_memberships Type: `array[string]` @@ -754,7 +1015,7 @@ Example: } ``` -#### custom_fields.hclegacy:submitter_affiliation +### custom_fields.hclegacy:submitter_affiliation Type: `string` @@ -767,7 +1028,7 @@ Example: } ``` -#### custom_fields.hclegacy:submitter_id +### custom_fields.hclegacy:submitter_id Type: `string` @@ -780,7 +1041,7 @@ Example: } ``` -#### custom_fields.hclegacy:total_views +### custom_fields.hclegacy:total_views Type: `integer` @@ -793,7 +1054,7 @@ Example: } ``` -#### custom_fields.hclegacy:total_downloads +### custom_fields.hclegacy:total_downloads Type: `integer` diff --git a/docs/build/_static/documentation_options.js b/docs/build/_static/documentation_options.js index 5abb8afc8..5ce2f0a14 100644 --- a/docs/build/_static/documentation_options.js +++ b/docs/build/_static/documentation_options.js @@ -1,5 +1,5 @@ const DOCUMENTATION_OPTIONS = { - VERSION: '0.3.3', + VERSION: '0.3.5', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/build/api.html b/docs/build/api.html new file mode 100644 index 000000000..7490bb91e --- /dev/null +++ b/docs/build/api.html @@ -0,0 +1,2093 @@ + + + + + + + + + API - Knowledge Commons Works 0.3.5 documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

API

+

KCWorks provides a robust REST API that allows clients to perform most operations on KCWorks records and collections.

+
+

The InvenioRDM REST API

+

KCWorks is built on top of InvenioRDM, which provides a REST API for creating, managing, and querying records. This API is documented at https://inveniordm.docs.cern.ch/reference/rest_api_index/.

+
+

Note: “Collections” are referred to as “communities” in the InvenioRDM API and its documentation. To avoid confusion with the social groups that are part of the Knowledge Commons network, KCWorks uses the term “collections” in its documentation and user interface. But operations involving collections are handled via the “communities” endpoint in the InvenioRDM REST API.

+
+

This REST API allows clients to retrieve and manage the following resources:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Resource

Supported Operations

Requires Authentication

Endpoint

InvenioRDM API documentation

draft works

read

yes

GET /api/records/{id}/draft

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-draft-record

create

yes

POST /api/records/

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#create-a-draft-record

update

yes

PUT /api/records/{id}

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#update-a-draft-record

publish

yes

POST /api/records/{id}/draft/actions/publish

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#publish-a-draft-record

delete

yes

DELETE /api/records/{id}/draft

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#delete-a-record

list files

yes

GET /api/records/{id}/draft/files

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#list-a-drafts-files

upload files[1]

yes

POST /api/records/{id}/draft/files

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#start-draft-file-uploads

view file metadata

yes

GET /api/records/{id}/draft/files/{filename}

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-draft-files-metadata

download file

yes

GET /api/records/{id}/draft/files/{filename}/content

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#download-a-draft-file

delete file

yes

DELETE /api/records/{id}/draft/files/{filename}

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#delete-a-draft-file

published works

read[2]

no

GET /api/records/

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-record

read all versions

no

GET /api/records/{id}/versions

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-all-versions

read latest version

no

GET /api/records/{id}/versions/latest

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-latest-version

search

no

GET /api/records/

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#search-records

update[3]

yes

POST /api/records/{id}/draft

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#update-a-draft-record

create new version

yes

POST /api/records/{id}/versions

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#create-a-new-version

attach files from a previous version[4]

yes

POST /api/records/{id}/draft/actions/files-import

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#link-files-from-previous-version

list files

no

GET /api/records/{id}/files

https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-all-files

collections

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_communities/

collection memberships

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_members/

reviews

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_reviews/

requests

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_requests/

users

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_users/

groups[5]

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_groups/

vocabularies

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_vocabularies/

names

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_names/

funders

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_funders/

awards

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_awards/

OAI-PMH sets

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_oaipmh_sets/

statistics

Documentation forthcoming

https://inveniordm.docs.cern.ch/reference/rest_api_statistics/

+
+

Note that for some operations where authentication is required, the client must also possess the appropriate permissions. (E.g., to edit a draft work, manage collection requests, etc.)

+

Note that several operations are NOT possible via the REST API, including:

+
    +
  • searching draft records

    +
      +
    • Draft records are by definition not intended to be distributed, and so are not discoverable via the search API until they have been published.

    • +
    +
  • +
  • creating a published work directly

    +
      +
    • Published works can only be created by first creating a draft work and then publishing it.

    • +
    +
  • +
  • deleting a published work

    +
      +
    • Published works are generally considered to be permanent and so cannot be deleted. If desired, access to a published work, or a version of a published work, can be set to “restricted”, in which case the work is no longer discoverable via the search API.

    • +
    +
  • +
  • modifying files for a published work

    +
      +
    • Again, published works are generally considered to be permanent and so their files cannot be modified. The only way to update the files associated with a published work is to create a new version of the work. If desired, access to the original version of the published work can be set to “restricted”, in which case only the new version and its files are discoverable.

    • +
    +
  • +
  • creating or modifying user accounts

    +
      +
    • The REST API endpoint for users is currently read-only. It is not possible to create or modify user accounts, or change and user profile information, via the REST API. These operations are handled via the KCWorks admin interface or via CLI commands.

    • +
    +
  • +
  • creating or modifying groups (permissions)

  • +
  • creating or modifying controlled vocabularies

  • +
+
+

Creating a new Work via the InvenioRDM REST API

+

Creating a new Work via the REST API requires several steps.

+
    +
  • Step 1: Create a draft record

  • +
  • Step 2: Initialize the file upload

  • +
  • Step 3: Upload the file content

    +
      +
    • This step must be repeated for each file being added to the work.

    • +
    +
  • +
  • Step 4: Commit the file upload

  • +
  • Step 5: Publish the draft record

  • +
+

If you want the work to be included in a collection at publication time, you must submit a request for the work to be published in the collection. The first four steps are the same as above, but in place of Step 5 (publication), you must submit a request for the work to be published in the collection.

+
    +
  • Step 5: Create a review request

  • +
  • Step 6: Submit review request

  • +
+

If the collection in question requires review before publication, the request will not be published until the review is accepted.

+
    +
  • Step 7: Accept and publish the record

  • +
+
+
+
+

Streamlined Import API

+

In order to streamline the process of uploading works to KCWorks, particularly for works intended for publication in a collection, KCWorks provides a streamlined import API. This API allows clients to upload a work and its files in a single step, without the need to create a draft record, initialize file uploads, commit file uploads, or submit a review request.

+

Why is this API needed? The InvenioRDM REST API can be fragile and difficult to use, particularly for clients who are not familiar with the system. The creation and acceptance of a review request is redundant where collection administrators are uploading works for a collection they administer. The file upload steps are also not truly stateless, introducing the possibility of a file upload being interrupted and left incomplete, even if the upload of the file’s content was successful.

+
+

Who can use the import API?

+

The import API is available to any registered active user who has obtained an OAuth token for API operations.

+

If the import is to include placing the work directly in a collection, without passing through the review process, the user must have sufficient permissions to publish directly in the collection. Exactly what role they must have in the collection (“owner”, “manager”, “curator”, “reader”) depends on the collection’s review policy.

+

The exception to this rule is for collection owners, who may override the collection’s review policy and import works directly into the collection without review.

+
+
+

The import request

+
+

Request

+
POST https://works.hcommons.org/api/import/ HTTP/1.1
+
+
+
+
+

Required headers

+
Content-Type: multipart/form-data
+Accept: application/json
+Authorization: Bearer \<your-api-key\>
+
+
+
+
+

Request body

+

This request must be made with a multipart/form-data request. The request body must include parts with following names:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Name

Required

Content Type

Description

files

yes

application/octet-stream

The (binary) file content to be uploaded. If multiple files are being uploaded, a body part with this same name (“files”) must be provided for each file. If more than three or four files are being uploaded, it is recommended to provide a single zip archive containing all of the files. The files will be assigned to the appropriate work based on filename, so where multiple files are provided these must be unique. If a zip archive is provided, the files must be contained in a single compressed folder with no subfolders.

metadata

yes

application/json

An array of JSON metadata objects, each of which will be used to create a new work. Each must following the KCWorks implementation of the InvenioRDM metadata schema described here. In addition, an array of owners for the work may optionally be provided by adding an access.owned_by property to each metadata object.

collection

no

text/plain

The ID (either the url slug or the UUID) of the collection to which the work should be published. If this value is provided, the work will be submitted to the collection immediately after import. If the collection requires review, the work will be placed in the collection’s review queue.

review_required

no

text/plain

A string representation of a boolean (either “true” or “false”) indicating whether the work should be reviewed before publication. This setting is only relevant if the work is intended for publication in a collection that requires review. It will override the collection’s usual review policy, since the work is being uploaded by a collection administrator. (Default: “true”)

strict_validation

no

text/plain

A string representation of a boolean (either “true” or “false”) indicating whether the import request should be rejected if any validation errors are encountered. If this value is “false”, the imported work will be created in KCWorks even if some of the provided metadata does not conform to the KCWorks metadata schema, provided these are not required fields. If this value is “true”, the import request will be rejected if any validation errors are encountered. (Default: “true”)

all_or_none

no

text/plain

A string representation of a boolean (either “true” or “false”) indicating whether the entire import request should be rejected if any of the works fail to be created (whether for validation errors, upload errors, or other reasons). If this value is “false”, the import request will be accepted even if some of the works cannot be created. The response in this case will include a list of works that were successfully created and a list of errors for the works that failed to be created. (Default: “true”)

+
+

The array of owners, if provided in a metadata object’s access.owned_by property, must include at least the full name and email address of the users to be added as owners of the work. If the user already has a Knowledge Commons account, their username should also be provided. Additional identifiers (e.g., ORCID) may be provided as well to help avoid duplicate accounts, since a KCWorks account will be created for each user if they do not already have one.

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +

key

required

type

description

full_name

yes

string

The full name of the user.

email

yes

string

The email address of the user.

identifiers

no

array

An array of identifiers for the user. Any identifier schemes supported by KCWorks will be accepted.

+
+

The resulting owners list should be shaped like this:

+
[
+    {
+        "full_name": "John Doe",
+        "email": "john.doe@example.com",
+        "identifiers": [
+            {
+                "identifier": "0000-0000-0000-0000",
+                "scheme": "orcid"
+            },
+            {
+                "identifier": "jdoe",
+                "scheme": "kc_username"
+            }
+        ]
+    }
+]
+
+
+

Note that it is not assumed that the creators of a work should be the work’s owners. The creators will only be added as owners if each of them is listed in the access.owned_by property of the work’s metadata object.

+
+
+
+

A successful import response

+
HTTP/1.1 201 Created
+Content-Type: application/json
+
+
+

This response will include a JSON object with the following fields:

+
    +
  • status: The status of the import request, which will be “success” if the import request was successful.

  • +
  • data: An array of JSON objects with the following fields:

  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

key

type

description

record_id

string

The ID of the new work.

record_url

string

The URL of the new work.

files

array

A list of the filenames for the files that were successfully uploaded. This is for convenience. Details about the files, including their size and checksum, are available in the files property of the metadata object.

collection_id

string

The ID of the collection to which the work was published, if any. This is provided for convenience. Details about the collection are available in the parent.communities property of the metadata object.

errors

array

A list of errors that occurred during the import process. These might include validation errors for certain fields in the provided metadata that did not prevent creation of the work. (Only provided if the request was made with strict_validation set to “false”.)

metadata

object

The metadata for the created work, in JSON format, following the KCWorks implementation of the InvenioRDM metadata schema described here.

+
+

The response object will be shaped like this:

+
{
+    "status": "success",
+    "data": [
+        {
+            "item_index": 0,
+            "record_id": "1234567890",
+            "record_url": "https://works.hcommons.org/records/1234567890",
+            "files": ["file1.pdf", "file2.pdf"],
+            "collection_id": "1234567890",
+            "errors": [],
+            "metadata": {
+                /* ... */
+            }
+        },
+        {
+            "item_index": 1,
+            "record_id": "1234567891",
+            "record_url": "https://works.hcommons.org/records/1234567891",
+            "files": ["file1.pdf", "file2.pdf"],
+            "collection_id": "1234567890",
+            "errors": [],
+            "metadata": {
+                /* ... */
+            }
+        }
+    ]
+}
+
+
+
+
+

An unsuccessful import response

+
+

The token does not have the necessary permissions

+
HTTP/1.1 403 Forbidden
+Content-Type: application/json
+
+
+

This response will include a JSON object:

+
{
+    "status": "error",
+    "message": "The user does not have the necessary permissions."
+}
+
+
+
+
+

The request metadata is malformed or invalid

+
HTTP/1.1 400 Bad Request
+Content-Type: application/json
+
+
+

This response is returned when some of the provided metadata for all of the works to be imported is malformed or invalid. This indicates that none of the works has been created and a new request must be made with corrected metadata. This response will only be received if either +a. the strict_validation request parameter was set to “true” and all of the supplied metadata objects raise validation errors, or +b. the strict_validation parameter is set to “false”, but the validation errors affected fields that are required for the works to be created. +c. the all_or_none request parameter is set to “true” and some of the supplied metadata objects raise validation errors.

+

The response will include a JSON object with the following fields:

+
{
+    "status": "error",
+    "message": "The request metadata is malformed or invalid.",
+    "errors": [
+        {
+            "item_index": 0,
+            "errors": [
+                {
+                    "field": "title",
+                    "message": "Required field missing."
+                }
+            ]
+        },
+        {
+            "item_index": 1,
+            "errors": [
+                {
+                    "field": "metadata.creators.0.occupation",
+                    "message": "Unknown field."
+                },
+                {
+                    "field": "metadata.publication_date",
+                    "message": "Date is not in Extended Date Time Format (EDTF)."
+                }
+            ]
+        }
+    ]
+}
+
+
+

If only some of the works to be imported are malformed or invalid, and the all_or_none request parameter is set to “false”, the response will be 207 Multi-Status.

+
+
+

The request file upload failed

+
HTTP/1.1 400 Bad Request
+Content-Type: application/json
+
+
+

If the file content is uploaded but for some reason is considered corrupted or invalid, a 400 Bad Request response will be returned. This response will include a JSON object with the following fields:

+
{
+    "status": "error",
+    "message": "The file content is corrupted or invalid.",
+    "errors": [
+        {
+            "item_index": 0,
+            "errors": [
+                {
+                    "file": "file1.pdf",
+                    "message": "The file size does not match the supplied metadata."
+                },
+                {
+                    "file": "file2.pdf",
+                    "message": "The file checksum does not match the supplied metadata."
+                }
+            ]
+        },
+        {
+            "item_index": 1,
+            "errors": [
+                {
+                    "file": "file3.pdf",
+                    "message": "The file exceeds the maximum file size."
+                }
+            ]
+        }
+    ]
+}
+
+
+

If an upload simply fails to complete and times out, the client will instead receive a 504 Gateway Timeout response.

+
+
+

Only some of the works to be imported failed

+
HTTP/1.1 207 Multi-Status
+Content-Type: application/json
+
+
+

If the all_or_none request parameter is set to “false”, it is possible that some of the works to be imported were successfully created and others were not. In this case, the response will be 207 Multi-Status and will include a JSON object with the following fields:

+
{
+    "status": "multi_status",
+    "data": {
+        "succeeded": [
+            {
+                "item_index": 0,
+                "record_id": "1234567890",
+                "record_url": "https://works.hcommons.org/records/1234567890",
+                "files": ["file1.pdf", "file2.pdf"],
+                "collection_id": "1234567890",
+                "errors": [],
+                "metadata": {
+                    /* ... */
+                }
+            },
+        ],
+        "failed": [
+            {
+                "item_index": 1,
+                "message": "The request metadata is malformed or invalid.",
+                "errors": [
+                    {
+                        "field": "title",
+                        "message": "Required field missing.",
+                    }
+                ]
+            },
+            {
+                "item_index": 2,
+                "message": "The file content is corrupted or invalid.",
+                "errors": [
+                    {
+                        "file": "file3.pdf",
+                        "message": "The file exceeds the maximum file size."
+                    }
+                ]
+            }
+        ]
+    }
+}
+
+
+
+
+
+

What happens to an import request that fails?

+

If all steps of an import request do not complete successfully, the work will not be created. The files that were successfully uploaded will be deleted, and any draft record created as part of the import request will be deleted. The client may attempt the import request again.

+
+
+

Making duplicate import requests

+

Note that it is possible to make duplicate import requests unless the work to be imported includes a pre-existing DOI identifier or some other unique identifier that has already been registered in KCWorks. In this case, the import request will be rejected with a 409 Conflict response code and a Location header pointing to the existing work.

+

In the absence of such a unique identifier, however, KCWorks will not try to detect duplicate works based on the metadata, file name, or file content. If the same work is imported multiple times without a pre-existing unique identifier, it will be created multiple times in KCWorks and each version will be assigned a newly minted DOI.

+
+
+
+

Group Collections API

+
https://works.hcommons.org/api/group_collections
+
+
+

The group_collections REST API endpoint allows a client to create, read, modify, or delete a collection in KCWorks owned and administered by a Knowledge Commons group. GET requests to retrieve information about group collections are open to all clients. POST, PUT, and DELETE requests are secured by an oauth token that must be obtained from the Knowledge Commons Works administrator.

+

This endpoint is not configured to receive all of the metadata required to create or modify group collections. Rather, the group_collections endpoint receives minimal signals from a Commons Instance and then obtains the full required metadata via an API callback to the Commons instance.

+
+

[!NOTE] +KCWorks uses the term “collection” in place of the default term “community” employed in other InvenioRDM installations. This is partly to accommodate exactly the integration with Knowledge Commons groups that is discussed here.

+
+
+

Group collection owner

+

InvenioRDM does not allow groups to be owners of a collection (community). When a collection is created for a group, though, we do not know which of the group’s administrators to assign as the individual owner. It is also awkward to change ownership of a collection later on if the group’s administrativer personnel change. So the collection is owned by an administrative user who is assigned the role group-collections-owner. The group’s administrators are then assigned privileges as “managers” of the group collection. This allows them to manage the collection’s settings and membership, but not to delete the collection or change its ownership.

+

Before the invenio_group_collections_kcworks module can be used, the administrator must create a role called group-collections-owner and assign membership in that role to one administrative user account. If multiple user accounts belong to that role, the first user account in the list will be assigned as the owner of group collections. If no user accounts belong to the role, the group collection creation will fail with a NoOwnerAvailable error.

+
+
+

Endpoint configuration

+

The configuration variable GROUP_COLLECTIONS_METADATA_ENDPOINTS must be provided in the invenio.cfg file in order to use this endpoint. This variable should hold a dictionary whose keys are Commons instance names. The value for each key is a dictionary containing the following keys:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +

key

value type

required

value

url

str

Y

The url on the Commons instance where a GET request can retrieve the metadata for a group. The url should include the placeholder {id} where the Commons instance id for the requested group should be placed.

token_name

str (upper case)

Y

The name of the environment variable that will hold the authentication token for requests to the Commons instance url for retrieving group metadata.

placeholder_avatar

str

N

The filename or last url component that identifies a placeholder avatar in the avatar image url supplied for the Commons group avatar.

+
+

A typical configuration might look like the following:

+
GROUP_COLLECTIONS_METADATA_ENDPOINTS = {
+    "knowledgeCommons": {
+            "url": "https://hcommons-dev.org/wp-json/commons/v1/groups/{id}",
+            "token_name": "COMMONS_API_TOKEN",
+            "placeholder_avatar": "mystery-group.png",
+    },
+}
+
+
+
+
+

Retrieving Group Collection Metadata (GET)

+

A GET request to this endpoint will retrieve metadata on Invenio collections +that are owned by a Commons group. A request to the bare endpoint without a +group ID or collection slug will return a list of all collections owned by +all Commons groups. (Commons Works collections not linked to a Commons group will not be included. If you wish to query all groups, please use the communities API endpoint.)

+
+

Query parameters

+

Four optional query parameters can be used to filter the results:

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

Parameter name

Description

commons_instance

the name of the Commons instance to which the group belongs. If this parameter is provided, the response will only include collections owned by groups in that instance.

commons_group_id

the ID of the Commons group. If this parameter is provided, the response will only include collections owned by that group.

collection

the slug of the collection. If this parameter is provided, the response will include only metadata for that collection.

page

the page number of the results

size

the number of results to include on each page

sort

the kind of sorting applied to the returned results

+
+
+
+

Sorting

+

The sort parameter can be set to one of the following sort types:

+
+ + + + + + + + + + + + + + + + + + + + +

Field name

Description

newest

Descending order based on created date

oldest

Ascending order based on created date

updated-desc

Descending order based on updated date

updated-asc

Ascending order based on updated date

+
+

By default the results are sorted by updated-desc

+
+
+

Pagination

+

Long result sets will be paginated. The response will include urls for the first, last, previous, and next pages of results in the link property of the response body. A url for the current page of results will also be included in the list as a self link. By default the page size is 25, but this can be changed by providing a value for the size query parameter.

+
+
+

Requesting all collections

+
+
Request
+
GET https://works.hcommons.org/api/group_collections HTTP/1.1
+
+
+
+
+
Successful Response Status Code
+

200 OK

+
+
+
Successful response body
+
{
+    "aggregations": {
+        "type": {
+            "buckets": [
+                {
+                    "doc_count": 50,
+                    "is_selected": false,
+                    "key": "event",
+                    "label": "Event",
+                },
+                {
+                    "doc_count": 50,
+                    "is_selected": false,
+                    "key": "organization",
+                    "label": "Organization",
+                },
+            ],
+            "label": "Type",
+        },
+        "visibility": {
+            "buckets": [
+                {
+                    "doc_count": 100,
+                    "is_selected": false,
+                    "key": "public",
+                    "label": "Public",
+                }
+            ],
+            "label": "Visibility",
+        },
+    },
+    "hits": {
+        "hits": [
+            {
+                "id": "5402d72b-b144-4891-aa8e-1038515d68f7",
+                "access": {
+                    "member_policy": "open",
+                    "record_policy": "open",
+                    "review_policy": "closed",
+                    "visibility": "public",
+                },
+                "children": {"allow": false},
+                "created": "2024-01-01T00:00:00Z",
+                "updated": "2024-01-01T00:00:00Z",
+                "links": {
+                    "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7",
+                    "self_html": "https://works.hcommons.org/communities/panda-group-collection",
+                    "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings",
+                    "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo",
+                    "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename",
+                    "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members",
+                    "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public",
+                    "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations",
+                    "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests",
+                    "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records",
+                    "featured": "https://works.hcommons.org/api/"
+                                "communities/"
+                                "5402d72b-b144-4891-aa8e-1038515d68f7/"
+                                "featured",
+                },
+                "revision_id": 1,
+                "slug": "panda-group-collection",
+                "metadata": {
+                    "title": "The Panda Group Collection",
+                    "curation_policy": "Curation policy",
+                    "page": "Information for the panda group collection",
+                    "description": "This is a collection about pandas.",
+                    "website": "https://works.hcommons.org/pandas",
+                    "organizations": [
+                        {
+                            "name": "Panda Research Institute",
+                        }
+                    ],
+                    "size": 100,
+                },
+                "deletion_status": {
+                    "is_deleted": false,
+                    "status": "P",
+                },
+                "custom_fields": {
+                    "kcr:commons_instance": "knowledgeCommons",
+                    "kcr:commons_group_description": "This is a group for panda research.",
+                    "kcr:commons_group_id": "12345",
+                    "kcr:commons_group_name": "Panda Research Group",
+                    "kcr:commons_group_visibility": "public",
+                },
+                "access": {
+                    "visibility": "public",
+                    "member_policy": "closed",
+                    "record_policy": "open",
+                    "review_policy": "open",
+                }
+            },
+            /* ... */
+        ],
+        "total": 100,
+    },
+    "links": {
+        "self": "https://works.hcommons.org/api/group_collections",
+        "first": "https://works.hcommons.org/api/group_collections?page=1",
+        "last": "https://works.hcommons.org/api/group_collections?page=10",
+        "prev": "https://works.hcommons.org/api/group_collections?page=1",
+        "next": "https://works.hcommons.org/api/group_collections?page=2",
+    }
+    "sortBy": "newest",
+    "order": "ascending",
+}
+
+
+
+
+
Successful Response Headers
+
+ + + + + + + + + + + +

Header name

Header value

Content-Type

application/json

+
+
+
+
+

Requesting collections for a Commons instance

+
+
Request
+
GET https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&sort=updated-asc HTTP/1.1
+
+
+
+
+
Successful response status code
+

200 OK

+
+
+
Successful Response Body:
+
{
+    "aggregations": {
+        "type": {
+            "buckets": [
+                {
+                    "doc_count": 45,
+                    "is_selected": false,
+                    "key": "event",
+                    "label": "Event",
+                },
+                {
+                    "doc_count": 45,
+                    "is_selected": false,
+                    "key": "organization",
+                    "label": "Organization",
+                },
+            ],
+            "label": "Type",
+        },
+        "visibility": {
+            "buckets": [
+                {
+                    "doc_count": 90,
+                    "is_selected": false,
+                    "key": "public",
+                    "label": "Public",
+                }
+            ],
+            "label": "Visibility",
+        },
+    },
+    "hits": {
+        "hits": [
+            {
+                "id": "5402d72b-b144-4891-aa8e-1038515d68f7",
+                "access": {
+                    "member_policy": "open",
+                    "record_policy": "open",
+                    "review_policy": "closed",
+                    "visibility": "public",
+                },
+                "children": {"allow": false},
+                "created": "2024-01-01T00:00:00Z",
+                "updated": "2024-01-01T00:00:00Z",
+                "links": {
+                    "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7",
+                    "self_html": "https://works.hcommons.org/communities/panda-group-collection",
+                    "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings",
+                    "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo",
+                    "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename",
+                    "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members",
+                    "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public",
+                    "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations",
+                    "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests",
+                    "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records",
+                    "featured": "https://works.hcommons.org/api/"
+                                "communities/"
+                                "5402d72b-b144-4891-aa8e-1038515d68f7/"
+                                "featured",
+                },
+                "revision_id": 1,
+                "slug": "panda-group-collection",
+                "metadata": {
+                    "title": "The Panda Group Collection",
+                    "curation_policy": "Curation policy",
+                    "page": "Information for the panda group collection",
+                    "description": "This is a collection about pandas.",
+                    "website": "https://works.hcommons.org/pandas",
+                    "organizations": [
+                        {
+                            "name": "Panda Research Institute",
+                        }
+                    ],
+                    "size": 100,
+                },
+                "deletion_status": {
+                    "is_deleted": false,
+                    "status": "P",
+                },
+                "custom_fields": {
+                    "kcr:commons_instance": "knowledgeCommons",
+                    "kcr:commons_group_description": "This is a group for panda research.",
+                    "kcr:commons_group_id": "12345",
+                    "kcr:commons_group_name": "Panda Research Group",
+                    "kcr:commons_group_visibility": "public",
+                },
+                "access": {
+                    "visibility": "public",
+                    "member_policy": "closed",
+                    "record_policy": "open",
+                    "review_policy": "open",
+                }
+            },
+            /* ... */
+        ],
+        "total": 90,
+    },
+    "links": {
+        "self": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons",
+        "first": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1",
+        "last": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=9",
+        "prev": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1",
+        "next": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=2",
+    }
+    "sortBy": "updated-asc",
+}
+
+
+
+
+
Successful response headers
+
+ + + + + + + + + + + + + + +

Header name

Header value

Content-Type

application/json

Link

<https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1>; rel="first", <https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=9>; rel="last", <https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1>; rel="prev", <https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=2>; rel="next"

+
+
+
+
+

Requesting collections for a specific group

+

Note that if you specify a commons_group_id value, you must also provide a commons_instance value. This is to avoid confusion if different Commons instances use the same internal id for groups.

+
+
Request
+
GET https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&commons_group_id=12345 HTTP/1.1
+
+
+
+
+
Successful response status code
+

200 OK

+
+
+
Successful Response Body:
+
{
+    "aggregations": {
+        "type": {
+            "buckets": [
+                {
+                    "doc_count": 2,
+                    "is_selected": false,
+                    "key": "event",
+                    "label": "Event",
+                },
+                {
+                    "doc_count": 2,
+                    "is_selected": false,
+                    "key": "organization",
+                    "label": "Organization",
+                },
+            ],
+            "label": "Type",
+        },
+        "visibility": {
+            "buckets": [
+                {
+                    "doc_count": 4,
+                    "is_selected": false,
+                    "key": "public",
+                    "label": "Public",
+                }
+            ],
+            "label": "Visibility",
+        },
+    },
+    "hits": {
+        "hits": [
+            {
+                "id": "5402d72b-b144-4891-aa8e-1038515d68f7",
+                "access": {
+                    "member_policy": "open",
+                    "record_policy": "open",
+                    "review_policy": "closed",
+                    "visibility": "public",
+                },
+                "children": {"allow": false},
+                "created": "2024-01-01T00:00:00Z",
+                "updated": "2024-01-01T00:00:00Z",
+                "links": {
+                    "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7",
+                    "self_html": "https://works.hcommons.org/communities/panda-group-collection",
+                    "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings",
+                    "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo",
+                    "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename",
+                    "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members",
+                    "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public",
+                    "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations",
+                    "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests",
+                    "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records",
+                    "featured": "https://works.hcommons.org/api/"
+                                "communities/"
+                                "5402d72b-b144-4891-aa8e-1038515d68f7/"
+                                "featured",
+                },
+                "revision_id": 1,
+                "slug": "panda-group-collection",
+                "metadata": {
+                    "title": "The Panda Group Collection",
+                    "curation_policy": "Curation policy",
+                    "page": "Information for the panda group collection",
+                    "description": "This is a collection about pandas.",
+                    "website": "https://works.hcommons.org/pandas",
+                    "organizations": [
+                        {
+                            "name": "Panda Research Institute",
+                        }
+                    ],
+                    "size": 2,
+                },
+                "deletion_status": {
+                    "is_deleted": false,
+                    "status": "P",
+                },
+                "custom_fields": {
+                    "kcr:commons_instance": "knowledgeCommons",
+                    "kcr:commons_group_description": "This is a group for panda research.",
+                    "kcr:commons_group_id": "12345",
+                    "kcr:commons_group_name": "Panda Research Group",
+                    "kcr:commons_group_visibility": "public",
+                },
+                "access": {
+                    "visibility": "public",
+                    "member_policy": "closed",
+                    "record_policy": "open",
+                    "review_policy": "open",
+                }
+            },
+            /* ... */
+        ],
+        "total": 4,
+    },
+    "links": {
+        "self": "https://works.hcommons.org/api/group_collections",
+        "first": "https://works.hcommons.org/api/group_collections?page=1",
+        "last": "https://works.hcommons.org/api/group_collections?page=1",
+        "prev": "https://works.hcommons.org/api/group_collections?page=1",
+        "next": "https://works.hcommons.org/api/group_collections?page=1",
+    }
+    "sortBy": "newest",
+}
+
+
+
+
+
Successful response headers
+
+ + + + + + + + + + + +

Header name

Header value

Content-Type

application/json

+
+
+
+
+

Requesting a specific collection

+

While other kinds of requests require query parameters, a request for metadata on a specific Commons Works collection can be made by simply adding the community’s slug to the end of the url path. Once again, this will only succeed for collections that are linked to a Commons instance group. Collections that exist independently on Knowledge Commons Works will not be found at the group_collections endpoint and should be requested at the communities endpoint instead.

+
+
Request
+
GET https://works.hcommons.org/api/group_collections/my-collection-slug HTTP/1.1
+
+
+
+
+
Successful Response Status Code
+

200 OK

+
+
+
Successful Response Body:
+
{
+    "id": "5402d72b-b144-4891-aa8e-1038515d68f7",
+    "created": "2024-01-01T00:00:00Z",
+    "updated": "2024-01-01T00:00:00Z",
+    "links": {
+        "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7",
+        "self_html": "https://works.hcommons.org/communities/panda-group-collection",
+        "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings",
+        "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo",
+        "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename",
+        "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members",
+        "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public",
+        "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations",
+        "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests",
+        "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records",
+        "featured": "https://works.hcommons.org/api/"
+                    "communities/"
+                    "5402d72b-b144-4891-aa8e-1038515d68f7/"
+                    "featured",
+    },
+    "revision_id": 1,
+    "slug": "panda-group-collection",
+    "metadata": {
+        "title": "The Panda Group Collection",
+        "curation_policy": "Curation policy",
+        "page": "Information for the panda group collection",
+        "description": "This is a collection about pandas.",
+        "website": "https://works.hcommons.org/pandas",
+        "organizations": [
+            {
+                "name": "Panda Research Institute",
+            }
+        ],
+        "size": 100,
+    },
+    "deletion_status": {
+        "is_deleted": false,
+        "status": "P",
+    },
+    "custom_fields": {
+        "kcr:commons_instance": "knowledgeCommons",
+        "kcr:commons_group_description": "This is a group for pandas research.",
+        "kcr:commons_group_id": "12345",
+        "kcr:commons_group_name": "Panda Research Group",
+        "kcr:commons_group_visibility": "public",
+    },
+    "access": {
+        "visibility": "public",
+        "member_policy": "closed",
+        "record_policy": "open",
+        "review_policy": "open",
+    }
+}
+
+
+
+
+
+
+

Creating a Collection for a Group (POST)

+

A POST request to this endpoint creates a new collection in Invenio owned by the specified Commons group. If the collection is successfully created, the response status code will be 201 Created, and the response body will be a JSON object containing the URL slug for the newly created collection.

+

The POST request will trigger a callback to the Commons instance to get the metadata for the specified group, using the configuration dictionary declared in the GROUP_COLLECTIONS_METADATA_ENDPOINTS config variable, under the key matching the Commons instance’s SAML IDP provider name (declared in the SSO_SAML_IDPS config variable). This callback request will be sent to the “url” specified in the configuration dictionary (e.g., GROUP_COLLECTIONS_METADATA_ENDPOINTS["knowledgeCommons"]["url"]). This request will be authenticated using the environment variable whose name matches the token_name from the same configuration dictionary. The metadata from this callback request will then be used to populate the collection metadata in Invenio.

+

If the metadata returned from the Commons instance includes a url for an avatar, that avatar will be downloaded and stored in the Invenio instance’s file storage. Since we do not want to use a placeholder avatar for the group, the instance’s configuration can include a placeholder_avatar key. If the file name or last segment of the supplied avatar url matches this placeholder_avatar value, it will be ignored.

+
+

Permissions and access in newly created collections

+

By default, the newly created collection will have the following access settings:

+
    +
  • Visibility: “public”

  • +
  • Member visibility: “public”

  • +
  • Member policy: “closed”

  • +
  • Record policy: “closed”

  • +
  • Review policy: “closed”

  • +
+

They will appear in search results and be visible to non-members of the collection. But users who are not group members will not be able to request membership, and all submissions to the group will be held for review by the collection curators.

+

The collection’s administrators can change these settings in the collection’s settings page.

+
+
+

Handling group name changes

+

Note that when a collection is created for a group, the collection’s slug will be generated from the group’s name. If the group’s name is changed in the Commons instance, the collection’s slug will not be automatically updated. This is to avoid breaking links to the collection. If the group’s name is changed, the collection’s slug will remain the same, but the collection’s metadata will be updated to reflect the new group name.

+
+
+

Handling collection name collisions

+

It is possible for two groups on Commons instances to share the same human readable name, even though their ids are different. Knowledge Commons Works will allow multiple collections to share identical human readable names, but group url slugs must be unique across all KC Works collections. So where group names collide, only the first of the identically-named collections will have its slug generated normally. Susequent collections with the same name will have a numerical disambiguator appended to the end of their slugs. So if we have three groups named “Panda Studies,” the first collection created for one of the groups will have the slug panda-studies. The other collections created by these groups will be assigned the slugs panda-studies-1 and panda-studies-2, in order of their creation in Knowledge Commons Works.

+
+
+

Handling deleted group collections

+

If a group collection is deleted, its slug will be reserved in the Invenio PID store and cannot be re-used for a new collection. If a new collection is created for the same group, the slug will have a numerical disambiguator appended to the end, exactly as in cases of group name collision. E.g., if the group panda-studies were deleted earlier, a request to create a new collection for the “Panda Studies” group would be assigned the URL slug panda-studies-1. This is to avoid breaking links to the deleted collection.

+

In future it may be possible to restore deleted collections, but this is not currently implemented.

+ +
+
+

Request

+
POST https://works.hcommons.org/api/group_collections HTTP/1.1
+
+
+

Required request headers:

+
+ + + + + + + + + + + + + + +

Header name

Header value

Content-Type

application/json

Authorization

Bearer <token>

+
+
+
+

Request body

+

The request body must be a JSON object with the following fields:

+
+ + + + + + + + + + + + + + + + + + + + + +

Field name

Required

Description

commons_instance

Y

The name of the Commons instance to which the group belongs. This must be the same string used to identify the instance in the GROUP_COLLECTIONS_METADATA_ENDPOINTS config variable.

commons_group_id

Y

The ID of the Commons group that will own the collection.

collection_visibility

N

The visibility setting for the collection to be created. Must be either “public” or “restricted”. [default: “restricted”]

+
+

The resulting request body will be shaped like this:

+
{
+    "commons_instance": "knowledgeCommons",
+    "commons_group_id": "12345",
+    "collection_visibility": "public",
+}
+
+
+
+
+

Successful response status code

+

201 Created

+
+
+

Successful response body

+
{
+    "commons_group_id": "12345",
+    "collection_slug": "new-collection-slug"
+}
+
+
+
+
+

Unsuccessful response codes

+
    +
  • 400 Bad Request: The request body is missing required fields or contains +invalid data.

  • +
  • 404 Not Found: The specified group could not be found by the callback to the Commons instance.

  • +
  • 403 Forbidden: The request is not authorized to modify the collection.

  • +
  • 409 Conflict: A collection already exists in Knowledge Commons Works linked to the specified group.

  • +
+
+
+
+

Changing the Group Ownership of a Collection (PATCH)

+

[!WARNING] +PATCH requests to change group ownership of the collection are not yet implemented.

+

A PATCH request to this endpoint modifies an existing collection in Invenio by changing the Commons group to which it belongs. This is the only modification that can be made to a collection via this endpoint. Other modifications to Commons group metadata should be handled by signalling the Invenio webhook for commons group metadata updates. Modifications to internal metadata or settings for the Invenio collection should be made view the Invenio “communities” API or the collection settings UI.

+

Note that the collection memberships in Invenio will be automatically transferred to the new Commons group. The corporate roles for the old Commons group will be removed from the collection and corporate roles for the new Commons group will be added to its membership with appropriate permissions. But any individual memberships that have been granted through the Invenio UI will be left unchanged. If the new collection administrators wish to change these individual memberships, they will need to do so through the Invenio UI.

+
+

Request

+
PATCH https://works.hcommons.org/api/group_collections/my-collection-slug HTTP/1.1
+
+
+

Required request headers:

+
+ + + + + + + + + + + + + + +

Header name

Header value

Content-Type

application/json

Authorization

Bearer <token>

+
+
+
+

Request body

+
{
+    "commons_instance": "knowledgeCommons",
+    "old_commons_group_id": "12345",
+    "new_commons_group_id": "67890",
+    "new_commons_group_name": "My Group",
+    "collection_visibility": "public",
+}
+
+
+
+
+

Successful response status code

+

200 OK

+
+
+

Successful response body

+
{
+    "collection": "my-collection-slug"
+    "old_commons_group_id": "12345",
+    "new_commons_group_id": "67890",
+}
+
+
+
+
+

Unsuccessful response codes

+
    +
  • 400 Bad Request: The request body is missing required fields or contains +invalid data.

  • +
  • 404 Not Found: The collection does not exist.

  • +
  • 403 Forbidden: The request is not authorized to modify the collection.

  • +
  • 304 Not Modified: The collection is already owned by the specified +Commons group.

  • +
+
+
+
+

Deleting a Group’s Collection (DELETE)

+

A DELETE request to this endpoint deletes a collection in Invenio owned by the specified Commons group. Note that the request must include all of:

+
    +
  • the collection slug as the url path parameter

  • +
  • the identifier of the Commons instance to which the group belongs, in the commons_instance query parameter

  • +
  • the Commons identifier of the group which owns the collection, in the commons_group_id query parameter

  • +
+

If any of these is missing the request will fail with a 400 Bad Request error. This is to ensure that collections are not deleted accidentally or by agents without authorization.

+

If the collection is successfully deleted, the response status code will be 204 No Content.

+

[!NOTE] +Once a group collection has been deleted, its former URL slug is still registered in Invenio’s PID store and reserved for the (now deleted) collection. Subsequent requests to create a collection for the same group cannot re-use the same URL slug. Instead the new slug will have a numerical disambiguator added to the end, exactly as in cases of group name collision. E.g., if the group panda-studies were deleted earlier, a request to create a new collection for the “Panda Studies” group would be assigned the URL slug panda-studies-1.

+

[!NOTE] +Group collections are soft deleted and can in principle be restored within a short period after the delete signal has been sent. Eventually, though, the soft deleted collection records will be +automatically purged entirely from the database. There is also no API mechanism for restoring them. So delete operations should be regarded as permanent and irrevocable.

+
+

Request

+
DELETE https://works.hcommons.org/api/group_collections/my-collection-slug?commons_instance=knowledgeCommons&commons_group_id=12345 HTTP/1.1
+
+
+

Required request headers:

+
+ + + + + + + + + + + + + + +

Header name

Header value

Content-Type

application/json

Authorization

Bearer <token>

+
+
+
+

Successful response status code

+

204 No Content

+
+
+

Unsuccessful response codes

+
    +
  • 400 Bad Request: The request did not include the required parameters or the parameters are not well formed.

  • +
  • 403 Forbidden: The requesting agent is not authorized to delete the collection. The collection may not belong to the Commons instance making the request, or it may not belong to the specified Commons group.

  • +
  • 404 Not Found: The collection does not exist.

  • +
  • 422 UnprocessableEntity: The deletion could not be performed because the

  • +
+
+
+
+
+

User and Group Data Updates (Internal Only)

+
https://works.hcommons.org/api/webhooks/user_data_update
+
+
+
+

[!WARNING] +This API endpoint is intended for internal use only. It is not intended to be used by clients outside of the Knowledge Commons system.

+
+
+

[!NOTE] +This API was implemented with a distributed network of independent Commons instances in mind. Currently, only the Knowledge Commons instance exists and is supported as a SAML IDP by KCWorks.

+
+

The api endpoint /api/webhooks/user_data_update is provided for Knowledge Commons applications and instances to signal that user or group metadata has been changed. These endpoints do not receive the actual updated data. They only receive notices that the metadata for a user or group has changed. KCWorks will then query the Commons instance’s endpoint to retrieve current metadata for the user or group.

+
+

User/Groups Metadata updates and SAML authentication

+

It is assumed that Commons instances have registered a SAML authentication IDP with KCWorks. The Commons identifiers for users in metadata update signals must be the same identifiers provided by the instance’s SAML IDP. This allows KCWorks to reliably identify the correct KCWorks user account, even if the same identifier happens to be used internally by multiple Commons instances. It also allows KCWorks to store Commons instance user ids in one central place within KCWorks, minimizing the chances of those links between a Commons instance user account and a KCWorks user account becoming corrupted.

+
+
+

GET requests

+

A GET request to this endpoint can be used to check that the endpoint is available and receiving messages. The response should have a 200 status code and should carry the following JSON response body:

+
{
+	"message": "Webhook receiver is active",
+	"status": 200,
+}
+
+
+
+
+

POST requests

+
+

Payload objects

+

Update notices should be sent via a POST request with a JSON payload object shaped like this:

+
{
+	"idp": "knowledgeCommons",
+	"updates": {
+		"users": [
+			{"id": "myusername", "event": "updated"},
+			{"id": "anotherusername", "event": "created"},
+		],
+		"groups": [{"id": "1234", "event": "updated"}],
+	},
+},
+
+
+

Top level payload object properties:

+
+ + + + + + + + + + + + + + + + + + + + +

Property

Type

Description

Required

idp

string

The name used by KCWorks to identify the identity provider the Commons instance has registered with KCWorks. This id should have been provided by the KCWorks administrators when the Commons instance’s IDP connection was established. For Knowledge Commons the value is knowledgeCommons

Y

updates

object

This object identifies the metadata updates that have taken place on the Commons instance. It allows updates of different kinds and for multiple entities to be signalled in a single request. Its properties are described below.

Y

+
+

updates object properties:

+
+ + + + + + + + + + + + + + + + + + + + +

Property

Type

Description

Required

users

array

An array of objects each representing one metadata change event for a single user.

N

groups

array

An array of objects each representing one metadata change for a single group.

N

+
+

NOTE: A valid payload must provide either a users array or a groups array with at least one member. Requests providing neither users nor groups, or providing only empty arrays, will result in an error response.

+

users and groups object properties

+
+ + + + + + + + + + + + + + + + + + + + +

Property

Type

Description

Required

id

number

The local identifier of the user or group on the Commons instance. This must be the same identifier that can be used to retrieve the entity’s metadata at the corresponding endpoint on the Commons instance.

Y

event

string

The nature of the metadata change for the entity. Must be one of updated, created, or deleted. The updated and deleted event types should be sent when an entity is first created or is deleted entirely from the Commons instance. These will trigger the creation or deletion of corresponding entities (a user or a group) on KC Works. All other metadata changes are updated events.

Y

+
+

NOTE: A valid payload’s user and/or group objects must each include both an id and an event value.

+
+
+

Event timing

+

There may be some delay between KC Works’ receiving an update signal and the updating of the corresponding entity’s metadata in KC Works. The actual updates are handled by background workers and in some cases there may be a slight delay before a worker is free. Usually this will only be a fraction of a second, but if intensive background tasks (like indexing) are ongoing it could be several minutes. The update also depends on a successful callback request from KC Works to the Commons instance’s endpoint for serving user or group metadata. If that request fails, it is possible for an update to fail even though the webhook signal was received successfully.

+
+
+

Success responses

+

If a signal is received successfully, the response will have a status of 202 and carry a JSON response object shaped like this:

+
{
+	"message": "Webhook received",
+	"status": 202,
+	"updates": {
+		"users": [
+			{"id": "myusername", "event": "updated"},
+			{"id": "anotherusername", "event": "created"},
+		],
+		"groups": [{"id": "1234", "event": "updated"}],
+	}
+}
+
+
+

The updates object should be identical to the updates object provided in the POST request. This confirms that the correct events have all been received and are being sent for processing.

+
+
+

Error responses

+

If multiple update signals are received in one POST request, it is possible that only some of the updates can be processed. The request might, for example, provide updated event signals for a number of entities, some of whose ids do not exist in KC Works. In this case the response code will be 207 Multi-Status and the response payload will be a JSON object

+
+ +
+
+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/build/architecture.html b/docs/build/architecture.html new file mode 100644 index 000000000..347ab7783 --- /dev/null +++ b/docs/build/architecture.html @@ -0,0 +1,769 @@ + + + + + + + + + KCWorks Architecture - Knowledge Commons Works 0.3.5 documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+
+

KCWorks Architecture

+
+

InvenioRDM’s Layered Architecture

+

InvenioRDM employs a layered architecture with:

+
    +
  1. Data layer

    +
      +
    • Low-level data storage and retrieval.

      +
        +
      • Primarily SQLAlchemy model classes.

      • +
      +
    • +
    • High-level data API classes that provide a Pythonic interface to the data layer.

      +
        +
      • Validate data before storing it.

      • +
      +
    • +
    +
  2. +
  3. Service layer

    +
      +
    • Retrieves and modifies data from the data layer, either for a view or for another service.

      +
        +
      • Providing abstract CRUD methods for operating on the data layer’s API classes.

      • +
      • Providing abstracted “result items” and “result lists”

      • +
      +
    • +
    • Enforces permission and access control policies.

    • +
    +
  4. +
  5. View layer

    +
      +
    • Consists of

      +
        +
      • Flask views (registered as Blueprints)

      • +
      • rendering either

        +
          +
        • Jinja2 templates to produce HTML

        • +
        • JSON to produce API responses

        • +
        +
      • +
      • in some cases, React components embedded in the Jinja2 templates

        +
          +
        • These are rendered on the client side

        • +
        • Data is passed from the Jinja2 templates to the React components via HTML data attributes

        • +
        +
      • +
      +
    • +
    +
  6. +
+
+
+

InvenioRDM Services

+

An InvenioRDM service is a class that provides methods for interacting with the data layer. The business logic of the service is usually delegated to one or more component classes, which are called during the service’s methods.

+
+

Service Classes

+
+

BaseService

+

The base Service class is defined in invenio_records_resources.services.base.Service. It defines methods for:

+
    +
  • Getting the service ID

    +
      +
    • id(self): Return the id of the service from config.

    • +
    +
  • +
  • Permissions checking

    +
      +
    • permission_policy(self, action_name, **kwargs): Factory for a permission policy instance.

    • +
    • check_permission(self, identity, action_name, **kwargs): Check a permission against the identity.

    • +
    • require_permission(self, identity, action_name, **kwargs): Require a specific permission from the permission policy.

    • +
    +
  • +
  • Handling service components

    +
      +
    • components(self): Return initialized instances of the service’s component classes.

    • +
    • run_components(self, action, *args, **kwargs): Run components for a given action.

    • +
    +
  • +
  • Producing result items and lists

    +
      +
    • result_item(self, *args, **kwargs): Create a new instance of the resource unit, i.e. whatever the service provides.

    • +
    • result_list(self, *args, **kwargs): Create a new list of resource units. In some cases this is a simple iterable of resource units, but in other cases it is a more complex object that includes additional data.

    • +
    +
  • +
+
+
+

RecordService

+

Services dealing with InvenioRDM records of some kind (e.g. records, drafts, communities, etc.) inherit from the RecordService class defined in invenio_records_resources.services.records.service. This class adds:

+
    +
  • properties and methods related to the service’s related data-layer API class

    +
      +
    • A schema property that returns a ServiceSchemaWrapper instance.

    • +
    • A record_cls property that returns the record class for the service.

    • +
    • A links_item_tpl property that returns a LinksTemplate instance for constructing links to a resource unit.

    • +
    • An expandable_fields property that returns a list of expandable fields for the service’s data-layer API class.

    • +
    +
  • +
  • Methods for creating searches

    +
      +
    • create_search(self, identity, record_cls, search_opts, permission_action="read", preference=None, extra_filter=None, versioning=True): Instantiate a search class.

    • +
    • search_records(self, identity, params, **kwargs): A low-level method to create an OpenSearch DSL instance for searching records.

    • +
    • search(self, identity, params=None, search_preference=None, expand=False, **kwargs): A high-level method to search for records matching the querystring.

    • +
    • scan(self, identity, params=None, search_preference=None, expand=False, **kwargs): A high-level method to perform a rolling “scroll” search for records matching the querystring. (This is used for searching through large numbers of records, since OpenSearch will not return more than 10,000 records at a time.)

    • +
    +
  • +
  • Methods for indexing records

    +
      +
    • reindex(self, identity, params=None, search_preference=None, search_query=None, extra_filter=None, **kwargs): A high-level method to reindex records matching the query parameters.

    • +
    • rebuild_index(self, identity, uow=None): A high-level method to reindex all records managed by this service.

    • +
    +
  • +
  • CRUD methods

    +
      +
    • create(self, identity, data, uow=None, expand=False): Create a record.

    • +
    • exists(self, identity, id_): Check if the record exists and user has permission. (Does not use the search index.)

    • +
    • read(self, identity, id_, expand=False, action="read"): Retrieve a record. (Does not use the search index.)

    • +
    • read_many(self, identity, ids, expand=False, action="read"): Retrieve multiple records using the search index.

    • +
    • read_all(self, identity, params=None, search_preference=None, expand=False, **kwargs): Retrieve all records matching the query parameters using the search index.

    • +
    • update(self, identity, id_, data, uow=None, expand=False): Update a record.

    • +
    • delete(self, identity, id_, uow=None): Delete a record.

    • +
    +
  • +
  • Helper methods for record management

    +
      +
    • check_revision_id(self, record, expected_revision_id): Validate the given revision_id with current record’s one.

    • +
    • on_relation_update(self, identity, record_type, records_info, notif_time, limit=100): Handles the update of a related field record when the related field is updated.

    • +
    +
  • +
+
+
+

Augmented RecordService

+

The invenio_drafts_resources package then overrides this with a RecordService class that adds (a) a distinction between published and draft records, (b) record versioning and a parent-child record relationship, and (c) file attachments to service records. This adds the following properties and methods to the RecordService class:

+
    +
  • Properties and methods for draft records

    +
      +
    • draft_cls(self): Return the record class for the service.

    • +
    • draft_files(self): Return the draft files service for the service.

    • +
    • draft_indexer(self): A factory for creating an indexer instance.

    • +
    • search_drafts(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs): Search for draft records matching the querystring.

    • +
    • read_draft(self, identity, id_, expand=False): Retrieve a draft record.

    • +
    • update_draft(self, identity, id_, data, revision_id=None, uow=None, expand=False): Replace a draft.

    • +
    • edit(self, identity, id_, uow=None, expand=False): Creates a new revision of a draft or a draft for an existing published record.

    • +
    • publish(self, identity, id_, uow=None, expand=False): Publishes a draft record.

    • +
    • delete_draft(self, identity, id_, revision_id=None, uow=None): Deletes a draft record. (Defaults to a soft delete, so the record is not actually deleted from the database or search index until a later cleanup operation.)

    • +
    • validate_draft(self, identity, id_, ignore_field_permissions=False): Validate a draft.

    • +
    • cleanup_drafts(self, timedelta, uow=None, search_gc_deletes=60): Hard delete of soft deleted drafts.

    • +
    +
  • +
  • Properties and methods for files

    +
      +
    • files(self): Return the files service for the service.

    • +
    • import_files(self, identity, id_, uow=None): Import files from previous record version.

    • +
    +
  • +
  • Properties and methods for versions and parent records

    +
      +
    • schema_parent(self): Return the parent schema for the service.

    • +
    • search_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read", **kwargs): Search for record’s versions.

    • +
    • read_latest(self, identity, id_, expand=False): Retrieve the latest version of a record.

    • +
    • new_version(self, identity, id_, uow=None, expand=False): Creates a new version of a record. +This overridden RecordService class also modifies the CRUD methods to enforce a workflow in which records are only modified via their draft records. This involves overriding:

    • +
    +
  • +
  • update(self, identity, id_, data, uow=None, expand=False): Now raises a NotImplementedError error.

  • +
  • create(self, identity, data, uow=None, expand=False): Now creates a draft record.

  • +
  • rebuild_index(self, identity): Now reindexes all draft records (instances of draft API class) as well as all published records (instances of record API class) and skips soft-deleted records.

  • +
+
+
+

RDMRecordService

+

The invenio_rdm_records package provides an RDMRecordService class that inherits from the RecordService class and adds:

+
    +
  • Additional properties for accessing subservices

    +
      +
    • access: Return the access service for the service.

    • +
    • pids: Return the PIDs service for the service.

    • +
    • review: Return the review service for the service.

    • +
    +
  • +
  • Methods for embargo handling

    +
      +
    • lift_embargo(self, identity, _id, uow=None): Lifts an embargo from the record and draft (if exists).

    • +
    • scan_expired_embargos(self, identity): Scan for records with an expired embargo.

    • +
    +
  • +
  • Properties and methods for file quota handling

    +
      +
    • schema_quota: Return the schema for quota information.

    • +
    • set_quota(self, identity, id_, data, files_attr="files", uow=None): Set the quota values for a record.

    • +
    • set_user_quota(self, identity, id_, data, uow=None): Set the user files quota.

    • +
    +
  • +
  • Properties and methods for deletion of published records

    +
      +
    • schema_tombstone: Return the schema for tombstone information.

    • +
    • delete_record(self, identity, id_, data, expand=False, uow=None, revision_id=None): Re-introduces soft-deletion of published records (which were previously removed by the RecordService class).

    • +
    • update_tombstone(self, identity, id_, data, expand=False, uow=None): Update the tombstone information for the (soft) deleted record.

    • +
    • cleanup_record(self, identity, id_, uow=None): Clean up a (soft) deleted record.

    • +
    • restore_record(self, identity, id_, expand=False, uow=None): Restore a record that has been (soft) deleted.

    • +
    • mark_record_for_purge(self, identity, id_, expand=False, uow=None): Mark a (soft) deleted record for purge.

    • +
    • unmark_record_for_purge(self, identity, id_, expand=False, uow=None): Remove the mark for deletion from a record, returning it to deleted state.

    • +
    • purge_record(self, identity, id_, uow=None): Purge a record that has been marked.

    • +
    +
  • +
  • Overridden methods to add deletion-related functionality

    +
      +
    • read(self, identity, id_, expand=False, action="read", include_deleted=False): Adds an include_deleted argument to the read method, and a check for the read_deleted permission if it is set to True.

    • +
    • read_draft(self, identity, id_, expand=False): Prevents reading a draft if there is a published deleted record. (410 response.)

    • +
    • search(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs): Adds a “read_deleted” permission action to the search method.

    • +
    • search_drafts(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs): Adds a filter to exclude soft-deleted records from the search results.

    • +
    • search_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read", **kwargs): Adds a “read_deleted” permission action to the search method.

    • +
    +
  • +
  • Additional overridden methods for other functionality

    +
      +
    • publish(self, identity, id_, uow=None, expand=False): Adds a check prior to the original publish method to allow enforcement of a config setting that requires a community to be present on a record before it can be published.

    • +
    • update_draft(self, identity, id_, data, revision_id=None, uow=None, expand=False): Adds a check prior to the original update_draft method to allow enforcement of a config setting that prevents a record from being restricted after the grace period.

    • +
    +
  • +
  • Additional new methods for other functionality

    +
      +
    • expandable_fields: Expands the communities field to return community details.

    • +
    • oai_result_item(self, identity, oai_record_source): Get a result item from a record source in the OAI server.

    • +
    • scan_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read_deleted", **kwargs): Search for record’s versions using a “scroll” search.

    • +
    +
  • +
+
+
+
+

Service Configuration

+

A service configuration is an object that provides the service with its configuration. It is passed to the service’s constructor when it is instantiated during the Flask app initialization.

+

The service configuration is defined in the service’s config attribute.

+

All service configurations inherit from the ServiceConfig class, which is defined in invenio_records_resources.services.base.config. They include at least:

+
    +
  • service_id: The ID of the service.

  • +
  • permission_policy_cls: The permission policy class to use for the service.

  • +
  • result_item_cls: The result item class to use for the service.

  • +
  • result_list_cls: The result list class to use for the service.

  • +
+

This is expanded in a RecordServiceConfig class by the invenio_records_resources package to add:

+
    +
  • record_cls: The record class to use for the service.

  • +
  • indexer_cls: The indexer class to use for the service.

  • +
  • indexer_queue_name: The name of the task queue to be used by the service’s indexer.

  • +
  • index_dumper: The dumper to be used for serializing records to be indexed by OpenSearch.

  • +
  • relations: The inverse relation mapping for the service, defining which fields relate to which record type.

  • +
  • search: The search configuration for the service. (This is a SearchOptions instance.)

  • +
  • schema: The schema to be used when validating the service’s records.

  • +
  • links_item: The template for creating url links for the service’s result items.

  • +
  • links_search: The template for creating url links for the service’s search endpoints.

  • +
  • components: A list of components that will be used by the service.

  • +
+

It is further expanded in an overridden RecordServiceConfig class by the invenio_drafts_resources package to add:

+
    +
  • draft_cls: The draft record class to use for the service.

  • +
  • draft_indexer_cls: The indexer class to use for the service’s draft records.

  • +
  • draft_indexer_queue_name: The name of the task queue to be used by the service’s draft records indexer.

  • +
  • schema_parent: The schema used to valid parent records for the service.

  • +
  • search_drafts: A search class for searching for draft records.

  • +
  • search_versions: A search class for searching for record versions.

  • +
  • default_files_enabled: Whether files are enabled by default for the service.

  • +
  • default_media_files_enabled: Whether media files are enabled by default for the service.

  • +
  • lock_edit_published_files: Whether to lock editing of published files for the service.

  • +
  • links_search_drafts: The template for creating url links for the service’s search drafts endpoint.

  • +
  • links_search_versions: The template for creating url links for the service’s search versions endpoint.

  • +
+

The RDMRecordServiceConfig class adds the following additional configuration attributes:

+
    +
  • max_files_count: The maximum number of files that can be attached to a record.

  • +
  • file_links_list: The list of file links for the service.

  • +
  • schema_access_settings: The schema for access settings.

  • +
  • schema_secret_link: The schema for secret links.

  • +
  • schema_grant: The schema for grants.

  • +
  • schema_grants: The schema for grants.

  • +
  • schema_request_access: The schema for request access.

  • +
  • schema_tombstone: The schema for tombstone.

  • +
  • schema_quota: The schema for quota.

  • +
+

Additional common configration attributes are added by inheriting from additional mixin classes.

+
+

Attaching configuration to the service

+

The service config class is passed to the service’s constructor when it is instantiated during the Flask app initialization:

+
service = MyService(config=MyServiceConfig)
+
+
+
+
+

File service configuration

+

The FileConfigMixin class (defined in invenio_records_resources.services.records.components.files) adds config class attributes for: ????

+
    +
  • _files_attr_key: The attribute key for the files field.

  • +
  • _files_data_key: The attribute key for the files data.

  • +
  • _files_bucket_attr_key: The attribute key for the files bucket.

  • +
  • _files_bucket_id_attr_key: The attribute key for the files bucket ID.

  • +
+
+
+

Search configuration

+
+
SearchOptionsMixin
+

This mixin class (defined in invenio_records_resources.services.base.config) adds config class attributes for:

+
    +
  • facets: The search facet definitions for searches on the service’s resource.

  • +
  • sort_options: The sort options for searches on the service’s resource.

  • +
  • sort_default: The default sort option for searches on the service’s resource.

  • +
  • sort_default_no_query: The default sort option for searches on the service’s resource when no query is present.

  • +
  • available_sort_options: The available sort options for searches on the service’s resource.

  • +
  • query_parser_cls: The query parser class to use in constructing searches on the service’s resource.

  • +
+
+
+
SearchConfig
+

The SearchConfig class (defined in invenio_records_resources.services.base.config) defines the search configuration that will be used to interface with OpenSearch.

+
+
+
FromConfigSearchOptions
+

The FromConfigSearchOptions class (defined in invenio_records_resources.services.base.config) is used to load search configuration from app config variables. In the service’s config class, it is used like this:

+
+
+
+

Loading configuration from app config variables

+

The FromConfig class (defined in invenio_records_resources.services.base.config) is used to load configuration from app config variables. In the service’s config class, it is used like this:

+
class MyServiceConfig(ServiceConfig):
+    foo = FromConfig("FOO", default=1)
+
+
+

In the app config, the config variable is defined like this:

+
FOO = 2
+
+
+

When the service is instantiated, the FromConfig class will load the config variable from the app config and assign it to the foo attribute.

+
+
+
+

Service Components

+

A service component is a class that provides methods that shadow the service’s methods. When a service method is called, it passes the call through each of the service’s components (using the Service.run_components() method), allowing each component to perform additional processing before the result is returned. If the service component includes a method with the same name as the service method that is being called, its matching method will be called. During this call, the component method is passed the service method’s arguments and keyword arguments, and the service method’s modified versions of these arguments are passed on to the next component. Once all the service’s components have been called, the result is returned to the service method, which returns the final result or performs the final action.

+
+

BaseServiceComponent

+

The BaseServiceComponent class (defined in invenio_records_resources.services.base.components) is the base class for all service components. It provides a uow property that returns the Unit of Work manager.

+

This class is overridden by the ServiceComponent class (defined in invenio_records_resources.services.base.components.base), which adds the following methods:

+
    +
  • create(self, identity, **kwargs): Perform additional processing while creating an item of the service’s resource.

  • +
  • read(self, identity, **kwargs): Perform additional processing while retrieving an item of the service’s resource.

  • +
  • update(self, identity, **kwargs): Perform additional processing while updating an item of the service’s resource.

  • +
  • delete(self, identity, **kwargs): Perform additional processing while deleting an item of the service’s resource.

  • +
  • search(self, identity, search, params, **kwargs): Perform additional processing while searching for items of the service’s resource.

  • +
+

The invenio_drafts_resources package overrides the ServiceComponent class to add methods matching the overridden RecordService methods for draft records and versioning.

+
    +
  • read_draft(self, identity, draft=None): Retrieve a draft record.

  • +
  • update_draft(self, identity, data=None, record=None, errors=None): Update a draft record.

  • +
  • delete_draft(self, identity, draft=None, record=None, force=False): Delete a draft record.

  • +
  • edit(self, identity, draft=None, record=None): Edit a record.

  • +
  • new_version(self, identity, draft=None, record=None): Create a new version of a record.

  • +
  • publish(self, identity, draft=None, record=None): Publish a draft record.

  • +
  • import_files(self, identity, draft=None, record=None): Import files from previous record version.

  • +
  • post_publish(self, identity, record=None, is_published=False): Post publish handler.

  • +
+
+
+

RecordService Components

+

The invenio_records_resources package provides the following components for the RecordService class:

+
    +
  • DataServiceComponent (create, update): Adds data to the record.

  • +
  • BaseRecordFilesComponent (create, update):

    +
      +
    • Handles enabling/disabling files for a record.

    • +
    • Handles setting the default preview file for a record.

    • +
    +
  • +
  • MetadataComponent (create, update): Adds metadata to the new/updated record from the input data.

  • +
  • RelationsComponent (read): Dereferences a record’s related fields in order to provide the data from the related records in a read result.

  • +
  • ChangeNotificationsComponent (update): Emits a change notification for the updated record.

  • +
+

The invenio_drafts_resources package provides additional components for the RecordService class:

+
    +
  • an overridden BaseRecordFilesComponent class that adds methods for ???

  • +
  • DraftFilesComponent: Handles files for draft records.

  • +
  • DraftMediaFilesComponent: Handles media files for draft records.

  • +
  • DraftMetadataComponent: Handles metadata for draft records.

  • +
  • PIDComponent (create, delete_draft): Handles registration of PIDs for draft records.

  • +
  • an overridden RelationsComponent class that adds a read_drafts method

  • +
+

The invenio_rdm_records package provides additional components for the RDMRecordService class:

+
    +
  • AccessComponent(create, update_draft, publish, edit, new_version): Handles access settings for records.

  • +
  • an overridden MetadataComponent class (create, update_draft, publish, edit, new_version): Adds metadata to the new/updated record from the input data. (Removes the update method from the earlier MetadataComponent class.)

  • +
  • CustomFieldsComponent(create, update_draft, publish, edit, new_version): Adds custom fields to the metadata of a record.

  • +
  • PIDsComponent(create, update_draft, delete_draft, publish, edit, new_version, delete_record, restore_record): Handles PIDs for records.

  • +
  • ParentPIDsComponent(create, publish, delete_record, restore_record): Handles parent PIDs for records.

  • +
  • RecordDeletionComponent(delete_record, update_tombstone, restore_record, mark_record, unmark_record, purge_record): Handles deletion of records.

  • +
  • RecordFilesProcessorComponent(publish, lift_embargo): Handles file processing for records.

  • +
  • ReviewComponent(create, delete_draft, publish): Handles reviews for records.

  • +
  • SignalComponent(publish): Triggers signals on publish.

  • +
  • ContentModerationComponent(publish): Creates a moderation request if the user is not verified.

  • +
+
+
+

RDMRecordService Components

+

The invenio_rdm_records package draws its list of components from the RDM_RECORDS_SERVICE_COMPONENTS config variable. The default list is defined in the DefaultRecordsComponents class (defined in invenio_rdm_records.services.config) and currently includes:.

+
[
+    MetadataComponent,
+    CustomFieldsComponent,
+    AccessComponent,
+    DraftFilesComponent,
+    DraftMediaFilesComponent,
+    RecordFilesProcessorComponent,
+    RecordDeletionComponent,
+    # for the internal `pid` field
+    PIDComponent,
+    # for the `pids` field (external PIDs)
+    PIDsComponent,
+    ParentPIDsComponent,
+    RelationsComponent,
+    ReviewComponent,
+    ContentModerationComponent,
+]
+
+
+

Note that the order of the components in the list is important, since the components are called in the order they are listed and some components depend on the results of previous components.

+
+
+
+
+ +
+
+
+ + +
+
+ + Made with Sphinx and @pradyunsg's + + Furo + +
+
+ +
+
+ +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/build/changelog.html b/docs/build/changelog.html new file mode 100644 index 000000000..5be723127 --- /dev/null +++ b/docs/build/changelog.html @@ -0,0 +1,495 @@ + + + + + + + + + Changes - Knowledge Commons Works 0.3.5 documentation + + + + + + + + + + + + + + + + Contents + + + + + + Menu + + + + + + + + Expand + + + + + + Light mode + + + + + + + + + + + + + + Dark mode + + + + + + + Auto light/dark, in light mode + + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Skip to content + + + +
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ + + + + Back to top + +
+ +
+ +
+ +
+
+ + +
+

Changes

+
+

0.3.5-beta8 (2025-01-10)

+
    +
  • Dashboard works search

    +
      +
    • Fixed the bug that broke works searching from the dashboard.

    • +
    +
  • +
+
+
+

0.3.4-beta7 (2025-01-09)

+
    +
  • Upload form collection selector

    +
      +
    • Fixed bug in collection selection modal where search results were always sorted by “newest” instead of “bestmatch” and so were useless for large result sets (original fix only worked in detail page)

    • +
    +
  • +
  • Documentation

    +
      +
    • Moved documentation from README.md and site/CHANGES.md into a static documentation site to be served by Github Pages.

    • +
    • Added more documentation for cli commands, metadata/identifiers/vocabularies, installation, and version control.

    • +
    +
  • +
  • Build system

    +
      +
    • Pinned the version of invenio-logging to less than 2.1.2 to avoid a webpack build conflict.

    • +
    +
  • +
+
+
+

0.3.3-beta6 (2024-12-18)

+
    +
  • Names

    +
      +
    • Added the infrastructure to customize the division of users’ names into parts so that it can be divided as desired when, e.g., the user’s name is being auto-filled in the name fields of the upload form. This involves

      +
        +
      • a new “name_parts_local” field to the user profile schema. This field contains the user’s name parts if they have been modified within the KCWorks system. This is sometimes necessary when the user data synced from the remote user data service does not divide the user’s name correctly.

      • +
      • a cli command to update the user’s name parts.

      • +
      • a new “names” js module that contains functions to get the user’s full name, full name in inverted order, family name, and given name from the user’s name parts.

      • +
      • updates to the CreatibutorsField component to use the new “names” js module and the customized name parts if they are present in a user’s profile.

      • +
      +
    • +
    +
  • +
  • Detail page

    +
      +
    • Added missing aria-label properties for accessibility

    • +
    +
  • +
  • Collections

    +
      +
    • Fixed wording of empty results message for collection members search

      +
        +
      • Previously, the empty results message used “community” instead of “collection”.

      • +
      +
    • +
    • Tweaks to layout of collection detail page header

    • +
    +
  • +
  • Remote user data service

    +
      +
    • Fixed bug where user profile data was not being updated because comparison with initial data was not being made correctly. This means that, among other things, ORCID ids will now be added correctly when the user chooses “add self” on the upload form.

    • +
    +
  • +
+
+
+

0.3.2-beta5 (2024-12-11)

+
    +
  • Added Bluesky sharing option to detail page

  • +
  • Fixed line wrapping of long values in record sidebar details

  • +
  • Added OpenGraph image metadata property to record detail page

    +
      +
    • This allows social media platforms to display the KCWorks logo instead of a random image they might find on the page.

    • +
    +
  • +
+
+
+

0.3.1-beta4 (2024-12-10)

+
    +
  • Added sort options for publication date to record search

    +
      +
    • This allows users to sort records by the date they were published.

    • +
    • It also allows publication-date sorting in API requests to the search API. Among other things, this allows users’ KC profiles to display records in publication date order.

    • +
    +
  • +
  • Community selection modal bug fixes

    +
      +
    • This affects the modal that appears both during record submission on the upload form and during collection management on the detail page.

    • +
    • Fixed the sort order of search results in the modal. These were being sorted by record creation date, leading to a confusing sort order. It now sorts by “best match”. This allows, e.g., “Knowledge Commons” to find the main KC collection.

    • +
    • Also fixed the handling of ‘/’ in the search query string. This allows, e.g., “ARLIS/NA” to find the ARLIS/NA collection, where previously it would produce an error.

    • +
    +
  • +
+
+
+

0.3.0-beta3 (2024-11-30)

+
    +
  • Record detail page

    +
      +
    • Added ui for collection management

      +
        +
      • A new menu appears in the detail page sidebar when a user has permission to edit a record. This +allows users to manage the record’s collections right from the detail page.

      • +
      • With this menu users can now

        +
          +
        • submit a request to have an existing published record added to a collection.

        • +
        • add a record to multiple collections

        • +
        • remove a record from some or all of its collections

        • +
        • view pending collection submissions for the record

        • +
        +
      • +
      • change which collection appears as the primary collection for the record (i.e., the collection whose logo appears in the record’s detail page sidebar)

      • +
      +
    • +
    • Refactored record management menu

    • +
    • Refactored all sidebar menus (including the record management menu) to allow accessible +keyboard navigation

    • +
    • Fixed display of event metadata

    • +
    • Added display for work doi as well as version doi

      +
        +
      • Each record has at least two DOIs: a work DOI and a version DOI. The work DOI is the DOI for the record as a whole. It always points to the most recent version of the work, even if the user creates new versions in the future. The other identifier is the version DOI, which will always point to the specific version of the work that the user is currently viewing. Previously, only the version DOI was displayed, which could be confusing if the user created a new version of the work.

      • +
      +
    • +
    +
  • +
  • Upload form

    +
      +
    • Added proper messages to collections widget for published records

      +
        +
      • since collections for published records are now managed from the detail page, the collections widget now displays messages to users pointing them to the detail page to manage collections.

      • +
      +
    • +
    • Added clearer titles to form when editing an existing record +or creating a new version

      +
        +
      • Previously, the form would display “Editing Published Record” both when editing the metadata of an existing published version and when creating a new version. The header now displays “Creating New Version” when creating a new version, and “Editing Published Record” when editing the metadata of an existing published version.

      • +
      +
    • +
    • Changed default publisher from “unknown” to “Knowledge Commons”

      +
        +
      • Previously, the default publisher was “unknown”. This was especially confusing for resource types where the publisher field is hidden on the upload form. Now, the default publisher is “Knowledge Commons”.

      • +
      +
    • +
    +
  • +
  • Solved collection links bug with custom routes

    +
      +
    • This is a back-end technical fix that should not be visible to users.

    • +
    +
  • +
+
+
+ +
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/docs/build/cli_commands.html b/docs/build/cli_commands.html index 385fb2586..6a493fbcc 100644 --- a/docs/build/cli_commands.html +++ b/docs/build/cli_commands.html @@ -3,10 +3,10 @@ - + - CLI Commands - Knowledge Commons Works 0.3.3 documentation + CLI Commands - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
-
Knowledge Commons Works 0.3.3 documentation
+
Knowledge Commons Works 0.3.5 documentation
-
+
diff --git a/docs/build/developing.html b/docs/build/developing.html index dd21230d6..ec48f0a88 100644 --- a/docs/build/developing.html +++ b/docs/build/developing.html @@ -6,7 +6,7 @@ - Developing KCWorks - Knowledge Commons Works 0.3.3 documentation + Developing KCWorks - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
-
Knowledge Commons Works 0.3.3 documentation
+
Knowledge Commons Works 0.3.5 documentation
@@ -190,7 +190,7 @@ diff --git a/docs/build/genindex.html b/docs/build/genindex.html index 38fc8c883..b784280be 100644 --- a/docs/build/genindex.html +++ b/docs/build/genindex.html @@ -4,7 +4,7 @@ - Index - Knowledge Commons Works 0.3.3 documentation + Index - Knowledge Commons Works 0.3.5 documentation @@ -164,7 +164,7 @@
-
Knowledge Commons Works 0.3.3 documentation
+
Knowledge Commons Works 0.3.5 documentation
+
diff --git a/docs/build/in_depth.html b/docs/build/in_depth.html index bb4f32b01..7d79621c1 100644 --- a/docs/build/in_depth.html +++ b/docs/build/in_depth.html @@ -6,7 +6,7 @@ - In-depth Installation Instructions (NEEDS UPDATING) - Knowledge Commons Works 0.3.3 documentation + In-depth Installation Instructions (NEEDS UPDATING) - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
-
Knowledge Commons Works 0.3.3 documentation
+
Knowledge Commons Works 0.3.5 documentation
@@ -190,7 +190,7 @@ diff --git a/docs/build/index.html b/docs/build/index.html index d4905ee31..8f794a4e5 100644 --- a/docs/build/index.html +++ b/docs/build/index.html @@ -6,7 +6,7 @@ - Knowledge Commons Works 0.3.3 documentation + Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
-
Knowledge Commons Works 0.3.3 documentation
+
Knowledge Commons Works 0.3.5 documentation
@@ -190,7 +190,7 @@
-
-

Welcome to Knowledge Commons Works’s documentation!

+
+

Welcome to the Knowledge Commons Works technical documentation!

Contents:

    @@ -260,11 +262,13 @@

    Welcome to Knowledge Commons Works’s documentation!Copyright

-
  • Changes
  • -
    -
    -

    Indices and tables

    -
    @@ -374,7 +387,7 @@

    Indices and tables +

    diff --git a/docs/build/infrastructure.html b/docs/build/infrastructure.html index f7b3e3709..3be958442 100644 --- a/docs/build/infrastructure.html +++ b/docs/build/infrastructure.html @@ -6,7 +6,7 @@ - KCWorks Infrastructure - Knowledge Commons Works 0.3.3 documentation + KCWorks Infrastructure - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
    -
    Knowledge Commons Works 0.3.3 documentation
    +
    Knowledge Commons Works 0.3.5 documentation
    +
    diff --git a/docs/build/installation.html b/docs/build/installation.html index bbdd89a1a..dde9baa4a 100644 --- a/docs/build/installation.html +++ b/docs/build/installation.html @@ -3,10 +3,10 @@ - + - Installation - Knowledge Commons Works 0.3.3 documentation + Installation - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
    -
    Knowledge Commons Works 0.3.3 documentation
    +
    Knowledge Commons Works 0.3.5 documentation
    @@ -190,7 +190,7 @@ -
    Metadata Schema and Vocabularies
    +
    Metadata Schema, Vocabularies, and Identifiers
    - +
    Previous
    -
    About
    +
    Changes
    @@ -467,7 +469,7 @@

    Controlling the KCWorks (Flask) application +

    diff --git a/docs/build/metadata.html b/docs/build/metadata.html index 0f9afadfc..11f93c44b 100644 --- a/docs/build/metadata.html +++ b/docs/build/metadata.html @@ -6,7 +6,7 @@ - Metadata Schema and Vocabularies - Knowledge Commons Works 0.3.3 documentation + Metadata Schema, Vocabularies, and Identifiers - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@
    -
    Knowledge Commons Works 0.3.3 documentation
    +
    Knowledge Commons Works 0.3.5 documentation
    @@ -190,7 +190,7 @@
    -
    -

    Metadata Schema and Vocabularies

    +
    +

    Metadata Schema, Vocabularies, and Identifiers

    The default metadata schema for InvenioRDM records is defined in the invenio-rdm-records package and documented here. It also includes a number of optional metadata fields which have been enabled in KCWorks, documented here.

    -

    Beyond these InvenioRDM fields, KCWorks adds a number of custom metadata fields to the schema using InvenioRDM’s custom field mechanism. These are all located in the top-level custom_fields field of the record metadata. They are prefixed with two different namespaces:

    -
      -
    • kcr: custom fields that are used to store data from the KC system. These fields may be used for new data, but are not required.

    • -
    • hclegacy: custom fields that are used to store data from the legacy CORE repository. These fields must not be used for new data.

    • -
    -
    -

    Example JSON record

    +

    In this documentation we provide

    +
      +
    1. A full example of a KCWorks record metadata object

    2. +
    3. A list of the controlled vocabularies and identifier schemes supported by KCWorks

    4. +
    5. Discussion of how some of the standard InvenioRDM metadata fields are used in KCWorks

    6. +
    7. A list of the custom metadata fields KCWorks adds to the base InvenioRDM schema

    8. +
    +
    +

    Example metadata record

    +
    +

    JSON object for record creation

    What follows is an example of a complete metadata record (JSON object) used to create a KCWorks record. The various fields and their possible values are described in the sections below.

    Note that no single actual record would include all of these fields. The example is provided to illustrate the structure of the metadata record and the sort of values that are valid for each field.

    {
    @@ -497,29 +502,298 @@ 

    Example JSON record +

    JSON object retrieved from the record API

    +

    The JSON object retrieved from the record API shares the same basic structure as the JSON object used to create the record, except that it includes a number of additional fields. Some properties are also filled out with additional details (e.g., readable titles for licenses, etc.)

    +

    +

    Controlled Vocabularies

    Subject headings

    FAST

    -

    The FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) is used for the subjects field.

    +

    The FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) is used for the subjects field. See the metadata.subjects section for more information about how to include FAST subjects in a KCWorks record.

    Homosaurus

    -

    The FAST vocabulary is augmented in KCWorks by the Homosaurus vocabulary (https://homosaurus.org/) for subjects related to sexuality and gender identity.

    +

    The FAST vocabulary is augmented in KCWorks by the Homosaurus vocabulary (https://homosaurus.org/) for subjects related to sexuality and gender identity. See the metadata.subjects section for information about how to include Homosaurus subjects in a KCWorks record.

    +
    +
    +
    +
    +

    Resource types

    +

    As an open repository that serves a multidisciplinary audience, KCWorks uses a custom vocabulary of resource types designed (a) to support the wide variety of scholarly materials we accept and (b) to facilitate ease of use for depositors. The terms in this vocabulary are mapped to DataCite’s resourceTypeGeneral vocabulary and a number of other resource type vocabularies (COAR, CSL, EUREPO, Schema.org). This allows correct export of metadata to DataCite and in other metadata formats.

    +

    InvenioRDM employs a hierarchical structure of resource types, each of which has a number of subtypes. In KCWorks the 8 top-level resource types are:

    +
      +
    • audiovisual

    • +
    • dataset

    • +
    • image

    • +
    • instructionalResource

    • +
    • presentation

    • +
    • software

    • +
    • textDocument

    • +
    • other

    • +
    +

    We selected these top-level types in part to allow division of the many subtypes into manageable groups. This allows us to provide a wide range of resource types while also allowing users to easily find the resource type that best fits their deposit.

    +

    Beneath these top-level types are a number of subtypes, which are listed below. Where the DataCite schema allows free-text, arbitrary subtypes, we have followed InvenioRDM’s approach of using a controlled vocabulary of subtypes. Where our list of top-level types is short, we have erred on the side of including more subtypes. Again, this allows us to support a wide range of materials without forcing them to choose a subtype that does not fit. It also allows us to tailor the user interface of the upload form to the specific subtype of the record being deposited, preventing the confusion and overwhelm of users being presented with many metadata fields which are not relevant to their material.

    +

    The following is the complete list of KCWorks resource types with their subtypes. This list may be expanded in the future.

    +
      +
    • audiovisual

      +
        +
      • documentary

      • +
      • interviewRecording

      • +
      • musicalRecording

      • +
      • other

      • +
      • performance

      • +
      • podcastEpisode

      • +
      • audioRecording

      • +
      • videoRecording

      • +
      +
    • +
    • dataset

    • +
    • image

      +
        +
      • chart

      • +
      • diagram

      • +
      • figure

      • +
      • map

      • +
      • visualArt

      • +
      • photograph

      • +
      • other

      • +
      +
    • +
    • instructionalResource

      +
        +
      • curriculum

      • +
      • lessonPlan

      • +
      • syllabus

      • +
      • other

      • +
      +
    • +
    • presentation

      +
        +
      • conferencePaper

      • +
      • conferencePoster

      • +
      • presentationText

      • +
      • slides

      • +
      • other

      • +
      +
    • +
    • software

      +
        +
      • 3DModel

      • +
      • application

      • +
      • computationalModel

      • +
      • computationalNotebook

      • +
      • service

      • +
      • other

      • +
      +
    • +
    • textDocument

      +
        +
      • abstract

      • +
      • bibliography

      • +
      • blogPost

      • +
      • book

      • +
      • bookSection

      • +
      • conferenceProceeding

      • +
      • dataManagementPlan

      • +
      • documentation

      • +
      • editorial

      • +
      • essay

      • +
      • interviewTranscript

      • +
      • journalArticle

      • +
      • legalComment

      • +
      • legalResponse

      • +
      • magazineArticle

      • +
      • monograph

      • +
      • newspaperArticle

      • +
      • onlinePublication

      • +
      • poeticWork

      • +
      • preprint

      • +
      • report

      • +
      • workingPaper

      • +
      • review

      • +
      • technicalStandard

      • +
      • thesis

      • +
      • whitePaper

      • +
      • other

      • +
      +
    • +
    • other

      +
        +
      • catalog

      • +
      • collection

      • +
      • event

      • +
      • interactiveResource

      • +
      • notes

      • +
      • patent

      • +
      • peerReview

      • +
      • physicalObject

      • +
      • workflow

      • +
      +
    • +
    +

    Note that (like with the base InvenioRDM resource types), neither the list of top-level resource types nor the list of subtypes exactly matches the vocabulary provided by DataCite under resourceTypeGeneral. Those types are all included in the KCWorks vocabulary–some as top-level types, some as subtypes. But because we do not follow DataCite in allowing arbitrary free-text subtypes, we have needed to greatly expand the list of subtypes to support the wide variety of materials we accept. As mentioned above, however, each subtype is mapped to a DataCite resourceTypeGeneral value for correct export to DataCite and other metadata formats.

    +

    You can compare the KCWorks resource types with the list from the original Humanities Commons CORE repository here. The KCWorks resource type vocabulary is not structured in the same way as the CORE vocabulary (which was a flat list), but the KCWorks subtypes encompass all of the original CORE types.

    +
    +
    +

    Creator/contributor roles

    +

    Keeping with our support for a wide variety of objects and disciplines, our creator roles are more diverse than just “author,” “editor,” or “translator.” For contribuors we were influenced by the CRediT Taxonomy, finding ways of recognizing labor even when the contribution is not immediately visible. Included in both creator and contributor roles a selection of types taken from the Variations Metadata taxonomy, providing the ability to credit those who engage in creative and musical works.

    +

    The complete list of creator/contributor roles is:

    +
      +
    • actor

    • +
    • adaptor

    • +
    • annotator

    • +
    • analyst

    • +
    • arranger

    • +
    • artisan

    • +
    • artist

    • +
    • attributedName

    • +
    • author

    • +
    • authorOfIntroduction

    • +
    • authorOfForeword

    • +
    • authorOfAfterword

    • +
    • committeeChair

    • +
    • choreographer

    • +
    • cinematographer

    • +
    • collaborator

    • +
    • collector

    • +
    • committeeMember

    • +
    • composer

    • +
    • conductor

    • +
    • consultant

    • +
    • contactperson

    • +
    • correspondent

    • +
    • datacollector

    • +
    • datacurator

    • +
    • datamanager

    • +
    • dedicatee

    • +
    • designer

    • +
    • director

    • +
    • distributor

    • +
    • donor

    • +
    • drafter

    • +
    • editor

    • +
    • examiner

    • +
    • formerOwner

    • +
    • hostinginstitution

    • +
    • illustrator

    • +
    • interviewee

    • +
    • interviewer

    • +
    • inventor

    • +
    • juror

    • +
    • licensee

    • +
    • lyricist

    • +
    • manufacturer

    • +
    • organizer

    • +
    • owner

    • +
    • performer

    • +
    • photographer

    • +
    • printer

    • +
    • producer

    • +
    • projectOrTeamLeader

    • +
    • projectOrTeamManager

    • +
    • projectOrTeamMember

    • +
    • recording engineer

    • +
    • referee

    • +
    • registrationagency

    • +
    • registrationauthority

    • +
    • relatedperson

    • +
    • reporter

    • +
    • researcher

    • +
    • researchgroup

    • +
    • researchParticipant

    • +
    • rightsholder

    • +
    • screenplayAuthor

    • +
    • speaker

    • +
    • supervisor

    • +
    • transcriber

    • +
    • translator

    • +
    • witness

    • +
    • workpackageleader

    • +
    • writerOfAccompanying

    • +
    +

    Note that where InvenioRDM provides distinct custom vocabularies for creators and contributors, KCWorks employs a single creator/contributor vocabulary. This is in keeping with our handling of the creators and contributors fields, discussed below.

    +
    +
    +

    Identifier Schemes

    +
    +

    Works

    +
    +

    DOI (primary identifier)

    +

    KCWorks (and InvenioRDM) supports the DOI identifier scheme to identify works in the repository. Note that two DOIs are minted for each KCWorks record: one for the current version of the record, and one for the work as a whole (including all versions). The version-specific DOI is stored in the pids property of the metadata record (pids.identifiers.doi). The work DOI is stored in the parent.pids.doi property of the parent object.

    +

    These DOIs are minted by DataCite (https://datacite.org/) and the attached metadata is maintained automatically by KCWorks.

    +

    Additional DOIs minted elsewhere can be attached to a KCWorks record. If provided at record creation such external DOIs can be used as the record’s primary identifier (in pids.doi). Otherwise, they can be added using the identifiers property of the metadata record using the scheme alternate-doi. In both cases, these externally minted DOIs are not maintained automatically by KCWorks.

    +
    +
    +

    OAI (secondary identifier)

    +

    KCWorks also supports the OAI identifier scheme. The OAI identifier for a KCWorks record is stored in the pids property of the metadata record (pids.identifiers.oai).

    +
    +
    +

    Handle (secondary identifier)

    +

    KCWorks also supports the Handle identifier scheme (https://handle.net/). The Handle identifier for a KCWorks record is stored in the identifiers property of the metadata record (identifiers[0].identifier) using the scheme handle.

    +
    +
    +

    ISSN (secondary identifier)

    +

    An ISSN is an eight digit code that identifies a print or electronic newspaper, journal, magazine, or other periodical. More information on the ISSN can be found on ISSN.org.

    +
    +
    +

    ISBN (secondary identifier)

    +

    An ISBN (International Standard Book Number) is a ten (pre-2007) or 13 digit (2007 to present) identifier used to identify both print and electronic published books. More information on the ISBN can be found on ISBN-international.org.

    +
    +
    +
    +

    People

    + + +
    +

    GND

    +

    KCWorks also supports the Integrated Authority File (GND) identifier scheme (https://www.dnb.de/EN/Professionell/Standardisierung/GND/gnd_node.html). The GND identifier of the submitter of the KCWorks record is stored in the person_or_org.identifiers property of the creators array (creators[0].person_or_org.identifiers.identifier) using the scheme gnd.

    +
    +
    +

    ISNI

    +

    KCWorks also supports the ISNI identifier scheme (https://isni.org/). The ISNI of the submitter of the KCWorks record is stored in the person_or_org.identifiers property of the creators array (creators[0].person_or_org.identifiers.identifier) using the scheme isni.

    Organizations

    -
    -

    ROR

    -

    The Research Organization Registry (https://ror.org/) is used for the organizations field.

    + +
    +

    Grid (deprecated)

    +

    KCWorks also supports the Grid identifier scheme (https://www.grid.ac/) for organizations using the scheme grid. This scheme is deprecated in favour of ROR, however, and should not be used for new identifiers.

    +
    +

    GND

    +

    KCWorks also supports the Integrated Authority File (GND) identifier scheme (https://www.dnb.de/EN/Professionell/Standardisierung/GND/gnd_node.html) for organizations using the scheme gnd.

    -
    -

    Notes about Implementation of Core InvenioRDM Fields

    +
    +

    Funders

    +
    +

    DOI

    +

    Funders in the metadata.funding array can be identified using DOIs formed with a FundRef id and the scheme doi.

    +
    +
    +

    OFR

    +

    Funders in the metadata.funding array can also be identified using the Open Funder Registry (https://openfunder.org/) identifiers and the scheme ofr.

    +
    +
    +
    +
    +

    KCWorks Implementation of Core InvenioRDM Fields

    metadata.subjects

    Note that KCWorks employs the FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) for the subjects field, complemented by the Homosaurus vocabulary (https://homosaurus.org/).

    @@ -581,10 +855,16 @@

    metadata.creators/metadata.contributors -

    KCWorks Custom Fields (kcworks/site/metadata_fields)

    +

    KCWorks Custom Fields (kcworks/site/metadata_fields)

    +

    Beyond the standard InvenioRDM metadata fields, KCWorks adds a number of custom metadata fields to the schema using InvenioRDM’s custom field mechanism. These are all located in the top-level custom_fields field of the record metadata. They are prefixed with two different namespaces:

    +
      +
    • kcr: custom fields that are used to store data from the KC system. These fields may be used for new data, but are not required.

    • +
    • hclegacy: custom fields that are used to store data from the legacy CORE repository. These fields must not be used for new data.

    • +
    -

    kcr:ai_usage

    +

    kcr:ai_usage

    Type: Object[boolean, string]

    This field stores data about any use of generative AI in the production of the record.

    Example:

    @@ -598,7 +878,7 @@

    kcr:ai_usage -

    kcr:media

    +

    kcr:media

    Type: Array[string]

    This field stores a list of media or materials involved in the creation of the record. This field is used to store free-form user-defined descriptors of the media or materials and does not impose any controlled vocabulary.

    Example:

    @@ -609,7 +889,7 @@

    kcr:media -

    kcr:commons_domain

    +

    kcr:commons_domain

    Type: string

    This field stores the KC organizational (Commons) domain associated with the KCWorks record, if any. The record should also be placed in the KCWorks collection associated with this organization.

    Example:

    @@ -620,7 +900,7 @@

    kcr:commons_domain -

    kcr:chapter_label

    +

    kcr:chapter_label

    Type: string

    This field stores the label of the chapter associated with the KCWorks record, if any. This allows us to differentiate between a simple chapter label (e.g. “Chapter 1”) and a more substantive title for the same chapter (e.g., “The Role of AI in Modern Art”).

    Example:

    @@ -631,7 +911,7 @@

    kcr:chapter_label -

    kcr:content_warning

    +

    kcr:content_warning

    Type: string

    This field stores an optional content warning for the KCWorks record. This is used to flag the record for KCWorks users so that they can be aware of potentially problematic content in the record. This field is not to be used for content moderation by KCWorks moderators or admins. It is only to be used voluntarily and as desired by the record submitter.

    Example:

    @@ -642,7 +922,7 @@

    kcr:content_warning -

    kcr:course_title

    +

    kcr:course_title

    Type: string

    This field stores the title of the course associated with the KCWorks record. It is intended primarily for use with syllabi and instructional materials.

    Example:

    @@ -653,7 +933,7 @@

    kcr:course_title -

    kcr:degree

    +

    kcr:degree

    Type: string

    This field stores the educational degree (e.g., PhD, DPhil, MA, etc.) associated with the KCWorks record. It is intended primarily for use with theses and dissertations.

    Example:

    @@ -664,7 +944,7 @@

    kcr:degree -

    kcr:discipline

    +

    kcr:discipline

    Type: string

    This field stores the academic discipline associated with the KCWorks record. It is intended primarily for use with theses, dissertations, and other educational artifacts. It is not intended as a general-purpose field for describing the subject matter of the KCWorks record. For that, you should use the metadata.subjects and kcr:user_defined_tags fields.

    This field is intended to complement the thesis:university and kcr:institution_department fields.

    @@ -677,7 +957,7 @@

    kcr:discipline -

    kcr:edition

    +

    kcr:edition

    Type: string

    This field stores a descriptor for the edition of the KCWorks record, if any.

    Example:

    @@ -688,7 +968,7 @@

    kcr:edition -

    kcr:meeting_organization

    +

    kcr:meeting_organization

    Type: string

    This field stores the name of the organization associated with the meeting or conference associated with the KCWorks record. It is intended primarily for use with conference papers, presentations, proceedings, etc.

    Example:

    @@ -699,7 +979,7 @@

    kcr:meeting_organization -

    kcr:project_title

    +

    kcr:project_title

    Type: string

    This field stores the title of a project for which the KCWorks record was created. It can be used flexibly for, e.g., grant-funded projects, research projects, artistic projects, etc.

    Example:

    @@ -710,7 +990,7 @@

    kcr:project_title -

    kcr:publication_url

    +

    kcr:publication_url

    Type: string (URL)

    This field stores the URL of the publication associated with the KCWorks record. It is not the URL of the KCWorks record itself or of the work it contains. For example, if the KCWorks record contains a journal article, it would not hold the URL for the published journal article. It is intended to hold the URL of the publication as a whole that the KCWorks record is based on or is a part of. So it might hold the main URL for the journal in which the article was published, or the main URL for the book in which the chapter was published, etc.

    This string must be a valid URL.

    @@ -722,7 +1002,7 @@

    kcr:publication_url -

    kcr:sponsoring_institution

    +

    kcr:sponsoring_institution

    Type: string

    This field stores the name of the institution that sponsored the KCWorks record. One intended use is for unpublished materials such white papers that were sponsored or commissioned by an institution. The field may also be used for the institution hosting a conference or workshop associated with the KCWorks record (as distinct from the organization that sponsored the event).

    Note that this field is not intended for the degree-granting institution associated with a thesis or dissertation. That institution’s title should be stored in the thesis:university field.

    @@ -734,7 +1014,7 @@

    kcr:sponsoring_institution -

    kcr:submitter_email

    +

    kcr:submitter_email

    Type: string (email address)

    This field stores the email address of the submitter of the KCWorks record. It must be a valid email address.

    Example:

    @@ -745,7 +1025,7 @@

    kcr:submitter_email -

    kcr:submitter_username

    +

    kcr:submitter_username

    Type: string

    This field stores the KC username of the submitter of the KCWorks record. This should be used even if the submitter is also a contributor to the KCWorks record and has included the same username in the metadata.creators.person_or_org.identifiers array.

    Example:

    @@ -756,7 +1036,7 @@

    kcr:submitter_username

    -

    kcr:institution_department

    +

    kcr:institution_department

    Type: string

    This field stores the institutional department in which a thesis, dissertation, or other educational artifact was produced. It is intended to complement the thesis:university field, which stores the degree-granting institution.

    Example:

    @@ -767,7 +1047,7 @@

    kcr:institution_department -

    kcr:book_series

    +

    kcr:book_series

    Type: Object[string, string]

    This field stores the title of a series that contains the KCWorks record, along with the optional volume number of the work within the series.

    Example:

    @@ -781,7 +1061,7 @@

    kcr:book_series -

    kcr:user_defined_tags

    +

    kcr:user_defined_tags

    Type: Array[string]

    This field stores a list of user-defined tags for the KCWorks record. Unlike the metadata.subjects field, these tags are not constrained by any controlled vocabulary. Items should be free-form strings that describe the KCWorks record in a way that is not covered by the metadata.subjects field.

    @@ -796,7 +1076,7 @@

    kcr:user_defined_tags

    -

    kcr:commons_search_recid (system field)

    +

    kcr:commons_search_recid (system field)

    This field is used to store the persistent identifier for the KCWorks record in the KC central search index.

    [!Warning] @@ -804,7 +1084,7 @@

    kcr:commons_search_recid (system field) -

    kcr:commons_search_updated (system field)

    +

    kcr:commons_search_updated (system field)

    Type: string (ISO 8601 datetime string)

    This field stores the date and time when the KCWorks record was last updated in the KC central search index.

    @@ -814,10 +1094,10 @@

    kcr:commons_search_updated (system field) -

    HC Legacy Custom Fields

    +

    HC Legacy Custom Fields

    The hclegacy namespace is used for custom fields that are used to store data from the legacy CORE database. These fields should not be used for new data.

    -

    custom_fields.hclegacy:groups_for_deposit

    +

    custom_fields.hclegacy:groups_for_deposit

    Type: Array[Object[string, string]]

    This field is used to store the groups to which a legacy CORE record belonged before import into KCWorks. It was used to create corresponding KCWorks collections during migration.

    Example:

    @@ -833,7 +1113,7 @@

    custom_fields.hclegacy:groups_for_deposit -

    custom_fields.hclegacy:collection

    +

    custom_fields.hclegacy:collection

    Type: string

    This field is used to store the org collection to which a legacy CORE record belonged before import into KCWorks. It was used to create corresponding KCWorks org collections during migration.

    Example:

    @@ -844,7 +1124,7 @@

    custom_fields.hclegacy:collection -

    custom_fields.hclegacy:committee_deposit

    +

    custom_fields.hclegacy:committee_deposit

    Type: integer

    This field is used to store the committee deposit number for a legacy CORE record. It was not used during migration and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -855,7 +1135,7 @@

    custom_fields.hclegacy:committee_deposit -

    custom_fields.hclegacy:file_location

    +

    custom_fields.hclegacy:file_location

    Type: string

    This field is used to store the relative path the the file for a legacy CORE record. It was not used during migration and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -866,7 +1146,7 @@

    custom_fields.hclegacy:file_location -

    custom_fields.hclegacy:file_pid

    +

    custom_fields.hclegacy:file_pid

    Type: string

    This field is used to store the persistent identifier for the file for a legacy CORE record. It was not used during migration and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -877,7 +1157,7 @@

    custom_fields.hclegacy:file_pid -

    custom_fields.hclegacy:previously_published

    +

    custom_fields.hclegacy:previously_published

    Type: string

    This field is used to store the previously published status for a legacy CORE record. It was not used during migration and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -888,7 +1168,7 @@

    custom_fields.hclegacy:previously_published -

    custom_fields.hclegacy:publication_type

    +

    custom_fields.hclegacy:publication_type

    Type: string

    This field is used to store the publication type for a legacy CORE record. It was used during migration to help determine the KCWorks resource type of the record. It is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -899,7 +1179,7 @@

    custom_fields.hclegacy:publication_type -

    custom_fields.hclegacy:record_change_date

    +

    custom_fields.hclegacy:record_change_date

    Type: string (ISO 8601 datetime string)

    This field is used to store the date of the last change to a legacy CORE record. It was not used during migration to KCWorks and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -910,7 +1190,7 @@

    custom_fields.hclegacy:record_change_date -

    custom_fields.hclegacy:record_creation_date

    +

    custom_fields.hclegacy:record_creation_date

    Type: string (ISO 8601 datetime string)

    This field is used to store the date of the creation of a legacy CORE record. It was not used during migration because InvenioRDM does not allow overriding of the record creation date. It is only preserved for historical purposes and should not be used for new data.

    Example:

    @@ -921,7 +1201,7 @@

    custom_fields.hclegacy:record_creation_date -

    custom_fields.hclegacy:record_identifier

    +

    custom_fields.hclegacy:record_identifier

    Type: string

    This field is used to store the internal system identifier for a legacy CORE record. It was not used during migration and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -932,7 +1212,7 @@

    custom_fields.hclegacy:record_identifier -

    custom_fields.hclegacy:submitter_org_memberships

    +

    custom_fields.hclegacy:submitter_org_memberships

    Type: array[string]

    This field is used to store the organizations to which a legacy CORE record’s submitter belonged before import into KCWorks. It was used to create corresponding KCWorks org collections during migration and assign the work to those org collections.

    Example:

    @@ -943,7 +1223,7 @@

    custom_fields.hclegacy:submitter_org_memberships -

    custom_fields.hclegacy:submitter_affiliation

    +

    custom_fields.hclegacy:submitter_affiliation

    Type: string

    This field is used to store the organizational affiliation of a legacy CORE record’s submitter at the time of import into KCWorks. It was not used during migration and is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -954,7 +1234,7 @@

    custom_fields.hclegacy:submitter_affiliation -

    custom_fields.hclegacy:submitter_id

    +

    custom_fields.hclegacy:submitter_id

    Type: string

    This field is used to store the internal KC system user id of a legacy CORE record’s submitter. It was used during migration to assign ownership of the newly created record, and is preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -965,7 +1245,7 @@

    custom_fields.hclegacy:submitter_id -

    custom_fields.hclegacy:total_views

    +

    custom_fields.hclegacy:total_views

    Type: integer

    This field is used to store the total number of views for a legacy CORE record prior to import into KCWorks. It was used during migration to create KCWorks usage stats aggregations for the record. It is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -976,7 +1256,7 @@

    custom_fields.hclegacy:total_views -

    custom_fields.hclegacy:total_downloads

    +

    custom_fields.hclegacy:total_downloads

    Type: integer

    This field is used to store the total number of downloads for a legacy CORE record prior to import into KCWorks. It was used during migration to create KCWorks usage stats aggregations for the record. It is only preserved for historical purposes. It should not be used for new data.

    Example:

    @@ -987,7 +1267,6 @@

    custom_fields.hclegacy:total_downloads diff --git a/docs/build/objects.inv b/docs/build/objects.inv index f342d2c02..8792a4eb7 100644 Binary files a/docs/build/objects.inv and b/docs/build/objects.inv differ diff --git a/docs/build/reference.html b/docs/build/reference.html index 11d535067..8a35ec6cd 100644 --- a/docs/build/reference.html +++ b/docs/build/reference.html @@ -6,7 +6,7 @@ - Reference - Knowledge Commons Works 0.3.3 documentation + Reference - Knowledge Commons Works 0.3.5 documentation @@ -166,7 +166,7 @@

    diff --git a/docs/build/searchindex.js b/docs/build/searchindex.js index a223e31e0..866369f0f 100644 --- a/docs/build/searchindex.js +++ b/docs/build/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"0.3.0-beta3 (2024-11-30)": [[0, "beta3-2024-11-30"]], "0.3.1-beta4 (2024-12-10)": [[0, "beta4-2024-12-10"]], "0.3.2-beta5 (2024-12-11)": [[0, "beta5-2024-12-11"]], "0.3.3-beta6 (2024-12-18)": [[0, "beta6-2024-12-18"]], "1. Clone the git repository": [[9, "clone-the-git-repository"]], "2. Create your configuration files": [[9, "create-your-configuration-files"]], "3. Start the docker-compose project": [[9, "start-the-docker-compose-project"]], "4. Initialize the database and other services, and build asset files": [[9, "initialize-the-database-and-other-services-and-build-asset-files"]], "5. Create your own admin user": [[9, "create-your-own-admin-user"]], "6. View the application": [[9, "view-the-application"]], "About": [[1, "about"]], "Add and Configure an Environment File": [[6, "add-and-configure-an-environment-file"]], "Adding new entry points": [[5, "adding-new-entry-points"]], "Adding new node.js packages to be included": [[5, "adding-new-node-js-packages-to-be-included"]], "Additional environment variables with sensitive information": [[6, "additional-environment-variables-with-sensitive-information"]], "Additional required environment variables with paths on your local file system": [[6, "additional-required-environment-variables-with-paths-on-your-local-file-system"]], "Build and Configure the Containerized Services": [[6, "build-and-configure-the-containerized-services"]], "Build and start the containers": [[6, "build-and-start-the-containers"]], "Bulk Record Import (invenio-record-importer-kcworks)": [[4, "bulk-record-import-invenio-record-importer-kcworks"]], "CLI Commands": [[2, "cli-commands"]], "Changes": [[0, "changes"]], "Changes to external python modules (including Invenio modules)": [[5, "changes-to-external-python-modules-including-invenio-modules"]], "Changes to html template files": [[5, "changes-to-html-template-files"]], "Changes to invenio.cfg": [[5, "changes-to-invenio-cfg"]], "Changes to python code in the site folder": [[5, "changes-to-python-code-in-the-site-folder"]], "Changes to static files": [[5, "changes-to-static-files"]], "Changes to theme (CSS) and javascript files": [[5, "changes-to-theme-css-and-javascript-files"]], "Clone the knowledge-commons-works Code": [[6, "clone-the-knowledge-commons-works-code"]], "Collections": [[4, "collections"]], "Collections for KC Groups (invenio-group-collections-kcworks)": [[4, "collections-for-kc-groups-invenio-group-collections-kcworks"]], "Commit strategy": [[5, "commit-strategy"]], "Configuration of InvenioRDM": [[3, "configuration-of-inveniordm"]], "Content moderation notifications": [[4, "content-moderation-notifications"]], "Contents:": [[7, null]], "Controlled Vocabularies": [[10, "controlled-vocabularies"]], "Controlling containerized nginx server": [[6, "controlling-containerized-nginx-server"]], "Controlling just the containerized services": [[6, "controlling-just-the-containerized-services"]], "Controlling the Application Services": [[6, "controlling-the-application-services"]], "Controlling the KCWorks (Flask) application": [[9, "controlling-the-kcworks-flask-application"]], "Copyright": [[1, "copyright"]], "Create an admin user": [[6, "create-an-admin-user"]], "Create and initialize the database, search indices, and task queue": [[6, "create-and-initialize-the-database-search-indices-and-task-queue"]], "Customizations to InvenioRDM": [[4, "customizations-to-inveniordm"]], "Deposit Form Customizations": [[4, "deposit-form-customizations"]], "Developing KCWorks": [[5, "developing-kcworks"]], "Digging deeper": [[5, "digging-deeper"]], "Docker log rotation": [[6, "docker-log-rotation"]], "Email templates": [[4, "email-templates"]], "Ensure some version of python is installed": [[6, "ensure-some-version-of-python-is-installed"]], "Example JSON record": [[10, "example-json-record"]], "FAST": [[10, "fast"]], "Fixing docker-compose \u201cnot found\u201d error": [[6, "fixing-docker-compose-not-found-error"]], "Forked Core Invenio Modules": [[4, "forked-core-invenio-modules"]], "Full local development setup": [[9, "full-local-development-setup"]], "Git Branching Strategy": [[5, "git-branching-strategy"]], "Git Submodules": [[5, "git-submodules"]], "HC Legacy Custom Fields": [[4, "hc-legacy-custom-fields"], [10, "hc-legacy-custom-fields"]], "Homosaurus": [[10, "homosaurus"]], "In-app notifications": [[4, "in-app-notifications"]], "In-depth Installation Instructions (NEEDS UPDATING)": [[6, "in-depth-installation-instructions-needs-updating"]], "Indices and tables": [[7, "indices-and-tables"]], "Install Docker 20.10.10+ and Docker-compose 1.17.0+": [[6, "install-docker-20-10-10-and-docker-compose-1-17-0"]], "Install Node.js and NVM": [[6, "install-node-js-and-nvm"]], "Install Python and Required Python Tools": [[6, "install-python-and-required-python-tools"]], "Install and enable Python 3.9.16": [[6, "install-and-enable-python-3-9-16"]], "Install pyenv and pipenv": [[6, "install-pyenv-and-pipenv"]], "Install the Invenio Python Modules": [[6, "install-the-invenio-python-modules"]], "Install the invenio-cli command line tool": [[6, "install-the-invenio-cli-command-line-tool"]], "Installation": [[9, "installation"]], "Integrations with KC": [[4, "integrations-with-kc"]], "InvenioRDM Documentation": [[11, "inveniordm-documentation"]], "Javascript tests": [[5, "javascript-tests"]], "KC Search Provisioning (invenio-remote-api-provisioner)": [[4, "kc-search-provisioning-invenio-remote-api-provisioner"]], "KCWorks Custom CLI Commands": [[2, "kcworks-custom-cli-commands"]], "KCWorks Custom Fields (kcworks/site/metadata_fields)": [[4, "kcworks-custom-fields-kcworks-site-metadata-fields"], [10, "kcworks-custom-fields-kcworks-site-metadata-fields"]], "KCWorks Infrastructure": [[8, "kcworks-infrastructure"]], "Linux": [[6, "linux"]], "MacOS": [[6, "macos"]], "Metadata Schema Customizations": [[4, "metadata-schema-customizations"]], "Metadata Schema and Vocabularies": [[10, "metadata-schema-and-vocabularies"]], "Modular Framework (invenio-modular-deposit-form)": [[4, "modular-framework-invenio-modular-deposit-form"]], "Modular Framework (invenio-modular-detail-page)": [[4, "modular-framework-invenio-modular-detail-page"]], "Naming Commits": [[5, "naming-commits"]], "Note about docker contexts": [[6, "note-about-docker-contexts"]], "Notes about Implementation of Core InvenioRDM Fields": [[4, "notes-about-implementation-of-core-inveniordm-fields"], [10, "notes-about-implementation-of-core-inveniordm-fields"]], "Notifications": [[4, "notifications"]], "Organizations": [[10, "organizations"]], "Overrides in the KCWorks Package (kcworks/site)": [[4, "overrides-in-the-kcworks-package-kcworks-site"], [4, "id1"]], "Page templates": [[4, "page-templates"]], "Python tests": [[5, "python-tests"]], "Quickstart": [[9, "quickstart"]], "ROR": [[10, "ror"]], "Rebuilding changed files on the fly (fast but limited)": [[5, "rebuilding-changed-files-on-the-fly-fast-but-limited"]], "Record Detail Page Customizations": [[4, "record-detail-page-customizations"]], "Reference": [[11, "reference"]], "Running CLI Commands in the KCWorks Container": [[2, "running-cli-commands-in-the-kcworks-container"]], "Running Invenio CLI Commands": [[2, "running-invenio-cli-commands"]], "Running automated tests (NEEDS UPDATING)": [[5, "running-automated-tests-needs-updating"]], "SAML Authentication": [[4, "saml-authentication"]], "Standardized environment variables": [[6, "standardized-environment-variables"]], "Start the uwsgi applications and celery worker": [[6, "start-the-uwsgi-applications-and-celery-worker"]], "Startup and shutdown scripts": [[6, "startup-and-shutdown-scripts"]], "Subject headings": [[10, "subject-headings"]], "Tagging Releases": [[5, "tagging-releases"]], "Template Customizations": [[4, "template-customizations"]], "The basic build process (slow)": [[5, "the-basic-build-process-slow"]], "Updating an Instance with Upstream Changes": [[5, "updating-an-instance-with-upstream-changes"]], "Updating the running KCWorks instance with development changes": [[5, "updating-the-running-kcworks-instance-with-development-changes"]], "Use the application!": [[6, "use-the-application"]], "User Data Sync (invenio-remote-user-data-kcworks)": [[4, "user-data-sync-invenio-remote-user-data-kcworks"]], "User-first-record notifications": [[4, "user-first-record-notifications"]], "Variables for local credentials": [[6, "variables-for-local-credentials"]], "Version Control": [[5, "version-control"]], "Version Numbering": [[5, "version-numbering"]], "View container logging output": [[6, "view-container-logging-output"]], "View logging output for uwsgi processes": [[6, "view-logging-output-for-uwsgi-processes"]], "Welcome to Knowledge Commons Works\u2019s documentation!": [[7, "welcome-to-knowledge-commons-works-s-documentation"]], "custom_fields.hclegacy:collection": [[4, "custom-fields-hclegacy-collection"], [10, "custom-fields-hclegacy-collection"]], "custom_fields.hclegacy:committee_deposit": [[4, "custom-fields-hclegacy-committee-deposit"], [10, "custom-fields-hclegacy-committee-deposit"]], "custom_fields.hclegacy:file_location": [[4, "custom-fields-hclegacy-file-location"], [10, "custom-fields-hclegacy-file-location"]], "custom_fields.hclegacy:file_pid": [[4, "custom-fields-hclegacy-file-pid"], [10, "custom-fields-hclegacy-file-pid"]], "custom_fields.hclegacy:groups_for_deposit": [[4, "custom-fields-hclegacy-groups-for-deposit"], [10, "custom-fields-hclegacy-groups-for-deposit"]], "custom_fields.hclegacy:previously_published": [[4, "custom-fields-hclegacy-previously-published"], [10, "custom-fields-hclegacy-previously-published"]], "custom_fields.hclegacy:publication_type": [[4, "custom-fields-hclegacy-publication-type"], [10, "custom-fields-hclegacy-publication-type"]], "custom_fields.hclegacy:record_change_date": [[4, "custom-fields-hclegacy-record-change-date"], [10, "custom-fields-hclegacy-record-change-date"]], "custom_fields.hclegacy:record_creation_date": [[4, "custom-fields-hclegacy-record-creation-date"], [10, "custom-fields-hclegacy-record-creation-date"]], "custom_fields.hclegacy:record_identifier": [[4, "custom-fields-hclegacy-record-identifier"], [10, "custom-fields-hclegacy-record-identifier"]], "custom_fields.hclegacy:submitter_affiliation": [[4, "custom-fields-hclegacy-submitter-affiliation"], [10, "custom-fields-hclegacy-submitter-affiliation"]], "custom_fields.hclegacy:submitter_id": [[4, "custom-fields-hclegacy-submitter-id"], [10, "custom-fields-hclegacy-submitter-id"]], "custom_fields.hclegacy:submitter_org_memberships": [[4, "custom-fields-hclegacy-submitter-org-memberships"], [10, "custom-fields-hclegacy-submitter-org-memberships"]], "custom_fields.hclegacy:total_downloads": [[4, "custom-fields-hclegacy-total-downloads"], [10, "custom-fields-hclegacy-total-downloads"]], "custom_fields.hclegacy:total_views": [[4, "custom-fields-hclegacy-total-views"], [10, "custom-fields-hclegacy-total-views"]], "invenio-communities": [[4, "invenio-communities"]], "invenio-rdm-records": [[4, "invenio-rdm-records"]], "invenio-records-resources": [[4, "invenio-records-resources"]], "invenio-vocabularies": [[4, "invenio-vocabularies"]], "kcr:ai_usage": [[4, "kcr-ai-usage"], [10, "kcr-ai-usage"]], "kcr:book_series": [[4, "kcr-book-series"], [10, "kcr-book-series"]], "kcr:chapter_label": [[4, "kcr-chapter-label"], [10, "kcr-chapter-label"]], "kcr:commons_domain": [[4, "kcr-commons-domain"], [10, "kcr-commons-domain"]], "kcr:commons_search_recid (system field)": [[4, "kcr-commons-search-recid-system-field"], [10, "kcr-commons-search-recid-system-field"]], "kcr:commons_search_updated (system field)": [[4, "kcr-commons-search-updated-system-field"], [10, "kcr-commons-search-updated-system-field"]], "kcr:content_warning": [[4, "kcr-content-warning"], [10, "kcr-content-warning"]], "kcr:course_title": [[4, "kcr-course-title"], [10, "kcr-course-title"]], "kcr:degree": [[4, "kcr-degree"], [10, "kcr-degree"]], "kcr:discipline": [[4, "kcr-discipline"], [10, "kcr-discipline"]], "kcr:edition": [[4, "kcr-edition"], [10, "kcr-edition"]], "kcr:institution_department": [[4, "kcr-institution-department"], [10, "kcr-institution-department"]], "kcr:media": [[4, "kcr-media"], [10, "kcr-media"]], "kcr:meeting_organization": [[4, "kcr-meeting-organization"], [10, "kcr-meeting-organization"]], "kcr:project_title": [[4, "kcr-project-title"], [10, "kcr-project-title"]], "kcr:publication_url": [[4, "kcr-publication-url"], [10, "kcr-publication-url"]], "kcr:sponsoring_institution": [[4, "kcr-sponsoring-institution"], [10, "kcr-sponsoring-institution"]], "kcr:submitter_email": [[4, "kcr-submitter-email"], [10, "kcr-submitter-email"]], "kcr:submitter_username": [[4, "kcr-submitter-username"], [10, "kcr-submitter-username"]], "kcr:user_defined_tags": [[4, "kcr-user-defined-tags"], [10, "kcr-user-defined-tags"]], "metadata.creators/metadata.contributors": [[4, "metadata-creators-metadata-contributors"], [10, "metadata-creators-metadata-contributors"]], "metadata.subjects": [[4, "metadata-subjects"], [10, "metadata-subjects"]]}, "docnames": ["CHANGES", "README", "cli_commands", "configuration", "customizations", "developing", "in_depth", "index", "infrastructure", "installation", "metadata", "reference"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["CHANGES.md", "README.md", "cli_commands.md", "configuration.md", "customizations.md", "developing.md", "in_depth.md", "index.rst", "infrastructure.md", "installation.md", "metadata.md", "reference.md"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"": [0, 2, 4, 5, 6, 10], "0": [1, 5, 7, 9, 10], "00": [4, 10], "0000": [4, 10], "0001": 10, "00k4n6c32": 10, "00z": [4, 10], "01": [4, 10], "01t00": [4, 10], "0378": 10, "04": 6, "06": 10, "09": 10, "1": [4, 5, 7, 10], "10": [7, 9, 10], "1001634": [4, 10], "1086436": 10, "11": [6, 7, 10], "111023": 10, "12": [6, 7, 9], "123": 10, "1234": 10, "123456": [4, 10], "12345abcd": 10, "1263": [4, 10], "17": [7, 9], "18": 7, "19": 6, "2": [5, 6, 7, 10], "20": [7, 9], "2018": 10, "2020": 10, "2022": 10, "2023": 1, "2024": [4, 7, 10], "2025": 10, "2029": 10, "22": 6, "2345": 10, "24": [1, 10], "251587": 10, "2nd": 10, "3": [1, 5, 7], "30": 7, "32": 10, "4": 10, "456": 10, "458": 10, "5601": [6, 9], "5955": 10, "6": 6, "60": 10, "6379": 6, "63932": 10, "6780": 10, "6789": 10, "8": 10, "8601": [4, 10], "8gb": 6, "9200": 6, "94682": 10, "958235": 10, "966892": 10, "A": [0, 4, 10], "At": 6, "But": 9, "By": 5, "For": [2, 4, 5, 6, 10], "If": [4, 5, 6, 9, 10], "In": [5, 7], "It": [0, 1, 2, 4, 5, 6, 9, 10], "NOT": 9, "No": 5, "On": [2, 6], "One": [4, 6, 10], "Or": [5, 6], "TO": 9, "That": [4, 10], "The": [0, 4, 6, 9, 10, 11], "Then": [2, 5, 6], "There": [5, 6], "These": [0, 2, 4, 5, 6, 9, 10], "To": [2, 5, 6, 9], "With": [0, 6], "__init__": 5, "__name__": 5, "abl": 6, "about": [5, 7], "abov": 5, "abus": [4, 10], "academ": [1, 4, 10], "access": [0, 6, 9, 10], "account": [4, 10], "acronym": 10, "activ": [5, 6, 9, 10], "actual": [6, 10], "ad": [0, 4], "add": [0, 4, 5, 7, 9, 10], "addit": [4, 5, 9, 10], "addition": 6, "additional_descript": 10, "additional_titl": 10, "address": [4, 6, 10], "admin": [4, 10], "administr": [6, 9], "advis": 6, "affect": 0, "affili": [4, 10], "after": [5, 6, 9], "ag": 10, "again": [5, 6], "against": 6, "aggreg": [2, 4, 10], "ai": [4, 10], "ai_descript": [4, 10], "ai_us": [4, 10], "alia": 6, "alias": 5, "all": [0, 2, 4, 5, 6, 9, 10], "allow": [0, 4, 6, 9, 10], "alon": 5, "along": [4, 5, 10], "alongsid": 6, "alreadi": [5, 6], "also": [0, 2, 4, 5, 6, 10], "altern": 6, "alwai": [0, 6], "ambigu": [4, 10], "american": [4, 10], "among": 0, "an": [0, 1, 4, 7, 9, 10, 11], "anaconda": 6, "ani": [2, 4, 5, 6, 10], "anoth": 6, "api": [0, 2, 5, 6, 9, 10], "api_token": 6, "app": [6, 9], "appear": [0, 9], "append": 6, "applic": [5, 7, 10], "approach": 6, "appropri": [6, 10], "ar": [0, 2, 4, 5, 6, 9, 10], "area": [4, 10], "aren": 6, "argument": 2, "aria": 0, "arli": 0, "arlisna": [4, 10], "arrai": [4, 10], "art": [4, 10], "articl": [4, 10], "artifact": [4, 10], "artist": [4, 10], "ask": 6, "asset": 5, "assign": [4, 6, 10], "assignfast": [4, 10], "associ": [4, 10], "assum": 6, "audio": [4, 10], "augment": 10, "austen": [4, 10], "author": 10, "auto": 0, "autom": 7, "automat": [4, 5, 6, 10], "avail": [2, 5, 6, 10], "avoid": 5, "aw": 6, "awar": [4, 6, 10], "award": 10, "b": 5, "back": [0, 2, 5], "backend": 4, "background": 6, "bar": [6, 10], "base": [4, 10], "bash": [2, 5, 6, 9], "bashrc": 6, "beat": 6, "becaus": [0, 4, 5, 6, 10], "been": [0, 4, 5, 6, 10], "befor": [2, 4, 5, 6, 10], "begin": [5, 6], "being": [0, 6], "belong": [4, 10], "below": [6, 9, 10], "best": [0, 6], "beta": 5, "beta3": 7, "beta4": 7, "beta5": 7, "beta6": [1, 5, 7], "between": [4, 5, 10], "beyond": [4, 10], "big": 6, "bin": 6, "bind": [6, 9], "block": 6, "blueski": 0, "book": [4, 10], "boolean": [4, 10], "boot": 6, "both": [0, 4], "break": 6, "browser": [5, 9], "bug": [0, 5], "bugfix": 5, "build": [4, 7], "builder": 4, "built": [1, 6, 11], "bulk": [2, 7], "bundl": 5, "c": [5, 6], "cach": 6, "call": [4, 6, 10], "can": [0, 2, 4, 5, 6, 9, 10, 11], "cannot": 5, "case": [4, 5], "caution": [2, 5, 9], "cc": 10, "cd": [5, 6, 9], "central": [4, 5, 10], "cern": 11, "ch": 11, "chang": [4, 6, 7, 9, 10], "change_m": 6, "chapter": [4, 10], "chatgpt": 10, "check": [4, 6], "chmod": 6, "choos": 0, "chose": 6, "chrome": 5, "chronolog": [4, 10], "class": 4, "clean": 5, "clearer": 0, "cli": [0, 7, 9], "click": 6, "client": 5, "clone": [5, 7], "cm": 10, "co": 6, "code": [7, 9, 10], "codebas": 9, "coderepositori": 10, "cog": 6, "collabor": [1, 10], "collect": [0, 2, 5, 7, 9], "com": [4, 6, 9, 10], "command": [0, 5, 7, 9], "commerci": 10, "commiss": [4, 10], "committe": [4, 10], "common": [0, 1, 4, 5, 9, 10, 11], "commons_api_token": 6, "commons_search_api_token": 6, "commun": [0, 9], "comparison": 0, "compil": 6, "complement": [4, 10], "complet": [4, 5, 9, 10], "compon": [0, 4, 5], "compos": [5, 7], "comput": 6, "condit": 10, "conf": 5, "confer": [4, 10], "config": [4, 6], "configur": [5, 7], "confus": 0, "consid": 5, "constrain": [4, 10], "contain": [0, 4, 5, 7, 9, 10], "container": 7, "content": [9, 10], "continu": 5, "control": [4, 7], "conveni": 6, "convert": [4, 10], "coordin": 10, "copi": [5, 6, 9], "copyright": 7, "core": [2, 7, 9], "corpor": [4, 10], "correct": [2, 6, 9], "correctli": 0, "correspond": [2, 4, 10], "could": 0, "count": 2, "cours": [4, 6, 10], "cover": [4, 10], "cpython": 6, "creat": [0, 2, 4, 5, 10], "creatibutorsfield": 0, "creation": [0, 4, 10], "creativecommon": 10, "credit": 10, "ctrl": [5, 6], "curl": 6, "current": [0, 5, 6], "custom": [0, 5, 6, 7, 11], "custom_pdf_viewer_j": 5, "d": [5, 6, 9], "daemon": 6, "dashboard": [6, 9], "data": [0, 2, 5, 6, 9, 10], "databas": [2, 4, 10], "dataset": 10, "date": [0, 4, 6, 10], "datetim": [4, 10], "db": 6, "de": 10, "debug": [5, 9], "declar": 5, "deeper": 7, "default": [0, 4, 5, 6, 10], "defin": [4, 5, 6, 10], "delet": [2, 5], "demo": 6, "depart": [4, 10], "depend": [5, 6, 9], "deploi": [2, 5], "deploy": 5, "deposit": [7, 9, 10], "depth": 7, "describ": [4, 5, 9, 10], "descript": 10, "descriptor": [4, 10], "desir": [0, 4, 10], "desktop": 6, "despair": 9, "destroi": [2, 6, 9], "detail": [0, 7, 9, 10], "determin": [4, 10], "dev": [6, 9], "develop": [2, 6, 7, 11], "developmentstatu": 10, "dict": 5, "dictionari": 5, "differ": [4, 9, 10], "differenti": [4, 10], "dig": 7, "digit": 10, "direction": 4, "directli": [5, 6], "directori": 6, "discoveri": [4, 10], "displai": [0, 2], "dissert": [4, 10], "distinct": [4, 5, 10], "distress": [4, 10], "distribut": 10, "divid": [0, 2, 4, 10], "divis": 0, "do": [5, 6, 9], "doc": [5, 6, 11], "docker": [2, 5, 7], "dockerfil": 6, "document": [2, 4, 5, 6, 10], "doe": [0, 4, 5, 6, 10], "doi": [0, 10], "domain": [4, 10], "don": 9, "done": [5, 6], "down": [6, 9], "download": [4, 6, 10], "dphil": [4, 10], "draft": 4, "driver": 6, "dummi": 6, "dure": [0, 2, 4, 5, 6, 9, 10], "dynam": [2, 5], "e": [0, 2, 4, 5, 9, 10], "e2": 5, "each": [0, 4, 5, 6, 9, 10], "easi": 6, "edit": 0, "educ": [4, 10], "effect": [4, 5, 10], "effici": [4, 10], "either": [2, 6, 9], "els": 6, "email": [2, 6, 9, 10], "emailbackend": 4, "embargo": 10, "emit": 4, "emploi": [4, 5, 10], "empti": 0, "emul": 5, "en": 10, "enabl": [4, 10], "encourag": [4, 10], "end": [0, 5], "eng": 10, "engin": 6, "english": 10, "enough": 6, "ensur": [5, 9], "enter": [5, 6, 9], "entri": [4, 10], "env": [6, 9], "environ": [5, 7, 9], "error": [0, 5], "especi": [0, 5, 6], "etc": [2, 4, 5, 9, 10], "europ": [4, 10], "even": [0, 4, 10], "event": [0, 2, 4, 6, 10], "everi": 5, "exampl": [2, 4, 5, 6, 7], "except": 5, "exec": [2, 5, 6, 9], "exist": [0, 2, 5, 6], "explain": [6, 9], "export": 2, "extens": [4, 5, 6], "extra": 6, "extrem": 2, "f": 6, "facet": [4, 10], "fall": 10, "famili": 0, "family_nam": 10, "fast": 4, "featur": [5, 10], "feedback": 9, "field": [0, 7], "file": [1, 2, 4, 7, 10], "fill": 0, "final": 6, "find": [0, 2, 5, 6, 9], "first": [2, 5, 6, 9], "firstrecordcreatednotificationbuild": 4, "firstrecordcreatednotificationservic": 4, "firstrecordpublishednotificationbuild": 4, "fix": [0, 5], "fixtur": [5, 6, 9], "flag": [4, 6, 10], "flask": [2, 4, 5, 7], "flexibli": [4, 10], "flow": 5, "folder": [6, 9], "follow": [2, 4, 5, 6, 9, 10], "foo": 10, "forc": [5, 6], "fork": [6, 7], "form": [0, 7, 9, 10], "format": 10, "formgenr": [4, 10], "found": [2, 4, 10, 11], "four": 5, "free": [4, 6, 10], "freecodecamp": 6, "from": [0, 2, 4, 5, 6, 9, 10], "frontend": 6, "full": [0, 6, 7], "function": 0, "fund": [4, 10], "funder": 10, "further": [2, 6, 9], "futur": 0, "g": [0, 2, 4, 5, 9, 10], "gender": 10, "gener": [2, 4, 5, 6, 10], "geograph": [4, 10], "geometri": 10, "geonam": 10, "geopattern": 5, "get": [0, 6], "gh_page": 5, "git": 6, "github": [5, 6, 9, 10], "gitlab": 5, "given": 0, "given_nam": 10, "gninx": 6, "good": 6, "grant": [4, 10], "grep": 2, "group": [2, 5, 6, 9, 10], "group_identifi": [4, 10], "group_nam": [4, 10], "guarante": 6, "guid": 6, "h1": 10, "ha": [0, 4, 5, 6, 10], "handl": 0, "have": [0, 4, 5, 6, 9, 10], "hcommon": [4, 10], "head": 4, "header": 0, "help": [2, 4, 5, 6, 10], "here": [4, 6, 9, 10], "hidden": 0, "histor": [4, 10], "histori": [4, 5, 10], "historian": [4, 10], "hold": [4, 6, 10], "homoit0000669": [4, 10], "homosauru": 4, "host": [4, 10], "hour": 9, "how": 2, "how2shout": 6, "html": [4, 6, 10], "http": [4, 5, 6, 9, 10, 11], "human": [4, 10], "hyphen": [4, 6, 10], "i": [0, 1, 2, 4, 5, 9, 10, 11], "icon": 6, "id": [0, 4, 10], "idea": 6, "ident": 10, "identifi": [0, 4, 10], "illustr": 10, "imag": [0, 5, 6], "immedi": 5, "implement": 7, "import": [2, 5, 6, 7, 9, 10], "import_data": 6, "impos": [4, 10], "imprint": 10, "includ": [0, 1, 2, 4, 6, 9, 10, 11], "increment": 5, "index": [2, 4, 6, 7, 10], "indic": [2, 9], "individu": 6, "info": [4, 6, 10], "inform": [5, 11], "informationen": 10, "infrastructur": [0, 7], "ini": 6, "init": [5, 9], "initi": [0, 5], "insert": [6, 9], "insid": [2, 5, 6, 9], "instal": [5, 7], "instanc": [1, 2, 6, 7, 9, 11], "instance_path": [6, 9], "instead": [0, 5, 6], "institut": [4, 10], "instruct": [4, 5, 7, 9, 10], "instructionalresourc": 10, "integ": [4, 10], "integr": [5, 7], "intend": [4, 10], "inter": [5, 9], "interact": 5, "interfac": 6, "intern": [4, 10], "interpret": 6, "intersex": [4, 10], "introduc": 5, "introduct": [4, 10], "invenio": [7, 9, 10], "invenio_": 6, "invenio_app": 6, "invenio_csrf_secret_salt": 6, "invenio_custom_pdf_view": 5, "invenio_datacite_password": 6, "invenio_instance_path": 6, "invenio_notif": 4, "invenio_record_importer_data_dir": 6, "invenio_record_importer_local_data_dir": 6, "invenio_search_domain": 6, "invenio_secret_kei": 6, "invenio_security_login_salt": 6, "invenio_site_api_url": 6, "invenio_site_ui_url": 6, "invenio_sqlalchemy_database_uri": 6, "inveniordm": [1, 2, 5, 6, 7], "invert": 0, "invok": 2, "involv": [0, 4, 5, 9, 10], "isbn": 10, "iscitedbi": 10, "iso": [4, 10], "issn": 10, "issu": 10, "item": [4, 10], "its": [0, 5, 6, 9], "itself": [4, 10], "j": [0, 7], "jammi": 6, "jane": [4, 10], "janedo": 10, "javascript": 10, "jdoe": [4, 10], "jest": 5, "jinja": 4, "john": [4, 10], "journal": [4, 10], "json": [2, 5, 6, 7], "kc": [0, 2, 6, 7, 10], "kc_usernam": [4, 10], "kcr": 6, "kcr_api": 6, "kcr_ui": 6, "kcwork": [0, 6, 7], "keep": [5, 6], "keyboard": 0, "kingston": [4, 10], "knowldg": 9, "knowledg": [0, 1, 5, 9, 11], "label": [0, 4, 10], "lago": 10, "lambda": 6, "lang": 10, "languag": 10, "last": [2, 4, 5, 10], "latest": 5, "latin": [4, 10], "launch": 10, "layout": 0, "lcsh": [4, 10], "lcsh2fast": [4, 10], "lead": 0, "least": [0, 6], "leav": 5, "leftov": 5, "legaci": 2, "legalcod": 10, "less": [4, 5, 10], "level": [4, 10], "lib": 6, "licens": [1, 10], "like": [5, 6], "likewis": 5, "line": [0, 2, 9], "link": [0, 2, 4, 10], "linux": 5, "list": [2, 4, 5, 10], "literatur": [4, 10], "live": [6, 9], "load": 2, "local": [2, 5, 7], "localhost": [6, 9], "locat": [4, 5, 10], "lock": 6, "log": 4, "loglevel": 6, "logo": 0, "long": [0, 5, 6], "lost": 2, "lowercas": [4, 10], "lt": 6, "m": 5, "ma": [4, 10], "mac": 6, "machin": 6, "made": [0, 5, 10], "mai": [4, 5, 6, 9, 10], "mail": 4, "main": [0, 2, 4, 5, 9, 10], "make": [5, 6, 9], "manag": [0, 5, 6], "manual": 6, "markdown": 4, "match": [0, 6], "materi": [4, 10], "matter": [4, 10], "md": 5, "mean": [0, 4, 5, 6, 10], "meantim": 5, "mechan": [4, 10], "media": 0, "meet": [4, 10], "member": 0, "memori": 6, "menu": 0, "merg": 5, "mesh": [1, 9], "messag": 0, "met": 10, "metadata": [0, 2, 7], "method": 10, "michigan": 10, "middl": 2, "might": [0, 4, 6, 10], "migrant": [4, 10], "migrat": [4, 10], "minor": 5, "minut": 6, "miss": 0, "mit": 1, "mla": [4, 10], "modal": 0, "moder": 10, "moderatorrolerecipi": 4, "modern": [4, 10], "modifi": [0, 5], "modul": [0, 7, 9], "modular": 9, "monotask": 5, "more": [4, 5, 10], "most": [0, 5, 6], "mount": [6, 9], "mq": 6, "msu": 10, "much": [4, 10], "multipl": 0, "must": [4, 5, 6, 10], "my": 10, "myapitoken": 6, "mycours": 10, "myevent": 10, "myinveniodatacitepassword": 6, "mytoken": 6, "na": 0, "name": [0, 2, 4, 6, 9, 10], "name_parts_loc": 0, "namespac": [4, 10], "navig": [0, 5, 6, 9], "nc": 10, "necessari": [0, 6, 9], "need": [2, 7, 9], "new": [0, 4, 6, 10], "newer": 6, "newli": [4, 10], "newlin": 6, "next": [5, 6], "nginx": 9, "node": 7, "node_modul": 5, "normal": [5, 6, 9], "note": [2, 5, 7, 9], "notif": 7, "notificationop": 4, "notifications_moderator_rol": 4, "now": [0, 6, 9], "npm": 5, "number": [2, 4, 7, 10], "nvm": 7, "o": 6, "object": [4, 5, 10], "obtain": 6, "occupi": 5, "oclc": [4, 10], "octob": 10, "often": [5, 6], "old": 6, "onc": [5, 6], "one": [4, 6, 9, 10], "onli": [0, 2, 4, 5, 10], "onto": 9, "open": 6, "opengraph": 0, "opensearch": [2, 6, 9], "oper": [4, 6, 9], "opt": [5, 6, 9], "option": [0, 2, 4, 5, 9, 10], "orcid": [0, 4, 10], "order": [0, 5], "org": [4, 5, 6, 10], "organ": 4, "organiz": [4, 10], "origin": 5, "other": [0, 4, 5, 6, 10], "otherwis": 6, "out": 6, "output": 5, "over": 5, "overrid": [5, 10], "overridden": 5, "own": [5, 6], "ownership": [4, 10], "p": [2, 9, 10], "packag": [2, 6, 9, 10], "page": [0, 5, 7, 9, 10], "panel": 6, "paper": [4, 10], "parent": [6, 9], "part": [0, 1, 2, 4, 10], "particular": [6, 9], "particularli": 5, "password": [2, 6, 9], "patch": 5, "path": [4, 10], "patienc": 9, "pdf": [4, 10], "pdfj": 5, "pedagogi": 10, "pend": 0, "perform": 6, "permiss": 0, "persist": [4, 5, 6, 10], "person": [4, 10], "person_or_org": [4, 10], "pgadmin": [6, 9], "pgadmin_default_email": 6, "pgadmin_default_password": 6, "phd": [4, 10], "pick": 5, "pid": [5, 6, 9], "pidfil": 6, "pip": [5, 6], "pipenv": [5, 9], "pipfil": 5, "place": [4, 10], "plaintext": 4, "platform": 0, "poetri": [4, 10], "point": [0, 10], "possibl": [5, 10], "postgres_db": 6, "postgres_password": 6, "postgres_us": 6, "postgresql": 6, "potenti": [4, 10], "practic": 5, "prefix": [4, 10], "present": [0, 4, 10], "preserv": [4, 10], "press": 6, "previous": [0, 4, 10], "primari": [0, 4, 10], "primarili": [2, 4, 10], "print": [6, 10], "prior": [4, 6, 10], "privat": [6, 9], "problem": 5, "problemat": [4, 10], "proce": 6, "proceed": [4, 10], "process": [2, 10], "produc": [0, 2, 4, 10], "product": [2, 4, 5, 10], "profil": [0, 4], "program": 10, "programminglanguag": 10, "project": [4, 5, 6, 10], "proper": [0, 4, 10], "properti": 0, "provid": [2, 4, 5, 6, 9, 10], "provision": [9, 10], "prune": 5, "psychologi": 10, "psycopg2": 6, "public": [0, 4, 10], "publication_d": 10, "publish": [0, 4, 10], "pull": [5, 6], "purpos": [4, 10], "push": 5, "put": 6, "py": [2, 5], "pypi": 6, "pyproject": 5, "pytest": 5, "python": [7, 9, 10], "python3": 6, "python_local_git_packages_path": 6, "python_local_site_packages_path": 6, "queri": 0, "quick": 9, "quickli": 6, "quickstart": 7, "rabbitmq": 6, "race": 10, "random": [0, 6], "rather": 6, "rdm": [6, 9, 10], "rdmrecord": 4, "re": [5, 6, 10], "reach": 5, "react": 5, "read": 2, "readabl": [4, 5, 10], "reader": [4, 10], "readi": 5, "readm": 5, "real": 6, "reason": [6, 10], "rebuild": 6, "rebuilt": 5, "receiv": [4, 6], "recent": [0, 5], "recipi": 4, "recommend": 6, "record": [0, 2, 5, 7, 9], "recreat": [5, 6], "recurs": [5, 9], "redi": 6, "redis_domain": 6, "redund": [5, 9], "refactor": 0, "refer": [5, 6, 7, 9, 10], "reflect": [5, 9], "refresh": 5, "refuge": [4, 10], "regardless": 6, "registri": 10, "rel": [4, 10], "relat": [5, 10], "related_identifi": 10, "relation_typ": 10, "releas": [1, 6], "reliabl": 6, "reload": [5, 6, 9], "remot": [0, 2, 5, 6, 9, 10], "remov": 0, "replac": 6, "repo": 5, "repositori": [1, 5, 6, 10], "repres": 5, "request": [0, 5, 6], "requir": [4, 5, 7, 9, 10], "research": [1, 4, 9, 10], "resid": [4, 10], "resourc": [0, 6, 9, 10], "resource_typ": 10, "respect": 5, "respond": 5, "respons": 6, "rest": [6, 9], "restart": [5, 6, 9], "restrict": 10, "result": 0, "right": [0, 6, 10], "role": [4, 6, 10], "root": [5, 6], "rout": 0, "run": [4, 6, 7, 9], "runner": 5, "same": [4, 6, 10], "sandbox": 10, "save": 4, "schema": [0, 7], "scheme": [4, 10], "scienc": [4, 10], "script": [5, 9], "search": [0, 2, 7, 9, 10], "second": [4, 10], "secret": 6, "section": 10, "secur": 6, "see": [1, 5, 6, 9], "seen": 6, "select": 0, "selenium": 5, "self": 0, "semant": [4, 5], "semver": 5, "send": 4, "sent": 4, "separ": [5, 6, 9], "seri": [4, 10], "serial": 2, "series_titl": [4, 10], "series_volum": [4, 10], "serv": [6, 9], "server": [5, 9], "serverless": 6, "servic": [0, 2, 4, 7, 10], "services_setup": 9, "session": 6, "set": [4, 5, 6, 9, 10], "setup": [5, 6, 7], "sever": 6, "sexual": 10, "sh": [5, 6, 9], "share": [0, 1, 6], "shell": 6, "should": [0, 4, 5, 6, 9, 10], "sidebar": 0, "signal": 4, "similarli": [4, 9, 10], "simpl": [4, 10], "simpli": [5, 6], "sinc": [0, 2, 5, 6], "singl": [2, 5, 10], "site": [2, 6, 9], "size": [6, 10], "sl": 6, "slackbot": 6, "slider": 6, "so": [0, 2, 4, 5, 6, 9, 10], "social": 0, "softwar": [4, 10], "solut": 6, "solv": 0, "some": [0, 4, 5, 9, 10], "someth": 6, "sometim": [0, 5], "sort": [0, 10], "sourc": [5, 6, 9], "specif": [0, 4, 6], "specifi": 6, "sponsor": [4, 10], "squash": 5, "src": 5, "stage": [2, 5, 6], "stamp": 6, "stand": 5, "standalon": 6, "standard": [4, 10], "start": 5, "stat": [2, 4, 10], "state": 10, "statement": 9, "static": 9, "statu": [4, 10], "stdout": 6, "step": [5, 6, 9], "stop": [5, 6, 9], "store": [1, 2, 4, 5, 6, 10], "string": [0, 4, 10], "strongli": [4, 10], "structur": [6, 10], "style": 5, "sub": [2, 4, 6, 10], "submiss": 0, "submit": 0, "submitt": [4, 10], "submodul": 9, "substant": [4, 10], "subtitl": 10, "sudo": 6, "suffix": 5, "suggest": [4, 10], "suit": 5, "suitabl": 2, "superus": 6, "supplement": [4, 10], "suppli": 6, "support": [5, 6], "sur": 6, "sure": [5, 6], "syllabi": [4, 10], "syllabu": 10, "sync": 0, "syntax": 6, "synthet": 2, "system": [0, 1, 2, 5, 9], "systemd": 6, "t": [6, 9], "tab": 6, "tabl": 6, "tag": [4, 10], "tail": 6, "take": [5, 6, 9], "task": 5, "teach": 10, "technic": [0, 10], "technisch": 10, "tell": 6, "templat": 7, "temporari": 5, "termin": [5, 6], "test": [6, 7, 9, 10], "than": 6, "thei": [0, 4, 5, 10], "them": [0, 5, 6], "theses": [4, 10], "thesi": [4, 10], "thi": [0, 2, 4, 5, 6, 9, 10], "thing": 0, "those": [4, 5, 6, 9, 10], "though": 5, "three": 6, "through": 5, "time": [4, 5, 6, 9, 10], "titl": [0, 4, 10], "tmp": [5, 6, 9], "token": 6, "token_hex": 6, "toml": 5, "tool": [1, 4, 7, 10], "top": [4, 6, 10], "topic": [4, 10], "toronto": [4, 10], "total": [4, 10], "total_volum": 10, "track": 4, "true": [4, 9, 10], "try": 5, "tweak": 0, "two": [0, 4, 5, 6, 10], "txt": 1, "type": [0, 4, 10], "u": [4, 6, 10], "ubuntu": 6, "ui": [0, 2, 4, 5, 6, 9], "ukranian": [4, 10], "ultim": 5, "under": [1, 5, 6, 9], "underli": 2, "uni": 4, "unit": [4, 5], "univers": [4, 10], "unknown": 0, "unless": [5, 6, 9], "unlik": [4, 10], "unpublish": [4, 10], "unread": 4, "until": [5, 10], "up": [2, 5, 6, 9], "updat": [0, 2, 4, 7, 9, 10], "upload": [0, 5], "upstream": 7, "url": [4, 10], "us": [0, 2, 4, 5, 7, 9, 10], "usag": [2, 4, 10], "user": [0, 2, 5, 10], "usermod": 6, "usernam": [4, 10], "usr": 6, "usual": 5, "uwsgi": [5, 9], "uwsgi_api": 9, "uwsgi_rest": 6, "uwsgi_ui": [5, 6, 9], "v": [5, 6], "v1": 10, "v16": 6, "v2": 6, "v3": [4, 10], "valid": [4, 10], "valu": [0, 4, 6, 10], "var": [6, 9], "variabl": [4, 5], "variat": [4, 10], "varieti": 9, "variou": [6, 9, 10], "veri": 5, "version": [0, 1, 4, 7, 9, 10], "via": [4, 5, 6], "view": [0, 4, 5, 10], "virtual": 6, "virtualenv": 6, "visibl": [0, 5], "vocabulari": [7, 9], "volum": [4, 5, 6, 10], "voluntarili": [4, 10], "wa": [0, 4, 10], "wai": [4, 6, 10], "walk": 5, "want": [4, 5, 6, 9, 10], "warn": [2, 4, 5, 10], "watch": [5, 6], "watercolor": [4, 10], "we": 5, "web": [5, 6, 9], "webdriv": 5, "webhook": 4, "webpack": 5, "webpackthemebundl": 5, "well": [0, 4, 5, 6], "were": [0, 4, 6, 10], "what": [5, 10], "when": [0, 4, 5, 6, 9, 10], "whenev": [2, 5], "where": [0, 5, 6, 9], "wherev": 5, "whether": 4, "which": [0, 4, 5, 6, 9, 10], "whichev": 6, "while": [5, 9], "white": [4, 10], "whole": [0, 4, 10], "whose": 0, "widget": 0, "wikidata": 10, "window": [5, 6], "wip": 5, "within": [0, 4, 10], "without": [5, 6], "word": 0, "work": [0, 1, 4, 5, 9, 10, 11], "worker": [2, 5], "workshop": [4, 10], "worldcat": [4, 10], "would": [0, 2, 4, 5, 6, 10], "wrap": [0, 2], "written": 6, "wsl2": 5, "www": [4, 6, 10], "x": [6, 10], "x86_64": 6, "ye": 5, "yet": 6, "yml": [5, 6, 9], "you": [2, 4, 5, 6, 9, 10], "your": 5, "zenodo": 10, "zshrc": 6}, "titles": ["Changes", "About", "CLI Commands", "Configuration of InvenioRDM", "Customizations to InvenioRDM", "Developing KCWorks", "In-depth Installation Instructions (NEEDS UPDATING)", "Welcome to Knowledge Commons Works\u2019s documentation!", "KCWorks Infrastructure", "Installation", "Metadata Schema and Vocabularies", "Reference"], "titleterms": {"": 7, "0": [0, 6], "1": [0, 6, 9], "10": [0, 6], "11": 0, "12": 0, "16": 6, "17": 6, "18": 0, "2": [0, 9], "20": 6, "2024": 0, "3": [0, 6, 9], "30": 0, "4": 9, "5": 9, "6": 9, "9": 6, "In": [4, 6], "The": 5, "about": [1, 4, 6, 10], "ad": 5, "add": 6, "addit": 6, "admin": [6, 9], "ai_usag": [4, 10], "an": [5, 6], "api": 4, "app": 4, "applic": [6, 9], "asset": 9, "authent": 4, "autom": 5, "basic": 5, "beta3": 0, "beta4": 0, "beta5": 0, "beta6": 0, "book_seri": [4, 10], "branch": 5, "build": [5, 6, 9], "bulk": 4, "celeri": 6, "cfg": 5, "chang": [0, 5], "chapter_label": [4, 10], "cli": [2, 6], "clone": [6, 9], "code": [5, 6], "collect": [4, 10], "command": [2, 6], "commit": 5, "committee_deposit": [4, 10], "common": [6, 7], "commons_domain": [4, 10], "commons_search_recid": [4, 10], "commons_search_upd": [4, 10], "commun": 4, "compos": [6, 9], "configur": [3, 6, 9], "contain": [2, 6], "container": 6, "content": [4, 7], "content_warn": [4, 10], "context": 6, "contributor": [4, 10], "control": [5, 6, 9, 10], "copyright": 1, "core": [4, 10], "course_titl": [4, 10], "creat": [6, 9], "creator": [4, 10], "credenti": 6, "css": 5, "custom": [2, 4, 10], "custom_field": [4, 10], "data": 4, "databas": [6, 9], "deeper": 5, "degre": [4, 10], "deposit": 4, "depth": 6, "detail": 4, "develop": [5, 9], "dig": 5, "disciplin": [4, 10], "docker": [6, 9], "document": [7, 11], "edit": [4, 10], "email": 4, "enabl": 6, "ensur": 6, "entri": 5, "environ": 6, "error": 6, "exampl": 10, "extern": 5, "fast": [5, 10], "field": [4, 10], "file": [5, 6, 9], "file_loc": [4, 10], "file_pid": [4, 10], "first": 4, "fix": 6, "flask": 9, "fly": 5, "folder": 5, "fork": 4, "form": 4, "found": 6, "framework": 4, "full": 9, "git": [5, 9], "group": 4, "groups_for_deposit": [4, 10], "hc": [4, 10], "hclegaci": [4, 10], "head": 10, "homosauru": 10, "html": 5, "i": 6, "implement": [4, 10], "import": 4, "includ": 5, "indic": [6, 7], "inform": 6, "infrastructur": 8, "initi": [6, 9], "instal": [6, 9], "instanc": 5, "institution_depart": [4, 10], "instruct": 6, "integr": 4, "invenio": [2, 4, 5, 6], "inveniordm": [3, 4, 10, 11], "j": [5, 6], "javascript": 5, "json": 10, "just": 6, "kc": 4, "kcr": [4, 10], "kcwork": [2, 4, 5, 8, 9, 10], "knowledg": [6, 7], "legaci": [4, 10], "limit": 5, "line": 6, "linux": 6, "local": [6, 9], "log": 6, "maco": 6, "media": [4, 10], "meeting_organ": [4, 10], "metadata": [4, 10], "metadata_field": [4, 10], "moder": 4, "modul": [4, 5, 6], "modular": 4, "name": 5, "need": [5, 6], "new": 5, "nginx": 6, "node": [5, 6], "note": [4, 6, 10], "notif": 4, "number": 5, "nvm": 6, "organ": 10, "other": 9, "output": 6, "overrid": 4, "own": 9, "packag": [4, 5], "page": 4, "path": 6, "pipenv": 6, "point": 5, "previously_publish": [4, 10], "process": [5, 6], "project": 9, "project_titl": [4, 10], "provis": 4, "provision": 4, "publication_typ": [4, 10], "publication_url": [4, 10], "pyenv": 6, "python": [5, 6], "queue": 6, "quickstart": 9, "rdm": 4, "rebuild": 5, "record": [4, 10], "record_change_d": [4, 10], "record_creation_d": [4, 10], "record_identifi": [4, 10], "refer": 11, "releas": 5, "remot": 4, "repositori": 9, "requir": 6, "resourc": 4, "ror": 10, "rotat": 6, "run": [2, 5], "saml": 4, "schema": [4, 10], "script": 6, "search": [4, 6], "sensit": 6, "server": 6, "servic": [6, 9], "setup": 9, "shutdown": 6, "site": [4, 5, 10], "slow": 5, "some": 6, "sponsoring_institut": [4, 10], "standard": 6, "start": [6, 9], "startup": 6, "static": 5, "strategi": 5, "subject": [4, 10], "submitter_affili": [4, 10], "submitter_email": [4, 10], "submitter_id": [4, 10], "submitter_org_membership": [4, 10], "submitter_usernam": [4, 10], "submodul": 5, "sync": 4, "system": [4, 6, 10], "tabl": 7, "tag": 5, "task": 6, "templat": [4, 5], "test": 5, "theme": 5, "tool": 6, "total_download": [4, 10], "total_view": [4, 10], "updat": [5, 6], "upstream": 5, "us": 6, "user": [4, 6, 9], "user_defined_tag": [4, 10], "uwsgi": 6, "variabl": 6, "version": [5, 6], "view": [6, 9], "vocabulari": [4, 10], "welcom": 7, "work": [6, 7], "worker": 6, "your": [6, 9]}}) \ No newline at end of file +Search.setIndex({"alltitles": {"0.3.0-beta3 (2024-11-30)": [[3, "beta3-2024-11-30"]], "0.3.1-beta4 (2024-12-10)": [[3, "beta4-2024-12-10"]], "0.3.2-beta5 (2024-12-11)": [[3, "beta5-2024-12-11"]], "0.3.3-beta6 (2024-12-18)": [[3, "beta6-2024-12-18"]], "0.3.4-beta7 (2025-01-09)": [[3, "beta7-2025-01-09"]], "0.3.5-beta8 (2025-01-10)": [[3, "beta8-2025-01-10"]], "1. Clone the git repository": [[11, "clone-the-git-repository"]], "2. Create your configuration files": [[11, "create-your-configuration-files"]], "3. Start the docker-compose project": [[11, "start-the-docker-compose-project"]], "4. Initialize the database and other services, and build asset files": [[11, "initialize-the-database-and-other-services-and-build-asset-files"]], "5. Create your own admin user": [[11, "create-your-own-admin-user"]], "6. View the application": [[11, "view-the-application"]], "A successful import response": [[1, "a-successful-import-response"]], "API": [[1, "api"]], "About": [[0, "about"]], "Add and Configure an Environment File": [[8, "add-and-configure-an-environment-file"]], "Adding new entry points": [[7, "adding-new-entry-points"]], "Adding new node.js packages to be included": [[7, "adding-new-node-js-packages-to-be-included"]], "Additional environment variables with sensitive information": [[8, "additional-environment-variables-with-sensitive-information"]], "Additional required environment variables with paths on your local file system": [[8, "additional-required-environment-variables-with-paths-on-your-local-file-system"]], "An unsuccessful import response": [[1, "an-unsuccessful-import-response"]], "Attaching configuration to the service": [[2, "attaching-configuration-to-the-service"]], "Augmented RecordService": [[2, "augmented-recordservice"]], "BaseService": [[2, "baseservice"]], "BaseServiceComponent": [[2, "baseservicecomponent"]], "Build and Configure the Containerized Services": [[8, "build-and-configure-the-containerized-services"]], "Build and start the containers": [[8, "build-and-start-the-containers"]], "Bulk Record Import (invenio-record-importer-kcworks)": [[6, "bulk-record-import-invenio-record-importer-kcworks"]], "CLI Commands": [[4, "cli-commands"]], "Changes": [[3, "changes"]], "Changes to external python modules (including Invenio modules)": [[7, "changes-to-external-python-modules-including-invenio-modules"]], "Changes to html template files": [[7, "changes-to-html-template-files"]], "Changes to invenio.cfg": [[7, "changes-to-invenio-cfg"]], "Changes to python code in the site folder": [[7, "changes-to-python-code-in-the-site-folder"]], "Changes to static files": [[7, "changes-to-static-files"]], "Changes to theme (CSS) and javascript files": [[7, "changes-to-theme-css-and-javascript-files"]], "Changing the Group Ownership of a Collection (PATCH)": [[1, "changing-the-group-ownership-of-a-collection-patch"]], "Clone the knowledge-commons-works Code": [[8, "clone-the-knowledge-commons-works-code"]], "Collections": [[6, "collections"]], "Collections for KC Groups (invenio-group-collections-kcworks)": [[6, "collections-for-kc-groups-invenio-group-collections-kcworks"]], "Commit strategy": [[7, "commit-strategy"]], "Configuration of InvenioRDM": [[5, "configuration-of-inveniordm"]], "Content moderation notifications": [[6, "content-moderation-notifications"]], "Contents:": [[9, null]], "Controlled Vocabularies": [[12, "controlled-vocabularies"]], "Controlling containerized nginx server": [[8, "controlling-containerized-nginx-server"]], "Controlling just the containerized services": [[8, "controlling-just-the-containerized-services"]], "Controlling the Application Services": [[8, "controlling-the-application-services"]], "Controlling the KCWorks (Flask) application": [[11, "controlling-the-kcworks-flask-application"]], "Copyright": [[0, "copyright"]], "Create an admin user": [[8, "create-an-admin-user"]], "Create and initialize the database, search indices, and task queue": [[8, "create-and-initialize-the-database-search-indices-and-task-queue"]], "Creating a Collection for a Group (POST)": [[1, "creating-a-collection-for-a-group-post"]], "Creating a new Work via the InvenioRDM REST API": [[1, "creating-a-new-work-via-the-inveniordm-rest-api"]], "Creator/contributor roles": [[12, "creator-contributor-roles"]], "Customizations to InvenioRDM": [[6, "customizations-to-inveniordm"]], "DOI": [[12, "doi"]], "DOI (primary identifier)": [[12, "doi-primary-identifier"]], "Deleting a Group\u2019s Collection (DELETE)": [[1, "deleting-a-group-s-collection-delete"]], "Deposit Form Customizations": [[6, "deposit-form-customizations"]], "Developing KCWorks": [[7, "developing-kcworks"]], "Digging deeper": [[7, "digging-deeper"]], "Docker log rotation": [[8, "docker-log-rotation"]], "Email templates": [[6, "email-templates"]], "Endpoint configuration": [[1, "endpoint-configuration"]], "Ensure some version of python is installed": [[8, "ensure-some-version-of-python-is-installed"]], "Error responses": [[1, "error-responses"]], "Event timing": [[1, "event-timing"]], "Example metadata record": [[12, "example-metadata-record"]], "FAST": [[12, "fast"]], "File service configuration": [[2, "file-service-configuration"]], "Fixing docker-compose \u201cnot found\u201d error": [[8, "fixing-docker-compose-not-found-error"]], "Forked Core Invenio Modules": [[6, "forked-core-invenio-modules"]], "FromConfigSearchOptions": [[2, "fromconfigsearchoptions"]], "Full local development setup": [[11, "full-local-development-setup"]], "Funders": [[12, "funders"]], "GET requests": [[1, "get-requests"]], "GND": [[12, "gnd"], [12, "id2"]], "Git Branching Strategy": [[7, "git-branching-strategy"]], "Git Submodules": [[7, "git-submodules"]], "Grid (deprecated)": [[12, "grid-deprecated"]], "Group Collections API": [[1, "group-collections-api"]], "Group collection owner": [[1, "group-collection-owner"]], "HC Legacy Custom Fields": [[6, "hc-legacy-custom-fields"], [12, "hc-legacy-custom-fields"]], "Handle (secondary identifier)": [[12, "handle-secondary-identifier"]], "Handling collection name collisions": [[1, "handling-collection-name-collisions"]], "Handling deleted group collections": [[1, "handling-deleted-group-collections"]], "Handling group name changes": [[1, "handling-group-name-changes"]], "Homosaurus": [[12, "homosaurus"]], "ISBN (secondary identifier)": [[12, "isbn-secondary-identifier"]], "ISNI": [[12, "isni"]], "ISSN (secondary identifier)": [[12, "issn-secondary-identifier"]], "Identifier Schemes": [[12, "identifier-schemes"]], "In-app notifications": [[6, "in-app-notifications"]], "In-depth Installation Instructions (NEEDS UPDATING)": [[8, "in-depth-installation-instructions-needs-updating"]], "Install Docker 20.10.10+ and Docker-compose 1.17.0+": [[8, "install-docker-20-10-10-and-docker-compose-1-17-0"]], "Install Node.js and NVM": [[8, "install-node-js-and-nvm"]], "Install Python and Required Python Tools": [[8, "install-python-and-required-python-tools"]], "Install and enable Python 3.9.16": [[8, "install-and-enable-python-3-9-16"]], "Install pyenv and pipenv": [[8, "install-pyenv-and-pipenv"]], "Install the Invenio Python Modules": [[8, "install-the-invenio-python-modules"]], "Install the invenio-cli command line tool": [[8, "install-the-invenio-cli-command-line-tool"]], "Installation": [[11, "installation"]], "Integrations with KC": [[6, "integrations-with-kc"]], "InvenioRDM Documentation": [[13, "inveniordm-documentation"]], "InvenioRDM Services": [[2, "inveniordm-services"]], "InvenioRDM\u2019s Layered Architecture": [[2, "inveniordm-s-layered-architecture"]], "JSON object for record creation": [[12, "json-object-for-record-creation"]], "JSON object retrieved from the record API": [[12, "json-object-retrieved-from-the-record-api"]], "Javascript tests": [[7, "javascript-tests"]], "KC Search Provisioning (invenio-remote-api-provisioner)": [[6, "kc-search-provisioning-invenio-remote-api-provisioner"]], "KC Username (recommended)": [[12, "kc-username-recommended"]], "KCWorks Architecture": [[2, "kcworks-architecture"]], "KCWorks Custom CLI Commands": [[4, "kcworks-custom-cli-commands"]], "KCWorks Custom Fields (kcworks/site/metadata_fields)": [[6, "kcworks-custom-fields-kcworks-site-metadata-fields"], [12, "kcworks-custom-fields-kcworks-site-metadata-fields"]], "KCWorks Implementation of Core InvenioRDM Fields": [[12, "kcworks-implementation-of-core-inveniordm-fields"]], "KCWorks Infrastructure": [[10, "kcworks-infrastructure"]], "Linux": [[8, "linux"]], "Loading configuration from app config variables": [[2, "loading-configuration-from-app-config-variables"]], "MacOS": [[8, "macos"]], "Making duplicate import requests": [[1, "making-duplicate-import-requests"]], "Metadata Schema Customizations": [[6, "metadata-schema-customizations"]], "Metadata Schema, Vocabularies, and Identifiers": [[12, "metadata-schema-vocabularies-and-identifiers"]], "Modular Framework (invenio-modular-deposit-form)": [[6, "modular-framework-invenio-modular-deposit-form"]], "Modular Framework (invenio-modular-detail-page)": [[6, "modular-framework-invenio-modular-detail-page"]], "Naming Commits": [[7, "naming-commits"]], "Note about docker contexts": [[8, "note-about-docker-contexts"]], "Notes about Implementation of Core InvenioRDM Fields": [[6, "notes-about-implementation-of-core-inveniordm-fields"]], "Notifications": [[6, "notifications"]], "OAI (secondary identifier)": [[12, "oai-secondary-identifier"]], "OFR": [[12, "ofr"]], "ORCID (recommended)": [[12, "orcid-recommended"]], "Only some of the works to be imported failed": [[1, "only-some-of-the-works-to-be-imported-failed"]], "Organizations": [[12, "organizations"]], "Overrides in the KCWorks Package (kcworks/site)": [[6, "overrides-in-the-kcworks-package-kcworks-site"], [6, "id1"]], "POST requests": [[1, "post-requests"]], "Page templates": [[6, "page-templates"]], "Pagination": [[1, "pagination"]], "Payload objects": [[1, "payload-objects"]], "People": [[12, "people"]], "Permissions and access in newly created collections": [[1, "permissions-and-access-in-newly-created-collections"]], "Python tests": [[7, "python-tests"]], "Query parameters": [[1, "query-parameters"]], "Quickstart": [[11, "quickstart"]], "RDMRecordService": [[2, "rdmrecordservice"]], "RDMRecordService Components": [[2, "rdmrecordservice-components"]], "ROR (recommended)": [[12, "ror-recommended"]], "Rebuilding changed files on the fly (fast but limited)": [[7, "rebuilding-changed-files-on-the-fly-fast-but-limited"]], "Record Detail Page Customizations": [[6, "record-detail-page-customizations"]], "RecordService": [[2, "recordservice"]], "RecordService Components": [[2, "recordservice-components"]], "Reference": [[13, "reference"]], "Request": [[1, "request"], [1, "id6"], [1, "id7"], [1, "id11"], [1, "id15"], [1, "id18"], [1, "id22"], [1, "id27"]], "Request body": [[1, "request-body"], [1, "id19"], [1, "id23"]], "Requesting a specific collection": [[1, "requesting-a-specific-collection"]], "Requesting all collections": [[1, "requesting-all-collections"]], "Requesting collections for a Commons instance": [[1, "requesting-collections-for-a-commons-instance"]], "Requesting collections for a specific group": [[1, "requesting-collections-for-a-specific-group"]], "Required headers": [[1, "required-headers"]], "Resource types": [[12, "resource-types"]], "Retrieving Group Collection Metadata (GET)": [[1, "retrieving-group-collection-metadata-get"]], "Running CLI Commands in the KCWorks Container": [[4, "running-cli-commands-in-the-kcworks-container"]], "Running Invenio CLI Commands": [[4, "running-invenio-cli-commands"]], "Running automated tests (NEEDS UPDATING)": [[7, "running-automated-tests-needs-updating"]], "SAML Authentication": [[6, "saml-authentication"]], "Search configuration": [[2, "search-configuration"]], "SearchConfig": [[2, "searchconfig"]], "SearchOptionsMixin": [[2, "searchoptionsmixin"]], "Service Classes": [[2, "service-classes"]], "Service Components": [[2, "service-components"]], "Service Configuration": [[2, "service-configuration"]], "Sorting": [[1, "sorting"]], "Standardized environment variables": [[8, "standardized-environment-variables"]], "Start the uwsgi applications and celery worker": [[8, "start-the-uwsgi-applications-and-celery-worker"]], "Startup and shutdown scripts": [[8, "startup-and-shutdown-scripts"]], "Streamlined Import API": [[1, "streamlined-import-api"]], "Subject headings": [[12, "subject-headings"]], "Success responses": [[1, "success-responses"]], "Successful Response Body:": [[1, "id9"], [1, "id13"], [1, "id17"]], "Successful Response Headers": [[1, "successful-response-headers"]], "Successful Response Status Code": [[1, "successful-response-status-code"], [1, "id16"]], "Successful response body": [[1, "successful-response-body"], [1, "id21"], [1, "id25"]], "Successful response headers": [[1, "id10"], [1, "id14"]], "Successful response status code": [[1, "id8"], [1, "id12"], [1, "id20"], [1, "id24"], [1, "id28"]], "Tagging Releases": [[7, "tagging-releases"]], "Template Customizations": [[6, "template-customizations"]], "The InvenioRDM REST API": [[1, "the-inveniordm-rest-api"]], "The basic build process (slow)": [[7, "the-basic-build-process-slow"]], "The import request": [[1, "the-import-request"]], "The request file upload failed": [[1, "the-request-file-upload-failed"]], "The request metadata is malformed or invalid": [[1, "the-request-metadata-is-malformed-or-invalid"]], "The token does not have the necessary permissions": [[1, "the-token-does-not-have-the-necessary-permissions"]], "Unsuccessful response codes": [[1, "unsuccessful-response-codes"], [1, "id26"], [1, "id29"]], "Updating an Instance with Upstream Changes": [[7, "updating-an-instance-with-upstream-changes"]], "Updating the running KCWorks instance with development changes": [[7, "updating-the-running-kcworks-instance-with-development-changes"]], "Use the application!": [[8, "use-the-application"]], "User Data Sync (invenio-remote-user-data-kcworks)": [[6, "user-data-sync-invenio-remote-user-data-kcworks"]], "User and Group Data Updates (Internal Only)": [[1, "user-and-group-data-updates-internal-only"]], "User-first-record notifications": [[6, "user-first-record-notifications"]], "User/Groups Metadata updates and SAML authentication": [[1, "user-groups-metadata-updates-and-saml-authentication"]], "Variables for local credentials": [[8, "variables-for-local-credentials"]], "Version Control": [[7, "version-control"]], "Version Numbering": [[7, "version-numbering"]], "View container logging output": [[8, "view-container-logging-output"]], "View logging output for uwsgi processes": [[8, "view-logging-output-for-uwsgi-processes"]], "Welcome to the Knowledge Commons Works technical documentation!": [[9, "welcome-to-the-knowledge-commons-works-technical-documentation"]], "What happens to an import request that fails?": [[1, "what-happens-to-an-import-request-that-fails"]], "Who can use the import API?": [[1, "who-can-use-the-import-api"]], "Works": [[12, "works"]], "custom_fields.hclegacy:collection": [[6, "custom-fields-hclegacy-collection"], [12, "custom-fields-hclegacy-collection"]], "custom_fields.hclegacy:committee_deposit": [[6, "custom-fields-hclegacy-committee-deposit"], [12, "custom-fields-hclegacy-committee-deposit"]], "custom_fields.hclegacy:file_location": [[6, "custom-fields-hclegacy-file-location"], [12, "custom-fields-hclegacy-file-location"]], "custom_fields.hclegacy:file_pid": [[6, "custom-fields-hclegacy-file-pid"], [12, "custom-fields-hclegacy-file-pid"]], "custom_fields.hclegacy:groups_for_deposit": [[6, "custom-fields-hclegacy-groups-for-deposit"], [12, "custom-fields-hclegacy-groups-for-deposit"]], "custom_fields.hclegacy:previously_published": [[6, "custom-fields-hclegacy-previously-published"], [12, "custom-fields-hclegacy-previously-published"]], "custom_fields.hclegacy:publication_type": [[6, "custom-fields-hclegacy-publication-type"], [12, "custom-fields-hclegacy-publication-type"]], "custom_fields.hclegacy:record_change_date": [[6, "custom-fields-hclegacy-record-change-date"], [12, "custom-fields-hclegacy-record-change-date"]], "custom_fields.hclegacy:record_creation_date": [[6, "custom-fields-hclegacy-record-creation-date"], [12, "custom-fields-hclegacy-record-creation-date"]], "custom_fields.hclegacy:record_identifier": [[6, "custom-fields-hclegacy-record-identifier"], [12, "custom-fields-hclegacy-record-identifier"]], "custom_fields.hclegacy:submitter_affiliation": [[6, "custom-fields-hclegacy-submitter-affiliation"], [12, "custom-fields-hclegacy-submitter-affiliation"]], "custom_fields.hclegacy:submitter_id": [[6, "custom-fields-hclegacy-submitter-id"], [12, "custom-fields-hclegacy-submitter-id"]], "custom_fields.hclegacy:submitter_org_memberships": [[6, "custom-fields-hclegacy-submitter-org-memberships"], [12, "custom-fields-hclegacy-submitter-org-memberships"]], "custom_fields.hclegacy:total_downloads": [[6, "custom-fields-hclegacy-total-downloads"], [12, "custom-fields-hclegacy-total-downloads"]], "custom_fields.hclegacy:total_views": [[6, "custom-fields-hclegacy-total-views"], [12, "custom-fields-hclegacy-total-views"]], "invenio-communities": [[6, "invenio-communities"]], "invenio-rdm-records": [[6, "invenio-rdm-records"]], "invenio-records-resources": [[6, "invenio-records-resources"]], "invenio-vocabularies": [[6, "invenio-vocabularies"]], "kcr:ai_usage": [[6, "kcr-ai-usage"], [12, "kcr-ai-usage"]], "kcr:book_series": [[6, "kcr-book-series"], [12, "kcr-book-series"]], "kcr:chapter_label": [[6, "kcr-chapter-label"], [12, "kcr-chapter-label"]], "kcr:commons_domain": [[6, "kcr-commons-domain"], [12, "kcr-commons-domain"]], "kcr:commons_search_recid (system field)": [[6, "kcr-commons-search-recid-system-field"], [12, "kcr-commons-search-recid-system-field"]], "kcr:commons_search_updated (system field)": [[6, "kcr-commons-search-updated-system-field"], [12, "kcr-commons-search-updated-system-field"]], "kcr:content_warning": [[6, "kcr-content-warning"], [12, "kcr-content-warning"]], "kcr:course_title": [[6, "kcr-course-title"], [12, "kcr-course-title"]], "kcr:degree": [[6, "kcr-degree"], [12, "kcr-degree"]], "kcr:discipline": [[6, "kcr-discipline"], [12, "kcr-discipline"]], "kcr:edition": [[6, "kcr-edition"], [12, "kcr-edition"]], "kcr:institution_department": [[6, "kcr-institution-department"], [12, "kcr-institution-department"]], "kcr:media": [[6, "kcr-media"], [12, "kcr-media"]], "kcr:meeting_organization": [[6, "kcr-meeting-organization"], [12, "kcr-meeting-organization"]], "kcr:project_title": [[6, "kcr-project-title"], [12, "kcr-project-title"]], "kcr:publication_url": [[6, "kcr-publication-url"], [12, "kcr-publication-url"]], "kcr:sponsoring_institution": [[6, "kcr-sponsoring-institution"], [12, "kcr-sponsoring-institution"]], "kcr:submitter_email": [[6, "kcr-submitter-email"], [12, "kcr-submitter-email"]], "kcr:submitter_username": [[6, "kcr-submitter-username"], [12, "kcr-submitter-username"]], "kcr:user_defined_tags": [[6, "kcr-user-defined-tags"], [12, "kcr-user-defined-tags"]], "metadata.creators/metadata.contributors": [[6, "metadata-creators-metadata-contributors"], [12, "metadata-creators-metadata-contributors"]], "metadata.subjects": [[6, "metadata-subjects"], [12, "metadata-subjects"]]}, "docnames": ["README", "api", "architecture", "changelog", "cli_commands", "configuration", "customizations", "developing", "in_depth", "index", "infrastructure", "installation", "metadata", "reference"], "envversion": {"sphinx": 61, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["README.md", "api.md", "architecture.md", "changelog.md", "cli_commands.md", "configuration.md", "customizations.md", "developing.md", "in_depth.md", "index.rst", "infrastructure.md", "installation.md", "metadata.md", "reference.md"], "indexentries": {}, "objects": {}, "objnames": {}, "objtypes": {}, "terms": {"": [3, 4, 6, 7, 8, 9, 12], "0": [0, 1, 7, 9, 11, 12], "00": [1, 6, 12], "000": 2, "0000": [1, 6, 12], "0001": 12, "00k4n6c32": 12, "00z": [1, 6, 12], "01": [1, 6, 9, 12], "01t00": [1, 6, 12], "0378": 12, "04": 8, "06": 12, "09": [9, 12], "1": [1, 2, 6, 7, 9, 12], "10": [1, 2, 9, 11, 12], "100": [1, 2], "1001634": [6, 12], "1038515d68f7": 1, "1086436": 12, "11": [8, 9, 12], "111023": 12, "12": [8, 9, 11], "123": 12, "1234": [1, 12], "12345": 1, "123456": [6, 12], "1234567890": 1, "1234567891": 1, "12345abcd": 12, "1263": [6, 12], "13": 12, "17": [9, 11], "18": 9, "19": 8, "2": [1, 2, 7, 8, 9, 12], "20": [9, 11], "200": 1, "2007": 12, "201": 1, "2018": 12, "202": 1, "2020": 12, "2022": 12, "2023": 0, "2024": [1, 6, 9, 12], "2025": [9, 12], "2029": 12, "204": 1, "207": 1, "22": 8, "2345": 12, "24": [0, 12], "25": 1, "251587": 12, "2nd": 12, "3": [0, 1, 7, 9], "30": 9, "304": 1, "32": 12, "3dmodel": 12, "4": [1, 9, 12], "400": 1, "403": 1, "404": 1, "409": 1, "410": 2, "422": 1, "45": 1, "456": 12, "458": 12, "4891": 1, "5": [0, 1, 9], "50": 1, "504": 1, "5402d72b": 1, "5601": [8, 11], "5955": 12, "6": [1, 8], "60": [2, 12], "6379": 8, "63932": 12, "6780": 12, "6789": 12, "67890": 1, "7": 1, "8": 12, "8601": [6, 12], "8gb": 8, "9": 1, "90": 1, "9200": 8, "94682": 12, "958235": 12, "966892": 12, "A": [2, 3, 6, 12], "As": 12, "At": 8, "But": [1, 11, 12], "By": [1, 7], "For": [1, 4, 6, 7, 8, 12], "If": [1, 2, 6, 7, 8, 11, 12], "In": [1, 2, 7, 9, 12], "It": [0, 1, 2, 3, 4, 6, 7, 8, 11, 12], "Its": 1, "NOT": [1, 11], "No": [1, 7], "Not": 1, "On": [4, 8], "One": [6, 8, 12], "Or": [7, 8], "TO": 11, "That": [6, 12], "The": [2, 3, 6, 8, 9, 11, 12, 13], "Then": [4, 7, 8], "There": [1, 7, 8], "These": [1, 2, 3, 4, 6, 7, 8, 11, 12], "To": [1, 4, 7, 8, 11], "With": [3, 8], "__init__": 7, "__name__": 7, "_files_attr_kei": 2, "_files_bucket_attr_kei": 2, "_files_bucket_id_attr_kei": 2, "_files_data_kei": 2, "_id": 2, "aa8": 1, "abil": 12, "abl": [1, 8], "about": [1, 7, 9, 12], "abov": [1, 7, 12], "absenc": 1, "abstract": [2, 12], "abus": [6, 12], "ac": 12, "academ": [0, 6, 12], "accept": [1, 12], "access": [2, 3, 8, 11, 12], "accesscompon": 2, "accident": 1, "accommod": 1, "account": [1, 6, 12], "acronym": 12, "across": 1, "action": [1, 2], "action_nam": 2, "activ": [1, 7, 8, 11, 12], "actor": 12, "actual": [1, 2, 8, 12], "ad": [1, 2, 3, 6, 12], "adaptor": 12, "add": [2, 3, 6, 7, 9, 11, 12], "addit": [1, 2, 6, 7, 11, 12], "addition": 8, "additional_descript": 12, "additional_titl": 12, "address": [1, 6, 8, 12], "admin": [1, 6, 12], "administ": 1, "administr": [1, 8, 11], "administrativ": 1, "advis": 8, "affect": [1, 3], "affili": [6, 12], "after": [1, 2, 7, 8, 11], "ag": 12, "again": [1, 7, 8, 12], "against": [2, 8], "agent": 1, "aggreg": [1, 4, 6, 12], "ai": [6, 12], "ai_descript": [6, 12], "ai_us": [6, 12], "alia": 8, "alias": 7, "all": [2, 3, 4, 6, 7, 8, 11, 12], "all_or_non": 1, "allow": [1, 2, 3, 6, 8, 11, 12], "alon": 7, "along": [6, 7, 12], "alongsid": 8, "alreadi": [1, 7, 8], "also": [1, 2, 3, 4, 6, 7, 8, 12], "altern": [8, 12], "although": 12, "alwai": [3, 8], "ambigu": [6, 12], "american": [6, 12], "among": 3, "an": [0, 2, 3, 6, 9, 11, 12, 13], "anaconda": 8, "analyst": 12, "ani": [1, 4, 6, 7, 8, 12], "annot": 12, "anoth": [2, 8], "anotherusernam": 1, "api": [2, 3, 4, 7, 8, 9, 11], "api_token": 8, "app": [8, 11], "appear": [1, 3, 11, 12], "append": [1, 8], "appli": 1, "applic": [1, 7, 9, 12], "approach": [8, 12], "appropri": [1, 8, 12], "ar": [1, 2, 3, 4, 6, 7, 8, 11, 12], "arbitrari": 12, "architectur": 9, "archiv": 1, "area": [6, 12], "aren": 8, "arg": 2, "argument": [2, 4], "aria": 3, "arli": 3, "arlisna": [6, 12], "around": 1, "arrai": [1, 6, 12], "arrang": 12, "art": [6, 12], "articl": [6, 12], "artifact": [6, 12], "artisan": 12, "artist": [6, 12], "asc": 1, "ascend": 1, "ask": 8, "asset": 7, "assign": [1, 2, 6, 8, 12], "assignfast": [6, 12], "associ": [1, 6, 12], "assum": [1, 8], "attach": [1, 12], "attempt": 1, "attribut": 2, "attributednam": 12, "audienc": 12, "audio": [6, 12], "audiorecord": 12, "audiovisu": 12, "augment": 12, "austen": [6, 12], "authent": [], "author": [1, 12], "authorofafterword": 12, "authorofforeword": 12, "authorofintroduct": 12, "auto": 3, "autom": 9, "automat": [1, 6, 7, 8, 12], "avail": [1, 2, 4, 7, 8, 12], "available_sort_opt": 2, "avatar": 1, "avoid": [1, 3, 7], "aw": 8, "awar": [6, 8, 12], "award": [1, 12], "awkward": 1, "b": [1, 2, 7, 12], "b144": 1, "back": [3, 4, 7], "backend": 6, "background": [1, 8], "bad": 1, "bar": [8, 12], "bare": 1, "base": [1, 2, 6, 12], "baserecordfilescompon": 2, "bash": [4, 7, 8, 11], "bashrc": 8, "basic": 12, "bearer": 1, "beat": 8, "becaus": [1, 3, 6, 7, 8, 12], "becom": 1, "been": [1, 2, 3, 6, 7, 8, 12], "befor": [1, 2, 4, 6, 7, 8, 12], "begin": [7, 8], "being": [1, 2, 3, 8, 12], "belong": [1, 6, 12], "below": [1, 8, 11, 12], "beneath": 12, "best": [3, 8, 12], "bestmatch": 3, "beta": 7, "beta3": 9, "beta4": 9, "beta5": 9, "beta6": [7, 9], "beta7": 9, "beta8": [0, 9], "between": [1, 2, 6, 7, 12], "beyond": [6, 12], "bibliographi": 12, "big": 8, "bin": 8, "binari": 1, "bind": [8, 11], "blob": [], "block": 8, "blogpost": 12, "blueprint": 2, "blueski": 3, "book": [6, 12], "booksect": 12, "boolean": [1, 6, 12], "boot": 8, "both": [1, 3, 6, 12], "break": [1, 8], "broke": 3, "browser": [7, 11], "bucket": [1, 2], "bug": [3, 7], "bugfix": 7, "build": [3, 6, 9], "builder": 6, "built": [0, 1, 8, 13], "bulk": [4, 9], "bundl": 7, "busi": 2, "c": [1, 2, 7, 8], "cach": 8, "call": [1, 2, 6, 8, 12], "callback": 1, "can": [2, 3, 4, 6, 7, 8, 11, 12, 13], "cannot": [1, 7], "carri": 1, "case": [1, 2, 6, 7, 12], "catalog": 12, "caution": [4, 7, 11], "cc": 12, "cd": [7, 8, 11], "central": [1, 6, 7, 12], "cern": [1, 13], "certain": 1, "cfg": 1, "ch": [1, 13], "chanc": 1, "chang": [2, 6, 8, 9, 11, 12], "change_m": 8, "changenotificationscompon": 2, "chapter": [6, 12], "chart": 12, "chatgpt": 12, "check": [1, 2, 6, 8], "check_permiss": 2, "check_revision_id": 2, "checksum": 1, "child": 2, "children": 1, "chmod": 8, "choos": [3, 12], "choreograph": 12, "chose": 8, "chrome": 7, "chronolog": [6, 12], "cinematograph": 12, "class": [1, 6], "clean": [2, 7], "cleanup": 2, "cleanup_draft": 2, "cleanup_record": 2, "clearer": 3, "cli": [1, 3, 9, 11], "click": 8, "client": [1, 2, 7], "clone": [7, 9], "close": 1, "cm": 12, "co": 8, "coar": 12, "code": [9, 11, 12], "codebas": 11, "coderepositori": 12, "cog": 8, "collabor": [0, 12], "collect": [3, 4, 7, 9, 11], "collection_id": 1, "collection_slug": 1, "collection_vis": 1, "collector": 12, "collid": 1, "com": [1, 6, 8, 11, 12], "command": [1, 3, 7, 9, 11], "commerci": 12, "commiss": [6, 12], "commit": 1, "committe": [6, 12], "committeechair": 12, "committeememb": 12, "common": [0, 2, 3, 6, 7, 11, 12, 13], "commons_api_token": [1, 8], "commons_group_descript": 1, "commons_group_id": 1, "commons_group_nam": 1, "commons_group_vis": 1, "commons_inst": 1, "commons_search_api_token": 8, "commun": [1, 2, 3, 11], "compar": 12, "comparison": 3, "compil": 8, "complement": [6, 12], "complet": [1, 6, 7, 11, 12], "complex": 2, "compon": [1, 3, 6, 7], "compos": [7, 9, 12], "compress": 1, "comput": 8, "computationalmodel": 12, "computationalnotebook": 12, "condit": 12, "conductor": 12, "conf": 7, "confer": [6, 12], "conferencepap": 12, "conferencepost": 12, "conferenceproceed": 12, "config": [1, 6, 8], "configr": 2, "configur": [7, 9], "confirm": 1, "conflict": [1, 3], "conform": 1, "confus": [1, 3, 12], "connect": 1, "consid": [1, 7], "consist": 2, "constrain": [6, 12], "construct": 2, "constructor": 2, "consult": 12, "contactperson": 12, "contain": [1, 3, 6, 7, 9, 11, 12], "container": 9, "content": [1, 11, 12], "contentmoderationcompon": 2, "continu": 7, "contribuor": 12, "contribut": 12, "contributor": 9, "control": [1, 2, 3, 6, 9], "conveni": [1, 8], "convert": [6, 12], "coordin": 12, "copi": [7, 8, 11], "copyright": 9, "core": [4, 9, 11], "corpor": [1, 6, 12], "correct": [1, 4, 8, 11, 12], "correctli": 3, "correspond": [1, 4, 6, 12], "corrupt": 1, "could": [1, 3], "count": 4, "cours": [6, 8, 12], "cover": [6, 12], "cpython": 8, "creat": [2, 3, 4, 6, 7, 12], "create_search": 2, "creatibutorsfield": 3, "creation": [1, 3, 6], "creativ": 12, "creativecommon": 12, "creator": [1, 9], "credit": 12, "crud": 2, "csl": 12, "ctrl": [7, 8], "curat": 1, "curation_polici": 1, "curl": 8, "current": [1, 2, 3, 7, 8, 12], "curriculum": 12, "custom": [2, 3, 7, 8, 9, 13], "custom_field": 1, "custom_pdf_viewer_j": 7, "customfieldscompon": 2, "d": [7, 8, 11], "daemon": 8, "dashboard": [3, 8, 11], "data": [2, 3, 4, 7, 8, 9, 11, 12], "databas": [1, 2, 4, 6, 12], "datacit": 12, "datacollector": 12, "datacur": 12, "datamanag": 12, "datamanagementplan": 12, "dataservicecompon": 2, "dataset": 12, "date": [1, 3, 6, 8, 12], "datetim": [6, 12], "db": 8, "de": 12, "deal": 2, "debug": [7, 11], "declar": [1, 7], "dedicate": 12, "deeper": 9, "default": [1, 2, 3, 6, 7, 8, 12], "default_files_en": 2, "default_media_files_en": 2, "defaultrecordscompon": 2, "defin": [2, 6, 7, 8, 12], "definit": [1, 2], "delai": 1, "deleg": 2, "delet": [2, 4, 7], "delete_draft": 2, "delete_record": 2, "deletion_statu": 1, "demo": 8, "depart": [6, 12], "depend": [1, 2, 7, 8, 11], "deploi": [4, 7], "deploy": 7, "deposit": [9, 11, 12], "depositor": 12, "depth": 9, "derefer": 2, "desc": 1, "descend": 1, "describ": [1, 6, 7, 11, 12], "descript": [1, 12], "descriptor": [6, 12], "design": 12, "desir": [1, 3, 6, 12], "desktop": 8, "despair": 11, "destroi": [4, 8, 11], "detail": [1, 2, 3, 9, 11, 12], "detect": 1, "determin": [6, 12], "dev": [1, 8, 11], "develop": [4, 8, 9, 13], "developmentstatu": 12, "diagram": 12, "dict": 7, "dictionari": [1, 7], "did": 1, "differ": [1, 6, 11, 12], "differenti": [6, 12], "difficult": 1, "dig": 9, "digit": 12, "direction": 6, "directli": [1, 7, 8], "director": 12, "directori": 8, "disabl": 2, "disambigu": 1, "discover": 1, "discoveri": [6, 12], "discuss": [1, 12], "displai": [3, 4], "dissert": [6, 12], "distinct": [2, 6, 7, 12], "distress": [6, 12], "distribut": [1, 12], "distributor": 12, "divers": 12, "divid": [3, 4, 6, 12], "divis": [3, 12], "dnb": 12, "do": [1, 7, 8, 11, 12], "doc": [1, 7, 8, 13], "doc_count": 1, "docker": [4, 7, 9], "dockerfil": 8, "document": [1, 3, 4, 6, 7, 8, 12], "documentari": 12, "doe": [2, 3, 6, 7, 8, 12], "doi": [1, 3], "domain": [6, 12], "don": 11, "done": [7, 8], "donor": 12, "down": [8, 11], "download": [1, 6, 8, 12], "dphil": [6, 12], "draft": [1, 2, 6], "draft_cl": 2, "draft_fil": 2, "draft_index": 2, "draft_indexer_cl": 2, "draft_indexer_queue_nam": 2, "drafter": 12, "draftfilescompon": 2, "draftmediafilescompon": 2, "draftmetadatacompon": 2, "draw": 2, "drawn": 12, "driver": 8, "dsl": 2, "dummi": 8, "dumper": 2, "duplic": [], "dure": [1, 2, 3, 4, 6, 7, 8, 11, 12], "dynam": [4, 7], "e": [1, 2, 3, 4, 6, 7, 11, 12], "e2": 7, "each": [1, 2, 3, 6, 7, 8, 11, 12], "earlier": [1, 2], "eas": 12, "easi": 8, "easili": 12, "edit": [1, 2, 3], "editor": 12, "editori": 12, "edtf": 1, "educ": [6, 12], "effect": [6, 7, 12], "effici": [6, 12], "eight": 12, "either": [1, 2, 4, 8, 11, 12], "electron": 12, "els": 8, "elsewher": 12, "email": [1, 4, 8, 11, 12], "emailbackend": 6, "embargo": [2, 12], "embed": 2, "emit": [2, 6], "emploi": [1, 2, 6, 7, 12], "empti": [1, 3], "emul": 7, "en": 12, "enabl": [2, 6, 12], "encompass": 12, "encount": 1, "encourag": [6, 12], "end": [1, 3, 7], "endpoint": 2, "enforc": 2, "eng": 12, "engag": 12, "engin": [8, 12], "english": 12, "enough": 8, "ensur": [1, 7, 11], "enter": [7, 8, 11], "entir": 1, "entiti": 1, "entri": [6, 12], "env": [8, 11], "environ": [1, 7, 9, 11], "er": 12, "error": [2, 3, 7], "especi": [3, 7, 8], "essai": 12, "establish": 1, "etc": [1, 2, 4, 6, 7, 11, 12], "eurepo": 12, "europ": [6, 12], "even": [1, 3, 6, 12], "event": [3, 4, 6, 8, 12], "eventu": 1, "everi": 7, "exactli": [1, 12], "examin": 12, "exampl": [1, 4, 6, 7, 8, 9], "exce": 1, "except": [1, 7, 12], "exclud": 2, "exec": [4, 7, 8, 11], "exist": [1, 2, 3, 4, 7, 8], "expand": [2, 12], "expandable_field": 2, "expected_revision_id": 2, "expir": 2, "explain": [8, 11], "export": [4, 12], "extend": 1, "extens": [6, 7, 8], "extern": [2, 12], "extra": 8, "extra_filt": 2, "extrem": 4, "f": 8, "facet": [2, 6, 12], "facilit": 12, "factori": 2, "fall": 12, "fals": [1, 2], "famili": 3, "familiar": 1, "family_nam": 12, "fast": 6, "favour": 12, "featur": [1, 7, 12], "feedback": 11, "field": [1, 2, 3, 9], "figur": 12, "file": [0, 4, 6, 9, 12], "file1": 1, "file2": 1, "file3": 1, "file_links_list": 2, "fileconfigmixin": 2, "filenam": 1, "files_attr": 2, "fill": [3, 12], "filter": [1, 2], "final": [1, 2, 8], "find": [3, 4, 7, 8, 11, 12], "first": [1, 4, 7, 8, 11], "firstrecordcreatednotificationbuild": 6, "firstrecordcreatednotificationservic": 6, "firstrecordpublishednotificationbuild": 6, "fit": 12, "fix": [3, 7], "fixtur": [7, 8, 11], "flag": [6, 8, 12], "flask": [2, 4, 6, 7, 9], "flat": 12, "flexibli": [6, 12], "flow": 7, "folder": [1, 8, 11], "follow": [1, 2, 4, 6, 7, 8, 11, 12], "foo": [2, 12], "forbidden": 1, "forc": [2, 7, 8, 12], "fork": [8, 9], "form": [1, 3, 9, 11, 12], "format": [1, 12], "former": 1, "formerown": 12, "formgenr": [6, 12], "forthcom": 1, "found": [1, 4, 6, 12, 13], "four": [1, 7], "fraction": 1, "fragil": 1, "free": [1, 6, 8, 12], "freecodecamp": 8, "from": [1, 3, 4, 6, 7, 8, 11], "fromconfig": 2, "frontend": 8, "full": [1, 3, 8, 9, 12], "full_nam": 1, "function": [2, 3], "fund": [6, 12], "funder": 1, "fundref": 12, "further": [2, 4, 8, 11], "futur": [1, 3, 12], "g": [1, 2, 3, 4, 6, 7, 11, 12], "gatewai": 1, "gender": 12, "gener": [1, 4, 6, 7, 8, 12], "geograph": [6, 12], "geometri": 12, "geonam": 12, "geopattern": 7, "get": [2, 3, 8], "gh_page": 7, "git": 8, "github": [3, 7, 8, 11, 12], "gitlab": 7, "given": [2, 3], "given_nam": 12, "gnd_node": 12, "gninx": 8, "good": 8, "grace": 2, "grant": [1, 2, 6, 12], "greatli": 12, "grep": 4, "group": [4, 7, 8, 9, 11, 12], "group_collect": 1, "group_collections_metadata_endpoint": 1, "group_identifi": [6, 12], "group_nam": [6, 12], "guarante": 8, "guid": 8, "h1": 12, "ha": [1, 2, 3, 6, 7, 8, 12], "handl": [2, 3], "handler": 2, "hard": 2, "have": [2, 3, 6, 7, 8, 11, 12], "hc": 9, "hcommon": [1, 6, 12], "head": 6, "header": 3, "held": 1, "help": [1, 4, 6, 7, 8, 12], "helper": 2, "here": [1, 6, 8, 11, 12], "hidden": 3, "hierarch": 12, "high": 2, "histor": [6, 12], "histori": [6, 7, 12], "historian": [6, 12], "hit": 1, "hold": [1, 6, 8, 12], "homoit0000669": [6, 12], "homosauru": 6, "hood": 1, "host": [6, 12], "hostinginstitut": 12, "hour": 11, "how": [4, 12], "how2shout": 8, "howev": [1, 12], "html": [2, 6, 8, 12], "http": [1, 6, 7, 8, 11, 12, 13], "human": [1, 6, 12], "hyphen": [6, 8, 12], "i": [0, 2, 3, 4, 6, 7, 11, 12, 13], "icon": 8, "id": [1, 2, 3, 6, 12], "id_": 2, "idea": 8, "ident": [1, 2, 12], "identifi": [1, 3, 6, 9], "identifier_orcid": 12, "idp": 1, "ignor": 1, "ignore_field_permiss": 2, "illustr": 12, "imag": [1, 3, 7, 8, 12], "immedi": [1, 7, 12], "implement": [1, 9], "import": [2, 4, 7, 8, 9, 11, 12], "import_data": 8, "import_fil": 2, "impos": [6, 12], "imprint": 12, "includ": [0, 1, 2, 3, 4, 6, 8, 11, 12, 13], "include_delet": 2, "incomplet": 1, "increment": 7, "independ": 1, "index": [1, 2, 4, 6, 8, 12], "index_dump": 2, "indexer_cl": 2, "indexer_queue_nam": 2, "indic": [1, 4, 11], "individu": [1, 8], "influenc": 12, "info": [6, 8, 12], "inform": [1, 2, 7, 12, 13], "informationen": 12, "infrastructur": [3, 9], "inherit": 2, "ini": 8, "init": [7, 11], "initi": [1, 2, 3, 7], "input": 2, "insert": [8, 11], "insid": [4, 7, 8, 11], "instal": [1, 3, 7, 9], "instanc": [0, 2, 4, 8, 9, 11, 13], "instance_path": [8, 11], "instanti": 2, "instead": [1, 3, 7, 8], "institut": [1, 6, 12], "instruct": [6, 7, 9, 11, 12], "instructionalresourc": 12, "integ": [6, 12], "integr": [1, 7, 9, 12], "intend": [1, 6, 12], "intens": 1, "inter": [7, 11], "interact": [2, 7], "interactiveresourc": 12, "interfac": [1, 2, 8, 12], "intern": [2, 6, 9, 12], "interpret": 8, "interrupt": 1, "intersex": [6, 12], "interview": 12, "interviewe": 12, "interviewrecord": 12, "interviewtranscript": 12, "introduc": [1, 2, 7], "introduct": [6, 12], "invenio": [1, 3, 9, 11, 12], "invenio_": 8, "invenio_app": 8, "invenio_csrf_secret_salt": 8, "invenio_custom_pdf_view": 7, "invenio_datacite_password": 8, "invenio_drafts_resourc": 2, "invenio_group_collections_kcwork": 1, "invenio_instance_path": 8, "invenio_notif": 6, "invenio_rdm_record": 2, "invenio_record_importer_data_dir": 8, "invenio_record_importer_local_data_dir": 8, "invenio_records_resourc": 2, "invenio_search_domain": 8, "invenio_secret_kei": 8, "invenio_security_login_salt": 8, "invenio_site_api_url": 8, "invenio_site_ui_url": 8, "invenio_sqlalchemy_database_uri": 8, "inveniordm": [0, 4, 7, 8, 9], "inventor": 12, "invers": 2, "invert": 3, "invit": 1, "invok": 4, "involv": [1, 2, 3, 6, 7, 11, 12], "irrevoc": 1, "is_delet": 1, "is_publish": 2, "is_select": 1, "isbn": [], "iscitedbi": 12, "iso": [6, 12], "issn": [], "issu": 12, "item": [2, 6, 12], "item_index": 1, "iter": 2, "its": [1, 2, 3, 7, 8, 11, 12], "itself": [6, 12], "j": [3, 9], "jammi": 8, "jane": [6, 12], "janedo": 12, "javascript": 12, "jdoe": [1, 6, 12], "jest": 7, "jinja": 6, "jinja2": 2, "john": [1, 6, 12], "journal": [6, 12], "journalarticl": 12, "json": [1, 2, 4, 7, 8], "juror": 12, "just": 12, "kc": [1, 3, 4, 8, 9], "kc_usernam": [1, 6, 12], "kcr": [1, 8], "kcr_api": 8, "kcr_ui": 8, "kcwork": [1, 3, 8, 9], "keep": [7, 8, 12], "kei": [1, 2], "keyboard": 3, "keyword": 2, "kind": [1, 2], "kingston": [6, 12], "know": 1, "knowldg": 11, "knowledg": [0, 1, 3, 7, 11, 12, 13], "knowledgecommon": 1, "kwarg": 2, "label": [1, 3, 6, 12], "labor": 12, "lago": 12, "lambda": 8, "lang": 12, "languag": 12, "larg": [2, 3], "last": [1, 4, 6, 7, 12], "later": [1, 2], "latest": [1, 2, 7], "latin": [6, 12], "launch": 12, "layer": 9, "layout": 3, "lcsh": [6, 12], "lcsh2fast": [6, 12], "lead": 3, "least": [1, 2, 3, 8], "leav": 7, "left": 1, "leftov": 7, "legaci": [4, 9], "legalcod": 12, "legalcom": 12, "legalrespons": 12, "less": [3, 6, 7, 12], "lessonplan": 12, "level": [1, 2, 6, 12], "lib": 8, "licens": [0, 12], "license": 12, "lift": 2, "lift_embargo": 2, "like": [1, 2, 7, 8, 12], "likewis": 7, "limit": 2, "line": [3, 4, 11], "link": [1, 2, 3, 4, 6, 12], "links_item": 2, "links_item_tpl": 2, "links_search": 2, "links_search_draft": 2, "links_search_vers": 2, "linkstempl": 2, "linux": 7, "list": [1, 2, 4, 6, 7, 12], "literatur": [6, 12], "live": [8, 11], "load": 4, "local": [1, 4, 7, 9], "localhost": [8, 11], "locat": [1, 6, 7, 12], "lock": [2, 8], "lock_edit_published_fil": 2, "log": [3, 6], "logic": 2, "loglevel": 8, "logo": [1, 3], "long": [1, 3, 7, 8], "longer": 1, "look": 1, "lost": 4, "low": 2, "lowercas": [6, 12], "lt": 8, "lyricist": 12, "m": 7, "ma": [6, 12], "mac": 8, "machin": 8, "made": [1, 3, 7, 12], "magazin": 12, "magazinearticl": 12, "mai": [1, 6, 7, 8, 11, 12], "mail": 6, "main": [3, 4, 6, 7, 11, 12], "maintain": 12, "make": [7, 8, 11], "manag": [1, 2, 3, 7, 8, 12], "mani": 12, "manual": 8, "manufactur": 12, "map": [2, 12], "mark": 2, "mark_record": 2, "mark_record_for_purg": 2, "markdown": 6, "match": [1, 2, 3, 8, 12], "materi": [6, 12], "matter": [6, 12], "max_files_count": 2, "maximum": [1, 2], "md": [3, 7], "mean": [3, 6, 7, 8, 12], "meantim": 7, "mechan": [1, 6, 12], "media": [2, 3], "meet": [6, 12], "member": [1, 3], "member_polici": 1, "membership": 1, "memori": 8, "mention": 12, "menu": 3, "merg": 7, "mesh": [0, 11], "messag": [1, 3], "met": 12, "metadata": [2, 3, 4, 9], "metadata_field": 9, "metadatacompon": 2, "method": [2, 12], "michigan": 12, "middl": 4, "might": [1, 3, 6, 8, 12], "migrant": [6, 12], "migrat": [6, 12], "mind": 1, "minim": 1, "minor": 7, "mint": [1, 12], "minut": [1, 8], "miss": [1, 3], "mit": 0, "mixin": 2, "mla": [6, 12], "modal": 3, "model": 2, "moder": [2, 12], "moderatorrolerecipi": 6, "modern": [6, 12], "modif": 1, "modifi": [1, 2, 3, 7], "modul": [1, 3, 9, 11], "modular": 11, "monograph": 12, "monotask": 7, "more": [1, 2, 3, 6, 7, 12], "most": [1, 3, 7, 8], "mount": [8, 11], "move": 3, "mq": 8, "msu": 12, "much": [6, 12], "multi": 1, "multi_statu": 1, "multidisciplinari": 12, "multipart": 1, "multipl": [1, 2, 3], "music": 12, "musicalrecord": 12, "must": [1, 6, 7, 8, 12], "my": [1, 12], "myapitoken": 8, "mycours": 12, "myevent": 12, "myinveniodatacitepassword": 8, "myservic": 2, "myserviceconfig": 2, "mysteri": 1, "mytoken": 8, "myusernam": 1, "n": 1, "na": 3, "name": [2, 3, 4, 6, 8, 11, 12], "name_parts_loc": 3, "namespac": [6, 12], "natur": 1, "navig": [3, 7, 8, 11], "nc": 12, "necessari": [3, 8, 11], "need": [1, 4, 9, 11, 12], "neither": [1, 12], "net": 12, "network": 1, "new": [2, 3, 6, 8, 12], "new_commons_group_id": 1, "new_commons_group_nam": 1, "new_vers": 2, "newer": 8, "newest": [1, 3], "newli": [6, 12], "newlin": 8, "newspap": 12, "newspaperarticl": 12, "next": [1, 2, 7, 8], "nginx": 11, "node": 9, "node_modul": 7, "non": 1, "none": [1, 2], "noowneravail": 1, "nor": [1, 12], "normal": [1, 7, 8, 11], "note": [1, 2, 4, 7, 11, 12], "notic": 1, "notif": [2, 9], "notif_tim": 2, "notificationop": 6, "notifications_moderator_rol": 6, "notimplementederror": 2, "now": [1, 2, 3, 8, 11], "npm": 7, "number": [1, 2, 4, 6, 9, 12], "numer": 1, "nvm": 9, "o": 8, "oai": [1, 2], "oai_record_sourc": 2, "oai_result_item": 2, "oauth": 1, "object": [2, 6, 7], "obtain": [1, 8], "occup": 1, "occupi": 7, "occur": 1, "oclc": [6, 12], "octet": 1, "octob": 12, "often": [7, 8], "ok": 1, "old": [1, 8], "old_commons_group_id": 1, "oldest": 1, "on_relation_upd": 2, "onc": [1, 2, 7, 8], "one": [1, 2, 6, 8, 11, 12], "ongo": 1, "onli": [2, 3, 4, 6, 7, 9, 12], "onlinepubl": 12, "onto": 11, "open": [1, 8, 12], "openfund": 12, "opengraph": 3, "opensearch": [2, 4, 8, 11], "oper": [1, 2, 6, 8, 11], "opt": [7, 8, 11], "option": [1, 2, 3, 4, 6, 7, 11, 12], "orcid": [1, 3, 6], "order": [1, 2, 3, 7], "org": [1, 6, 7, 8, 12], "organ": [1, 6], "organiz": [6, 12], "origin": [1, 2, 3, 7, 12], "other": [1, 2, 3, 6, 7, 8, 12], "otherwis": [8, 12], "our": 12, "out": [1, 8, 12], "output": 7, "outsid": 1, "over": 7, "overrid": [1, 2, 7, 12], "overridden": [2, 7], "overwhelm": 12, "own": [1, 7, 8], "owned_bi": 1, "owner": 12, "ownership": [6, 12], "p": [1, 4, 11, 12], "packag": [2, 4, 8, 11, 12], "page": [1, 3, 7, 9, 11, 12], "panda": 1, "panel": 8, "paper": [6, 12], "param": 2, "paramet": 2, "parent": [1, 2, 8, 11, 12], "parentpidscompon": 2, "parser": 2, "part": [0, 1, 3, 4, 6, 12], "particular": [8, 11], "particularli": [1, 7], "partli": 1, "pass": [1, 2], "password": [4, 8, 11], "patch": 7, "patent": 12, "path": [1, 6, 12], "patienc": 11, "pdf": [1, 6, 12], "pdfj": 7, "pedagogi": 12, "peerreview": 12, "pend": 3, "perform": [1, 2, 8, 12], "period": [1, 2, 12], "perman": 1, "permiss": [2, 3], "permission_act": 2, "permission_polici": 2, "permission_policy_cl": 2, "persist": [6, 7, 8, 12], "person": [6, 12], "person_or_org": [6, 12], "personnel": 1, "pgadmin": [8, 11], "pgadmin_default_email": 8, "pgadmin_default_password": 8, "phd": [6, 12], "photograph": 12, "physicalobject": 12, "pick": 7, "pid": [1, 2, 7, 8, 11, 12], "pidcompon": 2, "pidfil": 8, "pidscompon": 2, "pin": 3, "pip": [7, 8], "pipenv": [7, 11], "pipfil": 7, "place": [1, 6, 12], "placehold": 1, "placeholder_avatar": 1, "plain": 1, "plaintext": 6, "platform": 3, "pleas": 1, "pmh": 1, "png": 1, "podcastepisod": 12, "poeticwork": 12, "poetri": [6, 12], "point": [1, 3, 12], "polici": [1, 2], "popul": 1, "possess": 1, "possibl": [1, 7, 12], "post": 2, "post_publish": 2, "postgres_db": 8, "postgres_password": 8, "postgres_us": 8, "postgresql": 8, "potenti": [6, 12], "practic": 7, "pre": [1, 12], "prefer": 2, "prefix": [6, 12], "preprint": 12, "present": [2, 3, 6, 12], "presentationtext": 12, "preserv": [6, 12], "press": 8, "prev": 1, "prevent": [1, 2, 12], "preview": 2, "previou": [1, 2], "previous": [2, 3, 6, 12], "primari": [3, 6], "primarili": [2, 4, 6, 12], "principl": 1, "print": [8, 12], "printer": 12, "prior": [2, 6, 8, 12], "privat": [8, 11], "privileg": 1, "problem": 7, "problemat": [6, 12], "proce": 8, "proceed": [6, 12], "process": [1, 2, 4, 12], "produc": [2, 3, 4, 6, 12], "product": [4, 6, 7, 12], "professionel": 12, "profil": [1, 3, 6, 12], "program": 12, "programminglanguag": 12, "project": [6, 7, 8, 12], "projectorteamlead": 12, "projectorteammanag": 12, "projectorteammemb": 12, "proper": [3, 6, 12], "properti": [1, 2, 3, 12], "provid": [1, 2, 4, 6, 7, 8, 11, 12], "provision": [11, 12], "prune": 7, "psychologi": 12, "psycopg2": 8, "public": [1, 3, 6, 12], "public_memb": 1, "publication_d": [1, 12], "publish": [1, 2, 3, 6, 12], "pull": [7, 8], "purg": [1, 2], "purge_record": 2, "purpos": [6, 12], "push": 7, "put": [1, 8], "py": [4, 7], "pypi": 8, "pyproject": 7, "pytest": 7, "python": [2, 9, 11, 12], "python3": 8, "python_local_git_packages_path": 8, "python_local_site_packages_path": 8, "queri": [2, 3], "query_parser_cl": 2, "querystr": 2, "question": 1, "queue": [1, 2], "quick": 11, "quickli": 8, "quickstart": 9, "quota": 2, "rabbitmq": 8, "race": 12, "rais": [1, 2], "random": [3, 8], "rang": 12, "rather": [1, 8], "rdm": [8, 11, 12], "rdm_records_service_compon": 2, "rdmrecord": 6, "rdmrecordserviceconfig": 2, "re": [1, 2, 7, 8, 12], "reach": 7, "react": [2, 7], "read": [1, 2, 4], "read_al": 2, "read_delet": 2, "read_draft": 2, "read_latest": 2, "read_mani": 2, "readabl": [1, 6, 7, 12], "reader": [1, 6, 12], "readi": 7, "readm": [3, 7], "real": 8, "reason": [1, 8, 12], "rebuild": 8, "rebuild_index": 2, "rebuilt": 7, "receiv": [1, 6, 8], "recent": [3, 7], "recipi": 6, "recogn": 12, "recommend": [1, 8], "record": [1, 2, 3, 4, 7, 9, 11], "record_cl": 2, "record_id": 1, "record_polici": 1, "record_typ": 2, "record_url": 1, "recorddeletioncompon": 2, "recordfilesprocessorcompon": 2, "records_info": 2, "recordserviceconfig": 2, "recreat": [7, 8], "recurs": [7, 11], "redi": 8, "redis_domain": 8, "redund": [1, 7, 11], "refactor": 3, "refer": [1, 7, 8, 9, 11, 12], "refere": 12, "reflect": [1, 7, 11], "refresh": 7, "refuge": [6, 12], "regard": 1, "regardless": 8, "regist": [1, 2], "registr": 2, "registrationag": 12, "registrationauthor": 12, "registri": 12, "reindex": 2, "reject": 1, "rel": [1, 6, 12], "relat": [2, 7, 12], "related_identifi": 12, "relatedperson": 12, "relation_typ": 12, "relationscompon": 2, "relationship": 2, "releas": [0, 8], "relev": [1, 12], "reliabl": [1, 8], "reload": [7, 8, 11], "remain": 1, "remot": [3, 4, 7, 8, 11, 12], "remov": [1, 2, 3], "renam": 1, "render": 2, "repeat": 1, "replac": [1, 2, 8], "repo": 7, "report": 12, "repositori": [0, 7, 8, 12], "repres": [1, 7], "represent": 1, "request": [2, 3, 7, 8], "requir": [2, 6, 7, 9, 11, 12], "require_permiss": 2, "research": [0, 1, 6, 11, 12], "researchgroup": 12, "researchparticip": 12, "reserv": 1, "resid": [6, 12], "resourc": [1, 2, 3, 8, 9, 11], "resource_typ": 12, "resourcetypegener": 12, "respect": 7, "respond": 7, "respons": [2, 8], "rest": [8, 9, 11], "rest_api_award": 1, "rest_api_commun": 1, "rest_api_drafts_record": 1, "rest_api_fund": 1, "rest_api_group": 1, "rest_api_index": 1, "rest_api_memb": 1, "rest_api_nam": 1, "rest_api_oaipmh_set": 1, "rest_api_request": 1, "rest_api_review": 1, "rest_api_statist": 1, "rest_api_us": 1, "rest_api_vocabulari": 1, "restart": [7, 8, 11], "restor": [1, 2], "restore_record": 2, "restrict": [1, 2, 12], "result": [1, 2, 3], "result_item": 2, "result_item_cl": 2, "result_list": 2, "result_list_cl": 2, "retriev": 2, "return": [1, 2], "review": [1, 2, 12], "review_polici": 1, "review_requir": 1, "reviewcompon": 2, "revis": 2, "revision_id": [1, 2], "right": [3, 8, 12], "rightshold": 12, "robust": 1, "role": [1, 6, 8, 9], "roll": 2, "root": [7, 8], "rout": 3, "rule": 1, "run": [2, 6, 8, 9, 11], "run_compon": 2, "runner": 7, "same": [1, 2, 6, 8, 12], "sandbox": 12, "save": 6, "scan": 2, "scan_expired_embargo": 2, "scan_vers": 2, "schema": [1, 2, 3, 9], "schema_access_set": 2, "schema_gr": 2, "schema_par": 2, "schema_quota": 2, "schema_request_access": 2, "schema_secret_link": 2, "schema_tombston": 2, "scheme": [1, 6, 9], "scholarli": 12, "scienc": [6, 12], "screenplayauthor": 12, "script": [7, 11], "scroll": 2, "search": [1, 3, 4, 11, 12], "search_draft": 2, "search_gc_delet": 2, "search_opt": 2, "search_prefer": 2, "search_queri": 2, "search_record": 2, "search_vers": 2, "searchopt": 2, "second": [1, 6, 12], "secret": [2, 8], "section": 12, "secur": [1, 8], "see": [0, 7, 8, 11, 12], "seen": 8, "segment": 1, "select": [3, 12], "selector": 3, "selenium": 7, "self": [1, 2, 3], "self_html": 1, "semant": [6, 7], "semver": 7, "send": 6, "sent": [1, 6], "separ": [1, 7, 8, 11], "seri": [6, 12], "serial": [2, 4], "series_titl": [6, 12], "series_volum": [6, 12], "serv": [1, 3, 8, 11, 12], "server": [2, 7, 11], "serverless": 8, "servic": [3, 4, 6, 9, 12], "service_id": 2, "servicecompon": 2, "serviceconfig": 2, "services_setup": 11, "serviceschemawrapp": 2, "session": 8, "set": [1, 2, 3, 6, 7, 8, 11, 12], "set_quota": 2, "set_user_quota": 2, "settings_html": 1, "setup": [7, 8, 9], "sever": [1, 8], "sexual": 12, "sh": [7, 8, 11], "shadow": 2, "shape": 1, "share": [0, 1, 3, 8, 12], "shell": 8, "short": [1, 12], "should": [1, 3, 6, 7, 8, 11, 12], "side": [2, 12], "sidebar": 3, "signal": [1, 2, 6], "signalcompon": 2, "similarli": [6, 11, 12], "simpl": [2, 6, 12], "simpli": [1, 7, 8], "sinc": [1, 2, 3, 4, 7, 8], "singl": [1, 4, 7, 12], "site": [3, 4, 8, 9, 11], "size": [1, 8, 12], "skip": 2, "sl": 8, "slackbot": 8, "slide": 12, "slider": 8, "slight": 1, "slug": 1, "so": [1, 2, 3, 4, 6, 7, 8, 11, 12], "social": [1, 3], "soft": [1, 2], "softwar": [6, 12], "solut": 8, "solv": 3, "some": [2, 3, 6, 7, 11, 12], "someth": 8, "sometim": [3, 7], "sort": [2, 3, 12], "sort_default": 2, "sort_default_no_queri": 2, "sort_opt": 2, "sortbi": 1, "sourc": [2, 7, 8, 11], "speaker": 12, "specif": [2, 3, 6, 8, 12], "specifi": [1, 8], "sponsor": [6, 12], "sqlalchemi": 2, "squash": 7, "src": 7, "sso_saml_idp": 1, "stage": [4, 7, 8], "stamp": 8, "stand": 7, "standalon": 8, "standard": [6, 12], "standardisierung": 12, "start": [1, 7], "stat": [4, 6, 12], "state": [2, 12], "stateless": 1, "statement": 11, "static": [3, 11], "statist": 1, "statu": [6, 12], "stdout": 8, "step": [1, 7, 8, 11], "still": 1, "stop": [7, 8, 11], "storag": [1, 2], "store": [0, 1, 2, 4, 6, 7, 8, 12], "str": 1, "stream": 1, "streamlin": 9, "strict_valid": 1, "strictli": [], "string": [1, 3, 6, 12], "strongli": [6, 12], "structur": [8, 12], "studi": 1, "style": 7, "sub": [4, 6, 8, 12], "subfold": 1, "submiss": [1, 3], "submit": [1, 3], "submitt": [6, 12], "submodul": 11, "subsequ": 1, "subservic": 2, "substant": [6, 12], "subtitl": 12, "subtyp": 12, "succe": 1, "succeed": 1, "successfulli": 1, "sudo": 8, "suffici": 1, "suffix": 7, "suggest": [6, 12], "suit": 7, "suitabl": 4, "superus": 8, "supervisor": 12, "supplement": [6, 12], "suppli": [1, 8], "support": [1, 7, 8, 12], "sur": 8, "sure": [7, 8], "susequ": 1, "syllabi": [6, 12], "syllabu": 12, "sync": 3, "syntax": 8, "synthet": 4, "system": [0, 1, 3, 4, 7, 11], "systemd": 8, "t": [8, 11], "tab": 8, "tabl": 8, "tag": [6, 12], "tail": 8, "tailor": 12, "take": [7, 8, 11], "taken": [1, 12], "task": [1, 2, 7], "taxonomi": 12, "teach": 12, "technic": [3, 12], "technicalstandard": 12, "technisch": 12, "tell": 8, "templat": [2, 9], "temporari": 7, "ten": 12, "term": [1, 12], "termin": [7, 8], "test": [8, 9, 11, 12], "text": [1, 12], "textdocu": 12, "than": [1, 2, 3, 8, 12], "thei": [1, 2, 3, 6, 7, 12], "them": [1, 3, 7, 8, 12], "theses": [6, 12], "thesi": [6, 12], "thi": [1, 2, 3, 4, 6, 7, 8, 11, 12], "thing": 3, "those": [1, 6, 7, 8, 11, 12], "though": [1, 7], "three": [1, 8], "through": [1, 2, 7], "time": [2, 6, 7, 8, 11, 12], "timedelta": 2, "timeout": 1, "titl": [1, 3, 6, 12], "tmp": [7, 8, 11], "token": 8, "token_hex": 8, "token_nam": 1, "tombston": 2, "toml": 7, "tool": [0, 6, 9, 12], "top": [1, 6, 8, 12], "topic": [6, 12], "toronto": [6, 12], "total": [1, 6, 12], "total_volum": 12, "track": 6, "transcrib": 12, "transfer": 1, "translat": 12, "trigger": [1, 2], "true": [1, 2, 6, 11, 12], "truli": 1, "try": [1, 7], "tweak": 3, "two": [1, 3, 6, 7, 8, 12], "txt": 0, "type": [1, 2, 3, 6, 9], "typic": 1, "u": [6, 8, 12], "ubuntu": 8, "ui": [1, 3, 4, 6, 7, 8, 11], "ukranian": [6, 12], "ultim": 7, "unchang": 1, "under": [0, 1, 7, 8, 11, 12], "underli": 4, "uni": 6, "uniqu": 1, "unit": [2, 6, 7], "univers": [6, 12], "unknown": [1, 3], "unless": [1, 7, 8, 11], "unlik": [6, 12], "unmark_record": 2, "unmark_record_for_purg": 2, "unprocessableent": 1, "unpublish": [6, 12], "unread": 6, "until": [1, 2, 7, 12], "uow": 2, "up": [2, 4, 7, 8, 11], "updat": [2, 3, 4, 6, 9, 11, 12], "update_draft": 2, "update_tombston": 2, "upload": [3, 7, 12], "upper": 1, "upstream": 9, "url": [1, 2, 6, 12], "us": [2, 3, 4, 6, 7, 9, 11, 12], "usag": [4, 6, 12], "useless": 3, "user": [2, 3, 4, 7, 9, 12], "user_data_upd": 1, "user_object": 12, "user_profil": 12, "usermod": 8, "usernam": [1, 6], "usr": 8, "usual": [1, 2, 7], "uuid": 1, "uwsgi": [7, 11], "uwsgi_api": 11, "uwsgi_rest": 8, "uwsgi_ui": [7, 8, 11], "v": [7, 8], "v1": [1, 12], "v16": 8, "v2": 8, "v3": [6, 12], "valid": [1, 2, 6, 12], "validate_draft": 2, "valu": [1, 2, 3, 6, 8, 12], "var": [8, 11], "variabl": [1, 6, 7], "variat": [6, 12], "varieti": [11, 12], "variou": [8, 11, 12], "veri": 7, "verifi": 2, "version": [0, 1, 2, 3, 6, 9, 11, 12], "via": [2, 6, 7, 8], "videorecord": 12, "view": [1, 2, 3, 6, 7, 12], "virtual": 8, "virtualenv": 8, "visibl": [1, 3, 7, 12], "visualart": 12, "vocabulari": [1, 3, 9, 11], "volum": [6, 7, 8, 12], "voluntarili": [6, 12], "wa": [1, 3, 6, 12], "wai": [1, 6, 8, 12], "walk": 7, "want": [1, 6, 7, 8, 11, 12], "warn": [1, 4, 6, 7, 12], "watch": [7, 8], "watercolor": [6, 12], "we": [1, 7, 12], "web": [7, 8, 11], "webdriv": 7, "webhook": [1, 6], "webpack": [3, 7], "webpackthemebundl": 7, "websit": 1, "well": [1, 2, 3, 6, 7, 8], "were": [1, 2, 3, 6, 8, 12], "what": [7, 12], "whatev": 2, "when": [1, 2, 3, 6, 7, 8, 11, 12], "whenev": [4, 7], "where": [1, 3, 7, 8, 11, 12], "wherev": 7, "whether": [1, 2, 6], "which": [1, 2, 3, 6, 7, 8, 11, 12], "whichev": 8, "while": [1, 2, 7, 11, 12], "white": [6, 12], "whitepap": 12, "who": 12, "whole": [3, 6, 12], "whose": [1, 3], "why": 1, "wide": 12, "widget": 3, "wikidata": 12, "window": [7, 8], "wip": 7, "wish": 1, "wit": 12, "within": [1, 3, 6, 12], "without": [1, 7, 8, 12], "word": 3, "wordpress": [], "work": [0, 2, 3, 6, 7, 11, 13], "worker": [1, 4, 7], "workflow": [2, 12], "workingpap": 12, "workpackagelead": 12, "workshop": [6, 12], "worldcat": [6, 12], "would": [1, 3, 4, 6, 7, 8, 12], "wp": 1, "wrap": [3, 4], "wrapper": 1, "writerofaccompani": 12, "written": 8, "wsl2": 7, "www": [6, 8, 12], "x": [8, 12], "x86_64": 8, "y": 1, "ye": [1, 7], "yet": [1, 8], "yml": [7, 8, 11], "you": [1, 4, 6, 7, 8, 11, 12], "your": [1, 7], "zenodo": 12, "zip": 1, "zshrc": 8}, "titles": ["About", "API", "KCWorks Architecture", "Changes", "CLI Commands", "Configuration of InvenioRDM", "Customizations to InvenioRDM", "Developing KCWorks", "In-depth Installation Instructions (NEEDS UPDATING)", "Welcome to the Knowledge Commons Works technical documentation!", "KCWorks Infrastructure", "Installation", "Metadata Schema, Vocabularies, and Identifiers", "Reference"], "titleterms": {"": [1, 2], "0": [3, 8], "01": 3, "09": 3, "1": [3, 8, 11], "10": [3, 8], "11": 3, "12": 3, "16": 8, "17": 8, "18": 3, "2": [3, 11], "20": 8, "2024": 3, "2025": 3, "3": [3, 8, 11], "30": 3, "4": [3, 11], "5": [3, 11], "6": 11, "9": 8, "A": 1, "In": [6, 8], "The": [1, 7], "about": [0, 6, 8], "access": 1, "ad": 7, "add": 8, "addit": 8, "admin": [8, 11], "ai_usag": [6, 12], "all": 1, "an": [1, 7, 8], "api": [1, 6, 12], "app": [2, 6], "applic": [8, 11], "architectur": 2, "asset": 11, "attach": 2, "augment": 2, "authent": [1, 6], "autom": 7, "baseservic": 2, "baseservicecompon": 2, "basic": 7, "beta3": 3, "beta4": 3, "beta5": 3, "beta6": 3, "beta7": 3, "beta8": 3, "bodi": 1, "book_seri": [6, 12], "branch": 7, "build": [7, 8, 11], "bulk": 6, "can": 1, "celeri": 8, "cfg": 7, "chang": [1, 3, 7], "chapter_label": [6, 12], "class": 2, "cli": [4, 8], "clone": [8, 11], "code": [1, 7, 8], "collect": [1, 6, 12], "collis": 1, "command": [4, 8], "commit": 7, "committee_deposit": [6, 12], "common": [1, 8, 9], "commons_domain": [6, 12], "commons_search_recid": [6, 12], "commons_search_upd": [6, 12], "commun": 6, "compon": 2, "compos": [8, 11], "config": 2, "configur": [1, 2, 5, 8, 11], "contain": [4, 8], "container": 8, "content": [6, 9], "content_warn": [6, 12], "context": 8, "contributor": [6, 12], "control": [7, 8, 11, 12], "copyright": 0, "core": [6, 12], "course_titl": [6, 12], "creat": [1, 8, 11], "creation": 12, "creator": [6, 12], "credenti": 8, "css": 7, "custom": [4, 6, 12], "custom_field": [6, 12], "data": [1, 6], "databas": [8, 11], "deeper": 7, "degre": [6, 12], "delet": 1, "deposit": 6, "deprec": 12, "depth": 8, "detail": 6, "develop": [7, 11], "dig": 7, "disciplin": [6, 12], "docker": [8, 11], "document": [9, 13], "doe": 1, "doi": 12, "draft": [], "duplic": 1, "edit": [6, 12], "email": 6, "enabl": 8, "endpoint": 1, "ensur": 8, "entri": 7, "environ": 8, "error": [1, 8], "event": 1, "exampl": 12, "extern": 7, "fail": 1, "fast": [7, 12], "field": [6, 12], "file": [1, 2, 7, 8, 11], "file_loc": [6, 12], "file_pid": [6, 12], "first": 6, "fix": 8, "flask": 11, "fly": 7, "folder": 7, "fork": 6, "form": 6, "found": 8, "framework": 6, "from": [2, 12], "fromconfigsearchopt": 2, "full": 11, "funder": 12, "get": 1, "git": [7, 11], "gnd": 12, "grid": 12, "group": [1, 6], "groups_for_deposit": [6, 12], "handl": [1, 12], "happen": 1, "have": 1, "hc": [6, 12], "hclegaci": [6, 12], "head": 12, "header": 1, "homosauru": 12, "html": 7, "i": [1, 8], "identifi": 12, "implement": [6, 12], "import": [1, 6], "includ": 7, "indic": 8, "inform": 8, "infrastructur": 10, "initi": [8, 11], "instal": [8, 11], "instanc": [1, 7], "institution_depart": [6, 12], "instruct": 8, "integr": 6, "intern": 1, "invalid": 1, "invenio": [4, 6, 7, 8], "inveniordm": [1, 2, 5, 6, 12, 13], "isbn": 12, "isni": 12, "issn": 12, "j": [7, 8], "javascript": 7, "json": 12, "just": 8, "kc": [6, 12], "kcr": [6, 12], "kcwork": [2, 4, 6, 7, 10, 11, 12], "knowledg": [8, 9], "layer": 2, "legaci": [6, 12], "limit": 7, "line": 8, "linux": 8, "load": 2, "local": [8, 11], "log": 8, "maco": 8, "make": 1, "malform": 1, "media": [6, 12], "meeting_organ": [6, 12], "metadata": [1, 6, 12], "metadata_field": [6, 12], "moder": 6, "modul": [6, 7, 8], "modular": 6, "name": [1, 7], "necessari": 1, "need": [7, 8], "new": [1, 7], "newli": 1, "nginx": 8, "node": [7, 8], "note": [6, 8], "notif": 6, "number": 7, "nvm": 8, "oai": 12, "object": [1, 12], "ofr": 12, "onli": 1, "orcid": 12, "organ": 12, "other": 11, "output": 8, "overrid": 6, "own": 11, "owner": 1, "ownership": 1, "packag": [6, 7], "page": 6, "pagin": 1, "paramet": 1, "patch": 1, "path": 8, "payload": 1, "peopl": 12, "permiss": 1, "pipenv": 8, "point": 7, "post": 1, "previously_publish": [6, 12], "primari": 12, "process": [7, 8], "project": 11, "project_titl": [6, 12], "provis": 6, "provision": 6, "publication_typ": [6, 12], "publication_url": [6, 12], "pyenv": 8, "python": [7, 8], "queri": 1, "queue": 8, "quickstart": 11, "rdm": 6, "rdmrecordservic": 2, "rebuild": 7, "recommend": 12, "record": [6, 12], "record_change_d": [6, 12], "record_creation_d": [6, 12], "record_identifi": [6, 12], "recordservic": 2, "refer": 13, "releas": 7, "remot": 6, "repositori": 11, "request": 1, "requir": [1, 8], "resourc": [6, 12], "respons": 1, "rest": 1, "retriev": [1, 12], "role": 12, "ror": 12, "rotat": 8, "run": [4, 7], "saml": [1, 6], "schema": [6, 12], "scheme": 12, "script": 8, "search": [2, 6, 8], "searchconfig": 2, "searchoptionsmixin": 2, "secondari": 12, "sensit": 8, "server": 8, "servic": [2, 8, 11], "setup": 11, "shutdown": 8, "site": [6, 7, 12], "slow": 7, "some": [1, 8], "sort": 1, "specif": 1, "sponsoring_institut": [6, 12], "standard": 8, "start": [8, 11], "startup": 8, "static": 7, "statu": 1, "step": [], "strategi": 7, "streamlin": 1, "subject": [6, 12], "submitter_affili": [6, 12], "submitter_email": [6, 12], "submitter_id": [6, 12], "submitter_org_membership": [6, 12], "submitter_usernam": [6, 12], "submodul": 7, "success": 1, "sync": 6, "system": [6, 8, 12], "tabl": [], "tag": 7, "task": 8, "technic": 9, "templat": [6, 7], "test": 7, "theme": 7, "time": 1, "token": 1, "tool": 8, "total_download": [6, 12], "total_view": [6, 12], "type": 12, "unsuccess": 1, "updat": [1, 7, 8], "upload": 1, "upstream": 7, "us": [1, 8], "user": [1, 6, 8, 11], "user_defined_tag": [6, 12], "usernam": 12, "uwsgi": 8, "variabl": [2, 8], "version": [7, 8], "via": 1, "view": [8, 11], "vocabulari": [6, 12], "webhook": [], "welcom": 9, "what": 1, "who": 1, "work": [1, 8, 9, 12], "worker": 8, "your": [8, 11]}}) \ No newline at end of file diff --git a/docs/source/api.md b/docs/source/api.md index e69de29bb..04abde259 100644 --- a/docs/source/api.md +++ b/docs/source/api.md @@ -0,0 +1,1436 @@ +# API + +KCWorks provides a robust REST API that allows clients to perform most operations on KCWorks records and collections. + +## The InvenioRDM REST API + +KCWorks is built on top of InvenioRDM, which provides a REST API for creating, managing, and querying records. This API is documented at https://inveniordm.docs.cern.ch/reference/rest_api_index/. + +> **Note:** "Collections" are referred to as "communities" in the InvenioRDM API and its documentation. To avoid confusion with the social groups that are part of the Knowledge Commons network, KCWorks uses the term "collections" in its documentation and user interface. But operations involving collections are handled via the "communities" endpoint in the InvenioRDM REST API. + +This REST API allows clients to retrieve and manage the following resources: + +| Resource | Supported Operations | Requires Authentication | Endpoint | InvenioRDM API documentation | +| -------- | ----------- | ------------- | -------- | ---------------------------- | +| draft works | read | yes | GET /api/records/{id}/draft | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-draft-record | +| | create | yes | POST /api/records/ | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#create-a-draft-record | +| | update | yes | PUT /api/records/{id} | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#update-a-draft-record | +| | publish | yes | POST /api/records/{id}/draft/actions/publish | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#publish-a-draft-record | +| | delete | yes | DELETE /api/records/{id}/draft | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#delete-a-record | +| | list files | yes | GET /api/records/{id}/draft/files | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#list-a-drafts-files | +| | upload files[^draft-file-upload] | yes | POST /api/records/{id}/draft/files | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#start-draft-file-uploads | +| | view file metadata | yes | GET /api/records/{id}/draft/files/{filename} | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-draft-files-metadata | +| | download file | yes | GET /api/records/{id}/draft/files/{filename}/content | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#download-a-draft-file | +| | delete file | yes | DELETE /api/records/{id}/draft/files/{filename} | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#delete-a-draft-file | +| published works | read[^published-work-read] | no | GET /api/records/ | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-a-record | +| | read all versions | no | GET /api/records/{id}/versions | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-all-versions | +| | read latest version | no | GET /api/records/{id}/versions/latest | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-latest-version | +| | search | no | GET /api/records/ | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#search-records | +| | update[^published-work-update] | yes | POST /api/records/{id}/draft | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#update-a-draft-record | +| | create new version | yes | POST /api/records/{id}/versions | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#create-a-new-version | +| | attach files from a previous version[^attach-files-from-previous-version] | yes | POST /api/records/{id}/draft/actions/files-import | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#link-files-from-previous-version | +| | list files | no | GET /api/records/{id}/files | https://inveniordm.docs.cern.ch/reference/rest_api_drafts_records/#get-all-files | +| collections | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_communities/ | +| collection memberships | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_members/ | +| reviews | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_reviews/ | +| requests | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_requests/ | +| users | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_users/ | +| groups[^groups] | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_groups/ | +| vocabularies | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_vocabularies/ | +| names | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_names/ | +| funders | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_funders/ | +| awards | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_awards/ | +| OAI-PMH sets | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_oaipmh_sets/ | +| statistics | *Documentation forthcoming* | | | https://inveniordm.docs.cern.ch/reference/rest_api_statistics/ | + + +[^published-work-read]: Note that each version of a published work has its own `id`. The `read` operation retrieves the specific version whose `id` is provided. +[^published-work-update]: Published works cannot be updated directly. Rather, the update API call creates a new draft of the published work. The original version of the published work is not yet modified and remains discoverable via the search API. This draft may then be published, in which case it replaces the original published work. Note that this kind of update can only be made to the metadata of the published work, not to the files associated with it. In order to update the files associated with a published work, the client must create a new version of the work. +[^draft-file-upload]: The file upload process involves three separate API calls: + - `POST /api/records/{id}/draft/files` to start the upload process + - `POST /api/records/{id}/draft/files/{filename}/content` to upload the file's content + - `POST /api/records/{id}/draft/files/{filename}/commit` to finalize the upload and attach the file to the draft record +[^attach-files-from-previous-version]: The `files-import` operation attaches files from a previous version of a work to *the current draft* of that work. If the work already has multiple versions, clients must specify the `id` of the version whose files are to be attached. +[^groups]: Note that the `groups` managed by the `groups` API endpoint are not the same as KCWorks social groups or InvenioRDM collections. These are instead sets of users sharing a set of system permissions. (Under the hood these are wrappers around the invenio-accounts `Role` class.) This might include the "administration" group who are assigned certain admin permissions. It might also include a group who are all assigned "curator" permissions for a collection, etc. + +Note that for some operations where authentication is required, the client must also possess the appropriate permissions. (E.g., to edit a draft work, manage collection requests, etc.) + +Note that several operations are NOT possible via the REST API, including: + +- searching draft records + - Draft records are by definition not intended to be distributed, and so are not discoverable via the search API until they have been published. +- creating a published work directly + - Published works can only be created by first creating a draft work and then publishing it. +- deleting a published work + - Published works are generally considered to be permanent and so cannot be deleted. If desired, access to a published work, or a version of a published work, can be set to "restricted", in which case the work is no longer discoverable via the search API. +- modifying files for a published work + - Again, published works are generally considered to be permanent and so their files cannot be modified. The only way to update the files associated with a published work is to create a new version of the work. If desired, access to the original version of the published work can be set to "restricted", in which case only the new version and its files are discoverable. +- creating or modifying user accounts + - The REST API endpoint for users is currently read-only. It is not possible to create or modify user accounts, or change and user profile information, via the REST API. These operations are handled via the KCWorks admin interface or via CLI commands. +- creating or modifying groups (permissions) +- creating or modifying controlled vocabularies + +### Creating a new Work via the InvenioRDM REST API + +Creating a new Work via the REST API requires several steps. + +- Step 1: Create a draft record + +- Step 2: Initialize the file upload + +- Step 3: Upload the file content + - This step must be repeated for each file being added to the work. + +- Step 4: Commit the file upload + +- Step 5: Publish the draft record + +If you want the work to be included in a collection at publication time, you must submit a request for the work to be published in the collection. The first four steps are the same as above, but in place of Step 5 (publication), you must submit a request for the work to be published in the collection. + +- Step 5: Create a review request + +- Step 6: Submit review request + +If the collection in question requires review before publication, the request will not be published until the review is accepted. + +- Step 7: Accept and publish the record + +## Streamlined Import API + +In order to streamline the process of uploading works to KCWorks, particularly for works intended for publication in a collection, KCWorks provides a streamlined import API. This API allows clients to upload a work and its files in a single step, without the need to create a draft record, initialize file uploads, commit file uploads, or submit a review request. + +Why is this API needed? The InvenioRDM REST API can be fragile and difficult to use, particularly for clients who are not familiar with the system. The creation and acceptance of a review request is redundant where collection administrators are uploading works for a collection they administer. The file upload steps are also not truly stateless, introducing the possibility of a file upload being interrupted and left incomplete, even if the upload of the file's content was successful. + +### Who can use the import API? + +The import API is available to authorized organizations who have obtained an OAuth token for API operations. + +The import API places the works directly in a collection, without passing through the review process. So, the user to whom the token is issued must have sufficient permissions to publish directly in the collection. The exact role required depends on the collection's review policy: +- *If the review policy allows managers and curators to skip the review process*, the user of the import API must have one of the roles "manager," "curator," or "owner" in the collection. +- *If the review policy requires all submissions to be reviewed*, the user of the import API must have the "owner" role in the collection. + +### The import request + +#### Request +``` +POST https://works.hcommons.org/api/import/ HTTP/1.1 +``` + +#### Required headers +``` +Content-Type: multipart/form-data +Accept: application/json +Authorization: Bearer \ +``` + +#### Request url path parameters + +Only one URL path parameter is required: + +| Name | Required | Type | Description | +|------|----------|------|-------------| +| `collection` | no | `string` | The ID (either the url slug or the UUID) of the collection to which the work should be published. If this value is provided, the work will be submitted to the collection immediately after import. If the collection requires review, and the `review_required` parameter is set to "true", the work will be placed in the collection's review queue. | + + +#### Request body + +This request must be made with a multipart/form-data request. The request body must include parts with following names: + +| Name | Required | Content Type | Description | +|-------|----------|--------------|-------------| +| `files` | yes | `application/octet-stream` | The (binary) file content to be uploaded. If multiple files are being uploaded, a body part with this same name ("files") must be provided for each file. If more than three or four files are being uploaded, it is recommended to provide a single zip archive containing all of the files. The files will be assigned to the appropriate work based on filename, so where multiple files are provided these **must be unique**. If a zip archive is provided, the files must be contained in a single compressed folder with no subfolders. | +| `metadata` | yes | `application/json` | An array of JSON metadata objects, each of which will be used to create a new work. Each must following the KCWorks implementation of the InvenioRDM metadata schema described {ref}`here `. In addition, an array of owners for the work may optionally be provided by adding an `access.owned_by` property to each metadata object. Note that if no owners are provided, the work will be created with the organizational account that issued the OAuth token as the owner. | +| `review_required` | no | `text/plain` | A string representation of a boolean (either "true" or "false") indicating whether the work should be reviewed before publication. This setting is only relevant if the work is intended for publication in a collection that requires review. It will override the collection's usual review policy, since the work is being uploaded by a collection administrator. (Default: "true") | +| `strict_validation` | no | `text/plain` | A string representation of a boolean (either "true" or "false") indicating whether the import request should be rejected if any validation errors are encountered. If this value is "false", the imported work will be created in KCWorks even if some of the provided metadata does not conform to the KCWorks metadata schema, provided these are not required fields. If this value is "true", the import request will be rejected if any validation errors are encountered. (Default: "true") | +| `all_or_none` | no | `text/plain` | A string representation of a boolean (either "true" or "false") indicating whether the entire import request should be rejected if any of the works fail to be created (whether for validation errors, upload errors, or other reasons). If this value is "false", the import request will be accepted even if some of the works cannot be created. The response in this case will include a list of works that were successfully created and a list of errors for the works that failed to be created. (Default: "true") | + +#### Identifying the owners of the work + +The array of owners, if provided in a metadata object's `parent.access.owned_by` property, must include at least the full name and email address of the users to be added as owners of the work. If the user already has a Knowledge Commons account, their username should also be provided. Additional identifiers (e.g., ORCID) may be provided as well to help avoid duplicate accounts, since a KCWorks account will be created for each user if they do not already have one. + +| key | required | type | description | +|-----|----------|------|-------------| +| `full_name` | yes | `string` | The full name of the user. | +| `email` | yes | `string` | The email address of the user. | +| `identifiers` | no | `array` | An array of identifiers for the user. Any identifier schemes supported by KCWorks will be accepted. If the user already has a KCWorks account, the `kc_username` scheme should be used and the user's username provided as the identifier. If you wish to provide an ORCID, it is recommended to use the `orcid` scheme. Identifiers for external organizations should be provided using the `import_user_id` scheme. | + +The resulting `owners` list should be shaped like this: + +```json +[ + { + "full_name": "John Doe", + "email": "john.doe@example.com", + "identifiers": [ + { + "identifier": "0000-0000-0000-0000", + "scheme": "orcid" + }, + { + "identifier": "jdoe", + "scheme": "kc_username" + }, + { + "identifier": "1234567890", + "scheme": "import_user_id" + } + ] + } +] +``` +Note that it is *not* assumed that the creators of a work should be the work's owners. The creators will only be added as owners if each of them is listed in the `access.owned_by` property of the work's metadata object. + +> Note, too, that only the first member of the owners array will technically be assigned as the work's owner in KCWorks. The other owners will be assigned access grants to the work with "manage" permissions. + +#### Identifying the work for import + +It is crucial that each work to be imported is assigned a unique identifier. This may be an identifier used internally by the importing organization, it may be a universally unique string such as a UUID, or it may be a universal identifier such as a DOI or a handle. In either case it must be unique across all works to be imported for the collection. This identifier will be used to identify the work in the response, and will be used to identify the work when checking for duplicate imports. + +The identifier may be provided in the `metadata` object as an `identifiers` array with the scheme `import-recid`. E.g., + +```json +{ + "identifiers": [ + { + "identifier": "1234567890", + "scheme": "import-recid" + }, + // ... other identifiers ... + ] +} +``` + +### Example import request + +The following example shows a request to import a single work with two files and a single owner. + +#### Metadata JSON object + +The metadata JSON string for a journal article with a PDF file and a Word file, with a single owner might look like the sample below. **Note that the metadata must be provided as an array of metadata objects, even if it contains only a single object.** + +```json +[{ + "metadata": { + "resource_type": { + "id": "textDocument-journalArticle", + }, + "creators": [ + { + "person_or_org": { + "type": "personal", + "name": "Fitzpatrick, Kathleen", + "given_name": "Kathleen", + "family_name": "Fitzpatrick", + "identifiers": [ { "identifier": "kfitz", "scheme": "kc_username" } ] + }, + "role": { "id": "author" }, + "affiliations": [ { "name": "Modern Languages Association" } ] + } + ], + "title": "Giving It Away: Sharing and the Future of Scholarly Communication", + "publisher": "University of Toronto Press Inc. (UTPress)", + "publication_date": "2012", + "languages": [ { "id": "eng" } ], + "identifiers": [ + { "identifier": "1234567890", "scheme": "import-recid" }, + { "identifier": "10.3138/jsp.43.4.347", "scheme": "doi" }, + { "identifier": "1710-1166", "scheme": "issn" }, + ], + "rights": [ + { + "id": "cc-by-4.0", + "title": { + "en": "Creative Commons Attribution 4.0 International" + }, + } + ], + "description": "Open access has great potential to transform the future of scholarly communication, but its success will require a focus on values -- and particularly generosity -- rather than on costs." + }, + "custom_fields": { + "journal:journal": { + "title": "Journal of Scholarly Publishing", + "issue": "4", + "volume": "43", + "pages": "347-362", + "issn": "1198-9742" + }, + "kcr:user_defined_tags": [ + "open access", + "Scholarly communication" + ], + }, + "parent": { + "owned_by": [ + { + "full_name": "Kathleen Fitzpatrick", + "email": "kfitz@msu.edu", + "identifiers": [ { "identifier": "kfitz", "scheme": "kc_username" } ] + } + ] + }, + "files": { + "enabled": true, + "entries": { + "fitzpatrick-givingitaway.docx": { + "size": 149619, + "key": "fitzpatrick-givingitaway.docx", + }, + "fitzpatrick-givingitaway.pdf": { + "size": 234567, + "key": "fitzpatrick-givingitaway.pdf", + } + } + }, +}] +``` + +#### Request + +To submit the article to be included in the `my-organization` collection, one might use a command line tool like `curl`, with the following command. + +``` +curl -X POST https://works.hcommons.org/api/import/my-collection-id \ + -H "Content-Type: multipart/form-data" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer " \ + -F "files=@path/to/files/fitzpatrick-givingitaway.pdf" \ + -F "files=@path/to/files/fitzpatrick-givingitaway.docx" \ + -F "metadata={// ... metadata JSON object goes here as a string ... //}" +``` + +Of course, in most cases the request will be made programmatically, not via a command line tool. The syntax for the request will vary depending on the programming language and tools being used. + +### A successful import response + +``` +HTTP/1.1 201 Created +Content-Type: application/json +``` + +This response will include a JSON object with the following fields: + +- `status`: The status of the import request, which will be "success" if the import request was successful. +- `data`: An array of JSON objects, one for each record that was created in the operation +- `errors`: An array of JSON objects, one for each record that failed to be created. (In a successful import, this array will be empty.) +- `message`: A message describing the import request. (In a successful import, this will be "All records were successfully imported".) + +Each object in the `data` array will have the following fields: + +| key | type | description | +|-----|------|-------------| +| `item_index` | `integer` | The index of the record in the import request. (Starting with 0 for the first record.) | +| `record_id` | `string` | The internal KCWorks ID of the new work. | +| `source_id` | `string` | The external identifier for the work that was provided in the import request using the `import-recid` scheme. | +| `record_url` | `string` | The URL of the new work. This is the URL of the work's landing page on KCWorks. Other URLs for the work, including the endpoints for API operations, are available in the `links` property of the record's `metadata` object. | +| `files` | `object` | An object whose keys are the filenames for the files that were successfully uploaded and whose values are 2 member arrays. The first member is a string representing the status of the file upload operation. The second member is an array of string error messages if any errors occurred during the upload. Further details about the files, including their size and checksum, are available in the `files` property of the `metadata` object. | +| `collection_id` | `string` | The ID of the collection to which the work was published, if any. This is provided for convenience. Details about the collection are available in the `parent.communities` property of the `metadata` object. | +| `errors` | `array` | A list of objects, each of which describes an error that occurred during the import process. These might include validation errors for certain fields in the provided metadata that did not prevent creation of the work. | +| `metadata` | `object` | The metadata for the created work, in JSON format, following the KCWorks implementation of the InvenioRDM metadata schema described {ref}`here `. The returned metadata will include internal KCWorks system fields such as `created`, `updated`, `revision_id`, `id`, etc. It is identical to the metadata that would be returned by a GET request to the records API endpoint on KCWorks. | + +The response object will be shaped like this: + +```json +{ + "status": "success", + "data": [ + { + "item_index": 0, + "record_id": "1234567890", + "record_url": "https://works.hcommons.org/records/1234567890", + "files": { + "file1.pdf": ["success", []], + "file2.pdf": ["success", []] + }, + "collection_id": "1234567890", + "errors": [], + "metadata": { + /* ... */ + } + }, + { + "item_index": 1, + "record_id": "1234567891", + "record_url": "https://works.hcommons.org/records/1234567891", + "files": { + "file1.pdf": ["success", []], + "file2.pdf": ["success", []] + }, + "collection_id": "1234567890", + "errors": [], + "metadata": { + /* ... */ + } + } + ], + "errors": [], + "message": "All records were successfully imported." +} +``` + +### An unsuccessful import response + +#### The token does not have the necessary permissions + +``` +HTTP/1.1 403 Forbidden +Content-Type: application/json +``` + +This response will include a JSON object with the following fields: + +```json +{ + "status": "error", + "message": "The user does not have the necessary permissions." +} +``` + +#### The request metadata is malformed or invalid + +```http +HTTP/1.1 400 Bad Request +Content-Type: application/json +``` + +This response is returned when some of the provided metadata for all of the works to be imported is malformed or invalid. This indicates that *none of the works has been created* and a new request must be made with corrected metadata. This response will only be received if either +a. the `strict_validation` request parameter was set to "true" and all of the supplied metadata objects raise validation errors, or +b. the `strict_validation` parameter is set to "false", but the validation errors affected fields that are required for the works to be created. +c. the `all_or_none` request parameter is set to "true" and some of the supplied metadata objects raise validation errors. + +The response will include a JSON object with the same shape as the successful response, but with the following differences: + +- The `status` field will be "error". +- The `data` field will be an empty array. +- The `errors` field will be an array of objects, each of which describes a work that failed to be created. In each object the `record_id` and `record_url` fields will be `null`, since the work was not created. The `errors` field will be an array of objects, each of which describes an error that occurred during the attempt to create the work. The `metadata` field will still contain the metadata that was provided in the request for reference. + +```json +{ + "status": "error", + "message": ( + "No records were successfully imported. Please check the list of failed records " + "in the 'errors' field for more information. Each failed item should have its own " + "list of specific errors." + ), + "data": [], + "errors": [ + { + "item_index": 0, + "record_id": null, + "record_url": null, + "errors": [ + { + "field": "title", + "message": "Required field missing." + } + ], + "files": {}, + "collection_id": "1234567890", + "metadata": { + /* ... */ + } + }, + { + "item_index": 1, + "record_id": null, + "record_url": null, + "errors": [ + { + "field": "metadata.creators.0.occupation", + "message": "Unknown field." + }, + { + "field": "metadata.publication_date", + "message": "Date is not in Extended Date Time Format (EDTF)." + } + ], + "files": {}, + "collection_id": "1234567890", + "metadata": { + /* ... */ + } + } + ] +} +``` + +### A partially successful import response + +> NOT YET IMPLEMENTED. At present the `all_or_none` request parameter will always be "true". + +If only some of the works to be imported are malformed or invalid, and the `all_or_none` request parameter is set to "false", the response will be `207 Multi-Status`. In this case the response will be shaped much like the successful and unsuccessful responses described above, but there will be items in *both* the `data` and `errors` arrays. The items in the `data` array will be works that were successfully created, and the items in the `errors` array will be works that failed to be created. + +The response will be shaped like this: + +```json +{ + "status": "multi_status", + "message": ( + "Some records were successfully imported, but some failed. Please check the " + "list of failed records in the 'errors' field for more information. Each failed " + "item should have its own list of specific errors." + ), + "data": [ + { + "item_index": 1, + "record_id": "1234567891", + "source_id": "xxx1234567891", + "record_url": "https://works.hcommons.org/records/1234567891", + "files": { + "file1.pdf": ["success", []], + "file2.pdf": ["success", []] + }, + "collection_id": "1234567890", + "errors": [], + "metadata": { + /* ... */ + } + } + ], + "errors": [ + { + "item_index": 0, + "record_id": null, + "source_id": "xxx1234567890", + "record_url": null, + "errors": [ + { + "field": "title", + "message": "Required field missing." + } + ], + "files": {}, + "collection_id": "1234567890", + "metadata": { + /* ... */ + } + }, + ] +} +``` + +#### The request file upload failed + +```http +HTTP/1.1 400 Bad Request +Content-Type: application/json +``` + +If the file content is uploaded but for some reason is considered corrupted or invalid, a `400 Bad Request` response will be returned. This response will include a JSON object with the following fields: + +```json +{ + "status": "error", + "message": ( + "No records were successfully imported. Please check the list of failed records " + "in the 'errors' field for more information. Each failed item should have its own " + "list of specific errors." + ), + "data": [], + "errors": [ + { + "item_index": 0, + "record_id": null, + "source_id": "xxx1234567890", + "record_url": null, + "errors": [ + { + "validation_error": { + "metadata": {"title": ["Missing data for required field."]} + } + } + ], + "files": { + "file1.pdf": ["uploaded", []] + }, + "collection_id": "1234567890", + "metadata": { + /* ... */ + } + }, + { + "item_index": 1, + "record_id": null, + "source_id": "xxx1234567891", + "record_url": null, + "errors": [ + { + "validation_error": { + "metadata": {"creators" {"occupation": ["Unknown field."]}} + } + }, + { + "validation_error": { + "metadata": {"publication_date": ["Date is not in Extended Date Time Format (EDTF)."]} + } + }, + { + "file upload failures": { + "sample.pdf": [ + "failed", + ["File sample.pdf not found in list of files."], + ] + }, + }, + ], + "files": { + "sample.pdf": ["failed", ["File sample.pdf not found in list of files."]], + }, + "collection_id": "1234567890", + "metadata": { + /* ... */ + } + } + ] +} +``` + +If an upload simply fails to complete and times out, the client will instead receive a `504 Gateway Timeout` response. + +### What happens to an import request that fails? + +If all steps of an import request do not complete successfully, the work will not be created. The files that were successfully uploaded will be deleted, and any draft record created as part of the import request will be deleted. The client may attempt the import request again. + +### Making duplicate import requests + +Note that it is possible to make duplicate import requests *unless* the work to be imported includes a pre-existing DOI identifier or some other unique identifier that has already been registered in KCWorks. In this case, the import request will be rejected with a `409 Conflict` response code and a `Location` header pointing to the existing work. + +In the absence of such a unique identifier, however, KCWorks will not try to detect duplicate works based on the metadata, file name, or file content. If the same work is imported multiple times without a pre-existing unique identifier, it will be created multiple times in KCWorks and each version will be assigned a newly minted DOI. + + +## Group Collections API + +``` +https://works.hcommons.org/api/group_collections +``` + +The `group_collections` REST API endpoint allows a client to create, read, modify, or delete a collection in KCWorks owned and administered by a Knowledge Commons group. GET requests to retrieve information about group collections are open to all clients. POST, PUT, and DELETE requests are secured by an oauth token that must be obtained from the Knowledge Commons Works administrator. + +This endpoint is not configured to receive all of the metadata required to create or modify group collections. Rather, the `group_collections` endpoint receives minimal signals from a Commons Instance and then obtains the full required metadata via an API callback to the Commons instance. + +> [!NOTE] +> KCWorks uses the term "collection" in place of the default term "community" employed in other InvenioRDM installations. This is partly to accommodate exactly the integration with Knowledge Commons groups that is discussed here. + + +### Group collection owner + +InvenioRDM does not allow groups to be owners of a collection (community). When a collection is created for a group, though, we do not know which of the group's administrators to assign as the individual owner. It is also awkward to change ownership of a collection later on if the group's administrativer personnel change. So the collection is owned by an administrative user who is assigned the role `group-collections-owner`. The group's administrators are then assigned privileges as "managers" of the group collection. This allows them to manage the collection's settings and membership, but not to delete the collection or change its ownership. + +Before the invenio_group_collections_kcworks module can be used, the administrator must create a role called `group-collections-owner` and assign membership in that role to one administrative user account. If multiple user accounts belong to that role, the first user account in the list will be assigned as the owner of group collections. If no user accounts belong to the role, the group collection creation will fail with a NoOwnerAvailable error. + +### Endpoint configuration + +The configuration variable `GROUP_COLLECTIONS_METADATA_ENDPOINTS` must be provided in the `invenio.cfg` file in order to use this endpoint. This variable should hold a dictionary whose keys are Commons instance names. The value for each key is a dictionary containing the following keys: + +| key | value type | required | value | +| --- | ---------- | ----- | ----- | +| `url` | str | Y | The url on the Commons instance where a GET request can retrieve the metadata for a group. The url should include the placeholder `{id}` where the Commons instance id for the requested group should be placed. | +| `token_name` | str (upper case) | Y | The name of the environment variable that will hold the authentication token for requests to the Commons instance url for retrieving group metadata. | +| `placeholder_avatar` | str | N | The filename or last url component that identifies a placeholder avatar in the avatar image url supplied for the Commons group avatar. | + +A typical configuration might look like the following: + +```python +GROUP_COLLECTIONS_METADATA_ENDPOINTS = { + "knowledgeCommons": { + "url": "https://hcommons-dev.org/wp-json/commons/v1/groups/{id}", + "token_name": "COMMONS_API_TOKEN", + "placeholder_avatar": "mystery-group.png", + }, +} +``` + +### Retrieving Group Collection Metadata (GET) + +A GET request to this endpoint will retrieve metadata on Invenio collections +that are owned by a Commons group. A request to the bare endpoint without a +group ID or collection slug will return a list of all collections owned by +all Commons groups. (Commons Works collections not linked to a Commons group will not be included. If you wish to query all groups, please use the `communities` API endpoint.) + +#### Query parameters + +Four optional query parameters can be used to filter the results: + +| Parameter name | Description | +| ---------------|------------ | +| `commons_instance` | the name of the Commons instance to which the group belongs. If this parameter is provided, the response will only include collections owned by groups in that instance. | +| `commons_group_id` | the ID of the Commons group. If this parameter is provided, the response will only include collections owned by that group. | +| `collection` | the slug of the collection. If this parameter is provided, the response will include only metadata for that collection. | +| `page` | the page number of the results | +| `size` | the number of results to include on each page | +| `sort` | the kind of sorting applied to the returned results | + +#### Sorting + +The `sort` parameter can be set to one of the following sort types: + +| Field name | Description | +| -----------|-------------| +| newest | Descending order based on `created` date | +| oldest | Ascending order based on `created` date | +| updated-desc | Descending order based on `updated` date | +| updated-asc | Ascending order based on `updated` date | + +By default the results are sorted by `updated-desc` + +#### Pagination + +Long result sets will be paginated. The response will include urls for the `first`, `last`, `previous`, and `next` pages of results in the `link` property of the response body. A url for the current page of results will also be included in the list as a `self` link. By default the page size is 25, but this can be changed by providing a value for the `size` query parameter. + +#### Requesting all collections + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections HTTP/1.1 +``` + +##### Successful Response Status Code + +`200 OK` + +##### Successful response body + +```json +{ + "aggregations": { + "type": { + "buckets": [ + { + "doc_count": 50, + "is_selected": false, + "key": "event", + "label": "Event", + }, + { + "doc_count": 50, + "is_selected": false, + "key": "organization", + "label": "Organization", + }, + ], + "label": "Type", + }, + "visibility": { + "buckets": [ + { + "doc_count": 100, + "is_selected": false, + "key": "public", + "label": "Public", + } + ], + "label": "Visibility", + }, + }, + "hits": { + "hits": [ + { + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "access": { + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + }, + "children": {"allow": false}, + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 100, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for panda research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } + }, + /* ... */ + ], + "total": 100, + }, + "links": { + "self": "https://works.hcommons.org/api/group_collections", + "first": "https://works.hcommons.org/api/group_collections?page=1", + "last": "https://works.hcommons.org/api/group_collections?page=10", + "prev": "https://works.hcommons.org/api/group_collections?page=1", + "next": "https://works.hcommons.org/api/group_collections?page=2", + } + "sortBy": "newest", + "order": "ascending", +} +``` + +##### Successful Response Headers + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | + +#### Requesting collections for a Commons instance + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&sort=updated-asc HTTP/1.1 +``` + +##### Successful response status code + +`200 OK` + +##### Successful Response Body: + +```json +{ + "aggregations": { + "type": { + "buckets": [ + { + "doc_count": 45, + "is_selected": false, + "key": "event", + "label": "Event", + }, + { + "doc_count": 45, + "is_selected": false, + "key": "organization", + "label": "Organization", + }, + ], + "label": "Type", + }, + "visibility": { + "buckets": [ + { + "doc_count": 90, + "is_selected": false, + "key": "public", + "label": "Public", + } + ], + "label": "Visibility", + }, + }, + "hits": { + "hits": [ + { + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "access": { + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + }, + "children": {"allow": false}, + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 100, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for panda research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } + }, + /* ... */ + ], + "total": 90, + }, + "links": { + "self": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons", + "first": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1", + "last": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=9", + "prev": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=1", + "next": "https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&page=2", + } + "sortBy": "updated-asc", +} +``` + +##### Successful response headers + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Link | `; rel="first", ; rel="last", ; rel="prev", ; rel="next"` | + + +#### Requesting collections for a specific group + +Note that if you specify a `commons_group_id` value, you must *also* provide a `commons_instance` value. This is to avoid confusion if different Commons instances use the same internal id for groups. + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections?commons_instance=knowledgeCommons&commons_group_id=12345 HTTP/1.1 +``` + +##### Successful response status code + +`200 OK` + +##### Successful Response Body: + +```json +{ + "aggregations": { + "type": { + "buckets": [ + { + "doc_count": 2, + "is_selected": false, + "key": "event", + "label": "Event", + }, + { + "doc_count": 2, + "is_selected": false, + "key": "organization", + "label": "Organization", + }, + ], + "label": "Type", + }, + "visibility": { + "buckets": [ + { + "doc_count": 4, + "is_selected": false, + "key": "public", + "label": "Public", + } + ], + "label": "Visibility", + }, + }, + "hits": { + "hits": [ + { + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "access": { + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + }, + "children": {"allow": false}, + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 2, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for panda research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } + }, + /* ... */ + ], + "total": 4, + }, + "links": { + "self": "https://works.hcommons.org/api/group_collections", + "first": "https://works.hcommons.org/api/group_collections?page=1", + "last": "https://works.hcommons.org/api/group_collections?page=1", + "prev": "https://works.hcommons.org/api/group_collections?page=1", + "next": "https://works.hcommons.org/api/group_collections?page=1", + } + "sortBy": "newest", +} +``` + +##### Successful response headers + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | + +#### Requesting a specific collection + +While other kinds of requests require query parameters, a request for metadata on a specific Commons Works collection can be made by simply adding the community's slug to the end of the url path. Once again, this will only succeed for collections that are linked to a Commons instance group. Collections that exist independently on Knowledge Commons Works will not be found at the `group_collections` endpoint and should be requested at the `communities` endpoint instead. + +##### Request + +```http +GET https://works.hcommons.org/api/group_collections/my-collection-slug HTTP/1.1 +``` + +##### Successful Response Status Code + +`200 OK` + +##### Successful Response Body: + +```json +{ + "id": "5402d72b-b144-4891-aa8e-1038515d68f7", + "created": "2024-01-01T00:00:00Z", + "updated": "2024-01-01T00:00:00Z", + "links": { + "self": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7", + "self_html": "https://works.hcommons.org/communities/panda-group-collection", + "settings_html": "https://works.hcommons.org/communities/panda-group-collection/settings", + "logo": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/logo", + "rename": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/rename", + "members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members", + "public_members": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/members/public", + "invitations": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/invitations", + "requests": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/requests", + "records": "https://works.hcommons.org/api/communities/5402d72b-b144-4891-aa8e-1038515d68f7/records", + "featured": "https://works.hcommons.org/api/" + "communities/" + "5402d72b-b144-4891-aa8e-1038515d68f7/" + "featured", + }, + "revision_id": 1, + "slug": "panda-group-collection", + "metadata": { + "title": "The Panda Group Collection", + "curation_policy": "Curation policy", + "page": "Information for the panda group collection", + "description": "This is a collection about pandas.", + "website": "https://works.hcommons.org/pandas", + "organizations": [ + { + "name": "Panda Research Institute", + } + ], + "size": 100, + }, + "deletion_status": { + "is_deleted": false, + "status": "P", + }, + "custom_fields": { + "kcr:commons_instance": "knowledgeCommons", + "kcr:commons_group_description": "This is a group for pandas research.", + "kcr:commons_group_id": "12345", + "kcr:commons_group_name": "Panda Research Group", + "kcr:commons_group_visibility": "public", + }, + "access": { + "visibility": "public", + "member_policy": "closed", + "record_policy": "open", + "review_policy": "open", + } +} +``` + +### Creating a Collection for a Group (POST) + +A POST request to this endpoint creates a new collection in Invenio owned by the specified Commons group. If the collection is successfully created, the response status code will be 201 Created, and the response body will be a JSON object containing the URL slug for the newly created collection. + +The POST request will trigger a callback to the Commons instance to get the metadata for the specified group, using the configuration dictionary declared in the `GROUP_COLLECTIONS_METADATA_ENDPOINTS` config variable, under the key matching the Commons instance's SAML IDP provider name (declared in the `SSO_SAML_IDPS` config variable). This callback request will be sent to the "url" specified in the configuration dictionary (e.g., `GROUP_COLLECTIONS_METADATA_ENDPOINTS["knowledgeCommons"]["url"]`). This request will be authenticated using the environment variable whose name matches the `token_name` from the same configuration dictionary. The metadata from this callback request will then be used to populate the collection metadata in Invenio. + +If the metadata returned from the Commons instance includes a url for an avatar, that avatar will be downloaded and stored in the Invenio instance's file storage. Since we do not want to use a placeholder avatar for the group, the instance's configuration can include a `placeholder_avatar` key. If the file name or last segment of the supplied avatar url matches this `placeholder_avatar` value, it will be ignored. + +#### Permissions and access in newly created collections + +By default, the newly created collection will have the following access settings: + +- Visibility: "public" +- Member visibility: "public" +- Member policy: "closed" +- Record policy: "closed" +- Review policy: "closed" + +They will appear in search results and be visible to non-members of the collection. But users who are not group members will not be able to request membership, and all submissions to the group will be held for review by the collection curators. + +The collection's administrators can change these settings in the collection's settings page. + +#### Handling group name changes + +Note that when a collection is created for a group, the collection's slug will be generated from the group's name. If the group's name is changed in the Commons instance, the collection's slug will not be automatically updated. This is to avoid breaking links to the collection. If the group's name is changed, the collection's slug will remain the same, but the collection's metadata will be updated to reflect the new group name. + +#### Handling collection name collisions + +It is possible for two groups on Commons instances to share the same human readable name, even though their ids are different. Knowledge Commons Works *will* allow multiple collections to share identical human readable names, but group url *slugs* must be unique across all KC Works collections. So where group names collide, only the first of the identically-named collections will have its slug generated normally. Susequent collections with the same name will have a numerical disambiguator appended to the end of their slugs. So if we have three groups named "Panda Studies," the first collection created for one of the groups will have the slug `panda-studies`. The other collections created by these groups will be assigned the slugs `panda-studies-1` and `panda-studies-2`, in order of their creation in Knowledge Commons Works. + +#### Handling deleted group collections + +If a group collection is deleted, its slug will be reserved in the Invenio PID store and cannot be re-used for a new collection. If a new collection is created for the same group, the slug will have a numerical disambiguator appended to the end, exactly as in cases of group name collision. E.g., if the group `panda-studies` were deleted earlier, a request to create a new collection for the "Panda Studies" group would be assigned the URL slug `panda-studies-1`. This is to avoid breaking links to the deleted collection. + +In future it may be possible to restore deleted collections, but this is not currently implemented. + + +#### Request + +```http +POST https://works.hcommons.org/api/group_collections HTTP/1.1 +``` + +Required request headers: + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Authorization | `Bearer ` | + +#### Request body + +The request body must be a JSON object with the following fields: + +| Field name | Required | Description | +| -----------|----------|-------------| +| `commons_instance` | Y | The name of the Commons instance to which the group belongs. This must be the same string used to identify the instance in the `GROUP_COLLECTIONS_METADATA_ENDPOINTS` config variable. | +| `commons_group_id` | Y | The ID of the Commons group that will own the collection. | +| `collection_visibility` | N | The visibility setting for the collection to be created. Must be either "public" or "restricted". [default: "restricted"]| + +The resulting request body will be shaped like this: + +```json +{ + "commons_instance": "knowledgeCommons", + "commons_group_id": "12345", + "collection_visibility": "public", +} +``` + + +#### Successful response status code + +`201 Created` + +#### Successful response body + +```json +{ + "commons_group_id": "12345", + "collection_slug": "new-collection-slug" +} +``` + +#### Unsuccessful response codes + +- 400 Bad Request: The request body is missing required fields or contains + invalid data. +- 404 Not Found: The specified group could not be found by the callback to the Commons instance. +- 403 Forbidden: The request is not authorized to modify the collection. +- 409 Conflict: A collection already exists in Knowledge Commons Works linked to the specified group. + +### Changing the Group Ownership of a Collection (PATCH) + +[!WARNING] +PATCH requests to change group ownership of the collection are not yet implemented. + +A PATCH request to this endpoint modifies an existing collection in Invenio by changing the Commons group to which it belongs. This is the *only* modification that can be made to a collection via this endpoint. Other modifications to Commons group metadata should be handled by signalling the Invenio webhook for commons group metadata updates. Modifications to internal metadata or settings for the Invenio collection should be made view the Invenio "communities" API or the collection settings UI. + +Note that the collection memberships in Invenio will be automatically transferred to the new Commons group. The corporate roles for the old Commons group will be removed from the collection and corporate roles for the new Commons group will be added to its membership with appropriate permissions. But any individual memberships that have been granted through the Invenio UI will be left unchanged. If the new collection administrators wish to change these individual memberships, they will need to do so through the Invenio UI. + +#### Request + +```http +PATCH https://works.hcommons.org/api/group_collections/my-collection-slug HTTP/1.1 +``` + +Required request headers: + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Authorization | `Bearer ` | + +#### Request body + +```json +{ + "commons_instance": "knowledgeCommons", + "old_commons_group_id": "12345", + "new_commons_group_id": "67890", + "new_commons_group_name": "My Group", + "collection_visibility": "public", +} +``` + +#### Successful response status code + +`200 OK` + +#### Successful response body + +```json +{ + "collection": "my-collection-slug" + "old_commons_group_id": "12345", + "new_commons_group_id": "67890", +} +``` + +#### Unsuccessful response codes + +- 400 Bad Request: The request body is missing required fields or contains + invalid data. +- 404 Not Found: The collection does not exist. +- 403 Forbidden: The request is not authorized to modify the collection. +- 304 Not Modified: The collection is already owned by the specified + Commons group. + +### Deleting a Group's Collection (DELETE) + +A DELETE request to this endpoint deletes a collection in Invenio owned by the specified Commons group. Note that the request must include all of: + +- the collection slug as the url path parameter +- the identifier of the Commons instance to which the group belongs, in the `commons_instance` query parameter +- the Commons identifier of the group which owns the collection, in the `commons_group_id` query parameter + +If any of these is missing the request will fail with a `400 Bad Request` error. This is to ensure that collections are not deleted accidentally or by agents without authorization. + +If the collection is successfully deleted, the response status code will be 204 No Content. + +[!NOTE] +Once a group collection has been deleted, its former URL slug is still registered in Invenio's PID store and reserved for the (now deleted) collection. Subsequent requests to create a collection for the same group cannot re-use the same URL slug. Instead the new slug will have a numerical disambiguator added to the end, exactly as in cases of group name collision. E.g., if the group `panda-studies` were deleted earlier, a request to create a new collection for the "Panda Studies" group would be assigned the URL slug `panda-studies-1`. + +[!NOTE] +Group collections are soft deleted and can in principle be restored within a short period after the delete signal has been sent. Eventually, though, the soft deleted collection records will be +automatically purged entirely from the database. There is also no API mechanism for restoring them. So delete operations should be regarded as permanent and irrevocable. + +#### Request + +```http +DELETE https://works.hcommons.org/api/group_collections/my-collection-slug?commons_instance=knowledgeCommons&commons_group_id=12345 HTTP/1.1 +``` + +Required request headers: + +| Header name | Header value | +| ------------|-------------- | +| Content-Type | `application/json` | +| Authorization | `Bearer ` | + +#### Successful response status code + +`204 No Content` + +#### Unsuccessful response codes + +- 400 Bad Request: The request did not include the required parameters or the parameters are not well formed. +- 403 Forbidden: The requesting agent is not authorized to delete the collection. The collection may not belong to the Commons instance making the request, or it may not belong to the specified Commons group. +- 404 Not Found: The collection does not exist. +- 422 UnprocessableEntity: The deletion could not be performed because the + + +## User and Group Data Updates (Internal Only) + +``` +https://works.hcommons.org/api/webhooks/user_data_update +``` + +> [!WARNING] +> This API endpoint is intended for internal use only. It is not intended to be used by clients outside of the Knowledge Commons system. + +> [!NOTE] +> This API was implemented with a distributed network of independent Commons instances in mind. Currently, only the Knowledge Commons instance exists and is supported as a SAML IDP by KCWorks. + +The api endpoint `/api/webhooks/user_data_update` is provided for Knowledge Commons applications and instances to signal that user or group metadata has been changed. These endpoints do not receive the actual updated data. They only receive notices *that* the metadata for a user or group has changed. KCWorks will then query the Commons instance's endpoint to retrieve current metadata for the user or group. + +### User/Groups Metadata updates and SAML authentication + +It is assumed that Commons instances have registered a SAML authentication IDP with KCWorks. The Commons identifiers for users in metadata update signals must be the same identifiers provided by the instance's SAML IDP. This allows KCWorks to reliably identify the correct KCWorks user account, even if the same identifier happens to be used internally by multiple Commons instances. It also allows KCWorks to store Commons instance user ids in one central place within KCWorks, minimizing the chances of those links between a Commons instance user account and a KCWorks user account becoming corrupted. + +### GET requests + +A `GET` request to this endpoint can be used to check that the endpoint is available and receiving messages. The response should have a `200` status code and should carry the following JSON response body: + +```json +{ + "message": "Webhook receiver is active", + "status": 200, +} +``` + +### POST requests +#### Payload objects + +Update notices should be sent via a `POST` request with a JSON payload object shaped like this: + +```json +{ + "idp": "knowledgeCommons", + "updates": { + "users": [ + {"id": "myusername", "event": "updated"}, + {"id": "anotherusername", "event": "created"}, + ], + "groups": [{"id": "1234", "event": "updated"}], + }, +}, +``` + +Top level payload object properties: + +| Property | Type | Description | Required | +| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `idp` | string | The name used by KCWorks to identify the identity provider the Commons instance has registered with KCWorks. This id should have been provided by the KCWorks administrators when the Commons instance's IDP connection was established. For Knowledge Commons the value is `knowledgeCommons` | Y | +| `updates` | object | This object identifies the metadata updates that have taken place on the Commons instance. It allows updates of different kinds and for multiple entities to be signalled in a single request. Its properties are described below. | Y | + +`updates` object properties: + +| Property | Type | Description | Required | +| -------- | ----- | ----------------------------------------------------------------------------------- | -------- | +| `users` | array | An array of objects each representing one metadata change event for a single user. | N | +| `groups` | array | An array of objects each representing one metadata change for a single group. | N | + +NOTE: A valid payload *must* provide either a `users` array or a `groups` array with at least one member. Requests providing neither `users` nor `groups`, or providing only empty arrays, will result in an error response. + +`users` and `groups` object properties + +| Property | Type | Description | Required | +| -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| `id` | number | The local identifier of the user or group on the Commons instance. This must be the same identifier that can be used to retrieve the entity's metadata at the corresponding endpoint on the Commons instance. | Y | +| `event` | string | The nature of the metadata change for the entity. Must be one of `updated`, `created`, or `deleted`. The `updated` and `deleted` event types should be sent when an entity is first created or is deleted entirely from the Commons instance. These will trigger the creation or deletion of corresponding entities (a user or a group) on KC Works. All other metadata changes are `updated` events. | Y | + +NOTE: A valid payload's user and/or group objects must each include *both* an `id` *and* an `event` value. + +#### Event timing + +There may be some delay between KC Works' receiving an update signal and the updating of the corresponding entity's metadata in KC Works. The actual updates are handled by background workers and in some cases there may be a slight delay before a worker is free. Usually this will only be a fraction of a second, but if intensive background tasks (like indexing) are ongoing it could be several minutes. The update also depends on a successful callback request from KC Works to the Commons instance's endpoint for serving user or group metadata. If that request fails, it is possible for an update to fail even though the webhook signal was received successfully. + +#### Success responses + +If a signal is received successfully, the response will have a status of `202` and carry a JSON response object shaped like this: + +```json +{ + "message": "Webhook received", + "status": 202, + "updates": { + "users": [ + {"id": "myusername", "event": "updated"}, + {"id": "anotherusername", "event": "created"}, + ], + "groups": [{"id": "1234", "event": "updated"}], + } +} +``` + +The `updates` object should be identical to the `updates` object provided in the `POST` request. This confirms that the correct events have all been received and are being sent for processing. + +#### Error responses + +If multiple update signals are received in one `POST` request, it is possible that only some of the updates can be processed. The request might, for example, provide `updated` event signals for a number of entities, some of whose ids do not exist in KC Works. In this case the response code will be `207 Multi-Status` and the response payload will be a JSON object + + diff --git a/docs/source/architecture.md b/docs/source/architecture.md new file mode 100644 index 000000000..d39a593ff --- /dev/null +++ b/docs/source/architecture.md @@ -0,0 +1,348 @@ +# KCWorks Architecture + +## InvenioRDM's Layered Architecture + +InvenioRDM employs a layered architecture with: + +1. Data layer + - Low-level data storage and retrieval. + - Primarily SQLAlchemy model classes. + - High-level data API classes that provide a Pythonic interface to the data layer. + - Validate data before storing it. +2. Service layer + - Retrieves and modifies data from the data layer, either for a view or for another service. + - Providing abstract CRUD methods for operating on the data layer's API classes. + - Providing abstracted "result items" and "result lists" + - Enforces permission and access control policies. +3. View layer + - Consists of + - Flask views (registered as Blueprints) + - rendering either + - Jinja2 templates to produce HTML + - JSON to produce API responses + - in some cases, React components embedded in the Jinja2 templates + - These are rendered on the client side + - Data is passed from the Jinja2 templates to the React components via HTML data attributes + +## InvenioRDM Services + +An InvenioRDM service is a class that provides methods for interacting with the data layer. The business logic of the service is usually delegated to one or more component classes, which are called during the service's methods. + +### Service Classes + +#### BaseService + +The base Service class is defined in `invenio_records_resources.services.base.Service`. It defines methods for: + +- Getting the service ID + - `id(self)`: Return the id of the service from config. +- Permissions checking + - `permission_policy(self, action_name, **kwargs)`: Factory for a permission policy instance. + - `check_permission(self, identity, action_name, **kwargs)`: Check a permission against the identity. + - `require_permission(self, identity, action_name, **kwargs)`: Require a specific permission from the permission policy. +- Handling service components + - `components(self)`: Return initialized instances of the service's component classes. + - `run_components(self, action, *args, **kwargs)`: Run components for a given action. +- Producing result items and lists + - `result_item(self, *args, **kwargs)`: Create a new instance of the resource unit, i.e. whatever the service provides. + - `result_list(self, *args, **kwargs)`: Create a new list of resource units. In some cases this is a simple iterable of resource units, but in other cases it is a more complex object that includes additional data. + +#### RecordService + +Services dealing with InvenioRDM records of some kind (e.g. records, drafts, communities, etc.) inherit from the `RecordService` class defined in `invenio_records_resources.services.records.service`. This class adds: + +- properties and methods related to the service's related data-layer API class + - A `schema` property that returns a `ServiceSchemaWrapper` instance. + - A `record_cls` property that returns the record class for the service. + - A `links_item_tpl` property that returns a `LinksTemplate` instance for constructing links to a resource unit. + - An `expandable_fields` property that returns a list of expandable fields for the service's data-layer API class. +- Methods for creating searches + - `create_search(self, identity, record_cls, search_opts, permission_action="read", preference=None, extra_filter=None, versioning=True)`: Instantiate a search class. + - `search_records(self, identity, params, **kwargs)`: A low-level method to create an OpenSearch DSL instance for searching records. + - `search(self, identity, params=None, search_preference=None, expand=False, **kwargs)`: A high-level method to search for records matching the querystring. + - `scan(self, identity, params=None, search_preference=None, expand=False, **kwargs)`: A high-level method to perform a rolling "scroll" search for records matching the querystring. (This is used for searching through large numbers of records, since OpenSearch will not return more than 10,000 records at a time.) +- Methods for indexing records + - `reindex(self, identity, params=None, search_preference=None, search_query=None, extra_filter=None, **kwargs)`: A high-level method to reindex records matching the query parameters. + - `rebuild_index(self, identity, uow=None)`: A high-level method to reindex all records managed by this service. +- CRUD methods + - `create(self, identity, data, uow=None, expand=False)`: Create a record. + - `exists(self, identity, id_)`: Check if the record exists and user has permission. (Does *not* use the search index.) + - `read(self, identity, id_, expand=False, action="read")`: Retrieve a record. (Does *not* use the search index.) + - `read_many(self, identity, ids, expand=False, action="read")`: Retrieve multiple records using the search index. + - `read_all(self, identity, params=None, search_preference=None, expand=False, **kwargs)`: Retrieve all records matching the query parameters using the search index. + - `update(self, identity, id_, data, uow=None, expand=False)`: Update a record. + - `delete(self, identity, id_, uow=None)`: Delete a record. +- Helper methods for record management + - `check_revision_id(self, record, expected_revision_id)`: Validate the given revision_id with current record's one. + - `on_relation_update(self, identity, record_type, records_info, notif_time, limit=100)`: Handles the update of a related field record when the related field is updated. + +#### Augmented RecordService + +The `invenio_drafts_resources` package then overrides this with a `RecordService` class that adds (a) a distinction between published and draft records, (b) record versioning and a parent-child record relationship, and (c) file attachments to service records. This adds the following properties and methods to the `RecordService` class: + +- Properties and methods for draft records + - `draft_cls(self)`: Return the record class for the service. + - `draft_files(self)`: Return the draft files service for the service. + - `draft_indexer(self)`: A factory for creating an indexer instance. + - `search_drafts(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs)`: Search for draft records matching the querystring. + - `read_draft(self, identity, id_, expand=False)`: Retrieve a draft record. + - `update_draft(self, identity, id_, data, revision_id=None, uow=None, expand=False)`: Replace a draft. + - `edit(self, identity, id_, uow=None, expand=False)`: Creates a new revision of a draft or a draft for an existing published record. + - `publish(self, identity, id_, uow=None, expand=False)`: Publishes a draft record. + - `delete_draft(self, identity, id_, revision_id=None, uow=None)`: Deletes a draft record. (Defaults to a soft delete, so the record is not actually deleted from the database or search index until a later cleanup operation.) + - `validate_draft(self, identity, id_, ignore_field_permissions=False)`: Validate a draft. + - `cleanup_drafts(self, timedelta, uow=None, search_gc_deletes=60)`: Hard delete of soft deleted drafts. +- Properties and methods for files + - `files(self)`: Return the files service for the service. + - `import_files(self, identity, id_, uow=None)`: Import files from previous record version. +- Properties and methods for versions and parent records + - `schema_parent(self)`: Return the parent schema for the service. + - `search_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read", **kwargs)`: Search for record's versions. + - `read_latest(self, identity, id_, expand=False)`: Retrieve the latest version of a record. + - `new_version(self, identity, id_, uow=None, expand=False)`: Creates a new version of a record. +This overridden `RecordService` class also modifies the CRUD methods to enforce a workflow in which records are only modified via their draft records. This involves overriding: + +- `update(self, identity, id_, data, uow=None, expand=False)`: Now raises a `NotImplementedError` error. +- `create(self, identity, data, uow=None, expand=False)`: Now creates a draft record. +- `rebuild_index(self, identity)`: Now reindexes all draft records (instances of draft API class) as well as all published records (instances of record API class) and skips soft-deleted records. + +#### RDMRecordService + +The `invenio_rdm_records` package provides an `RDMRecordService` class that inherits from the `RecordService` class and adds: + +- Additional properties for accessing subservices + - `access`: Return the access service for the service. + - `pids`: Return the PIDs service for the service. + - `review`: Return the review service for the service. +- Methods for embargo handling + - `lift_embargo(self, identity, _id, uow=None)`: Lifts an embargo from the record and draft (if exists). + - `scan_expired_embargos(self, identity)`: Scan for records with an expired embargo. +- Properties and methods for file quota handling + - `schema_quota`: Return the schema for quota information. + - `set_quota(self, identity, id_, data, files_attr="files", uow=None)`: Set the quota values for a record. + - `set_user_quota(self, identity, id_, data, uow=None)`: Set the user files quota. +- Properties and methods for deletion of published records + - `schema_tombstone`: Return the schema for tombstone information. + - `delete_record(self, identity, id_, data, expand=False, uow=None, revision_id=None)`: Re-introduces soft-deletion of published records (which were previously removed by the `RecordService` class). + - `update_tombstone(self, identity, id_, data, expand=False, uow=None)`: Update the tombstone information for the (soft) deleted record. + - `cleanup_record(self, identity, id_, uow=None)`: Clean up a (soft) deleted record. + - `restore_record(self, identity, id_, expand=False, uow=None)`: Restore a record that has been (soft) deleted. + - `mark_record_for_purge(self, identity, id_, expand=False, uow=None)`: Mark a (soft) deleted record for purge. + - `unmark_record_for_purge(self, identity, id_, expand=False, uow=None)`: Remove the mark for deletion from a record, returning it to deleted state. + - `purge_record(self, identity, id_, uow=None)`: Purge a record that has been marked. +- Overridden methods to add deletion-related functionality + - `read(self, identity, id_, expand=False, action="read", include_deleted=False)`: Adds an `include_deleted` argument to the read method, and a check for the `read_deleted` permission if it is set to `True`. + - `read_draft(self, identity, id_, expand=False)`: Prevents reading a draft if there is a published deleted record. (410 response.) + - `search(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs)`: Adds a "read_deleted" permission action to the search method. + - `search_drafts(self, identity, params=None, search_preference=None, expand=False, extra_filter=None, **kwargs)`: Adds a filter to exclude soft-deleted records from the search results. + - `search_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read", **kwargs)`: Adds a "read_deleted" permission action to the search method. +- Additional overridden methods for other functionality + - `publish(self, identity, id_, uow=None, expand=False)`: Adds a check prior to the original publish method to allow enforcement of a config setting that requires a community to be present on a record before it can be published. + - `update_draft(self, identity, id_, data, revision_id=None, uow=None, expand=False)`: Adds a check prior to the original update_draft method to allow enforcement of a config setting that prevents a record from being restricted after the grace period. +- Additional new methods for other functionality + - `expandable_fields`: Expands the `communities` field to return community details. + - `oai_result_item(self, identity, oai_record_source)`: Get a result item from a record source in the OAI server. + - `scan_versions(self, identity, id_, params=None, search_preference=None, expand=False, permission_action="read_deleted", **kwargs)`: Search for record's versions using a "scroll" search. + +### Service Configuration + +A service configuration is an object that provides the service with its configuration. It is passed to the service's constructor when it is instantiated during the Flask app initialization. + +The service configuration is defined in the service's `config` attribute. + +All service configurations inherit from the `ServiceConfig` class, which is defined in `invenio_records_resources.services.base.config`. They include at least: + +- `service_id`: The ID of the service. +- `permission_policy_cls`: The permission policy class to use for the service. +- `result_item_cls`: The result item class to use for the service. +- `result_list_cls`: The result list class to use for the service. + +This is expanded in a `RecordServiceConfig` class by the `invenio_records_resources` package to add: + +- `record_cls`: The record class to use for the service. +- `indexer_cls`: The indexer class to use for the service. +- `indexer_queue_name`: The name of the task queue to be used by the service's indexer. +- `index_dumper`: The dumper to be used for serializing records to be indexed by OpenSearch. +- `relations`: The inverse relation mapping for the service, defining which fields relate to which record type. +- `search`: The search configuration for the service. (This is a `SearchOptions` instance.) +- `schema`: The schema to be used when validating the service's records. +- `links_item`: The template for creating url links for the service's result items. +- `links_search`: The template for creating url links for the service's search endpoints. +- `components`: A list of components that will be used by the service. + +It is further expanded in an overridden `RecordServiceConfig` class by the `invenio_drafts_resources` package to add: + +- `draft_cls`: The draft record class to use for the service. +- `draft_indexer_cls`: The indexer class to use for the service's draft records. +- `draft_indexer_queue_name`: The name of the task queue to be used by the service's draft records indexer. +- `schema_parent`: The schema used to valid parent records for the service. +- `search_drafts`: A search class for searching for draft records. +- `search_versions`: A search class for searching for record versions. +- `default_files_enabled`: Whether files are enabled by default for the service. +- `default_media_files_enabled`: Whether media files are enabled by default for the service. +- `lock_edit_published_files`: Whether to lock editing of published files for the service. +- `links_search_drafts`: The template for creating url links for the service's search drafts endpoint. +- `links_search_versions`: The template for creating url links for the service's search versions endpoint. + +The `RDMRecordServiceConfig` class adds the following additional configuration attributes: + +- `max_files_count`: The maximum number of files that can be attached to a record. +- `file_links_list`: The list of file links for the service. +- `schema_access_settings`: The schema for access settings. +- `schema_secret_link`: The schema for secret links. +- `schema_grant`: The schema for grants. +- `schema_grants`: The schema for grants. +- `schema_request_access`: The schema for request access. +- `schema_tombstone`: The schema for tombstone. +- `schema_quota`: The schema for quota. + + +Additional common configration attributes are added by inheriting from additional mixin classes. + +#### Attaching configuration to the service + +The service config class can be passed to the service's constructor when it is instantiated during the Flask app initialization (i.e., in the `init_app()` method of the extension): + +```python +service = MyService(config=MyServiceConfig) +``` + +Alternatively, if the service config class inherits from the `ConfiguratorMixin` class, the service and its config class can be initialized like this: + +```python +service = MyService(MyServiceConfig.build(app)) +``` + +#### File service configuration + +The `FileConfigMixin` class (defined in `invenio_records_resources.services.records.components.files`) adds config class attributes for: ???? + +- `_files_attr_key`: The attribute key for the files field. +- `_files_data_key`: The attribute key for the files data. +- `_files_bucket_attr_key`: The attribute key for the files bucket. +- `_files_bucket_id_attr_key`: The attribute key for the files bucket ID. + +#### Search configuration + +##### SearchOptionsMixin + +This mixin class (defined in `invenio_records_resources.services.base.config`) adds config class attributes for: + +- `facets`: The search facet definitions for searches on the service's resource. +- `sort_options`: The sort options for searches on the service's resource. +- `sort_default`: The default sort option for searches on the service's resource. +- `sort_default_no_query`: The default sort option for searches on the service's resource when no query is present. +- `available_sort_options`: The available sort options for searches on the service's resource. +- `query_parser_cls`: The query parser class to use in constructing searches on the service's resource. + +##### SearchConfig + +The SearchConfig class (defined in `invenio_records_resources.services.base.config`) defines the search configuration that will be used to interface with OpenSearch. + +##### FromConfigSearchOptions + +The `FromConfigSearchOptions` class (defined in `invenio_records_resources.services.base.config`) is used to load search configuration from app config variables. In the service's config class, it is used like this: + +#### Loading configuration from app config variables + +The `FromConfig` class (defined in `invenio_records_resources.services.base.config`) is used to load configuration from app config variables. In the service's config class, it is used like this: + +```python +class MyServiceConfig(ServiceConfig): + foo = FromConfig("FOO", default=1) +``` + +In the app config, the config variable is defined like this: + +```python +FOO = 2 +``` + +When the service is instantiated, the `FromConfig` class will load the config variable from the app config and assign it to the `foo` attribute. + +### Service Components + +A service component is a class that provides methods that shadow the service's methods. When a service method is called, it passes the call through each of the service's components (using the `Service.run_components()` method), allowing each component to perform additional processing before the result is returned. If the service component includes a method with the same name as the service method that is being called, its matching method will be called. During this call, the component method is passed the service method's arguments and keyword arguments, and the service method's modified versions of these arguments are passed on to the next component. Once all the service's components have been called, the result is returned to the service method, which returns the final result or performs the final action. + +#### BaseServiceComponent + +The `BaseServiceComponent` class (defined in `invenio_records_resources.services.base.components`) is the base class for all service components. It provides a `uow` property that returns the Unit of Work manager. + +This class is overridden by the `ServiceComponent` class (defined in `invenio_records_resources.services.base.components.base`), which adds the following methods: + +- `create(self, identity, **kwargs)`: Perform additional processing while creating an item of the service's resource. +- `read(self, identity, **kwargs)`: Perform additional processing while retrieving an item of the service's resource. +- `update(self, identity, **kwargs)`: Perform additional processing while updating an item of the service's resource. +- `delete(self, identity, **kwargs)`: Perform additional processing while deleting an item of the service's resource. +- `search(self, identity, search, params, **kwargs)`: Perform additional processing while searching for items of the service's resource. + +The `invenio_drafts_resources` package overrides the `ServiceComponent` class to add methods matching the overridden RecordService methods for draft records and versioning. + +- `read_draft(self, identity, draft=None)`: Retrieve a draft record. +- `update_draft(self, identity, data=None, record=None, errors=None)`: Update a draft record. +- `delete_draft(self, identity, draft=None, record=None, force=False)`: Delete a draft record. +- `edit(self, identity, draft=None, record=None)`: Edit a record. +- `new_version(self, identity, draft=None, record=None)`: Create a new version of a record. +- `publish(self, identity, draft=None, record=None)`: Publish a draft record. +- `import_files(self, identity, draft=None, record=None)`: Import files from previous record version. +- `post_publish(self, identity, record=None, is_published=False)`: Post publish handler. + +#### RecordService Components + +The `invenio_records_resources` package provides the following components for the `RecordService` class: + +- `DataServiceComponent` (create, update): Adds data to the record. +- `BaseRecordFilesComponent` (create, update): + - Handles enabling/disabling files for a record. + - Handles setting the default preview file for a record. +- `MetadataComponent` (create, update): Adds metadata to the new/updated record from the input data. +- `RelationsComponent` (read): Dereferences a record's related fields in order to provide the data from the related records in a read result. +- `ChangeNotificationsComponent` (update): Emits a change notification for the updated record. + +The `invenio_drafts_resources` package provides additional components for the `RecordService` class: + +- an overridden `BaseRecordFilesComponent` class that adds methods for ??? +- `DraftFilesComponent`: Handles files for draft records. +- `DraftMediaFilesComponent`: Handles media files for draft records. +- `DraftMetadataComponent`: Handles metadata for draft records. +- `PIDComponent` (create, delete_draft): Handles registration of PIDs for draft records. +- an overridden `RelationsComponent` class that adds a `read_drafts` method + +The `invenio_rdm_records` package provides additional components for the `RDMRecordService` class: + +- `AccessComponent`(create, update_draft, publish, edit, new_version): Handles access settings for records. +- an overridden `MetadataComponent` class (create, update_draft, publish, edit, new_version): Adds metadata to the new/updated record from the input data. (Removes the `update` method from the earlier `MetadataComponent` class.) +- `CustomFieldsComponent`(create, update_draft, publish, edit, new_version): Adds custom fields to the metadata of a record. +- `PIDsComponent`(create, update_draft, delete_draft, publish, edit, new_version, delete_record, restore_record): Handles PIDs for records. +- `ParentPIDsComponent`(create, publish, delete_record, restore_record): Handles parent PIDs for records. +- `RecordDeletionComponent`(delete_record, update_tombstone, restore_record, mark_record, unmark_record, purge_record): Handles deletion of records. +- `RecordFilesProcessorComponent`(publish, lift_embargo): Handles file processing for records. +- `ReviewComponent`(create, delete_draft, publish): Handles reviews for records. +- `SignalComponent`(publish): Triggers signals on publish. +- `ContentModerationComponent`(publish): Creates a moderation request if the user is not verified. + +#### RDMRecordService Components + +The `invenio_rdm_records` package draws its list of components from the `RDM_RECORDS_SERVICE_COMPONENTS` config variable. The default list is defined in the `DefaultRecordsComponents` class (defined in `invenio_rdm_records.services.config`) and currently includes:. + +```python +[ + MetadataComponent, + CustomFieldsComponent, + AccessComponent, + DraftFilesComponent, + DraftMediaFilesComponent, + RecordFilesProcessorComponent, + RecordDeletionComponent, + # for the internal `pid` field + PIDComponent, + # for the `pids` field (external PIDs) + PIDsComponent, + ParentPIDsComponent, + RelationsComponent, + ReviewComponent, + ContentModerationComponent, +] +``` + +Note that the order of the components in the list is important, since the components are called in the order they are listed and some components depend on the results of previous components. \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index bed1354ca..e03b8c1c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,6 +20,7 @@ exclude_patterns = [] autosectionlabel_prefix_document = True +autosectionlabel_maxdepth = 4 # -- Options for HTML output ------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 5df881c7c..4d71aac95 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,9 +16,11 @@ Welcome to the Knowledge Commons Works technical documentation! metadata customizations configuration + api cli_commands infrastructure developing + architecture in_depth reference diff --git a/docs/source/metadata.md b/docs/source/metadata.md index a8d671d88..088f2e8e6 100644 --- a/docs/source/metadata.md +++ b/docs/source/metadata.md @@ -1,3 +1,4 @@ +(metadata-schema-vocabularies-and-identifiers)= # Metadata Schema, Vocabularies, and Identifiers The default metadata schema for InvenioRDM records is defined in the `invenio-rdm-records` package and documented [here](https://inveniordm.docs.cern.ch/reference/metadata/). It also includes a number of optional metadata fields which have been enabled in KCWorks, documented [here](https://inveniordm.docs.cern.ch/reference/metadata/optional_metadata/). @@ -260,11 +261,11 @@ The JSON object retrieved from the record API shares the same basic structure as #### FAST -The FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) is used for the `subjects` field. See the [metadata.subjects](#metadata.subjects) section for more information about how to include FAST subjects in a KCWorks record. +The FAST controlled vocabulary (https://www.oclc.org/research/areas/data-science/fast.html) is used for the `subjects` field. See the [metadata.subjects](#metadata-subjects) section for more information about how to include FAST subjects in a KCWorks record. #### Homosaurus -The FAST vocabulary is augmented in KCWorks by the Homosaurus vocabulary (https://homosaurus.org/) for subjects related to sexuality and gender identity. See the [metadata.subjects](#metadata.subjects) section for information about how to include Homosaurus subjects in a KCWorks record. +The FAST vocabulary is augmented in KCWorks by the Homosaurus vocabulary (https://homosaurus.org/) for subjects related to sexuality and gender identity. See the [metadata.subjects](#metadata-subjects) section for information about how to include Homosaurus subjects in a KCWorks record. ## Resource types @@ -481,13 +482,13 @@ An ISBN (International Standard Book Number) is a ten (pre-2007) or 13 digit (20 KCWorks (and InvenioRDM) supports the ORCID identifier scheme. The ORCID of the submitter of the KCWorks record is stored in the `person_or_org.identifiers` property of the `creators` array (`creators[0].person_or_org.identifiers.identifier`). A KCWorks user's ORCID id is also drawn from their KC profile (if they have provided one) and stored in their system user profile (as `.user_profile.identifier_orcid`). -For details on how to use ORCID identifiers in KCWorks, see the section on [Metadata.creators](#metadata.creators) below. +For details on how to use ORCID identifiers in KCWorks, see the section on [Metadata.creators](#metadata-creators-metadata-contributors) below. #### KC Username (recommended) KCWorks also allows the use of Knowledge Commons usernames as identifiers. The KC username of the submitter of the KCWorks record is stored in the `person_or_org.identifiers` property of the `creators` array (`creators[0].person_or_org.identifiers.identifier`) using the scheme `kc_username`. -For details on how to use KC usernames in KCWorks, see the section on [Metadata.creators](#metadata.creators) below. +For details on how to use KC usernames in KCWorks, see the section on [Metadata.creators](#metadata-creators-metadata-contributors) below. #### GND diff --git a/invenio.cfg b/invenio.cfg index 1be1a872a..1a738d7d9 100644 --- a/invenio.cfg +++ b/invenio.cfg @@ -300,6 +300,8 @@ RATELIMIT_AUTHENTICATED_USER = "50000 per hour;1000 per minute" RATELIMIT_GUEST_USER = "10000 per hour;200 per minute" RATELIMIT_ENABLED = True if os.getenv("INVENIO_RATELIMIT_ENABLED") == "True" else False +MAX_CONTENT_LENGTH = (10**10) * 50 # 500 GB + # Deposit form file quota FILES_REST_DEFAULT_QUOTA_SIZE = (10**10) * 50 # 500 GB APP_RDM_DEPOSIT_FORM_QUOTA = { diff --git a/site/kcworks/dependencies/invenio-record-importer-kcworks b/site/kcworks/dependencies/invenio-record-importer-kcworks index d4de88408..4fa418ba7 160000 --- a/site/kcworks/dependencies/invenio-record-importer-kcworks +++ b/site/kcworks/dependencies/invenio-record-importer-kcworks @@ -1 +1 @@ -Subproject commit d4de884081bd6155335cfe3b7e6af3e06e148547 +Subproject commit 4fa418ba7d801928c63f9f9670bc9013ed7ad074 diff --git a/site/kcworks/dependencies/invenio-remote-user-data-kcworks b/site/kcworks/dependencies/invenio-remote-user-data-kcworks index 4eef862a8..0aeda19ec 160000 --- a/site/kcworks/dependencies/invenio-remote-user-data-kcworks +++ b/site/kcworks/dependencies/invenio-remote-user-data-kcworks @@ -1 +1 @@ -Subproject commit 4eef862a88edf36276d76f7890c70681b412e7c6 +Subproject commit 0aeda19ec81c45a996383449827de52865014715 diff --git a/site/pyproject.toml b/site/pyproject.toml index 6b4de55d1..0b8ba3e2a 100644 --- a/site/pyproject.toml +++ b/site/pyproject.toml @@ -67,7 +67,6 @@ plugins = [ "tests.fixtures.vocabularies.resource_types", "tests.fixtures.vocabularies.roles", "tests.fixtures.vocabularies.subjects", - "tests.helpers.sample_records.basic", ] [tool.setuptools.package-data] diff --git a/site/run_tests.sh b/site/run_tests.sh index 97d755688..2ecfeb6a3 100644 --- a/site/run_tests.sh +++ b/site/run_tests.sh @@ -53,10 +53,10 @@ fi eval "$(PIPENV_DOTENV_LOCATION=/Users/ianscott/Development/knowledge-commons-works/site/tests/.env pipenv run docker-services-cli up --db ${DB:-postgresql} --cache ${CACHE:-redis} --search opensearch --mq ${MQ:-rabbitmq} --env)" # Note: expansion of pytest_args looks like below to not cause an unbound # variable error when 1) "nounset" and 2) the array is empty. -if [ -z "pytest_args[@]" ]; then - PIPENV_DOTENV_LOCATION=/Users/ianscott/Development/knowledge-commons-works/site/tests/.env pipenv run python -m pytest ${pytest_args[@]} -else +if [ ${#pytest_args[@]} -eq 0 ]; then PIPENV_DOTENV_LOCATION=/Users/ianscott/Development/knowledge-commons-works/site/tests/.env pipenv run python -m pytest -vv +else + PIPENV_DOTENV_LOCATION=/Users/ianscott/Development/knowledge-commons-works/site/tests/.env pipenv run python -m pytest ${pytest_args[@]} fi # python -m sphinx.cmd.build -qnN -b doctest docs docs/_build/doctest tests_exit_code=$? diff --git a/site/tests/api/conftest.py b/site/tests/api/conftest.py index 3455b3dbc..4be031308 100644 --- a/site/tests/api/conftest.py +++ b/site/tests/api/conftest.py @@ -3,12 +3,12 @@ # from pprint import pprint import pytest -# from invenio_app.factory import create_api +from invenio_app.factory import create_api # from pytest_invenio.fixtures import UserFixture, database, db -# @pytest.fixture(scope='module') +# @pytest.fixture(scope="module") # def create_app(): # return create_api @@ -18,7 +18,7 @@ def headers(): """Default headers for making requests.""" return { "content-type": "application/json", - "accept": "application/vnd.inveniordm.v1+json", + # "accept": "application/vnd.inveniordm.v1+json", } diff --git a/site/tests/api/test_api_import.py b/site/tests/api/test_api_import.py new file mode 100644 index 000000000..25bc3ecc6 --- /dev/null +++ b/site/tests/api/test_api_import.py @@ -0,0 +1,77 @@ +from invenio_access.permissions import authenticated_user +from invenio_access.utils import get_identity +import json +from pathlib import Path +from pprint import pformat + + +def test_import_records( + running_app, + db, + client_with_login, + minimal_community_factory, + user_factory, + minimal_record_metadata, + search_clear, + mock_send_remote_api_update_fixture, +): + app = running_app.app + community = minimal_community_factory() + u = user_factory(email="test@example.com", token=True, saml_id=None) + token = u.allowed_token + identity = get_identity(u.user) + identity.provides.add(authenticated_user) + + file_path = ( + Path(__file__).parent.parent.parent / "tests/helpers/sample_files/sample.pdf" + ) + file_list = [{"key": "sample.pdf"}] + minimal_record_metadata["files"] = {"enabled": True, "entries": file_list} + + with app.test_client() as client: + with open( + file_path, + "rb", + ) as binary_file_data: + binary_file_data.seek(0) + response = client.post( + f"{app.config['SITE_API_URL']}/import/{community.to_dict()['slug']}", + content_type="multipart/form-data", + data={ + "metadata": json.dumps(minimal_record_metadata), + "review_required": "true", + "strict_validation": "true", + "all_or_none": "true", + "files": [ + ( + file_path, + "sample.pdf", + "application/pdf", + ) + ], + }, + headers={ + "Content-Type": "multipart/form-data", + "Authorization": f"Bearer {token}", + }, + ) + print(response.text) + assert response.status_code == 201 + assert response.json == {"status": "success", "data": []} + + +# import requests + +# url = "https://works.hcommons.org/api/import" + +# payload = {'collection': 'mlacommons'} +# files=[ +# ('file1',('Test.pdf',open('/Users/ianscott/Downloads/Test.pdf','rb'),'application/pdf')) +# ] +# headers = { +# 'Cookie': 'SimpleSAMLCommons=41b2316ef1cefa7c21fa257f50b95b1b' +# } + +# response = requests.request("POST", url, headers=headers, data=payload, files=files) + +# print(response.text) diff --git a/site/tests/api/test_api_notifications.py b/site/tests/api/test_api_notifications.py index d872b8798..56d352d25 100644 --- a/site/tests/api/test_api_notifications.py +++ b/site/tests/api/test_api_notifications.py @@ -29,11 +29,10 @@ def test_notify_for_request_acceptance( running_app, - appctx, db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -58,7 +57,9 @@ def test_notify_for_request_acceptance( """ app = running_app.app admin_id = admin.user.id - community = minimal_community_factory(owner=admin_id) + community_rec = minimal_community_factory(owner=admin_id) + community_meta = community_rec.to_dict() + app.logger.debug(f"community_meta: {pformat(community_meta)}") assert len(mailbox) == 0 # Create a user with a community submission @@ -81,7 +82,7 @@ def test_notify_for_request_acceptance( logged_in_client = client response = logged_in_client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) assert response.status_code == 201 @@ -103,12 +104,12 @@ def test_notify_for_request_acceptance( # create a review request (can't use REST API for this) review_body = { - "receiver": {"community": f"{community['id']}"}, + "receiver": {"community": f"{community_meta['id']}"}, "type": "community-submission", } response = logged_in_client.put( - f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/" "review", + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/review", data=json.dumps(review_body), headers={**headers, "Authorization": f"Bearer {token}"}, ) @@ -129,7 +130,7 @@ def test_notify_for_request_acceptance( # not asserting it assert request_data["status"] == "created" # assert request_data["updated"] == request_created_date # not exact - assert request_data["receiver"] == {"community": f"{community['id']}"} + assert request_data["receiver"] == {"community": f"{community_meta['id']}"} assert request_data["revision_id"] == 2 request_id = request_data.get("id") @@ -183,7 +184,7 @@ def test_notify_for_request_acceptance( load_community_needs(reviewer_identity) # since we didn't log in review_accepted = current_requests_service.execute_action( reviewer_identity, - submitted_request.id, + request_id, "accept", data=accept_body, ) @@ -243,7 +244,7 @@ def test_notify_for_request_decline( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -269,7 +270,8 @@ def test_notify_for_request_decline( app = running_app.app admin_id = admin.user.id - community = minimal_community_factory(owner=admin_id) + community_rec = minimal_community_factory(owner=admin_id) + community_meta = community_rec.to_dict() assert len(mailbox) == 0 # Create a user with a community submission @@ -292,7 +294,7 @@ def test_notify_for_request_decline( logged_in_client = client response = logged_in_client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) assert response.status_code == 201 @@ -313,7 +315,7 @@ def test_notify_for_request_decline( # create a review request (can't use REST API for this) review_body = { - "receiver": {"community": f"{community['id']}"}, + "receiver": {"community": f"{community_meta['id']}"}, "type": "community-submission", } @@ -339,7 +341,7 @@ def test_notify_for_request_decline( # not asserting it assert request_data["status"] == "created" # assert request_data["updated"] == request_created_date # not exact - assert request_data["receiver"] == {"community": f"{community['id']}"} + assert request_data["receiver"] == {"community": f"{community_meta['id']}"} assert request_data["revision_id"] == 2 request_id = request_data.get("id") @@ -453,7 +455,7 @@ def test_notify_for_request_cancellation( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -468,7 +470,8 @@ def test_notify_for_request_cancellation( """ app = running_app.app admin_id = admin.user.id - community = minimal_community_factory(owner=admin_id) + community_rec = minimal_community_factory(owner=admin_id) + community_meta = community_rec.to_dict() assert len(mailbox) == 0 # Create a user with a community submission @@ -491,7 +494,7 @@ def test_notify_for_request_cancellation( logged_in_client = client response = logged_in_client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) assert response.status_code == 201 @@ -512,7 +515,7 @@ def test_notify_for_request_cancellation( # create a review request (can't use REST API for this) review_body = { - "receiver": {"community": f"{community['id']}"}, + "receiver": {"community": f"{community_meta['id']}"}, "type": "community-submission", } @@ -538,7 +541,7 @@ def test_notify_for_request_cancellation( # not asserting it assert request_data["status"] == "created" # assert request_data["updated"] == request_created_date # not exact - assert request_data["receiver"] == {"community": f"{community['id']}"} + assert request_data["receiver"] == {"community": f"{community_meta['id']}"} assert request_data["revision_id"] == 2 request_id = request_data.get("id") @@ -631,7 +634,7 @@ def test_notify_for_new_request_comment( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -646,7 +649,8 @@ def test_notify_for_new_request_comment( """ app = running_app.app admin_id = admin.user.id - community = minimal_community_factory(owner=admin_id) + community_rec = minimal_community_factory(owner=admin_id) + community_meta = community_rec.to_dict() assert len(mailbox) == 0 # Create a user with a community submission @@ -666,7 +670,7 @@ def test_notify_for_new_request_comment( with app.test_client() as client: response = client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) assert response.status_code == 201 @@ -687,7 +691,7 @@ def test_notify_for_new_request_comment( # create a review request (can't use REST API for this) review_body = { - "receiver": {"community": f"{community['id']}"}, + "receiver": {"community": f"{community_meta['id']}"}, "type": "community-submission", } @@ -713,7 +717,7 @@ def test_notify_for_new_request_comment( # not asserting it assert request_data["status"] == "created" # assert request_data["updated"] == request_created_date # not exact - assert request_data["receiver"] == {"community": f"{community['id']}"} + assert request_data["receiver"] == {"community": f"{community_meta['id']}"} assert request_data["revision_id"] == 2 request_id = request_data.get("id") @@ -830,7 +834,7 @@ def test_read_unread_notifications_by_service( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -908,7 +912,7 @@ def test_clear_unread_notifications_by_service( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -1123,7 +1127,7 @@ def test_clear_unread_notifications_by_view( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -1231,7 +1235,7 @@ def test_clear_one_unread_notification_by_view( db, user_factory, minimal_community_factory, - minimal_record, + minimal_record_metadata, client, client_with_login, headers, @@ -1394,7 +1398,7 @@ def test_unread_endpoint_bad_methods( def test_notification_on_first_upload( running_app, user_factory, - minimal_record, + minimal_record_metadata, db, search_clear, client, @@ -1453,14 +1457,14 @@ def test_notification_on_first_upload( login_user_via_session(client, email=user.email) # Create the first draft - response = client.post( + draft1_response = client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) - assert response.status_code == 201 - first_draft_id = response.json.get("id") - record_title = response.json.get("metadata").get("title") + assert draft1_response.status_code == 201 + first_draft_id = draft1_response.json.get("id") + record_title = draft1_response.json.get("metadata").get("title") # Check that an email was sent to the admin assert len(mailbox) == 1 @@ -1480,9 +1484,9 @@ def test_notification_on_first_upload( f"'{app.config.get('SITE_UI_URL')}/records/{first_draft_id}'>" f"View draft)" in email.html ) - assert f"Draft title: {minimal_record['metadata']['title']}" in email.body - assert f"Draft title: {minimal_record['metadata']['title']}" in email.html - # assert f"Full metadata: {minimal_record}" in email.body + assert f"Draft title: {minimal_record_metadata['metadata']['title']}" in email.body + assert f"Draft title: {minimal_record_metadata['metadata']['title']}" in email.html + # assert f"Full metadata: {minimal_record_metadata}" in email.body assert f"User ID: {user_id}" in email.body assert f"User ID: {user_id}" in email.html assert f"User email: {user_email}" in email.body @@ -1492,24 +1496,24 @@ def test_notification_on_first_upload( assert "A new user has created their first draft." in email.body assert "A new user has created their first draft." in email.html - # Create a second draft - response = client.post( + # Create a second draft work (different work) + draft2_response = client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) - assert response.status_code == 201 + assert draft2_response.status_code == 201 # Check that no new notification was created for the admin assert len(mailbox) == 1 # Publish the first draft - response = client.post( + draft1_publish_response = client.post( f"{app.config['SITE_API_URL']}/records/{first_draft_id}/draft/" "actions/publish", headers={**headers, "Authorization": f"Bearer {token}"}, ) - assert response.status_code == 202 + assert draft1_publish_response.status_code == 202 # Check that a new notification was created for the admin assert len(mailbox) == 2 @@ -1520,10 +1524,11 @@ def test_notification_on_first_upload( f"a work: '{record_title}'" in email.subject ) assert email.sender == app.config["MAIL_DEFAULT_SENDER"] + app.logger.debug(f"email.body: {pformat(email.body)}") assert f"Work ID: {first_draft_id}" in email.body assert f"Work ID: {first_draft_id}" in email.html - assert f"Work title: {minimal_record['metadata']['title']}" in email.body - assert f"Work title: {minimal_record['metadata']['title']}" in email.html + assert f"Work title: {minimal_record_metadata['metadata']['title']}" in email.body + assert f"Work title: {minimal_record_metadata['metadata']['title']}" in email.html assert f"User ID: {user_id}" in email.body assert f"User ID: {user_id}" in email.html assert f"User email: {user_email}" in email.body diff --git a/site/tests/api/test_api_record_ops.py b/site/tests/api/test_api_record_ops.py index 27d966168..3fccb8117 100644 --- a/site/tests/api/test_api_record_ops.py +++ b/site/tests/api/test_api_record_ops.py @@ -1,45 +1,25 @@ -import json import pytest -import re -from invenio_access.permissions import system_identity +import arrow +from datetime import timedelta +import hashlib +from invenio_access.permissions import authenticated_user, system_identity +from invenio_access.utils import get_identity +from invenio_files_rest.helpers import compute_checksum from invenio_rdm_records.proxies import current_rdm_records_service as records_service +import json +from pathlib import Path +from pprint import pformat +import re from ..fixtures.users import user_data_set -import arrow -links_template = { - "self": "{0}/records/{1}/draft", - "self_html": "{2}/uploads/{1}", - "self_iiif_manifest": "{0}/iiif/draft:{1}/manifest", - "self_iiif_sequence": "{0}/iiif/draft:{1}/sequence/default", - "files": "{0}/records/{1}/draft/files", - "media_files": "{0}/records/{1}/draft/media-files", - "archive": "{0}/records/{1}/draft/files-archive", - "archive_media": "{0}/records/{1}/draft/media-files-archive", - "record": "{0}/records/{1}", - "record_html": "{2}/records/{1}", - "publish": "{0}/records/{1}/draft/actions/publish", - "review": "{0}/records/{1}/draft/review", - "versions": "{0}/records/{1}/versions", - "access_links": "{0}/records/{1}/access/links", - "access_grants": "{0}/records/{1}/access/grants", - "access_users": "{0}/records/{1}/access/users", - "access_groups": "{0}/records/{1}/access/groups", - "access_request": "{0}/records/{1}/access/request", - "access": "{0}/records/{1}/access", - "reserve_doi": "{0}/records/{1}/draft/pids/doi", - "communities": "{0}/records/{1}/communities", - "communities-suggestions": "{0}/records/{1}/communities-suggestions", - "requests": "{0}/records/{1}/requests", -} - - -def test_draft_creation( +def test_draft_creation_api( running_app, db, + build_draft_record_links, user_factory, client_with_login, - minimal_record, + minimal_record_metadata, headers, search_clear, celery_worker, @@ -49,25 +29,23 @@ def test_draft_creation( u = user_factory( email=user_data_set["user1"]["email"], - password="test", token=True, - admin=True, ) user = u.user - # identity = u.identity - # print(identity) token = u.allowed_token - minimal_record.update({"files": {"enabled": False}}) + + minimal_record_metadata.update({"files": {"enabled": False}}) with app.test_client() as client: - logged_in_client, _ = client_with_login(client, user) + logged_in_client = client_with_login(client, user) response = logged_in_client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) assert response.status_code == 201 actual_draft = response.json + app.logger.debug(f"actual_draft: {pformat(actual_draft)}") actual_draft_id = actual_draft["id"] # ensure the id is in the correct format @@ -86,12 +64,9 @@ def test_draft_creation( == actual_draft["updated"] ) - test_api_url = app.config["SITE_API_URL"] - test_ui_url = app.config["SITE_UI_URL"] - assert actual_draft["links"] == { - k: v.format(test_api_url, actual_draft_id, test_ui_url) - for k, v in links_template.items() - } + assert actual_draft["links"] == build_draft_record_links( + actual_draft_id, app.config["SITE_API_URL"], app.config["SITE_UI_URL"] + ) # assert actual_draft['revision_id'] == 5 # TODO: Why is this 5? @@ -167,82 +142,116 @@ def test_draft_creation( } assert actual_draft["status"] == "draft" publication_date = arrow.get(actual_draft["metadata"]["publication_date"]) - assert actual_draft["ui"][ - "publication_date_l10n_medium" - ] == publication_date.format("MMM D, YYYY") - assert actual_draft["ui"][ - "publication_date_l10n_long" - ] == publication_date.format("MMMM D, YYYY") - created_date = arrow.get(actual_draft["created"]) - assert actual_draft["ui"]["created_date_l10n_long"] == created_date.format( - "MMMM D, YYYY" - ) - updated_date = arrow.get(actual_draft["updated"]) - assert actual_draft["ui"]["updated_date_l10n_long"] == updated_date.format( - "MMMM D, YYYY" - ) - assert actual_draft["ui"]["resource_type"] == { - "id": "image-photograph", - "title_l10n": "Photo", - } - assert actual_draft["ui"]["custom_fields"] == {} - assert actual_draft["ui"]["access_status"] == { - "id": "metadata-only", - "title_l10n": "Metadata-only", - "description_l10n": "No files are available for this record.", - "icon": "tag", - "embargo_date_l10n": None, - "message_class": "", - } - assert actual_draft["ui"]["creators"] == { - "affiliations": [], - "creators": [ - { - "person_or_org": { - "type": "personal", - "name": "Brown, Troy", - "given_name": "Troy", - "family_name": "Brown", - } - }, - { - "person_or_org": { - "type": "organizational", - "name": "Troy Inc.", - } - }, - ], - } - assert actual_draft["ui"]["version"] == "v1" - assert actual_draft["ui"]["is_draft"] - - # publish the record - # publish_response = logged_in_client.post( - # f"{app.config['SITE_API_URL']}/records/{actual_draft_id}/draft" - # "/actions/publish", - # headers={**headers, "Authorization": f"Bearer {token}"}, + + # TODO: UI field only present in object sent to jinja template + # we need to test that the jinja template is working correctly + # + # assert actual_draft["ui"][ + # "publication_date_l10n_medium" + # ] == publication_date.format("MMM D, YYYY") + # assert actual_draft["ui"][ + # "publication_date_l10n_long" + # ] == publication_date.format("MMMM D, YYYY") + # created_date = arrow.get(actual_draft["created"]) + # assert actual_draft["ui"]["created_date_l10n_long"] == created_date.format( + # "MMMM D, YYYY" # ) - # assert publish_response.status_code == 202 + # updated_date = arrow.get(actual_draft["updated"]) + # assert actual_draft["ui"]["updated_date_l10n_long"] == updated_date.format( + # "MMMM D, YYYY" + # ) + # assert actual_draft["ui"]["resource_type"] == { + # "id": "image-photograph", + # "title_l10n": "Photo", + # } + # assert actual_draft["ui"]["custom_fields"] == {} + # assert actual_draft["ui"]["access_status"] == { + # "id": "metadata-only", + # "title_l10n": "Metadata-only", + # "description_l10n": "No files are available for this record.", + # "icon": "tag", + # "embargo_date_l10n": None, + # "message_class": "", + # } + # assert actual_draft["ui"]["creators"] == { + # "affiliations": [], + # "creators": [ + # { + # "person_or_org": { + # "type": "personal", + # "name": "Brown, Troy", + # "given_name": "Troy", + # "family_name": "Brown", + # } + # }, + # { + # "person_or_org": { + # "type": "organizational", + # "name": "Troy Inc.", + # } + # }, + # ], + # } + # assert actual_draft["ui"]["version"] == "v1" + # assert actual_draft["ui"]["is_draft"] + + +def test_draft_creation_service( + running_app, + db, + client_with_login, + minimal_record_metadata, + headers, + user_factory, + search_clear, + celery_worker, + minimal_draft_record_factory, +): + app = running_app.app + result = minimal_draft_record_factory(metadata=minimal_record_metadata) + actual_draft = result.to_dict() + app.logger.debug(f"actual_draft: {pformat(actual_draft)}") + assert actual_draft["is_draft"] + assert not actual_draft["is_published"] + assert not actual_draft["versions"]["is_latest"] # TODO: Why is this False? + assert actual_draft["versions"]["is_latest_draft"] is True + assert actual_draft["versions"]["index"] == 1 + assert actual_draft["status"] == "draft" + assert actual_draft["files"]["enabled"] == False + assert actual_draft["files"]["entries"] == {} + assert ( + actual_draft["metadata"]["creators"] + == minimal_record_metadata["metadata"]["creators"] + ) + assert ( + actual_draft["metadata"]["publisher"] + == minimal_record_metadata["metadata"]["publisher"] + ) + assert ( + actual_draft["metadata"]["publication_date"] + == minimal_record_metadata["metadata"]["publication_date"] + ) + assert ( + actual_draft["metadata"]["resource_type"]["id"] + == minimal_record_metadata["metadata"]["resource_type"]["id"] + ) + assert ( + actual_draft["metadata"]["title"] + == minimal_record_metadata["metadata"]["title"] + ) - # actual_published = publish_response.json - # assert actual_published["id"] == actual_draft_id - # assert actual_published["is_published"] - # assert not actual_published["is_draft"] - # assert actual_published["revision_id"] == 3 - # assert actual_published["versions"]["is_latest"] - # assert actual_published["versions"]["is_latest_draft"] - # assert actual_published["versions"]["index"] == 1 - # assert actual_published["status"] == "published" - # assert actual_published["ui"]["version"] == "v1" - # assert not actual_published["ui"]["is_draft"] + read_result = records_service.read_draft(system_identity, actual_draft["id"]) + actual_read = read_result.to_dict() + assert actual_read["id"] == actual_draft["id"] + assert actual_read["metadata"]["title"] == actual_draft["metadata"]["title"] # @pytest.mark.skip(reason="Not implemented") -def test_record_publication_metadata_only_api( +def test_record_publication_api( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, headers, user_factory, search_clear, @@ -262,11 +271,11 @@ def test_record_publication_metadata_only_api( # print(identity) with app.test_client() as client: - logged_in_client, _ = client_with_login(client, user) - minimal_record.update({"files": {"enabled": False}}) + logged_in_client = client_with_login(client, user) + minimal_record_metadata.update({"files": {"enabled": False}}) response = logged_in_client.post( f"{app.config['SITE_API_URL']}/records", - data=json.dumps(minimal_record), + data=json.dumps(minimal_record_metadata), headers={**headers, "Authorization": f"Bearer {token}"}, ) assert response.status_code == 201 @@ -274,10 +283,6 @@ def test_record_publication_metadata_only_api( actual_draft = response.json actual_draft_id = actual_draft["id"] - # mock_search_api_request( - # "POST", actual_draft_id, minimal_record, app.config["SITE_API_URL"] - # ) - publish_response = logged_in_client.post( f"{app.config['SITE_API_URL']}/records/{actual_draft_id}/draft" "/actions/publish", @@ -293,32 +298,29 @@ def test_record_publication_metadata_only_api( assert actual_published["versions"]["is_latest_draft"] is True assert actual_published["versions"]["index"] == 1 assert actual_published["status"] == "published" - assert actual_published["ui"]["version"] == "v1" - assert not actual_published["ui"]["is_draft"] -def test_record_publication_metadata_only_service( +def test_record_publication_service( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, headers, user_factory, search_clear, celery_worker, mock_send_remote_api_update_fixture, + minimal_draft_record_factory, ): """Test that a system user can create a draft record internally.""" - app = running_app.app - minimal_record.update({"files": {"enabled": False}}) - response = records_service.create(system_identity, minimal_record) - actual_draft = response.to_dict() + minimal_record_metadata.update({"files": {"enabled": False}}) + result = minimal_draft_record_factory(metadata=minimal_record_metadata) + actual_draft = result.to_dict() actual_draft_id = actual_draft["id"] - publish_response = records_service.publish(system_identity, actual_draft_id) - - actual_published = publish_response.to_dict() + publish_result = records_service.publish(system_identity, actual_draft_id) + actual_published = publish_result.to_dict() assert actual_published["id"] == actual_draft_id assert actual_published["is_published"] assert not actual_published["is_draft"] @@ -339,56 +341,99 @@ def test_record_publication_metadata_only_service( assert actual_read["status"] == "published" -@pytest.mark.skip(reason="Not implemented") -def test_record_draft_update_metadata_only_api( +def test_record_draft_update_api( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, headers, user_factory, search_clear, + mock_send_remote_api_update_fixture, ): - pass + app = running_app.app + + u = user_factory( + email=user_data_set["user1"]["email"], + token=True, + ) + user = u.user + token = u.allowed_token + + minimal_record_metadata.update({"files": {"enabled": False}}) + with app.test_client() as client: + logged_in_client = client_with_login(client, user) + creation_response = logged_in_client.post( + f"{app.config['SITE_API_URL']}/records", + data=json.dumps(minimal_record_metadata), + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert creation_response.status_code == 201 + actual_draft = creation_response.json + actual_draft_id = actual_draft["id"] -def test_record_draft_update_metadata_only_service( + minimal_record_metadata["metadata"]["title"] = "A Romans Story 2" + update_response = logged_in_client.put( + f"{app.config['SITE_API_URL']}/records/{actual_draft_id}/draft", + data=json.dumps(minimal_record_metadata), + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert update_response.status_code == 200 + + actual_draft_updated = update_response.json + assert actual_draft_updated["id"] == actual_draft_id + assert actual_draft_updated["metadata"]["title"] == "A Romans Story 2" + assert actual_draft_updated["is_draft"] + assert not actual_draft_updated["is_published"] + assert actual_draft_updated["versions"]["is_latest"] is False + assert actual_draft_updated["versions"]["is_latest_draft"] is True + assert actual_draft_updated["versions"]["index"] == 1 + assert actual_draft_updated["revision_id"] == 7 # TODO: Why is this 7? + assert actual_draft_updated["status"] == "draft" + + # Check that the change is available via the service + read_result = records_service.read_draft(system_identity, actual_draft_id) + actual_read = read_result.to_dict() + assert actual_read["id"] == actual_draft_id + assert actual_read["metadata"]["title"] == "A Romans Story 2" + assert actual_read["is_draft"] + assert not actual_read["is_published"] + assert actual_read["versions"]["is_latest"] is False + assert actual_read["versions"]["is_latest_draft"] is True + assert actual_read["versions"]["index"] == 1 + assert actual_read["revision_id"] == 7 # TODO: Why is this 7? + assert actual_read["status"] == "draft" + + +def test_record_draft_update_service( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, + minimal_draft_record_factory, headers, user_factory, search_clear, celery_worker, mock_send_remote_api_update_fixture, ): - app = running_app.app - - minimal_record.update({"files": {"enabled": False}}) - response = records_service.create(system_identity, minimal_record) - actual_draft = response.to_dict() - actual_draft_id = actual_draft["id"] - - minimal_edited = minimal_record.copy() - minimal_edited["metadata"]["title"] = "A Romans Story 2" - edited_draft = records_service.update_draft( - system_identity, actual_draft_id, minimal_edited + minimal_record_metadata.update({"files": {"enabled": False}}) + draft_result = minimal_draft_record_factory(metadata=minimal_record_metadata) + minimal_record_metadata["metadata"]["title"] = "A Romans Story 2" + edited_draft_result = records_service.update_draft( + system_identity, draft_result.id, minimal_record_metadata ) - actual_edited = edited_draft.to_dict() - actual_edited["metadata"]["title"] = "A Romans Story 2" - - publish_response = records_service.publish(system_identity, actual_edited["id"]) - - actual_published = publish_response.to_dict() - assert actual_published["id"] == actual_draft_id - assert actual_published["metadata"]["title"] == "A Romans Story 2" - assert actual_published["is_published"] - assert not actual_published["is_draft"] - assert actual_published["versions"]["is_latest"] - assert actual_published["versions"]["is_latest_draft"] is True - assert actual_published["versions"]["index"] == 1 - assert actual_published["status"] == "published" + actual_edited = edited_draft_result.to_dict() + assert actual_edited["id"] == draft_result.id + assert actual_edited["metadata"]["title"] == "A Romans Story 2" + assert not actual_edited["is_published"] + assert actual_edited["is_draft"] + assert not actual_edited["versions"]["is_latest"] # TODO: Why is this False? + assert actual_edited["versions"]["is_latest_draft"] is True + assert actual_edited["versions"]["index"] == 1 + assert actual_edited["status"] == "draft" + assert actual_edited["revision_id"] == 7 # TODO: Why is this 7? @pytest.mark.skip(reason="Not implemented") @@ -396,10 +441,11 @@ def test_record_published_update( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, headers, user_factory, search_clear, + mock_send_remote_api_update_fixture, ): pass @@ -409,36 +455,481 @@ def test_record_versioning( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, headers, user_factory, search_clear, + mock_send_remote_api_update_fixture, ): pass -@pytest.mark.skip(reason="Not implemented") -def test_record_file_upload( +def test_record_file_upload_api_not_enabled( running_app, db, client_with_login, - minimal_record, + minimal_record_metadata, headers, user_factory, search_clear, + minimal_draft_record_factory, + mock_send_remote_api_update_fixture, ): - pass + """Test that a user cannot upload files to a record that has files disabled.""" + app = running_app.app + u = user_factory( + email=user_data_set["user1"]["email"], + password="test", + token=True, + admin=True, + ) + user = u.user + token = u.allowed_token + identity = get_identity(user) + identity.provides.add(authenticated_user) + file_list = [{"key": "sample.pdf"}] -@pytest.mark.skip(reason="Not implemented") -def test_db(running_app, client): + with app.test_client() as client: + minimal_record_metadata["files"] = {"enabled": False} + draft_result = minimal_draft_record_factory( + identity=identity, metadata=minimal_record_metadata + ) + draft_id = draft_result.id + + # logged_in_client = client_with_login(client, user) + + headers.update({"content-type": "application/json"}) + response = client.post( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files", + data=json.dumps(file_list), + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 403 + + +def test_record_file_upload_api( + running_app, + db, + client_with_login, + minimal_record_metadata, + headers, + user_factory, + search_clear, + minimal_draft_record_factory, + mock_send_remote_api_update_fixture, +): + """ + Test the record file upload API. + + Create a draft record, upload a file to it via the API, and confirm that + the file is uploaded. Check the `files` property of the draft's retrieved + metadata object. Then delete the file via the API and confirm that + it is deleted. Check the `files` property of the draft's retrieved + metadata object again to ensure that the file is no longer present. + """ + app = running_app.app + u = user_factory( + email=user_data_set["user1"]["email"], + password="test", + token=True, + admin=True, + ) + user = u.user + token = u.allowed_token + identity = get_identity(user) + identity.provides.add(authenticated_user) + + file_path = ( + Path(__file__).parent.parent.parent / "tests/helpers/sample_files/sample.pdf" + ) + file_list = [{"key": "sample.pdf"}] + + with app.test_client() as client: + minimal_record_metadata["files"] = {"enabled": True} + draft_result = minimal_draft_record_factory( + identity=identity, metadata=minimal_record_metadata + ) + draft_id = draft_result.id + + # logged_in_client = client_with_login(client, user) + + # Initialize the file upload + + headers.update({"content-type": "application/json"}) + response = client.post( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files", + data=json.dumps(file_list), + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + csrf_cookie = response.headers.get("Set-Cookie") + print("headers") + print(headers) + print("response headers") + print(response.headers) + assert response.status_code == 201 + assert response.json["enabled"] + assert response.json["default_preview"] is None + assert response.json["order"] == [] + assert len(response.json["entries"]) == 1 + non_date_entries_vals = { + k: v + for k, v in response.json["entries"][0].items() + if k not in ["updated", "created"] + } + assert non_date_entries_vals == { + "access": {"hidden": False}, + "key": "sample.pdf", + "metadata": None, + "status": "pending", + "links": { + "content": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/" + "files/sample.pdf/content" + ), + "self": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/" + "files/sample.pdf" + ), + "commit": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/" + "files/sample.pdf/commit" + ), + "iiif_api": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf" + "/full/full/0/default.png" + ), + "iiif_base": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf" + ), + "iiif_canvas": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}/canvas/" + "sample.pdf" + ), + "iiif_info": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf/" + "info.json" + ), + }, + } + assert arrow.utcnow() - arrow.get( + response.json["entries"][0]["created"] + ) < timedelta(seconds=1) + assert arrow.utcnow() - arrow.get( + response.json["entries"][0]["updated"] + ) < timedelta(seconds=1) + assert response.json["links"] == { + "self": (f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files"), + "archive": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files-archive" + ), + } + + # upload the file content + with open( + file_path, + "rb", + ) as binary_file_data: + binary_file_data.seek(0) + + headers.update({"content-type": "application/octet-stream"}) + response = client.put( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf/content", + data=binary_file_data, + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert response.status_code == 200 + assert response.json["key"] == "sample.pdf" + assert arrow.utcnow() - arrow.get(response.json["updated"]) < timedelta( + seconds=1 + ) + assert arrow.utcnow() - arrow.get(response.json["created"]) < timedelta( + seconds=1 + ) + assert response.json["metadata"] is None + assert response.json["status"] == "pending" + assert response.json["links"] == { + "content": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf/content" + ), + "self": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf" + ), + "commit": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf/commit" + ), + "iiif_api": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf/" + "full/full/0/default.png" + ), + "iiif_base": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf" + ), + "iiif_canvas": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}/canvas/" + "sample.pdf" + ), + "iiif_info": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf/" + "info.json" + ), + } + + # calculate the md5 checksum + binary_file_data.seek(0) + md5_checksum = compute_checksum(binary_file_data, "md5", hashlib.md5()) + + # finalize the file upload + headers.update({"content-type": "application/json"}) + commit_response = client.post( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf/commit", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert commit_response.status_code == 200 + + assert commit_response.json["key"] == "sample.pdf" + assert arrow.utcnow() - arrow.get(commit_response.json["updated"]) < timedelta( + seconds=1 + ) + assert arrow.utcnow() - arrow.get(commit_response.json["created"]) < timedelta( + seconds=1 + ) + assert re.match(r"^md5:[a-f0-9]{32}$", commit_response.json["checksum"]) + assert commit_response.json["mimetype"] == "application/pdf" + assert commit_response.json["size"] == 13264 + assert commit_response.json["status"] == "completed" + assert commit_response.json["metadata"] is None + assert re.match( + r"^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$", + commit_response.json["file_id"], + ) + assert re.match( + r"^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$", + commit_response.json["version_id"], + ) + assert re.match( + r"^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$", + commit_response.json["bucket_id"], + ) + assert commit_response.json["storage_class"] == "L" + assert commit_response.json["links"] == { + "content": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf/content" + ), + "self": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/sample.pdf" + ), + "commit": ( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/" + "sample.pdf/commit" + ), + "iiif_api": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf/" + "full/full/0/default.png" + ), + "iiif_base": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf" + ), + "iiif_canvas": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}/canvas/sample.pdf" + ), + "iiif_info": ( + f"{app.config['SITE_API_URL']}/iiif/draft:{draft_id}:sample.pdf/" + "info.json" + ), + } + + # confirm the file is in the draft + draft_after_upload = records_service.read_draft( + identity=system_identity, id_=draft_id + ) + assert draft_after_upload["files"]["order"] == [] + assert draft_after_upload["files"]["enabled"] + assert draft_after_upload["files"]["count"] == 1 + assert draft_after_upload["files"]["total_bytes"] == 13264 + entries = draft_after_upload["files"]["entries"] + assert len(entries.keys()) == 1 + assert entries["sample.pdf"]["key"] == "sample.pdf" + # assert entries["sample.pdf"]["checksum"] == md5_checksum + # FIXME: these checksums are not matching + assert entries["sample.pdf"]["mimetype"] == "application/pdf" + assert entries["sample.pdf"]["size"] == 13264 + assert entries["sample.pdf"]["storage_class"] == "L" + + # delete the uploaded file from the draft + delete_response = client.delete( + f"{app.config['SITE_API_URL']}/records/{draft_id}/draft/files/sample.pdf", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert delete_response.status_code == 204 + + # confirm the file is deleted from the draft + draft_after_delete = records_service.read_draft( + identity=system_identity, id_=draft_id + ) + assert len(draft_after_delete["files"]["entries"].keys()) == 0 + assert draft_after_delete["files"]["enabled"] + assert draft_after_delete["files"]["count"] == 0 + assert draft_after_delete["files"]["total_bytes"] == 0 + + +def test_record_view_api( + running_app, + db, + minimal_record_metadata, + minimal_published_record_factory, + search_clear, + celery_worker, + build_published_record_links, + mock_send_remote_api_update_fixture, +): + """ + Test the record view API. + + Create a published record and test that its metadata is returned from the + records API endpoint. + """ + app = running_app.app + record = minimal_published_record_factory() - res = client.get("/api/records/") - assert res.json == { - "message": "The requested URL was not found on the server. If you " - "entered the URL manually please check your spelling and " - "try again.", - "status": 404, - } + with app.test_client() as client: + record_response = client.get(f"/api/records/{record.id}") + record = record_response.json + assert arrow.utcnow() - arrow.get(record["created"]) < timedelta(seconds=2) + assert arrow.utcnow() - arrow.get(record["updated"]) < timedelta(seconds=2) + assert record["access"] == { + "embargo": {"active": False, "reason": None}, + "files": "public", + "record": "public", + "status": "metadata-only", + } + assert record["files"] == { + "count": 0, + "enabled": False, + "entries": {}, + "order": [], + "total_bytes": 0, + } + assert record["deletion_status"] == { + "is_deleted": False, + "status": "P", + } + assert record["custom_fields"] == {} + assert record["media_files"] == { + "count": 0, + "enabled": False, + "entries": {}, + "order": [], + "total_bytes": 0, + } + assert ( + record["metadata"]["creators"] + == minimal_record_metadata["metadata"]["creators"] + ) + assert ( + record["metadata"]["publication_date"] + == minimal_record_metadata["metadata"]["publication_date"] + ) + assert ( + record["metadata"]["publisher"] + == minimal_record_metadata["metadata"]["publisher"] + ) + # Add title to resource type (updated by system after draft creation) + minimal_record_metadata["metadata"]["resource_type"]["title"] = {"en": "Photo"} + assert ( + record["metadata"]["resource_type"] + == minimal_record_metadata["metadata"]["resource_type"] + ) + assert not record["is_draft"] + assert record["is_published"] + assert record["links"] == build_published_record_links( + record["id"], + app.config["SITE_API_URL"], + app.config["SITE_UI_URL"], + record["parent"]["id"], + ) + assert record["parent"]["access"] == { + "owned_by": None, + "settings": { + "accept_conditions_text": None, + "allow_guest_requests": False, + "allow_user_requests": False, + "secret_link_expiration": 0, + }, + } + assert record["parent"]["communities"] == {} + assert record["parent"]["id"] == record["parent"]["id"] + assert record["parent"]["pids"] == { + "doi": { + "client": "datacite", + "identifier": record["parent"]["pids"]["doi"]["identifier"], + "provider": "datacite", + }, + } + assert record["pids"] == { + "doi": { + "client": "datacite", + "identifier": f"10.17613/{record['id']}", + "provider": "datacite", + }, + "oai": { + "identifier": f"oai:{app.config['SITE_UI_URL']}:{record['id']}", + "provider": "oai", + }, + } + assert record["revision_id"] == 3 + assert record["stats"] == { + "all_versions": { + "data_volume": 0.0, + "downloads": 0, + "unique_downloads": 0, + "unique_views": 0, + "views": 0, + }, + "this_version": { + "data_volume": 0.0, + "downloads": 0, + "unique_downloads": 0, + "unique_views": 0, + "views": 0, + }, + } + assert record["status"] == "published" + assert record["versions"] == {"index": 1, "is_latest": True} + assert record["custom_fields"] == {} - assert True + +def test_records_api_endpoint_not_found(running_app): + """ + Test that the records API endpoint returns a 404 error when the requested + record is not found. + """ + app = running_app.app + with app.test_client() as client: + response = client.get("/api/records/1234567890") + assert response.json == { + "message": "The persistent identifier does not exist.", + "status": 404, + } + + +def test_records_api_bare_endpoint(running_app): + """ + Test that the records API endpoint returns a 404 error when the requested + record is not found. + """ + app = running_app.app + with app.test_client() as client: + response = client.get("/api/records/") + assert response.json == { + "message": "The requested URL was not found on the server. If you " + "entered the URL manually please check your spelling and " + "try again.", + "status": 404, + } diff --git a/site/tests/api/test_collections.py b/site/tests/api/test_collections.py new file mode 100644 index 000000000..ed0964e84 --- /dev/null +++ b/site/tests/api/test_collections.py @@ -0,0 +1,329 @@ +import pytest +import arrow +from datetime import timedelta +import json +from invenio_access.permissions import authenticated_user +from invenio_access.utils import get_identity + +# from pprint import pformat +import re + + +def test_collection_submission_by_owner_open( + running_app, + db, + user_factory, + client_with_login, + minimal_community_factory, + minimal_draft_record_factory, + headers, + celery_worker, + mock_send_remote_api_update_fixture, +): + """ + Test collection submission by the owner when the collection does not require review. + + FIXME: This should not be allowed for collection owners. It violates the + review policy. + + Create a collection that requires review, submit a record to it, and confirm + that the record is published without review. + """ + app = running_app.app + + collection_admin = user_factory(token=True) + token = collection_admin.allowed_token + admin_user = collection_admin.user + admin_id = admin_user.id + identity = get_identity(admin_user) + identity.provides.add(authenticated_user) + + # review policy is closed, so *all* submissions require review + # record policy is open, so submissions can be received + collection_rec = minimal_community_factory( + owner=admin_id, + access={ + "record_policy": "open", + "review_policy": "closed", + }, + ) + collection_meta = collection_rec.to_dict() + + draft = minimal_draft_record_factory(identity=identity) + draft_owner = draft.to_dict()["parent"]["access"]["owned_by"] + app.logger.debug(draft_owner) + + from invenio_rdm_records.services.permissions import RDMRecordPermissionPolicy + + app.logger.debug("RDMPermissionPolicy allows?") + app.logger.debug( + RDMRecordPermissionPolicy(action="update_draft") + .generators[0] + .needs(record=draft._record) + ) + + with app.test_client() as client: + # client = client_with_login(client, admin_user) + create_review_response = client.put( + f"{app.config['SITE_API_URL']}/records/{draft['id']}/draft/review", + headers={**headers, "Authorization": f"Bearer {token}"}, + data=json.dumps( + { + "receiver": { + "community": collection_meta["id"], + }, + "type": "community-submission", + } + ), + ) + + assert create_review_response.status_code == 200 + created = create_review_response.json["created"] + updated = create_review_response.json["updated"] + review_id = create_review_response.json["id"] + assert create_review_response.json == { + "created": created, + "created_by": {"user": f"{admin_id}"}, + "expires_at": None, + "id": review_id, + "is_closed": False, + "is_expired": False, + "is_open": False, + "links": { + "actions": { + "submit": ( + f"{app.config['SITE_API_URL']}/requests/{review_id}/" + "actions/submit" + ), + }, + "comments": ( + f"{app.config['SITE_API_URL']}/requests/{review_id}/comments" + ), + "self": f"{app.config['SITE_API_URL']}/requests/{review_id}", + "self_html": f"{app.config['SITE_UI_URL']}/requests/{review_id}", + "timeline": ( + f"{app.config['SITE_API_URL']}/requests/{review_id}/timeline" + ), + }, + "number": "1", + "receiver": {"community": collection_meta["id"]}, + "revision_id": 2, + "status": "created", + "title": "", + "topic": {"record": draft["id"]}, + "type": "community-submission", + "updated": updated, + } + + read_draft_response = client.get( + f"{app.config['SITE_API_URL']}/records/{draft['id']}/draft", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + + assert read_draft_response.status_code == 200 + assert read_draft_response.json["id"] == draft["id"] + assert read_draft_response.json["parent"]["review"] == { + "id": review_id, + "links": {}, + "receiver": {"community": collection_meta["id"]}, + "status": "created", + "title": "", + "type": "community-submission", + } + + submit_response = client.post( + f"{app.config['SITE_API_URL']}/records/{draft['id']}/draft/" + "actions/submit-review", + headers={**headers, "Authorization": f"Bearer {token}"}, + data=json.dumps( + { + "payload": { + "content": "Thank you in advance for the review", + "format": "html", + } + } + ), + ) + assert submit_response.status_code == 202 + + read_response = client.get( + f"{app.config['SITE_API_URL']}/records/{draft['id']}", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert read_response.status_code == 200 + assert read_response.json["id"] == draft["id"] + assert read_response.json["is_published"] + assert read_response.json["status"] == "published" + + +def test_collection_submission_by_curator_closed( + running_app, + db, + user_factory, + client_with_login, + minimal_community_factory, + minimal_draft_record_factory, + headers, + mock_send_remote_api_update_fixture, + celery_worker, +): + """ + Test the collection submission API by a curator when the collection requires review. + + Intended to confirm that the review policy is enforced. + + Create a collection that requires review, submit a record to it, and confirm + that the record is submitted and the review is pending. Accept the review and + confirm that the record is published. + """ + app = running_app.app + u = user_factory(email="test@example.com", token=True, saml_id=None) + token = u.allowed_token + identity = get_identity(u.user) + identity.provides.add(authenticated_user) + + admin_u = user_factory(email="admin@example.com", token=True, saml_id=None) + admin_token = admin_u.allowed_token + admin_identity = get_identity(admin_u.user) + admin_identity.provides.add(authenticated_user) + + collection_rec = minimal_community_factory( + owner=admin_u.user.id, + access={"record_policy": "open", "review_policy": "closed"}, + members={"curator": [u.user.id]}, + ) + collection_meta = collection_rec.to_dict() + + draft = minimal_draft_record_factory(identity=identity) + draft_owner = draft.to_dict()["parent"]["access"]["owned_by"] + app.logger.debug(draft_owner) + + with app.test_client() as client: + review_response = client.put( + f"{app.config['SITE_API_URL']}/records/{draft['id']}/draft/review", + headers={**headers, "Authorization": f"Bearer {token}"}, + data=json.dumps( + { + "receiver": {"community": collection_meta["id"]}, + "type": "community-submission", + } + ), + ) + assert review_response.status_code == 200 + review_id = review_response.json["id"] + + submit_response = client.post( + f"{app.config['SITE_API_URL']}/records/{draft['id']}/draft/" + "actions/submit-review", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert submit_response.status_code == 202 + + read_response = client.get( + f"{app.config['SITE_API_URL']}/records/{draft['id']}", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert read_response.status_code == 404 + + read_draft_response = client.get( + f"{app.config['SITE_API_URL']}/records/{draft['id']}/draft", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert read_draft_response.status_code == 200 + assert read_draft_response.json["id"] == draft["id"] + assert not read_draft_response.json["is_published"] + assert read_draft_response.json["status"] == "in_review" + + accept_response = client.post( + f"{app.config['SITE_API_URL']}/requests/{review_id}/actions/accept", + headers={**headers, "Authorization": f"Bearer {admin_token}"}, + ) + assert accept_response.status_code == 200 + + read_response = client.get( + f"{app.config['SITE_API_URL']}/records/{draft['id']}", + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + assert read_response.status_code == 200 + assert read_response.json["id"] == draft["id"] + assert read_response.json["is_published"] + assert read_response.json["status"] == "published" + + +def test_group_collection_read_all( + running_app, + headers, + user_factory, + sample_communities_factory, + communities_links_factory, + mock_send_remote_api_update_fixture, +): + app = running_app.app + u = user_factory(token=True) + token = u.allowed_token + identity = get_identity(u.user) + identity.provides.add(authenticated_user) + + sample_communities_factory() + + app.logger.debug(app.config["KC_SEARCH_URL_DOCS"]) + + with app.test_client() as client: + response = client.get( + f"{app.config['SITE_API_URL']}/group_collections?size=4", + follow_redirects=True, + headers={**headers, "Authorization": f"Bearer {token}"}, + ) + app.logger.debug(response.text) + app.logger.debug(response.json) + assert response.status_code == 200 + assert response.json["hits"]["total"] == 8 + assert len(response.json["hits"]["hits"]) == 4 + assert response.json["sortBy"] == "updated-desc" + assert response.json["links"] == { + "next": f"{app.config['SITE_API_URL']}/group_collections?" + "page=2&q=%2B_exists_%3Acustom_fields.kcr%5C%3Acommons_instance%20" + "&size=4&sort=updated-desc", + "self": f"{app.config['SITE_API_URL']}/group_collections?" + "page=1&q=%2B_exists_%3Acustom_fields.kcr%5C%3Acommons_instance%20" + "&size=4&sort=updated-desc", + } + for hit in response.json["hits"]["hits"]: + assert re.match( + r"^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$", + hit["id"], + ) + assert hit["access"] == { + "member_policy": "open", + "members_visibility": "public", + "record_policy": "open", + "review_policy": "closed", + "visibility": "public", + } + assert hit["children"] == {"allow": False} + assert arrow.utcnow() - arrow.get(hit["created"]) < timedelta(seconds=3) + assert hit["links"] == communities_links_factory(hit["id"], hit["slug"]) + assert hit["metadata"]["curation_policy"] == "Curation policy" + assert "description" in hit["metadata"]["description"] + assert "Organization" in hit["metadata"]["organizations"][0]["name"] + assert "Information for" in hit["metadata"]["page"] + assert re.match(r".* Community \d+$", hit["metadata"]["title"]) + assert hit["metadata"]["website"] + assert hit["metadata"]["type"]["id"] in [ + "event", + "commons", + "group", + "organization", + ] + assert arrow.utcnow() - arrow.get(hit["updated"]) < timedelta(seconds=3) + assert re.match(r".*-community-\d+$", hit["slug"]) + assert hit["custom_fields"]["kcr:commons_group_description"] + assert hit["custom_fields"]["kcr:commons_group_name"] + assert hit["custom_fields"]["kcr:commons_group_id"] + assert hit["custom_fields"]["kcr:commons_group_visibility"] + assert hit["custom_fields"]["kcr:commons_instance"] + + +@pytest.mark.skip(reason="Not implemented") +def test_group_collections_read_one(running_app, headers, user_factory): + pass diff --git a/site/tests/api/test_search_provisioning.py b/site/tests/api/test_search_provisioning.py index 9160e6907..67dd18127 100644 --- a/site/tests/api/test_search_provisioning.py +++ b/site/tests/api/test_search_provisioning.py @@ -21,7 +21,7 @@ def test_trigger_search_provisioning( db, requests_mock, monkeypatch, - minimal_record, + minimal_record_metadata, user_factory, create_records_custom_fields, celery_worker, @@ -54,13 +54,13 @@ def test_trigger_search_provisioning( service = current_rdm_records.records_service # Draft creation, no remote API operations should be prompted - draft = service.create(system_identity, minimal_record) + draft = service.create(system_identity, minimal_record_metadata) actual_draft = draft.data assert actual_draft["metadata"]["title"] == "A Romans story" assert mock_adapter.call_count == 0 # Draft edit, no remote API operations should be prompted - minimal_edited = minimal_record.copy() + minimal_edited = minimal_record_metadata.copy() minimal_edited["metadata"]["title"] = "A Romans Story 2" edited_draft = service.update_draft(system_identity, draft.id, minimal_edited) actual_edited = edited_draft.data.copy() @@ -657,7 +657,7 @@ def test_trigger_community_provisioning( def test_search_id_recording_callback( running_app, - minimal_record, + minimal_record_metadata, location, search, search_clear, @@ -679,7 +679,7 @@ def test_search_id_recording_callback( # Set up minimal record to update after search provisioning service = current_rdm_records.records_service - draft = service.create(system_identity, minimal_record) + draft = service.create(system_identity, minimal_record_metadata) read_record = service.read_draft(system_identity, draft.id) assert read_record.data["metadata"]["title"] == "A Romans story" assert read_record.data["custom_fields"].get("kcr:commons_search_recid") is None diff --git a/site/tests/api/test_stats.py b/site/tests/api/test_stats.py index 7752d9960..20f4c97d3 100644 --- a/site/tests/api/test_stats.py +++ b/site/tests/api/test_stats.py @@ -11,8 +11,8 @@ @pytest.mark.skip("Not implemented") -def test_stat_creation(running_app, db, search_clear, minimal_record): - draft = current_rdm_records_service.create(system_identity, minimal_record) +def test_stat_creation(running_app, db, search_clear, minimal_record_metadata): + draft = current_rdm_records_service.create(system_identity, minimal_record_metadata) published = current_rdm_records_service.publish(system_identity, draft["id"]) record_id = published["id"] metadata_record = published["metadata"] @@ -24,13 +24,13 @@ def test_stats_backend_processing( running_app, db, search_clear, - minimal_record, + minimal_record_metadata, user_factory, create_stats_indices, celery_worker, mock_send_remote_api_update_fixture, ): - draft = current_rdm_records_service.create(system_identity, minimal_record) + draft = current_rdm_records_service.create(system_identity, minimal_record_metadata) published = current_rdm_records_service.publish(system_identity, draft["id"]) record_id = published.id metadata_record = published.to_dict() diff --git a/site/tests/conftest.py b/site/tests/conftest.py index b850dde2d..91f2b0b42 100644 --- a/site/tests/conftest.py +++ b/site/tests/conftest.py @@ -47,7 +47,6 @@ "tests.fixtures.vocabularies.resource_types", "tests.fixtures.vocabularies.roles", "tests.fixtures.vocabularies.subjects", - "tests.helpers.sample_records.basic", ) diff --git a/site/tests/fixtures/communities.py b/site/tests/fixtures/communities.py index e73869ec8..4e63fcadc 100644 --- a/site/tests/fixtures/communities.py +++ b/site/tests/fixtures/communities.py @@ -1,109 +1,148 @@ import pytest -from invenio_access.permissions import system_identity +from invenio_access.permissions import system_identity, authenticated_user from invenio_access.utils import get_identity from invenio_accounts.proxies import current_accounts from invenio_communities.communities.records.api import Community from invenio_communities.proxies import current_communities import marshmallow as ma import traceback -from typing import Callable +from typing import Callable, Optional -def group_communities_data_set(): +@pytest.fixture(scope="function") +def communities_links_factory(): """ - Create metadata for group collections for testing. + Create links for communities for testing. """ - communities_data = [] - groups_data = { - "knowledgeCommons": [ - ( - "123", - "Commons Group 1", - "Community 1", - ), - ( - "456", - "Commons Group 2", - "Community 2", - ), - ( - "789", - "Commons Group 3", - "Community 3", - ), - ( - "101112", - "Commons Group 4", - "Community 4", - ), - ], - "msuCommons": [ - ( - "131415", - "MSU Group 1", - "MSU Community 1", - ), - ( - "161718", - "MSU Group 2", - "MSU Community 2", + + def assemble_links(community_id: str, slug: str): + return { + "featured": f"https://localhost/api/communities/{community_id}/featured", + "invitations": ( + f"https://localhost/api/communities/{community_id}/invitations" ), - ( - "181920", - "MSU Group 3", - "MSU Community 3", + "logo": f"https://localhost/api/communities/{community_id}/logo", + "members": f"https://localhost/api/communities/{community_id}/members", + "membership_requests": ( + f"https://localhost/api/communities/{community_id}/membership-requests" ), - ( - "212223", - "MSU Group 4", - "MSU Community 4", + "public_members": ( + f"https://localhost/api/communities/{community_id}/members/public" ), - ], - } - # Each top-level key is a commons instance, and each value is a list of tuples, - # where each tuple is a group on that commons. - # In each group tuple, the first element is the commons group id, the second - # is the group name, and the third is the community name. - - for instance in groups_data.keys(): - for c in groups_data[instance]: - slug = c[2].lower().replace("-", "").replace(" ", "") - rec_data = { - "access": { - "visibility": "public", - "member_policy": "open", - "record_policy": "open", - }, - "slug": c[2].lower().replace(" ", "-"), - "metadata": { - "title": c[2], - "description": c[2] + " description", - "type": { - "id": "event", + "records": f"https://localhost/api/communities/{community_id}/records", + "rename": f"https://localhost/api/communities/{community_id}/rename", + "requests": f"https://localhost/api/communities/{community_id}/requests", + "self": f"https://localhost/api/communities/{community_id}", + "self_html": f"https://localhost/collections/{slug}", + "settings_html": f"https://localhost/collections/{slug}/settings", + } + + return assemble_links + + +@pytest.fixture(scope="function") +def group_communities_data_factory(): + """ + Create metadata for group collections for testing. + """ + + def assemble_data() -> list[dict]: + communities_data = [] + groups_data = { + "knowledgeCommons": [ + ( + "123", + "Commons Group 1", + "Community 1", + ), + ( + "456", + "Commons Group 2", + "Community 2", + ), + ( + "789", + "Commons Group 3", + "Community 3", + ), + ( + "101112", + "Commons Group 4", + "Community 4", + ), + ], + "msuCommons": [ + ( + "131415", + "MSU Group 1", + "MSU Community 1", + ), + ( + "161718", + "MSU Group 2", + "MSU Community 2", + ), + ( + "181920", + "MSU Group 3", + "MSU Community 3", + ), + ( + "212223", + "MSU Group 4", + "MSU Community 4", + ), + ], + } + # Each top-level key is a commons instance, and each value is a list of tuples, + # where each tuple is a group on that commons. + # In each group tuple, the first element is the commons group id, the second + # is the group name, and the third is the community name. + + for instance in groups_data.keys(): + for c in groups_data[instance]: + slug = c[2].lower().replace("-", "").replace(" ", "") + rec_data = { + "access": { + "visibility": "public", + "member_policy": "open", + "record_policy": "open", + "review_policy": "closed", + "members_visibility": "public", + }, + "slug": c[2].lower().replace(" ", "-"), + "children": {"allow": False}, + "metadata": { + "title": c[2], + "description": c[2] + " description", + "type": { + "id": "event", + }, + "curation_policy": "Curation policy", + "page": f"Information for {c[2].lower()}", + "website": f"https://{slug}.com", + "organizations": [ + { + "name": "Organization 1", + } + ], + }, + "custom_fields": { + "kcr:commons_instance": instance, + "kcr:commons_group_id": c[0], + "kcr:commons_group_name": c[1], + "kcr:commons_group_description": (f"{c[1]} description"), + "kcr:commons_group_visibility": "public", }, - "curation_policy": "Curation policy", - "page": f"Information for {c[2].lower()}", - "website": f"https://{slug}.com", - "organizations": [ - { - "name": "Organization 1", - } - ], - }, - "custom_fields": { - "kcr:commons_instance": instance, - "kcr:commons_group_id": c[0], - "kcr:commons_group_name": c[1], - "kcr:commons_group_description": (f"{c[1]} description"), - "kcr:commons_group_visibility": "public", - }, - } - communities_data.append(rec_data) - return communities_data + } + communities_data.append(rec_data) + return communities_data + + return assemble_data @pytest.fixture(scope="function") -def minimal_community_factory(app, user_factory, create_communities_custom_fields): +def minimal_community_factory(app, db, user_factory, create_communities_custom_fields): """ Create a minimal community for testing. @@ -111,61 +150,91 @@ def minimal_community_factory(app, user_factory, create_communities_custom_field for testing. That function returns the created community record. """ - def create_minimal_community(owner=None): + def create_minimal_community( + owner: Optional[int] = None, + slug: Optional[str] = None, + metadata: dict = {}, + access: dict = {}, + custom_fields: dict = {}, + members: dict = {"reader": [], "curator": [], "manager": [], "owner": []}, + ): + """ + Create a minimal community for testing. + + Allows overriding of default metadata, access, and custom fields values. + Also allows specifying the members of the community with their roles. + + If no owner is specified, a new user is created and used as the owner. + """ if owner is None: owner = user_factory().user.id + slug = slug or "my-community" - community_data = { - "access": { - "visibility": "public", - "member_policy": "open", - "record_policy": "open", - }, - "slug": "my-community", - "metadata": { - "title": "My Community", - "description": "A description", - "type": { - "id": "event", - }, - "curation_policy": "Curation policy", - "page": "Information for my community", - "website": "https://my-community.com", - "organizations": [ - { - "name": "Organization 1", - } - ], - }, - "custom_fields": { - "kcr:commons_instance": "knowledgeCommons", - "kcr:commons_group_id": "mygroup", - "kcr:commons_group_name": "My Group", - "kcr:commons_group_description": "My group description", - "kcr:commons_group_visibility": "public", + access_data = { + "visibility": "public", + "members_visibility": "public", + "member_policy": "open", + "record_policy": "open", + "review_policy": "open", + } + access_data.update(access) + metadata_data = { + "title": "My Community", + "description": "A description", + "type": { + "id": "event", }, + "curation_policy": "Curation policy", + "page": "Information for my community", + "website": "https://my-community.com", + "organizations": [ + { + "name": "Organization 1", + } + ], } + metadata_data.update(metadata) - rec = current_communities.service.create( - identity=system_identity, data=community_data - ) - current_communities.service.members.add( - system_identity, - rec["id"], - data={ - "members": [{"type": "user", "id": str(owner)}], - "role": "owner", - }, + custom_fields_data = {} + custom_fields_data.update(custom_fields) + + community_data = { + "slug": slug, + "access": access_data, + "metadata": metadata_data, + "custom_fields": custom_fields_data, + } + + owner_identity = get_identity(current_accounts.datastore.get_user_by_id(owner)) + owner_identity.provides.add(authenticated_user) + community_rec = current_communities.service.create( + identity=owner_identity, data=community_data ) - assert rec["metadata"]["title"] == community_data["metadata"]["title"] + community_id = community_rec.id + + for m in members.keys(): + for user_id in members[m]: + current_communities.service.members.add( + system_identity, + community_rec["id"], + data={ + "members": [{"type": "user", "id": str(user_id)}], + "role": m, + }, + ) Community.index.refresh() - return rec.to_dict() + + return current_communities.service.read( + identity=system_identity, id_=community_id + ) return create_minimal_community @pytest.fixture(scope="function") -def sample_communities_factory(app, db, create_communities_custom_fields) -> Callable: +def sample_communities_factory( + app, db, create_communities_custom_fields, group_communities_data_factory +) -> Callable: """ Create communities for testing linked to commons groups. @@ -186,7 +255,7 @@ def sample_communities_factory(app, db, create_communities_custom_fields) -> Cal to commons groups. """ - def create_communities(app, metadata=[]) -> None: + def create_communities(metadata: list[dict] = []) -> None: """ Create communities for testing linked to commons groups. """ @@ -196,7 +265,7 @@ def create_communities(app, metadata=[]) -> None: if communities.total > 0: print("Communities already exist.") return - communities_data = metadata or group_communities_data_set() + communities_data = metadata or group_communities_data_factory() try: for rec_data in communities_data: rec = current_communities.service.create( diff --git a/site/tests/fixtures/files.py b/site/tests/fixtures/files.py new file mode 100644 index 000000000..74a1d9bc7 --- /dev/null +++ b/site/tests/fixtures/files.py @@ -0,0 +1,11 @@ +import pytest + + +@pytest.fixture(scope="function") +def build_file_links(): + def _factory(record_id, base_url, upload_url): + return { + "self": f"{base_url}/records/{record_id}/draft/files", + } + + return _factory diff --git a/site/tests/fixtures/records.py b/site/tests/fixtures/records.py index d3d3a02dc..7bc8bec80 100644 --- a/site/tests/fixtures/records.py +++ b/site/tests/fixtures/records.py @@ -1,8 +1,37 @@ import pytest +from flask_principal import Identity +from invenio_access.permissions import system_identity +from invenio_rdm_records.proxies import current_rdm_records_service as records_service +from typing import Optional + + +@pytest.fixture(scope="function") +def minimal_draft_record_factory(running_app, db, minimal_record_metadata): + def _factory( + metadata: Optional[dict] = None, identity: Optional[Identity] = None, **kwargs + ): + input_metadata = metadata or minimal_record_metadata + identity = identity or system_identity + return records_service.create(identity, input_metadata) + + return _factory + + +@pytest.fixture(scope="function") +def minimal_published_record_factory(running_app, db, minimal_record_metadata): + def _factory( + metadata: Optional[dict] = None, identity: Optional[Identity] = None, **kwargs + ): + input_metadata = metadata or minimal_record_metadata + identity = identity or system_identity + draft = records_service.create(identity, input_metadata) + return records_service.publish(identity, draft.id) + + return _factory @pytest.fixture() -def minimal_record(): +def minimal_record_metadata(): """Minimal record data as dict coming from the external world.""" return { "pids": {}, @@ -19,6 +48,7 @@ def minimal_record(): "person_or_org": { "family_name": "Brown", "given_name": "Troy", + "name": "Brown, Troy", "type": "personal", } }, @@ -33,6 +63,254 @@ def minimal_record(): # because DATACITE_ENABLED is True, this field is required "publisher": "Acme Inc", "resource_type": {"id": "image-photograph"}, - "title": "A Romans Story", + "title": "A Romans story", + }, + } + + +@pytest.fixture(scope="function") +def full_record_metadata(users): + """Full record data as dict coming from the external world.""" + return { + "pids": { + "doi": { + "identifier": "10.5281/inveniordm.1234", + "provider": "datacite", + "client": "inveniordm", + }, + "oai": { + "identifier": "oai:vvv.com:abcde-fghij", + "provider": "oai", + }, + }, + "uuid": "445aaacd-9de1-41ab-af52-25ab6cb93df7", + "version_id": "1", + "created": "2023-01-01", + "updated": "2023-01-02", + "metadata": { + "resource_type": {"id": "image-photograph"}, + "creators": [ + { + "person_or_org": { + "name": "Nielsen, Lars Holm", + "type": "personal", + "given_name": "Lars Holm", + "family_name": "Nielsen", + "identifiers": [ + { + "scheme": "orcid", + "identifier": "0000-0001-8135-3489", + } + ], + }, + "affiliations": [{"id": "cern"}, {"name": "free-text"}], + } + ], + "title": "InvenioRDM", + "additional_titles": [ + { + "title": "a research data management platform", + "type": {"id": "subtitle"}, + "lang": {"id": "eng"}, + } + ], + "publisher": "InvenioRDM", + "publication_date": "2018/2020-09", + "subjects": [ + {"id": "http://id.nlm.nih.gov/mesh/A-D000007"}, + {"subject": "custom"}, + ], + "contributors": [ + { + "person_or_org": { + "name": "Nielsen, Lars Holm", + "type": "personal", + "given_name": "Lars Holm", + "family_name": "Nielsen", + "identifiers": [ + { + "scheme": "orcid", + "identifier": "0000-0001-8135-3489", + } + ], + }, + "role": {"id": "other"}, + "affiliations": [{"id": "cern"}], + } + ], + "dates": [ + { + "date": "1939/1945", + "type": {"id": "other"}, + "description": "A date", + } + ], + "languages": [{"id": "dan"}, {"id": "eng"}], + "identifiers": [{"identifier": "1924MNRAS..84..308E", "scheme": "bibcode"}], + "related_identifiers": [ + { + "identifier": "10.1234/foo.bar", + "scheme": "doi", + "relation_type": {"id": "iscitedby"}, + "resource_type": {"id": "dataset"}, + } + ], + "sizes": ["11 pages"], + "formats": ["application/pdf"], + "version": "v1.0", + "rights": [ + { + "title": {"en": "A custom license"}, + "description": {"en": "A description"}, + "link": "https://customlicense.org/licenses/by/4.0/", + }, + {"id": "cc-by-4.0"}, + ], + "description": "

    A description

    with HTML tags

    ", + "additional_descriptions": [ + { + "description": "Bla bla bla", + "type": {"id": "methods"}, + "lang": {"id": "eng"}, + } + ], + "locations": { + "features": [ + { + "geometry": { + "type": "Point", + "coordinates": [-32.94682, -60.63932], + }, + "place": "test location place", + "description": "test location description", + "identifiers": [ + {"identifier": "12345abcde", "scheme": "wikidata"}, + {"identifier": "12345abcde", "scheme": "geonames"}, + ], + } + ] + }, + "funding": [ + { + "funder": { + "id": "00k4n6c32", + }, + "award": {"id": "00k4n6c32::755021"}, + } + ], + "references": [ + { + "reference": "Nielsen et al,..", + "identifier": "0000 0001 1456 7559", + "scheme": "isni", + } + ], + }, + "provenance": { + "created_by": {"user": users[0].id}, + "on_behalf_of": {"user": users[1].id}, }, + "access": { + "record": "public", + "files": "restricted", + "embargo": { + "active": True, + "until": "2131-01-01", + "reason": "Only for medical doctors.", + }, + }, + "files": { + "enabled": True, + "total_size": 1114324524355, + "count": 1, + "bucket": "81983514-22e5-473a-b521-24254bd5e049", + "default_preview": "big-dataset.zip", + "order": ["big-dataset.zip"], + "entries": [ + { + "checksum": "md5:234245234213421342", + "mimetype": "application/zip", + "size": 1114324524355, + "key": "big-dataset.zip", + "file_id": "445aaacd-9de1-41ab-af52-25ab6cb93df7", + "uuid": "445aaacd-9de1-41ab-af52-25ab6cb93df7", + "version_id": "1", + "created": "2023-01-01", + "updated": "2023-01-02", + "object_version_id": "1", + "metadata": {}, + "id": "445aaacd-9de1-41ab-af52-25ab6cb93df7", + } + ], + "meta": {"big-dataset.zip": {"description": "File containing the data."}}, + }, + "notes": ["Under investigation for copyright infringement."], } + + +@pytest.fixture(scope="function") +def build_draft_record_links(): + def _factory(record_id, base_url, ui_base_url): + return { + "self": f"{base_url}/records/{record_id}/draft", + "self_html": f"{ui_base_url}/uploads/{record_id}", + "self_iiif_manifest": f"{base_url}/iiif/draft:{record_id}/manifest", + "self_iiif_sequence": f"{base_url}/iiif/draft:{record_id}/sequence/default", + "files": f"{base_url}/records/{record_id}/draft/files", + "media_files": f"{base_url}/records/{record_id}/draft/media-files", + "archive": f"{base_url}/records/{record_id}/draft/files-archive", + "archive_media": ( + f"{base_url}/records/{record_id}/draft/media-files-archive" + ), + "record": f"{base_url}/records/{record_id}", + "record_html": f"{ui_base_url}/records/{record_id}", + "publish": f"{base_url}/records/{record_id}/draft/actions/publish", + "review": f"{base_url}/records/{record_id}/draft/review", + "versions": f"{base_url}/records/{record_id}/versions", + "access_links": f"{base_url}/records/{record_id}/access/links", + "access_grants": f"{base_url}/records/{record_id}/access/grants", + "access_users": f"{base_url}/records/{record_id}/access/users", + "access_groups": f"{base_url}/records/{record_id}/access/groups", + "access_request": f"{base_url}/records/{record_id}/access/request", + "access": f"{base_url}/records/{record_id}/access", + "reserve_doi": f"{base_url}/records/{record_id}/draft/pids/doi", + "communities": f"{base_url}/records/{record_id}/communities", + "communities-suggestions": ( + f"{base_url}/records/{record_id}/communities-suggestions" + ), + "requests": f"{base_url}/records/{record_id}/requests", + } + + return _factory + + +@pytest.fixture(scope="function") +def build_published_record_links(build_draft_record_links): + def _factory(record_id, base_url, ui_base_url, parent_id): + links = build_draft_record_links(record_id, base_url, ui_base_url) + links["archive"] = f"{base_url}/records/{record_id}/files-archive" + links["archive_media"] = f"{base_url}/records/{record_id}/media-files-archive" + links["doi"] = f"https://handle.stage.datacite.org/10.17613/{record_id}" + links["draft"] = f"{base_url}/records/{record_id}/draft" + links["files"] = f"{base_url}/records/{record_id}/files" + links["latest"] = f"{base_url}/records/{record_id}/versions/latest" + links["latest_html"] = f"{ui_base_url}/records/{record_id}/latest" + links["media_files"] = f"{base_url}/records/{record_id}/media-files" + del links["publish"] + del links["record"] + del links["record_html"] + links["parent"] = f"{base_url}/records/{parent_id}" + links["parent_doi"] = f"{ui_base_url}/doi/10.17613/{parent_id}" + links["parent_html"] = f"{ui_base_url}/records/{parent_id}" + del links["review"] + links["self"] = f"{base_url}/records/{record_id}" + links["self_html"] = f"{ui_base_url}/records/{record_id}" + links["self_doi"] = f"{ui_base_url}/doi/10.17613/{record_id}" + links["self_iiif_manifest"] = f"{base_url}/iiif/record:{record_id}/manifest" + links["self_iiif_sequence"] = ( + f"{base_url}/iiif/record:{record_id}/sequence/default" + ) + + return links + + return _factory diff --git a/site/tests/fixtures/search_provisioning.py b/site/tests/fixtures/search_provisioning.py index c3882970a..706373a79 100644 --- a/site/tests/fixtures/search_provisioning.py +++ b/site/tests/fixtures/search_provisioning.py @@ -1,7 +1,7 @@ import pytest from celery import shared_task -from invenio_queues.proxies import current_queues -from pprint import pformat + +# from pprint import pformat from typing import Optional diff --git a/site/tests/fixtures/users.py b/site/tests/fixtures/users.py index 462ce4af8..102070892 100644 --- a/site/tests/fixtures/users.py +++ b/site/tests/fixtures/users.py @@ -334,46 +334,9 @@ def client_with_login(requests_mock, app): def log_in_user( client, user: User, - new_remote_data: dict = {}, ): - saml_id = user.external_identifiers[0].id - token = os.getenv("COMMONS_API_TOKEN") - - # Mock remote data that's already in the user fixture. - mock_remote_data = { - "username": saml_id, - "email": user.email, - "name": user.user_profile.get("full_name", ""), - "first_name": user.user_profile.get("first_name", ""), - "last_name": user.user_profile.get("last_name", ""), - "institutional_affiliation": user.user_profile.get( - "affiliations", "" - ), # noqa: E501 - "orcid": user.user_profile.get("orcid", ""), - "preferred_language": user.user_profile.get( - "preferred_language", "" - ), # noqa: E501 - "time_zone": user.user_profile.get("time_zone", ""), - "groups": user.user_profile.get("groups", ""), - } - - # Mock adding any missing data from remote API call. - mock_remote_data.update(new_remote_data) - - # Mock the remote api call. - protocol = os.environ.get( - "INVENIO_COMMONS_API_REQUEST_PROTOCOL", "http" - ) # noqa: E501 - base_url = f"{protocol}://hcommons-dev.org/wp-json/commons/v1/users" - remote_url = f"{base_url}/{saml_id}" - mock_adapter = requests_mock.get( - remote_url, - json=mock_remote_data, - headers={"Authorization": f"Bearer {token}"}, - ) - login_user(user) login_user_via_session(client, email=user.email) - return client, mock_adapter + return client return log_in_user diff --git a/site/tests/helpers/sample_files/sample.pdf b/site/tests/helpers/sample_files/sample.pdf new file mode 100644 index 000000000..774c2ea70 Binary files /dev/null and b/site/tests/helpers/sample_files/sample.pdf differ diff --git a/site/tests/helpers/sample_records/__init__.py b/site/tests/helpers/sample_records/__init__.py index e9a6c7614..0a9d00db2 100644 --- a/site/tests/helpers/sample_records/__init__.py +++ b/site/tests/helpers/sample_records/__init__.py @@ -1,4 +1,4 @@ -from .sample583 import rec583 +from .sample_metadata_presentation_pdf import sample_metadata_presentation_pdf from .sample11451 import rec11451 from .sample16079 import rec16079 from .sample22625 import rec22625 diff --git a/site/tests/helpers/sample_records/basic.py b/site/tests/helpers/sample_records/basic.py deleted file mode 100644 index 512a1d7c3..000000000 --- a/site/tests/helpers/sample_records/basic.py +++ /dev/null @@ -1,218 +0,0 @@ -import pytest - - -@pytest.fixture(scope="function") -def minimal_record(): - """Minimal record data as dict coming from the external world.""" - return { - "pids": {}, - "access": { - "record": "public", - "files": "public", - }, - "files": { - "enabled": False, # Most tests don't care about files - }, - "metadata": { - "creators": [ - { - "person_or_org": { - "family_name": "Brown", - "given_name": "Troy", - "type": "personal", - } - }, - { - "person_or_org": { - "name": "Troy Inc.", - "type": "organizational", - }, - }, - ], - "publication_date": "2020-06-01", - # because DATACITE_ENABLED is True, this field is required - "publisher": "Acme Inc", - "resource_type": {"id": "image-photograph"}, - "title": "A Romans story", - }, - } - - -@pytest.fixture(scope="function") -def full_record(users): - """Full record data as dict coming from the external world.""" - return { - "pids": { - "doi": { - "identifier": "10.5281/inveniordm.1234", - "provider": "datacite", - "client": "inveniordm", - }, - "oai": { - "identifier": "oai:vvv.com:abcde-fghij", - "provider": "oai", - }, - }, - "uuid": "445aaacd-9de1-41ab-af52-25ab6cb93df7", - "version_id": "1", - "created": "2023-01-01", - "updated": "2023-01-02", - "metadata": { - "resource_type": {"id": "image-photograph"}, - "creators": [ - { - "person_or_org": { - "name": "Nielsen, Lars Holm", - "type": "personal", - "given_name": "Lars Holm", - "family_name": "Nielsen", - "identifiers": [ - { - "scheme": "orcid", - "identifier": "0000-0001-8135-3489", - } - ], - }, - "affiliations": [{"id": "cern"}, {"name": "free-text"}], - } - ], - "title": "InvenioRDM", - "additional_titles": [ - { - "title": "a research data management platform", - "type": {"id": "subtitle"}, - "lang": {"id": "eng"}, - } - ], - "publisher": "InvenioRDM", - "publication_date": "2018/2020-09", - "subjects": [ - {"id": "http://id.nlm.nih.gov/mesh/A-D000007"}, - {"subject": "custom"}, - ], - "contributors": [ - { - "person_or_org": { - "name": "Nielsen, Lars Holm", - "type": "personal", - "given_name": "Lars Holm", - "family_name": "Nielsen", - "identifiers": [ - { - "scheme": "orcid", - "identifier": "0000-0001-8135-3489", - } - ], - }, - "role": {"id": "other"}, - "affiliations": [{"id": "cern"}], - } - ], - "dates": [ - { - "date": "1939/1945", - "type": {"id": "other"}, - "description": "A date", - } - ], - "languages": [{"id": "dan"}, {"id": "eng"}], - "identifiers": [{"identifier": "1924MNRAS..84..308E", "scheme": "bibcode"}], - "related_identifiers": [ - { - "identifier": "10.1234/foo.bar", - "scheme": "doi", - "relation_type": {"id": "iscitedby"}, - "resource_type": {"id": "dataset"}, - } - ], - "sizes": ["11 pages"], - "formats": ["application/pdf"], - "version": "v1.0", - "rights": [ - { - "title": {"en": "A custom license"}, - "description": {"en": "A description"}, - "link": "https://customlicense.org/licenses/by/4.0/", - }, - {"id": "cc-by-4.0"}, - ], - "description": "

    A description

    with HTML tags

    ", - "additional_descriptions": [ - { - "description": "Bla bla bla", - "type": {"id": "methods"}, - "lang": {"id": "eng"}, - } - ], - "locations": { - "features": [ - { - "geometry": { - "type": "Point", - "coordinates": [-32.94682, -60.63932], - }, - "place": "test location place", - "description": "test location description", - "identifiers": [ - {"identifier": "12345abcde", "scheme": "wikidata"}, - {"identifier": "12345abcde", "scheme": "geonames"}, - ], - } - ] - }, - "funding": [ - { - "funder": { - "id": "00k4n6c32", - }, - "award": {"id": "00k4n6c32::755021"}, - } - ], - "references": [ - { - "reference": "Nielsen et al,..", - "identifier": "0000 0001 1456 7559", - "scheme": "isni", - } - ], - }, - "provenance": { - "created_by": {"user": users[0].id}, - "on_behalf_of": {"user": users[1].id}, - }, - "access": { - "record": "public", - "files": "restricted", - "embargo": { - "active": True, - "until": "2131-01-01", - "reason": "Only for medical doctors.", - }, - }, - "files": { - "enabled": True, - "total_size": 1114324524355, - "count": 1, - "bucket": "81983514-22e5-473a-b521-24254bd5e049", - "default_preview": "big-dataset.zip", - "order": ["big-dataset.zip"], - "entries": [ - { - "checksum": "md5:234245234213421342", - "mimetype": "application/zip", - "size": 1114324524355, - "key": "big-dataset.zip", - "file_id": "445aaacd-9de1-41ab-af52-25ab6cb93df7", - "uuid": "445aaacd-9de1-41ab-af52-25ab6cb93df7", - "version_id": "1", - "created": "2023-01-01", - "updated": "2023-01-02", - "object_version_id": "1", - "metadata": {}, - "id": "445aaacd-9de1-41ab-af52-25ab6cb93df7", - } - ], - "meta": {"big-dataset.zip": {"description": "File containing the data."}}, - }, - "notes": ["Under investigation for copyright infringement."], - } diff --git a/site/tests/helpers/sample_records/sample583.py b/site/tests/helpers/sample_records/sample_metadata_presentation_pdf.py similarity index 97% rename from site/tests/helpers/sample_records/sample583.py rename to site/tests/helpers/sample_records/sample_metadata_presentation_pdf.py index 4c9f1da81..d103b891a 100644 --- a/site/tests/helpers/sample_records/sample583.py +++ b/site/tests/helpers/sample_records/sample_metadata_presentation_pdf.py @@ -1,4 +1,4 @@ -rec583 = { +sample_metadata_presentation_pdf = { "input": { "created": "2016-02-29T13:11:39Z", "custom_fields": { @@ -13,9 +13,7 @@ "hclegacy:groups_for_deposit": [ { "group_identifier": "69", - "group_name": ( - "HEP Part-Time and Contingent Faculty Issues" - ), + "group_name": ("HEP Part-Time and Contingent Faculty Issues"), }, { "group_identifier": "47", @@ -69,8 +67,7 @@ "dates": "10 January 2016", "place": "Marriott Hotel, Austin, Texas", "title": ( - "131st Annual Convention of the Modern Languages " - "Association" + "131st Annual Convention of the Modern Languages " "Association" ), }, }, @@ -192,9 +189,7 @@ "id": "arr", "icon": "copyright", "props": { - "url": ( - "https://en.wikipedia.org/wiki/All_rights_reserved" - ) + "url": ("https://en.wikipedia.org/wiki/All_rights_reserved") }, "title": {"en": "All Rights Reserved"}, } @@ -247,9 +242,7 @@ "hclegacy:groups_for_deposit": [ { "group_identifier": "69", - "group_name": ( - "HEP Part-Time and Contingent Faculty " "Issues" - ), + "group_name": ("HEP Part-Time and Contingent Faculty " "Issues"), }, { "group_identifier": "47", @@ -303,8 +296,7 @@ "dates": "10 January 2016", "place": "Marriott Hotel, Austin, Texas", "title": ( - "131st Annual Convention of the Modern Languages " - "Association" + "131st Annual Convention of the Modern Languages " "Association" ), }, }, @@ -463,9 +455,7 @@ "hclegacy:groups_for_deposit": [ { "group_identifier": "69", - "group_name": ( - "HEP Part-Time and Contingent Faculty Issues" - ), + "group_name": ("HEP Part-Time and Contingent Faculty Issues"), }, { "group_identifier": "47", @@ -519,8 +509,7 @@ "dates": "10 January 2016", "place": "Marriott Hotel, Austin, Texas", "title": ( - "131st Annual Convention of the Modern Languages " - "Association" + "131st Annual Convention of the Modern Languages " "Association" ), }, }, @@ -646,9 +635,7 @@ "id": "arr", "props": { "scheme": "spdx", - "url": ( - "https://arr.org/licenses/" "all-rights-reserved" - ), + "url": ("https://arr.org/licenses/" "all-rights-reserved"), }, "title": {"en": "All Rights Reserved"}, } diff --git a/templates/semantic-ui/invenio_app_rdm/header.html b/templates/semantic-ui/invenio_app_rdm/header.html index 4fec197a5..f5cb7cfd6 100644 --- a/templates/semantic-ui/invenio_app_rdm/header.html +++ b/templates/semantic-ui/invenio_app_rdm/header.html @@ -47,9 +47,9 @@ {%- block site_banner %} - + {# #} - + {%- endblock site_banner %}