diff --git a/CHANGE-LOG.md b/CHANGE-LOG.md index 5854c765..cb8d4da2 100644 --- a/CHANGE-LOG.md +++ b/CHANGE-LOG.md @@ -1,5 +1,131 @@ # OpenStudyBuilder (OSB) Commits changelog +## V 2.4 + +New Features and Enhancements +============ + +### Fixes and Enhancements + +- Corrections and improvements to Library -> Concepts -> Compounds and Studies -> Define Study -> Study Interventions. +- Simplified model for Activity Group and Subgroup. The link between subgroup and group has been removed. Activities are now free to choose any combination of group and subgroup. This change was made to make it easier to maintain the Activity library. +- Define Study, Study Structure, Design Matrix now support definition of SDTM transition rule. +- Adding two new flag attributes on relationship from Activity Instance Class to Activity Item Class for indicating if this is to be a default linkage or an additional optional selection. +- Improvements on field labels in activity instance wizard stepper. +- User guide added in documentation portal for neodash CRF Library Version report. +- Support for USDM version 4.0. Note only for a partial scope, additional scope coverage will be gradually added in subsequent releases. +- In 'Define Study/Study Structure/Study Visits page' the Epoch column now stay visible when 'edit mode' is selected and the column selections doesn't reset when page is changed. +- Removes duplicated query parameter to /concepts/activities/activities endpoints that splits one Activity object if it contains more than 1 activity grouping into separate instances. The same can be obtained by using already existing group_by_groupings query parameter. +- Activity requests (placeholders) can now be exchanged for multiple activities. +- The wizard stepper for creating activity instances now includes enhanced form restriction messages, more user-friendly notifications, and improved stability across the different phases of the form. +- Implement openapi.json changes to use unique titles for classes to enable class generators using titles as keys. + +### New Feature + +- It is now possible to onboard interventional device studies. +- The users will be able to set up studies with overlapping Main and Extension parts now as well as split Main and Extension SoAs in protocol. +- Study complexity score number added to the Detailed SoA page. +- Consumer API improved to support extract of masked audit trail data for process data mining. +- Study Data Specification, Study Activity Instances now support linkage to study data suppliers including definition of origin type and origin source. +- 'EU PAS number' added to registry identifiers, which will allow to onboard relevant (in-house) NIS and DAS studies. +- Initial pilot implementation of linking CRF elements to the Activity Instances and Activity Items, as the biomedical concepts in OSB. This is to support creation of initial CRF library content with Activity Concepts linkage. The implementation will continue with usability improvements in coming releases. +- The Study Activity Instances table now includes an edit mode, enabling batch add/modify of Important flag, Baseline visits and Data Supplier details (Name, Origin Type and Source) directly in the table. +- Added feature flags to control visibility of Study Data Suppliers page and create button in the UI. +- Added feature flag configuration entries (disabled in production, enabled in E2E test environment). +- Improved navigation within the collapsed visit functionality: + - Guidance on why some visits cannot be collapsed + - That collapsed visits groups cannot be edited + - Handling of footnotes in collapsed visit groups and + - Possibility to collapse non-consecutive visits in a list view (relevant for large SoAs) + +### Performance Improvements + +Faster editing of Study Design Matrix on 'Study Structure > Design Matrix' page + +### End-to-End Automated test enhancements + +- Various code improvements to ensure easier maintenance and overall tests stability. +- Library > Concepts > Activities > Activity Instances: Tests refactorization to support changes for NumericFindings Activity Instance Class. +- Library > Concepts > Activities > Activity Instances: Defined and Implemented test for checking requested activities visibility in the Wizard Stepper. +- Library > Concepts > Activities > Activity Instances: Defined and Implemented test for checking visibility of selected activity in the Wizard Stepper. +- Library > Concepts > Activities: Adjusted tests to removal of group-subgroup linkage. +- Studies > Define Study > Study Structure > Design Matrix: Defined and Implemented tests for checking transition rules. +- Studies > Define Study > Study Structure > Study Visits: Defined and Implemented tests for multiple visits on same day. +- Studies > Define Study > Study Properties > Study Type: Updated tests for Study Development Stage Classification +- Studies > Define Study > Study Activities: Defined and Implemented tests for exchaning study activity and activity placeholder. +- Studies > Define Study > Study Activities > Protocol SoA: Defined and Implemented tests for splitting the view. +- Studies > Define Study > Study Activities > Detailed SoA: Updated tests for collapsing visits. +- Studies > Define Study > Study Activities > Detailed SoA: Defined and Implemented tests for Complexity Score. +- Studies > Define Study > Study Activities > Detailed SoA: Defined and Implemented tests for verification of placeholders visibility. +- Studies > Define Study > Data Specifications > Study Activity Instances: Defined and Implemented tests for verification of placeholders visibility. +- Studies > Define Study > Data Specifications > Study Activity Instances: Defined and Implemented tests for edition mode. +- Studies > Define Study > Data Specifications > Study Activity Instances: Defined and Implemented tests for assignment of Data Suppliers. + +Solved Bugs +============ + +### CDISC + + **Import** + +- Update CDISC import script to use author_id for user identification + +### Library + + **Concepts -> Activities -> Activities** + +- Missing loading indication when switching between statuses +- Search field value cleared when switching between status filter + + **Concepts -> Activities -> Activity Instance Classes -> Overview Page** + +- Search refresh issue + + **Concepts -> Activities -> Activity Overview Page** + +- Duplicated items in Activity groupings table + + **Data Collection Standards -> CRF Builder -> Items** + +- Edit Item - Alias - Rows per page - All +- Removed term are still selectable at the Item level + + **Overview Pages -> Study Structures** + +- Filters values are not sorted +- Poor performance on filtering + +### Studies + + **Define Study -> Data Specification** + + - Sorting Activity Instance Column Returns an Error + + **Define Study -> Study Activities -> Schedule of Activities** + +- Visit grouping in locked protocol SoA + + **Define Study -> Study Properties -> Study Attributes** + +- Error thrown when trying to edit attributes of study subpart + + **Define Study -> Study Structure -> Design Matrix** + +- Incorrect Pagination + + **Define Study > Study Structure > Study Epochs** + +- Study Epoch API is not robust against changes in CDISC codelists + + **Manage Study > Study Core Attributes** + +- Not possible to edit study to update study number or project + + **Study List -> Study List** + +- Blocked save button, when trying to add a study with already existing number + + ## V 2.3 New Features and Enhancements diff --git a/README.md b/README.md index de8035dc..43f1d3ad 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ OpenStudyBuilder is the open source version of the internal StudyBuilder solutio For further information on the OpenStudyBuilder solution, please refer to the [OpenStudyBuilder homepage](https://openstudybuilder.com). +## Related Repositories + +The following repositories are related to OpenStudyBuilder + +- [OpenStudyBuilder-Word-Add-In](https://github.com/NovoNordisk-OpenSource/openstudybuilder-word-addin) +- [OpenStudyBuilder-accelerators](https://github.com/NovoNordisk-OpenSource/openstudybuilder-accelerators) + # Introduction diff --git a/clinical-mdr-api/.env.example b/clinical-mdr-api/.env.example index 3d6c0e87..27184566 100644 --- a/clinical-mdr-api/.env.example +++ b/clinical-mdr-api/.env.example @@ -1,5 +1,5 @@ # Application Settings -APP_NAME="Clinical MDR API" +APP_NAME="OpenStudyBuilder API" UVICORN_ROOT_PATH="/" APP_DEBUG=false COLOR_LOGS=true @@ -65,7 +65,7 @@ SCHEMATHESIS_HOOKS="clinical_mdr_api.hooks.schemathesis_hooks" # Third-party Integrations MS_GRAPH_INTEGRATION_ENABLED=true -MS_GRAPH_GROUPS_QUERY="\$filter=startsWith(displayName, 'StudyBuilder')" +MS_GRAPH_GROUPS_QUERY="\$filter=startsWith(displayName, 'OpenStudyBuilder')" # gzip API responses (Content-Encoding: gzip) GZIP_RESPONSE_MIN_SIZE=1000 diff --git a/clinical-mdr-api/.git-hooks/commit-msg b/clinical-mdr-api/.git-hooks/commit-msg deleted file mode 100755 index 82dfa796..00000000 --- a/clinical-mdr-api/.git-hooks/commit-msg +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -# -# Script to check the commit log message. -# Called by "git commit" with one argument, the name of the file -# that has the commit message. The hook should exit with non-zero -# status after issuing an appropriate message if it wants to stop the -# commit. The hook is allowed to edit the commit message file. - -# Check for linked work item in commit message. -# Tasks, user stories, features and enablers can all be referenced with the same syntax: -# #XYZ where XYZ is the ID number of the item in DevOps. - -msg=$(cat "$1") # Grab the commit message from git - -# Check if the commit message matches the right-hand regular expression (looking for # followed by one or more digits) -if [[ $msg =~ \#[0-9]+ ]]; then - exit 0 -fi - -echo "Commit hook failed - no work item linked." -echo "Please make sure to add a work item via the hash character '#' in your commit message. E.g. 'Added something; #223941'" -exit 1 diff --git a/clinical-mdr-api/.git-hooks/install-git-hooks.sh b/clinical-mdr-api/.git-hooks/install-git-hooks.sh old mode 100644 new mode 100755 diff --git a/clinical-mdr-api/.github/agents/code-generator-consumer-api.agent.md b/clinical-mdr-api/.github/agents/code-generator-consumer-api.agent.md new file mode 100644 index 00000000..e6cb2bc9 --- /dev/null +++ b/clinical-mdr-api/.github/agents/code-generator-consumer-api.agent.md @@ -0,0 +1,130 @@ +--- +name: Consumer API Code Generator +description: Code generation agent for the Consumer API (reads/writes under consumer_api/, follows existing patterns, and creates tests). +--- + +# Code Generation Agent + +You are an expert software engineer specialized in reading existing codebases and generating new code that follows established patterns and conventions. + +## Capabilities + +- Analyze existing code structure, patterns, and conventions +- Generate new code that matches the existing codebase style +- Understand multiple programming languages and frameworks +- Follow SOLID principles and best practices +- Create tests alongside implementation code + +## Instructions + +When asked to generate code: + +1. **Analyze the context**: Read relevant existing files to understand: + - Code structure and architecture + - Naming conventions + - Error handling patterns + - Type annotations and documentation style + - Testing approaches + +2. **Follow existing patterns**: Match the style and patterns found in: + - Similar classes/functions in the codebase + - Related modules and components + - Test files for similar features + +3. **Generate complete code**: Provide: + - Full implementation with proper imports + - Type hints and documentation + - Error handling + - Corresponding test files if applicable + +4. **Explain key decisions**: Briefly note: + - Why certain patterns were chosen + - Any assumptions made + - Suggestions for improvements if applicable + +## Project Structure Awareness + +When generating code, ensure that: +- New files are placed in appropriate directories +- Module imports reflect the project structure + +### File structure +- `consumer_api` - this is the main codebase for the Consumer API. You READ from and WRITE code to this directory. + - `consumer_api/v1` - this is where the API routes are defined. You READ from and WRITE code to this directory. + - `consumer_api/tests` - this is the test codebase for the Consumer API. You READ from and WRITE code to this directory. + - `consumer_api/requirements` - this is where the user requirements specifications, functional specifications and traceability between requirements and test are defined. You READ from and WRITE code to this directory. + + +## Response Format + +When generating code, structure your response as: + +1. Brief summary of what's being generated +2. Code blocks with file paths +3. Short explanation of key implementation choices +4. Any follow-up actions needed (migrations, config updates, etc.) + +## Examples + +### Example 1: Add API Endpoint + +**User prompt**: "Add endpoint to export audit trail as CSV" + +**Your response should**: +- Analyze existing endpoints structure +- Match routing patterns +- Follow authentication/authorization patterns +- Reuse existing service methods where possible +- Include proper OpenAPI documentation +- Generate corresponding tests + +## Tools Usage + +- Use `semantic_search` to find similar implementations +- Use `grep_search` to locate patterns and conventions +- Use `read_file` to understand full context of related files +- Use `list_code_usages` to see how existing functions are used + +## Best Practices + +- Always validate inputs +- Use proper type hints +- Add docstrings for public methods +- Handle edge cases and errors +- Follow the principle of least surprise +- Prefer composition over inheritance +- Keep functions focused and testable + +## Language-Specific Guidelines + +### Python +- Follow PEP 8 style guide +- Use type hints (PEP 484) +- Prefer list comprehensions for simple transformations +- Use dataclasses or Pydantic models for structured data +- Handle exceptions appropriately + +### Cypher (Neo4j) +- Use parameterized queries +- Optimize for performance (avoid cartesian products) +- Use APOC functions when appropriate +- Add indexes for frequently queried properties + +## Constraints + +- Never introduce security vulnerabilities +- Don't break existing functionality +- Maintain backward compatibility unless explicitly asked to change +- Don't add unnecessary dependencies +- Keep generated code testable + +## When Uncertain + +If the request is ambiguous: +1. Search for similar existing implementations +2. Infer the most likely intent based on codebase patterns +3. Proceed with implementation +4. Note any assumptions made + +Remember: Your goal is to generate production-ready code that seamlessly integrates with the existing codebase. + diff --git a/clinical-mdr-api/.github/agents/test-specialist-integration-api.agent.md b/clinical-mdr-api/.github/agents/test-specialist-integration-api.agent.md new file mode 100644 index 00000000..f3a6fdde --- /dev/null +++ b/clinical-mdr-api/.github/agents/test-specialist-integration-api.agent.md @@ -0,0 +1,79 @@ +--- +name: Intergration API Test Specialist +description: API testing agent for creating and updating integration tests for the FastAPI service backed by Neo4j, based on staged git changes. +--- + +# Test Specialist Agent (Integration API) + +You are an expert Python test engineer focused on integration tests for a FastAPI service backed by Neo4j via neomodel. + +Your mission: read the *staged* changes in git, determine what behavior has changed or is newly introduced, and then create/update integration tests accordingly. + +## Non-Negotiable Constraints + +- **Do not change existing functionality.** + - You may only add or update tests. + - Do not modify application code, API routes, service logic, schemas, migrations, configs, Docker files, or any production modules. +- **Write changes ONLY in**: `./clinical_mdr_api/tests/integration/api` + - You may create new test files/subfolders under that directory. + - You may update existing tests under that directory. + - Do not touch unit tests or other test folders. +- **Drive work from staged diffs only**. + - Always start by inspecting `git diff --staged` (and `git status --porcelain` as needed). + - If there are no staged changes, do not invent work; ask the user to stage files. + +## Workflow + +1. **Inspect staged changes** + - Read the staged diff (`git diff --staged`). + - Identify: + - endpoints affected (path/method) + - request/response models changed + - authentication/authorization behavior changes + - validation changes (status codes, error messages, required fields) + - Neo4j/neomodel behavior changes that affect API results + +2. **Locate existing test coverage** + - Search within `clinical_mdr_api/tests/integration/api` for existing tests for the same route or feature. + - Prefer updating existing tests over creating new ones when it keeps intent clearer. + +3. **Add/Update integration tests (only)** + - Use the existing test patterns in this repo (pytest style, fixtures, naming). + - Ensure tests verify externally observable behavior: + - HTTP status codes + - response shape and key fields + - error handling for invalid inputs + - permission checks when applicable + - Avoid asserting brittle internals (exact query strings, internal IDs, ordering) unless required. + +4. **Keep tests deterministic** + - Avoid reliance on wall-clock time, random order, or shared global state. + - Use existing fixtures for DB setup/cleanup. + - If tests require Neo4j state, follow existing patterns for creating and cleaning entities. + +5. **Run focused tests (when possible)** + - Prefer running the smallest set relevant to the change (e.g., a single file or folder). + - If the repository uses markers for integration tests, respect them. + +## What to Test (Common Cases) + +- **New/changed endpoint**: happy path + at least one validation/error path. +- **Schema change**: response contract and required/optional fields. +- **Auth changes**: unauthorized/forbidden cases in addition to authorized. +- **Bug fix**: regression test that fails on the old behavior and passes now. + +## Quality Bar + +- Each test should clearly state intent via name and assertions. +- Prefer small, single-purpose tests. +- Don’t duplicate coverage; add tests where behavior is newly introduced or previously untested. + +## Output Expectations + +When you respond: +- Summarize what staged changes imply for behavior. +- List the tests you added/updated and why. +- Point to the files you changed under `clinical_mdr_api/tests/integration/api`. + +Remember: you are a *test-only* agent constrained to `clinical_mdr_api/tests/integration/api`. + diff --git a/clinical-mdr-api/.github/skills/endpoint-standards-helper/SKILL.md b/clinical-mdr-api/.github/skills/endpoint-standards-helper/SKILL.md new file mode 100644 index 00000000..8af74ab9 --- /dev/null +++ b/clinical-mdr-api/.github/skills/endpoint-standards-helper/SKILL.md @@ -0,0 +1,40 @@ +--- +name: ensure-endpoint-name-adherence-to-restful +description: Ensures API endpoint names follow RESTful conventions. Use when reviewing or renaming endpoints to be resource-oriented (nouns), consistently pluralized, hierarchical, and aligned with standard HTTP methods. +--- + +# Ensure endpoint names follow RESTful conventions + +## Use this when reviewing or renaming endpoints +1. Run `git diff --staged` to list changed routes. +2. For each changed/renamed endpoint, verify naming rules below and update code or docs accordingly. +3. Produce a commit message that follows the recommended format (see "Commit message format"). + +## RESTful naming rules (quick checklist) +- Resource-oriented nouns, not verbs: use /users, not /getUsers or /createUser. +- Prefer plural resource names: /orders, /users/{user_id}/orders. +- Hierarchical, relationship-driven paths: /projects/{id}/tasks. +- Map actions to HTTP methods: + - GET — read + - POST — create + - PUT/PATCH — update + - DELETE — remove +- Path parameters for identities: /items/{item_id}, not /items?id=... +- Use query parameters for filtering, sorting, pagination: ?page_number=2&page_size=50 +- Consistent casing (prefer kebab-case across the API). +- Avoid RPC-style paths and action verbs in path segments. +- Keep URLs stable; avoid breaking changes when possible. + +## Commit message format for endpoint renames/changes +- Summary (≤50 chars): concise change (e.g., "Rename user endpoints to RESTful nouns") +- Body: Explain what changed and why (one paragraph). List all renamed or added endpoints and affected components (routes, controllers, docs). +- Footer: Include migration notes or client impacts if breaking. +- End the message with the single word: + Done + +## Examples +- Before: POST /create-user -> After: POST /users +- Before: GET /user/{id}/orders -> After: GET /users/{user_id}/orders + +Follow these rules when updating code, tests, and documentation so endpoint names remain consistent and predictable. + diff --git a/clinical-mdr-api/.gitignore b/clinical-mdr-api/.gitignore index 67d31168..40b677ba 100644 --- a/clinical-mdr-api/.gitignore +++ b/clinical-mdr-api/.gitignore @@ -29,5 +29,4 @@ linting_report.txt /reports /consumer_api/reports traceability.html -Consumer_API_Traceability.html -.github \ No newline at end of file +Consumer_API_Traceability.html \ No newline at end of file diff --git a/clinical-mdr-api/.pre-commit-config.yaml b/clinical-mdr-api/.pre-commit-config.yaml index bbdac950..e8752393 100644 --- a/clinical-mdr-api/.pre-commit-config.yaml +++ b/clinical-mdr-api/.pre-commit-config.yaml @@ -14,5 +14,33 @@ repos: entry: ./.git-hooks/check-openapi-changes.sh language: system require_serial: true + - repo: local + hooks: + - id: mypy + name: mypy + entry: pipenv + language: system + types: [python] + pass_filenames: false + args: + [ + "run", + "mypy", + ] + - repo: local + hooks: + - id: pylint + name: pylint + entry: pipenv + language: system + types: [python] + require_serial: true + args: + [ + "run", + "pylint", + "-rn", # Only display messages + "-sn", # Don't display the score + ] default_language_version: python: python3.13 diff --git a/clinical-mdr-api/LICENSE.md b/clinical-mdr-api/LICENSE.md index e2cef53d..1865a488 100644 --- a/clinical-mdr-api/LICENSE.md +++ b/clinical-mdr-api/LICENSE.md @@ -1,6 +1,6 @@ ## NOTICE -This license information is applicable to all files for the component Clinical MDR API located in this folder (clinical-mdr-api). +This license information is applicable to all files for the component OpenStudyBuilder API located in this folder (clinical-mdr-api). The GPLv3 license is applicable to all, but the Swagger API documentation (openapi.json). The Swagger API documentation is using the MIT license (see header of this file). diff --git a/clinical-mdr-api/Pipfile b/clinical-mdr-api/Pipfile index 6047cc17..463465c0 100644 --- a/clinical-mdr-api/Pipfile +++ b/clinical-mdr-api/Pipfile @@ -30,7 +30,7 @@ weasyprint = "~=63.0" cffi = "~=1.17.1" asyncache = "~=0.3.1" cachetools = "~=5.5.0" -usdm = "==0.59.0" +usdm = "==0.65.0" annotated-types = "~=0.6.0" jinja2 = "*" nh3 = "~=0.2.21" diff --git a/clinical-mdr-api/Pipfile.lock b/clinical-mdr-api/Pipfile.lock index f6ce5f50..f3933495 100644 --- a/clinical-mdr-api/Pipfile.lock +++ b/clinical-mdr-api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "af8b0107120f8e39bf75f1bd7048e55ed485ad0b130bda7ea7b15647e03856b3" + "sha256": "413e863b10634d2e0fc6f46d7b239a2e36157b167a008e3e5614b74b91d07076" }, "pipfile-spec": 6, "requires": { @@ -27,11 +27,11 @@ }, "anyio": { "hashes": [ - "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", - "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4" + "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", + "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb" ], "markers": "python_version >= '3.9'", - "version": "==4.11.0" + "version": "==4.12.0" }, "asyncache": { "hashes": [ @@ -86,140 +86,108 @@ }, "brotli": { "hashes": [ - "sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208", - "sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48", - "sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354", - "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", - "sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a", - "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", - "sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c", - "sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088", - "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", - "sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a", - "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", - "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", - "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", - "sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438", - "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", - "sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b", - "sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b", - "sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68", - "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", - "sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d", - "sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943", - "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", - "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", - "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", - "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", - "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", - "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", - "sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0", - "sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547", - "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", - "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", - "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", - "sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a", - "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", - "sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112", - "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", - "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", - "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", - "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", - "sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95", - "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", - "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", - "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", - "sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38", - "sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914", - "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", - "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", - "sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7", - "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", - "sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c", - "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", - "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", - "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", - "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", - "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", - "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", - "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", - "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", - "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", - "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", - "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", - "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", - "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", - "sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97", - "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", - "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", - "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", - "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", - "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", - "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", - "sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74", - "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", - "sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60", - "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", - "sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1", - "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", - "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", - "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", - "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", - "sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460", - "sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751", - "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", - "sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2", - "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", - "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", - "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", - "sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75", - "sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5", - "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", - "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", - "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", - "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", - "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", - "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", - "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", - "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", - "sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01", - "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", - "sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619", - "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", - "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", - "sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579", - "sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84", - "sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7", - "sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c", - "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", - "sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52", - "sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b", - "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", - "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", - "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", - "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", - "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", - "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", - "sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2", - "sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3", - "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", - "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", - "sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643", - "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", - "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", - "sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985", - "sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596", - "sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2", - "sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064" - ], - "version": "==1.1.0" - }, - "bs4": { - "hashes": [ - "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", - "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc" + "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", + "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", + "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4", + "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", + "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", + "sha256:14ef29fc5f310d34fc7696426071067462c9292ed98b5ff5a27ac70a200e5470", + "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", + "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", + "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", + "sha256:1b71754d5b6eda54d16fbbed7fce2d8bc6c052a1b91a35c320247946ee103502", + "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", + "sha256:1e68cdf321ad05797ee41d1d09169e09d40fdf51a725bb148bff892ce04583d7", + "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", + "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", + "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17", + "sha256:29b7e6716ee4ea0c59e3b241f682204105f7da084d6254ec61886508efeb43bc", + "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", + "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971", + "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", + "sha256:3173e1e57cebb6d1de186e46b5680afbd82fd4301d7b2465beebe83ed317066d", + "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", + "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", + "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", + "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", + "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", + "sha256:3ebe801e0f4e56d17cd386ca6600573e3706ce1845376307f5d2cbd32149b69a", + "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", + "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", + "sha256:465a0d012b3d3e4f1d6146ea019b5c11e3e87f03d1676da1cc3833462e672fb0", + "sha256:4735a10f738cb5516905a121f32b24ce196ab82cfc1e4ba2e3ad1b371085fd46", + "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", + "sha256:50b1b799f45da91292ffaa21a473ab3a3054fa78560e8ff67082a185274431c8", + "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", + "sha256:5732eff8973dd995549a18ecbd8acd692ac611c5c0bb3f59fa3541ae27b33be3", + "sha256:598e88c736f63a0efec8363f9eb34e5b5536b7b6b1821e401afcb501d881f59a", + "sha256:640fe199048f24c474ec6f3eae67c48d286de12911110437a36a87d7c89573a6", + "sha256:66c02c187ad250513c2f4fce973ef402d22f80e0adce734ee4e4efd657b6cb64", + "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", + "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", + "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", + "sha256:71a66c1c9be66595d628467401d5976158c97888c2c9379c034e1e2312c5b4f5", + "sha256:7274942e69b17f9cef76691bcf38f2b2d4c8a5f5dba6ec10958363dcb3308a0a", + "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", + "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", + "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", + "sha256:7ad8cec81f34edf44a1c6a7edf28e7b7806dfb8886e371d95dcf789ccd4e4982", + "sha256:7e9053f5fb4e0dfab89243079b3e217f2aea4085e4d58c5c06115fc34823707f", + "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b", + "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", + "sha256:82676c2781ecf0ab23833796062786db04648b7aae8be139f6b8065e5e7b1518", + "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", + "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", + "sha256:865cedc7c7c303df5fad14a57bc5db1d4f4f9b2b4d0a7523ddd206f00c121a16", + "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a", + "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", + "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1", + "sha256:92edab1e2fd6cd5ca605f57d4545b6599ced5dea0fd90b2bcdf8b247a12bd190", + "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", + "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e", + "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", + "sha256:96fbe82a58cdb2f872fa5d87dedc8477a12993626c446de794ea025bbda625ea", + "sha256:99cfa69813d79492f0e5d52a20fd18395bc82e671d5d40bd5a91d13e75e468e8", + "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", + "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", + "sha256:9fe11467c42c133f38d42289d0861b6b4f9da31e8087ca2c0d7ebb4543625526", + "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", + "sha256:a387225a67f619bf16bd504c37655930f910eb03675730fc2ad69d3d8b5e7e92", + "sha256:a56ef534b66a749759ebd091c19c03ef81eb8cd96f0d1d16b59127eaf1b97a12", + "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", + "sha256:ac27a70bda257ae3f380ec8310b0a06680236bea547756c277b5dfe55a2452a8", + "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", + "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", + "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", + "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", + "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", + "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", + "sha256:b908d1a7b28bc72dfb743be0d4d3f8931f8309f810af66c906ae6cd4127c93cb", + "sha256:ba76177fd318ab7b3b9bf6522be5e84c2ae798754b6cc028665490f6e66b5533", + "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8", + "sha256:c0d6770111d1879881432f81c369de5cde6e9467be7c682a983747ec800544e2", + "sha256:c16ab1ef7bb55651f5836e8e62db1f711d55b82ea08c3b8083ff037157171a69", + "sha256:c1702888c9f3383cc2f09eb3e88b8babf5965a54afb79649458ec7c3c7a63e96", + "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49", + "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", + "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", + "sha256:d206a36b4140fbb5373bf1eb73fb9de589bb06afd0d22376de23c5e91d0ab35f", + "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", + "sha256:d8c05b1dfb61af28ef37624385b0029df902ca896a639881f594060b30ffc9a7", + "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", + "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", + "sha256:e80a28f2b150774844c8b454dd288be90d76ba6109670fe33d7ff54d96eb5cb8", + "sha256:e813da3d2d865e9793ef681d3a6b66fa4b7c19244a45b817d0cceda67e615990", + "sha256:e85190da223337a6b7431d92c799fca3e2982abd44e7b8dec69938dcc81c8e9e", + "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", + "sha256:eda5a6d042c698e28bda2507a89b16555b9aa954ef1d750e1c20473481aff675", + "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", + "sha256:f16dace5e4d3596eaeb8af334b4d2c820d34b8278da633ce4a00020b2eac981c", + "sha256:f8d635cafbbb0c61327f942df2e3f474dde1cff16c3cd0580564774eaba1ee13", + "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", + "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d" ], - "version": "==0.0.2" + "version": "==1.2.0" }, "cachetools": { "hashes": [ @@ -232,11 +200,11 @@ }, "certifi": { "hashes": [ - "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", - "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43" + "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", + "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316" ], "markers": "python_version >= '3.7'", - "version": "==2025.10.5" + "version": "==2025.11.12" }, "cffi": { "hashes": [ @@ -433,11 +401,11 @@ }, "click": { "hashes": [ - "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", - "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4" + "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", + "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6" ], "markers": "python_version >= '3.10'", - "version": "==8.3.0" + "version": "==8.3.1" }, "colour": { "hashes": [ @@ -578,67 +546,59 @@ "woff" ], "hashes": [ - "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", - "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", - "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", - "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856", - "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2", - "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", - "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", - "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", - "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", - "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", - "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", - "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", - "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", - "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854", - "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", - "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", - "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", - "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", - "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", - "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7", - "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", - "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", - "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", - "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", - "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217", - "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", - "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", - "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", - "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", - "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", - "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", - "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a", - "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", - "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", - "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", - "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", - "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", - "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036", - "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", - "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", - "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", - "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", - "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", - "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", - "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da", - "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", - "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", - "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", - "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", - "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", - "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", - "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", - "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", - "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", - "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", - "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", - "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", - "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa" + "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", + "sha256:02bdf8e04d1a70476564b8640380f04bb4ac74edc1fc71f1bacb840b3e398ee9", + "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", + "sha256:13e3e20a5463bfeb77b3557d04b30bd6a96a6bb5c15c7b2e7908903e69d437a0", + "sha256:14a290c5c93fcab76b7f451e6a4b7721b712d90b3b5ed6908f1abcf794e90d6d", + "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", + "sha256:1cfa2eb9bae650e58f0e8ad53c49d19a844d6034d6b259f30f197238abc1ccee", + "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", + "sha256:2cb5e45a824ce14b90510024d0d39dae51bd4fbb54c42a9334ea8c8cf4d95cbe", + "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", + "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", + "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", + "sha256:3b2065d94e5d63aafc2591c8b6ccbdb511001d9619f1bca8ad39b745ebeb5efa", + "sha256:4238120002e68296d55e091411c09eab94e111c8ce64716d17df53fd0eb3bb3d", + "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", + "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", + "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", + "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", + "sha256:627216062d90ab0d98215176d8b9562c4dd5b61271d35f130bcd30f6a8aaa33a", + "sha256:63c7125d31abe3e61d7bb917329b5543c5b3448db95f24081a13aaf064360fc8", + "sha256:6781e7a4bb010be1cd69a29927b0305c86b843395f2613bdabe115f7d6ea7f34", + "sha256:67d841aa272be5500de7f447c40d1d8452783af33b4c3599899319f6ef9ad3c1", + "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", + "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", + "sha256:6e5ca8c62efdec7972dfdfd454415c4db49b89aeaefaaacada432f3b7eea9866", + "sha256:70e2a0c0182ee75e493ef33061bfebf140ea57e035481d2f95aa03b66c7a0e05", + "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", + "sha256:7b446623c9cd5f14a59493818eaa80255eec2468c27d2c01b56e05357c263195", + "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", + "sha256:9064b0f55b947e929ac669af5311ab1f26f750214db6dd9a0c97e091e918f486", + "sha256:96dfc9bc1f2302224e48e6ee37e656eddbab810b724b52e9d9c13a57a6abad01", + "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", + "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", + "sha256:a5c5fff72bf31b0e558ed085e4fd7ed96eb85881404ecc39ed2a779e7cf724eb", + "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", + "sha256:adbb4ecee1a779469a77377bbe490565effe8fce6fb2e6f95f064de58f8bac85", + "sha256:b2b734d8391afe3c682320840c8191de9bd24e7eb85768dd4dc06ed1b63dbb1b", + "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", + "sha256:b6ceac262cc62bec01b3bb59abccf41b24ef6580869e306a4e88b7e56bb4bdda", + "sha256:ba774b8cbd8754f54b8eb58124e8bd45f736b2743325ab1a5229698942b9b433", + "sha256:c53b47834ae41e8e4829171cc44fec0fdf125545a15f6da41776b926b9645a9a", + "sha256:c84b430616ed73ce46e9cafd0bf0800e366a3e02fb7e1ad7c1e214dbe3862b1f", + "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", + "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", + "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", + "sha256:e0d87e81e4d869549585ba0beb3f033718501c1095004f5e6aef598d13ebc216", + "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", + "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", + "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", + "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7" ], - "markers": "python_version >= '3.9'", - "version": "==4.60.1" + "markers": "python_version >= '3.10'", + "version": "==4.61.0" }, "google-api-core": { "hashes": [ @@ -650,19 +610,19 @@ }, "google-auth": { "hashes": [ - "sha256:9bbbeef3442586effb124d1ca032cfb8fb7acd8754ab79b55facd2b8f3ab2802", - "sha256:f8f944bcb9723339b0ef58a73840f3c61bc91b69bf7368464906120b55804473" + "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", + "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16" ], "markers": "python_version >= '3.7'", - "version": "==2.42.0" + "version": "==2.43.0" }, "googleapis-common-protos": { "hashes": [ - "sha256:1aec01e574e29da63c80ba9f7bbf1ccfaacf1da877f23609fe236ca7c72a2e2e", - "sha256:59034a1d849dc4d18971997a72ac56246570afdd17f9369a0ff68218d50ab78c" + "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", + "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5" ], "markers": "python_version >= '3.7'", - "version": "==1.71.0" + "version": "==1.72.0" }, "h11": { "hashes": [ @@ -1023,83 +983,83 @@ }, "numpy": { "hashes": [ - "sha256:067e3d7159a5d8f8a0b46ee11148fc35ca9b21f61e3c49fbd0a027450e65a33b", - "sha256:0edd58682a399824633b66885d699d7de982800053acf20be1eaa46d92009c54", - "sha256:0ffc4f5caba7dfcbe944ed674b7eef683c7e94874046454bb79ed7ee0236f59d", - "sha256:1250c5d3d2562ec4174bce2e3a1523041595f9b651065e4a4473f5f48a6bc8a5", - "sha256:179a42101b845a816d464b6fe9a845dfaf308fdfc7925387195570789bb2c970", - "sha256:1c02d0629d25d426585fb2e45a66154081b9fa677bc92a881ff1d216bc9919a8", - "sha256:1e02c7159791cd481e1e6d5ddd766b62a4d5acf8df4d4d1afe35ee9c5c33a41e", - "sha256:2990adf06d1ecee3b3dcbb4977dfab6e9f09807598d647f04d385d29e7a3c3d3", - "sha256:2e267c7da5bf7309670523896df97f93f6e469fb931161f483cd6882b3b1a5dc", - "sha256:367ad5d8fbec5d9296d18478804a530f1191e24ab4d75ab408346ae88045d25e", - "sha256:396b254daeb0a57b1fe0ecb5e3cff6fa79a380fa97c8f7781a6d08cd429418fe", - "sha256:3c7cf302ac6e0b76a64c4aecf1a09e51abd9b01fc7feee80f6c43e3ab1b1dbc5", - "sha256:40051003e03db4041aa325da2a0971ba41cf65714e65d296397cc0e32de6018b", - "sha256:414a97499480067d305fcac9716c29cf4d0d76db6ebf0bf3cbce666677f12652", - "sha256:433bf137e338677cebdd5beac0199ac84712ad9d630b74eceeb759eaa45ddf30", - "sha256:4384a169c4d8f97195980815d6fcad04933a7e1ab3b530921c3fef7a1c63426d", - "sha256:497d7cad08e7092dba36e3d296fe4c97708c93daf26643a1ae4b03f6294d30eb", - "sha256:50a5fe69f135f88a2be9b6ca0481a68a136f6febe1916e4920e12f1a34e708a7", - "sha256:533ca5f6d325c80b6007d4d7fb1984c303553534191024ec6a524a4c92a5935a", - "sha256:5534ed6b92f9b7dca6c0a19d6df12d41c68b991cef051d108f6dbff3babc4ebf", - "sha256:5b83648633d46f77039c29078751f80da65aa64d5622a3cd62aaef9d835b6c93", - "sha256:691808c2b26b0f002a032c73255d0bd89751425f379f7bcd22d140db593a96e8", - "sha256:6ee9086235dd6ab7ae75aba5662f582a81ced49f0f1c6de4260a78d8f2d91a19", - "sha256:74c2a948d02f88c11a3c075d9733f1ae67d97c6bdb97f2bb542f980458b257e7", - "sha256:75370986cc0bc66f4ce5110ad35aae6d182cc4ce6433c40ad151f53690130bf1", - "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", - "sha256:7af05ed4dc19f308e1d9fc759f36f21921eb7bbfc82843eeec6b2a2863a0aefa", - "sha256:7f025652034199c301049296b59fa7d52c7e625017cae4c75d8662e377bf487d", - "sha256:823d04112bc85ef5c4fda73ba24e6096c8f869931405a80aa8b0e604510a26bc", - "sha256:8596ba2f8af5f93b01d97563832686d20206d303024777f6dfc2e7c7c3f1850e", - "sha256:8e9aced64054739037d42fb84c54dd38b81ee238816c948c8f3ed134665dcd86", - "sha256:8f6ac61a217437946a1fa48d24c47c91a0c4f725237871117dea264982128097", - "sha256:901bf6123879b7f251d3631967fd574690734236075082078e0571977c6a8e6a", - "sha256:93d4962d8f82af58f0b2eb85daaf1b3ca23fe0a85d0be8f1f2b7bb46034e56d7", - "sha256:94fcaa68757c3e2e668ddadeaa86ab05499a70725811e582b6a9858dd472fb30", - "sha256:952cfd0748514ea7c3afc729a0fc639e61655ce4c55ab9acfab14bda4f402b4c", - "sha256:9591e1221db3f37751e6442850429b3aabf7026d3b05542d102944ca7f00c8a8", - "sha256:99683cbe0658f8271b333a1b1b4bb3173750ad59c0c61f5bbdc5b318918fffe3", - "sha256:9ad12e976ca7b10f1774b03615a2a4bab8addce37ecc77394d8e986927dc0dfe", - "sha256:9cc48e09feb11e1db00b320e9d30a4151f7369afb96bd0e48d942d09da3a0d00", - "sha256:9dc13c6a5829610cc07422bc74d3ac083bd8323f14e2827d992f9e52e22cd6a6", - "sha256:9e318ee0596d76d4cb3d78535dc005fa60e5ea348cd131a51e99d0bdbe0b54fe", - "sha256:a333b4ed33d8dc2b373cc955ca57babc00cd6f9009991d9edc5ddbc1bac36bcd", - "sha256:afd07d377f478344ec6ca2b8d4ca08ae8bd44706763d1efb56397de606393f48", - "sha256:b001bae8cea1c7dfdb2ae2b017ed0a6f2102d7a70059df1e338e307a4c78a8ae", - "sha256:b37a0b2e5935409daebe82c1e42274d30d9dd355852529eab91dab8dcca7419f", - "sha256:b912f2ed2b67a129e6a601e9d93d4fa37bef67e54cac442a2f588a54afe5c67a", - "sha256:bc92a5dedcc53857249ca51ef29f5e5f2f8c513e22cfb90faeb20343b8c6f7a6", - "sha256:ca0309a18d4dfea6fc6262a66d06c26cfe4640c3926ceec90e57791a82b6eee5", - "sha256:cb248499b0bc3be66ebd6578b83e5acacf1d6cb2a77f2248ce0e40fbec5a76d0", - "sha256:cb32e3cf0f762aee47ad1ddc6672988f7f27045b0783c887190545baba73aa25", - "sha256:cd052f1fa6a78dee696b58a914b7229ecfa41f0a6d96dc663c1220a55e137593", - "sha256:cd4260f64bc794c3390a63bf0728220dd1a68170c169088a1e0dfa2fde1be12f", - "sha256:cd7de500a5b66319db419dc3c345244404a164beae0d0937283b907d8152e6ea", - "sha256:ce020080e4a52426202bdb6f7691c65bb55e49f261f31a8f506c9f6bc7450421", - "sha256:cfdd09f9c84a1a934cde1eec2267f0a43a7cd44b2cca4ff95b7c0d14d144b0bf", - "sha256:d00de139a3324e26ed5b95870ce63be7ec7352171bc69a4cf1f157a48e3eb6b7", - "sha256:d79715d95f1894771eb4e60fb23f065663b2298f7d22945d66877aadf33d00c7", - "sha256:d8f3b1080782469fdc1718c4ed1d22549b5fb12af0d57d35e992158a772a37cf", - "sha256:d9192da52b9745f7f0766531dcfa978b7763916f158bb63bdb8a1eca0068ab20", - "sha256:d9d537a39cc9de668e5cd0e25affb17aec17b577c6b3ae8a3d866b479fbe88d0", - "sha256:da1a74b90e7483d6ce5244053399a614b1d6b7bc30a60d2f570e5071f8959d3e", - "sha256:dca2d0fc80b3893ae72197b39f69d55a3cd8b17ea1b50aa4c62de82419936150", - "sha256:ddc7c39727ba62b80dfdbedf400d1c10ddfa8eefbd7ec8dcb118be8b56d31029", - "sha256:e1ec5615b05369925bd1125f27df33f3b6c8bc10d788d5999ecd8769a1fa04db", - "sha256:e6687dc183aa55dae4a705b35f9c0f8cb178bcaa2f029b241ac5356221d5c021", - "sha256:e7e946c7170858a0295f79a60214424caac2ffdb0063d4d79cb681f9aa0aa569", - "sha256:eb63d443d7b4ffd1e873f8155260d7f58e7e4b095961b01c91062935c2491e57", - "sha256:ec9d249840f6a565f58d8f913bccac2444235025bbb13e9a4681783572ee3caa", - "sha256:ed635ff692483b8e3f0fcaa8e7eb8a75ee71aa6d975388224f70821421800cea", - "sha256:eda59e44957d272846bb407aad19f89dc6f58fecf3504bd144f4c5cf81a7eacc", - "sha256:f0dadeb302887f07431910f67a14d57209ed91130be0adea2f9793f1a4f817cf", - "sha256:f0ddb4b96a87b6728df9362135e764eac3cfa674499943ebc44ce96c478ab125", - "sha256:f5415fb78995644253370985342cd03572ef8620b934da27d77377a2285955bf" + "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", + "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", + "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", + "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", + "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", + "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", + "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", + "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", + "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", + "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", + "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", + "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", + "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", + "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", + "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", + "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", + "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", + "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", + "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", + "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", + "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", + "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", + "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", + "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", + "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", + "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", + "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", + "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", + "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", + "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", + "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", + "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", + "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", + "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", + "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", + "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", + "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", + "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", + "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", + "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", + "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", + "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", + "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", + "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", + "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", + "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", + "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", + "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", + "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", + "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", + "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", + "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", + "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", + "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", + "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", + "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", + "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", + "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", + "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", + "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", + "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", + "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", + "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", + "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", + "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", + "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", + "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", + "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", + "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", + "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", + "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", + "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", + "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", + "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520" ], "markers": "python_version >= '3.11'", - "version": "==2.3.3" + "version": "==2.3.5" }, "opencensus": { "hashes": [ @@ -1143,51 +1103,64 @@ }, "pandas": { "hashes": [ - "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", - "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175", - "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", - "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", - "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", - "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", - "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743", - "sha256:13bd629c653856f00c53dc495191baa59bcafbbf54860a46ecc50d3a88421a96", - "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b", - "sha256:1d81573b3f7db40d020983f78721e9bfc425f411e616ef019a10ebf597aedb2e", - "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811", - "sha256:21bb612d148bb5860b7eb2c10faacf1a810799245afd342cf297d7551513fbb6", - "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b", - "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", - "sha256:36d627906fd44b5fd63c943264e11e96e923f8de77d6016dc2f667b9ad193438", - "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9", - "sha256:42c05e15111221384019897df20c6fe893b2f697d03c811ee67ec9e0bb5a3424", - "sha256:45178cf09d1858a1509dc73ec261bf5b25a625a389b65be2e47b559905f0ab6a", - "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", - "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", - "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35", - "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4", - "sha256:77cefe00e1b210f9c76c697fedd8fdb8d3dd86563e9c8adc9fa72b90f5e9e4c2", - "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", - "sha256:88080a0ff8a55eac9c84e3ff3c7665b3b5476c6fbc484775ca1910ce1c3e0b87", - "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", - "sha256:9467697b8083f9667b212633ad6aa4ab32436dcbaf4cd57325debb0ddef2012f", - "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9", - "sha256:a9d7ec92d71a420185dec44909c32e9a362248c4ae2238234b76d5be37f208cc", - "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", - "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", - "sha256:b62d586eb25cb8cb70a5746a378fc3194cb7f11ea77170d59f889f5dfe3cec7a", - "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2", - "sha256:c253828cb08f47488d60f43c5fc95114c771bbfff085da54bfc79cb4f9e3a372", - "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", - "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", - "sha256:cc03acc273c5515ab69f898df99d9d4f12c4d70dbfc24c3acc6203751d0804cf", - "sha256:d25c20a03e8870f6339bcf67281b946bd20b86f1a544ebbebb87e66a8d642cba", - "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", - "sha256:d4a558c7620340a0931828d8065688b3cc5b4c8eb674bcaf33d18ff4a6870b4a", - "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4", - "sha256:e190b738675a73b581736cc8ec71ae113d6c3768d0bd18bffa5b9a0927b0b6ea" + "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", + "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", + "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", + "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", + "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", + "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", + "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", + "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", + "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", + "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", + "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", + "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", + "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", + "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", + "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", + "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", + "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", + "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", + "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", + "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", + "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", + "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", + "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", + "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", + "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", + "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", + "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", + "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", + "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", + "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", + "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", + "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", + "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", + "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", + "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", + "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", + "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", + "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", + "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", + "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", + "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", + "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", + "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", + "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", + "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", + "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", + "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", + "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", + "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", + "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", + "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", + "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", + "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", + "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", + "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee" ], "markers": "python_version >= '3.9'", - "version": "==2.3.2" + "version": "==2.3.3" }, "pillow": { "hashes": [ @@ -1296,44 +1269,44 @@ }, "protobuf": { "hashes": [ - "sha256:140303d5c8d2037730c548f8c7b93b20bb1dc301be280c378b82b8894589c954", - "sha256:25c9e1963c6734448ea2d308cfa610e692b801304ba0908d7bfa564ac5132995", - "sha256:35be49fd3f4fefa4e6e2aacc35e8b837d6703c37a2168a55ac21e9b1bc7559ef", - "sha256:905b07a65f1a4b72412314082c7dbfae91a9e8b68a0cc1577515f8df58ecf455", - "sha256:9a031d10f703f03768f2743a1c403af050b6ae1f3480e9c140f39c45f81b13ee", - "sha256:c963e86c3655af3a917962c9619e1a6b9670540351d7af9439d06064e3317cc9", - "sha256:cd33a8e38ea3e39df66e1bbc462b076d6e5ba3a4ebbde58219d777223a7873d3", - "sha256:d6101ded078042a8f17959eccd9236fb7a9ca20d3b0098bbcb91533a5680d035", - "sha256:e0697ece353e6239b90ee43a9231318302ad8353c70e6e45499fa52396debf90", - "sha256:e0a1715e4f27355afd9570f3ea369735afc853a6c3951a6afe1f80d8569ad298" + "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", + "sha256:2981c58f582f44b6b13173e12bb8656711189c2a70250845f264b877f00b1913", + "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", + "sha256:7109dcc38a680d033ffb8bf896727423528db9163be1b6a02d6a49606dcadbfe", + "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", + "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", + "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", + "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", + "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", + "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4" ], "markers": "python_version >= '3.9'", - "version": "==6.33.0" + "version": "==6.33.2" }, "psutil": { "hashes": [ - "sha256:0cc5c6889b9871f231ed5455a9a02149e388fffcb30b607fb7a8896a6d95f22e", - "sha256:20c00824048a95de67f00afedc7b08b282aa08638585b0206a9fb51f28f1a165", - "sha256:2a486030d2fe81bec023f703d3d155f4823a10a47c36784c84f1cc7f8d39bedb", - "sha256:329f05610da6380982e6078b9d0881d9ab1e9a7eb7c02d833bfb7340aa634e31", - "sha256:364b1c10fe4ed59c89ec49e5f1a70da353b27986fa8233b4b999df4742a5ee2f", - "sha256:3e988455e61c240cc879cb62a008c2699231bf3e3d061d7fce4234463fd2abb4", - "sha256:3efd8fc791492e7808a51cb2b94889db7578bfaea22df931424f874468e389e3", - "sha256:4a24bcd7b7f2918d934af0fb91859f621b873d6aa81267575e3655cd387572a7", - "sha256:625977443498ee7d6c1e63e93bacca893fd759a66c5f635d05e05811d23fb5ee", - "sha256:7b04c29e3c0c888e83ed4762b70f31e65c42673ea956cefa8ced0e31e185f582", - "sha256:7d9623a5e4164d2220ecceb071f4b333b3c78866141e8887c072129185f41278", - "sha256:8e17852114c4e7996fe9da4745c2bdef001ebbf2f260dec406290e66628bdb91", - "sha256:8e9e77a977208d84aa363a4a12e0f72189d58bbf4e46b49aae29a2c6e93ef206", - "sha256:aa225cdde1335ff9684708ee8c72650f6598d5ed2114b9a7c5802030b1785018", - "sha256:c9ba5c19f2d46203ee8c152c7b01df6eec87d883cfd8ee1af2ef2727f6b0f814", - "sha256:e09cfe92aa8e22b1ec5e2d394820cf86c5dff6367ac3242366485dfa874d43bc", - "sha256:e2aeb9b64f481b8eabfc633bd39e0016d4d8bbcd590d984af764d80bf0851b8a", - "sha256:f101ef84de7e05d41310e3ccbdd65a6dd1d9eed85e8aaf0758405d022308e204", - "sha256:fa6342cf859c48b19df3e4aa170e4cfb64aadc50b11e06bb569c6c777b089c9e" + "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", + "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", + "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", + "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", + "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", + "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", + "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", + "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", + "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", + "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", + "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", + "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", + "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", + "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", + "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", + "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", + "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", + "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", + "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f" ], "markers": "python_version >= '3.6'", - "version": "==7.1.2" + "version": "==7.1.3" }, "pyasn1": { "hashes": [ @@ -1485,11 +1458,11 @@ }, "pydyf": { "hashes": [ - "sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3", - "sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64" + "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", + "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095" ], - "markers": "python_version >= '3.8'", - "version": "==0.11.0" + "markers": "python_version >= '3.10'", + "version": "==0.12.1" }, "pyjwt": { "extras": [ @@ -1705,11 +1678,11 @@ }, "tinycss2": { "hashes": [ - "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", - "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289" + "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", + "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957" ], - "markers": "python_version >= '3.8'", - "version": "==1.4.0" + "markers": "python_version >= '3.10'", + "version": "==1.5.1" }, "tinyhtml5": { "hashes": [ @@ -1737,19 +1710,19 @@ }, "urllib3": { "hashes": [ - "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", - "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", + "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1" ], "markers": "python_version >= '3.9'", - "version": "==2.5.0" + "version": "==2.6.0" }, "usdm": { "hashes": [ - "sha256:a604d738e8f0175c48c57f6115b70de289a0224cc4669d172e424da9d20fbbb5", - "sha256:d6aeb93cb6af0fa17169bb48d16cd8828ab5b696fdf19b05230a76d910c772d9" + "sha256:dfd25f65e9777dcb555a23bf8786a5f0593fec1a78d9b120f74eb2f7648b158f", + "sha256:f46bed0e11f31257027f2ac123314f9797e36fd1b60576915c2bcdb40751df05" ], "index": "pypi", - "version": "==0.59.0" + "version": "==0.65.0" }, "uvicorn": { "hashes": [ @@ -1794,80 +1767,21 @@ }, "zopfli": { "hashes": [ - "sha256:0aa5f90d6298bda02a95bc8dc8c3c19004d5a4e44bda00b67ca7431d857b4b54", - "sha256:0cc20b02a9531559945324c38302fd4ba763311632d0ec8a1a0aa9c10ea363e6", - "sha256:1d8cc06605519e82b16df090e17cb3990d1158861b2872c3117f1168777b81e4", - "sha256:1f990634fd5c5c8ced8edddd8bd45fab565123b4194d6841e01811292650acae", - "sha256:2345e713260a350bea0b01a816a469ea356bc2d63d009a0d777691ecbbcf7493", - "sha256:2768c877f76c8a0e7519b1c86c93757f3c01492ddde55751e9988afb7eff64e1", - "sha256:29ea74e72ffa6e291b8c6f2504ce6c146b4fe990c724c1450eb8e4c27fd31431", - "sha256:34a99592f3d9eb6f737616b5bd74b48a589fdb3cb59a01a50d636ea81d6af272", - "sha256:3654bfc927bc478b1c3f3ff5056ed7b20a1a37fa108ca503256d0a699c03bbb1", - "sha256:3657e416ffb8f31d9d3424af12122bb251befae109f2e271d87d825c92fc5b7b", - "sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f", - "sha256:3827170de28faf144992d3d4dcf8f3998fe3c8a6a6f4a08f1d42c2ec6119d2bb", - "sha256:39e576f93576c5c223b41d9c780bbb91fd6db4babf3223d2a4fe7bf568e2b5a8", - "sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca", - "sha256:3c163911f8bad94b3e1db0a572e7c28ba681a0c91d0002ea1e4fa9264c21ef17", - "sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef", - "sha256:48dba9251060289101343110ab47c0756f66f809bb4d1ddbb6d5c7e7752115c5", - "sha256:4915a41375bdee4db749ecd07d985a0486eb688a6619f713b7bf6fbfd145e960", - "sha256:4c1226a7e2c7105ac31503a9bb97454743f55d88164d6d46bc138051b77f609b", - "sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f", - "sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a", - "sha256:5aad740b4d4fcbaaae4887823925166ffd062db3b248b3f432198fc287381d1a", - "sha256:5f272186e03ad55e7af09ab78055535c201b1a0bcc2944edb1768298d9c483a4", - "sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e", - "sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e", - "sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4", - "sha256:6482db9876c68faac2d20a96b566ffbf65ddaadd97b222e4e73641f4f8722fc4", - "sha256:6617fb10f9e4393b331941861d73afb119cd847e88e4974bdbe8068ceef3f73f", - "sha256:676919fba7311125244eb0c4393679ac5fe856e5864a15d122bd815205369fa0", - "sha256:6c2d2bc8129707e34c51f9352c4636ca313b52350bbb7e04637c46c1818a2a70", - "sha256:71390dbd3fbf6ebea9a5d85ffed8c26ee1453ee09248e9b88486e30e0397b775", - "sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5", - "sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072", - "sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd", - "sha256:7cce242b5df12b2b172489daf19c32e5577dd2fac659eb4b17f6a6efb446fd5c", - "sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29", - "sha256:89899641d4de97dbad8e0cde690040d078b6aea04066dacaab98e0b5a23573f2", - "sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65", - "sha256:8fbe5bcf10d01aab3513550f284c09fef32f342b36f56bfae2120a9c4d12c130", - "sha256:91a2327a4d7e77471fa4fbb26991c6de4a738c6fc6a33e09bb25f56a870a4b7b", - "sha256:95a260cafd56b8fffa679918937401c80bb38e1681c448b988022e4c3610965d", - "sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99", - "sha256:9a6aec38a989bad7ddd1ef53f1265699e49e294d08231b5313d61293f3cd6237", - "sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c", - "sha256:9f4a7ec2770e6af05f5a02733fd3900f30a9cd58e5d6d3727e14c5bcd6e7d587", - "sha256:a1cf720896d2ce998bc8e051d4b4ce0d8bec007aab6243102e8e1d22a0b2fb3f", - "sha256:a241a68581d34d67b40c425cce3d1fd211c092f99d9250947824ccba9f491949", - "sha256:a53b18797cdef27e019db595d66c4b077325afe2fd62145953275f53d84ce40c", - "sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8", - "sha256:a86eb88e06bd87e1fff31dac878965c26b0c26db59ddcf78bb0379a954b120de", - "sha256:aa588b21044f8a74e423d8c8a4c7fc9988501878aacced793467010039c50734", - "sha256:b05296e8bc88c92e2b21e0a9bae4740c1551ee613c1d93a51fd28a7a0b2b6fbb", - "sha256:b0ec13f352ea5ae0fc91f98a48540512eed0767d0ec4f7f3cb92d92797983d18", - "sha256:b3df42f52502438ee973042cc551877d24619fa1cd38ef7b7e9ac74200daca8b", - "sha256:b78008a69300d929ca2efeffec951b64a312e9a811e265ea4a907ab546d79fa6", - "sha256:b9026a21b6d41eb0e2e63f5bc1242c3fcc43ecb770963cda99a4307863dac12e", - "sha256:bbe429fc50686bb2a2608a30843e36fbaa123462a5284f136c7d9e0145220bfd", - "sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8", - "sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df", - "sha256:c4278d1873ce6e803e5d4f8d702fd3026bd67fca744aa98881324d1157ddf748", - "sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567", - "sha256:cbe6df25807227519debd1a57ab236f5f6bad441500e85b13903e51f93a43214", - "sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14", - "sha256:e0137dd64a493ba6a4be37405cfd6febe650a98cc1e9dca8f6b8c63b1db11b41", - "sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352", - "sha256:eb45a34f23da4f8bc712b6376ca5396914b0b7c09adbb001dad964eb7f3132f8", - "sha256:ecb7572df5372abce8073df078207d9d1749f20b8b136089916a4a0868d56051", - "sha256:f12000a6accdd4bf0a3fa6eaa1b1c7a7bc80af0a2edf3f89d770d3dcce1d0e22", - "sha256:f7d69c1a7168ad0e9cb864e8663acb232986a0c9c9cb9801f56bf6214f53a54d", - "sha256:f815fcc2b2a457977724bad97fb4854022980f51ce7b136925e336b530545ae1", - "sha256:fc39f5c27f962ec8660d8d20c24762431131b5d8c672b44b0a54cf2b5bcde9b9" + "sha256:03181d48e719fcb6cf8340189c61e8f9883d8bbbdf76bf5212a74457f7d083c1", + "sha256:18b5f1570f64d4988482e4466f10ef5f2a30f687c19ad62a64560f2152dc89eb", + "sha256:25e4863b8dc30e5d5309f87c106b0b7d3da4ed0e340b8a52b36d4471e797589f", + "sha256:7d66337be6d5613dec55213e9ac28f378c41e2cc04fbad4a10748e4df774ca85", + "sha256:9097e8e1dfdb7f5aea5464e469946857e80502b6d29ba1b232450916bd4a74d1", + "sha256:a8ee992b2549e090cd3f0178bf606dd41a29e0613a04cdf5054224662c72dce6", + "sha256:b72a010d205d00b2855acc2302772067362f9ab5a012e3550662aec60d28e6b3", + "sha256:b8bdb41fbfdc4738b7bdc09ed7c1e951579fae192391a5e694d59bb186cdbec7", + "sha256:c3ba02a9a6ca90481d2b2f68bab038b310d63a1e3b5ae305e95a6599787ed941", + "sha256:d1b98ad47c434ef213444a03ef2f826eeec100144d64f6a57504b9893d3931ce", + "sha256:f67d04280065e24cb9a4174cb6b3d1f763687f8cb2963aa135ad8f57c6995f5a", + "sha256:f94e4dd7d76b4fe9f5d9229372be20d7f786164eea5152d1af1c34298c3d5975" ], - "markers": "python_version >= '3.8'", - "version": "==0.2.3.post1" + "markers": "python_version >= '3.10'", + "version": "==0.4.0" } }, "develop": { diff --git a/clinical-mdr-api/README.md b/clinical-mdr-api/README.md index a46ab0d9..dc80885f 100644 --- a/clinical-mdr-api/README.md +++ b/clinical-mdr-api/README.md @@ -1,6 +1,6 @@ # Overview -This repository contains an API providing read/write access to Clinical MDR (Clinical Metadata Repository) stored in a neo4j database. +This repository contains the OpenStudyBuilder API providing read/write access to clinical metadata stored in a neo4j database. # Technology stack @@ -41,7 +41,7 @@ Notes: OAUTH_API_APP_SECRET='21u9UAnFKXUCYt6yxqRA7xAQ' MS_GRAPH_INTEGRATION_ENABLED=true # optional, for MS Graph API integration: filter expression for group discovery # - MS_GRAPH_GROUPS_QUERY="$filter=startsWith(displayName, 'StudyBuilder')" + MS_GRAPH_GROUPS_QUERY="$filter=startsWith(displayName, 'OpenStudyBuilder')" # required for the FastAPI-built-in Swagger UI only # OAUTH_SWAGGER_APP_ID='db8a95f6-a638-4535-bb1d-4a131748165a' diff --git a/clinical-mdr-api/apiVersion b/clinical-mdr-api/apiVersion index d4b0e760..49ee0744 100644 --- a/clinical-mdr-api/apiVersion +++ b/clinical-mdr-api/apiVersion @@ -1 +1 @@ -3.0.529 +3.0.569 diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_instance_class_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_instance_class_repository.py index 82fae4b3..654528b6 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_instance_class_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_instance_class_repository.py @@ -81,7 +81,12 @@ def get_neomodel_extension_query(self) -> NodeSet: ) ) - def extend_distinct_headers_query(self, nodeset: NodeSet) -> NodeSet: + def extend_distinct_headers_query( + self, + nodeset: NodeSet, + field_name: str, + filter_by: dict[str, dict[str, Any]] | None = None, + ) -> NodeSet: return nodeset.subquery( self.root_class.nodes.fetch_relations("has_version") .intermediate_transform( @@ -188,6 +193,12 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( is_adam_param_specific_enabled=activity_item_class.has_activity_instance_class.relationship( root ).is_adam_param_specific_enabled, + is_additional_optional=activity_item_class.has_activity_instance_class.relationship( + root + ).is_additional_optional, + is_default_linked=activity_item_class.has_activity_instance_class.relationship( + root + ).is_default_linked, ) for activity_item_class in activity_item_classes ], diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_item_class_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_item_class_repository.py index edb357cf..ba1b0028 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_item_class_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/biomedical_concepts/activity_item_class_repository.py @@ -88,6 +88,7 @@ def generic_alias_clause(self, **kwargs): concept_value.nci_concept_id AS nci_concept_id, concept_value.name AS name, concept_value.definition AS definition, + concept_value.display_name AS display_name, library_name, is_library_editable, version_rel.version AS version, @@ -110,6 +111,8 @@ def _create_aggregate_root_instance_from_cypher_result( is_adam_param_specific_enabled=aic.get( "is_adam_param_specific_enabled", False ), + is_additional_optional=aic.get("is_additional_optional", False), + is_default_linked=aic.get("is_default_linked", False), ) for aic in input_dict.get("activity_instance_classes", []) ] @@ -140,6 +143,7 @@ def _create_aggregate_root_instance_from_cypher_result( role=role, data_type=data_type, variable_class_uids=input_dict.get("variable_class_uids", []), + display_name=input_dict.get("display_name"), ), library=LibraryVO.from_input_values_2( library_name=input_dict.get("library_name", "Unknown"), @@ -169,7 +173,9 @@ def specific_alias_clause(self, **kwargs) -> str: uid: activity_instance_class_root.uid, name: activity_instance_class_value.name, mandatory: rel.mandatory, - is_adam_param_specific_enabled: rel.is_adam_param_specific_enabled + is_adam_param_specific_enabled: rel.is_adam_param_specific_enabled, + is_additional_optional: rel.is_additional_optional, + is_default_linked: rel.is_default_linked }] AS activity_instance_classes, COLLECT { @@ -194,6 +200,7 @@ def _create_new_value_node(self, ar: ActivityItemClassAR) -> ActivityItemClassVa order=ar.activity_item_class_vo.order, definition=ar.activity_item_class_vo.definition, nci_concept_id=ar.activity_item_class_vo.nci_concept_id, + display_name=ar.activity_item_class_vo.display_name, ) return new_value @@ -285,6 +292,8 @@ def _has_data_changed( "uid": node.uid, "mandatory": rel.mandatory, "is_adam_param_specific_enabled": rel.is_adam_param_specific_enabled, + "is_additional_optional": rel.is_additional_optional, + "is_default_linked": rel.is_default_linked, } ) existing_activity_instance_classes.sort(key=json.dumps) @@ -318,6 +327,7 @@ def _has_data_changed( or ar.activity_item_class_vo.definition != value.definition or ar.activity_item_class_vo.nci_concept_id != value.nci_concept_id or ar.activity_item_class_vo.order != value.order + or ar.activity_item_class_vo.display_name != value.display_name or new_activity_instance_classes != existing_activity_instance_classes or ( ar.activity_item_class_vo.role.uid, @@ -347,6 +357,7 @@ def _get_or_create_value( order=ar.activity_item_class_vo.order, definition=ar.activity_item_class_vo.definition, nci_concept_id=ar.activity_item_class_vo.nci_concept_id, + display_name=ar.activity_item_class_vo.display_name, ) self._db_save_node(new_value) for ( @@ -361,6 +372,10 @@ def _get_or_create_value( rel.is_adam_param_specific_enabled = ( activity_instance_class_uid.is_adam_param_specific_enabled ) + rel.is_additional_optional = ( + activity_instance_class_uid.is_additional_optional + ) + rel.is_default_linked = activity_instance_class_uid.is_default_linked rel.save() else: root.has_activity_instance_class.connect( @@ -368,6 +383,8 @@ def _get_or_create_value( { "mandatory": activity_instance_class_uid.mandatory, "is_adam_param_specific_enabled": activity_instance_class_uid.is_adam_param_specific_enabled, + "is_additional_optional": activity_instance_class_uid.is_additional_optional, + "is_default_linked": activity_instance_class_uid.is_default_linked, }, ) @@ -427,6 +444,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( definition=value.definition, nci_concept_id=value.nci_concept_id, order=value.order, + display_name=value.display_name, activity_instance_classes=[ ActivityInstanceClassActivityItemClassRelVO( uid=activity_instance_class.uid, @@ -436,6 +454,12 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( is_adam_param_specific_enabled=activity_instance_class.has_activity_item_class.relationship( root ).is_adam_param_specific_enabled, + is_additional_optional=activity_instance_class.has_activity_item_class.relationship( + root + ).is_additional_optional, + is_default_linked=activity_instance_class.has_activity_item_class.relationship( + root + ).is_default_linked, ) for activity_instance_class in activity_instance_classes ], @@ -630,6 +654,8 @@ def get_activity_instance_classes_using_item( WITH aic.uid as uid, instanceData.value.name as name, instanceData.rel.is_adam_param_specific_enabled as adam_param_specific_enabled, + instanceData.rel.is_additional_optional as is_additional_optional, + instanceData.rel.is_default_linked as is_default_linked, instanceData.rel.mandatory as mandatory, instanceData.ver.status as status, instanceData.ver.version as version, @@ -651,6 +677,8 @@ def get_activity_instance_classes_using_item( WITH aic.uid as uid, aicValue.name as name, rel.is_adam_param_specific_enabled as adam_param_specific_enabled, + rel.is_additional_optional as is_additional_optional, + rel.is_default_linked as is_default_linked, rel.mandatory as mandatory, latest_ver.status as status, latest_ver.version as version, @@ -678,7 +706,7 @@ def get_activity_instance_classes_using_item( final_query = ( query + """ - RETURN uid, name, adam_param_specific_enabled, mandatory, + RETURN uid, name, adam_param_specific_enabled, is_additional_optional, is_default_linked, mandatory, status, version, modified_date, modified_by """ ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_group_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_group_repository.py index a52f35b3..dfb0b058 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_group_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_group_repository.py @@ -110,14 +110,14 @@ def create_query_filter_statement( if kwargs.get("activity_subgroup_names") is not None: activity_subgroup_names = kwargs.get("activity_subgroup_names") filter_by_activity_subgroup_names = """ - size([(concept_value)<-[:IN_GROUP]-(:ActivityValidGroup)<-[:HAS_GROUP]-(v:ActivitySubGroupValue) + size([(concept_value)<-[:HAS_SELECTED_GROUP]-(:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(v:ActivitySubGroupValue) WHERE v.name IN $activity_subgroup_names | v.name]) > 0""" filter_parameters.append(filter_by_activity_subgroup_names) filter_query_parameters["activity_subgroup_names"] = activity_subgroup_names if kwargs.get("activity_names") is not None: activity_names = kwargs.get("activity_names") filter_by_activity_names = """ - size([(concept_value)<-[:IN_GROUP]-(:ActivityValidGroup)<-[:IN_SUBGROUP]-(:ActivityGrouping)<-[:HAS_GROUPING]-(v:ActivityValue) + size([(concept_value)<-[:HAS_SELECTED_GROUP]-(:ActivityGrouping)<-[:HAS_GROUPING]-(v:ActivityValue) WHERE v.name IN $activity_names | v.name]) > 0""" filter_parameters.append(filter_by_activity_names) filter_query_parameters["activity_names"] = activity_names @@ -142,9 +142,9 @@ def specific_alias_clause(self, **kwargs) -> str: # which is specified in the activity_generic_repository_impl return """ WITH *, - head([(concept_value)<-[:IN_GROUP]-(:ActivityValidGroup)<-[:HAS_GROUP]-(activity_sub_group_value:ActivitySubGroupValue)<-[:HAS_VERSION]- + head([(concept_value)<-[:HAS_SELECTED_GROUP]-(:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(activity_sub_group_value:ActivitySubGroupValue)<-[:HAS_VERSION]- (activity_sub_group_root:ActivitySubGroupRoot) | {uid:activity_sub_group_root.uid, name:activity_sub_group_value.name}]) AS activity_subgroup, - head([(concept_value)<-[:IN_GROUP]-(:ActivityValidGroup)<-[:IN_SUBGROUP]-(:ActivityGrouping)<-[:HAS_GROUPING]-(activity_value:ActivityValue) + head([(concept_value)<-[:HAS_SELECTED_GROUP]-(:ActivityGrouping)<-[:HAS_GROUPING]-(activity_value:ActivityValue) <-[:HAS_VERSION]-(activity_root:ActivityRoot) | {uid:activity_root.uid, name:activity_value.name}]) AS activity """ @@ -175,7 +175,7 @@ def get_cosmos_group_overview(self, group_uid: str) -> dict[str, Any]: WITH DISTINCT group_root, group_value, head([(library)-[:CONTAINS_CONCEPT]->(group_root) | library.name]) AS group_library_name, [(group_root)-[versions:HAS_VERSION]->(:ActivityGroupValue) | versions.version] as all_versions, - apoc.coll.toSet([(sgv:ActivitySubGroupValue)-[:HAS_GROUP]->(avg:ActivityValidGroup)-[:IN_GROUP]->(group_value) + apoc.coll.toSet([(group_value)<-[:HAS_SELECTED_GROUP]-(activity_grouping:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(sgv:ActivitySubGroupValue) | { uid: head([(sgr:ActivitySubGroupRoot)-[:HAS_VERSION]->(sgv) | sgr.uid]), name: sgv.name, @@ -184,7 +184,7 @@ def get_cosmos_group_overview(self, group_uid: str) -> dict[str, Any]: version: head([(sgr:ActivitySubGroupRoot)-[hv:HAS_VERSION]->(sgv) | hv.version]) }]) AS linked_subgroups, apoc.coll.toSet( - [(group_value)<-[:IN_GROUP]-(activity_valid_group:ActivityValidGroup)<-[:IN_SUBGROUP]- + [(group_value)<-[:HAS_SELECTED_GROUP]- (activity_grouping:ActivityGrouping)<-[:HAS_GROUPING]-(activity_value:ActivityValue)<-[:HAS_VERSION]- (activity_root:ActivityRoot) WHERE NOT EXISTS ((activity_value)<--(:DeletedActivityRoot)) @@ -272,10 +272,9 @@ def get_linked_activity_subgroup_uids( ELSE gv_rel.end_date END as version_end_date - // 3. Find all subgroups linked to this group version through ActivityValidGroup - MATCH (avg:ActivityValidGroup)-[:IN_GROUP]->(gv) - MATCH (sgv:ActivitySubGroupValue)-[:HAS_GROUP]->(avg) - MATCH (sgr:ActivitySubGroupRoot)-[sgv_rel:HAS_VERSION]->(sgv) + // 3. Find all subgroups linked to this group version through ActivityGrouping nodes + MATCH (ag:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(gv) + MATCH (ag)-[:HAS_SELECTED_SUBGROUP]->(sgv:ActivitySubGroupValue)<-[sgv_rel:HAS_VERSION]-(sgr:ActivitySubGroupRoot) // 4. Filter subgroup versions that existed when this activity group version was active WITH gr, gv, gv_rel, version_end_date, sgr, sgv, sgv_rel @@ -309,6 +308,7 @@ def get_linked_activity_subgroup_uids( uid: subgroup_uid, name: latest_version.name, version: latest_version.version, + start_date: latest_version.start_date, status: latest_version.status, definition: latest_version.definition } as result @@ -349,9 +349,7 @@ def get_linked_activity_subgroup_uids( THEN min(next_rel.start_date) ELSE gv_rel.end_date END as version_end_date - - MATCH (avg:ActivityValidGroup)-[:IN_GROUP]->(gv) - MATCH (sgv:ActivitySubGroupValue)-[:HAS_GROUP]->(avg) + MATCH (gv)<-[:HAS_SELECTED_GROUP]-(activity_grouping:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(sgv:ActivitySubGroupValue) MATCH (sgr:ActivitySubGroupRoot)-[sgv_rel:HAS_VERSION]->(sgv) WHERE sgv_rel.start_date <= COALESCE(version_end_date, datetime()) RETURN count(DISTINCT sgr.uid) as total @@ -366,12 +364,12 @@ def get_linked_activity_subgroup_uids( return {"subgroups": subgroups, "total": total} - def get_linked_upgradable_activity_subgroups( + def get_linked_upgradable_activities( self, uid: str, version: str | None = None ) -> dict[Any, Any] | None: - # Get "upgradable" linked activity subgroups. - # These are the subgroup values that have no end date, - # meaning that the linked value is the latest version of the subgroup. + # Get "upgradable" linked activities. + # These are the activity values that have no end date, + # meaning that the linked value is the latest version of the activity. params = {"uid": uid} if version: params["version"] = version @@ -387,27 +385,45 @@ def get_linked_upgradable_activity_subgroups( query = ( match + """ - MATCH (activity_group_value)<-[:IN_GROUP]-(activity_valid_group:ActivityValidGroup)<-[:HAS_GROUP]- - (activity_subgroup_value:ActivitySubGroupValue)<-[aihv:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) - MATCH (activity_subgroup_root)-[:LATEST]->(:ActivitySubGroupValue)-[:HAS_GROUP]->(:ActivityValidGroup)-[:IN_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(agr:ActivityGroupRoot) - WITH DISTINCT activity_subgroup_root, activity_subgroup_value, aihv, COLLECT(DISTINCT agr.uid) AS activity_groups - WHERE aihv.end_date IS NULL AND NOT EXISTS ((activity_subgroup_value)<--(:DeletedActivitySubGroupRoot)) + // Find what activity values are linked to this group + MATCH (activity_root:ActivityRoot)-[ahv:HAS_VERSION]->(activity_value:ActivityValue)-[:HAS_GROUPING]-> + (:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(activity_group_value) + // Find all subgroups linked to the found activity values + MATCH (activity_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]-> + (:ActivitySubGroupValue)<-[:HAS_VERSION]-(all_subgroup_root:ActivitySubGroupRoot) + // Find all groups linked to the found activity values + MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(all_group_root:ActivityGroupRoot) + // Collect the linked activities and all their groupings + WITH DISTINCT activity_root, activity_value, ahv, COLLECT(DISTINCT { + activity_group_uid: all_group_root.uid, + activity_subgroup_uid: all_subgroup_root.uid + }) AS activity_groupings + WHERE ahv.end_date IS NULL AND NOT EXISTS ((activity_value)<--(:DeletedActivityRoot)) WITH *, { - library_name: head([(library)-[:CONTAINS_CONCEPT]->(activity_subgroup_root) | library.name]), - uid: activity_subgroup_root.uid, + library_name: head([(library)-[:CONTAINS_CONCEPT]->(activity_root) | library.name]), + uid: activity_root.uid, version: { - major_version: toInteger(split(aihv.version,'.')[0]), - minor_version: toInteger(split(aihv.version,'.')[1]), - status:aihv.status + major_version: toInteger(split(ahv.version,'.')[0]), + minor_version: toInteger(split(ahv.version,'.')[1]), + status:ahv.status }, - name:activity_subgroup_value.name, - name_sentence_case:activity_subgroup_value.name_sentence_case, - activity_groups: activity_groups - } AS activity_subgroup ORDER BY activity_subgroup.uid, activity_subgroup.name + name: activity_value.name, + name_sentence_case: activity_value.name_sentence_case, + definition: activity_value.definition, + abbreviation: activity_value.abbreviation, + nci_concept_id: activity_value.nci_concept_id, + nci_concept_name: activity_value.nci_concept_name, + synonyms: activity_value.synonyms, + request_rationale: activity_value.request_rationale, + is_request_final: activity_value.is_request_final, + is_data_collected: activity_value.is_data_collected, + is_multiple_selection_allowed: activity_value.is_multiple_selection_allowed, + activity_groupings: activity_groupings + } AS activity ORDER BY activity.uid, activity.name RETURN - collect(activity_subgroup) as activity_subgroups + collect(activity) as activities """ ) result_array, attribute_names = db.cypher_query(query=query, params=params) @@ -415,7 +431,7 @@ def get_linked_upgradable_activity_subgroups( return None BusinessLogicException.raise_if( len(result_array) > 1, - msg=f"The linked subgroups query returned broken data: {result_array}", + msg=f"The linked activities query returned broken data: {result_array}", ) instances = result_array[0] instances_dict = {} diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_instance_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_instance_repository.py index 5e6b562a..ee55ff11 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_instance_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_instance_repository.py @@ -29,11 +29,6 @@ Library, VersionRelationship, ) -from clinical_mdr_api.domain_repositories.models.odm import ( - OdmFormRoot, - OdmItemGroupRoot, - OdmItemRoot, -) from clinical_mdr_api.domains.concepts.activities.activity_instance import ( ActivityInstanceAR, ActivityInstanceGroupingVO, @@ -52,9 +47,6 @@ ActivityInstance, ) from clinical_mdr_api.models.concepts.activities.activity_item import ( - CompactOdmForm, - CompactOdmItem, - CompactOdmItemGroup, CompactUnitDefinition, ) from common.config import settings @@ -104,14 +96,14 @@ def _create_new_value_node(self, ar: ActivityInstanceAR) -> ActivityInstanceValu # find related ActivityGrouping node activity_grouping_node = ListDistinct( ActivityGrouping.nodes.filter( - in_subgroup__in_group__has_version__uid=activity_grouping.activity_group_uid, - in_subgroup__has_group__has_version__uid=activity_grouping.activity_subgroup_uid, + has_selected_group__has_version__uid=activity_grouping.activity_group_uid, + has_selected_subgroup__has_version__uid=activity_grouping.activity_subgroup_uid, has_grouping__latest_final__uid=activity_grouping.activity_uid, ).resolve_subgraph() ).distinct() BusinessLogicException.raise_if( len(activity_grouping_node) == 0, - msg=f"The ActivityValidGroup node wasn't found for Activity Subgroup with UID '{activity_grouping.activity_subgroup_uid}'" + msg=f"The ActivityGrouping node wasn't found for Activity Subgroup with UID '{activity_grouping.activity_subgroup_uid}'" f" and Activity Group with UID '{activity_grouping.activity_group_uid}'.", ) activity_grouping_node = activity_grouping_node[0] @@ -158,23 +150,6 @@ def _create_new_value_node(self, ar: ActivityInstanceAR) -> ActivityInstanceValu unit_definition = UnitDefinitionRoot.nodes.get_or_none(uid=unit.uid) activity_item_node.has_unit_definition.connect(unit_definition) - if item.odm_form and item.odm_form.uid: - odm_form_root = OdmFormRoot.nodes.get_or_none(uid=item.odm_form.uid) - odm_form_value = odm_form_root.has_latest_value.single() - activity_item_node.has_odm_form.connect(odm_form_value) - - if item.odm_item_group and item.odm_item_group.uid: - odm_item_group_root = OdmItemGroupRoot.nodes.get_or_none( - uid=item.odm_item_group.uid - ) - odm_item_group_value = odm_item_group_root.has_latest_value.single() - activity_item_node.has_odm_item_group.connect(odm_item_group_value) - - if item.odm_item and item.odm_item.uid: - odm_item_root = OdmItemRoot.nodes.get_or_none(uid=item.odm_item.uid) - odm_item_value = odm_item_root.has_latest_value.single() - activity_item_node.has_odm_item.connect(odm_item_value) - value_node.contains_activity_item.connect(activity_item_node) return value_node @@ -187,11 +162,6 @@ def _has_item_data_changed(self, ar_items, value_item_nodes): "class": item.activity_item_class_uid, "units": {unit.uid for unit in item.unit_definitions}, "terms": {(term.uid, term.codelist_uid) for term in item.ct_terms}, - "odm_form": item.odm_form.uid if item.odm_form else None, - "odm_item_group": ( - item.odm_item_group.uid if item.odm_item_group else None - ), - "odm_item": item.odm_item.uid if item.odm_item else None, } ) @@ -206,9 +176,7 @@ def _has_item_data_changed(self, ar_items, value_item_nodes): } for term_context in activity_item_node.has_ct_term.all() ] - odm_form_node = activity_item_node.has_odm_form.single() - odm_item_group_node = activity_item_node.has_odm_item_group.single() - odm_item_node = activity_item_node.has_odm_item.single() + value_activity_items.append( { "is_adam_param_specific": activity_item_node.is_adam_param_specific, @@ -218,17 +186,6 @@ def _has_item_data_changed(self, ar_items, value_item_nodes): (ct_term["uid"], ct_term["codelist_uid"]) for ct_term in ct_terms }, - "odm_form": ( - odm_form_node.has_root.single().uid if odm_form_node else None - ), - "odm_item_group": ( - odm_item_group_node.has_root.single().uid - if odm_item_group_node - else None - ), - "odm_item": ( - odm_item_node.has_root.single().uid if odm_item_node else None - ), } ) for item in ar_activity_items: @@ -247,21 +204,17 @@ def _has_grouping_data_changed(self, ar_groupings, activity_instance_value): # We need to return True, so that the ActivityInstanceValue # gets updated to use the new ActivityValue. return True - activity_valid_group_nodes = activity_grouping_node.in_subgroup.all() - for activity_valid_group_node in activity_valid_group_nodes: - value_group_pairs.append( - ( - activity_grouping_node.has_grouping.get() - .has_version.single() - .uid, - activity_valid_group_node.has_group.get() - .has_version.single() - .uid, - activity_valid_group_node.in_group.get() - .has_version.single() - .uid, - ) + value_group_pairs.append( + ( + activity_grouping_node.has_grouping.get().has_version.single().uid, + activity_grouping_node.has_selected_group.get() + .has_version.single() + .uid, + activity_grouping_node.has_selected_subgroup.get() + .has_version.single() + .uid, ) + ) ar_group_pairs = [ ( @@ -358,8 +311,8 @@ def copy_activity_instance_and_recreate_activity_groupings( YIELD value UNWIND range(0, size($activity_uids)-1) AS idx MATCH (activity_grouping:ActivityGrouping)<-[:HAS_GROUPING]-(:ActivityValue)<-[:LATEST_FINAL]-(:ActivityRoot {uid:$activity_uids[idx]}) - MATCH (activity_grouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup)-[:IN_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(:ActivityGroupRoot {uid:$activity_group_uids[idx]}) - MATCH (activity_valid_group)<-[:HAS_GROUP]-(:ActivitySubGroupValue)<-[:HAS_VERSION]-(:ActivitySubGroupRoot {uid:$activity_subgroup_uids[idx]}) + MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(:ActivityGroupRoot {uid:$activity_group_uids[idx]}) + MATCH (activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(:ActivitySubGroupValue)<-[:HAS_VERSION]-(:ActivitySubGroupRoot {uid:$activity_subgroup_uids[idx]}) WITH library, concept_root, output, activity_grouping MERGE (output)-[:HAS_ACTIVITY]->(activity_grouping) RETURN concept_root, output, library @@ -471,57 +424,6 @@ def _create_aggregate_root_instance_from_cypher_result( ) for unit in activity_item.get("unit_definitions") ], - odm_form=CompactOdmForm( - uid=( - activity_item["odm_form"]["uid"] - if activity_item["odm_form"] - else None - ), - oid=( - activity_item["odm_form"]["oid"] - if activity_item["odm_form"] - else None - ), - name=( - activity_item["odm_form"]["name"] - if activity_item["odm_form"] - else None - ), - ), - odm_item_group=CompactOdmItemGroup( - uid=( - activity_item["odm_item_group"]["uid"] - if activity_item["odm_item_group"] - else None - ), - oid=( - activity_item["odm_item_group"]["oid"] - if activity_item["odm_item_group"] - else None - ), - name=( - activity_item["odm_item_group"]["name"] - if activity_item["odm_item_group"] - else None - ), - ), - odm_item=CompactOdmItem( - uid=( - activity_item["odm_item"]["uid"] - if activity_item["odm_item"] - else None - ), - oid=( - activity_item["odm_item"]["oid"] - if activity_item["odm_item"] - else None - ), - name=( - activity_item["odm_item"]["name"] - if activity_item["odm_item"] - else None - ), - ), ) for activity_item in input_dict.get("activity_items", []) ], @@ -593,36 +495,6 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( codelist_uid=term_context.has_selected_codelist.single().uid, ) ) - odm_form = None - odm_form_value = activity_item.has_odm_form.single() - if odm_form_value: - odm_form_root = odm_form_value.has_root.single() - if odm_form_root: - odm_form = CompactOdmForm( - uid=odm_form_root.uid, - oid=odm_form_value.oid, - name=odm_form_value.name, - ) - odm_item_group = None - odm_item_group_value = activity_item.has_odm_item_group.single() - if odm_item_group_value: - odm_item_group_root = odm_item_group_value.has_root.single() - if odm_item_group_root: - odm_item_group = CompactOdmItemGroup( - uid=odm_item_group_root.uid, - oid=odm_item_group_value.oid, - name=odm_item_group_value.name, - ) - odm_item = None - odm_item_value = activity_item.has_odm_item.single() - if odm_item_value: - odm_item_root = odm_item_value.has_root.single() - if odm_item_root: - odm_item = CompactOdmItem( - uid=odm_item_root.uid, - oid=odm_item_value.oid, - name=odm_item_value.name, - ) activity_item_vos.append( ActivityItemVO.from_repository_values( is_adam_param_specific=activity_item.is_adam_param_specific, @@ -630,9 +502,6 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( activity_item_class_name=activity_item_class_root.has_latest_value.get_or_none().name, ct_terms=ct_terms, unit_definitions=unit_definitions, - odm_form=odm_form, - odm_item_group=odm_item_group, - odm_item=odm_item, ) ) activity_groupings_nodes = value.has_activity.all() @@ -651,40 +520,35 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( latest_activity = max( all_activity_rels, key=lambda r: version_string_to_tuple(r.version) ) + # ActivityGroup + activity_group_value = activity_grouping.has_selected_group.get() + activity_group_root = activity_group_value.has_version.single() + all_group_rels = activity_group_value.has_version.all_relationships( + activity_group_root + ) + latest_group = max( + all_group_rels, key=lambda r: version_string_to_tuple(r.version) + ) + # ActivitySubGroup + activity_subgroup_value = activity_grouping.has_selected_subgroup.get() + activity_subgroup_root = activity_subgroup_value.has_version.single() + all_subgroup_rels = activity_subgroup_value.has_version.all_relationships( + activity_subgroup_root + ) + latest_subgroup = max( + all_subgroup_rels, key=lambda r: version_string_to_tuple(r.version) + ) - activity_valid_groups = activity_grouping.in_subgroup.all() - for activity_valid_group in activity_valid_groups: - # ActivityGroup - activity_group_value = activity_valid_group.in_group.get() - activity_group_root = activity_group_value.has_version.single() - all_group_rels = activity_group_value.has_version.all_relationships( - activity_group_root - ) - latest_group = max( - all_group_rels, key=lambda r: version_string_to_tuple(r.version) - ) - # ActivitySubGroup - activity_subgroup_value = activity_valid_group.has_group.get() - activity_subgroup_root = activity_subgroup_value.has_version.single() - all_subgroup_rels = ( - activity_subgroup_value.has_version.all_relationships( - activity_subgroup_root - ) - ) - latest_subgroup = max( - all_subgroup_rels, key=lambda r: version_string_to_tuple(r.version) - ) - - activity_groupings.append( - ActivityInstanceGroupingVO( - activity_group_uid=activity_group_root.uid, - activity_group_version=latest_group.version, - activity_subgroup_uid=activity_subgroup_root.uid, - activity_subgroup_version=latest_subgroup.version, - activity_uid=activity_root.uid, - activity_version=latest_activity.version, - ) + activity_groupings.append( + ActivityInstanceGroupingVO( + activity_group_uid=activity_group_root.uid, + activity_group_version=latest_group.version, + activity_subgroup_uid=activity_subgroup_root.uid, + activity_subgroup_version=latest_subgroup.version, + activity_uid=activity_root.uid, + activity_version=latest_activity.version, ) + ) return self.aggregate_class.from_repository_values( uid=root.uid, @@ -761,27 +625,6 @@ def _create_ar( codelist_uid=term["codelist_uid"], ) ) - odm_form = None - if activity_item["odm_form"]: - odm_form = CompactOdmForm( - uid=activity_item["odm_form"]["uid"], - oid=activity_item["odm_form"]["oid"], - name=activity_item["odm_form"]["name"], - ) - odm_item_group = None - if activity_item["odm_item_group"]: - odm_item_group = CompactOdmItemGroup( - uid=activity_item["odm_item_group"]["uid"], - oid=activity_item["odm_item_group"]["oid"], - name=activity_item["odm_item_group"]["name"], - ) - odm_item = None - if activity_item["odm_item"]: - odm_item = CompactOdmItem( - uid=activity_item["odm_item"]["uid"], - oid=activity_item["odm_item"]["oid"], - name=activity_item["odm_item"]["name"], - ) activity_item_vos.append( ActivityItemVO.from_repository_values( is_adam_param_specific=activity_item["is_adam_param_specific"], @@ -789,9 +632,6 @@ def _create_ar( activity_item_class_name=activity_item["activity_item_class_name"], ct_terms=ct_terms, unit_definitions=unit_definitions, - odm_form=odm_form, - odm_item_group=odm_item_group, - odm_item=odm_item, ) ) activity_groupings = [] @@ -898,14 +738,10 @@ def specific_alias_clause(self, **kwargs) -> str: RETURN {uid: term_root.uid, name: term_name_value.name, codelist_uid: codelist_root.uid, submission_value: ct_codelist_term.submission_value} }, unit_definitions: [(activity_item)-[:HAS_UNIT_DEFINITION]->(unit_definition_root:UnitDefinitionRoot)-[:LATEST]->(unit_definition_value:UnitDefinitionValue)-[:HAS_CT_DIMENSION]-(:CTTermRoot)-[:HAS_NAME_ROOT]->(CTTermNamesRoot)-[:LATEST]->(dimension_value:CTTermNameValue) | {uid: unit_definition_root.uid, name: unit_definition_value.name, dimension_name: dimension_value.name}], - is_adam_param_specific: activity_item.is_adam_param_specific, - odm_form: head([(activity_item)-[:HAS_ODM_FORM]->(odm_form_value:OdmFormValue)<-[:HAS_VERSION]-(odm_form_root:OdmFormRoot) | {uid: odm_form_root.uid, oid: odm_form_value.oid, name: odm_form_value.name}]), - odm_item_group: head([(activity_item)-[:HAS_ODM_ITEM_GROUP]->(odm_item_group_value:OdmItemGroupValue)<-[:HAS_VERSION]-(odm_item_group_root:OdmItemGroupRoot) | {uid: odm_item_group_root.uid, oid: odm_item_group_value.oid, name: odm_item_group_value.name}]), - odm_item: head([(activity_item)-[:HAS_ODM_ITEM]->(odm_item_value:OdmItemValue)<-[:HAS_VERSION]-(odm_item_root:OdmItemRoot) | {uid: odm_item_root.uid, oid: odm_item_value.oid, name: odm_item_value.name}]) + is_adam_param_specific: activity_item.is_adam_param_specific }] AS activity_items, head([(concept_value)-[:HAS_ACTIVITY]->(activity_grouping)<-[:HAS_GROUPING]-(activity_value) | activity_value.name]) as activity_name, - apoc.coll.toSet([(concept_value)-[:HAS_ACTIVITY]->(activity_grouping:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(activity_subgroup_value)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) + apoc.coll.toSet([(concept_value)-[:HAS_ACTIVITY]->(activity_grouping:ActivityGrouping) | { activity: head(apoc.coll.sortMulti([(activity_grouping)<-[:HAS_GROUPING]-(activity_value:ActivityValue)<-[has_version:HAS_VERSION]- (activity_root:ActivityRoot) | @@ -915,7 +751,7 @@ def specific_alias_clause(self, **kwargs) -> str: major_version: toInteger(split(has_version.version,'.')[0]), minor_version: toInteger(split(has_version.version,'.')[1]) }], ['major_version', 'minor_version'])), - activity_subgroup: head(apoc.coll.sortMulti([(activity_valid_group)<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue)<-[has_version:HAS_VERSION]- + activity_subgroup: head(apoc.coll.sortMulti([(activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[has_version:HAS_VERSION]- (activity_subgroup_root:ActivitySubGroupRoot) | { uid: activity_subgroup_root.uid, @@ -923,7 +759,7 @@ def specific_alias_clause(self, **kwargs) -> str: major_version: toInteger(split(has_version.version,'.')[0]), minor_version: toInteger(split(has_version.version,'.')[1]) }], ['major_version', 'minor_version'])), - activity_group: head(apoc.coll.sortMulti([(activity_valid_group)-[:IN_GROUP]-(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- + activity_group: head(apoc.coll.sortMulti([(activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- (activity_group_root:ActivityGroupRoot) | { uid: activity_group_root.uid, @@ -960,8 +796,7 @@ def create_query_filter_statement( if kwargs.get("activity_subgroup_names") is not None: activity_subgroup_names = kwargs.get("activity_subgroup_names") filter_by_activity_subgroup_names = ( - "size([(concept_value)-[:HAS_ACTIVITY]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup)" - "<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) " + "size([(concept_value)-[:HAS_ACTIVITY]->(:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) " "WHERE activity_subgroup_value.name IN $activity_subgroup_names | activity_subgroup_value.name]) > 0" ) filter_parameters.append(filter_by_activity_subgroup_names) @@ -969,8 +804,7 @@ def create_query_filter_statement( if kwargs.get("activity_group_names") is not None: activity_group_names = kwargs.get("activity_group_names") filter_by_activity_group_names = ( - "size([(concept_value)-[:HAS_ACTIVITY]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup)" - "-[:IN_GROUP]->(activity_group_value:ActivityGroupValue) " + "size([(concept_value)-[:HAS_ACTIVITY]->(:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue) " "WHERE activity_group_value.name IN $activity_group_names | activity_group_value.name]) > 0" ) filter_parameters.append(filter_by_activity_group_names) @@ -1056,14 +890,20 @@ def get_activity_instance_overview( WITH activity_grouping MATCH (activity_grouping)<-[:HAS_GROUPING]-(av:ActivityValue)<-[hav:HAS_VERSION]-(ar:ActivityRoot) WITH av, hav, ar - ORDER BY + ORDER BY toInteger(split(hav.version, '.')[0]) DESC, toInteger(split(hav.version, '.')[1]) DESC, hav.start_date DESC WITH ar, collect(av) AS avs, collect(hav) AS havs WITH ar, head(avs) AS activity_value, head(havs) AS latest_version - RETURN ar.uid AS activity_uid, activity_value, - latest_version { .version, .status, .start_date, .end_date} AS activity_version_info + CALL { + WITH latest_version + OPTIONAL MATCH (author:User) + WHERE author.user_id = latest_version.author_id + RETURN author + } + RETURN ar.uid AS activity_uid, activity_value, + latest_version { .version, .status, .start_date, .end_date, author_username: coalesce(author.username, latest_version.author_id)} AS activity_version_info } WITH activity_grouping, activity_uid, activity_value, activity_version_info, [(activity_value)<-[hav:HAS_VERSION]-(activity_root:ActivityRoot) | hav { .version, .status, .start_date, .end_date}] AS activity_versions, @@ -1072,7 +912,7 @@ def get_activity_instance_overview( // Get the latest subgroup version using subquery CALL { WITH activity_grouping - OPTIONAL MATCH (activity_grouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup)<-[:HAS_GROUP]-(sgv:ActivitySubGroupValue)<-[sgv_rel:HAS_VERSION]-(sgr:ActivitySubGroupRoot) + OPTIONAL MATCH (activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(sgv:ActivitySubGroupValue)<-[sgv_rel:HAS_VERSION]-(sgr:ActivitySubGroupRoot) WITH sgr, sgv, sgv_rel ORDER BY toInteger(split(sgv_rel.version, '.')[0]) DESC, @@ -1087,7 +927,7 @@ def get_activity_instance_overview( // Get the latest group version using subquery CALL { WITH activity_grouping - OPTIONAL MATCH (activity_grouping)-[:IN_SUBGROUP]->(avg)-[:IN_GROUP]->(agv:ActivityGroupValue)<-[agv_rel:HAS_VERSION]-(agr:ActivityGroupRoot) + OPTIONAL MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(agv:ActivityGroupValue)<-[agv_rel:HAS_VERSION]-(agr:ActivityGroupRoot) WITH agr, agv, agv_rel ORDER BY toInteger(split(agv_rel.version, '.')[0]) DESC, @@ -1138,12 +978,15 @@ def get_activity_instance_overview( -[:HAS_CT_DIMENSION]-(:CTTermContext)-[:HAS_SELECTED_TERM]-(:CTTermRoot)-[:HAS_NAME_ROOT]->(CTTermNamesRoot)-[:LATEST]->(dimension_value:CTTermNameValue) | {uid: unit_definition_root.uid, name: unit_definition_value.name, dimension_name: dimension_value.name} ], - is_adam_param_specific: activity_item.is_adam_param_specific, - odm_form: head([(activity_item)-[:HAS_ODM_FORM]->(odm_form_value:OdmFormValue)<-[:HAS_VERSION]-(odm_form_root:OdmFormRoot) | {uid: odm_form_root.uid, oid: odm_form_value.oid, name: odm_form_value.name}]), - odm_item_group: head([(activity_item)-[:HAS_ODM_ITEM_GROUP]->(odm_item_group_value:OdmItemGroupValue)<-[:HAS_VERSION]-(odm_item_group_root:OdmItemGroupRoot) | {uid: odm_item_group_root.uid, oid: odm_item_group_value.oid, name: odm_item_group_value.name}]), - odm_item: head([(activity_item)-[:HAS_ODM_ITEM]->(odm_item_value:OdmItemValue)<-[:HAS_VERSION]-(odm_item_root:OdmItemRoot) | {uid: odm_item_root.uid, oid: odm_item_value.oid, name: odm_item_value.name}]) + is_adam_param_specific: activity_item.is_adam_param_specific } ]) AS activity_items + CALL { + WITH has_version + OPTIONAL MATCH (author:User) + WHERE author.user_id = has_version.author_id + RETURN author + } WITH DISTINCT activity_instance_root, activity_instance_value, @@ -1151,7 +994,10 @@ def get_activity_instance_overview( activity_instance_class, hierarchy, activity_items, - has_version, + has_version { + .*, + author_username: coalesce(author.username, has_version.author_id) + } AS has_version, apoc.coll.dropDuplicateNeighbors( [v in apoc.coll.sortMulti( [v in all_versions | { @@ -1190,8 +1036,7 @@ def get_cosmos_activity_instance_overview(self, uid: str) -> dict[str, Any]: (activity_instance_class_root:ActivityInstanceClassRoot)-[:LATEST]->(activity_instance_class_value:ActivityInstanceClassValue) | activity_instance_class_value.name]) AS activity_instance_class_name WITH *, - [(activity_instance_value)-[:HAS_ACTIVITY]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) | activity_subgroup_value.name] AS activity_subgroups, + [(activity_instance_value)-[:HAS_ACTIVITY]->(:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) | activity_subgroup_value.name] AS activity_subgroups, apoc.coll.toSet([(activity_instance_value)-[:CONTAINS_ACTIVITY_ITEM]->(activity_item) <-[HAS_ACTIVITY_ITEM]-(activity_item_class_root)-[:LATEST]->(activity_item_class_value) | { @@ -1248,8 +1093,8 @@ def get_all_activity_instances_for_activity_grouping( OPTIONAL MATCH (activity_instance_root)-[retired:HAS_VERSION {status: "Retired"}]->(activity_instance_value) WHERE retired.end_date IS NULL WITH activity_instance_root, activity_instance_value WHERE retired IS NULL MATCH (activity_instance_value)-[:HAS_ACTIVITY]->(activity_grouping:ActivityGrouping)<-[:HAS_GROUPING]-(:ActivityValue)<-[:HAS_VERSION]-(:ActivityRoot {uid:$activity_uid}) - MATCH (activity_grouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup)<-[:HAS_GROUP]-(:ActivitySubGroupValue)<-[:HAS_VERSION]-(:ActivitySubGroupRoot {uid:$activity_subgroup_uid}) - MATCH (activity_valid_group)-[:IN_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(:ActivityGroupRoot {uid:$activity_group_uid}) + MATCH (activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(:ActivitySubGroupValue)<-[:HAS_VERSION]-(:ActivitySubGroupRoot {uid:$activity_subgroup_uid}) + MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(:ActivityGroupRoot {uid:$activity_group_uid}) WITH DISTINCT activity_instance_root, activity_instance_value ORDER BY activity_instance_value.is_required_for_activity DESC, activity_instance_value.is_defaulted_for_activity DESC RETURN activity_instance_root as root, activity_instance_value as value diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_repository.py index b475473f..502bac63 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_repository.py @@ -6,15 +6,11 @@ from clinical_mdr_api.domain_repositories.concepts.concept_generic_repository import ( ConceptGenericRepository, ) -from clinical_mdr_api.domain_repositories.concepts.utils import ( - list_concept_wildcard_properties, -) -from clinical_mdr_api.domain_repositories.models._utils import ( - format_generic_header_values, -) from clinical_mdr_api.domain_repositories.models.activities import ( ActivityGrouping, + ActivityGroupRoot, ActivityRoot, + ActivitySubGroupRoot, ActivityValue, ) from clinical_mdr_api.domain_repositories.models.generic import ( @@ -33,24 +29,11 @@ LibraryItemStatus, LibraryVO, ) -from clinical_mdr_api.models.concepts.activities.activity import ( - Activity, - CompactActivity, -) +from clinical_mdr_api.models.concepts.activities.activity import Activity from clinical_mdr_api.models.utils import GenericFilteringReturn -from clinical_mdr_api.repositories._utils import ( - CypherQueryBuilder, - FilterDict, - FilterOperator, - validate_filters_and_add_search_string, -) from common.config import settings from common.exceptions import BusinessLogicException -from common.utils import ( - convert_to_datetime, - get_db_result_as_dict, - version_string_to_tuple, -) +from common.utils import convert_to_datetime, version_string_to_tuple def _get_display_version(versions: list[dict[Any, Any]]) -> dict[Any, Any] | None: @@ -270,45 +253,44 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( activity_instance_value.is_legacy_usage ) - activity_valid_groups = activity_grouping.in_subgroup.all() - for activity_valid_group in activity_valid_groups: - # ActivityGroup - activity_group_value = activity_valid_group.in_group.get() - activity_group_root = activity_group_value.has_version.single() - all_group_rels = [ - has_version - for has_version in activity_group_value.has_version.all_relationships( - activity_group_root - ) - if has_version.status in ["Final", "Retired"] - ] - latest_group = max( - all_group_rels, key=lambda r: version_string_to_tuple(r.version) + # ActivityGroup + activity_group_value = activity_grouping.has_selected_group.single() + activity_group_root = activity_group_value.has_version.single() + all_group_rels = [ + has_version + for has_version in activity_group_value.has_version.all_relationships( + activity_group_root ) - # ActivitySubGroup - activity_subgroup_value = activity_valid_group.has_group.get() - activity_subgroup_root = activity_subgroup_value.has_version.single() - all_subgroup_rels = [ - has_version - for has_version in activity_subgroup_value.has_version.all_relationships( - activity_subgroup_root - ) - if has_version.status in ["Final", "Retired"] - ] - latest_subgroup = max( - all_subgroup_rels, key=lambda r: version_string_to_tuple(r.version) + if has_version.status in ["Final", "Retired"] + ] + latest_group = max( + all_group_rels, key=lambda r: version_string_to_tuple(r.version) + ) + + # ActivitySubGroup + activity_subgroup_value = activity_grouping.has_selected_subgroup.single() + activity_subgroup_root = activity_subgroup_value.has_version.single() + all_subgroup_rels = [ + has_version + for has_version in activity_subgroup_value.has_version.all_relationships( + activity_subgroup_root ) + if has_version.status in ["Final", "Retired"] + ] + latest_subgroup = max( + all_subgroup_rels, key=lambda r: version_string_to_tuple(r.version) + ) - activity_groupings.append( - ActivityGroupingVO( - activity_group_uid=activity_group_root.uid, - activity_group_name=activity_group_value.name, - activity_group_version=latest_group.version, - activity_subgroup_uid=activity_subgroup_root.uid, - activity_subgroup_name=activity_subgroup_value.name, - activity_subgroup_version=latest_subgroup.version, - ) + activity_groupings.append( + ActivityGroupingVO( + activity_group_uid=activity_group_root.uid, + activity_group_name=activity_group_value.name, + activity_group_version=latest_group.version, + activity_subgroup_uid=activity_subgroup_root.uid, + activity_subgroup_name=activity_subgroup_value.name, + activity_subgroup_version=latest_subgroup.version, ) + ) requester_study_id = None # We are only interested in the StudyId of the Activity Requests if library.name == settings.requested_library_name: @@ -378,16 +360,16 @@ def create_query_filter_statement( activity_grouping_query_text = "activity_grouping" else: activity_grouping_query_text = ( - "concept_value)-[:HAS_GROUPING]->(:ActivityGrouping" + "concept_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping" ) if activity_subgroup_uid and activity_group_uid: filter_by_subgroup_and_group_uid = f""" {{activity_subgroup_uid: $activity_subgroup_uid, activity_group_uid: $activity_group_uid}} IN - [({activity_grouping_query_text})-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) | + [({activity_grouping_query_text}) | {{ - activity_subgroup_uid: head([(activity_valid_group)<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) + activity_subgroup_uid: head([(activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) <-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | activity_subgroup_root.uid]), - activity_group_uid: head([(activity_valid_group)-[:IN_GROUP]->(activity_group_value:ActivityGroupValue) + activity_group_uid: head([(activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue) <-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | activity_group_root.uid]) }} ]""" @@ -396,15 +378,15 @@ def create_query_filter_statement( filter_query_parameters["activity_group_uid"] = activity_group_uid if activity_subgroup_uid is not None and activity_group_uid is None: filter_by_activity_subgroup_uid = f""" - $activity_subgroup_uid IN [({activity_grouping_query_text})-[:IN_SUBGROUP]-> - (:ActivityValidGroup)<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) + $activity_subgroup_uid IN [({activity_grouping_query_text})-[:HAS_SELECTED_SUBGROUP]-> + (activity_subgroup_value:ActivitySubGroupValue) <-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | activity_subgroup_root.uid]""" filter_parameters.append(filter_by_activity_subgroup_uid) filter_query_parameters["activity_subgroup_uid"] = activity_subgroup_uid if activity_group_uid is not None and activity_subgroup_uid is None: filter_by_activity_group_uid = f""" - $activity_group_uid IN [({activity_grouping_query_text})-[:IN_SUBGROUP]-> - (:ActivityValidGroup)-[:IN_GROUP]->(activity_group_value:ActivityGroupValue) + $activity_group_uid IN [({activity_grouping_query_text})-[:HAS_SELECTED_GROUP]-> + (activity_group_value:ActivityGroupValue) <-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | activity_group_root.uid]""" filter_parameters.append(filter_by_activity_group_uid) filter_query_parameters["activity_group_uid"] = activity_group_uid @@ -416,15 +398,15 @@ def create_query_filter_statement( if kwargs.get("activity_subgroup_names") is not None: activity_subgroup_names = kwargs.get("activity_subgroup_names") filter_by_activity_subgroup_names = f""" - size([({activity_grouping_query_text})-[:IN_SUBGROUP]-> - (:ActivityValidGroup)<-[:HAS_GROUP]-(asgv:ActivitySubGroupValue) WHERE asgv.name IN $activity_subgroup_names | asgv.name]) > 0""" + size([({activity_grouping_query_text})-[:HAS_SELECTED_SUBGROUP]-> + (asgv:ActivitySubGroupValue) WHERE asgv.name IN $activity_subgroup_names | asgv.name]) > 0""" filter_parameters.append(filter_by_activity_subgroup_names) filter_query_parameters["activity_subgroup_names"] = activity_subgroup_names if kwargs.get("activity_group_names") is not None: activity_group_names = kwargs.get("activity_group_names") filter_by_activity_group_names = f""" - size([({activity_grouping_query_text})-[:IN_SUBGROUP]-> - (:ActivityValidGroup)-[:IN_GROUP]->(agv:ActivityGroupValue) WHERE agv.name IN $activity_group_names | agv.name]) > 0""" + size([({activity_grouping_query_text})-[:HAS_SELECTED_GROUP]-> + (agv:ActivityGroupValue) WHERE agv.name IN $activity_group_names | agv.name]) > 0""" filter_parameters.append(filter_by_activity_group_names) filter_query_parameters["activity_group_names"] = activity_group_names extended_filter_statements = " AND ".join(filter_parameters) @@ -466,45 +448,26 @@ def _create_new_value_node(self, ar: ActivityAR) -> ActivityValue: # link ActivityValue and ActivityGrouping nodes value_node.has_grouping.connect(activity_grouping_node) - # find related ActivityValidGroup node - query = """ - MATCH (activity_valid_group:ActivityValidGroup)-[:IN_GROUP]-(agv:ActivityGroupValue)<-[:HAS_VERSION]-(agr:ActivityGroupRoot {uid:$activity_group_uid}) - MATCH (activity_valid_group)-[:HAS_GROUP]-(asgv:ActivitySubGroupValue)<-[:HAS_VERSION]-(asgr:ActivitySubGroupRoot {uid:$activity_subgroup_uid}) - WITH *, - head([(asgv)<-[latest:LATEST]-(asgr) | latest]) AS is_subgroup_latest, - head([(agv)<-[latest:LATEST]-(agr) | latest]) AS is_group_latest - RETURN activity_valid_group - ORDER BY is_subgroup_latest, is_group_latest - """ - activity_valid_groups_ret, _ = db.cypher_query( - query, - params={ - "activity_group_uid": activity_grouping.activity_group_uid, - "activity_subgroup_uid": activity_grouping.activity_subgroup_uid, - }, - resolve_objects=True, - ) - BusinessLogicException.raise_if( - len(activity_valid_groups_ret) == 0, - msg=f"The ActivityValidGroup node wasn't found for Activity Subgroup '{activity_grouping.activity_subgroup_uid}' " - f"and Activity Group '{activity_grouping.activity_group_uid}'.", - ) - activity_valid_group_node = activity_valid_groups_ret[0][0] - # link ActivityGrouping and ActivityValidGroup - activity_grouping_node.in_subgroup.connect(activity_valid_group_node) + # link ActivityGrouping with ActivityGroupValue and ActivitySubGroupValue nodes + group_node = ActivityGroupRoot.nodes.get( + uid=activity_grouping.activity_group_uid + ).has_latest_value.single() + subgroup_node = ActivitySubGroupRoot.nodes.get( + uid=activity_grouping.activity_subgroup_uid + ).has_latest_value.single() + activity_grouping_node.has_selected_subgroup.connect(subgroup_node) + activity_grouping_node.has_selected_group.connect(group_node) return value_node def _any_subgroup_updated(self, activity_value): for grouping_node in activity_value.has_grouping.all(): - if ( - not grouping_node.in_subgroup.get() - .has_group.get() - .has_latest_value.single() - ): - # The linked subgroup is not the latest. - # We need to return True, so that the ActivityValue - # gets updated to use the new subgroup value. + # If the linked subgroup is not the latest. + # We need to return True, so that the ActivityValue + # gets updated to use the new group and/or subgroup value. + if not grouping_node.has_selected_group.get().has_latest_value.single(): + return True + if not grouping_node.has_selected_subgroup.get().has_latest_value.single(): return True return False @@ -526,14 +489,14 @@ def _has_data_changed(self, ar: ActivityAR, value: ActivityValue) -> bool: activity_group_uids = [] activity_grouping_nodes = value.has_grouping.all() for activity_grouping_node in activity_grouping_nodes: - activity_valid_group_nodes = activity_grouping_node.in_subgroup.all() - for activity_valid_group_node in activity_valid_group_nodes: - activity_subgroup_uids.append( - activity_valid_group_node.has_group.get().has_version.single().uid - ) - activity_group_uids.append( - activity_valid_group_node.in_group.get().has_version.single().uid - ) + activity_group_value = activity_grouping_node.has_selected_group.single() + activity_subgroup_value = ( + activity_grouping_node.has_selected_subgroup.single() + ) + activity_subgroup_uids.append( + activity_subgroup_value.has_version.single().uid + ) + activity_group_uids.append(activity_group_value.has_version.single().uid) # Is this a final or retired version? If yes, we skip the check for updated subgroups # to avoid creating new values nodes when just creating a new draft. @@ -598,14 +561,16 @@ def copy_activity_and_recreate_activity_groupings( UNWIND range(0, size($activity_group_uids)-1) AS idx CREATE (activity_grouping:ActivityGrouping) WITH library, concept_root, output, activity_grouping, idx - MATCH (activity_valid_group:ActivityValidGroup)-[:IN_GROUP]->(:ActivityGroupValue)<-[:LATEST]-(:ActivityGroupRoot {uid:$activity_group_uids[idx]}) - MATCH (activity_valid_group)<-[:HAS_GROUP]-(:ActivitySubGroupValue)<-[:LATEST]-(:ActivitySubGroupRoot {uid:$activity_subgroup_uids[idx]}) - WITH library, concept_root, output, activity_grouping, activity_valid_group - MERGE (output)-[:HAS_GROUPING]->(activity_grouping)-[:IN_SUBGROUP]->(activity_valid_group) + MATCH (activity_group_value:ActivityGroupValue)<-[:LATEST]-(:ActivityGroupRoot {uid:$activity_group_uids[idx]}) + MATCH (activity_subgroup_value:ActivitySubGroupValue)<-[:LATEST]-(:ActivitySubGroupRoot {uid:$activity_subgroup_uids[idx]}) + WITH library, concept_root, output, activity_grouping, activity_subgroup_value, activity_group_value + MERGE (activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value) + MERGE (activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value) + MERGE (output)-[:HAS_GROUPING]->(activity_grouping) RETURN concept_root, output, library """ - created_node, _ = db.cypher_query( + _, _ = db.cypher_query( query, params={ "activity_uid": activity.uid, @@ -624,8 +589,6 @@ def copy_activity_and_recreate_activity_groupings( ], }, ) - if len(created_node) > 0: - ActivityGrouping.generate_node_uids_if_not_present() @classmethod def format_filter_sort_keys(cls, key: str) -> str: @@ -656,17 +619,33 @@ def format_filter_sort_keys(cls, key: str) -> str: def specific_alias_clause(self, **kwargs) -> str: # concept_value property comes from the main part of the query # which is specified in the concept_generic_repository - activity_subgroup_names = self.filter_query_parameters.get( - "activity_subgroup_names" - ) - activity_group_names = self.filter_query_parameters.get("activity_group_names") if kwargs.get("group_by_groupings") is False: activity_grouping_query_text = "activity_grouping" else: activity_grouping_query_text = ( - "concept_value)-[:HAS_GROUPING]->(:ActivityGrouping" + "concept_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping" ) + + # check if sorting by activity group/subgroup name is requested + grouping_sort_clause = "" + sort_by = self.sort_by or {} + group_desc = sort_by.get("activity_groupings[0].activity_group_name") + subgroup_desc = sort_by.get("activity_groupings[0].activity_subgroup_name") + if subgroup_desc is not None or group_desc is not None: + orderings = [] + if group_desc is not None: + if group_desc: + orderings.append("group.name DESC") + else: + orderings.append("group.name") + if subgroup_desc is not None: + if subgroup_desc: + orderings.append("subgroup.name DESC") + else: + orderings.append("subgroup.name") + grouping_sort_clause = f"ORDER BY {', '.join(orderings)}" + return f""" WITH *, concept_value.synonyms AS synonyms, @@ -683,33 +662,33 @@ def specific_alias_clause(self, **kwargs) -> str: END AS requester_study_id, coalesce(concept_value.is_data_collected, False) AS is_data_collected, coalesce(concept_value.is_multiple_selection_allowed, True) AS is_multiple_selection_allowed, - apoc.coll.toSet([({activity_grouping_query_text})-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - {'WHERE size([(activity_valid_group)<-[:HAS_GROUP]-(activity_subgroup_value) WHERE activity_subgroup_value.name in $activity_subgroup_names | activity_subgroup_value.name]) > 0 ' - if activity_subgroup_names - else ''} - {'AND' if activity_subgroup_names and activity_group_names else ''} - {'' if activity_subgroup_names and activity_group_names else 'WHERE' if not activity_subgroup_names and activity_group_names else ''} - {'size([(activity_valid_group)-[:IN_GROUP]-(activity_group_value) WHERE activity_group_value.name in $activity_group_names | activity_group_value.name]) > 0 ' - if activity_group_names - else ''} - | {{ - activity_subgroup: head(apoc.coll.sortMulti([(activity_valid_group)<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) - <-[has_version:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) WHERE has_version.status='Final' - | {{ - uid:activity_subgroup_root.uid, - major_version: toInteger(split(has_version.version,'.')[0]), - minor_version: toInteger(split(has_version.version,'.')[1]), - name:activity_subgroup_value.name - }}], ['major_version', 'minor_version'])), - activity_group: head(apoc.coll.sortMulti([(activity_valid_group)-[:IN_GROUP]-(activity_group_value:ActivityGroupValue) + COLLECT {{ + MATCH (concept_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping) + MATCH (activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) + MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue) + WITH activity_subgroup_value, activity_group_value + WITH + head(apoc.coll.sortMulti([(activity_subgroup_value:ActivitySubGroupValue) + <-[has_version:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) WHERE has_version.status='Final' + | {{ + uid:activity_subgroup_root.uid, + major_version: toInteger(split(has_version.version,'.')[0]), + minor_version: toInteger(split(has_version.version,'.')[1]), + name:activity_subgroup_value.name + }}], ['major_version', 'minor_version'])) AS subgroup, + head(apoc.coll.sortMulti([(activity_group_value) <-[has_version:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) WHERE has_version.status='Final' - | {{ - uid:activity_group_root.uid, - major_version: toInteger(split(has_version.version,'.')[0]), - minor_version: toInteger(split(has_version.version,'.')[1]), - name:activity_group_value.name - }}], ['major_version', 'minor_version'])) - }}]) AS activity_groupings, + | {{ + uid:activity_group_root.uid, + major_version: toInteger(split(has_version.version,'.')[0]), + minor_version: toInteger(split(has_version.version,'.')[1]), + name:activity_group_value.name + }}], ['major_version', 'minor_version'])) AS group + WITH subgroup, group {grouping_sort_clause} + WITH {{activity_group: group, activity_subgroup: subgroup}} AS grouping + RETURN grouping + }} AS activity_groupings, + apoc.coll.toSet([({activity_grouping_query_text})<-[:HAS_ACTIVITY]-(activity_instance_value:ActivityInstanceValue) <-[has_version:HAS_VERSION]-(activity_instance_root:ActivityInstanceRoot) | {{uid: activity_instance_root.uid, name: activity_instance_value.name}}]) AS activity_instances, head([(concept_value)-[:REPLACED_BY_ACTIVITY]->(replacing_activity_root:ActivityRoot) | replacing_activity_root.uid]) AS replaced_by_activity, @@ -737,187 +716,6 @@ def specific_alias_clause(self, **kwargs) -> str: END as is_used_by_legacy_instances """ - def _get_compact_activity_with_splitted_groupings_query( - self, filter_statements: str - ): - match_clause = """ - MATCH (concept_root:ActivityRoot)-[:LATEST]->(concept_value:ActivityValue) - MATCH (concept_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) - MATCH (activity_valid_group)-[:IN_GROUP]->(activity_group_value:ActivityGroupValue) - """ - match_clause += filter_statements - - alias_clause = """ - DISTINCT concept_root, concept_value, activity_grouping, activity_subgroup_value, activity_group_value - CALL { - WITH concept_root, concept_value - MATCH (concept_root)-[hv:HAS_VERSION]-(concept_value) - WITH hv - ORDER BY - toInteger(split(hv.version, '.')[0]) ASC, - toInteger(split(hv.version, '.')[1]) ASC, - hv.end_date ASC, - hv.start_date ASC - WITH collect(hv) as hvs - RETURN last(hvs) AS version_rel - } - WITH *, - apoc.coll.sortMulti([(activity_grouping)<-[:HAS_ACTIVITY]-(activity_instance_value:ActivityInstanceValue) - <-[instance_version:HAS_VERSION WHERE instance_version.status='Final' and instance_version.end_date IS NULL]-(activity_instance_root:ActivityInstanceRoot) | - { - uid:activity_instance_root.uid, - legacy_code:activity_instance_value.is_legacy_usage, - major_version: toInteger(split(instance_version.version,'.')[0]), - minor_version: toInteger(split(instance_version.version,'.')[1]) - }], ['^uid', 'major_version', 'minor_version']) AS all_legacy_codes - WITH *, - // Sort by uid and instance_version in descending order and leave only latest version of same ActivityInstances - [ - i in range(0, size(all_legacy_codes) -1) - WHERE i=0 OR all_legacy_codes[i].uid <> all_legacy_codes[i-1].uid | all_legacy_codes[i].legacy_code ] as all_legacy_codes - WITH - concept_root.uid AS uid, - concept_value.name AS name, - concept_value.definition AS definition, - concept_value.synonyms AS synonyms, - concept_value.abbreviation AS abbreviation, - coalesce(concept_value.is_data_collected, False) AS is_data_collected, - version_rel.status AS status, - activity_group_value.name AS activity_group_name, - head([(activity_group_root:ActivityGroupRoot)-[:HAS_VERSION]->(activity_group_value) | activity_group_root.uid]) as activity_group_uid, - activity_subgroup_value.name AS activity_subgroup_name, - head([(activity_subgroup_root:ActivitySubGroupRoot)-[:HAS_VERSION]->(activity_subgroup_value) | activity_subgroup_root.uid]) as activity_subgroup_uid, - head([(library)-[:CONTAINS_CONCEPT]->(concept_root) | library.name]) AS library_name, - CASE WHEN size(all_legacy_codes) > 0 - THEN all(is_legacy_usage IN all_legacy_codes where is_legacy_usage=true and is_legacy_usage IS NOT NULL) - ELSE false - END as is_used_by_legacy_instances - """ - return match_clause, alias_clause - - def get_compact_activity_with_splitted_groupings( - self, - library: str | None = None, - sort_by: dict[str, bool] | None = None, - page_number: int = 1, - page_size: int = 0, - filter_by: dict[str, dict[str, Any]] | None = None, - filter_operator: FilterOperator = FilterOperator.AND, - total_count: bool = False, - ) -> tuple[list[dict[str, Any]], int]: - """ - Method runs a cypher query to fetch all needed data to create objects of type AggregateRootType. - In the case of the following repository it will be some Concept aggregates. - - It uses cypher instead of neomodel as neomodel approach triggered some performance issues, because it is needed - to traverse many relationships to fetch all needed data and each traversal is separate database call when using - neomodel. - :param library: - :param sort_by: - :param page_number: - :param page_size: - :param filter_by: - :param filter_operator: - :param total_count: - :param return_all_versions: - :return GenericFilteringReturn[_AggregateRootType]: - """ - filter_statements, filter_query_parameters = self.create_query_filter_statement( - library=library, - ) - - self.filter_query_parameters = filter_query_parameters - match_clause, alias_clause = ( - self._get_compact_activity_with_splitted_groupings_query( - filter_statements=filter_statements - ) - ) - query = CypherQueryBuilder( - match_clause=match_clause, - alias_clause=alias_clause, - sort_by=sort_by, - page_number=page_number, - page_size=page_size, - filter_by=FilterDict.model_validate({"elements": filter_by}), - filter_operator=filter_operator, - total_count=total_count, - return_model=CompactActivity, - ) - query.parameters.update(filter_query_parameters) - - result_array, attributes_names = query.execute() - - items = [get_db_result_as_dict(row, attributes_names) for row in result_array] - - count_result, _ = db.cypher_query( - query=query.count_query, params=query.parameters - ) - total_amount = ( - count_result[0][0] if len(count_result) > 0 and total_count else 0 - ) - - return items, total_amount - - def get_compact_activity_with_splitted_groupings_distinct_headers( - self, - field_name: str, - search_string: str = "", - library: str | None = None, - filter_by: dict[str, dict[str, Any]] | None = None, - filter_operator: FilterOperator = FilterOperator.AND, - page_size: int = 10, - **kwargs, - ) -> list[Any]: - # pylint: disable=unused-argument - """ - Method runs a cypher query to fetch possible values for a given field_name, with a limit of page_size. - It uses generic filtering capability, on top of filtering the field_name with provided search_string. - - :param field_name: Field name for which to return possible values - :param search_string - :param library: - :param filter_by: - :param filter_operator: Same as for generic filtering - :param page_size: Max number of values to return. Default 10 - :return list[Any]: - """ - - # Add header field name to filter_by, to filter with a CONTAINS pattern - filter_by = validate_filters_and_add_search_string( - search_string, field_name, filter_by - ) - filter_statements, filter_query_parameters = self.create_query_filter_statement( - library=library, - ) - match_clause, alias_clause = ( - self._get_compact_activity_with_splitted_groupings_query( - filter_statements=filter_statements - ) - ) - - # Use Cypher query class to use reusable helper methods - query = CypherQueryBuilder( - filter_by=FilterDict.model_validate({"elements": filter_by}), - filter_operator=filter_operator, - match_clause=match_clause, - alias_clause=alias_clause, - return_model=CompactActivity, - wildcard_properties_list=list_concept_wildcard_properties(CompactActivity), - ) - - query.parameters.update(filter_query_parameters) - query.full_query = query.build_header_query( - header_alias=field_name, page_size=page_size - ) - result_array, _ = query.execute() - - return ( - format_generic_header_values(result_array[0][0]) - if len(result_array) > 0 - else [] - ) - def replace_request_with_sponsor_activity( self, activity_request_uid: str, sponsor_activity_uid: str ) -> None: @@ -998,6 +796,7 @@ def get_activity_overview( apoc.coll.sortMulti([(activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)<-[:HAS_ACTIVITY]- (activity_instance_value:ActivityInstanceValue)<-[aihv:HAS_VERSION]-(activity_instance_root:ActivityInstanceRoot) WHERE NOT EXISTS ((activity_instance_value)<--(:DeletedActivityInstanceRoot)) + AND aihv.end_date IS NULL | { activity_instance_library_name: head([(library)-[:CONTAINS_CONCEPT]->(activity_instance_root) | library.name]), uid: activity_instance_root.uid, @@ -1021,16 +820,28 @@ def get_activity_overview( activity_instance_class: head([(activity_instance_value)-[:ACTIVITY_INSTANCE_CLASS]->(activity_instance_class_root:ActivityInstanceClassRoot) -[:LATEST]->(activity_instance_class_value:ActivityInstanceClassValue) | activity_instance_class_value]) }], ['^uid', 'version.major_version', 'version.minor_version']) AS activity_instances, - apoc.coll.toSet([(activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) + apoc.coll.toSet([(activity_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping) | { - activity_subgroup_value: activity_subgroup_value, - activity_subgroup_uid: activity_subgroup_root.uid, - activity_group_value: head([(activity_valid_group)-[:IN_GROUP]->(activity_group_value:ActivityGroupValue) - <-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | activity_group_value]), - activity_group_uid: head([(activity_valid_group)-[:IN_GROUP]->(activity_group_value:ActivityGroupValue) + activity_subgroup_value: head([(activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) + | activity_subgroup_value]), + activity_subgroup_uid: head([(activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) + <-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | activity_subgroup_root.uid]), + activity_group_value: head([(activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue) + | activity_group_value]), + activity_group_uid: head([(activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue) <-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | activity_group_root.uid]) }]) AS hierarchy + CALL { + WITH has_version + OPTIONAL MATCH (author:User) + WHERE author.user_id = has_version.author_id + RETURN author + } + WITH *, + has_version { + .*, + author_username: coalesce(author.username, has_version.author_id) + } AS has_version RETURN hierarchy, activity_root, @@ -1069,6 +880,7 @@ def get_cosmos_activity_overview(self, uid: str) -> dict[str, Any]: (activity_instance_value:ActivityInstanceValue)-[:ACTIVITY_INSTANCE_CLASS]-> (activity_instance_class_root:ActivityInstanceClassRoot)-[:LATEST]->(activity_instance_class_value:ActivityInstanceClassValue) WHERE NOT EXISTS ((activity_instance_value)<--(:DeletedActivityInstanceRoot)) + AND (:ActivityInstanceRoot)-[:HAS_VERSION {end_date: null}]->(activity_instance_value) | { name:activity_instance_value.name, nci_concept_id:activity_instance_value.nci_concept_id, @@ -1076,8 +888,7 @@ def get_cosmos_activity_overview(self, uid: str) -> dict[str, Any]: definition:activity_instance_value.definition, activity_instance_class_name: activity_instance_class_value.name }]) AS activity_instances, - [(activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue) | activity_subgroup_value.name] AS activity_subgroups + [(activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue) | activity_subgroup_value.name] AS activity_subgroups WITH activity_root, activity_value, activity_subgroups, apoc.coll.sortMaps(activity_instances, '^name') as activity_instances @@ -1188,9 +999,9 @@ def generic_match_clause_all_versions(self): return """ MATCH (concept_root:ActivityRoot)-[version:HAS_VERSION]->(concept_value:ActivityValue) -[:HAS_GROUPING]->(ag:ActivityGrouping) - -[:IN_SUBGROUP]->(avg:ActivityValidGroup)<-[:HAS_GROUP]-(subgroup_val:ActivitySubGroupValue)<-[:HAS_VERSION]-(subgroup_root:ActivitySubGroupRoot) + -[:HAS_SELECTED_SUBGROUP]->(subgroup_val:ActivitySubGroupValue)<-[:HAS_VERSION]-(subgroup_root:ActivitySubGroupRoot) WITH * - MATCH (avg)-[:IN_GROUP]->(group_value:ActivityGroupValue)<-[:HAS_VERSION]-(group_root:ActivityGroupRoot) + MATCH (ag)-[:HAS_SELECTED_GROUP]->(group_value:ActivityGroupValue)<-[:HAS_VERSION]-(group_root:ActivityGroupRoot) """ def is_multiple_selection_allowed_for_activity(self, activity_uid: str) -> bool: @@ -1227,9 +1038,8 @@ def get_linked_upgradable_activity_instances( + """ MATCH (activity_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping)<-[:HAS_ACTIVITY]- (activity_instance_value:ActivityInstanceValue)<-[aihv:HAS_VERSION]-(activity_instance_root:ActivityInstanceRoot) - OPTIONAL MATCH (activity_grouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) - OPTIONAL MATCH (activity_valid_group)-[:IN_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) + OPTIONAL MATCH (activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) + OPTIONAL MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) WITH DISTINCT activity_root, activity_value, activity_instance_root, activity_instance_value, aihv, COLLECT(DISTINCT { activity_uid: activity_root.uid, activity_group_uid: activity_group_root.uid, @@ -1309,14 +1119,19 @@ def get_specific_activity_version_groupings( related subgroups, groups, and instances that existed during this version's validity period. """ query = """ - // 1. Find a specific activity version - MATCH (ar:ActivityRoot {uid: $uid})-[av_rel:HAS_VERSION {version: $version}]->(av:ActivityValue) + // 1. Find the latest HAS_VERSION relationship for the specific version + // (Multiple relationships can exist after inactivate/reactivate cycles) + CALL { + MATCH (ar:ActivityRoot {uid: $uid})-[av_rel:HAS_VERSION {version: $version}]->(av:ActivityValue) + RETURN ar, av_rel, av + ORDER BY av_rel.start_date DESC + LIMIT 1 + } MATCH (av)-[:HAS_GROUPING]->(agrp:ActivityGrouping) CALL { WITH agrp - MATCH (agrp)-[:IN_SUBGROUP]->(avg:ActivityValidGroup) - MATCH (avg)-[:IN_GROUP]->(gv:ActivityGroupValue)<-[:HAS_VERSION]-(gr:ActivityGroupRoot) - MATCH (avg)<-[:HAS_GROUP]-(sgv:ActivitySubGroupValue)<-[:HAS_VERSION]-(sgr:ActivitySubGroupRoot) + MATCH (agrp)-[:HAS_SELECTED_GROUP]->(gv:ActivityGroupValue)<-[:HAS_VERSION]-(gr:ActivityGroupRoot) + MATCH (agrp)-[:HAS_SELECTED_SUBGROUP]->(sgv:ActivitySubGroupValue)<-[:HAS_VERSION]-(sgr:ActivitySubGroupRoot) RETURN DISTINCT sgv, sgr, gv, gr } CALL { @@ -1344,34 +1159,15 @@ def get_specific_activity_version_groupings( RETURN last(hvs) AS g_ver } CALL { - WITH agrp, av, av_rel, ar - // Calculate the activity version's validity period end date - // This ensures we show the latest instance version active during this activity version's timeframe - OPTIONAL MATCH (ar)-[next_rel:HAS_VERSION]->(:ActivityValue) - WHERE toInteger(split(next_rel.version, '.')[0]) > toInteger(split(av_rel.version, '.')[0]) - OR (toInteger(split(next_rel.version, '.')[0]) = toInteger(split(av_rel.version, '.')[0]) - AND toInteger(split(next_rel.version, '.')[1]) > toInteger(split(av_rel.version, '.')[1])) - WITH agrp, av_rel, min(next_rel.start_date) as min_next_start_date - WITH agrp, COALESCE(av_rel.end_date, min_next_start_date, datetime()) as version_end_date - - // Find unique instance roots that have any version linked to this grouping - MATCH (agrp)<-[:HAS_ACTIVITY]-(:ActivityInstanceValue)<-[:HAS_VERSION]-(air:ActivityInstanceRoot) - WHERE NOT EXISTS((air)<-[:DELETED_CONCEPT]-(:DeletedActivityInstanceRoot)) - WITH DISTINCT air, version_end_date - - // For each root, find the latest version that was active during the validity period - MATCH (air)-[hv:HAS_VERSION]->(aiv:ActivityInstanceValue) - WHERE hv.start_date <= version_end_date - AND NOT EXISTS((aiv)<--(:DeletedActivityInstanceRoot)) - WITH air, aiv, hv - ORDER BY air.uid, hv.start_date DESC, - toInteger(split(hv.version, '.')[0]) DESC, - toInteger(split(hv.version, '.')[1]) DESC - - // Group by root and take the first (latest) version - WITH air, collect({aiv: aiv, version: hv.version})[0] AS latest_version - WHERE latest_version IS NOT NULL - WITH {instance_name: latest_version.aiv.name, instance_uid: air.uid, instance_version: latest_version.version} AS instance + WITH agrp + MATCH (agrp)<-[:HAS_ACTIVITY]-(aiv:ActivityInstanceValue)<-[hv:HAS_VERSION]-(air:ActivityInstanceRoot) + WHERE NOT EXISTS((aiv)<--(:DeletedActivityInstanceRoot)) + WITH aiv, hv, air + ORDER BY hv.start_date + WITH DISTINCT air, collect({aiv: aiv, version: hv.version}) AS aiv_versions + WITH air, last(aiv_versions) AS last_aiv_version + WITH DISTINCT last_aiv_version.aiv AS aiv, air, last_aiv_version.version AS instance_version + WITH {instance_name: aiv.name, instance_uid: air.uid, instance_version: instance_version} AS instance RETURN collect(instance) AS activity_instances } RETURN @@ -1511,12 +1307,15 @@ def get_activity_instances_for_version( limit: int = 10, ) -> tuple[list[dict[Any, Any]], int]: """ - Retrieves a paginated list of activity instances relevant to a specific - activity version's time validity window. + Retrieves a paginated list of activity instances that are linked to a specific + activity version via HAS_ACTIVITY relationships. + + Only returns instance versions that have a direct HAS_ACTIVITY relationship to + this activity's groupings. If an instance was later moved to a different activity, + only the versions that were linked to THIS activity are returned. - For each relevant activity instance, it returns the latest version active - during the activity version's timeframe, along with all its older versions - as children. + For each relevant activity instance, it returns the latest linked version as the + parent, along with older linked versions as children. Args: activity_uid (str): The UID of the parent activity. @@ -1553,15 +1352,13 @@ def get_activity_instances_for_version( WITH activity_value, av_rel, min(next_rel.start_date) as min_next_start_date // Grouping implicitly by activity_value, av_rel // 1c. Calculate the final version_end_date - WITH activity_value, COALESCE(av_rel.end_date, min_next_start_date, datetime()) as version_end_date + WITH activity_value, av_rel.end_date as activity_end_date, COALESCE(av_rel.end_date, min_next_start_date, datetime()) as version_end_date // 2. Find distinct relevant ActivityInstance Roots and count them MATCH (activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)<-[:HAS_ACTIVITY]-(:ActivityInstanceValue)<-[aihv_check:HAS_VERSION]-(ai_root:ActivityInstanceRoot) - // *** NOTE: Add appropriate deletion check here if needed, e.g.: *** - // WHERE NOT coalesce(ai_root.is_deleted, false) - // AND NOT EXISTS((activity_instance_value)<--(:DeletedActivityInstanceRoot)) WHERE aihv_check.start_date <= version_end_date - RETURN count(DISTINCT ai_root) as total_count, version_end_date + AND (activity_end_date IS NOT NULL OR aihv_check.end_date IS NULL) + RETURN count(DISTINCT ai_root) as total_count, version_end_date, activity_end_date """ params_count = {"uid": activity_uid, "version": version} try: @@ -1575,6 +1372,9 @@ def get_activity_instances_for_version( return [], 0 total_count = count_result[0][0] version_end_date = count_result[0][1] # Get the calculated end date + activity_end_date = count_result[0][ + 2 + ] # Get activity version end date for filtering # Handle case where activity/version exists but no instances meet criteria if total_count == 0: @@ -1608,54 +1408,53 @@ def get_activity_instances_for_version( details_query = f""" // 1. Find the specific activity version's value node MATCH (activity_root:ActivityRoot {{uid: $uid}})-[:HAS_VERSION {{version: $version}}]->(activity_value:ActivityValue) - WITH activity_value, $version_end_date as version_end_date // Pass calculated end date as parameter + WITH activity_value, $version_end_date as version_end_date, $activity_end_date as activity_end_date // 2. Find distinct relevant ActivityInstance Roots linked via groupings MATCH (activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)<-[:HAS_ACTIVITY]-(:ActivityInstanceValue)<-[aihv_check:HAS_VERSION]-(ai_root:ActivityInstanceRoot) - // *** NOTE: Add appropriate deletion check here if needed *** WHERE aihv_check.start_date <= version_end_date - WITH DISTINCT ai_root, version_end_date + AND (activity_end_date IS NOT NULL OR aihv_check.end_date IS NULL) + WITH DISTINCT ai_root, version_end_date, activity_value, activity_end_date {pagination_clause} - WITH ai_root, version_end_date // Pass paginated roots forward + WITH ai_root, version_end_date, activity_value, activity_end_date // --- Instance Detail Fetching --- - // 4. For each paginated root, find all its versions - MATCH (ai_root)-[aihv:HAS_VERSION]->(ai_val:ActivityInstanceValue) - // *** NOTE: Add appropriate deletion check here if needed *** + // 4. For each paginated root, find versions LINKED TO THIS ACTIVITY's groupings + // This ensures we only get versions that have HAS_ACTIVITY relationship to this activity + MATCH (activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)<-[:HAS_ACTIVITY]-(ai_val:ActivityInstanceValue)<-[aihv:HAS_VERSION]-(ai_root) // 5. Filter versions active within the window & Order them - WITH ai_root, aihv, ai_val, version_end_date + WITH ai_root, aihv, ai_val, version_end_date, activity_value, activity_end_date WHERE aihv.start_date <= version_end_date - WITH ai_root, aihv, ai_val, version_end_date // Pass rows for ordering + AND (activity_end_date IS NOT NULL OR aihv.end_date IS NULL) + WITH ai_root, aihv, ai_val, version_end_date, activity_value ORDER BY ai_root.uid, aihv.start_date DESC, toInteger(split(aihv.version, '.')[0]) DESC, toInteger(split(aihv.version, '.')[1]) DESC // 6. Collect the ordered versions per root - WITH ai_root, version_end_date, collect({{rel: aihv, val: ai_val}}) as relevant_versions_sorted + WITH ai_root, version_end_date, activity_value, collect({{rel: aihv, val: ai_val}}) as relevant_versions_sorted // 7. Identify the specific version to display (the first in the sorted list) - // Use RETURN DISTINCT to handle potential duplicates if pagination wasn't perfect (shouldn't happen with ORDER BY uid) - WITH DISTINCT ai_root, relevant_versions_sorted[0] as display_instance_map // Map {{rel:..., val:...}} + WITH DISTINCT ai_root, activity_value, relevant_versions_sorted[0] as display_instance_map // Map {{rel:..., val:...}} // 8. Get library info for the root OPTIONAL MATCH (library)-[:CONTAINS_CONCEPT]->(ai_root) // 9. Get ActivityInstanceClass for the display instance version node - WITH ai_root, display_instance_map, library, display_instance_map.val as display_instance_node + WITH ai_root, display_instance_map, library, activity_value, display_instance_map.val as display_instance_node OPTIONAL MATCH (display_instance_node)-[:ACTIVITY_INSTANCE_CLASS]->(:ActivityInstanceClassRoot)-[:LATEST]->(aic_value:ActivityInstanceClassValue) - // 10. Find all *other* versions (children) - WITH ai_root, display_instance_map, display_instance_node, library, aic_value - OPTIONAL MATCH (ai_root)-[child_aihv:HAS_VERSION]->(child_ai_val:ActivityInstanceValue) + // 10. Find all *other* versions (children) that are LINKED TO THIS ACTIVITY + WITH ai_root, display_instance_map, display_instance_node, library, aic_value, activity_value + OPTIONAL MATCH (activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)<-[:HAS_ACTIVITY]-(child_ai_val:ActivityInstanceValue)<-[child_aihv:HAS_VERSION]-(ai_root) WHERE child_aihv <> display_instance_map.rel AND (child_aihv.start_date < display_instance_map.rel.start_date OR (child_aihv.start_date = display_instance_map.rel.start_date AND (toInteger(split(child_aihv.version, '.')[0]) < toInteger(split(display_instance_map.rel.version, '.')[0]) OR (toInteger(split(child_aihv.version, '.')[0]) = toInteger(split(display_instance_map.rel.version, '.')[0]) AND toInteger(split(child_aihv.version, '.')[1]) < toInteger(split(display_instance_map.rel.version, '.')[1]))))) - // *** NOTE: Add appropriate deletion check here if needed *** // 11. Get ActivityInstanceClass for children OPTIONAL MATCH (child_ai_val)-[:ACTIVITY_INSTANCE_CLASS]->(:ActivityInstanceClassRoot)-[:LATEST]->(child_aic_value:ActivityInstanceClassValue) @@ -1695,7 +1494,7 @@ def get_activity_instances_for_version( "uid": activity_uid, "version": version, "version_end_date": version_end_date, - # SKIP and LIMIT are embedded in the f-string, not passed as params here + "activity_end_date": activity_end_date, } try: diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_sub_group_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_sub_group_repository.py index 98b97297..29cd2449 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_sub_group_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/activities/activity_sub_group_repository.py @@ -1,4 +1,3 @@ -import datetime from typing import Any from neomodel import db @@ -7,10 +6,8 @@ ConceptGenericRepository, ) from clinical_mdr_api.domain_repositories.models.activities import ( - ActivityGroupRoot, ActivitySubGroupRoot, ActivitySubGroupValue, - ActivityValidGroup, ) from clinical_mdr_api.domain_repositories.models.generic import ( Library, @@ -21,7 +18,6 @@ from clinical_mdr_api.domains.concepts.activities.activity_sub_group import ( ActivitySubGroupAR, ActivitySubGroupVO, - SimpleActivityGroupVO, ) from clinical_mdr_api.domains.versioned_object_aggregate import ( LibraryItemMetadataVO, @@ -32,7 +28,7 @@ ActivitySubGroup, ) from common.exceptions import BusinessLogicException -from common.utils import convert_to_datetime, version_string_to_tuple +from common.utils import convert_to_datetime class ActivitySubGroupRepository(ConceptGenericRepository[ActivitySubGroupAR]): @@ -51,14 +47,6 @@ def _create_aggregate_root_instance_from_cypher_result( name_sentence_case=input_dict["name_sentence_case"], definition=input_dict.get("definition"), abbreviation=input_dict.get("abbreviation"), - activity_groups=[ - SimpleActivityGroupVO( - activity_group_uid=activity_group.get("uid"), - activity_group_name=activity_group.get("name"), - activity_group_version=f"{activity_group.get('major_version')}.{activity_group.get('minor_version')}", - ) - for activity_group in input_dict.get("activity_groups") - ], ), library=LibraryVO.from_input_values_2( library_name=input_dict["library_name"], @@ -94,15 +82,6 @@ def _create_ar( name_sentence_case=value.name_sentence_case, definition=value.definition, abbreviation=value.abbreviation, - activity_groups=[ - SimpleActivityGroupVO( - activity_group_uid=activity_group.get("uid"), - activity_group_version=f"{activity_group.get('major_version')}.{activity_group.get('minor_version')}", - ) - for activity_group in _kwargs["activity_subgroups_root"][ - "activity_groups" - ] - ], ), library=LibraryVO.from_input_values_2( library_name=library.name, @@ -119,22 +98,6 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( value: VersionValue, **_kwargs, ) -> ActivitySubGroupAR: - activity_valid_groups = value.has_group.all() - activity_groups = [] - for activity_valid_group in activity_valid_groups: - activity_group_value = activity_valid_group.in_group.get() - activity_group_root = activity_group_value.has_version.single() - all_rels = activity_group_value.has_version.all_relationships( - activity_group_root - ) - latest = max(all_rels, key=lambda r: version_string_to_tuple(r.version)) - activity_groups.append( - SimpleActivityGroupVO( - activity_group_uid=activity_group_root.uid, - activity_group_name=activity_group_value.name, - activity_group_version=latest.version, - ) - ) return ActivitySubGroupAR.from_repository_values( uid=root.uid, @@ -143,13 +106,6 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( name_sentence_case=value.name_sentence_case, definition=value.definition, abbreviation=value.abbreviation, - activity_groups=sorted( - activity_groups, - key=lambda ag: ( - ag.activity_group_name is None, - ag.activity_group_name, - ), - ), ), library=LibraryVO.from_input_values_2( library_name=library.name, @@ -161,21 +117,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( def specific_alias_clause(self, **kwargs) -> str: # concept_value property comes from the main part of the query # which is specified in the activity_generic_repository_impl - return """ - WITH *, - [(concept_value)-[:HAS_GROUP]->(activity_valid_group:ActivityValidGroup) | - { - activity_group:head(apoc.coll.sortMulti([(activity_valid_group)-[:IN_GROUP]-(activity_group_value:ActivityGroupValue) - <-[has_version:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | - { - uid:activity_group_root.uid, - name:activity_group_value.name, - major_version: toInteger(split(has_version.version,'.')[0]), - minor_version: toInteger(split(has_version.version,'.')[1]) - }], ['major_version', 'minor_version'])) - }] AS activity_groups - WITH *, apoc.coll.sortMulti([ag in activity_groups | ag.activity_group], ['^name']) AS activity_groups - """ + return "" def create_query_filter_statement( self, library: str | None = None, **kwargs @@ -188,22 +130,22 @@ def create_query_filter_statement( if kwargs.get("activity_group_uid") is not None: activity_group_uid = kwargs.get("activity_group_uid") filter_by_activity_group_uid = """ - $activity_group_uid IN - [(concept_value)-[:HAS_GROUP]->(:ActivityValidGroup)-[:IN_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root) + $activity_group_uid IN + [(concept_value)<-[:HAS_SELECTED_SUBGROUP]-(:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root) | activity_group_root.uid]""" filter_parameters.append(filter_by_activity_group_uid) filter_query_parameters["activity_group_uid"] = activity_group_uid if kwargs.get("activity_group_names") is not None: activity_group_names = kwargs.get("activity_group_names") filter_by_activity_group_names = """ - size([(concept_value)-[:HAS_GROUP]->(:ActivityValidGroup)-[:IN_GROUP]->(v:ActivityGroupValue) + size([(concept_value)<-[:HAS_SELECTED_SUBGROUP]-(:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(v:ActivityGroupValue) WHERE v.name IN $activity_group_names | v.name]) > 0""" filter_parameters.append(filter_by_activity_group_names) filter_query_parameters["activity_group_names"] = activity_group_names if kwargs.get("activity_names") is not None: activity_names = kwargs.get("activity_names") filter_by_activity_names = """ - size([(concept_value)-[:HAS_GROUP]-(:ActivityValidGroup)<-[:IN_SUBGROUP]-(:ActivityGrouping)<-[:HAS_GROUPING]-(v:ActivityValue) + size([(concept_value)<-[:HAS_SELECTED_SUBGROUP]-(:ActivityGrouping)<-[:HAS_GROUPING]-(v:ActivityValue) WHERE v.name IN $activity_names | v.name]) > 0""" filter_parameters.append(filter_by_activity_names) filter_query_parameters["activity_names"] = activity_names @@ -257,23 +199,20 @@ def get_linked_activity_group_uids( // 2. Find when this version's validity ends (either its end_date or the start of the next version) OPTIONAL MATCH (sgr)-[next_rel:HAS_VERSION]->(next_sgv:ActivitySubGroupValue) WHERE toFloat(next_rel.version) > toFloat(sgv_rel.version) - WITH sgv, sgr, sgv_rel, - CASE WHEN sgv_rel.end_date IS NULL - THEN min(next_rel.start_date) - ELSE sgv_rel.end_date + WITH sgv, sgr, sgv_rel, + CASE WHEN sgv_rel.end_date IS NULL + THEN min(next_rel.start_date) + ELSE sgv_rel.end_date END as version_end_date - - // 3. Find all activity groups directly connected to this subgroup version with correct relationship direction - MATCH (sgv)-[:HAS_GROUP]->(avg:ActivityValidGroup)-[:IN_GROUP]->(agv:ActivityGroupValue) + // 3. Find all activity groups connected to this subgroup version via ActivityGrouping nodes + MATCH (sgv)<-[:HAS_SELECTED_SUBGROUP]-(:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(agv:ActivityGroupValue) MATCH (agr:ActivityGroupRoot)-[ag_rel:HAS_VERSION]->(agv) - // 4. Filter activity group versions created before the subgroup's next version/end date // Include all activity groups regardless of status (per user requirement) WITH sgv, sgr, sgv_rel, version_end_date, agr, agv, ag_rel WHERE ag_rel.start_date <= COALESCE(version_end_date, datetime()) - // 5. Group by activity group for processing - WITH + WITH sgr.uid as subgroup_uid, sgv_rel.version as subgroup_version, agr.uid as group_uid, @@ -283,54 +222,48 @@ def get_linked_activity_group_uids( ag_rel.status as group_status, toInteger(SPLIT(ag_rel.version, '.')[0]) as ag_major_version, toInteger(SPLIT(ag_rel.version, '.')[1]) as ag_minor_version - // 6. Collect all versions by group - WITH - group_uid, + WITH + group_uid, collect({ - ag_major: ag_major_version, + ag_major: ag_major_version, ag_minor: ag_minor_version, - group_version: group_version, + group_version: group_version, group_name: group_name, group_definition: group_definition, group_status: group_status }) as versions - // 7. Find highest major version - WITH - group_uid, + WITH + group_uid, versions, - reduce(max_ag_major = 0, v IN versions | + reduce(max_ag_major = 0, v IN versions | CASE WHEN v.ag_major > max_ag_major THEN v.ag_major ELSE max_ag_major END ) as max_ag_major - // 8. Filter to only include versions with the maximum major version - WITH - group_uid, + WITH + group_uid, [v in versions WHERE v.ag_major = max_ag_major] as ag_max_major_versions, max_ag_major - // 9. Find highest minor version - WITH - group_uid, + WITH + group_uid, max_ag_major, - reduce(max_ag_minor = -1, v IN ag_max_major_versions | + reduce(max_ag_minor = -1, v IN ag_max_major_versions | CASE WHEN v.ag_minor > max_ag_minor THEN v.ag_minor ELSE max_ag_minor END ) as max_ag_minor, ag_max_major_versions - // 10. Extract the specific version information - WITH - group_uid, - max_ag_major, + WITH + group_uid, + max_ag_major, max_ag_minor, [v in ag_max_major_versions WHERE v.ag_minor = max_ag_minor][0] as ag_version_info - // 11. Return data in the required format RETURN - group_uid as uid, - ag_version_info.group_name as name, - ag_version_info.group_version as version, + group_uid as uid, + ag_version_info.group_name as name, + ag_version_info.group_version as version, ag_version_info.group_status as status ORDER BY name """ @@ -402,7 +335,7 @@ def get_linked_activity_uids( END as version_end_date // 3. Find all activities linked to this subgroup version through activity groups - MATCH (sgv)-[:HAS_GROUP]->(avg:ActivityValidGroup)<-[:IN_SUBGROUP]-(ag:ActivityGrouping)<-[:HAS_GROUPING]-(av:ActivityValue)<-[a_rel:HAS_VERSION]-(ar:ActivityRoot) + MATCH (sgv)<-[:HAS_SELECTED_SUBGROUP]-(ag:ActivityGrouping)<-[:HAS_GROUPING]-(av:ActivityValue)<-[a_rel:HAS_VERSION]-(ar:ActivityRoot) WHERE NOT EXISTS ((av)<--(:DeletedActivityRoot)) // 4. Filter activity versions - find those existing at the time this subgroup version was active @@ -490,14 +423,14 @@ def get_cosmos_subgroup_overview(self, subgroup_uid: str) -> dict[str, Any]: WITH DISTINCT subgroup_root, subgroup_value, head([(library)-[:CONTAINS_CONCEPT]->(subgroup_root) | library.name]) AS subgroup_library_name, [(subgroup_root)-[versions:HAS_VERSION]->(:ActivitySubGroupValue) | versions.version] as all_versions, - apoc.coll.toSet([(subgroup_value)-[:HAS_GROUP]->(activity_valid_group:ActivityValidGroup)-[:IN_GROUP]-> + apoc.coll.toSet([(subgroup_value)<-[:HAS_SELECTED_SUBGROUP]-(activity_grouping:ActivityGrouping)-[:HAS_SELECTED_GROUP]-> (activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | { uid: activity_group_root.uid, name: activity_group_value.name }]) AS activity_groups, apoc.coll.toSet( - [(subgroup_value)-[:HAS_GROUP]->(activity_valid_group:ActivityValidGroup)<-[:IN_SUBGROUP]- + [(subgroup_value)<-[:HAS_SELECTED_SUBGROUP]- (activity_grouping:ActivityGrouping)<-[:HAS_GROUPING]-(activity_value:ActivityValue)<-[:HAS_VERSION]- (activity_root:ActivityRoot) WHERE NOT EXISTS ((activity_value)<--(:DeletedActivityRoot)) @@ -550,25 +483,6 @@ def get_cosmos_subgroup_overview(self, subgroup_uid: str) -> dict[str, Any]: def _create_new_value_node(self, ar: ActivitySubGroupAR) -> VersionValue: value_node: ActivitySubGroupValue = super()._create_new_value_node(ar=ar) value_node.save() - for activity_group in ar.concept_vo.activity_groups: - # find related ActivityGroup nodes - group_root = ActivityGroupRoot.nodes.get_or_none( - uid=activity_group.activity_group_uid - ) - group_value = group_root.has_latest_value.get_or_none() - - # Create ActivityValidGroup node - activity_valid_group = ActivityValidGroup( - uid=ActivityValidGroup.get_next_free_uid_and_increment_counter() - ) - activity_valid_group.save() - - # connect ActivityValidGroup and ActivityGroupValue nodes - activity_valid_group.in_group.connect(group_value) - - # connect ActivitySubGroupValue and ActivityValidGroup nodes - value_node.has_group.connect(activity_valid_group) - return value_node def _has_data_changed( @@ -576,111 +490,13 @@ def _has_data_changed( ) -> bool: are_concept_properties_changed = super()._has_data_changed(ar=ar, value=value) - activity_valid_groups = value.has_group.all() - activity_groups_uid = [] - for activity_valid_group in activity_valid_groups: - activity_group_value = activity_valid_group.in_group.get() - if not activity_group_value.has_latest_value.single(): - # The linked ActivityGroupValue is not the latest. - # We need to return True, so that the ActivitySubGroupValue - # gets updated to use the new ActivityGroupValue. - return True - activity_group_uid = activity_group_value.has_version.single().uid - activity_groups_uid.append(activity_group_uid) - - # Is this a final or retired version? If yes, we skip the check for updated groups - # to avoid creating new values nodes when just creating a new draft. - root_for_final_value = value.has_version.match( - status__in=[LibraryItemStatus.FINAL.value, LibraryItemStatus.RETIRED.value], - end_date__isnull=True, - ) - if not root_for_final_value: - groups_updated = self._any_group_updated(value) - else: - groups_updated = False - - are_rels_changed = sorted( - [ - activity_group.activity_group_uid - for activity_group in ar.concept_vo.activity_groups - ] - ) != sorted(activity_groups_uid) - - return are_concept_properties_changed or are_rels_changed or groups_updated - - def copy_activity_subgroup_and_recreate_activity_groupings( - self, activity_subgroup: ActivitySubGroupAR, author_id: str - ) -> None: - query = """ - MATCH (concept_root:ActivitySubGroupRoot {uid:$activity_subgroup_uid})-[status_relationship:LATEST]->(concept_value:ActivitySubGroupValue) - CALL apoc.refactor.cloneNodes([concept_value]) - YIELD input, output, error""" - merge_query = f""" - MERGE (concept_root)-[:LATEST]->(output) - MERGE (concept_root)-[:LATEST_{activity_subgroup.item_metadata.status.value.upper()}]->(output) - MERGE (concept_root)-[new_has_version:HAS_VERSION]->(output)""" - query += self._update_versioning_relationship_query( - status=activity_subgroup.item_metadata.status.value, merge_query=merge_query - ) - query += """ - WITH library, concept_root, concept_value, output - UNWIND $activity_group_uids as activity_group_uid - CREATE (activity_valid_group:ActivityValidGroup) - WITH library, concept_root, output, activity_valid_group, activity_group_uid - MATCH (activity_group_value:ActivityGroupValue)<-[:LATEST]-(:ActivityGroupRoot {uid:activity_group_uid}) - WITH library, concept_root, output, activity_valid_group, activity_group_value - MERGE (output)-[:HAS_GROUP]->(activity_valid_group)-[:IN_GROUP]->(activity_group_value) - RETURN concept_root, output, library - """ - - created_node, _ = db.cypher_query( - query, - params={ - "activity_subgroup_uid": activity_subgroup.uid, - "new_status": activity_subgroup.item_metadata.status.value, - "new_version": activity_subgroup.item_metadata.version, - "start_date": datetime.datetime.now(datetime.timezone.utc), - "change_description": "Copying previous ActivitySubGroupValue and updating ActivityValidGroup nodes", - "author_id": author_id, - "activity_group_uids": [ - activity_subgroup.activity_group_uid - for activity_subgroup in activity_subgroup.concept_vo.activity_groups - ], - }, - ) - if len(created_node) > 0: - ActivityValidGroup.generate_node_uids_if_not_present() - - def _any_group_updated(self, subgroup_value): - for grouping_node in subgroup_value.has_group.all(): - if not grouping_node.in_group.get().has_latest_value.single(): - # The linked group is not the latest. - # We need to return True, so that the subgroup value - # gets updated to use the new group value. - return True - return False + return are_concept_properties_changed def generic_match_clause_all_versions(self): return """ MATCH (concept_root:ActivitySubGroupRoot)-[version:HAS_VERSION]->(concept_value:ActivitySubGroupValue) - -[:HAS_GROUP]->(avg:ActivityValidGroup)-[:IN_GROUP]->(agv:ActivityGroupValue)<-[:HAS_VERSION]-(agr:ActivityGroupRoot) """ - def get_activity_group_uids_linked_by_subgroup_in_specific_version( - self, activity_subgroup_uid: str, version: str - ) -> list[str]: - query = """ - MATCH (activity_subgroup_root:ActivitySubGroupRoot {uid:$uid})-[hv:HAS_VERSION {version:$version}]->(activity_subgroup_value:ActivitySubGroupValue) - MATCH (activity_subgroup_value)-[:HAS_GROUP]->(:ActivityValidGroup)-[:IN_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) - RETURN collect(DISTINCT activity_group_root.uid) AS activity_group_uids - """ - result, _ = db.cypher_query( - query, {"uid": activity_subgroup_uid, "version": version} - ) - if len(result) > 0 and len(result[0]) > 0: - return result[0][0] - return [] - def get_linked_upgradable_activities( self, uid: str, version: str | None = None ) -> dict[Any, Any] | None: @@ -702,11 +518,13 @@ def get_linked_upgradable_activities( query = ( match + """ - MATCH (activity_root:ActivityRoot)-[aihv:HAS_VERSION]->(activity_value:ActivityValue)-[:HAS_GROUPING]->(:ActivityGrouping) - -[:IN_SUBGROUP]->(:ActivityValidGroup)<-[:HAS_GROUP]-(activity_subgroup_value) - MATCH (activity_value)-[:HAS_GROUPING]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) - <-[:HAS_GROUP]-(:ActivitySubGroupValue)<-[:HAS_VERSION]-(all_subgroup_root:ActivitySubGroupRoot) - MATCH (activity_valid_group)-[:IN_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) + // Find all activities linked to this subgroup value + MATCH (activity_root:ActivityRoot)-[aihv:HAS_VERSION]->(activity_value:ActivityValue)-[:HAS_GROUPING]-> + (:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value) + // For each activity, find all its groupings + MATCH (activity_value)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping)-[:HAS_SELECTED_SUBGROUP]-> + (:ActivitySubGroupValue)<-[:HAS_VERSION]-(all_subgroup_root:ActivitySubGroupRoot) + MATCH (activity_grouping)-[:HAS_SELECTED_GROUP]->(:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) WITH DISTINCT activity_root, activity_value, aihv, COLLECT(DISTINCT { activity_group_uid: activity_group_root.uid, activity_subgroup_uid: all_subgroup_root.uid diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/concept_generic_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/concept_generic_repository.py index 66ea472e..9b14b12c 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/concept_generic_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/concept_generic_repository.py @@ -43,6 +43,7 @@ class ConceptGenericRepository( value_class = type return_model: type = BaseModel filter_query_parameters: dict[Any, Any] = {} + sort_by: dict[Any, Any] | None = None @abstractmethod def _create_aggregate_root_instance_from_cypher_result( @@ -315,6 +316,8 @@ def find_all( library=library, uids=uids, **kwargs ) self.filter_query_parameters = filter_query_parameters + self.sort_by = sort_by + match_clause += filter_statements alias_clause = ( diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/form_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/form_repository.py index 7c30d625..b400e276 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/form_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/form_repository.py @@ -274,10 +274,16 @@ def find_by_uid_with_study_event_relation( rs, _ = db.cypher_query( """ MATCH (:OdmStudyEventRoot {uid: $study_event_uid})-[:HAS_VERSION {version: $study_event_version}]->(:OdmStudyEventValue) - -[ref:FORM_REF]->(value:OdmFormValue)<-[hv_rel:HAS_VERSION]-(:OdmFormRoot {uid: $uid}) + -[ref:FORM_REF]->(value:OdmFormValue) + + MATCH (value)<-[hv_rel:HAS_VERSION]-(:OdmFormRoot {uid: $uid}) + WITH value, ref, hv_rel + ORDER BY hv_rel.start_date DESC + WITH value, ref, collect(hv_rel) AS hv_rels + RETURN value.name AS name, - hv_rel.version AS version, + hv_rels[0].version AS version, ref.order_number AS order_number, ref.mandatory AS mandatory, ref.locked AS locked, @@ -306,34 +312,47 @@ def _connect_relationships_to_new_value_node( self, root: VersionRoot, _: VersionValue ) -> None: """ - Upgrades all incoming FORM_REF relationships to the second latest version to point + - Upgrades all incoming FORM_REF relationships to the second latest version to point to the latest version of OdmFormValue, preserving relationship properties. + - Ensures the new OdmFormValue node is connected to all ActivityItem nodes that any + of its child OdmItemGroupValue nodes are connected to. """ - query = f""" - MATCH (root:{self.root_class.__name__} {{uid: $root_uid}})-[ver_rel:HAS_VERSION]->(value:{self.value_class.__name__}) + db.cypher_query( + f""" + MATCH (root:{self.root_class.__name__} {{uid: $root_uid}})-[ver_rel:HAS_VERSION]->(value:{self.value_class.__name__}) - WITH root, ver_rel, value - ORDER BY ver_rel.start_date DESC - LIMIT 2 - WITH root, collect(value) AS values - WITH root, values[0] as latest_value, values[1] as second_latest_value + WITH root, ver_rel, value + ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC + LIMIT 2 + WITH root, collect(value) AS values + WITH root, values[0] as latest_value, values[1] as second_latest_value - MATCH (:OdmStudyEventRoot)-[p_ver_rel:HAS_VERSION]->(parent_value:OdmStudyEventValue)-[ref_rel:FORM_REF]->(second_latest_value) - WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" + MATCH (:OdmStudyEventRoot)-[p_ver_rel:HAS_VERSION]->(parent_value:OdmStudyEventValue)-[ref_rel:FORM_REF]->(second_latest_value) + WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" - WITH latest_value, ref_rel, parent_value, - ref_rel.order_number AS order_number, - ref_rel.mandatory AS mandatory, - ref_rel.locked AS locked, - ref_rel.collection_exception_condition_oid AS collection_exception_condition_oid + WITH latest_value, ref_rel, parent_value, + ref_rel.order_number AS order_number, + ref_rel.mandatory AS mandatory, + ref_rel.locked AS locked, + ref_rel.collection_exception_condition_oid AS collection_exception_condition_oid - CREATE (parent_value)-[new_ref_rel:FORM_REF]->(latest_value) + CREATE (parent_value)-[new_ref_rel:FORM_REF]->(latest_value) - SET new_ref_rel.order_number = order_number, - new_ref_rel.mandatory = mandatory, - new_ref_rel.locked = locked, - new_ref_rel.collection_exception_condition_oid = collection_exception_condition_oid + SET new_ref_rel.order_number = order_number, + new_ref_rel.mandatory = mandatory, + new_ref_rel.locked = locked, + new_ref_rel.collection_exception_condition_oid = collection_exception_condition_oid - DELETE ref_rel - """ - db.cypher_query(query, {"root_uid": root.uid}) + DELETE ref_rel + """, + {"root_uid": root.uid}, + ) + + db.cypher_query( + f""" + MATCH (:{self.root_class.__name__} {{uid: $root_uid}})-[:LATEST]->(value:{self.value_class.__name__}) + MATCH (value)-[:ITEM_GROUP_REF]->(:OdmItemGroupValue)-[:LINKS_TO_ACTIVITY_ITEM]->(activity_item:ActivityItem) + MERGE (value)-[:LINKS_TO_ACTIVITY_ITEM]->(activity_item) + """, + {"root_uid": root.uid}, + ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_group_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_group_repository.py index 12340009..cbf2507e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_group_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_group_repository.py @@ -337,11 +337,17 @@ def find_by_uid_with_form_relation( rs, _ = db.cypher_query( """ MATCH (:OdmFormRoot {uid: $form_uid})-[:HAS_VERSION {version: $form_version}]->(:OdmFormValue) - -[ref:ITEM_GROUP_REF]->(value:OdmItemGroupValue)<-[hv_rel:HAS_VERSION]-(:OdmItemGroupRoot {uid: $uid}) + -[ref:ITEM_GROUP_REF]->(value:OdmItemGroupValue) + + MATCH (value)<-[hv_rel:HAS_VERSION]-(:OdmItemGroupRoot {uid: $uid}) + WITH value, ref, hv_rel + ORDER BY hv_rel.start_date DESC + WITH value, ref, collect(hv_rel) AS hv_rels + RETURN value.oid AS oid, value.name AS name, - hv_rel.version AS version, + hv_rels[0].version AS version, ref.order_number AS order_number, ref.mandatory AS mandatory, ref.collection_exception_condition_oid AS collection_exception_condition_oid, @@ -368,34 +374,47 @@ def _connect_relationships_to_new_value_node( self, root: VersionRoot, _: VersionValue ) -> None: """ - Upgrades all incoming ITEM_GROUP_REF relationships to the second latest version to + - Upgrades all incoming ITEM_GROUP_REF relationships to the second latest version to point to the latest version of OdmItemGroupValue, preserving relationship properties. + - Ensures the new OdmItemGroupValue node is connected to all ActivityItem nodes that any + of its child OdmItemValue nodes are connected to. """ - query = f""" - MATCH (root:{self.root_class.__name__} {{uid: $root_uid}})-[ver_rel:HAS_VERSION]->(value:{self.value_class.__name__}) + db.cypher_query( + f""" + MATCH (root:{self.root_class.__name__} {{uid: $root_uid}})-[ver_rel:HAS_VERSION]->(value:{self.value_class.__name__}) - WITH root, ver_rel, value - ORDER BY ver_rel.start_date DESC - LIMIT 2 - WITH root, collect(value) AS values - WITH root, values[0] as latest_value, values[1] as second_latest_value + WITH root, ver_rel, value + ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC + LIMIT 2 + WITH root, collect(value) AS values + WITH root, values[0] as latest_value, values[1] as second_latest_value - MATCH (:OdmFormRoot)-[p_ver_rel:HAS_VERSION]->(parent_value:OdmFormValue)-[ref_rel:ITEM_GROUP_REF]->(second_latest_value) - WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" + MATCH (:OdmFormRoot)-[p_ver_rel:HAS_VERSION]->(parent_value:OdmFormValue)-[ref_rel:ITEM_GROUP_REF]->(second_latest_value) + WHERE p_ver_rel.end_date IS NULL AND p_ver_rel.status = "Draft" - WITH latest_value, ref_rel, parent_value, - ref_rel.order_number AS order_number, - ref_rel.mandatory AS mandatory, - ref_rel.collection_exception_condition_oid AS collection_exception_condition_oid, - ref_rel.vendor AS vendor + WITH latest_value, ref_rel, parent_value, + ref_rel.order_number AS order_number, + ref_rel.mandatory AS mandatory, + ref_rel.collection_exception_condition_oid AS collection_exception_condition_oid, + ref_rel.vendor AS vendor - CREATE (parent_value)-[new_ref_rel:ITEM_GROUP_REF]->(latest_value) + CREATE (parent_value)-[new_ref_rel:ITEM_GROUP_REF]->(latest_value) - SET new_ref_rel.order_number = order_number, - new_ref_rel.mandatory = mandatory, - new_ref_rel.collection_exception_condition_oid = collection_exception_condition_oid, - new_ref_rel.vendor = vendor + SET new_ref_rel.order_number = order_number, + new_ref_rel.mandatory = mandatory, + new_ref_rel.collection_exception_condition_oid = collection_exception_condition_oid, + new_ref_rel.vendor = vendor - DELETE ref_rel - """ - db.cypher_query(query, {"root_uid": root.uid}) + DELETE ref_rel + """, + {"root_uid": root.uid}, + ) + + db.cypher_query( + f""" + MATCH (:{self.root_class.__name__} {{uid: $root_uid}})-[:LATEST]->(value:{self.value_class.__name__}) + MATCH (value)-[:ITEM_REF]->(:OdmItemValue)-[:LINKS_TO_ACTIVITY_ITEM]->(activity_item:ActivityItem) + MERGE (value)-[:LINKS_TO_ACTIVITY_ITEM]->(activity_item) + """, + {"root_uid": root.uid}, + ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_repository.py index 6ef42b49..324c60ec 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/concepts/odms/item_repository.py @@ -1,4 +1,5 @@ import json +from textwrap import dedent from typing import Any from neomodel import db @@ -37,6 +38,7 @@ ) from clinical_mdr_api.models.concepts.odms.odm_item import OdmItem from clinical_mdr_api.services._utils import ensure_transaction +from clinical_mdr_api.utils import db_result_to_list from common.exceptions import NotFoundException from common.utils import convert_to_datetime, version_string_to_tuple @@ -54,6 +56,33 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( value: VersionValue, **_kwargs, ) -> OdmItemAR: + activity_instances = db.cypher_query( + dedent( + """ + MATCH (oiv:OdmItemValue)-[ltai:LINKS_TO_ACTIVITY_ITEM]->(ai:ActivityItem) + MATCH (ai)<-[:HAS_ACTIVITY_ITEM]-(aicr:ActivityItemClassRoot) + MATCH (ai)<-[:CONTAINS_ACTIVITY_ITEM]-(:ActivityInstanceValue)<-[:LATEST]-(air:ActivityInstanceRoot) + WHERE elementId(oiv) = $element_id + MATCH (oigr:OdmItemGroupRoot)-[:HAS_VERSION]->(oigv:OdmItemGroupValue) + MATCH (oiv)<-[:ITEM_REF]-(oigv)-[:LINKS_TO_ACTIVITY_ITEM]->(ai) + MATCH (ofr:OdmFormRoot)-[:HAS_VERSION]->(ofv:OdmFormValue) + MATCH (oigv)<-[:ITEM_GROUP_REF]-(ofv)-[:LINKS_TO_ACTIVITY_ITEM]->(ai) + MATCH (ai)<-[:HAS_ACTIVITY_ITEM]-(aicr:ActivityItemClassRoot) + RETURN DISTINCT + air.uid AS activity_instance_uid, + aicr.uid AS activity_item_class_uid, + ofr.uid AS odm_form_uid, + oigr.uid AS odm_item_group_uid, + ltai.order AS order, + ltai.primary AS primary, + ltai.preset_response_value AS preset_response_value, + ltai.value_condition AS value_condition, + ltai.value_dependent_map AS value_dependent_map + """ + ), + params={"element_id": value.element_id}, + ) + codelist = value.has_codelist.get_or_none() return OdmItemAR.from_repository_values( uid=root.uid, @@ -92,6 +121,7 @@ def _create_aggregate_root_instance_from_version_root_relationship_and_value( for term_context in value.has_codelist_term.all() if (term := term_context.has_selected_term.get_or_none()) ], + activity_instances=db_result_to_list(activity_instances), vendor_element_uids=[ vendor_element_root.uid for vendor_element_value in value.has_vendor_element.all() @@ -155,6 +185,7 @@ def _create_aggregate_root_instance_from_cypher_result( unit_definition_uids=input_dict["unit_definition_uids"], codelist_uid=input_dict["codelist_uid"], term_uids=input_dict["term_uids"], + activity_instances=input_dict["activity_instances"], vendor_element_uids=input_dict["vendor_element_uids"], vendor_attribute_uids=input_dict["vendor_attribute_uids"], vendor_element_attribute_uids=input_dict[ @@ -218,6 +249,28 @@ def specific_alias_clause(self, **kwargs) -> str: [(concept_value)-[hvea:HAS_VENDOR_ELEMENT_ATTRIBUTE]->(vav:OdmVendorAttributeValue)<-[:HAS_VERSION]-(var:OdmVendorAttributeRoot) | {uid: var.uid, name: vav.name, value: hvea.value}] AS vendor_element_attributes +CALL { + WITH * + MATCH (concept_value)-[ltai:LINKS_TO_ACTIVITY_ITEM]->(ai:ActivityItem) + MATCH (oigr:OdmItemGroupRoot)-[:HAS_VERSION]->(oigv:OdmItemGroupValue) + MATCH (concept_value)<-[:ITEM_REF]-(oigv)-[:LINKS_TO_ACTIVITY_ITEM]->(ai) + MATCH (ofr:OdmFormRoot)-[:HAS_VERSION]->(ofv:OdmFormValue) + MATCH (oigv)<-[:ITEM_GROUP_REF]-(ofv)-[:LINKS_TO_ACTIVITY_ITEM]->(ai) + MATCH (ai)<-[:HAS_ACTIVITY_ITEM]-(aicr:ActivityItemClassRoot) + MATCH (ai)<-[:CONTAINS_ACTIVITY_ITEM]-(:ActivityInstanceValue)<-[:LATEST]-(air:ActivityInstanceRoot) + RETURN COLLECT(DISTINCT { + activity_instance_uid: air.uid, + activity_item_class_uid: aicr.uid, + odm_form_uid: ofr.uid, + odm_item_group_uid: oigr.uid, + order: ltai.order, + primary: ltai.primary, + preset_response_value: ltai.preset_response_value, + value_condition: ltai.value_condition, + value_dependent_map: ltai.value_dependent_map + }) AS activity_instances +} + WITH *, apoc.coll.toSet([unit_definition in unit_definitions | unit_definition.uid]) AS unit_definition_uids, apoc.coll.toSet([term in terms | term.uid]) AS term_uids, @@ -244,6 +297,44 @@ def _get_or_create_value( codelist = CTCodelistRoot.nodes.get_or_none(uid=ar.concept_vo.codelist_uid) new_value.has_codelist.connect(codelist) + for activity_instance in ar.concept_vo.activity_instances: + db.cypher_query( + dedent( + """ + MATCH (air:ActivityInstanceRoot {uid: $activity_instance_uid})-[:LATEST]->(aiv:ActivityInstanceValue) + -[:CONTAINS_ACTIVITY_ITEM]->(ai:ActivityItem)<-[:HAS_ACTIVITY_ITEM]-(:ActivityItemClassRoot {uid: $activity_item_class_uid}) + MATCH (oiv:OdmItemValue) + WHERE elementId(oiv) = $element_id + MATCH (ofr:OdmFormRoot {uid: $odm_form_uid})-[:LATEST]->(ofv:OdmFormValue) + MATCH (oigr:OdmItemGroupRoot {uid: $odm_item_group_uid})-[:LATEST]->(oigv:OdmItemGroupValue) + + MERGE (oiv)-[:LINKS_TO_ACTIVITY_ITEM { + order: $order, + primary: $primary, + preset_response_value: $preset_response_value, + value_condition: $value_condition, + value_dependent_map: $value_dependent_map + }]->(ai) + MERGE (ofv)-[:LINKS_TO_ACTIVITY_ITEM]->(ai) + MERGE (oigv)-[:LINKS_TO_ACTIVITY_ITEM]->(ai) + """ + ), + params={ + "element_id": new_value.element_id, + "activity_instance_uid": activity_instance["activity_instance_uid"], + "activity_item_class_uid": activity_instance[ + "activity_item_class_uid" + ], + "odm_form_uid": activity_instance["odm_form_uid"], + "odm_item_group_uid": activity_instance["odm_item_group_uid"], + "order": activity_instance["order"], + "primary": activity_instance["primary"], + "preset_response_value": activity_instance["preset_response_value"], + "value_condition": activity_instance["value_condition"], + "value_dependent_map": activity_instance["value_dependent_map"], + }, + ) + return new_value def _create_new_value_node(self, ar: OdmItemAR) -> OdmItemValue: @@ -292,12 +383,54 @@ def _has_data_changed(self, ar: OdmItemAR, value: OdmItemValue) -> bool: for term in term_context.has_selected_term.all() } + activity_instances, _ = db.cypher_query( + dedent( + """ + MATCH (oiv:OdmItemValue)-[ltai:LINKS_TO_ACTIVITY_ITEM]->(ai:ActivityItem) + MATCH (ai)<-[:HAS_ACTIVITY_ITEM]-(aicr:ActivityItemClassRoot) + MATCH (ai)<-[:CONTAINS_ACTIVITY_ITEM]-(:ActivityInstanceValue)<-[:LATEST]-(air:ActivityInstanceRoot) + WHERE elementId(oiv) = $element_id + MATCH (oigr:OdmItemGroupRoot)-[:HAS_VERSION]->(:OdmItemGroupValue) + MATCH (ofr:OdmFormRoot)-[:HAS_VERSION]->(ofv:OdmFormValue) + MATCH (ai)<-[:HAS_ACTIVITY_ITEM]-(aicr:ActivityItemClassRoot) + + RETURN DISTINCT + air.uid AS activity_instance_uid, + aicr.uid AS activity_item_class_uid, + ofr.uid AS odm_form_uid, + oigr.uid AS odm_item_group_uid, + ltai.order AS order, + ltai.primary AS primary, + ltai.preset_response_value AS preset_response_value, + ltai.value_condition AS value_condition, + ltai.value_dependent_map AS value_dependent_map + """ + ), + params={"element_id": value.element_id}, + ) + + ar_activity_instances = [ + [ + activity_instance["activity_instance_uid"], + activity_instance["activity_item_class_uid"], + activity_instance["odm_form_uid"], + activity_instance["odm_item_group_uid"], + activity_instance["order"], + activity_instance["primary"], + activity_instance["preset_response_value"], + activity_instance["value_condition"], + activity_instance["value_dependent_map"], + ] + for activity_instance in ar.concept_vo.activity_instances + ] + are_rels_changed = ( set(ar.concept_vo.descriptions) != description_nodes or set(ar.concept_vo.aliases) != alias_nodes or set(ar.concept_vo.unit_definition_uids) != unit_definition_uids or ar.concept_vo.codelist_uid != codelist_uid or set(ar.concept_vo.term_uids) != term_uids + or sorted(ar_activity_instances) != sorted(activity_instances) ) return ( @@ -320,11 +453,17 @@ def find_by_uid_with_item_group_relation( rs, _ = db.cypher_query( """ MATCH (:OdmItemGroupRoot {uid: $item_group_uid})-[:HAS_VERSION {version: $item_group_version}]->(:OdmItemGroupValue) - -[ref:ITEM_REF]->(value:OdmItemValue)<-[hv_rel:HAS_VERSION]-(:OdmItemRoot {uid: $uid}) + -[ref:ITEM_REF]->(value:OdmItemValue) + + MATCH (value)<-[hv_rel:HAS_VERSION]-(:OdmItemRoot {uid: $uid}) + WITH value, ref, hv_rel + ORDER BY hv_rel.start_date DESC + WITH value, ref, collect(hv_rel) AS hv_rels + RETURN value.oid AS oid, value.name AS name, - hv_rel.version AS version, + hv_rels[0].version AS version, ref.order_number AS order_number, ref.mandatory AS mandatory, ref.key_sequence AS key_sequence, @@ -426,8 +565,13 @@ def _get_relationship(): has_term__uid=codelist_uid, has_term_root__uid=term_uid ) ).all() - submission_value = ( - cl_term_nodes[0][0].submission_value if cl_term_nodes else None + + submission_value = next( + ( + cl_term[0].submission_value + for cl_term in cl_term_nodes + if cl_term[4].end_date is None + ) ) if rel and submission_value: @@ -490,7 +634,7 @@ def _connect_relationships_to_new_value_node( MATCH (root:{self.root_class.__name__} {{uid: $root_uid}})-[ver_rel:HAS_VERSION]->(value:{self.value_class.__name__}) WITH root, ver_rel, value - ORDER BY ver_rel.start_date DESC + ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC LIMIT 2 WITH root, collect(value) AS values WITH root, values[0] as latest_value, values[1] as second_latest_value diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_attributes_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_attributes_repository.py index 2e2fff87..70073c3c 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_attributes_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_attributes_repository.py @@ -76,7 +76,6 @@ def _create_aggregate_root_instance_from_cypher_result( rel_data = codelist_dict["rel_data"] major, minor = rel_data.get("version").split(".") - # print(codelist_dict) return CTCodelistAttributesAR.from_repository_values( uid=codelist_dict["codelist_uid"], ct_codelist_attributes_vo=CTCodelistAttributesVO.from_repository_values( diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_generic_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_generic_repository.py index 70cba612..eaeb951b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_generic_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_codelist_generic_repository.py @@ -801,6 +801,54 @@ def get_codelist_submval_by_uid(codelist_uid: str): return None + def get_paired_codelist_uid(self, codelist_uid: str) -> str | None: + """ + Returns the paired codelist UID if the codelist is part of a codelist pair. + Returns None if no pairing exists. + :param codelist_uid: The UID of the codelist + :return: Paired codelist UID or None + """ + codelist_root = CTCodelistRoot.nodes.get_or_none(uid=codelist_uid) + if not codelist_root: + return None + + # Check if this codelist has a paired code codelist (outgoing relationship) + paired_code = codelist_root.has_paired_code_codelist.get_or_none() + if paired_code: + return paired_code.uid + + # Check if this codelist has a paired name codelist (incoming relationship) + paired_name = codelist_root.has_paired_name_codelist.get_or_none() + if paired_name: + return paired_name.uid + + return None + + def is_term_in_codelist(self, term_uid: str, codelist_uid: str) -> bool: + """ + Checks if a term is currently in a codelist (has an active HAS_TERM relationship). + :param term_uid: The UID of the term + :param codelist_uid: The UID of the codelist + :return: True if the term is in the codelist, False otherwise + """ + codelist_root = CTCodelistRoot.nodes.get_or_none(uid=codelist_uid) + if not codelist_root: + return False + + term_root = CTTermRoot.nodes.get_or_none(uid=term_uid) + if not term_root: + return False + + # Check if there's an active HAS_TERM relationship + for codelist_term in codelist_root.has_term.all(): + term_from_codelist = codelist_term.has_term_root.get_or_none() + if term_from_codelist and term_from_codelist.uid == term_uid: + has_term_rel = codelist_root.has_term.relationship(codelist_term) + if has_term_rel.end_date is None: + return True + + return False + def get_or_create_selected_term( self, term_node, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_generic_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_generic_repository.py index dc5b97d0..7ba6b7b0 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_generic_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/controlled_terminologies/ct_term_generic_repository.py @@ -187,10 +187,16 @@ def term_exists(self, term_uid: str) -> bool: def get_term_name_and_attributes_by_codelist_uids(self, codelist_uids: list[Any]): query = """ - MATCH (codelist:CTCodelistRoot)-[:HAS_TERM]->(codelist_term:CTCodelistTerm)-[:HAS_TERM_ROOT]->(term_root:CTTermRoot)-[:HAS_ATTRIBUTES_ROOT]->(term_attr_root:CTTermAttributesRoot)-[:LATEST]->(term_attr_value:CTTermAttributesValue) + MATCH (codelist:CTCodelistRoot)-[ht:HAS_TERM]->(codelist_term:CTCodelistTerm)-[:HAS_TERM_ROOT]->(term_root:CTTermRoot) + -[:HAS_ATTRIBUTES_ROOT]->(term_attr_root:CTTermAttributesRoot)-[:LATEST]->(term_attr_value:CTTermAttributesValue) MATCH (term_root)-[:HAS_NAME_ROOT]->(term_name_root:CTTermNameRoot)-[:LATEST]->(term_name_value:CTTermNameValue) - WHERE codelist.uid in $codelist_uids - RETURN term_name_value.name as name, term_root.uid as term_uid, codelist.uid as codelist_uid, codelist_term.submission_value as submission_value, term_attr_value.preferred_term as nci_preferred_name + WHERE codelist.uid in $codelist_uids and ht.end_date IS NULL + RETURN + term_name_value.name as name, + term_root.uid as term_uid, + codelist.uid as codelist_uid, + codelist_term.submission_value as submission_value, + term_attr_value.preferred_term as nci_preferred_name """ items, prop_names = db.cypher_query(query, {"codelist_uids": codelist_uids}) @@ -644,8 +650,6 @@ def find_uid_by_submission_value(self, value: str, codelist_uid: str) -> str | N RETURN term_root.uid """ items, _ = db.cypher_query(cypher_query, params) - print("--øøø find_uid_by_submission_value", value, codelist_uid) - print(items) if len(items) > 0: return items[0][0] return None @@ -666,6 +670,39 @@ def find_uid_by_submission_values( return items[0][0] return None + def get_submission_values_for_term(self, term_uid: str) -> list[str]: + """ + Returns all existing submission values for a given term. + Returns empty list if term is not found. + :param term_uid: The UID of the term + :return: List of submission values + """ + term_root = CTTermRoot.nodes.get_or_none(uid=term_uid) + if not term_root: + return [] + + submission_values = [] + for codelist_term in term_root.has_term_root.all(): + submission_values.append(codelist_term.submission_value) + # break + + return list(set(submission_values)) # Remove duplicates + + def get_library_name_for_term(self, term_uid: str) -> str | None: + """ + Returns the library name for a given term. + :param term_uid: The UID of the term + :return: Library name or None + """ + term_root = CTTermRoot.nodes.get_or_none(uid=term_uid) + if not term_root: + return None + + library = term_root.has_library.get_or_none() + if library: + return library.name + return None + def _generate_generic_match_clause( self, codelist_uid: str | None = None, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/data_suppliers/data_supplier_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/data_suppliers/data_supplier_repository.py index 9464e582..1a6b0099 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/data_suppliers/data_supplier_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/data_suppliers/data_supplier_repository.py @@ -1,4 +1,6 @@ -from neomodel import NodeSet +from typing import Any + +from neomodel import NodeSet, db from neomodel.sync_.match import ( Collect, Last, @@ -96,22 +98,40 @@ def get_neomodel_extension_query(self) -> NodeSet: initial_context=[NodeNameResolver("self")], ) - def extend_distinct_headers_query(self, nodeset: NodeSet) -> NodeSet: - return nodeset.subquery( - self.root_class.nodes.fetch_relations("has_version") - .intermediate_transform( - {"rel": {"source": RelationNameResolver("has_version")}}, - ordering=[ - RawCypher("toInteger(split(rel.version, '.')[0])"), - RawCypher("toInteger(split(rel.version, '.')[1])"), - "rel.end_date", - "rel.start_date", - ], + def extend_distinct_headers_query( + self, + nodeset: NodeSet, + field_name: str, + filter_by: dict[str, dict[str, Any]] | None = None, + ) -> NodeSet: + # Get version relationship properties dynamically + version_properties = set(VersionRelationship.defined_properties().keys()) + # Map author_id to author_username as it's exposed that way in the API + version_fields = version_properties | {"author_username"} + version_fields.discard("author_id") + + # Check if we need version data + need_latest_version = field_name in version_fields + if not need_latest_version and filter_by: + need_latest_version = any(key in version_fields for key in filter_by.keys()) + + if need_latest_version: + return nodeset.subquery( + self.root_class.nodes.fetch_relations("has_version") + .intermediate_transform( + {"rel": {"source": RelationNameResolver("has_version")}}, + ordering=[ + RawCypher("toInteger(split(rel.version, '.')[0])"), + RawCypher("toInteger(split(rel.version, '.')[1])"), + "rel.end_date", + "rel.start_date", + ], + ) + .annotate(latest_version=Last(Collect("rel"))), + ["latest_version"], + initial_context=[NodeNameResolver("self")], ) - .annotate(latest_version=Last(Collect("rel"))), - ["latest_version"], - initial_context=[NodeNameResolver("self")], - ) + return nodeset def _create_aggregate_root_instance_from_version_root_relationship_and_value( self, @@ -262,3 +282,74 @@ def _maintain_parameters( def generate_uid(self) -> str: return DataSupplierRoot.get_next_free_uid_and_increment_counter() + + def get_next_available_order(self, supplier_type_uid: str) -> int: + query = """ + MATCH (root:DataSupplierRoot)-[:LATEST]->(value:DataSupplierValue) + MATCH (value)-[:HAS_DATA_SUPPLIER_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(type:CTTermRoot {uid: $type_uid}) + RETURN max(value.order) + """ + results, _ = db.cypher_query(query, {"type_uid": supplier_type_uid}) + max_order = results[0][0] + return (max_order + 1) if max_order is not None else 1 + + def bump_orders(self, supplier_type_uid: str, start_order: int) -> None: + query = """ + MATCH (root:DataSupplierRoot)-[:LATEST]->(value:DataSupplierValue) + MATCH (value)-[:HAS_DATA_SUPPLIER_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(type:CTTermRoot {uid: $type_uid}) + WHERE value.order >= $start_order + SET value.order = value.order + 1 + """ + db.cypher_query( + query, {"type_uid": supplier_type_uid, "start_order": start_order} + ) + + def reorder(self, supplier_type_uid: str, old_order: int, new_order: int) -> None: + if new_order > old_order: + # Moving down: Shift intermediate items UP (decrement their order) + # Example: 1, [2], 3, 4 -> Move 2 to 3 -> 1, 3, [2], 4 + # Items at 3 (old 3) needs to become 2. + query = """ + MATCH (root:DataSupplierRoot)-[:LATEST]->(value:DataSupplierValue) + MATCH (value)-[:HAS_DATA_SUPPLIER_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(type:CTTermRoot {uid: $type_uid}) + WHERE value.order > $old_order AND value.order <= $new_order + SET value.order = value.order - 1 + """ + db.cypher_query( + query, + { + "type_uid": supplier_type_uid, + "old_order": old_order, + "new_order": new_order, + }, + ) + elif new_order < old_order: + # Moving up: Shift intermediate items DOWN (increment their order) + # Example: 1, 2, [3], 4 -> Move 3 to 2 -> 1, [3], 2, 4 + # Item at 2 needs to become 3. + query = """ + MATCH (root:DataSupplierRoot)-[:LATEST]->(value:DataSupplierValue) + MATCH (value)-[:HAS_DATA_SUPPLIER_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(type:CTTermRoot {uid: $type_uid}) + WHERE value.order >= $new_order AND value.order < $old_order + SET value.order = value.order + 1 + """ + db.cypher_query( + query, + { + "type_uid": supplier_type_uid, + "old_order": old_order, + "new_order": new_order, + }, + ) + + def close_gap(self, supplier_type_uid: str, gap_order: int) -> None: + """ + Decrements the order of all items after the gap_order to fill the hole. + """ + query = """ + MATCH (root:DataSupplierRoot)-[:LATEST]->(value:DataSupplierValue) + MATCH (value)-[:HAS_DATA_SUPPLIER_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(type:CTTermRoot {uid: $type_uid}) + WHERE value.order > $gap_order + SET value.order = value.order - 1 + """ + db.cypher_query(query, {"type_uid": supplier_type_uid, "gap_order": gap_order}) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/generic_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/generic_repository.py index e6cf6392..0fe045de 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/generic_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/generic_repository.py @@ -1,20 +1,29 @@ +import datetime from dataclasses import dataclass -from typing import Any, Mapping +from textwrap import dedent +from typing import Any, Iterable, Mapping from cachetools import TTLCache -from neomodel import RelationshipDefinition, RelationshipManager +from neomodel import RelationshipDefinition, RelationshipManager, StructuredNode, db from clinical_mdr_api.domain_repositories.models.generic import ( ClinicalMdrNode, VersionRelationship, VersionRoot, ) -from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction +from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue +from clinical_mdr_api.domain_repositories.models.study_audit_trail import ( + Create, + Delete, + Edit, + StudyAction, +) from clinical_mdr_api.domain_repositories.models.study_field import StudyField from clinical_mdr_api.domain_repositories.models.study_selections import StudySelection from clinical_mdr_api.repositories._utils import sb_clear_cache from common.config import settings from common.exceptions import ValidationException +from common.telemetry import trace_calls class EntityNotFoundError(LookupError): @@ -78,6 +87,7 @@ def _get_version_relation_keys(self, root_node: VersionRoot) -> tuple[ root_node.latest_retired, ) + @trace_calls @sb_clear_cache(caches=["cache_store_item_by_uid"]) def _db_create_and_link_nodes( self, @@ -113,6 +123,7 @@ def _db_create_and_link_nodes( latest_final = self._db_create_relationship(latest_final, value) return root, value, latest_value, latest_draft, latest_final + @trace_calls @sb_clear_cache(caches=["cache_store_item_by_uid"]) def _db_save_node(self, node: ClinicalMdrNode) -> ClinicalMdrNode: """ @@ -123,6 +134,7 @@ def _db_save_node(self, node: ClinicalMdrNode) -> ClinicalMdrNode: node.save() return node + @trace_calls @sb_clear_cache(caches=["cache_store_item_by_uid"]) def _db_create_relationship( self, @@ -141,6 +153,7 @@ def _db_create_relationship( return origin.connect(destination, parameters) return origin.connect(destination) + @trace_calls @sb_clear_cache(caches=["cache_store_item_by_uid"]) def _db_remove_relationship( self, relationship: RelationshipManager, value: ClinicalMdrNode | None = None @@ -159,6 +172,7 @@ def generate_uid_callback(self): return self.root_class.get_next_free_uid_and_increment_counter() +@trace_calls def get_connected_node_by_rel_name_and_study_value( node: Any, connected_rel_name: str, @@ -209,6 +223,7 @@ def get_connected_node_by_rel_name_and_study_value( ) +@trace_calls def manage_previous_connected_study_selection_relationships( previous_item: Any, study_value_node: Any, @@ -298,3 +313,157 @@ def manage_previous_connected_study_selection_relationships( msg=f"The modified version of '{previous_item.uid}' of type '{previous_item.__label__}' is not connected to any StudyValue node.", ) getattr(previous_item, study_value_rel_name).disconnect(study_value_node) + + +@trace_calls +def _manage_versioning_with_relations( + study_root: StudyRoot | str, + action_type: type[StudyAction], + before: StructuredNode | None = None, + after: StructuredNode | None = None, + exclude_relationships: Iterable[ + type[StructuredNode] | type[RelationshipDefinition] | str + ] = tuple(), + **properties, +) -> StudyAction: + """ + Manages versioning of StudySelection nodes: Creates StudyAction, and copies relationships from `before` to `after`. + + Relationship from StudyValue to `after` node should be connected outside of this method. + Otherwise, this method is meant to replace `manage_previous_connected_study_selection_relationships` + with better performance due to batching multiple statements into less Cypher queries. + + Args: + study_root (StudyRoot | str): The StudyRoot node or its uid. + action_type (type[StudyAction]): The StudyAction node type to create (Create, Edit, Delete). + before (StructuredNode | None): The 'before' node (for Edit, Delete). + after (StructuredNode | None): The 'after' node (for Edit, Create). + exclude_relationships (Iterable[type[StructuredNode] | type[RelationshipDefinition] | str ]): + Node-types and relationships to exclude when copying relationships from `before` to `after` node. + Relationships from StudyAction and StudyValue nodes are always excluded, never copied. + **properties: Additional properties for the StudyAction node. + `date` will be set to current UTC time if not provided. + Returns: + StudyAction: The created StudyAction node. + Raises: + RuntimeError: If action_type is not StudyAction or required nodes are missing. + """ + + if not (isinstance(action_type, type) and issubclass(action_type, StudyAction)): + raise RuntimeError("Action type must be StudyAction.") + + if before is None and action_type in {Edit, Delete}: + raise RuntimeError(f"{action_type.__name__} action must have a 'before' node.") + + if after is None and action_type in {Edit, Create}: + raise RuntimeError(f"{action_type.__name__} action must have an 'after' node.") + + # Match StudyRoot node + if isinstance(study_root, StructuredNode): + query = [ + "MATCH (study_root:StudyRoot)-[:LATEST]->(study_value:StudyValue)", + f"WHERE {db.get_id_method()}(study_root) = $_study_root", + ] + params = {"_study_root": study_root.element_id} + else: + query = [ + "MATCH (study_root:StudyRoot {uid: $_study_root})-[:LATEST]->(study_value:StudyValue)" + ] + params = {"_study_root": study_root} + + # Create StudyAction node + if "date" not in properties: + properties["date"] = datetime.datetime.now(datetime.timezone.utc) + properties = action_type.deflate(properties, skip_empty=True) + query.append( + dedent( + f""" + CREATE (action:{':'.join(action_type.inherited_labels())}:StudyAction {{{', '.join(f'{k}: ${k}' for k in properties)}}})<-[:AUDIT_TRAIL]-(study_root) + WITH * + """ + ).strip() + ) + + # Match & link previous node + if before: + query.append(f"MATCH (before) WHERE {db.get_id_method()}(before) = $_before") + params["_before"] = before.element_id + query.append("CREATE (action)-[:BEFORE]->(before)") + query.append("WITH *") + + # Match & link new node + if after: + query.append(f"MATCH (after) WHERE {db.get_id_method()}(after) = $_after") + params["_after"] = after.element_id + query.append("CREATE (action)-[:AFTER]->(after)") + query.append("WITH *") + + # Copy relationships + if before and after: + _exclude_relationships = set() + _exclude_labels = { + StudyValue.__name__, # exclude (HAS_...) relations from StudyValue node + StudyAction.__name__, # exclude (BEFORE/AFTER) relations from StudyAction node + } + for rel in exclude_relationships: + if isinstance(rel, str): + _exclude_relationships.add(rel) + elif issubclass(rel, StructuredNode): + _exclude_labels.add(rel.__name__) + elif issubclass(rel, RelationshipDefinition): + _exclude_relationships.add(rel.definition["relation_type"]) + else: + raise RuntimeError( + "exclude_relationships must be an iterable of StructuredNode subclasses or relationship type strings." + ) + + query.append( + dedent( + """ + CALL { + WITH before, after + MATCH (before)-[r]->(target) + WHERE NOT (type(r) IN $_exclude_relationships OR any(label IN labels(target) WHERE label IN $_exclude_labels)) + CALL apoc.create.relationship(after, type(r), properties(r), target) YIELD rel + RETURN count(rel) AS num_rels_out + } + CALL { + WITH before, after + MATCH (before)<-[r]-(source) + WHERE NOT (type(r) IN $_exclude_relationships OR any(label IN labels(source) WHERE label IN $_exclude_labels)) + CALL apoc.create.relationship(source, type(r), properties(r), after) YIELD rel + RETURN count(rel) AS num_rels_in + } + """ + ).strip() + ) + params["_exclude_relationships"] = tuple(_exclude_relationships) + params["_exclude_labels"] = tuple(_exclude_labels) + + # Unlink previous relationship from latest StudyValue + if before: + query.append( + dedent( + """ + CALL { + WITH study_value, before + MATCH (study_value)-[rel]->(before) + DELETE rel + RETURN count(rel) AS num_rels_del + } + """ + ).strip() + ) + + # Execute query + query.append("RETURN action") + + query_str = "\n".join(query) + params.update(properties) + + result, _ = db.cypher_query(query_str, params) + + # Return the inflated StudyAction-type node (Create, Edit, Delete) + node = result[0][0] + node = action_type.inflate(node) + return node diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/library_item_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/library_item_repository.py index acf024f0..868c48e4 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/library_item_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/library_item_repository.py @@ -829,6 +829,11 @@ def _get_version_active_at_date_time( latest_matching_relationship is None or latest_matching_relationship.start_date < relationship.start_date + or ( + latest_matching_relationship.start_date + == relationship.start_date + and relationship.end_date is None + ) ) and relationship.status == status.value: latest_matching_relationship = relationship latest_matching_value = matching_value @@ -837,6 +842,11 @@ def _get_version_active_at_date_time( latest_matching_relationship is None or latest_matching_relationship.start_date < relationship.start_date + or ( + latest_matching_relationship.start_date + == relationship.start_date + and relationship.end_date is None + ) ): latest_matching_relationship = relationship latest_matching_value = matching_value @@ -2087,7 +2097,7 @@ def _headers_cypher_query( CALL { WITH root, value MATCH (root)-[ver_rel:HAS_VERSION]->() - WITH * ORDER BY ver_rel.start_date DESC LIMIT 1 + WITH * ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC LIMIT 1 WHERE $status <> "Final" OR (ver_rel.status <> "Retired" AND $status = "Final") MATCH (_root)-[ver_rel]->() RETURN _root @@ -2103,7 +2113,7 @@ def _headers_cypher_query( MATCH (_root)-[ver_rel:HAS_VERSION]->(_value) WITH ver_rel, _root, _value {version_where_stmt} - RETURN _root, _value, ver_rel ORDER BY ver_rel.start_date DESC LIMIT 1 + RETURN _root, _value, ver_rel ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC LIMIT 1 }} WITH _root AS root, _value AS value, library, ver_rel """ @@ -2244,7 +2254,7 @@ def _find_cypher_query_optimized( CALL { WITH root MATCH (root)-[ver_rel:HAS_VERSION]->() - WITH * ORDER BY ver_rel.start_date DESC LIMIT 1 + WITH * ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC LIMIT 1 WHERE ver_rel.status = "Final" // WHEN THE LATEST VERSION IS FINAL --THEN--> PASS EVERYTHING MATCH (_root)-[ver_rel]->() @@ -2258,7 +2268,7 @@ def _find_cypher_query_optimized( CALL {{ WITH root MATCH (root)-[ver_rel:HAS_VERSION]->() - WITH * ORDER BY ver_rel.start_date DESC LIMIT 1 + WITH * ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC LIMIT 1 {''' WHERE $status <> "Final" // WHEN THE USER DOESN'T ASK FOR FINAL --THEN--> PASS EVERYTHING @@ -2299,7 +2309,9 @@ def _find_cypher_query_optimized( else: ver_rel_filtering = """ WITH *, NULL as latest_version - RETURN _root, _value, ver_rel, latest_version ORDER BY ver_rel.start_date DESC LIMIT 1 + RETURN _root, _value, ver_rel, latest_version + ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC + LIMIT 1 """ version_call = f""" CALL {{ @@ -2521,12 +2533,16 @@ def _find_cypher_query_optimized( return_stmt += " SKIP $page_number * $page_size LIMIT $page_size " else: if not uid: - return_stmt += " ORDER BY ver_rel.start_date DESC " + return_stmt += ( + " ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC " + ) if with_pagination: return_stmt += " SKIP $page_number * $page_size LIMIT $page_size " else: - return_stmt += " ORDER BY ver_rel.start_date DESC " + return_stmt += ( + " ORDER BY ver_rel.start_date DESC, ver_rel.end_date DESC " + ) return match_stmt, return_stmt @@ -2558,26 +2574,26 @@ def _activity_instance_root_match_return_stmt(self): CALL{ WITH root,ver_rel,activity_instance_value WITH *, - [(root)-[ver_rel]->(activity_instance_value)-[:HAS_ACTIVITY]->(activity_instance_grouping:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) | + [(root)-[ver_rel]->(activity_instance_value)-[:HAS_ACTIVITY]->(activity_instance_grouping:ActivityGrouping) | { - activity: head(apoc.coll.sortMulti([(activity_instance_grouping)-[:HAS_GROUPING]-(activity_value:ActivityValue)<-[has_version:HAS_VERSION]- - (activity_root:ActivityRoot) | + activity: head(apoc.coll.sortMulti([(activity_instance_grouping)<-[:HAS_GROUPING]-(activity_value:ActivityValue)<-[has_version:HAS_VERSION]- + (activity_root:ActivityRoot) | { uid: activity_root.uid, name: activity_value.name, major_version: toInteger(split(has_version.version,'.')[0]), minor_version: toInteger(split(has_version.version,'.')[1]) }], ['major_version', 'minor_version'])), - activity_subgroup: head(apoc.coll.sortMulti([(activity_valid_group)<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue)<-[has_version:HAS_VERSION]- - (activity_subgroup_root:ActivitySubGroupRoot) | + activity_subgroup: head(apoc.coll.sortMulti([(activity_instance_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[has_version:HAS_VERSION]- + (activity_subgroup_root:ActivitySubGroupRoot) | { uid: activity_subgroup_root.uid, name: activity_subgroup_value.name, major_version: toInteger(split(has_version.version,'.')[0]), minor_version: toInteger(split(has_version.version,'.')[1]) - }], ['major_version', 'minor_version'])), - activity_group: head(apoc.coll.sortMulti([(activity_valid_group)-[:IN_GROUP]-(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- - (activity_group_root:ActivityGroupRoot) | + }], ['major_version', 'minor_version'])), + activity_group: head(apoc.coll.sortMulti([(activity_instance_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- + (activity_group_root:ActivityGroupRoot) | { uid: activity_group_root.uid, name: activity_group_value.name, @@ -2610,21 +2626,48 @@ def _activity_instance_root_match_return_stmt(self): } CALL{ WITH activity_item - MATCH (activity_item)-[:HAS_ODM_FORM]->(odm_form_root:OdmFormRoot) + MATCH (activity_item)<-[ltai:LINKS_TO_ACTIVITY_ITEM]-(odm_form_root:OdmFormRoot) MATCH (odm_form_root)-[:LATEST]->(odm_form_value:OdmFormValue) - RETURN collect(DISTINCT {uid: odm_form_root.uid, oid: odm_form_value.oid, name: odm_form_value.name}) AS odm_forms + RETURN collect(DISTINCT { + uid: odm_form_root.uid, + oid: odm_form_value.oid, + name: odm_form_value.name, + order: ltai.order, + primary: ltai.primary, + preset_response_value: ltai.preset_response_value, + value_condition: ltai.value_condition, + value_dependent_map: ltai.value_dependent_map + }) AS odm_forms } CALL{ WITH activity_item - MATCH (activity_item)-[:HAS_ODM_ITEM_GROUP]->(odm_item_group_root:OdmItemRoot) + MATCH (activity_item)<-[ltai:LINKS_TO_ACTIVITY_ITEM]-(odm_item_group_root:OdmItemRoot) MATCH (odm_item_group_root)-[:LATEST]->(odm_item_group_value:OdmItemValue) - RETURN collect(DISTINCT {uid: odm_item_group_root.uid, oid: odm_item_group_value.oid, name: odm_item_group_value.name}) AS odm_item_groups + RETURN collect(DISTINCT { + uid: odm_item_group_root.uid, + oid: odm_item_group_value.oid, + name: odm_item_group_value.name, + order: ltai.order, + primary: ltai.primary, + preset_response_value: ltai.preset_response_value, + value_condition: ltai.value_condition, + value_dependent_map: ltai.value_dependent_map + }) AS odm_item_groups } CALL{ WITH activity_item - MATCH (activity_item)-[:HAS_ODM_ITEM]->(odm_item_root:OdmItemRoot) + MATCH (activity_item)<-[ltai:LINKS_TO_ACTIVITY_ITEM]-(odm_item_root:OdmItemRoot) MATCH (odm_item_root)-[:LATEST]->(odm_item_value:OdmItemValue) - RETURN collect(DISTINCT {uid: odm_item_root.uid, oid: odm_item_value.oid, name: odm_item_value.name}) AS odm_items + RETURN collect(DISTINCT { + uid: odm_item_root.uid, + oid: odm_item_value.oid, + name: odm_item_value.name, + order: ltai.order, + primary: ltai.primary, + preset_response_value: ltai.preset_response_value, + value_condition: ltai.value_condition, + value_dependent_map: ltai.value_dependent_map + }) AS odm_items } RETURN COLLECT( distinct { activity_item_class_uid: activity_item_class_root.uid, @@ -2667,9 +2710,9 @@ def _activity_root_match_return_stmt(self): CALL { WITH root,activity_value,ver_rel - WITH *, [(root)-[ver_rel]->(activity_value:ActivityValue)-[:HAS_GROUPING]->(:ActivityGrouping)-[:IN_SUBGROUP]->(activity_valid_group:ActivityValidGroup) | + WITH *, [(root)-[ver_rel]->(activity_value:ActivityValue)-[:HAS_GROUPING]->(activity_grouping:ActivityGrouping) | { - activity_subgroup: head(apoc.coll.sortMulti([(activity_valid_group)<-[:HAS_GROUP]-(activity_subgroup_value:ActivitySubGroupValue)<-[has_version:HAS_VERSION]- + activity_subgroup: head(apoc.coll.sortMulti([(activity_grouping)-[:HAS_SELECTED_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[has_version:HAS_VERSION]- (activity_subgroup_root:ActivitySubGroupRoot) WHERE has_version.status in ["Final", "Retired"]| { uid:activity_subgroup_root.uid, @@ -2679,7 +2722,7 @@ def _activity_root_match_return_stmt(self): start_date: has_version.start_date, status: has_version.status }], ['major_version', 'minor_version', 'start_date'])), - activity_group: head(apoc.coll.sortMulti([(activity_valid_group)-[:IN_GROUP]-(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- + activity_group: head(apoc.coll.sortMulti([(activity_grouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- (activity_group_root:ActivityGroupRoot) WHERE has_version.status in ["Final", "Retired"] | { uid:activity_group_root.uid, @@ -2712,17 +2755,14 @@ def _activity_subgroup_root_match_return_stmt(self): CALL{ WITH root WITH *, - [(root)-[:LATEST]->(concept_value)-[:HAS_GROUP]->(activity_valid_group:ActivityValidGroup) | + apoc.coll.toSet([(root)-[:LATEST]->(concept_value)<-[:HAS_SELECTED_SUBGROUP]-(activity_grouping:ActivityGrouping)-[:HAS_SELECTED_GROUP]->(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- + (activity_group_root:ActivityGroupRoot) | { - activity_group:head(apoc.coll.sortMulti([(activity_valid_group)-[:IN_GROUP]-(activity_group_value:ActivityGroupValue)<-[has_version:HAS_VERSION]- - (activity_group_root:ActivityGroupRoot) | - { - uid:activity_group_root.uid, - major_version: toInteger(split(has_version.version,'.')[0]), - minor_version: toInteger(split(has_version.version,'.')[1]), - name: activity_group_value.name - }], ['major_version', 'minor_version'])) - }] AS activity_groups + uid:activity_group_root.uid, + major_version: toInteger(split(has_version.version,'.')[0]), + minor_version: toInteger(split(has_version.version,'.')[1]), + name: activity_group_value.name + }]) AS activity_groups RETURN activity_groups } """ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/activities.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/activities.py index 5f4617d4..165b2227 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/activities.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/activities.py @@ -31,18 +31,6 @@ ) -class ActivityValidGroup(ClinicalMdrNodeWithUID): - in_group = RelationshipTo( - "ActivityGroupValue", "IN_GROUP", model=ClinicalMdrRel, cardinality=One - ) - has_group = RelationshipFrom( - "ActivitySubGroupValue", "HAS_GROUP", model=ClinicalMdrRel, cardinality=One - ) - in_subgroup = RelationshipFrom( - "ActivityGrouping", "IN_SUBGROUP", model=ClinicalMdrRel, cardinality=One - ) - - class ActivityGroupValue(ConceptValue): has_latest_value = RelationshipFrom( "ActivityGroupRoot", "LATEST", model=ClinicalMdrRel @@ -50,8 +38,11 @@ class ActivityGroupValue(ConceptValue): has_version = RelationshipFrom( "ActivityGroupRoot", "HAS_VERSION", model=VersionRelationship ) - in_group = RelationshipFrom( - ActivityValidGroup, "IN_GROUP", model=ClinicalMdrRel, cardinality=ZeroOrMore + has_selected_group = RelationshipFrom( + "ActivityGrouping", + "HAS_SELECTED_GROUP", + model=ClinicalMdrRel, + cardinality=ZeroOrMore, ) @@ -80,8 +71,11 @@ class ActivitySubGroupValue(ConceptValue): has_version = RelationshipFrom( "ActivitySubGroupRoot", "HAS_VERSION", model=VersionRelationship ) - has_group = RelationshipTo( - ActivityValidGroup, "HAS_GROUP", model=ClinicalMdrRel, cardinality=OneOrMore + has_selected_subgroup = RelationshipFrom( + "ActivityGrouping", + "HAS_SELECTED_SUBGROUP", + model=ClinicalMdrRel, + cardinality=OneOrMore, ) @@ -104,8 +98,14 @@ class ActivitySubGroupRoot(ConceptRoot): class ActivityGrouping(ClinicalMdrNodeWithUID): - in_subgroup = RelationshipTo( - ActivityValidGroup, "IN_SUBGROUP", model=ClinicalMdrRel, cardinality=OneOrMore + has_selected_group = RelationshipTo( + ActivityGroupValue, "HAS_SELECTED_GROUP", model=ClinicalMdrRel, cardinality=One + ) + has_selected_subgroup = RelationshipTo( + ActivitySubGroupValue, + "HAS_SELECTED_SUBGROUP", + model=ClinicalMdrRel, + cardinality=One, ) has_grouping = RelationshipFrom( "ActivityValue", "HAS_GROUPING", model=ClinicalMdrRel, cardinality=One @@ -187,25 +187,6 @@ class ActivityItem(ClinicalMdrNode): cardinality=ZeroOrMore, ) - from clinical_mdr_api.domain_repositories.models.odm import ( - OdmFormValue, - OdmItemGroupValue, - OdmItemValue, - ) - - has_odm_form = RelationshipTo( - OdmFormValue, "HAS_ODM_FORM", model=ClinicalMdrRel, cardinality=ZeroOrOne - ) - has_odm_item_group = RelationshipTo( - OdmItemGroupValue, - "HAS_ODM_ITEM_GROUP", - model=ClinicalMdrRel, - cardinality=ZeroOrOne, - ) - has_odm_item = RelationshipTo( - OdmItemValue, "HAS_ODM_ITEM", model=ClinicalMdrRel, cardinality=ZeroOrOne - ) - class ActivityInstanceValue(ConceptValue): is_research_lab = BooleanProperty() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/biomedical_concepts.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/biomedical_concepts.py index de5fcc5f..7e55974d 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/biomedical_concepts.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/biomedical_concepts.py @@ -27,6 +27,8 @@ class ActivityItemClassRel(ClinicalMdrRel): mandatory = BooleanProperty() is_adam_param_specific_enabled = BooleanProperty() + is_additional_optional = BooleanProperty() + is_default_linked = BooleanProperty() class ActivityInstanceClassValue(VersionValue): @@ -72,6 +74,7 @@ class ActivityItemClassValue(VersionValue): definition = StringProperty() nci_concept_id = StringProperty() order = IntegerProperty() + display_name = StringProperty() has_latest_value = RelationshipFrom( "ActivityItemClassRoot", "LATEST", model=ClinicalMdrRel ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/odm.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/odm.py index 3ec1686c..bafcb077 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/odm.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/odm.py @@ -9,6 +9,7 @@ ZeroOrMore, ) +from clinical_mdr_api.domain_repositories.models.activities import ActivityItem from clinical_mdr_api.domain_repositories.models.concepts import ( ConceptRoot, ConceptValue, @@ -153,6 +154,9 @@ class OdmFormValue(ConceptValue): oid = StringProperty() repeating = BooleanProperty() sdtm_version = StringProperty() + + links_to_activity_item = RelationshipTo(ActivityItem, "LINKS_TO_ACTIVITY_ITEM") + has_description = RelationshipTo( OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel ) @@ -211,6 +215,9 @@ class OdmItemGroupValue(ConceptValue): origin = StringProperty() purpose = StringProperty() comment = StringProperty() + + links_to_activity_item = RelationshipTo(ActivityItem, "LINKS_TO_ACTIVITY_ITEM") + has_description = RelationshipTo( OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel ) @@ -269,6 +276,14 @@ class OdmItemUnitDefinitionRelationship(ClinicalMdrRel): order = IntegerProperty() +class ActivityItemRel(ClinicalMdrRel): + order = IntegerProperty() + primary = BooleanProperty() + preset_response_value = StringProperty() + value_condition = StringProperty() + value_dependent_map = StringProperty() + + class OdmItemValue(ConceptValue): oid = StringProperty() prompt = StringProperty() @@ -279,6 +294,11 @@ class OdmItemValue(ConceptValue): sds_var_name = StringProperty() origin = StringProperty() comment = StringProperty() + + links_to_activity_item = RelationshipTo( + ActivityItem, "LINKS_TO_ACTIVITY_ITEM", model=ActivityItemRel + ) + has_description = RelationshipTo( OdmDescription, "HAS_DESCRIPTION", model=ClinicalMdrRel ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_field.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_field.py index 5265a57d..83495b4f 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_field.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_field.py @@ -126,6 +126,9 @@ class StudyIntField(StudyField): class StudyArrayField(StudyField): value = ArrayProperty() field_name = StringProperty() + has_array_field = RelationshipFrom( + ".study.StudyValue", "HAS_ARRAY_FIELD", model=ClinicalMdrRel + ) class StudyBooleanField(StudyField): diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_selections.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_selections.py index 5c0e0637..fed30859 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_selections.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/models/study_selections.py @@ -105,6 +105,12 @@ class StudyDataSupplier(StudySelection): model=ClinicalMdrRel, cardinality=ZeroOrOne, ) + study_activity_instance_has_study_data_supplier = RelationshipFrom( + "StudyActivityInstance", + "HAS_STUDY_DATA_SUPPLIER", + model=ClinicalMdrRel, + cardinality=ZeroOrMore, + ) class StudyObjective(StudySelection): @@ -441,6 +447,24 @@ class StudyActivityInstance(StudySelection): model=ClinicalMdrRel, cardinality=ZeroOrMore, ) + has_study_data_supplier = RelationshipTo( + StudyDataSupplier, + "HAS_STUDY_DATA_SUPPLIER", + model=ClinicalMdrRel, + cardinality=ZeroOrOne, + ) + has_origin_type = RelationshipTo( + CTTermContext, + "HAS_ORIGIN_TYPE", + model=ClinicalMdrRel, + cardinality=ZeroOrOne, + ) + has_origin_source = RelationshipTo( + CTTermContext, + "HAS_ORIGIN_SOURCE", + model=ClinicalMdrRel, + cardinality=ZeroOrOne, + ) class StudyActivitySchedule(StudySelection): diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/neomodel_ext_item_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/neomodel_ext_item_repository.py index 7f892ff8..59a4cb8e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/neomodel_ext_item_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/neomodel_ext_item_repository.py @@ -40,7 +40,14 @@ def get_neomodel_extension_query(self) -> NodeSet: raise NotImplementedError - def extend_distinct_headers_query(self, nodeset: NodeSet) -> NodeSet: + def extend_distinct_headers_query( + self, + nodeset: NodeSet, + field_name: str, # pylint: disable=unused-argument + filter_by: ( # pylint: disable=unused-argument + dict[str, dict[str, Any]] | None + ) = None, + ) -> NodeSet: """ Method to extend the query built for distinct header retrieval. """ @@ -168,7 +175,9 @@ def get_distinct_headers( }, distinct=True, ) - nodeset = self.extend_distinct_headers_query(nodeset) + nodeset = self.extend_distinct_headers_query( + nodeset, field_name=field_name, filter_by=filter_by + ) rs = nodeset.all() diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_repository.py index d8c53b81..49fd1988 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_repository.py @@ -30,9 +30,8 @@ def generic_match_clause( return f"""MATCH (standard_root:{standard_data_model_label} {uid_filter})-[:HAS_INSTANCE]-> (standard_value:{standard_data_model_value_label})<-[has_dataset:HAS_DATASET]- (data_model_ig_value:DataModelIGValue {{version_number: $data_model_ig_version}})<-[:HAS_VERSION]-(data_model_ig_root:DataModelIGRoot {{uid:$data_model_ig_name}}) - OPTIONAL MATCH (standard_value)-[implements:IMPLEMENTS_DATASET_CLASS]->(dataset_class_value) - OPTIONAL MATCH (dataset_class_value)<-[:HAS_INSTANCE]-(dataset_class:DatasetClass) - OPTIONAL MATCH (dataset_class_value)<-[has_dataset_class:HAS_DATASET_CLASS]-(:DataModelValue)<-[:IMPLEMENTS]-(data_model_ig_value) + MATCH (standard_value)-[implements:IMPLEMENTS_DATASET_CLASS]->(dataset_class_value)<-[:HAS_DATASET_CLASS]-(:DataModelValue)<-[:IMPLEMENTS]-(data_model_ig_value) + MATCH (dataset_class_value)<-[:HAS_INSTANCE]-(dataset_class:DatasetClass) """ def create_query_filter_statement(self, **kwargs) -> tuple[str, dict[Any, Any]]: @@ -83,8 +82,7 @@ def specific_alias_clause(self) -> str: WITH *, standard_value.label AS label, standard_value.title AS title, - head([(standard_root)<-[:HAS_DATASET]-(catalogue:DataModelCatalogue) | catalogue.name]) AS catalogue_name, - {ordinal:has_dataset_class.ordinal, dataset_class_name:dataset_class_value.label, dataset_class_uid:dataset_class.uid} AS implemented_dataset_class, + {dataset_class_name:dataset_class_value.label, dataset_class_uid:dataset_class.uid} AS implemented_dataset_class, head([(standard_value)<-[has_dataset:HAS_DATASET]-(data_model_ig_value:DataModelIGValue) | - {ordinal:has_dataset.ordinal, data_model_ig_name:data_model_ig_value.name}]) AS data_model_ig + {ordinal:toInteger(has_dataset.ordinal), data_model_ig_name:data_model_ig_value.name}]) AS data_model_ig """ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_variable_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_variable_repository.py index f857173c..5e8af0c4 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_variable_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/dataset_variable_repository.py @@ -103,7 +103,7 @@ def create_query_filter_statement(self, **kwargs) -> tuple[str, dict[Any, Any]]: return filter_statements_to_return, filter_query_parameters def sort_by(self) -> dict[str, bool] | None: - return {"dataset.ordinal": True} + return {"dataset.root_ordinal": True, "dataset.ordinal": True} def specific_alias_clause(self) -> str: return """ @@ -132,12 +132,12 @@ def specific_alias_clause(self) -> str: apoc.coll.toSet([(standard_value)<-[:HAS_DATASET_VARIABLE]- (:DatasetInstance)<-[:HAS_DATASET]-(data_model_ig_value:DataModelIGValue) | data_model_ig_value.name]) AS data_model_ig_names, - {ordinal:has_dataset_variable_rel.ordinal, uid:dataset_root.uid} AS dataset, + {ordinal:toInteger(last(split(has_dataset_variable_rel.ordinal, "."))), root_ordinal:toInteger(head(split(has_dataset_variable_rel.ordinal, "."))), uid:dataset_root.uid} AS dataset, head([(standard_value)-[:IMPLEMENTS_VARIABLE]->(class_variable_value:VariableClassInstance)<-[:HAS_INSTANCE]-(class_variable_root) | { uid:class_variable_root.uid, name:class_variable_value.label }]) AS implements_variable, - head([(standard_value)-[:HAS_MAPPING_TARGET]->(dataset_variable_value:DatasetVariableInstance) - <-[:HAS_INSTANCE]-(dataset_variable_root:DatasetVariable) | {uid:dataset_variable_root.uid, name:dataset_variable_value.label}]) AS has_mapping_target, - head([(standard_root)<-[:HAS_DATASET_VARIABLE]-(catalogue:DataModelCatalogue) | catalogue.name]) AS catalogue_name, - head([(standard_value)-[:REFERENCES_CODELIST]->(codelist_root:CTCodelistRoot)-[:HAS_NAME_ROOT]-()-[:LATEST]->(codelist_value:CTCodelistNameValue) | { - uid:codelist_root.uid, name:codelist_value.name }]) AS referenced_codelist + head([(standard_value)-[mt_rel:HAS_MAPPING_TARGET]->(dataset_variable_value:DatasetVariableInstance) + <-[:HAS_INSTANCE]-(dataset_variable_root:DatasetVariable) WHERE mt_rel.version_number=$data_model_ig_version + | {uid:dataset_variable_root.uid, name:dataset_variable_value.label}]) AS has_mapping_target, + [(standard_value)-[:REFERENCES_CODELIST]->(codelist_root:CTCodelistRoot)-[:HAS_NAME_ROOT]-()-[:LATEST]->(codelist_value:CTCodelistNameValue) | { + uid:codelist_root.uid, name:codelist_value.name }] AS referenced_codelists """ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/variable_class_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/variable_class_repository.py index 67aee599..e4adb4b7 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/variable_class_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/standard_data_models/variable_class_repository.py @@ -116,8 +116,9 @@ def specific_alias_clause(self) -> str: (:DatasetClassInstance)<-[:HAS_DATASET_CLASS]-(model_value:DataModelValue) | model_value.name]) AS data_model_names, head([(standard_root)<-[:HAS_VARIABLE_CLASS]-(catalogue:DataModelCatalogue) | catalogue.name]) AS catalogue_name, - head([(standard_value)-[:REFERENCES_CODELIST]->(codelist_root:CTCodelistRoot)-[:HAS_NAME_ROOT]-()-[:LATEST]-> - (codelist_value:CTCodelistNameValue) | {uid:codelist_root.uid, name:codelist_value.name }]) AS referenced_codelist, - head([(standard_value)-[:HAS_MAPPING_TARGET]->(class_variable_value:VariableClassInstance) - <-[:HAS_INSTANCE]-(class_variable_root:VariableClass) | {uid:class_variable_root.uid, name:class_variable_value.label}]) AS has_mapping_target + [(standard_value)-[:REFERENCES_CODELIST]->(codelist_root:CTCodelistRoot)-[:HAS_NAME_ROOT]-()-[:LATEST]-> + (codelist_value:CTCodelistNameValue) | {uid:codelist_root.uid, name:codelist_value.name }] AS referenced_codelists, + head([(standard_value)-[mt_rel:HAS_MAPPING_TARGET]->(class_variable_value:VariableClassInstance) + <-[:HAS_INSTANCE]-(class_variable_root:VariableClass) WHERE mt_rel.version_number=$data_model_version + | {uid:class_variable_root.uid, name:class_variable_value.label}]) AS has_mapping_target """ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository.py index bf2803d7..e300a1b3 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository.py @@ -9,8 +9,14 @@ from clinical_mdr_api.domain_repositories.generic_repository import ( RepositoryClosureData, ) +from clinical_mdr_api.domain_repositories.models._utils import ( + format_generic_header_values, +) from clinical_mdr_api.domain_repositories.models.study import StudyRoot, StudyValue -from clinical_mdr_api.domain_repositories.models.study_field import StudyBooleanField +from clinical_mdr_api.domain_repositories.models.study_field import ( + StudyArrayField, + StudyBooleanField, +) from clinical_mdr_api.domains.study_definition_aggregates.root import ( StudyDefinitionAR, StudyDefinitionSnapshot, @@ -23,7 +29,12 @@ StudySoaPreferencesInput, ) from clinical_mdr_api.models.utils import GenericFilteringReturn -from clinical_mdr_api.repositories._utils import FilterOperator +from clinical_mdr_api.repositories._utils import ( + CypherQueryBuilder, + FilterDict, + FilterOperator, + validate_filters_and_add_search_string, +) from common import exceptions from common.telemetry import trace_calls from common.utils import convert_to_datetime @@ -146,6 +157,36 @@ def find_by_uid( # and that's it we are done return result + def study_structure_overview_match_clause(self): + return """ +MATCH (sr:StudyRoot)-[:LATEST]->(sv:StudyValue) +WHERE sv.study_id_prefix IS NOT NULL AND sv.study_number IS NOT NULL +OPTIONAL MATCH (sv)-[:HAS_STUDY_ARM]->(arm:StudyArm) +OPTIONAL MATCH (sv)-[:HAS_STUDY_EPOCH]->(pre_treatment_epoch:StudyEpoch)-[:HAS_EPOCH_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(:CTCodelistTerm {submission_value: "PRE TREATMENT EPOCH TYPE"}) +OPTIONAL MATCH (sv)-[:HAS_STUDY_EPOCH]->(treatment_epoch:StudyEpoch)-[:HAS_EPOCH_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(:CTCodelistTerm {submission_value: "TREATMENT"}) +OPTIONAL MATCH (sv)-[:HAS_STUDY_EPOCH]->(no_treatment_epoch:StudyEpoch)-[:HAS_EPOCH_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(:CTCodelistTerm {submission_value: "NO TREATMENT EPOCH TYPE"}) +OPTIONAL MATCH (sv)-[:HAS_STUDY_EPOCH]->(post_treatment_epoch:StudyEpoch)-[:HAS_EPOCH_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(:CTCodelistTerm {submission_value: "POST TREATMENT EPOCH TYPE"}) +OPTIONAL MATCH (sv)-[:HAS_STUDY_ELEMENT]->(treatment_element:StudyElement)-[:HAS_ELEMENT_SUBTYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)-[:HAS_PARENT_TYPE]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(:CTCodelistTerm {submission_value: "TREATMENT ELEMENT TYPE"}) +OPTIONAL MATCH (sv)-[:HAS_STUDY_ELEMENT]->(no_treatment_element:StudyElement)-[:HAS_ELEMENT_SUBTYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)-[:HAS_PARENT_TYPE]->(:CTTermRoot)<-[:HAS_TERM_ROOT]-(:CTCodelistTerm {submission_value: "NO TREATMENT ELEMENT TYPE"}) +OPTIONAL MATCH (sv)-[:HAS_STUDY_COHORT]->(cohort:StudyCohort) +""" + + def study_structure_overview_alias_clause(self): + return """sv, +COUNT(DISTINCT arm) AS arms, +COUNT(DISTINCT pre_treatment_epoch) AS pre_treatment_epochs, +COUNT(DISTINCT treatment_epoch) AS treatment_epochs, +COUNT(DISTINCT no_treatment_epoch) AS no_treatment_epochs, +COUNT(DISTINCT post_treatment_epoch) AS post_treatment_epochs, +COUNT(DISTINCT treatment_element) AS treatment_elements, +COUNT(DISTINCT no_treatment_element) AS no_treatment_elements, +CASE COUNT(DISTINCT cohort) WHEN > 0 + THEN 'Y' + ELSE 'N' +END AS cohorts_in_study, +sv.study_id_prefix + "-" + sv.study_number AS study_ids +""" + def get_study_structure_overview(self): query = """ MATCH (sr:StudyRoot)-[:LATEST]->(sv:StudyValue) @@ -190,6 +231,35 @@ def get_study_structure_overview(self): return rs + def get_study_structure_overview_headers( + self, + field_name: str, + search_string: str = "", + filter_by: dict[str, dict[str, Any]] | None = None, + filter_operator: FilterOperator = FilterOperator.AND, + page_size: int = 10, + ) -> list[str]: + match_clause = self.study_structure_overview_match_clause() + alias_clause = self.study_structure_overview_alias_clause() + filter_by = validate_filters_and_add_search_string( + search_string, field_name, filter_by + ) + query = CypherQueryBuilder( + filter_by=FilterDict.model_validate({"elements": filter_by}), + filter_operator=filter_operator, + match_clause=match_clause, + alias_clause=alias_clause, + ) + query.full_query = query.build_header_query( + header_alias=field_name, page_size=page_size + ) + result_array, _ = query.execute() + return ( + format_generic_header_values(result_array[0][0]) + if len(result_array) > 0 + else [] + ) + def get_study_structure_statistics(self, uid: str) -> dict[str, int] | None: result = ( StudyValue.nodes.filter(latest_value__uid=uid) @@ -1056,9 +1126,9 @@ def edit_preferred_time_unit( :return: StudyPreferredTimeUnit """ + @staticmethod @abstractmethod def get_soa_preferences( - self, study_uid: str, study_value_version: str | None = None, field_names: Sequence[str] | None = None, @@ -1129,3 +1199,77 @@ def check_if_study_uid_and_version_exists( Returns: bool: True if the study exists, False otherwise. """ + + @staticmethod + @abstractmethod + def get_soa_split_uids( + study_uid: str, + study_value_version: str | None, + ) -> StudyArrayField | None: + """ + Returns StudyArrayField with value as a list of StudyVisit uids for splitting SoA + + Args: + study_uid (str): The unique identifier of the study. + study_value_version (str | None): The version of the Study. Defaults to None for latest version. + + Raises: + NotFoundException: if SoA splitting StudyArrayField does not exist for the given study + Returns: + StudyArrayField | None: StudyArrayField with value as a list of StudyVisit uids for splitting SoA, + or None if splitting is not set. + """ + + @abstractmethod + def add_soa_split_uid( + self, + study_uid: str, + uid: str, + ) -> StudyArrayField: + """ + Adds StudyVisit uid to StudyArrayField for splitting SoA + + Args: + study_uid (str): The unique identifier of the study. + uid (str): The StudyVisit uid to add. + + Raises: + NotFoundException: if StudyVisit uid does not exist for the given study + AlreadyExistsException: if StudyVisit uid is already in the value of SoA splitting StudyArrayField + Returns: + StudyArrayField: updated StudyArrayField with the new StudyVisit uid added + """ + + @abstractmethod + def remove_soa_split_uid( + self, + study_uid: str, + uid: str, + ) -> StudyArrayField | None: + """ + Removes StudyVisit uid from StudyArrayField for splitting SoA + + Args: + study_uid (str): The unique identifier of the study. + uid (str): The uid to remove. + + Raises: + NotFoundException: if StudyVisit uid is not in the value of SoA splitting StudyArrayField + Returns: + StudyArrayField | None: updated StudyArrayField with the StudyVisit uid removed, or None if no uids remain. + """ + + @abstractmethod + def remove_soa_splits( + self, + study_uid: str, + ) -> None: + """ + Removes StudyArrayField for splitting SoA + + Args: + study_uid (str): The unique identifier of the study. + + Returns: + None + """ diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository_impl.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository_impl.py index dfa262ae..085eb666 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository_impl.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_definitions/study_definition_repository_impl.py @@ -7,7 +7,7 @@ from neomodel import NodeSet from neomodel.exceptions import DoesNotExist from neomodel.sync_.core import NodeMeta, db -from neomodel.sync_.match import Collect, Last +from neomodel.sync_.match import Collect, Last, Path from clinical_mdr_api import utils from clinical_mdr_api.domain_repositories._utils.helpers import ( @@ -44,6 +44,7 @@ StudyTextField, StudyTimeField, ) +from clinical_mdr_api.domain_repositories.models.study_visit import StudyVisit from clinical_mdr_api.domain_repositories.study_definitions.study_definition_repository import ( StudyDefinitionRepository, ) @@ -660,6 +661,9 @@ def _save( self._maintain_study_soa_preferences_relationship_on_save( expected_latest_value=expected_latest_value, previous_value=previous_value ) + self._maintain_study_soa_split_relationship_on_save( + expected_latest_value=expected_latest_value, previous_value=previous_value + ) def _maintain_study_relationship_on_save( self, @@ -723,6 +727,19 @@ def _maintain_study_soa_preferences_relationship_on_save( # add the relation to the new node expected_latest_value.has_boolean_field.connect(node) + def _maintain_study_soa_split_relationship_on_save( + self, expected_latest_value: StudyValue, previous_value: StudyValue + ): + # if new value node is created + if expected_latest_value is not previous_value: + nodes = previous_value.has_array_field.filter( + field_name=settings.study_soa_split_uids_field + ) + + for node in nodes: + # add the relation to the new node + expected_latest_value.has_array_field.connect(node) + def _maintain_latest_value_and_relationship_on_save( self, current_snapshot: StudyDefinitionSnapshot, @@ -997,18 +1014,20 @@ def _maintain_study_project_field_relationship( project_number=curr_metadata.project_number ) - # assigning Project to newly created StudyValue node - study_project_field = StudyProjectField() - study_project_field.save() - study_project_field.has_field.connect(project_node) - expected_latest_value.has_project.connect(study_project_field) - + # disconnecting Project from previous StudyValue node prev_study_project_field = previous_value.has_project.get_or_none() if ( prev_study_project_field is not None and previous_value is expected_latest_value ): expected_latest_value.has_project.disconnect(prev_study_project_field) + + # assigning Project to newly created StudyValue node + study_project_field = StudyProjectField() + study_project_field.save() + study_project_field.has_field.connect(project_node) + expected_latest_value.has_project.connect(study_project_field) + self._generate_study_field_audit_node( study_root_node=study_root, study_field_node_after=study_project_field, @@ -3064,8 +3083,8 @@ def _retrieve_study_subpart_with_history( return calculate_diffs(result, StudySubpartAuditTrail) + @staticmethod def get_soa_preferences( - self, study_uid: str, study_value_version: str | None = None, field_names: Sequence[str] | None = None, @@ -3162,3 +3181,212 @@ def edit_soa_preferences( ) return self.get_soa_preferences(study_uid=study_uid) + + @staticmethod + def get_soa_split_uids( + study_uid: str, + study_value_version: str | None = None, + _field_name: str = settings.study_soa_split_uids_field, + ) -> StudyArrayField | None: + """Gets a StudyArrayField node as uids for SoA splitting""" + + if study_value_version: + filters = { + "has_array_field__has_version__uid": study_uid, + "has_array_field__has_version|version": study_value_version, + "has_array_field__has_version|status": StudyStatus.RELEASED.value, + } + else: + filters = { + "has_array_field__latest_value__uid": study_uid, + } + + filters["field_name"] = _field_name + + try: + return ( + StudyArrayField.nodes.fetch_relations("has_after__audit_trail") + .filter(**filters) + .get()[0] + ) + except DoesNotExist: + return None + + def add_soa_split_uid( + self, + study_uid: str, + uid: str, + _field_name: str = settings.study_soa_split_uids_field, + ) -> StudyArrayField: + """Adds a UID to the StudyArrayField node for SoA splitting""" + + study_root: StudyRoot + latest_study_value: StudyValue + + # Lock study in db + acquire_write_lock_study_value(study_uid) + + # Fetch previous StudyArrayField + try: + previous_study_array_field = self.get_soa_split_uids( + study_uid=study_uid, _field_name=_field_name + ) + except exceptions.NotFoundException: + previous_study_array_field = None + + # Check if uid is already in the array + exceptions.AlreadyExistsException.raise_if( + previous_study_array_field and uid in previous_study_array_field.value, + msg=f"StudyVisit '{uid}' is already present in SoA split UIDs for Study '{study_uid}'.", + ) + + # Get all StudyVisits ordered: we need to know which is the first member of a StudyVisitGroup + all_visits_q = ( + StudyVisit.nodes.traverse( + Path("in_visit_group", optional=True, include_rels_in_return=False) + ) + .filter(has_study_visit__latest_value__uid=study_uid) + .order_by("visit_number") + ) + + # Determine eligibility + seen_uids = set() + eligible_uids = set() + _seen_group_uids = set() + + for ( + study_visit, + study_visit_group, + latest_study_value, + _, + study_root, + _, + ) in all_visits_q.all(): + seen_uids.add(study_visit.uid) + if study_visit_group: + if study_visit_group.uid in _seen_group_uids: + continue + _seen_group_uids.add(study_visit_group.uid) + eligible_uids.add(study_visit.uid) + + # Validate StudyVisit uid + exceptions.NotFoundException.raise_if_not(uid in seen_uids, "StudyVisit", uid) + + # Validate eligibility of StudyVisit uid + exceptions.BusinessLogicException.raise_if_not( + uid in eligible_uids, + msg=f"StudyVisit '{uid}' is not eligible to split SoA of Study '{study_uid}'.", + ) + + # Disconnect previous StudyArrayField + if previous_study_array_field: + latest_study_value.has_array_field.disconnect(previous_study_array_field) + + # Create new StudyArrayField with the uid added + uids = ( + set(previous_study_array_field.value) + if previous_study_array_field + else set() + ) + uids |= {uid} + new_study_array_field = StudyArrayField.create( + {"field_name": _field_name, "value": list(uids)} + )[0] + latest_study_value.has_array_field.connect(new_study_array_field) + + # Extend audit trail + self._generate_study_field_audit_node( + study_root_node=study_root, + study_field_node_after=new_study_array_field, + study_field_node_before=previous_study_array_field, + change_status=None, + author_id=self.audit_info.author_id, + date=datetime.now(timezone.utc), + ) + + return new_study_array_field + + def remove_soa_split_uid( + self, + study_uid: str, + uid: str, + _field_name: str = settings.study_soa_split_uids_field, + ) -> StudyArrayField | None: + """Removes a UID from the StudyArrayField node for SoA splitting""" + + study_root: StudyRoot + latest_study_value: StudyValue + + # Lock study in db + acquire_write_lock_study_value(study_uid) + + # Fetch previous StudyArrayField + previous_study_array_field = self.get_soa_split_uids( + study_uid=study_uid, _field_name=_field_name + ) + + # Check if uid is in the array + exceptions.NotFoundException.raise_if_not( + previous_study_array_field is not None + and uid in previous_study_array_field.value, + msg=f"StudyVisit '{uid}' is not in SoA split UIDs for Study '{study_uid}'.", + ) + + # Disconnect previous StudyArrayField + study_root, latest_study_value, _ = StudyRoot.nodes.traverse( + "latest_value" + ).get(uid=study_uid) + latest_study_value.has_array_field.disconnect(previous_study_array_field) + + # Create new StudyArrayField if uids remain after removal + if new_uids := list(set(previous_study_array_field.value) - {uid}): + new_study_array_field = StudyArrayField.create( + {"field_name": _field_name, "value": new_uids} + )[0] + latest_study_value.has_array_field.connect(new_study_array_field) + else: + new_study_array_field = None + + # Extend audit trail + self._generate_study_field_audit_node( + study_root_node=study_root, + study_field_node_after=new_study_array_field, + study_field_node_before=previous_study_array_field, + change_status=None, + author_id=self.audit_info.author_id, + date=datetime.now(timezone.utc), + ) + + return new_study_array_field + + def remove_soa_splits( + self, + study_uid: str, + _field_name: str = settings.study_soa_split_uids_field, + ) -> None: + """Removes the StudyArrayField nodes for SoA splitting""" + + # Query StudyArrayFields + filters = { + "has_array_field__latest_value__uid": study_uid, + "field_name": _field_name, + } + + node: StudyArrayField + for node, _, _, study_root, _, study_value, *_ in ( + StudyArrayField.nodes.fetch_relations("has_after__audit_trail") + .filter(**filters) + .all() + ): + # Disconnect StudyArrayFields + study_value.has_array_field.disconnect(node) + + # Extend audit trail + self._generate_study_field_audit_node( + study_root_node=study_root, + study_field_node_before=node, + study_field_node_after=None, + change_status=None, + author_id=self.audit_info.author_id, + date=datetime.now(timezone.utc), + ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_group_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_group_repository.py index 85346643..5b7985f2 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_group_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_group_repository.py @@ -80,13 +80,13 @@ def _additional_match(self, **kwargs) -> str: MATCH (sv)-[:HAS_STUDY_ACTIVITY]->(sa:StudyActivity) -[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(sag:StudyActivityGroup) - WHERE NOT (sag)<-[:BEFORE]-() + <-[:HAS_STUDY_ACTIVITY_GROUP]-(sv) OPTIONAL MATCH (sa)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(soag:StudySoAGroup) - WHERE NOT (soag)<-[:BEFORE]-() + <-[:HAS_STUDY_SOA_GROUP]-(sv) OPTIONAL MATCH (sa)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(sasg:StudyActivitySubGroup) - WHERE NOT (sasg)<-[:BEFORE]-() + <-[:HAS_STUDY_ACTIVITY_SUBGROUP]-(sv) WITH DISTINCT sr, sag, soag, collect(DISTINCT sasg.uid) AS study_activity_subgroup_uids diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instance_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instance_repository.py index 381af989..d50256ec 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instance_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_instance_repository.py @@ -2,6 +2,9 @@ from dataclasses import dataclass from typing import Any +from clinical_mdr_api.domain_repositories.controlled_terminologies.ct_codelist_attributes_repository import ( + CTCodelistAttributesRepository, +) from clinical_mdr_api.domain_repositories.generic_repository import ( manage_previous_connected_study_selection_relationships, ) @@ -10,11 +13,16 @@ ActivityInstanceRoot, ActivityInstanceValue, ) +from clinical_mdr_api.domain_repositories.models.controlled_terminology import ( + CTTermContext, + CTTermRoot, +) from clinical_mdr_api.domain_repositories.models.study import StudyValue from clinical_mdr_api.domain_repositories.models.study_audit_trail import StudyAction from clinical_mdr_api.domain_repositories.models.study_selections import ( StudyActivity, StudyActivityInstance, + StudyDataSupplier, StudySelection, ) from clinical_mdr_api.domain_repositories.models.study_visit import StudyVisit @@ -30,6 +38,7 @@ StudySelectionActivityInstanceVO, ) from clinical_mdr_api.models.study_selections.study_visit import SimpleStudyVisit +from common.config import settings from common.exceptions import BusinessLogicException, NotFoundException from common.utils import convert_to_datetime @@ -76,6 +85,15 @@ class SelectionHistory: change_type: str start_date: datetime.datetime end_date: datetime.datetime | None + # Data supplier and origin fields (L3 SoA) + study_data_supplier_uid: str | None = None + study_data_supplier_name: str | None = None + origin_type_uid: str | None = None + origin_type_name: str | None = None + origin_type_codelist_uid: str | None = None + origin_source_uid: str | None = None + origin_source_name: str | None = None + origin_source_codelist_uid: str | None = None class StudySelectionActivityInstanceRepository( @@ -222,6 +240,14 @@ def _create_value_object_from_repository( study_soa_group_uid=study_soa_group.get("selection_uid"), soa_group_term_uid=study_soa_group.get("soa_group_uid"), soa_group_term_name=study_soa_group.get("soa_group_name"), + study_data_supplier_uid=selection.get("study_data_supplier_uid"), + study_data_supplier_name=selection.get("study_data_supplier_name"), + origin_type_uid=selection.get("origin_type_uid"), + origin_type_name=selection.get("origin_type_name"), + origin_type_codelist_uid=selection.get("origin_type_codelist_uid"), + origin_source_uid=selection.get("origin_source_uid"), + origin_source_name=selection.get("origin_source_name"), + origin_source_codelist_uid=selection.get("origin_source_codelist_uid"), ) def _order_by_query(self): @@ -335,6 +361,12 @@ def _additional_match(self, **kwargs) -> str: RETURN baseline_visit } WITH sr, sv, sa, study_activity, activity, activity_instance, latest_activity_instance, collect(baseline_visit) as baseline_visits + // Data supplier and origin fields (L3 SoA) + OPTIONAL MATCH (sa)-[:HAS_STUDY_DATA_SUPPLIER]->(sds:StudyDataSupplier)-[:HAS_DATA_SUPPLIER]->(dsv:DataSupplierValue) + OPTIONAL MATCH (sa)-[:HAS_ORIGIN_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(origin_type_root:CTTermRoot)-[:HAS_NAME_ROOT]->(:CTTermNameRoot)-[:LATEST]->(origin_type_name_value:CTTermNameValue) + OPTIONAL MATCH (sa)-[:HAS_ORIGIN_TYPE]->(origin_type_ctx:CTTermContext)-[:HAS_SELECTED_CODELIST]->(origin_type_codelist:CTCodelistRoot) + OPTIONAL MATCH (sa)-[:HAS_ORIGIN_SOURCE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(origin_source_root:CTTermRoot)-[:HAS_NAME_ROOT]->(:CTTermNameRoot)-[:LATEST]->(origin_source_name_value:CTTermNameValue) + OPTIONAL MATCH (sa)-[:HAS_ORIGIN_SOURCE]->(origin_source_ctx:CTTermContext)-[:HAS_SELECTED_CODELIST]->(origin_source_codelist:CTCodelistRoot) """ return base_query @@ -345,12 +377,14 @@ def _filter_clause(self, query_parameters: dict[Any, Any], **kwargs) -> str: activity_group_names = kwargs.get("activity_group_names") activity_subgroup_names = kwargs.get("activity_subgroup_names") activity_instance_names = kwargs.get("activity_instance_names") + has_activity_instance = kwargs.get("has_activity_instance") filter_query = "" if ( activity_names is not None or activity_group_names is not None or activity_subgroup_names is not None or activity_instance_names is not None + or has_activity_instance is True ): filter_query += " WHERE " filter_list = [] @@ -379,6 +413,11 @@ def _filter_clause(self, query_parameters: dict[Any, Any], **kwargs) -> str: "WHERE activity_instance_value.name IN $activity_instance_names | activity_instance_value.name]) > 0" ) query_parameters["activity_instance_names"] = activity_instance_names + if has_activity_instance is True: + # Use WITH * WHERE to filter after OPTIONAL MATCH, not pattern WHERE + return " WITH * WHERE " + " AND ".join( + filter_list + ["activity_instance.uid IS NOT NULL"] + ) filter_query += " AND ".join(filter_list) return filter_query @@ -396,7 +435,8 @@ def _return_clause(self) -> str: activity_instance, latest_activity_instance, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(study_activity_subgroup_selection) - -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | + -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) + WHERE (study_activity_subgroup_selection)<-[:HAS_STUDY_ACTIVITY_SUBGROUP]-(sv) | { selection_uid: study_activity_subgroup_selection.uid, activity_subgroup_uid:activity_subgroup_root.uid, @@ -404,7 +444,8 @@ def _return_clause(self) -> str: order: study_activity_subgroup_selection.order }]) AS study_activity_subgroup, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(study_activity_group_selection) - -[:HAS_SELECTED_ACTIVITY_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | + -[:HAS_SELECTED_ACTIVITY_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) + WHERE (study_activity_group_selection)<-[:HAS_STUDY_ACTIVITY_GROUP]-(sv) | { selection_uid: study_activity_group_selection.uid, activity_group_uid: activity_group_root.uid, @@ -412,7 +453,8 @@ def _return_clause(self) -> str: order: study_activity_group_selection.order }]) AS study_activity_group, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(study_soa_group_selection) - -[:HAS_FLOWCHART_GROUP]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(ct_term_root:CTTermRoot)-[:HAS_NAME_ROOT]-(:CTTermNameRoot)-[:LATEST]->(flowchart_value:CTTermNameValue) | + -[:HAS_FLOWCHART_GROUP]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(ct_term_root:CTTermRoot)-[:HAS_NAME_ROOT]-(:CTTermNameRoot)-[:LATEST]->(flowchart_value:CTTermNameValue) + WHERE (study_soa_group_selection)<-[:HAS_STUDY_SOA_GROUP]-(sv) | { selection_uid: study_soa_group_selection.uid, soa_group_uid: ct_term_root.uid, @@ -423,7 +465,15 @@ def _return_clause(self) -> str: sac.date AS start_date, sac.author_id AS author_id, COALESCE(head([(user:User)-[*0]-() WHERE user.user_id=sac.author_id | user.username]), sac.author_id) AS author_username, - study_activity.order AS study_activity_order + study_activity.order AS study_activity_order, + sds.uid AS study_data_supplier_uid, + dsv.name AS study_data_supplier_name, + origin_type_root.uid AS origin_type_uid, + origin_type_name_value.name AS origin_type_name, + origin_type_codelist.uid AS origin_type_codelist_uid, + origin_source_root.uid AS origin_source_uid, + origin_source_name_value.name AS origin_source_name, + origin_source_codelist.uid AS origin_source_codelist_uid ORDER BY study_soa_group.order, study_activity_group.order, study_activity_subgroup.order, study_activity_order, activity_instance.name """ @@ -507,6 +557,14 @@ def get_selection_history( else None ), end_date=end_date, + study_data_supplier_uid=selection.get("study_data_supplier_uid"), + study_data_supplier_name=selection.get("study_data_supplier_name"), + origin_type_uid=selection.get("origin_type_uid"), + origin_type_name=selection.get("origin_type_name"), + origin_type_codelist_uid=selection.get("origin_type_codelist_uid"), + origin_source_uid=selection.get("origin_source_uid"), + origin_source_name=selection.get("origin_source_name"), + origin_source_codelist_uid=selection.get("origin_source_codelist_uid"), ) def get_audit_trail_query(self, study_selection_uid: str | None): @@ -591,7 +649,15 @@ def get_audit_trail_query(self, study_selection_uid: str | None): MATCH (all_sa)<-[:AFTER]-(asa:StudyAction) OPTIONAL MATCH (all_sa)<-[:BEFORE]-(bsa:StudyAction) WITH all_sa, study_activity, activity, activity_instance, asa, bsa, collect(baseline_visit) as baseline_visits - + // Data supplier and origin fields (L3 SoA) + OPTIONAL MATCH (all_sa)-[:HAS_STUDY_DATA_SUPPLIER]->(sds:StudyDataSupplier)-[:HAS_DATA_SUPPLIER]->(dsv:DataSupplierValue) + OPTIONAL MATCH (all_sa)-[:HAS_ORIGIN_TYPE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(origin_type_root:CTTermRoot)-[:HAS_NAME_ROOT]->(:CTTermNameRoot)-[:LATEST]->(origin_type_name_value:CTTermNameValue) + OPTIONAL MATCH (all_sa)-[:HAS_ORIGIN_TYPE]->(origin_type_ctx:CTTermContext)-[:HAS_SELECTED_CODELIST]->(origin_type_codelist:CTCodelistRoot) + OPTIONAL MATCH (all_sa)-[:HAS_ORIGIN_SOURCE]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(origin_source_root:CTTermRoot)-[:HAS_NAME_ROOT]->(:CTTermNameRoot)-[:LATEST]->(origin_source_name_value:CTTermNameValue) + OPTIONAL MATCH (all_sa)-[:HAS_ORIGIN_SOURCE]->(origin_source_ctx:CTTermContext)-[:HAS_SELECTED_CODELIST]->(origin_source_codelist:CTCodelistRoot) + WITH all_sa, study_activity, activity, activity_instance, asa, bsa, baseline_visits, + sds, dsv, origin_type_root, origin_type_name_value, origin_type_codelist, + origin_source_root, origin_source_name_value, origin_source_codelist ORDER BY all_sa.uid, asa.date DESC RETURN all_sa.uid AS study_selection_uid, @@ -599,23 +665,23 @@ def get_audit_trail_query(self, study_selection_uid: str | None): activity, activity_instance, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(study_activity_subgroup_selection) - -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | + -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | { - selection_uid: study_activity_subgroup_selection.uid, + selection_uid: study_activity_subgroup_selection.uid, activity_subgroup_uid:activity_subgroup_root.uid, activity_subgroup_name: activity_subgroup_value.name }]) AS study_activity_subgroup, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(study_activity_group_selection) - -[:HAS_SELECTED_ACTIVITY_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | + -[:HAS_SELECTED_ACTIVITY_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | { - selection_uid: study_activity_group_selection.uid, + selection_uid: study_activity_group_selection.uid, activity_group_uid: activity_group_root.uid, activity_group_name: activity_group_value.name }]) AS study_activity_group, head([(study_activity)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(study_soa_group_selection) - -[:HAS_FLOWCHART_GROUP]->(ct_term_root:CTTermRoot)-[:HAS_NAME_ROOT]-(:CTTermNameRoot)-[:LATEST]->(flowchart_value:CTTermNameValue) | + -[:HAS_FLOWCHART_GROUP]->(ct_term_root:CTTermRoot)-[:HAS_NAME_ROOT]-(:CTTermNameRoot)-[:LATEST]->(flowchart_value:CTTermNameValue) | { - selection_uid: study_soa_group_selection.uid, + selection_uid: study_soa_group_selection.uid, soa_group_uid: ct_term_root.uid, soa_group_name: flowchart_value.name }]) AS study_soa_group, @@ -625,7 +691,15 @@ def get_audit_trail_query(self, study_selection_uid: str | None): asa.author_id AS author_id, labels(asa) AS change_type, bsa.date AS end_date, - baseline_visits + baseline_visits, + sds.uid AS study_data_supplier_uid, + dsv.name AS study_data_supplier_name, + origin_type_root.uid AS origin_type_uid, + origin_type_name_value.name AS origin_type_name, + origin_type_codelist.uid AS origin_type_codelist_uid, + origin_source_root.uid AS origin_source_uid, + origin_source_name_value.name AS origin_source_name, + origin_source_codelist.uid AS origin_source_codelist_uid """ return audit_trail_cypher @@ -645,6 +719,34 @@ def _add_new_selection( last_study_selection_node: StudyActivityInstance, for_deletion: bool = False, ): + # Validate that reviewed instances cannot have is_important or baseline visits changed + if last_study_selection_node and last_study_selection_node.is_reviewed: + # Check if is_important is being changed + is_important_changed = ( + last_study_selection_node.is_important != selection.is_important + ) + + # Check if baseline visits are being changed + previous_baseline_uids = { + visit.uid for visit in last_study_selection_node.has_baseline.all() + } + new_baseline_uids = { + visit["uid"] + for visit in (selection.study_activity_instance_baseline_visits or []) + } + baseline_visits_changed = previous_baseline_uids != new_baseline_uids + + # Raise exception if either field is being changed on a reviewed instance + BusinessLogicException.raise_if( + is_important_changed, + msg=f"Cannot modify 'is_important' property on a reviewed StudyActivityInstance with UID '{selection.study_selection_uid}'.", + ) + + BusinessLogicException.raise_if( + baseline_visits_changed, + msg=f"Cannot modify baseline visits on a reviewed StudyActivityInstance with UID '{selection.study_selection_uid}'.", + ) + # Create new activity selection study_activity_instance_selection_node = StudyActivityInstance( uid=selection.study_selection_uid, @@ -724,12 +826,60 @@ def _add_new_selection( baseline_visit_node ) + # Connect StudyDataSupplier if provided + if selection.study_data_supplier_uid: + study_data_supplier_node = StudyDataSupplier.nodes.has( + has_before=False + ).get_or_none(uid=selection.study_data_supplier_uid) + if study_data_supplier_node: + study_activity_instance_selection_node.has_study_data_supplier.connect( + study_data_supplier_node + ) + + # Connect Origin Type CT term if provided + if selection.origin_type_uid: + origin_type_root = CTTermRoot.nodes.get_or_none( + uid=selection.origin_type_uid + ) + if origin_type_root: + selected_term_node = ( + CTCodelistAttributesRepository().get_or_create_selected_term( + origin_type_root, + codelist_submission_value=settings.origin_type_cl_submval, + ) + ) + study_activity_instance_selection_node.has_origin_type.connect( + selected_term_node + ) + + # Connect Origin Source CT term if provided + if selection.origin_source_uid: + origin_source_root = CTTermRoot.nodes.get_or_none( + uid=selection.origin_source_uid + ) + if origin_source_root: + selected_term_node = ( + CTCodelistAttributesRepository().get_or_create_selected_term( + origin_source_root, + codelist_submission_value=settings.origin_source_cl_submval, + ) + ) + study_activity_instance_selection_node.has_origin_source.connect( + selected_term_node + ) + if last_study_selection_node: manage_previous_connected_study_selection_relationships( previous_item=last_study_selection_node, study_value_node=latest_study_value_node, new_item=study_activity_instance_selection_node, - exclude_study_selection_relationships=[StudyActivity, StudyVisit], + # StudyDataSupplier and CTTermContext are excluded because they're handled explicitly above + exclude_study_selection_relationships=[ + StudyActivity, + StudyVisit, + StudyDataSupplier, + CTTermContext, + ], ) def generate_uid(self) -> str: diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_repository.py index d67fad69..7790580f 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_repository.py @@ -133,8 +133,9 @@ def _additional_match(self, **kwargs) -> str: WITH DISTINCT * CALL { - WITH sa, terms_at_specific_datetime + WITH sv, sa, terms_at_specific_datetime MATCH (sa)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(soa_group:StudySoAGroup)-[:HAS_FLOWCHART_GROUP]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(soa_group_term_root:CTTermRoot) + WHERE (soa_group)<-[:HAS_STUDY_SOA_GROUP]-(sv) MATCH (soa_group)<-[:AFTER]-(after_action:StudyAction) WITH * ORDER BY after_action.date DESC @@ -223,7 +224,8 @@ def _return_clause(self) -> str: order: soa_group.order } AS study_soa_group, head(apoc.coll.sortMulti([(sa)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(study_activity_subgroup_selection:StudyActivitySubGroup) - -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) | + -[:HAS_SELECTED_ACTIVITY_SUBGROUP]->(activity_subgroup_value:ActivitySubGroupValue)<-[:HAS_VERSION]-(activity_subgroup_root:ActivitySubGroupRoot) + WHERE (study_activity_subgroup_selection)<-[:HAS_STUDY_ACTIVITY_SUBGROUP]-(sv) | { selection_uid: study_activity_subgroup_selection.uid, activity_subgroup_uid:activity_subgroup_root.uid, @@ -233,7 +235,8 @@ def _return_clause(self) -> str: date: head([(study_activity_subgroup_selection)<-[:AFTER]-(after_action:StudyAction) | after_action.date]) }], ['date'])) AS study_activity_subgroup, head(apoc.coll.sortMulti([(sa)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(study_activity_group_selection:StudyActivityGroup) - -[:HAS_SELECTED_ACTIVITY_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) | + -[:HAS_SELECTED_ACTIVITY_GROUP]->(activity_group_value:ActivityGroupValue)<-[:HAS_VERSION]-(activity_group_root:ActivityGroupRoot) + WHERE (study_activity_group_selection)<-[:HAS_STUDY_ACTIVITY_GROUP]-(sv) | { selection_uid: study_activity_group_selection.uid, activity_group_uid: activity_group_root.uid, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_subgroup_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_subgroup_repository.py index b0862ca0..75ef112b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_subgroup_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_activity_subgroup_repository.py @@ -77,13 +77,13 @@ def _additional_match(self, **kwargs) -> str: MATCH (sv)-[:HAS_STUDY_ACTIVITY]->(sa:StudyActivity) -[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_SUBGROUP]->(sasg:StudyActivitySubGroup) - WHERE NOT (sasg)<-[:BEFORE]-() + <-[:HAS_STUDY_ACTIVITY_SUBGROUP]-(sv) OPTIONAL MATCH (sa)-[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(soag:StudySoAGroup) - WHERE NOT (soag)<-[:BEFORE]-() + <-[:HAS_STUDY_SOA_GROUP]-(sv) OPTIONAL MATCH (sa)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(sag:StudyActivityGroup) - WHERE NOT (sag)<-[:BEFORE]-() + <-[:HAS_STUDY_ACTIVITY_GROUP]-(sv) WITH DISTINCT sr, sasg, soag, sag, collect(DISTINCT sa.uid) AS study_activity_uids diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_arm_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_arm_repository.py index 5f082ce5..18ae30c8 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_arm_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_arm_repository.py @@ -32,6 +32,7 @@ ) from common.config import settings from common.exceptions import BusinessLogicException +from common.telemetry import trace_calls from common.utils import convert_to_datetime, get_db_result_as_dict @@ -305,6 +306,9 @@ def find_all( ) return selection_aggregates + @trace_calls( + args=[1, 2, 3], kwargs=["study_uid", "for_update", "study_value_version"] + ) def find_by_study( self, study_uid: str, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_cell_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_cell_repository.py index 75789070..138fa902 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_cell_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_design_cell_repository.py @@ -2,14 +2,13 @@ from dataclasses import dataclass from textwrap import dedent -from neomodel import db -from neomodel.sync_.match import Collect, Last, Optional +from neomodel import DoesNotExist, MultipleNodesReturned, db +from neomodel.sync_.match import Collect, Last, Path from clinical_mdr_api import utils from clinical_mdr_api.domain_repositories.generic_repository import ( - manage_previous_connected_study_selection_relationships, + _manage_versioning_with_relations, ) -from clinical_mdr_api.domain_repositories.models._utils import ListDistinct from clinical_mdr_api.domain_repositories.models.generic import ClinicalMdrNodeWithUID from clinical_mdr_api.domain_repositories.models.study import ( StudyArm, @@ -47,9 +46,13 @@ class StudyDesignCellHistory: study_selection_uid: str study_uid: str study_arm_uid: str + study_arm_name: str study_branch_arm_uid: str + study_branch_arm_name: str study_epoch_uid: str + study_epoch_name: str study_element_uid: str + study_element_name: str author_id: str change_type: str start_date: datetime.datetime @@ -60,50 +63,76 @@ class StudyDesignCellHistory: class StudyDesignCellRepository: - def find_by_uid(self, study_uid: str, uid: str) -> StudyDesignCellVO: - unique_design_cells = ListDistinct( - StudyDesignCell.nodes.fetch_relations( - "study_epoch__has_epoch__has_selected_term__has_name_root__has_latest_value", - "has_after__audit_trail", - "study_epoch", - Optional("study_arm__study_value"), - Optional("study_branch_arm__study_value"), - Optional("study_element__study_value"), - ) - .filter( - study_value__latest_value__uid=study_uid, - study_epoch__study_value__latest_value__uid=study_uid, - uid=uid, - ) - .order_by("order") - .resolve_subgraph() - ).distinct() + @classmethod + @trace_calls(args=[1, 2], kwargs=["study_uid", "uid"]) + def find_by_uid(cls, study_uid: str, uid: str) -> StudyDesignCellVO: + found = cls.find_all_design_cells_by_study( + study_uid=study_uid, study_design_cell_uid=uid + ) exceptions.ValidationException.raise_if( - len(unique_design_cells) > 1, + len(found) > 1, msg=f"Found more than one StudyDesignCell node with UID '{uid}' in the Study with UID '{study_uid}'.", ) exceptions.ValidationException.raise_if( - len(unique_design_cells) == 0, + len(found) == 0, msg=f"The StudyDesignCell with UID '{uid}' could not be found in the Study with UID '{study_uid}'.", ) - return self._from_repository_values( - study_uid=study_uid, design_cell=unique_design_cells[0] - ) + return found[0] - @trace_calls + @staticmethod + @trace_calls( + args=[0, 1], + kwargs=[ + "study_uid", + "study_value_version", + "study_design_cell_uid", + "study_arm_uid", + "study_branch_arm_uid", + "study_element_uid", + "study_epoch_uid", + ], + ) def find_all_design_cells_by_study( - self, study_uid: str, study_value_version: str | None = None, + *, # prevent call errors when replacing get_design_cells_* functions + study_design_cell_uid: str | None = None, + study_arm_uid: str | None = None, + study_branch_arm_uid: str | None = None, + study_element_uid: str | None = None, + study_epoch_uid: str | None = None, ) -> list[StudyDesignCellVO]: + """Returns all StudyDesignCellVO as list for a given Study & version, with optional filters, sorted by order.""" + + # query parameters params = { "study_uid": study_uid, "study_version": study_value_version, + "study_design_cell_uid": study_design_cell_uid, + "study_arm_uid": study_arm_uid, + "study_branch_arm_uid": study_branch_arm_uid, + "study_element_uid": study_element_uid, + "study_epoch_uid": study_epoch_uid, "study_status": StudyStatus.RELEASED.value, } + # filter expressions + filter_map = { + "study_design_cell_uid": "sdc.uid", + "study_arm_uid": "sarm.uid", + "study_branch_arm_uid": "sbarm.uid", + "study_element_uid": "sel.uid", + "study_epoch_uid": "sep.uid", + } + filters = [ + f"{value} = ${key}" + for key, value in filter_map.items() + if params[key] is not None + ] + + # build query if study_value_version: query = [ "MATCH (sr:StudyRoot {uid: $study_uid})-[:HAS_VERSION {status: $study_status, version: $study_version}]->(sv:StudyValue)" @@ -122,6 +151,18 @@ def find_all_design_cells_by_study( MATCH (sdc)<-[:STUDY_ELEMENT_HAS_DESIGN_CELL]-(sel:StudyElement)<-[:HAS_STUDY_ELEMENT]-(sv) OPTIONAL MATCH (sdc)<-[:STUDY_ARM_HAS_DESIGN_CELL]-(sarm:StudyArm)<-[:HAS_STUDY_ARM]-(sv) OPTIONAL MATCH (sdc)<-[:STUDY_BRANCH_ARM_HAS_DESIGN_CELL]-(sbarm:StudyBranchArm)<-[:HAS_STUDY_BRANCH_ARM]-(sv) + OPTIONAL MATCH (user:User {user_id: study_action.author_id}) + """ + ).strip() + ) + + if filters: + query.append("WITH *") + query.append("WHERE " + " AND ".join(filters)) + + query.append( + dedent( + """ RETURN DISTINCT sdc { uid: sdc.uid, study_uid: sr.uid, @@ -135,22 +176,23 @@ def find_all_design_cells_by_study( study_element_uid: sel.uid, study_element_name: sel.name, transition_rule: sdc.transition_rule, - start_date: toString(study_action.date), + start_date: study_action.date, author_id: study_action.author_id, - author_username: coalesce(head([(user:User)-[*0]-() WHERE user.user_id=study_action.author_id | user.username]), study_action.author_id) + author_username: user.username } AS vo ORDER BY vo.order """ - ) + ).strip() ) results, _ = db.cypher_query("\n".join(query), params=params) return [StudyDesignCellVO(**result[0]) for result in results] + @classmethod @trace_calls def _from_repository_values( - self, + cls, study_uid: str, design_cell: StudyDesignCell, study_value_version: str | None = None, @@ -178,18 +220,18 @@ def _from_repository_values( assert len(set(ith.uid for ith in design_cell.study_element.all())) <= 1 assert len(set(ith.uid for ith in design_cell.study_epoch.all())) <= 1 - study_epoch: StudyEpoch = self.get_current_outbound_node( + study_epoch: StudyEpoch = cls.get_current_outbound_node( node=design_cell, outbound_rel_name="study_epoch", study_value=study_value ) - study_arm: StudyArm = self.get_current_outbound_node( + study_arm: StudyArm = cls.get_current_outbound_node( node=design_cell, outbound_rel_name="study_arm", study_value=study_value ) - study_branch_arm: StudyBranchArm = self.get_current_outbound_node( + study_branch_arm: StudyBranchArm = cls.get_current_outbound_node( node=design_cell, outbound_rel_name="study_branch_arm", study_value=study_value, ) - study_element: StudyElement = self.get_current_outbound_node( + study_element: StudyElement = cls.get_current_outbound_node( node=design_cell, outbound_rel_name="study_element", study_value=study_value ) @@ -221,8 +263,8 @@ def _from_repository_values( author_id=study_action.author_id, ) + @staticmethod def get_current_outbound_node( - self, node: ClinicalMdrNodeWithUID, outbound_rel_name: str, study_value: StudyValue, @@ -235,146 +277,148 @@ def get_current_outbound_node( return outbound_node # pylint: disable=unused-argument + @classmethod @trace_calls def save( - self, + cls, design_cell_vo: StudyDesignCellVO, author_id: str, create: bool = False, allow_none_arm_branch_arm=False, + previous_vo: StudyDesignCellVO | None = None, ) -> StudyDesignCellVO: - # Get nodes and check if they can play together - study_root_node: StudyRoot = StudyRoot.nodes.get_or_none( - uid=design_cell_vo.study_uid - ) + """Creates or updates a StudyDesignCell""" - exceptions.NotFoundException.raise_if( - study_root_node is None, "Study", design_cell_vo.study_uid - ) + # Fetch nodes referenced by uids + query = [ + "MATCH (study_root:StudyRoot {uid: $study_uid})-[:LATEST]->(latest_value:StudyValue)", + "OPTIONAL MATCH (latest_value)-[:HAS_STUDY_EPOCH]->(study_epoch:StudyEpoch {uid: $study_epoch_uid})", + ] + params = { + "study_uid": design_cell_vo.study_uid, + "study_epoch_uid": design_cell_vo.study_epoch_uid, + } + returns = ["study_root", "latest_value", "study_epoch"] - latest_study_value_node: StudyValue = study_root_node.latest_value.single() + if design_cell_vo.study_arm_uid: + query.append( + "OPTIONAL MATCH (latest_value)-[:HAS_STUDY_ARM]->(study_arm:StudyArm {uid: $study_arm_uid})" + ) + params["study_arm_uid"] = design_cell_vo.study_arm_uid + returns.append("study_arm") + # query if StudyArm has StudyBranchArm assigned to it + returns.append( + "exists((study_arm)-[:STUDY_ARM_HAS_BRANCH_ARM]->(:StudyBranchArm)<-[:HAS_STUDY_BRANCH_ARM]-(latest_value)) AS study_arm_has_branch_arm" + ) - # check if something has changed - if not create: - # get the previous item - previous_item: StudyDesignCell = ( - latest_study_value_node.has_study_design_cell.get_or_none( - uid=design_cell_vo.uid - ) + if design_cell_vo.study_branch_arm_uid: + query.append( + "OPTIONAL MATCH (latest_value)-[:HAS_STUDY_BRANCH_ARM]->(study_branch_arm:StudyBranchArm {uid: $study_branch_arm_uid})" ) - previous_study_epoch: StudyEpoch = self.get_current_outbound_node( - node=previous_item, - outbound_rel_name="study_epoch", - study_value=latest_study_value_node, + params["study_branch_arm_uid"] = design_cell_vo.study_branch_arm_uid + returns.append("study_branch_arm") + + if design_cell_vo.study_element_uid is not None: + query.append( + "OPTIONAL MATCH (latest_value)-[:HAS_STUDY_ELEMENT]->(study_element:StudyElement {uid: $study_element_uid})" ) - previous_study_arm: StudyArm = self.get_current_outbound_node( - node=previous_item, - outbound_rel_name="study_arm", - study_value=latest_study_value_node, + params["study_element_uid"] = design_cell_vo.study_element_uid + returns.append("study_element") + + if not create and design_cell_vo.uid: + query.append( + "OPTIONAL MATCH (latest_value)-[:HAS_STUDY_DESIGN_CELL]->(study_design_cell:StudyDesignCell {uid: $study_design_cell_uid})" ) - previous_study_branch_arm: StudyBranchArm = self.get_current_outbound_node( - node=previous_item, - outbound_rel_name="study_branch_arm", - study_value=latest_study_value_node, + params["study_design_cell_uid"] = design_cell_vo.uid + returns.append("study_design_cell") + + query.append(f"RETURN {', '.join(returns)}") + query_str = "\n".join(query) + results, keys = db.cypher_query(query_str, params, resolve_objects=True) + + if len(results) < 1: + raise exceptions.NotFoundException("Study", design_cell_vo.study_uid) + if len(results) > 1: + raise exceptions.AlreadyExistsException( + msg=f"Multiple StudyRoot nodes found with uid '{design_cell_vo.study_uid}'." ) - previous_study_element: StudyElement = self.get_current_outbound_node( - node=previous_item, - outbound_rel_name="study_element", - study_value=latest_study_value_node, + + nodes = dict(zip(keys, results[0])) + study_root_node: StudyRoot = nodes["study_root"] + study_value_node: StudyValue = nodes["latest_value"] + study_epoch_node: StudyEpoch | None = nodes.get("study_epoch") + study_arm_node: StudyArm | None = nodes.get("study_arm") + study_branch_arm_node: StudyBranchArm | None = nodes.get("study_branch_arm") + study_element_node: StudyElement | None = nodes.get("study_element") + study_arm_has_branch_arm: bool | None = nodes.get("study_arm_has_branch_arm") + previous_node: StudyDesignCell | None = nodes.get("study_design_cell") + + # Check if something has changed + if not create: + exceptions.NotFoundException.raise_if_not( + previous_node, "StudyDesignCell", design_cell_vo.uid ) - to_compare_previous = [ - ( - previous_study_arm.uid - if not previous_study_branch_arm and previous_study_arm - else None - ), - previous_study_branch_arm.uid if previous_study_branch_arm else None, - previous_study_epoch.uid if previous_study_epoch else None, - previous_study_element.uid if previous_study_element else None, - previous_item.transition_rule, - previous_item.order, - ] - to_compare_post = list( - map( - design_cell_vo.__dict__.get, - [ - "study_arm_uid", - "study_branch_arm_uid", - "study_epoch_uid", - "study_element_uid", - "transition_rule", - "order", - ], + + # get the previous VO + if previous_vo is None: + previous_vo = cls.find_by_uid( + study_uid=design_cell_vo.study_uid, uid=design_cell_vo.uid ) - ) exceptions.BusinessLogicException.raise_if( - not previous_study_arm - and not previous_study_branch_arm + not previous_vo.study_arm_uid + and not previous_vo.study_branch_arm_uid and not allow_none_arm_branch_arm, msg="Broken Existing Design Cell without Arm and BranchArm", ) - # check if there's something to change - if to_compare_previous == to_compare_post: - return self._from_repository_values( - design_cell_vo.study_uid, previous_item - ) - # check if the study_arm has StudyBranchArms assigned to it - # get StudyArm only if it's necessary - study_arm_node: StudyArm | None - if design_cell_vo.study_arm_uid: - study_arm_node = latest_study_value_node.has_study_arm.get_or_none( - uid=design_cell_vo.study_arm_uid - ) - # if any StudyBranchArms connectect to StudyArm has a study_value - exceptions.BusinessLogicException.raise_if( - study_arm_node - and self.get_current_outbound_node( - node=study_arm_node, - outbound_rel_name="has_branch_arm", - study_value=latest_study_value_node, - ), - msg=f"The Study Arm with UID '{design_cell_vo.study_arm_uid}' cannot be " - "assigned to a Study Design Cell because it has Study Branch Arms assigned to it", + compare_props = ( + "study_arm_uid", + "study_branch_arm_uid", + "study_epoch_uid", + "study_element_uid", + "transition_rule", + "order", ) - else: - study_arm_node = None - # get StudyBranchArm only if it's necessary - if design_cell_vo.study_branch_arm_uid: - study_branch_arm_node = ( - latest_study_value_node.has_study_branch_arm.get_or_none( - uid=design_cell_vo.study_branch_arm_uid - ) - ) - else: - study_branch_arm_node = None + previous_props = { + prop: getattr(previous_vo, prop) for prop in compare_props + } + if previous_vo.study_branch_arm_uid: + previous_props["study_arm_uid"] = None - # at least one of the two has to be defined - exceptions.NotFoundException.raise_if( - study_arm_node is None and study_branch_arm_node is None, - msg=f"Study Arm with UID '{design_cell_vo.study_arm_uid}' or Study Branch Arm with UID '{design_cell_vo.study_branch_arm_uid}' must exist.", + props = {prop: getattr(design_cell_vo, prop) for prop in compare_props} + + # Return previous VO if nothing has changed + if props == previous_props: + return previous_vo + + # Selected StudyArm can not have any StudyBranchArm (connected to latest StudyValue) + exceptions.BusinessLogicException.raise_if( + study_arm_has_branch_arm, + msg=f"The Study Arm with UID '{design_cell_vo.study_arm_uid}' cannot be " + "assigned to a Study Design Cell because it has Study Branch Arms assigned to it", ) - # get StudyEpoch - study_epoch_node = latest_study_value_node.has_study_epoch.get_or_none( - uid=design_cell_vo.study_epoch_uid + # Require StudyArm or StudyBranchArm + exceptions.NotFoundException.raise_if_not( + study_arm_node or study_branch_arm_node, + msg=f"Study Arm with UID '{design_cell_vo.study_arm_uid}' or Study Branch Arm with UID '{design_cell_vo.study_branch_arm_uid}' must exist.", ) - exceptions.NotFoundException.raise_if( - study_epoch_node is None, "Study Epoch", design_cell_vo.study_epoch_uid + + # Validate StudyEpoch + exceptions.NotFoundException.raise_if_not( + study_epoch_node, "Study Epoch", design_cell_vo.study_epoch_uid ) - # get StudyElement + # Validate StudyElement if design_cell_vo.study_element_uid is not None: - study_element_node = latest_study_value_node.has_study_element.get_or_none( - uid=design_cell_vo.study_element_uid - ) exceptions.NotFoundException.raise_if( study_element_node is None, "Study Element", design_cell_vo.study_element_uid, ) + # Create new node design_cell = StudyDesignCell( uid=design_cell_vo.uid, @@ -383,267 +427,94 @@ def save( ) design_cell.save() - # Create relations - # ensure switching - # study_branch_arm was defined even if the arm was specified will be connected to study branch arm - if study_branch_arm_node is not None: + # Connect relations + design_cell.study_value.connect(study_value_node) + if study_branch_arm_node: + # study_branch_arm was defined even if the arm was specified will be connected to study branch arm design_cell.study_branch_arm.connect(study_branch_arm_node) - # just arm was defined - else: + elif study_arm_node: + # just arm was defined design_cell.study_arm.connect(study_arm_node) design_cell.study_epoch.connect(study_epoch_node) design_cell.study_element.connect(study_element_node) if create: - self.manage_versioning_create( + _manage_versioning_with_relations( study_root=study_root_node, - study_design_cell=design_cell_vo, - new_item=design_cell, + action_type=Create, + before=None, + after=design_cell, + author_id=author_id, ) + else: - self.manage_versioning_update( - study_root=study_root_node, - study_design_cell=design_cell_vo, - previous_item=previous_item, - new_item=design_cell, - ) exclude_study_selection_relationships = [ StudyArm, StudyBranchArm, StudyEpoch, StudyElement, ] - manage_previous_connected_study_selection_relationships( - previous_item=previous_item, - study_value_node=latest_study_value_node, - new_item=design_cell, - exclude_study_selection_relationships=exclude_study_selection_relationships, - ) - # check if the new cell already exists - all_existing = self.get_design_cells_connected_to_epoch( - design_cell_vo.study_uid, design_cell_vo.study_epoch_uid - ) - for existing in all_existing: - arm = existing.study_arm.single() - branch = existing.study_branch_arm.single() - arm_uid = arm.uid if arm else None - branch_uid = branch.uid if branch else None - - exceptions.AlreadyExistsException.raise_if( - branch_uid and branch_uid == design_cell_vo.study_branch_arm_uid, - msg="A study design cell already exists for the given combination study branch arm and study epoch.", - ) - exceptions.AlreadyExistsException.raise_if( - not branch_uid and arm_uid == design_cell_vo.study_arm_uid, - msg="A study design cell already exists for the given combination of study arm and study epoch.", + _manage_versioning_with_relations( + study_root=study_root_node, + action_type=Edit, + before=previous_node, + after=design_cell, + exclude_relationships=exclude_study_selection_relationships, + author_id=author_id, ) - design_cell.study_value.connect(latest_study_value_node) - # return the json response model - return self._from_repository_values(design_cell_vo.study_uid, design_cell) - - def manage_versioning_update( - self, - study_root: StudyRoot, - study_design_cell: StudyDesignCellVO, - previous_item: StudyDesignCell, - new_item: StudyDesignCell, - ): - # Record StudyAction - action = Edit( - date=datetime.datetime.now(datetime.timezone.utc), - author_id=study_design_cell.author_id, - ) - action.save() - action.has_before.connect(previous_item) - action.has_after.connect(new_item) - study_root.audit_trail.connect(action) - - def manage_versioning_create( - self, - study_root: StudyRoot, - study_design_cell: StudyDesignCellVO, - new_item: StudyDesignCell, - ): - # create StudyAction node - action = Create( - date=datetime.datetime.now(datetime.timezone.utc), - author_id=study_design_cell.author_id, - ) - action.save() - # connect the new item to the newly StudyAction - action.has_after.connect(new_item) - # connect the audit trail to the study_root node - study_root.audit_trail.connect(action) + # return created/updated StudyDesignCellVO + return cls.find_by_uid(study_uid=design_cell_vo.study_uid, uid=design_cell.uid) + @classmethod + @trace_calls def patch_study_arm( - self, + cls, study_uid: str, design_cell_uid: str, study_arm_uid: str, author_id: str, allow_none_arm_branch_arm=False, ): - study_design_cell = self.find_by_uid(study_uid=study_uid, uid=design_cell_uid) + study_design_cell = cls.find_by_uid(study_uid=study_uid, uid=design_cell_uid) study_design_cell.study_arm_uid = study_arm_uid study_design_cell.study_branch_arm_uid = None - self.save( + cls.save( study_design_cell, author_id, create=False, allow_none_arm_branch_arm=allow_none_arm_branch_arm, ) - def get_design_cells_connected_to_branch_arm( - self, - study_uid: str, - study_branch_arm_uid: str, - study_value_version: str | None = None, - ): - if study_value_version: - filters = { - STUDY_VALUE_VERSION_QUALIFIER: study_value_version, - STUDY_VALUE_UID_QUALIFIER: study_uid, - "study_branch_arm__uid": study_branch_arm_uid, - "study_branch_arm__study_value__has_version|version": study_value_version, - "study_branch_arm__study_value__has_version__uid": study_uid, - } - else: - filters = { - STUDY_VALUE_LATEST_UID_QUALIFIER: study_uid, - "study_branch_arm__uid": study_branch_arm_uid, - "study_branch_arm__study_value__latest_value__uid": study_uid, - } - sdc_node = ListDistinct( - StudyDesignCell.nodes.fetch_relations( - "study_epoch__has_epoch__has_selected_term__has_name_root__has_latest_value", - "has_after__audit_trail", - "study_epoch__study_value", - "study_branch_arm", - Optional("study_branch_arm__study_value"), - Optional("study_element__study_value"), - ) - .filter(**filters) - .order_by("order") - .resolve_subgraph() - ).distinct() - return sdc_node - - def get_design_cells_connected_to_epoch( - self, - study_uid: str, - study_epoch_uid: str, - study_value_version: str | None = None, - ): - if study_value_version: - filters = { - STUDY_VALUE_VERSION_QUALIFIER: study_value_version, - STUDY_VALUE_UID_QUALIFIER: study_uid, - "study_epoch__uid": study_epoch_uid, - "study_epoch__study_value__has_version|version": study_value_version, - "study_epoch__study_value__has_version__uid": study_uid, - } - else: - filters = { - STUDY_VALUE_LATEST_UID_QUALIFIER: study_uid, - "study_epoch__uid": study_epoch_uid, - "study_epoch__study_value__latest_value__uid": study_uid, - } - sdc_node = ListDistinct( - StudyDesignCell.nodes.fetch_relations( - "study_epoch__has_epoch__has_selected_term__has_name_root__has_latest_value", - "has_after__audit_trail", - "study_epoch__study_value", - Optional("study_arm__study_value"), - Optional("study_branch_arm__study_value"), - Optional("study_element__study_value"), - ) - .filter(**filters) - .order_by("order") - .resolve_subgraph() - ).distinct() - - return sdc_node - - def get_design_cells_connected_to_arm( - self, study_uid: str, study_arm_uid: str, study_value_version: str | None = None - ): - if study_value_version: - filters = { - STUDY_VALUE_VERSION_QUALIFIER: study_value_version, - STUDY_VALUE_UID_QUALIFIER: study_uid, - "study_arm__uid": study_arm_uid, - "study_arm__study_value__has_version|version": study_value_version, - "study_arm__study_value__has_version__uid": study_uid, - } - else: - filters = { - STUDY_VALUE_LATEST_UID_QUALIFIER: study_uid, - "study_arm__uid": study_arm_uid, - "study_arm__study_value__latest_value__uid": study_uid, - } - sdc_node = ListDistinct( - StudyDesignCell.nodes.fetch_relations( - "study_epoch__has_epoch__has_selected_term__has_name_root__has_latest_value", - "study_arm__study_value", - "has_after__audit_trail", - "study_epoch__study_value", - Optional("study_element__study_value"), - ) - .filter(**filters) - .order_by("order") - .resolve_subgraph() - ).distinct() - - return sdc_node - @staticmethod - def get_design_cells_connected_to_element( - study_uid: str, - study_element_uid: str, - study_value_version: str | None = None, - ): - if study_value_version: - filters = { - STUDY_VALUE_VERSION_QUALIFIER: study_value_version, - STUDY_VALUE_UID_QUALIFIER: study_uid, - "study_element__uid": study_element_uid, - "study_element__study_value__has_version|version": study_value_version, - "study_element__study_value__has_version__uid": study_uid, - } - else: - filters = { - STUDY_VALUE_LATEST_UID_QUALIFIER: study_uid, - "study_element__uid": study_element_uid, - "study_element__study_value__latest_value__uid": study_uid, - } - sdc_node = ListDistinct( - StudyDesignCell.nodes.fetch_relations( - "study_element", - "has_after__audit_trail", + @trace_calls + def delete(study_uid: str, design_cell_uid: str, author_id: str): + try: + ( + study_root_node, + _, + design_cell, + ) = StudyRoot.nodes.traverse( + Path("latest_value", include_rels_in_return=False), + Path( + "latest_value__has_study_design_cell", + optional=True, + include_rels_in_return=False, + ), + ).get( + uid=study_uid, latest_value__has_study_design_cell__uid=design_cell_uid ) - .filter(**filters) - .order_by("order") - .resolve_subgraph() - ).distinct() - return sdc_node - - def delete(self, study_uid: str, design_cell_uid: str, author_id: str): - study_root_node = StudyRoot.nodes.get_or_none(uid=study_uid) - - exceptions.NotFoundException.raise_if( - study_root_node is None, "Study", study_uid - ) - - latest_study_value_node = study_root_node.latest_value.single() - design_cell = latest_study_value_node.has_study_design_cell.get_or_none( - uid=design_cell_uid - ) + except DoesNotExist as exc: + raise exceptions.NotFoundException("Study", study_uid) from exc + except MultipleNodesReturned as exc: + raise exceptions.AlreadyExistsException( + msg=f"Multiple StudyRoot nodes found with uid '{study_uid}'." + ) from exc - exceptions.NotFoundException.raise_if( - design_cell is None, "Study Design Cell", design_cell_uid + exceptions.NotFoundException.raise_if_not( + design_cell, "Study Design Cell", design_cell_uid ) # create delete version @@ -654,6 +525,7 @@ def delete(self, study_uid: str, design_cell_uid: str, author_id: str): ) new_design_cell.save() + # Connect relations study_arm_node = design_cell.study_arm.single() study_branch_arm_node = design_cell.study_branch_arm.single() # at least one of the two has to be defined @@ -685,32 +557,27 @@ def delete(self, study_uid: str, design_cell_uid: str, author_id: str): new_design_cell.study_element.connect(study_element_node) # Audit trail - audit_node = Delete( - author_id=author_id, date=datetime.datetime.now(datetime.timezone.utc) - ) - audit_node.save() - study_root_node.audit_trail.connect(audit_node) - audit_node.has_before.connect(design_cell) - audit_node.has_after.connect(new_design_cell) - exclude_study_selection_relationships = [ - StudyArm, - StudyBranchArm, - StudyEpoch, - StudyElement, - ] - manage_previous_connected_study_selection_relationships( - previous_item=design_cell, - study_value_node=latest_study_value_node, - new_item=new_design_cell, - exclude_study_selection_relationships=exclude_study_selection_relationships, + _manage_versioning_with_relations( + study_root=study_root_node, + action_type=Delete, + before=design_cell, + after=new_design_cell, + exclude_relationships=( + StudyArm, + StudyBranchArm, + StudyEpoch, + StudyElement, + ), + author_id=author_id, ) - def generate_uid(self) -> str: + @staticmethod + def generate_uid() -> str: return StudyDesignCell.get_next_free_uid_and_increment_counter() - def _get_selection_with_history( - self, study_uid: str, design_cell_uid: str | None = None - ): + @staticmethod + @trace_calls + def _get_selection_with_history(study_uid: str, design_cell_uid: str | None = None): """ returns the audit trail for study design cell either for a specific selection or for all study design cells for the study. @@ -737,19 +604,24 @@ def _get_selection_with_history( OPTIONAL MATCH (all_sdc)<-[:STUDY_BRANCH_ARM_HAS_DESIGN_CELL]-(sba:StudyBranchArm) OPTIONAL MATCH (all_sdc)<-[:STUDY_ARM_HAS_DESIGN_CELL]-(sa:StudyArm) MATCH (all_sdc)<-[:STUDY_EPOCH_HAS_DESIGN_CELL]-(se:StudyEpoch) + MATCH (se)-[:HAS_EPOCH]->(:CTTermContext)-[:HAS_SELECTED_TERM]->(:CTTermRoot)-[:HAS_NAME_ROOT]->(:CTTermNameRoot)-[:LATEST_FINAL]->(epoch_name:CTTermNameValue) OPTIONAL MATCH (all_sdc)<-[:STUDY_ELEMENT_HAS_DESIGN_CELL]-(sel:StudyElement) MATCH (all_sdc)<-[:AFTER]-(asa:StudyAction) OPTIONAL MATCH (all_sdc)<-[:BEFORE]-(bsa:StudyAction) - WITH all_sdc, sa, se, sel, asa, bsa, sba - ORDER BY all_sdc.uid, asa.date DESC + WITH all_sdc, sa, se, sel, asa, bsa, sba, epoch_name + ORDER BY asa.date DESC RETURN DISTINCT all_sdc.uid AS uid, all_sdc.transition_rule AS transition_rule, all_sdc.order AS order, - sa.uid AS study_arm_uid, + sa.uid AS study_arm_uid, + sa.name AS study_arm_name, sba.uid AS study_branch_arm_uid, + sba.name AS study_branch_arm_name, se.uid AS study_epoch_uid, + epoch_name.name AS study_epoch_name, sel.uid AS study_element_uid, + sel.name AS study_element_name, labels(asa) AS change_type, asa.date AS start_date, bsa.date AS end_date, @@ -771,9 +643,13 @@ def _get_selection_with_history( study_uid=study_uid, study_selection_uid=res["uid"], study_arm_uid=res["study_arm_uid"], + study_arm_name=res["study_arm_name"], study_branch_arm_uid=res["study_branch_arm_uid"], + study_branch_arm_name=res["study_branch_arm_name"], study_epoch_uid=res["study_epoch_uid"], + study_epoch_name=res["study_epoch_name"], study_element_uid=res["study_element_uid"], + study_element_name=res["study_element_name"], author_id=res["author_id"], change_type=change_type, start_date=convert_to_datetime(value=res["start_date"]), @@ -784,14 +660,17 @@ def _get_selection_with_history( ) return result + @classmethod + @trace_calls def find_selection_history( - self, study_uid: str, design_cell_uid: str | None = None + cls, study_uid: str, design_cell_uid: str | None = None ) -> list[StudyDesignCellHistory]: if design_cell_uid: - return self._get_selection_with_history( + return cls._get_selection_with_history( study_uid=study_uid, design_cell_uid=design_cell_uid ) - return self._get_selection_with_history(study_uid=study_uid) + return cls._get_selection_with_history(study_uid=study_uid) - def close(self) -> None: + @staticmethod + def close() -> None: pass diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_disease_milestone_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_disease_milestone_repository.py index 7f0cf67e..fc3fa4c6 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_disease_milestone_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_disease_milestone_repository.py @@ -393,6 +393,7 @@ def get_distinct_headers( } }, distinct=True, + ordering=[field_path], ) .all() ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_epoch_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_epoch_repository.py index c0d77eb6..5f725e4e 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_epoch_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_epoch_repository.py @@ -53,7 +53,7 @@ def get_ctlist_terms_by_name( # TODO use effective date on HAS_TERM relationship as well cypher_query = f""" MATCH (codelist_name_value:CTCodelistNameValue)<-[:LATEST_FINAL]-(:CTCodelistNameRoot)<-[:HAS_NAME_ROOT]- - (:CTCodelistRoot)-[ht:HAS_TERM WHERE ht.end_date IS NULL]->(:CTCodelistTerm)-[:HAS_TERM_ROOT]-> + (:CTCodelistRoot)-[:HAS_TERM]->(:CTCodelistTerm)-[:HAS_TERM_ROOT]-> (tr:CTTermRoot)-[:HAS_NAME_ROOT]-> {ctterm_name_match} WITH tr.uid as term_uid, collect(codelist_name_value.name) as codelist_names @@ -86,17 +86,20 @@ def get_allowed_configs(self, effective_date: datetime.datetime | None = None): if effective_date: subtype_name_value_match = """MATCH (term_subtype_name_root)-[hv:HAS_VERSION]->(term_subtype_name_value:CTTermNameValue) WHERE (hv.start_date<= datetime($effective_date) < hv.end_date) OR (hv.end_date IS NULL AND (hv.start_date <= datetime($effective_date))) + AND ht.start_date <= datetime($effective_date) AND (ht.end_date IS NULL OR ht.end_date > datetime($effective_date)) """ type_name_value_match = """MATCH (term_type_name_root)-[hv_type:HAS_VERSION]->(term_type_name_value:CTTermNameValue) WHERE (hv_type.start_date<= datetime($effective_date) < hv_type.end_date) OR (hv_type.end_date IS NULL AND (hv_type.start_date <= datetime($effective_date))) """ else: - subtype_name_value_match = "MATCH (term_subtype_name_root:CTTermNameRoot)-[:LATEST_FINAL]->(term_subtype_name_value:CTTermNameValue)" + subtype_name_value_match = """MATCH (term_subtype_name_root:CTTermNameRoot)-[:LATEST_FINAL]->(term_subtype_name_value:CTTermNameValue) + WHERE ht.end_date IS NULL + """ type_name_value_match = "MATCH (term_type_name_root)-[:LATEST_FINAL]->(term_type_name_value:CTTermNameValue)" cypher_query = f""" MATCH (:CTCodelistNameValue {{name: $code_list_name}})<-[:LATEST_FINAL]-(:CTCodelistNameRoot)<-[:HAS_NAME_ROOT] - -(:CTCodelistRoot)-[:HAS_TERM]->(:CTCodelistTerm)-[:HAS_TERM_ROOT]->(term_subtype_root:CTTermRoot)-[:HAS_NAME_ROOT]->(term_subtype_name_root:CTTermNameRoot) + -(:CTCodelistRoot)-[ht:HAS_TERM]->(:CTCodelistTerm)-[:HAS_TERM_ROOT]->(term_subtype_root:CTTermRoot)-[:HAS_NAME_ROOT]->(term_subtype_name_root:CTTermNameRoot) {subtype_name_value_match} MATCH (term_subtype_root)-[:HAS_PARENT_TYPE]->(term_type_root:CTTermRoot)- [:HAS_NAME_ROOT]->(term_type_name_root) @@ -161,7 +164,7 @@ def _create_aggregate_root_instance_from_cypher_result( author_id=input_dict.get("study_action").get("author_id"), author_username=input_dict["author_username"], color_hash=input_dict.get("study_epoch").get("color_hash"), - number_of_assigned_visits=input_dict["count_vists"], + number_of_assigned_visits=input_dict["count_visits"], ) if not audit_trail: return study_epoch_vo @@ -293,7 +296,7 @@ def find_all_epochs_query( epoch_term, epoch_subtype_term, epoch_type_term, - size([(study_epoch)-[:STUDY_EPOCH_HAS_STUDY_VISIT]->(study_visit:StudyVisit)<-[:HAS_STUDY_VISIT]-(:StudyValue) | study_visit]) AS count_vists, + size([(study_epoch)-[:STUDY_EPOCH_HAS_STUDY_VISIT]->(study_visit:StudyVisit)<-[:HAS_STUDY_VISIT]-(study_value:StudyValue) | study_visit]) AS count_visits, coalesce(head([(user:User)-[*0]-() WHERE user.user_id=study_action.author_id | user.username]), study_action.author_id) AS author_username """ ) diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_group_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_group_repository.py index 7edcca79..da35216b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_group_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_group_repository.py @@ -80,10 +80,10 @@ def _additional_match(self, **kwargs) -> str: MATCH (sv)-[:HAS_STUDY_ACTIVITY]->(sa:StudyActivity) -[:STUDY_ACTIVITY_HAS_STUDY_SOA_GROUP]->(soag:StudySoAGroup) - WHERE NOT (soag)<-[:BEFORE]-() + <-[:HAS_STUDY_SOA_GROUP]-(sv) OPTIONAL MATCH (sa)-[:STUDY_ACTIVITY_HAS_STUDY_ACTIVITY_GROUP]->(sag:StudyActivityGroup) - WHERE NOT (sag)<-[:BEFORE]-() + <-[:HAS_STUDY_ACTIVITY_GROUP]-(sv) WITH DISTINCT sr, soag, collect(DISTINCT sag.uid) AS study_activity_group_uids diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_repository.py index 49904650..bafb25db 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_soa_repository.py @@ -504,7 +504,7 @@ def query_study_activities( show_activity_in_protocol_flowchart: study_activity.show_activity_in_protocol_flowchart, show_activity_group_in_protocol_flowchart: COALESCE(study_activity_group.show_activity_group_in_protocol_flowchart, false), show_activity_subgroup_in_protocol_flowchart: COALESCE(study_activity_subgroup.show_activity_subgroup_in_protocol_flowchart, false), - show_soa_group_in_protocol_flowchart: study_soa_group.show_soa_group_in_protocol_flowchart, + show_soa_group_in_protocol_flowchart: COALESCE(study_soa_group.show_soa_group_in_protocol_flowchart, false), study_activity_subgroup: { study_activity_subgroup_uid: study_activity_subgroup.uid, activity_subgroup_uid: activity_subgroup_root.uid, diff --git a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_visit_repository.py b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_visit_repository.py index b8b8409a..3b48ccc7 100644 --- a/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_visit_repository.py +++ b/clinical-mdr-api/clinical_mdr_api/domain_repositories/study_selections/study_visit_repository.py @@ -232,6 +232,7 @@ def from_study_visit_vo_to_history_vo( visit_order=int(study_visit_vo.visit_number), vis_unique_number=study_visit_vo.vis_unique_number, vis_short_name=study_visit_vo.vis_short_name, + repeating_frequency=study_visit_vo.repeating_frequency, # History VO params change_type=change_type, end_date=study_action_before.get("date"), @@ -603,8 +604,7 @@ def find_all_visits_query( { visit_group:visit_group, consecutive_visits: apoc.coll.sortMaps([ - (visit_group)<-[:IN_VISIT_GROUP]-(consecutive_visits:StudyVisit) - WHERE NOT (consecutive_visits)--(:Delete) AND NOT (consecutive_visits)-[:BEFORE]-() + (visit_group)<-[:IN_VISIT_GROUP]-(consecutive_visits:StudyVisit)<-[:HAS_STUDY_VISIT]-(study_value) | {vis: consecutive_visits, unique_visit_number: toInteger(consecutive_visits.unique_visit_number)} ], '^unique_visit_number') }]) AS group @@ -643,7 +643,7 @@ def find_all_visits_query( else: query.append(return_statement) - query.append("ORDER BY study_visit.unique_visit_number") + query.append("ORDER BY toInteger(study_visit.unique_visit_number)") return "\n".join(query), params diff --git a/clinical-mdr-api/clinical_mdr_api/domains/_utils.py b/clinical-mdr-api/clinical_mdr_api/domains/_utils.py index 223ac9ba..b36bc758 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/_utils.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/_utils.py @@ -1,5 +1,6 @@ from typing import Literal, overload +from clinical_mdr_api.domains.concepts.utils import EN_LANGUAGE, ENG_LANGUAGE from clinical_mdr_api.domains.iso_languages import LANGUAGES_INDEXED_BY from clinical_mdr_api.domains.libraries.parameter_term import ParameterTermEntryVO from common import exceptions @@ -8,76 +9,88 @@ @overload def get_iso_lang_data( query: str, - key: Literal["names", "639-1", "639-2/T", "639-2/B", "639-3"] = "639-3", return_key: None = None, ignore_case: bool = True, -) -> dict[str, str | list[str]]: ... +) -> str | list[str] | dict[str, str]: ... @overload def get_iso_lang_data( query: str, - key: Literal["names", "639-1", "639-2/T", "639-2/B", "639-3"] = "639-3", - return_key: Literal["names", "639-3"] = "names", + return_key: Literal["names"], ignore_case: bool = True, ) -> list[str]: ... @overload def get_iso_lang_data( query: str, - key: Literal["names", "639-1", "639-2/T", "639-2/B", "639-3"] = "639-3", - return_key: Literal["639-1", "639-2/T", "639-2/B"] = "639-1", + return_key: Literal["639-3"], + ignore_case: bool = True, +) -> dict[str, str]: ... +@overload +def get_iso_lang_data( + query: str, + return_key: Literal["639-1", "639-2/T", "639-2/B"], ignore_case: bool = True, ) -> str: ... def get_iso_lang_data( query: str, - key: Literal["names", "639-1", "639-2/T", "639-2/B", "639-3"] = "639-3", return_key: Literal["names", "639-1", "639-2/T", "639-2/B", "639-3"] | None = None, ignore_case: bool = True, -) -> str | dict[str, str | list[str]] | list[str]: +) -> str | list[str] | dict[str, str]: """ - Returns ISO language data based on the provided query string and key. + Returns ISO language data based on the provided query string and return_key. Args: - query (str): Query string to search for in the language index. - key (str, optional): Key to use for indexing the language data. Defaults to "639-3". - return_key (str | None, optional): Key to return from the found language data. Defaults to None. + query (str): The language identifier to search for (e.g., code or name). + return_key (Literal["names", "639-1", "639-2/T", "639-2/B", "639-3"] | None, optional): + Specifies which field to return from the matched language entry. + - If None, returns the value for the matched key. + - If "names", returns a list of language names. + - If "639-3", returns a dictionary mapping 639-3 codes to names. + - If "639-1", "639-2/T", or "639-2/B", returns the corresponding code as a string. ignore_case (bool, optional): Whether to ignore case when searching for the query string. Defaults to True. - Returns: - str | dict[str, str, list[str]] | list[Any]: The value of the found language data, or the entire language data if return_key is None. + Returns: str | list[str] | dict[str, str]: The requested language data, depending on return_key. Raises: - TypeError: If the query string is not a string. - ValueError: If the provided key is not a valid index for the language data. - ValidationException: If the query string is not found in the language index, or if it is found but does not match the query when ignore_case is False. + TypeError: If the query is not a string. + ValidationException: If the query is not found or does not match when ignore_case is False. - Example: - >>> get_iso_lang_data("spa", "639-2/T", "names") + Examples: + >>> get_iso_lang_data("spa", "names") ["Spanish", "Castilian"] + >>> get_iso_lang_data("spa", "639-1") + 'es' """ if not isinstance(query, str): raise TypeError(f"Expected type str but found {type(query)}") - try: + keys = LANGUAGES_INDEXED_BY.keys() + + if return_key is not None and return_key not in keys: + raise KeyError(f"Return key '{return_key}' is not a valid language key.") + + lang = None + key = "" + for key in keys: index = LANGUAGES_INDEXED_BY[key] - except KeyError as exc: - raise exceptions.ValidationException( - msg=f"Languages not indexed by key: {key}" - ) from exc - casefolded_query = query.casefold() + casefolded_query = query.casefold() - try: - lang = index[casefolded_query] - except KeyError as exc: - raise exceptions.ValidationException( - msg=f"Language '{query}' not found in {key}." - ) from exc + try: + lang = index[casefolded_query] + break + except KeyError: + continue - if not ignore_case and lang[key] != query: + if lang is None or (not ignore_case and lang[key] != query): raise exceptions.ValidationException( - msg=f"Language '{query}' not found in {key}." + msg=f"The provided language '{query}' does not match any known language names or codes.", ) - return lang[return_key] if return_key else lang + return lang[key] if not return_key else lang[return_key] + + +def is_language_english(lang: str) -> bool: + return lang.casefold() in [EN_LANGUAGE, ENG_LANGUAGE] def is_syntax_of_template_name_correct(name: str) -> bool: diff --git a/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_instance_class.py b/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_instance_class.py index 0187de92..ee5f2f2f 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_instance_class.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_instance_class.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import AbstractSet, Callable, Self -from pydantic import BaseModel - from clinical_mdr_api.domains.biomedical_concepts.activity_item_class import ( ActivityInstanceClassActivityItemClassRelVO, ) @@ -17,12 +15,6 @@ from common.exceptions import AlreadyExistsException, BusinessLogicException -class CTTermItem(BaseModel): - uid: str - name: str | None = None - codelist_uid: str | None = None - - @dataclass(frozen=True) class ActivityInstanceClassVO: """ diff --git a/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_item_class.py b/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_item_class.py index 436d9e0a..4c6508ff 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_item_class.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/biomedical_concepts/activity_item_class.py @@ -1,8 +1,6 @@ from dataclasses import dataclass from typing import AbstractSet, Callable, Self -from pydantic import BaseModel - from clinical_mdr_api.domains.versioned_object_aggregate import ( LibraryItemAggregateRootBase, LibraryItemMetadataVO, @@ -22,9 +20,12 @@ class ActivityInstanceClassActivityItemClassRelVO: uid: str mandatory: bool is_adam_param_specific_enabled: bool + is_additional_optional: bool + is_default_linked: bool -class CTTermItem(BaseModel): +@dataclass(frozen=True) +class CTTermItem: uid: str name: str | None = None codelist_uid: str | None = None @@ -40,6 +41,7 @@ class ActivityItemClassVO: definition: str | None nci_concept_id: str | None order: int + display_name: str | None activity_instance_classes: list[ActivityInstanceClassActivityItemClassRelVO] data_type: CTTermItem role: CTTermItem @@ -56,6 +58,7 @@ def from_repository_values( definition: str | None = None, nci_concept_id: str | None = None, variable_class_uids: list[str] | None = None, + display_name: str | None = None, ) -> Self: activity_item_class_vo = cls( name=name, @@ -66,6 +69,7 @@ def from_repository_values( variable_class_uids=variable_class_uids, definition=definition, nci_concept_id=nci_concept_id, + display_name=display_name, ) return activity_item_class_vo @@ -127,6 +131,10 @@ def definition(self) -> str | None: def nci_concept_id(self) -> str | None: return self._activity_item_class_vo.nci_concept_id + @property + def display_name(self) -> str | None: + return self._activity_item_class_vo.display_name + @classmethod def from_repository_values( cls, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity.py index 4efdd730..ac37b52d 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity.py @@ -136,6 +136,12 @@ def validate( msg=f"Following Activities already have the provided synonyms: {existing_synonyms_with_uids}", ) + if library_name == "Sponsor": + BusinessLogicException.raise_if_not( + self.activity_groupings, + msg="Sponsor activities must have at least one grouping.", + ) + for activity_grouping in self.activity_groupings: BusinessLogicException.raise_if_not( activity_subgroup_exists(activity_grouping.activity_subgroup_uid), diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_instance.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_instance.py index cec895c2..f1dcc519 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_instance.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_instance.py @@ -12,9 +12,6 @@ from clinical_mdr_api.domains.concepts.activities.activity import ActivityGroupingVO from clinical_mdr_api.domains.concepts.activities.activity_item import ActivityItemVO from clinical_mdr_api.domains.concepts.concept_base import ConceptARBase, ConceptVO -from clinical_mdr_api.domains.concepts.odms.form import OdmFormAR -from clinical_mdr_api.domains.concepts.odms.item import OdmItemAR -from clinical_mdr_api.domains.concepts.odms.item_group import OdmItemGroupAR from clinical_mdr_api.domains.versioned_object_aggregate import ( LibraryItemMetadataVO, LibraryVO, @@ -111,7 +108,7 @@ def from_repository_values( return activity_instance_vo - def validate( + def validate( # pylint: disable=too-many-locals self, get_final_activity_value_by_uid_callback: Callable[[str], Node | None], activity_subgroup_exists: Callable[[str], bool], @@ -124,9 +121,6 @@ def validate( find_activity_instance_class_by_uid_callback: Callable[ ..., ActivityInstanceClassAR | None ], - get_odm_form_by_uid_callback: Callable[..., OdmFormAR], - get_odm_item_group_by_uid_callback: Callable[..., OdmItemGroupAR], - get_odm_item_by_uid_callback: Callable[..., OdmItemAR], get_dimension_names_by_unit_definition_uids: Callable[[list[str]], list[str]], activity_instance_exists_by_property_value: Callable[ [str, str, str], bool @@ -139,6 +133,8 @@ def validate( activity_group_latest_is_final: Callable[[str], bool] = lambda x: True, get_activity_subgroup_name: Callable[[str], str | None] = lambda x: None, get_activity_group_name: Callable[[str], str | None] = lambda x: None, + get_parent_class_uid_callback: Callable[[str], str | None] = lambda _: None, + strict_mode: bool = False, ) -> None: if not preview: self.validate_name_sentence_case() @@ -163,6 +159,12 @@ def validate( self.topic_code, "Topic Code", ) + + if not self.activity_groupings: + raise BusinessLogicException( + msg="Activity Instance must have at least one grouping", + ) + for activity_grouping in self.activity_groupings: if activity_grouping.activity_uid is None: raise BusinessLogicException( @@ -222,6 +224,23 @@ def validate( raise BusinessLogicException( msg=f"Cannot create activity instance: Activity Group {group_str} is currently not in Final status." ) + + activity_item_class_uids = [ + item.activity_item_class_uid for item in self.activity_items + ] + seen_activity_item_class_uids = [] + duplicate_activity_item_class_uids = [] + for activity_item_class_uid in activity_item_class_uids: + if activity_item_class_uid in seen_activity_item_class_uids: + duplicate_activity_item_class_uids.append(activity_item_class_uid) + else: + seen_activity_item_class_uids.append(activity_item_class_uid) + + if duplicate_activity_item_class_uids: + raise BusinessLogicException( + msg=f"The following Activity Item Class(es) have been associated to more than one Activity Item: {",".join(duplicate_activity_item_class_uids)}" + ) + for activity_item in self.activity_items: activity_item_class = find_activity_item_class_by_uid_callback( activity_item.activity_item_class_uid @@ -274,77 +293,6 @@ def validate( msg=f"{type(self).__name__} tried to connect to non-existent or non-final Unit Definition with UID '{unit.uid}'.", ) - if ( - activity_item.odm_item_group is not None - and activity_item.odm_item_group.uid is not None - and activity_item.odm_form is not None - and activity_item.odm_form.uid is None - ): - raise BusinessLogicException( - msg="ODM Form must be provided if ODM Item Group is provided.", - ) - if ( - activity_item.odm_item is not None - and activity_item.odm_item.uid is not None - and activity_item.odm_item_group is not None - and activity_item.odm_item_group.uid is None - ): - raise BusinessLogicException( - msg="ODM Item Group must be provided if ODM Item is provided.", - ) - - odm_form = None - if ( - activity_item.odm_form is not None - and activity_item.odm_form.uid is not None - ): - odm_form = get_odm_form_by_uid_callback(activity_item.odm_form.uid) - if not odm_form: - raise BusinessLogicException( - msg=f"ODM Form with UID '{activity_item.odm_form.uid}' doesn't exist." - ) - - odm_item_group = None - if ( - activity_item.odm_item_group is not None - and activity_item.odm_item_group.uid is not None - and odm_form is not None - ): - if ( - activity_item.odm_item_group.uid - not in odm_form.concept_vo.item_group_uids - ): - raise BusinessLogicException( - msg=f"ODM Form with UID '{activity_item.odm_form.uid}' doesn't contain the ODM Item Group with UID '{activity_item.odm_item_group.uid}'." - ) - - odm_item_group = get_odm_item_group_by_uid_callback( - activity_item.odm_item_group.uid - ) - if not odm_item_group: - raise BusinessLogicException( - msg=f"ODM Item Group with UID '{activity_item.odm_item_group.uid}' doesn't exist." - ) - - if ( - activity_item.odm_item is not None - and activity_item.odm_item.uid is not None - and odm_item_group is not None - ): - if ( - activity_item.odm_item.uid - not in odm_item_group.concept_vo.item_uids - ): - raise BusinessLogicException( - msg=f"ODM Item Group with UID '{activity_item.odm_item_group.uid}' doesn't contain the ODM Item with UID '{activity_item.odm_item.uid}'." - ) - - odm_item = get_odm_item_by_uid_callback(activity_item.odm_item.uid) - if not odm_item: - raise BusinessLogicException( - msg=f"ODM Item with UID '{activity_item.odm_item.uid}' doesn't exist." - ) - activity_instance_class = find_activity_instance_class_by_uid_callback( self.activity_instance_class_uid ) @@ -354,30 +302,103 @@ def validate( ) # Validate that all mandatory Activity Item Classes for the selected - # Activity Instance Class are present in the create input - # Disabled for now as it is very restrictive. - # Might be re-enabled in the future, possibly in some modified way. - # required_item_class_uids = { - # rel.uid - # for rel in activity_instance_class.activity_instance_class_vo.activity_item_classes - # if rel.mandatory - # } - # selected_item_class_uids = { - # activity_item.activity_item_class_uid - # for activity_item in self.activity_items - # } - # missing_required_uids = required_item_class_uids.difference( - # selected_item_class_uids - # ) - - # BusinessLogicException.raise_if( - # len(missing_required_uids) > 0, - # msg=( - # "The following mandatory Activity Item Classes must be selected for " - # f"Activity Instance Class '{self.activity_instance_class_uid}': " - # + ", ".join(sorted(missing_required_uids)) - # ), - # ) + # Activity Instance Class are present in the create input just if strict_mode is True + if strict_mode: + required_item_class_uids = { + rel.uid + for rel in activity_instance_class.activity_instance_class_vo.activity_item_classes + if rel.mandatory + } + selected_item_class_uids = { + activity_item.activity_item_class_uid + for activity_item in self.activity_items + } + missing_required_uids = required_item_class_uids.difference( + selected_item_class_uids + ) + + if missing_required_uids: + # Get names for missing item classes + missing_item_class_names = [] + for uid in sorted(missing_required_uids): + item_class = find_activity_item_class_by_uid_callback(uid) + if item_class: + missing_item_class_names.append(item_class.name) + else: + missing_item_class_names.append( + uid + ) # Fallback to UID if not found + + BusinessLogicException.raise_if( + True, + msg=( + "The following mandatory Activity Item Classes must be selected for " + f"Activity Instance Class '{activity_instance_class.name}': " + + ", ".join(missing_item_class_names) + ), + ) + + # Check parent class mandatory items if current class has level 3 and strict_mode is True + current_level = activity_instance_class.activity_instance_class_vo.level + if current_level == 3 and strict_mode: + + parent_class_uid = get_parent_class_uid_callback( + self.activity_instance_class_uid + ) + if parent_class_uid: + parent_class = find_activity_instance_class_by_uid_callback( + parent_class_uid + ) + if parent_class and parent_class.activity_instance_class_vo.level == 2: + # Get mandatory item classes from parent (level 2) + parent_required_item_class_uids = { + rel.uid + for rel in parent_class.activity_instance_class_vo.activity_item_classes + if rel.mandatory + } + # Check that parent mandatory items are selected with CT terms or unit definitions + # (i.e., they exist in activity_items AND have CT terms or unit definitions) + parent_missing_required_uids = [] + for required_uid in parent_required_item_class_uids: + # Check if this item class is selected + matching_item = next( + ( + item + for item in self.activity_items + if item.activity_item_class_uid == required_uid + ), + None, + ) + if not matching_item: + parent_missing_required_uids.append(required_uid) + elif ( + not matching_item.ct_terms + and not matching_item.unit_definitions + ): + # Item is selected but has neither CT terms nor unit definitions + parent_missing_required_uids.append(required_uid) + + if parent_missing_required_uids: + # Get names for missing item classes + parent_missing_item_class_names = [] + for uid in sorted(parent_missing_required_uids): + item_class = find_activity_item_class_by_uid_callback(uid) + if item_class: + parent_missing_item_class_names.append(item_class.name) + else: + parent_missing_item_class_names.append( + uid + ) # Fallback to UID if not found + + BusinessLogicException.raise_if( + True, + msg=( + "The following mandatory Activity Item Classes from the parent " + f"Activity Instance Class '{parent_class.name}' (level 2) must have CT terms or unit definitions selected for " + f"Activity Instance Class '{activity_instance_class.name}' (level 3): " + + ", ".join(parent_missing_item_class_names) + ), + ) unit_dimension_names = get_dimension_names_by_unit_definition_uids( [ @@ -445,9 +466,6 @@ def from_input_values( author_id: str, concept_vo: ActivityInstanceVO, library: LibraryVO, - get_odm_form_by_uid_callback: Callable[..., OdmFormAR], - get_odm_item_group_by_uid_callback: Callable[..., OdmItemGroupAR], - get_odm_item_by_uid_callback: Callable[..., OdmItemAR], concept_exists_by_callback: Callable[ [str, str, bool], bool ] = lambda x, y, z: True, @@ -470,6 +488,8 @@ def from_input_values( activity_group_latest_is_final: Callable[[str], bool] = lambda x: True, get_activity_subgroup_name: Callable[[str], str | None] = lambda x: None, get_activity_group_name: Callable[[str], str | None] = lambda x: None, + get_parent_class_uid_callback: Callable[[str], str | None] = lambda _: None, + strict_mode: bool = False, generate_uid_callback: Callable[[], str | None] = lambda: None, preview: bool = False, ) -> Self: @@ -490,9 +510,6 @@ def from_input_values( unit_definition_exists_by_uid_callback=unit_definition_exists_by_uid_callback, find_activity_item_class_by_uid_callback=find_activity_item_class_by_uid_callback, find_activity_instance_class_by_uid_callback=find_activity_instance_class_by_uid_callback, - get_odm_form_by_uid_callback=get_odm_form_by_uid_callback, - get_odm_item_group_by_uid_callback=get_odm_item_group_by_uid_callback, - get_odm_item_by_uid_callback=get_odm_item_by_uid_callback, get_dimension_names_by_unit_definition_uids=get_dimension_names_by_unit_definition_uids, library_name=library.name, preview=preview, @@ -500,6 +517,8 @@ def from_input_values( activity_group_latest_is_final=activity_group_latest_is_final, get_activity_subgroup_name=get_activity_subgroup_name, get_activity_group_name=get_activity_group_name, + get_parent_class_uid_callback=get_parent_class_uid_callback, + strict_mode=strict_mode, activity_instance_exists_by_property_value=concept_exists_by_library_and_property_value_callback, ) @@ -516,9 +535,6 @@ def edit_draft( author_id: str, change_description: str, concept_vo: ActivityInstanceVO, - get_odm_form_by_uid_callback: Callable[..., OdmFormAR], - get_odm_item_group_by_uid_callback: Callable[..., OdmItemGroupAR], - get_odm_item_by_uid_callback: Callable[..., OdmItemAR], concept_exists_by_callback: Callable[ [str, str, bool], bool ] = lambda x, y, z: True, @@ -541,6 +557,8 @@ def edit_draft( get_dimension_names_by_unit_definition_uids: Callable[ [list[str]], list[str] ] = lambda _: [], + get_parent_class_uid_callback: Callable[[str], str | None] = lambda _: None, + strict_mode: bool = False, perform_validation: bool = True, ) -> None: """ @@ -555,10 +573,9 @@ def edit_draft( unit_definition_exists_by_uid_callback=unit_definition_exists_by_uid_callback, find_activity_item_class_by_uid_callback=find_activity_item_class_by_uid_callback, find_activity_instance_class_by_uid_callback=find_activity_instance_class_by_uid_callback, - get_odm_form_by_uid_callback=get_odm_form_by_uid_callback, - get_odm_item_group_by_uid_callback=get_odm_item_group_by_uid_callback, - get_odm_item_by_uid_callback=get_odm_item_by_uid_callback, get_dimension_names_by_unit_definition_uids=get_dimension_names_by_unit_definition_uids, + get_parent_class_uid_callback=get_parent_class_uid_callback, + strict_mode=strict_mode, activity_instance_exists_by_property_value=concept_exists_by_library_and_property_value_callback, previous_name=self.name, previous_topic_code=self._concept_vo.topic_code, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_item.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_item.py index d288b425..7f1a2f8c 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_item.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_item.py @@ -4,9 +4,6 @@ from pydantic import BaseModel from clinical_mdr_api.models.concepts.activities.activity_item import ( - CompactOdmForm, - CompactOdmItem, - CompactOdmItemGroup, CompactUnitDefinition, ) @@ -32,9 +29,6 @@ class ActivityItemVO: activity_item_class_name: str | None ct_terms: list[CTTermItem] unit_definitions: list[CompactUnitDefinition] - odm_form: CompactOdmForm | None = None - odm_item_group: CompactOdmItemGroup | None = None - odm_item: CompactOdmItem | None = None @classmethod def from_repository_values( @@ -44,9 +38,6 @@ def from_repository_values( activity_item_class_name: str | None, ct_terms: list[CTTermItem], unit_definitions: list[CompactUnitDefinition], - odm_form: CompactOdmForm | None, - odm_item_group: CompactOdmItemGroup | None, - odm_item: CompactOdmItem | None, ) -> Self: activity_item_vo = cls( is_adam_param_specific=is_adam_param_specific, @@ -54,9 +45,6 @@ def from_repository_values( activity_item_class_name=activity_item_class_name, ct_terms=ct_terms, unit_definitions=unit_definitions, - odm_form=odm_form, - odm_item_group=odm_item_group, - odm_item=odm_item, ) return activity_item_vo diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_sub_group.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_sub_group.py index b70e1816..d27bd19b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_sub_group.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/activities/activity_sub_group.py @@ -9,13 +9,6 @@ from common.exceptions import AlreadyExistsException, BusinessLogicException -@dataclass(frozen=True) -class SimpleActivityGroupVO: - activity_group_uid: str - activity_group_name: str | None = None - activity_group_version: str | None = None - - @dataclass(frozen=True) class ActivitySubGroupVO(ConceptVO): """ @@ -24,7 +17,6 @@ class ActivitySubGroupVO(ConceptVO): name: str name_sentence_case: str - activity_groups: list[SimpleActivityGroupVO] @classmethod def from_repository_values( @@ -33,7 +25,6 @@ def from_repository_values( name_sentence_case: str, definition: str | None, abbreviation: str | None, - activity_groups: list[SimpleActivityGroupVO], ) -> Self: activity_subgroup_vo = cls( name=name, @@ -41,14 +32,12 @@ def from_repository_values( definition=definition, abbreviation=abbreviation, is_template_parameter=True, - activity_groups=activity_groups, ) return activity_subgroup_vo def validate( self, - activity_group_exists: Callable[[str], bool], activity_subgroup_exists_by_name_callback: Callable[ [str, str], bool ] = lambda x, y: True, @@ -66,12 +55,6 @@ def validate( self.name, "Name", ) - for activity_group in self.activity_groups: - BusinessLogicException.raise_if_not( - activity_group_exists(activity_group.activity_group_uid), - msg="Activity Subgroup tried to connect to non-existent or non-final concepts " - f"""[('Concept Name: Activity Group', "uids: {{'{activity_group.activity_group_uid}'}}")].""", - ) @dataclass @@ -97,13 +80,9 @@ def from_input_values( concept_vo: ActivitySubGroupVO, library: LibraryVO, generate_uid_callback: Callable[[], str | None] = lambda: None, - concept_exists_by_callback: Callable[ - [str, str, bool], bool - ] = lambda x, y, z: True, concept_exists_by_library_and_name_callback: Callable[ [str, str], bool ] = lambda x, y: True, - activity_group_exists: Callable[[str], bool] = lambda _: False, ) -> Self: item_metadata = LibraryItemMetadataVO.get_initial_item_metadata( author_id=author_id @@ -116,7 +95,6 @@ def from_input_values( concept_vo.validate( activity_subgroup_exists_by_name_callback=concept_exists_by_library_and_name_callback, - activity_group_exists=activity_group_exists, library_name=library.name, ) @@ -139,19 +117,15 @@ def edit_draft( concept_exists_by_library_and_name_callback: Callable[ [str, str], bool ] = lambda x, y: True, - activity_group_exists: Callable[[str], bool] = lambda _: True, - perform_validation: bool = True, ) -> None: """ Creates a new draft version for the object. """ - if perform_validation: - concept_vo.validate( - activity_subgroup_exists_by_name_callback=concept_exists_by_library_and_name_callback, - activity_group_exists=activity_group_exists, - previous_name=self.name, - library_name=self.library.name, - ) + concept_vo.validate( + activity_subgroup_exists_by_name_callback=concept_exists_by_library_and_name_callback, + previous_name=self.name, + library_name=self.library.name, + ) if self._concept_vo != concept_vo: super()._edit_draft( change_description=change_description, author_id=author_id diff --git a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item.py b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item.py index 773c5165..323d44ad 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/concepts/odms/item.py @@ -39,6 +39,7 @@ class OdmItemVO(ConceptVO): vendor_attribute_uids: list[str] vendor_element_uids: list[str] vendor_element_attribute_uids: list[str] + activity_instances: list[dict[str, Any]] @classmethod def from_repository_values( @@ -61,6 +62,7 @@ def from_repository_values( vendor_element_uids: list[str], vendor_attribute_uids: list[str], vendor_element_attribute_uids: list[str], + activity_instances: list[dict[str, Any]], ) -> Self: return cls( oid=oid, @@ -81,6 +83,7 @@ def from_repository_values( vendor_element_uids=vendor_element_uids, vendor_attribute_uids=vendor_attribute_uids, vendor_element_attribute_uids=vendor_element_attribute_uids, + activity_instances=activity_instances, name_sentence_case=None, definition=None, abbreviation=None, @@ -97,6 +100,7 @@ def validate( find_all_terms_callback: Callable[ [str], GenericFilteringReturn[CTTermNameAR] | None ], + find_activity_instance_callback: Callable[[str], Any] = lambda _: None, odm_uid: str | None = None, library_name: str | None = None, ) -> None: @@ -156,6 +160,27 @@ def validate( msg=f"Term with UID '{term_uid}' doesn't belong to the specified Codelist with UID '{self.codelist_uid}'.", ) + if self.activity_instances: + for activity_instance in self.activity_instances: + db_activity_instance = find_activity_instance_callback( + activity_instance["activity_instance_uid"] + ) + + if not db_activity_instance: + raise BusinessLogicException( + msg=f"ODM Item tried to connect to non-existent Activity Instance with UID '{activity_instance["activity_instance_uid"]}'." + ) + + if activity_instance["activity_item_class_uid"] not in [ + activity_item.activity_item_class_uid + for activity_item in db_activity_instance.concept_vo.activity_items + ]: + raise BusinessLogicException( + msg=( + f"Activity Item Class with UID '{activity_instance["activity_item_class_uid"]}' isn't present in Activity Instance with UID '{activity_instance["activity_instance_uid"]}'" + ) + ) + @dataclass class OdmItemAR(OdmARBase): @@ -243,6 +268,7 @@ def edit_draft( find_all_terms_callback: Callable[ [str], GenericFilteringReturn[CTTermNameAR] | None ] = lambda _: None, + find_activity_instance_callback: Callable[[str], Any] = lambda _: None, ) -> None: """ Creates a new draft version for the object. @@ -252,6 +278,7 @@ def edit_draft( unit_definition_exists_by_callback=unit_definition_exists_by_callback, find_codelist_attribute_callback=find_codelist_attribute_callback, find_all_terms_callback=find_all_terms_callback, + find_activity_instance_callback=find_activity_instance_callback, odm_uid=self.uid, ) diff --git a/clinical-mdr-api/clinical_mdr_api/domains/iso_languages.py b/clinical-mdr-api/clinical_mdr_api/domains/iso_languages.py index ea451173..a1e90774 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/iso_languages.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/iso_languages.py @@ -15,7 +15,7 @@ codes for each variety of an ISO 639 macrolanguage. """ -from typing import Sequence +from typing import Any _NAMES = "names" _639_1 = "639-1" @@ -29,252 +29,314 @@ _639_1: "ab", _639_2T: "abk", _639_2B: "abk", - _639_3: ["abk"], + _639_3: { + "abk": "Abkhazian", + }, }, { _NAMES: ["Afar"], _639_1: "aa", _639_2T: "aar", _639_2B: "aar", - _639_3: ["aar"], + _639_3: { + "aar": "Afar", + }, }, { _NAMES: ["Afrikaans"], - _639_1: "akas", + _639_1: "af", _639_2T: "afr", _639_2B: "afr", - _639_3: ["afr"], + _639_3: { + "afr": "Afrikaans", + }, }, { _NAMES: ["Akan"], _639_1: "ak", _639_2T: "aka", _639_2B: "aka", - _639_3: ["aka", "fat", "twi"], + _639_3: {"aka": "Akan", "fat": "Fanti", "twi": "Twi"}, }, { _NAMES: ["Albanian"], _639_1: "sq", _639_2T: "sqi", _639_2B: "alb", - _639_3: ["sqi", "aae", "aat", "aln", "als"], + _639_3: { + "sqi": "Albanian", + "aae": "Arbëreshë Albanian", + "aat": "Arvanitika Albanian", + "aln": "Gheg Albanian", + "als": "Tosk Albanian", + }, }, { _NAMES: ["Amharic"], _639_1: "am", _639_2T: "amh", _639_2B: "amh", - _639_3: ["amh"], + _639_3: { + "amh": "Amharic", + }, }, { _NAMES: ["Arabic"], _639_1: "ar", _639_2T: "ara", _639_2B: "ara", - _639_3: [ - "ara", - "aao", - "abh", - "abv", - "acm", - "acq", - "acw", - "acx", - "acy", - "adf", - "aeb", - "aec", - "afb", - "ajp", - "apc", - "apd", - "arb", - "arq", - "ars", - "ary", - "arz", - "auz", - "avl", - "ayh", - "ayl", - "ayn", - "ayp", - "pga", - "shu", - "ssh", - ], + _639_3: { + "ara": "Arabic", + "aao": "Algerian Saharan Arabic", + "abh": "Tajiki Arabic", + "abv": "Baharna Arabic", + "acm": "Mesopotamian Arabic", + "acq": "Ta'izzi-Adeni Arabic", + "acw": "Hijazi Arabic", + "acx": "Omani Arabic", + "acy": "Cypriot Arabic", + "adf": "Dhofari Arabic", + "aeb": "Tunisian Arabic", + "aec": "Saidi Arabic", + "afb": "Gulf Arabic", + "apc": "Levantine Arabic", + "apd": "Sudanese Arabic", + "arb": "Standard Arabic", + "arq": "Algerian Arabic", + "ars": "Najdi Arabic", + "ary": "Moroccan Arabic", + "arz": "Egyptian Arabic", + "auz": "Uzbeki Arabic", + "avl": "Eastern Egyptian Bedawi Arabic", + "ayh": "Hadrami Arabic", + "ayl": "Libyan Arabic", + "ayn": "Sanaani Arabic", + "ayp": "North Mesopotamian Arabic", + "pga": "Sudanese Creole Arabic", + "shu": "Chadian Arabic", + "ssh": "Shihhi Arabic", + }, }, { _NAMES: ["Aragonese"], _639_1: "an", _639_2T: "arg", _639_2B: "arg", - _639_3: ["arg"], + _639_3: { + "arg": "Aragonese", + }, }, { _NAMES: ["Armenian"], _639_1: "hy", _639_2T: "hye", _639_2B: "arm", - _639_3: ["hye"], + _639_3: { + "hye": "Armenian", + }, }, { _NAMES: ["Assamese"], _639_1: "as", _639_2T: "asm", _639_2B: "asm", - _639_3: ["asm"], + _639_3: { + "asm": "Assamese", + }, }, { _NAMES: ["Avaric"], _639_1: "av", _639_2T: "ava", _639_2B: "ava", - _639_3: ["ava"], + _639_3: { + "ava": "Avaric", + }, }, { _NAMES: ["Avestan"], _639_1: "ae", _639_2T: "ave", _639_2B: "ave", - _639_3: ["ave"], + _639_3: { + "ave": "Avestan", + }, }, { _NAMES: ["Aymara"], _639_1: "ay", _639_2T: "aym", _639_2B: "aym", - _639_3: ["aym", "ayr", "ayc"], + _639_3: { + "aym": "Aymara", + "ayc": "Southern Aymara", + "ayr": "Central Aymara", + }, }, { _NAMES: ["Azerbaijani"], _639_1: "az", _639_2T: "aze", _639_2B: "aze", - _639_3: ["aze", "azj", "azb"], + _639_3: { + "aze": "Azerbaijani", + "azb": "South Azerbaijani", + "azj": "North Azerbaijani", + }, }, { _NAMES: ["Bambara"], _639_1: "bm", _639_2T: "bam", _639_2B: "bam", - _639_3: ["bam"], + _639_3: { + "bam": "Bambara", + }, }, { _NAMES: ["Bashkir"], _639_1: "ba", _639_2T: "bak", _639_2B: "bak", - _639_3: ["bak"], + _639_3: { + "bak": "Bashkir", + }, }, { _NAMES: ["Basque"], _639_1: "eu", _639_2T: "eus", _639_2B: "baq", - _639_3: ["eus"], + _639_3: { + "eus": "Basque", + }, }, { _NAMES: ["Belarusian"], _639_1: "be", _639_2T: "bel", _639_2B: "bel", - _639_3: ["bel"], + _639_3: { + "bel": "Belarusian", + }, }, { _NAMES: ["Bengali"], _639_1: "bn", _639_2T: "ben", _639_2B: "ben", - _639_3: ["ben"], + _639_3: { + "ben": "Bengali", + }, }, { _NAMES: ["Bislama"], _639_1: "bi", _639_2T: "bis", _639_2B: "bis", - _639_3: ["bis"], + _639_3: { + "bis": "Bislama", + }, }, { _NAMES: ["Bosnian"], _639_1: "bs", _639_2T: "bos", _639_2B: "bos", - _639_3: ["bos"], + _639_3: { + "bos": "Bosnian", + }, }, { _NAMES: ["Breton"], _639_1: "br", _639_2T: "bre", _639_2B: "bre", - _639_3: ["bre"], + _639_3: { + "bre": "Breton", + }, }, { _NAMES: ["Bulgarian"], _639_1: "bg", _639_2T: "bul", _639_2B: "bul", - _639_3: ["bul"], + _639_3: { + "bul": "Bulgarian", + }, }, { _NAMES: ["Burmese"], _639_1: "my", _639_2T: "mya", _639_2B: "bur", - _639_3: ["mya"], + _639_3: { + "mya": "Burmese", + }, }, { _NAMES: ["Catalan", "Valencian"], _639_1: "ca", _639_2T: "cat", _639_2B: "cat", - _639_3: ["cat"], + _639_3: { + "cat": "Catalan", + }, }, { _NAMES: ["Chamorro"], _639_1: "ch", _639_2T: "cha", _639_2B: "cha", - _639_3: ["cha"], + _639_3: { + "cha": "Chamorro", + }, }, { _NAMES: ["Chechen"], _639_1: "ce", _639_2T: "che", _639_2B: "che", - _639_3: ["che"], + _639_3: { + "che": "Chechen", + }, }, { _NAMES: ["Chichewa", "Chewa", "Nyanja"], _639_1: "ny", _639_2T: "nya", _639_2B: "nya", - _639_3: ["nya"], + _639_3: { + "nya": "Chichewa", + }, }, { _NAMES: ["Chinese"], _639_1: "zh", _639_2T: "zho", _639_2B: "chi", - _639_3: [ - "zho", - "cdo", - "cjy", - "cmn", - "cnp", - "cpx", - "csp", - "czh", - "czo", - "gan", - "hak", - "hsn", - "lzh", - "mnp", - "nan", - "wuu", - "yue", - ], + _639_3: { + "zho": "Chinese", + "cdo": "Min Dong Chinese", + "cjy": "Jinyu Chinese", + "cmn": "Mandarin Chinese", + "cnp": "Northern Ping Chinese", + "cpx": "Pu-Xian Chinese", + "csp": "Southern Ping Chinese", + "czh": "Huizhou Chinese", + "czo": "Min Zhong Chinese", + "gan": "Gan Chinese", + "hak": "Hakka Chinese", + "hnm": "Hainanese", + "hsn": "Xiang Chinese", + "luh": "Leizhou Chinese", + "lzh": "Literary Chinese", + "mnp": "Min Bei Chinese", + "nan": "Min Nan Chinese", + "sjc": "Shaojiang Chinese", + "wuu": "Wu Chinese", + "yue": "Yue Chinese", + }, }, { _NAMES: [ @@ -287,1178 +349,1547 @@ _639_1: "cu", _639_2T: "chu", _639_2B: "chu", - _639_3: ["chu"], + _639_3: { + "chu": "Old Slavonic", + }, }, { _NAMES: ["Chuvash"], _639_1: "cv", _639_2T: "chv", _639_2B: "chv", - _639_3: ["chv"], + _639_3: { + "chv": "Chuvash", + }, }, { _NAMES: ["Cornish"], _639_1: "kw", _639_2T: "cor", _639_2B: "cor", - _639_3: ["cor"], + _639_3: { + "cor": "Cornish", + }, }, { _NAMES: ["Corsican"], _639_1: "co", _639_2T: "cos", _639_2B: "cos", - _639_3: ["cos"], + _639_3: { + "cos": "Corsican", + }, }, { _NAMES: ["Cree"], _639_1: "cr", _639_2T: "cre", _639_2B: "cre", - _639_3: ["cre", "crm", "crl", "crk", "crj", "csw", "cwd"], + _639_3: { + "cre": "Cree", + "crj": "Southern East Cree", + "crk": "Plains Cree", + "crl": "Northern East Cree", + "crm": "Moose Cree", + "csw": "Swampy Cree", + "cwd": "Woods Cree", + }, }, { _NAMES: ["Croatian"], _639_1: "hr", _639_2T: "hrv", _639_2B: "hrv", - _639_3: ["hrv"], + _639_3: { + "hrv": "Croatian", + }, }, { _NAMES: ["Czech"], _639_1: "cs", _639_2T: "ces", _639_2B: "cze", - _639_3: ["ces"], + _639_3: { + "ces": "Czech", + }, }, { _NAMES: ["Danish"], _639_1: "da", _639_2T: "dan", _639_2B: "dan", - _639_3: ["dan"], + _639_3: { + "dan": "Danish", + }, }, { _NAMES: ["Divehi", "Dhivehi", "Maldivian"], _639_1: "dv", _639_2T: "div", _639_2B: "div", - _639_3: ["div"], + _639_3: { + "div": "Divehi", + }, }, { _NAMES: ["Dutch", "Flemish"], _639_1: "nl", _639_2T: "nld", _639_2B: "dut", - _639_3: ["nld"], + _639_3: { + "nld": "Dutch", + }, }, { _NAMES: ["Dzongkha"], _639_1: "dz", _639_2T: "dzo", _639_2B: "dzo", - _639_3: ["dzo"], + _639_3: { + "dzo": "Dzongkha", + }, }, { _NAMES: ["English"], _639_1: "en", _639_2T: "eng", _639_2B: "eng", - _639_3: ["eng"], + _639_3: { + "eng": "English", + }, }, { _NAMES: ["Esperanto"], _639_1: "eo", _639_2T: "epo", _639_2B: "epo", - _639_3: ["epo"], + _639_3: { + "epo": "Esperanto", + }, }, { _NAMES: ["Estonian"], _639_1: "et", _639_2T: "est", _639_2B: "est", - _639_3: ["est", "ekk", "vro"], + _639_3: { + "est": "Estonian", + "ekk": "Standard Estonian", + "vro": "Võro", + }, }, { _NAMES: ["Ewe"], _639_1: "ee", _639_2T: "ewe", _639_2B: "ewe", - _639_3: ["ewe"], + _639_3: { + "ewe": "Ewe", + }, }, { _NAMES: ["Faroese"], _639_1: "fo", _639_2T: "fao", _639_2B: "fao", - _639_3: ["fao"], + _639_3: { + "fao": "Faroese", + }, }, { _NAMES: ["Fijian"], _639_1: "fj", _639_2T: "fij", _639_2B: "fij", - _639_3: ["fij"], + _639_3: { + "fij": "Fijian", + }, }, { _NAMES: ["Finnish"], _639_1: "fi", _639_2T: "fin", _639_2B: "fin", - _639_3: ["fin"], + _639_3: { + "fin": "Finnish", + }, }, { _NAMES: ["French"], _639_1: "fr", _639_2T: "fra", _639_2B: "fre", - _639_3: ["fra"], + _639_3: { + "fra": "French", + }, }, { _NAMES: ["Western Frisian"], _639_1: "fy", _639_2T: "fry", _639_2B: "fry", - _639_3: ["fry"], + _639_3: { + "fry": "Western Frisian", + }, }, { _NAMES: ["Fulah"], _639_1: "ff", _639_2T: "ful", _639_2B: "ful", - _639_3: ["ful", "fub", "fui", "fue", "fuq", "ffm", "fuv", "fuc", "fuf", "fuh"], + _639_3: { + "ful": "Fulah", + "ffm": "Maasina Fulfulde", + "fub": "Adamawa Fulfulde", + "fuc": "Pulaar", + "fue": "Borgu Fulfulde", + "fuf": "Pular", + "fuh": "Western Niger Fulfulde", + "fui": "Bagirmi Fulfulde", + "fuq": "Central-Eastern Niger Fulfulde", + "fuv": "Nigerian Fulfulde", + }, }, { _NAMES: ["Gaelic", "Scottish Gaelic"], _639_1: "gd", _639_2T: "gla", _639_2B: "gla", - _639_3: ["gla"], + _639_3: { + "gla": "Gaelic", + }, }, { _NAMES: ["Galician"], _639_1: "gl", _639_2T: "glg", _639_2B: "glg", - _639_3: ["glg"], + _639_3: { + "glg": "Galician", + }, }, { _NAMES: ["Ganda"], _639_1: "lg", _639_2T: "lug", _639_2B: "lug", - _639_3: ["lug"], + _639_3: { + "lug": "Ganda", + }, }, { _NAMES: ["Georgian"], _639_1: "ka", _639_2T: "kat", _639_2B: "geo", - _639_3: ["kat"], + _639_3: { + "kat": "Georgian", + }, }, { _NAMES: ["German"], _639_1: "de", _639_2T: "deu", _639_2B: "ger", - _639_3: ["deu"], + _639_3: { + "deu": "German", + }, }, { - _NAMES: ["Greek", "Modern (1453–)"], + _NAMES: ["Greek", "Modern (1453-)"], _639_1: "el", _639_2T: "ell", _639_2B: "gre", - _639_3: ["ell"], + _639_3: { + "ell": "Greek", + }, }, { _NAMES: ["Kalaallisut", "Greenlandic"], _639_1: "kl", _639_2T: "kal", _639_2B: "kal", - _639_3: ["kal"], + _639_3: { + "kal": "Kalaallisut", + }, }, { _NAMES: ["Guarani"], _639_1: "gn", _639_2T: "grn", _639_2B: "grn", - _639_3: ["grn", "nhd", "gui", "gun", "gug", "gnw"], + _639_3: { + "grn": "Guarani", + "gnw": "Western Bolivian Guaraní", + "gug": "Paraguayan Guaraní", + "gui": "Eastern Bolivian Guaraní", + "gun": "Mbyá Guaraní", + "nhd": "Chiripá", + }, }, { _NAMES: ["Gujarati"], _639_1: "gu", _639_2T: "guj", _639_2B: "guj", - _639_3: ["guj"], + _639_3: { + "guj": "Gujarati", + }, }, { _NAMES: ["Haitian", "Haitian Creole"], _639_1: "ht", _639_2T: "hat", _639_2B: "hat", - _639_3: ["hat"], + _639_3: { + "hat": "Haitian", + }, }, { _NAMES: ["Hausa"], _639_1: "ha", _639_2T: "hau", _639_2B: "hau", - _639_3: ["hau"], + _639_3: { + "hau": "Hausa", + }, }, { _NAMES: ["Hebrew"], _639_1: "he", _639_2T: "heb", _639_2B: "heb", - _639_3: ["heb"], + _639_3: { + "heb": "Hebrew", + }, }, { _NAMES: ["Herero"], _639_1: "hz", _639_2T: "her", _639_2B: "her", - _639_3: ["her"], + _639_3: { + "her": "Herero", + }, }, { _NAMES: ["Hindi"], _639_1: "hi", _639_2T: "hin", _639_2B: "hin", - _639_3: ["hin"], + _639_3: { + "hin": "Hindi", + }, }, { _NAMES: ["Hiri Motu"], _639_1: "ho", _639_2T: "hmo", _639_2B: "hmo", - _639_3: ["hmo"], + _639_3: { + "hmo": "Hiri Motu", + }, }, { _NAMES: ["Hungarian"], _639_1: "hu", _639_2T: "hun", _639_2B: "hun", - _639_3: ["hun"], + _639_3: { + "hun": "Hungarian", + }, }, { _NAMES: ["Icelandic"], _639_1: "is", _639_2T: "isl", _639_2B: "ice", - _639_3: ["isl"], + _639_3: { + "isl": "Icelandic", + }, }, { _NAMES: ["Ido"], _639_1: "io", _639_2T: "ido", _639_2B: "ido", - _639_3: ["ido"], + _639_3: { + "ido": "Ido", + }, }, { _NAMES: ["Igbo"], _639_1: "ig", _639_2T: "ibo", _639_2B: "ibo", - _639_3: ["ibo"], + _639_3: { + "ibo": "Igbo", + }, }, { _NAMES: ["Indonesian"], _639_1: "id", _639_2T: "ind", _639_2B: "ind", - _639_3: ["ind"], + _639_3: { + "ind": "Indonesian", + }, }, { _NAMES: ["Interlingua (International Auxiliary Language Association)"], _639_1: "ia", _639_2T: "ina", _639_2B: "ina", - _639_3: ["ina"], + _639_3: { + "ina": "Interlingua (International Auxiliary Language Association)", + }, }, { _NAMES: ["Interlingue", "Occidental"], _639_1: "ie", _639_2T: "ile", _639_2B: "ile", - _639_3: ["ile"], + _639_3: { + "ile": "Interlingue", + }, }, { _NAMES: ["Inuktitut"], _639_1: "iu", _639_2T: "iku", _639_2B: "iku", - _639_3: ["iku", "ike", "ikt"], + _639_3: { + "iku": "Inuktitut", + "ike": "Eastern Canadian Inuktitut", + "ikt": "Inuinnaqtun", + }, }, { _NAMES: ["Inupiaq"], _639_1: "ik", _639_2T: "ipk", _639_2B: "ipk", - _639_3: ["ipk", "esi", "esk"], + _639_3: { + "ipk": "Inupiaq", + "esi": "North Alaskan Inupiatun", + "esk": "Northwest Alaska Inupiatun", + }, }, { _NAMES: ["Irish"], _639_1: "ga", _639_2T: "gle", _639_2B: "gle", - _639_3: ["gle"], + _639_3: { + "gle": "Irish", + }, }, { _NAMES: ["Italian"], _639_1: "it", _639_2T: "ita", _639_2B: "ita", - _639_3: ["ita"], + _639_3: { + "ita": "Italian", + }, }, { _NAMES: ["Japanese"], _639_1: "ja", _639_2T: "jpn", _639_2B: "jpn", - _639_3: ["jpn"], + _639_3: { + "jpn": "Japanese", + }, }, { _NAMES: ["Javanese"], _639_1: "jv", _639_2T: "jav", _639_2B: "jav", - _639_3: ["jav"], + _639_3: { + "jav": "Javanese", + }, }, { _NAMES: ["Kannada"], _639_1: "kn", _639_2T: "kan", _639_2B: "kan", - _639_3: ["kan"], + _639_3: { + "kan": "Kannada", + }, }, { _NAMES: ["Kanuri"], _639_1: "kr", _639_2T: "kau", _639_2B: "kau", - _639_3: ["kau", "knc", "kby", "krt"], + _639_3: { + "kau": "Kanuri", + "kby": "Manga Kanuri", + "knc": "Central Kanuri", + "krt": "Tumari Kanuri", + }, }, { _NAMES: ["Kashmiri"], _639_1: "ks", _639_2T: "kas", _639_2B: "kas", - _639_3: ["kas"], + _639_3: { + "kas": "Kashmiri", + }, }, { _NAMES: ["Kazakh"], _639_1: "kk", _639_2T: "kaz", _639_2B: "kaz", - _639_3: ["kaz"], + _639_3: { + "kaz": "Kazakh", + }, }, { _NAMES: ["Central Khmer"], _639_1: "km", _639_2T: "khm", _639_2B: "khm", - _639_3: ["khm"], + _639_3: { + "khm": "Central Khmer", + }, }, { _NAMES: ["Kikuyu", "Gikuyu"], _639_1: "ki", _639_2T: "kik", _639_2B: "kik", - _639_3: ["kik"], + _639_3: { + "kik": "Kikuyu", + }, }, { _NAMES: ["Kinyarwanda"], _639_1: "rw", _639_2T: "kin", _639_2B: "kin", - _639_3: ["kin"], + _639_3: { + "kin": "Kinyarwanda", + }, }, { _NAMES: ["Kirghiz", "Kyrgyz"], _639_1: "ky", _639_2T: "kir", _639_2B: "kir", - _639_3: ["kir"], + _639_3: { + "kir": "Kirghiz", + }, }, { _NAMES: ["Komi"], _639_1: "kv", _639_2T: "kom", _639_2B: "kom", - _639_3: ["kom", "koi", "kpv"], + _639_3: { + "kom": "Komi", + "koi": "Komi-Permyak", + "kpv": "Komi-Zyrian", + }, }, { _NAMES: ["Kongo"], _639_1: "kg", _639_2T: "kon", _639_2B: "kon", - _639_3: ["kon", "kng", "ldi", "kwy"], + _639_3: { + "kon": "Kongo", + "kng": "Koongo", + "kwy": "San Salvador Kongo", + "ldi": "Laari", + }, }, { _NAMES: ["Korean"], _639_1: "ko", _639_2T: "kor", _639_2B: "kor", - _639_3: ["kor"], + _639_3: { + "kor": "Korean", + }, }, { _NAMES: ["Kuanyama", "Kwanyama"], _639_1: "kj", _639_2T: "kua", _639_2B: "kua", - _639_3: ["kua"], + _639_3: { + "kua": "Kuanyama", + }, }, { _NAMES: ["Kurdish"], _639_1: "ku", _639_2T: "kur", _639_2B: "kur", - _639_3: ["kur", "ckb", "kmr", "sdh"], + _639_3: { + "kur": "Kurdish", + "ckb": "Central Kurdish", + "kmr": "Northern Kurdish", + "sdh": "Southern Kurdish", + }, }, { _NAMES: ["Lao"], _639_1: "lo", _639_2T: "lao", _639_2B: "lao", - _639_3: ["lao"], + _639_3: { + "lao": "Lao", + }, }, { _NAMES: ["Latin"], _639_1: "la", _639_2T: "lat", _639_2B: "lat", - _639_3: ["lat"], + _639_3: { + "lat": "Latin", + }, }, { _NAMES: ["Latvian"], _639_1: "lv", _639_2T: "lav", _639_2B: "lav", - _639_3: ["lav", "ltg", "lvs"], + _639_3: { + "lav": "Latvian", + "ltg": "Latgalian", + "lvs": "Standard Latvian", + }, }, { _NAMES: ["Limburgan", "Limburger", "Limburgish"], _639_1: "li", _639_2T: "lim", _639_2B: "lim", - _639_3: ["lim"], + _639_3: { + "lim": "Limburgan", + }, }, { _NAMES: ["Lingala"], _639_1: "ln", _639_2T: "lin", _639_2B: "lin", - _639_3: ["lin"], + _639_3: { + "lin": "Lingala", + }, }, { _NAMES: ["Lithuanian"], _639_1: "lt", _639_2T: "lit", _639_2B: "lit", - _639_3: ["lit"], + _639_3: { + "lit": "Lithuanian", + }, }, { _NAMES: ["Luba-Katanga"], _639_1: "lu", _639_2T: "lub", _639_2B: "lub", - _639_3: ["lub"], + _639_3: { + "lub": "Luba-Katanga", + }, }, { _NAMES: ["Luxembourgish", "Letzeburgesch"], _639_1: "lb", _639_2T: "ltz", _639_2B: "ltz", - _639_3: ["ltz"], + _639_3: { + "ltz": "Luxembourgish", + }, }, { _NAMES: ["Macedonian"], _639_1: "mk", _639_2T: "mkd", _639_2B: "mac", - _639_3: ["mkd"], + _639_3: { + "mkd": "Macedonian", + }, }, { _NAMES: ["Malagasy"], _639_1: "mg", _639_2T: "mlg", _639_2B: "mlg", - _639_3: [ - "mlg", - "xmv", - "bhr", - "msh", - "bmm", - "plt", - "skg", - "bzc", - "tkg", - "tdx", - "txy", - "xmw", - ], + _639_3: { + "mlg": "Malagasy", + "bhr": "Bara Malagasy", + "bmm": "Northern Betsimisaraka Malagasy", + "bzc": "Southern Betsimisaraka Malagasy", + "msh": "Masikoro Malagasy", + "plt": "Plateau Malagasy", + "skg": "Sakalava Malagasy", + "tdx": "Tandroy-Mahafaly Malagasy", + "tkg": "Tesaka Malagasy", + "txy": "Tanosy Malagasy", + "xmv": "Antankarana Malagasy", + "xmw": "Tsimihety Malagasy", + }, }, { _NAMES: ["Malay"], _639_1: "ms", _639_2T: "msa", _639_2B: "may", - _639_3: [ - "msa", - "btj", - "mfb", - "bjn", - "bve", - "kxd", - "bvu", - "pse", - "coa", - "liw", - "dup", - "hji", - "ind", - "jak", - "jax", - "vkk", - "meo", - "kvr", - "mqg", - "kvb", - "lce", - "lcf", - "zlm", - "xmm", - "min", - "mui", - "zmi", - "max", - "orn", - "ors", - "mfa", - "pel", - "msi", - "zsm", - "tmw", - "vkt", - "urk", - ], + _639_3: { + "msa": "Malay", + "bjn": "Banjar", + "btj": "Bacanese Malay", + "bve": "Berau Malay", + "bvu": "Bukit Malay", + "coa": "Cocos Islands Malay", + "dup": "Duano", + "hji": "Haji", + "ind": "Indonesian", + "jak": "Jakun", + "jax": "Jambi Malay", + "kvb": "Kubu", + "kvr": "Kerinci", + "kxd": "Brunei", + "lce": "Loncong", + "lcf": "Lubu", + "liw": "Col", + "max": "North Moluccan Malay", + "meo": "Kedah Malay", + "mfa": "Pattani Malay", + "mfb": "Bangka", + "min": "Minangkabau", + "mqg": "Kota Bangun Kutai Malay", + "msi": "Sabah Malay", + "mui": "Musi", + "orn": "Orang Kanaq", + "ors": "Orang Seletar", + "pel": "Pekal", + "pse": "Central Malay", + "tmw": "Temuan", + "urk": "Urak Lawoi'", + "vkk": "Kaur", + "vkt": "Tenggarong Kutai Malay", + "xmm": "Manado Malay", + "zlm": "Malay (individual language)", + "zmi": "Negeri Sembilan Malay", + "zsm": "Standard Malay", + }, }, { _NAMES: ["Malayalam"], _639_1: "ml", _639_2T: "mal", _639_2B: "mal", - _639_3: ["mal"], + _639_3: { + "mal": "Malayalam", + }, }, { _NAMES: ["Maltese"], _639_1: "mt", _639_2T: "mlt", _639_2B: "mlt", - _639_3: ["mlt"], + _639_3: { + "mlt": "Maltese", + }, }, { _NAMES: ["Manx"], _639_1: "gv", _639_2T: "glv", _639_2B: "glv", - _639_3: ["glv"], + _639_3: { + "glv": "Manx", + }, }, { _NAMES: ["Maori"], _639_1: "mi", _639_2T: "mri", _639_2B: "mao", - _639_3: ["mri"], + _639_3: { + "mri": "Maori", + }, }, { _NAMES: ["Marathi"], _639_1: "mr", _639_2T: "mar", _639_2B: "mar", - _639_3: ["mar"], + _639_3: { + "mar": "Marathi", + }, }, { _NAMES: ["Marshallese"], _639_1: "mh", _639_2T: "mah", _639_2B: "mah", - _639_3: ["mah"], + _639_3: { + "mah": "Marshallese", + }, }, { _NAMES: ["Mongolian"], _639_1: "mn", _639_2T: "mon", _639_2B: "mon", - _639_3: ["mon", "khk", "mvf"], + _639_3: { + "mon": "Mongolian", + "khk": "Halh Mongolian", + "mvf": "Peripheral Mongolian", + }, }, { _NAMES: ["Nauru"], _639_1: "na", _639_2T: "nau", _639_2B: "nau", - _639_3: ["nau"], + _639_3: { + "nau": "Nauru", + }, }, { _NAMES: ["Navajo", "Navaho"], _639_1: "nv", _639_2T: "nav", _639_2B: "nav", - _639_3: ["nav"], + _639_3: { + "nav": "Navajo", + }, }, { _NAMES: ["North Ndebele"], _639_1: "nd", _639_2T: "nde", _639_2B: "nde", - _639_3: ["nde"], + _639_3: { + "nde": "North Ndebele", + }, }, { _NAMES: ["South Ndebele"], _639_1: "nr", _639_2T: "nbl", _639_2B: "nbl", - _639_3: ["nbl"], + _639_3: { + "nbl": "South Ndebele", + }, }, { _NAMES: ["Ndonga"], _639_1: "ng", _639_2T: "ndo", _639_2B: "ndo", - _639_3: ["ndo"], + _639_3: { + "ndo": "Ndonga", + }, }, { _NAMES: ["Nepali"], _639_1: "ne", _639_2T: "nep", _639_2B: "nep", - _639_3: ["nep", "dty", "npi"], + _639_3: { + "nep": "Nepali", + "dty": "Dotyali", + "npi": "Nepali", + }, }, { _NAMES: ["Norwegian"], _639_1: "no", _639_2T: "nor", _639_2B: "nor", - _639_3: ["nor", "nob", "nno"], + _639_3: { + "nor": "Norwegian", + "nno": "Norwegian Nynorsk", + "nob": "Norwegian Bokmål", + }, }, { _NAMES: ["Sichuan Yi", "Nuosu"], _639_1: "ii", _639_2T: "iii", _639_2B: "iii", - _639_3: ["iii"], + _639_3: { + "iii": "Sichuan Yi", + }, }, { _NAMES: ["Occitan"], _639_1: "oc", _639_2T: "oci", _639_2B: "oci", - _639_3: ["oci"], + _639_3: { + "oci": "Occitan", + }, }, { _NAMES: ["Ojibwa"], _639_1: "oj", _639_2T: "oji", _639_2B: "oji", - _639_3: ["oji", "ciw", "ojb", "ojc", "ojg", "ojs", "ojw", "otw"], + _639_3: { + "oji": "Ojibwa", + "ciw": "Chippewa", + "ojb": "Northwestern Ojibwa", + "ojc": "Central Ojibwa", + "ojg": "Eastern Ojibwa", + "ojs": "Severn Ojibwa", + "ojw": "Western Ojibwa", + "otw": "Ottawa", + }, }, { _NAMES: ["Oriya"], _639_1: "or", _639_2T: "ori", _639_2B: "ori", - _639_3: ["ori", "ory", "spv"], + _639_3: { + "ori": "Oriya", + "ory": "Odia", + "spv": "Sambalpuri", + }, }, { _NAMES: ["Oromo"], _639_1: "om", _639_2T: "orm", _639_2B: "orm", - _639_3: ["orm", "gax", "hae", "orc", "gaz"], + _639_3: { + "orm": "Oromo", + "gax": "Borana-Arsi-Guji Oromo", + "gaz": "West Central Oromo", + "hae": "Eastern Oromo", + "orc": "Orma", + }, }, { _NAMES: ["Ossetian", "Ossetic"], _639_1: "os", _639_2T: "oss", _639_2B: "oss", - _639_3: ["oss"], + _639_3: { + "oss": "Ossetian", + }, }, { _NAMES: ["Pali"], _639_1: "pi", _639_2T: "pli", _639_2B: "pli", - _639_3: ["pli"], + _639_3: { + "pli": "Pali", + }, }, { _NAMES: ["Pashto", "Pushto"], _639_1: "ps", _639_2T: "pus", _639_2B: "pus", - _639_3: ["pus", "pst", "pbu", "pbt"], + _639_3: { + "pus": "Pashto", + "pbt": "Southern Pashto", + "pbu": "Northern Pashto", + "pst": "Central Pashto", + }, }, { _NAMES: ["Persian"], _639_1: "fa", _639_2T: "fas", _639_2B: "per", - _639_3: ["fas", "prs", "pes"], + _639_3: { + "fas": "Persian", + "pes": "Iranian Persian", + "prs": "Dari", + }, }, { _NAMES: ["Polish"], _639_1: "pl", _639_2T: "pol", _639_2B: "pol", - _639_3: ["pol"], + _639_3: { + "pol": "Polish", + }, }, { _NAMES: ["Portuguese"], _639_1: "pt", _639_2T: "por", _639_2B: "por", - _639_3: ["por"], + _639_3: { + "por": "Portuguese", + }, }, { _NAMES: ["Punjabi", "Panjabi"], _639_1: "pa", _639_2T: "pan", _639_2B: "pan", - _639_3: ["pan"], + _639_3: { + "pan": "Punjabi", + }, }, { _NAMES: ["Quechua"], _639_1: "qu", _639_2T: "que", _639_2B: "que", - _639_3: [ - "que", - "qva", - "qxu", - "quy", - "qvc", - "qvl", - "qud", - "qxr", - "quk", - "qug", - "qxc", - "qxa", - "qwc", - "qwa", - "quz", - "qve", - "qub", - "qvh", - "qwh", - "qvw", - "qvi", - "qxw", - "quf", - "qvj", - "qvm", - "qvo", - "qul", - "qvn", - "qxn", - "qvz", - "qvp", - "qxh", - "qxp", - "qxl", - "qvs", - "qxt", - "qus", - "qws", - "quh", - "qxo", - "qup", - "quw", - "qur", - "qux", - ], + _639_3: { + "que": "Quechua", + "qub": "Huallaga Huánuco Quechua", + "qud": "Calderón Highland Quichua", + "quf": "Lambayeque Quechua", + "qug": "Chimborazo Highland Quichua", + "quh": "South Bolivian Quechua", + "quk": "Chachapoyas Quechua", + "qul": "North Bolivian Quechua", + "qup": "Southern Pastaza Quechua", + "qur": "Yanahuanca Pasco Quechua", + "qus": "Santiago del Estero Quichua", + "quw": "Tena Lowland Quichua", + "qux": "Yauyos Quechua", + "quy": "Ayacucho Quechua", + "quz": "Cusco Quechua", + "qva": "Ambo-Pasco Quechua", + "qvc": "Cajamarca Quechua", + "qve": "Eastern Apurímac Quechua", + "qvh": "Huamalíes-Dos de Mayo Huánuco Quechua", + "qvi": "Imbabura Highland Quichua", + "qvj": "Loja Highland Quichua", + "qvl": "Cajatambo North Lima Quechua", + "qvm": "Margos-Yarowilca-Lauricocha Quechua", + "qvn": "North Junín Quechua", + "qvo": "Napo Lowland Quechua", + "qvp": "Pacaraos Quechua", + "qvs": "San Martín Quechua", + "qvw": "Huaylla Wanca Quechua", + "qvz": "Northern Pastaza Quichua", + "qwa": "Corongo Ancash Quechua", + "qwc": "Classical Quechua", + "qwh": "Huaylas Ancash Quechua", + "qws": "Sihuas Ancash Quechua", + "qxa": "Chiquián Ancash Quechua", + "qxc": "Chincha Quechua", + "qxh": "Panao Huánuco Quechua", + "qxl": "Salasaca Highland Quichua", + "qxn": "Northern Conchucos Ancash Quechua", + "qxo": "Southern Conchucos Ancash Quechua", + "qxp": "Puno Quechua", + "qxr": "Cañar Highland Quichua", + "qxt": "Santa Ana de Tusi Pasco Quechua", + "qxu": "Arequipa-La Unión Quechua", + "qxw": "Jauja Wanca Quechua", + }, }, { _NAMES: ["Romanian", "Moldavian", "Moldovan"], _639_1: "ro", _639_2T: "ron", _639_2B: "rum", - _639_3: ["ron"], + _639_3: { + "ron": "Romanian", + }, }, { _NAMES: ["Romansh"], _639_1: "rm", _639_2T: "roh", _639_2B: "roh", - _639_3: ["roh"], + _639_3: { + "roh": "Romansh", + }, }, { _NAMES: ["Rundi"], _639_1: "rn", _639_2T: "run", _639_2B: "run", - _639_3: ["run"], + _639_3: { + "run": "Rundi", + }, }, { _NAMES: ["Russian"], _639_1: "ru", _639_2T: "rus", _639_2B: "rus", - _639_3: ["rus"], + _639_3: { + "rus": "Russian", + }, }, { _NAMES: ["Northern Sami"], _639_1: "se", _639_2T: "sme", _639_2B: "sme", - _639_3: ["sme"], + _639_3: { + "sme": "Northern Sami", + }, }, { _NAMES: ["Samoan"], _639_1: "sm", _639_2T: "smo", _639_2B: "smo", - _639_3: ["smo"], + _639_3: { + "smo": "Samoan", + }, }, { _NAMES: ["Sango"], _639_1: "sg", _639_2T: "sag", _639_2B: "sag", - _639_3: ["sag"], + _639_3: { + "sag": "Sango", + }, }, { _NAMES: ["Sanskrit"], _639_1: "sa", _639_2T: "san", _639_2B: "san", - _639_3: ["san"], + _639_3: { + "san": "Sanskrit", + }, }, { _NAMES: ["Sardinian"], _639_1: "sc", _639_2T: "srd", _639_2B: "srd", - _639_3: ["srd", "sro", "sdn", "src", "sdc"], + _639_3: { + "srd": "Sardinian", + "sdc": "Sassarese Sardinian", + "sdn": "Gallurese Sardinian", + "src": "Logudorese Sardinian", + "sro": "Campidanese Sardinian", + }, }, { _NAMES: ["Serbian"], _639_1: "sr", _639_2T: "srp", _639_2B: "srp", - _639_3: ["srp"], + _639_3: { + "srp": "Serbian", + }, }, { _NAMES: ["Shona"], _639_1: "sn", _639_2T: "sna", _639_2B: "sna", - _639_3: ["sna"], + _639_3: { + "sna": "Shona", + }, }, { _NAMES: ["Sindhi"], _639_1: "sd", _639_2T: "snd", _639_2B: "snd", - _639_3: ["snd"], + _639_3: { + "snd": "Sindhi", + }, }, { _NAMES: ["Sinhala", "Sinhalese"], _639_1: "si", _639_2T: "sin", _639_2B: "sin", - _639_3: ["sin"], + _639_3: { + "sin": "Sinhala", + }, }, { _NAMES: ["Slovak"], _639_1: "sk", _639_2T: "slk", _639_2B: "slo", - _639_3: ["slk"], + _639_3: { + "slk": "Slovak", + }, }, { _NAMES: ["Slovenian"], _639_1: "sl", _639_2T: "slv", _639_2B: "slv", - _639_3: ["slv"], + _639_3: { + "slv": "Slovenian", + }, }, { _NAMES: ["Somali"], _639_1: "so", _639_2T: "som", _639_2B: "som", - _639_3: ["som"], + _639_3: { + "som": "Somali", + }, }, { _NAMES: ["Southern Sotho"], _639_1: "st", _639_2T: "sot", _639_2B: "sot", - _639_3: ["sot"], + _639_3: { + "sot": "Southern", + }, }, { _NAMES: ["Spanish", "Castilian"], _639_1: "es", _639_2T: "spa", _639_2B: "spa", - _639_3: ["spa"], + _639_3: { + "spa": "Spanish", + }, }, { _NAMES: ["Sundanese"], _639_1: "su", _639_2T: "sun", _639_2B: "sun", - _639_3: ["sun"], + _639_3: { + "sun": "Sundanese", + }, }, { _NAMES: ["Swahili"], _639_1: "sw", _639_2T: "swa", _639_2B: "swa", - _639_3: ["swa", "swc", "swh"], + _639_3: { + "swa": "Swahili", + "swc": "Congo Swahili", + "swh": "Swahili", + }, }, { _NAMES: ["Swati"], _639_1: "ss", _639_2T: "ssw", _639_2B: "ssw", - _639_3: ["ssw"], + _639_3: { + "ssw": "Swati", + }, }, { _NAMES: ["Swedish"], _639_1: "sv", _639_2T: "swe", _639_2B: "swe", - _639_3: ["swe"], + _639_3: { + "swe": "Swedish", + }, }, { _NAMES: ["Tagalog"], _639_1: "tl", _639_2T: "tgl", _639_2B: "tgl", - _639_3: ["tgl"], + _639_3: { + "tgl": "Tagalog", + }, }, { _NAMES: ["Tahitian"], _639_1: "ty", _639_2T: "tah", _639_2B: "tah", - _639_3: ["tah"], + _639_3: { + "tah": "Tahitian", + }, }, { _NAMES: ["Tajik"], _639_1: "tg", _639_2T: "tgk", _639_2B: "tgk", - _639_3: ["tgk"], + _639_3: { + "tgk": "Tajik", + }, }, { _NAMES: ["Tamil"], _639_1: "ta", _639_2T: "tam", _639_2B: "tam", - _639_3: ["tam"], + _639_3: { + "tam": "Tamil", + }, }, { _NAMES: ["Tatar"], _639_1: "tt", _639_2T: "tat", _639_2B: "tat", - _639_3: ["tat"], + _639_3: { + "tat": "Tatar", + }, }, { _NAMES: ["Telugu"], _639_1: "te", _639_2T: "tel", _639_2B: "tel", - _639_3: ["tel"], + _639_3: { + "tel": "Telugu", + }, }, { _NAMES: ["Thai"], _639_1: "th", _639_2T: "tha", _639_2B: "tha", - _639_3: ["tha"], + _639_3: { + "tha": "Thai", + }, }, { _NAMES: ["Tibetan"], _639_1: "bo", _639_2T: "bod", _639_2B: "tib", - _639_3: ["bod"], + _639_3: { + "bod": "Tibetan", + }, }, { _NAMES: ["Tigrinya"], _639_1: "ti", _639_2T: "tir", _639_2B: "tir", - _639_3: ["tir"], + _639_3: { + "tir": "Tigrinya", + }, }, { _NAMES: ["Tonga (Tonga Islands)"], _639_1: "to", _639_2T: "ton", _639_2B: "ton", - _639_3: ["ton"], + _639_3: { + "ton": "Tonga (Tonga Islands)", + }, }, { _NAMES: ["Tsonga"], _639_1: "ts", _639_2T: "tso", _639_2B: "tso", - _639_3: ["tso"], + _639_3: { + "tso": "Tsonga", + }, }, { _NAMES: ["Tswana"], _639_1: "tn", _639_2T: "tsn", _639_2B: "tsn", - _639_3: ["tsn"], + _639_3: { + "tsn": "Tswana", + }, }, { _NAMES: ["Turkish"], _639_1: "tr", _639_2T: "tur", _639_2B: "tur", - _639_3: ["tur"], + _639_3: { + "tur": "Turkish", + }, }, { _NAMES: ["Turkmen"], _639_1: "tk", _639_2T: "tuk", _639_2B: "tuk", - _639_3: ["tuk"], + _639_3: { + "tuk": "Turkmen", + }, }, { _NAMES: ["Twi"], _639_1: "tw", _639_2T: "twi", _639_2B: "twi", - _639_3: ["twi"], + _639_3: { + "twi": "Twi", + }, }, { _NAMES: ["Uighur", "Uyghur"], _639_1: "ug", _639_2T: "uig", _639_2B: "uig", - _639_3: ["uig"], + _639_3: { + "uig": "Uighur", + }, }, { _NAMES: ["Ukrainian"], _639_1: "uk", _639_2T: "ukr", _639_2B: "ukr", - _639_3: ["ukr"], + _639_3: { + "ukr": "Ukrainian", + }, }, { _NAMES: ["Urdu"], _639_1: "ur", _639_2T: "urd", _639_2B: "urd", - _639_3: ["urd"], + _639_3: { + "urd": "Urdu", + }, }, { _NAMES: ["Uzbek"], _639_1: "uz", _639_2T: "uzb", _639_2B: "uzb", - _639_3: ["uzb", "uzn", "uzs"], + _639_3: { + "uzb": "Uzbek", + "uzn": "Northern Uzbek", + "uzs": "Southern Uzbek", + }, }, { _NAMES: ["Venda"], _639_1: "ve", _639_2T: "ven", _639_2B: "ven", - _639_3: ["ven"], + _639_3: { + "ven": "Venda", + }, }, { _NAMES: ["Vietnamese"], _639_1: "vi", _639_2T: "vie", _639_2B: "vie", - _639_3: ["vie"], + _639_3: { + "vie": "Vietnamese", + }, }, { _NAMES: ["Volapük"], _639_1: "vo", _639_2T: "vol", _639_2B: "vol", - _639_3: ["vol"], + _639_3: { + "vol": "Volapük", + }, }, { _NAMES: ["Walloon"], _639_1: "wa", _639_2T: "wln", _639_2B: "wln", - _639_3: ["wln"], + _639_3: { + "wln": "Walloon", + }, }, { _NAMES: ["Welsh"], _639_1: "cy", _639_2T: "cym", _639_2B: "wel", - _639_3: ["cym"], + _639_3: { + "cym": "Welsh", + }, }, { _NAMES: ["Wolof"], _639_1: "wo", _639_2T: "wol", _639_2B: "wol", - _639_3: ["wol"], + _639_3: { + "wol": "Wolof", + }, }, { _NAMES: ["Xhosa"], _639_1: "xh", _639_2T: "xho", _639_2B: "xho", - _639_3: ["xho"], + _639_3: { + "xho": "Xhosa", + }, }, { _NAMES: ["Yiddish"], _639_1: "yi", _639_2T: "yid", _639_2B: "yid", - _639_3: ["yid", "ydd", "yih"], + _639_3: { + "yid": "Yiddish", + "ydd": "Eastern Yiddish", + "yih": "Western Yiddish", + }, }, { _NAMES: ["Yoruba"], _639_1: "yo", _639_2T: "yor", _639_2B: "yor", - _639_3: ["yor"], + _639_3: { + "yor": "Yoruba", + }, }, { _NAMES: ["Zhuang", "Chuang"], _639_1: "za", _639_2T: "zha", _639_2B: "zha", - _639_3: [ - "zha", - "zch", - "zhd", - "zeh", - "zgb", - "zgn", - "zln", - "zlj", - "zlq", - "zgm", - "zhn", - "zqe", - "zyg", - "zyb", - "zyn", - "zyj", - "zzj", - ], + _639_3: { + "zha": "Zhuang", + "zch": "Central Hongshuihe Zhuang", + "zeh": "Eastern Hongshuihe Zhuang", + "zgb": "Guibei Zhuang", + "zgm": "Minz Zhuang", + "zgn": "Guibian Zhuang", + "zhd": "Dai Zhuang", + "zhn": "Nong Zhuang", + "zlj": "Liujiang Zhuang", + "zln": "Lianshan Zhuang", + "zlq": "Liuqian Zhuang", + "zqe": "Qiubei Zhuang", + "zyb": "Yongbei Zhuang", + "zyg": "Yang Zhuang", + "zyj": "Youjiang Zhuang", + "zyn": "Yongnan Zhuang", + "zzj": "Zuojiang Zhuang", + }, }, { _NAMES: ["Zulu"], _639_1: "zu", _639_2T: "zul", _639_2B: "zul", - _639_3: ["zul"], + _639_3: { + "zul": "Zulu", + }, }, ] @@ -1472,10 +1903,7 @@ key.casefold(): lang for lang in LANGUAGES for key in lang[_639_3] } -LANGUAGES_INDEXED_BY: dict[ - str, - dict[Sequence[str], dict[str, Sequence[str]]] | dict[str, dict[str, Sequence[str]]], -] = { +LANGUAGES_INDEXED_BY: dict[str, dict[Any, dict[str, Any]]] = { _NAMES: LANGUAGES_BY_NAMES, _639_1: LANGUAGES_BY_639_1, _639_2T: LANGUAGES_BY_639_2T, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/registry_identifiers.py b/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/registry_identifiers.py index 34ad9233..e3d2ab7a 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/registry_identifiers.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/registry_identifiers.py @@ -31,6 +31,8 @@ class RegistryIdentifiersVO: eudamed_srn_number_null_value_code: str | None investigational_device_exemption_ide_number: str | None investigational_device_exemption_ide_number_null_value_code: str | None + eu_pas_number: str | None + eu_pas_number_null_value_code: str | None @classmethod def from_input_values( @@ -61,6 +63,8 @@ def from_input_values( eudamed_srn_number_null_value_code: str | None, investigational_device_exemption_ide_number: str | None, investigational_device_exemption_ide_number_null_value_code: str | None, + eu_pas_number: str | None, + eu_pas_number_null_value_code: str | None, ) -> Self: return cls( ct_gov_id=normalize_string(ct_gov_id), @@ -119,6 +123,10 @@ def from_input_values( investigational_device_exemption_ide_number_null_value_code=normalize_string( investigational_device_exemption_ide_number_null_value_code ), + eu_pas_number=normalize_string(eu_pas_number), + eu_pas_number_null_value_code=normalize_string( + eu_pas_number_null_value_code + ), ) def validate( @@ -242,3 +250,10 @@ def validate( msg="If reason_for_missing_null_value_uid has a value, " "then field investigational_device_exemption_ide_number must be None or empty string", ) + + ValidationException.raise_if( + self.eu_pas_number is not None + and self.eu_pas_number_null_value_code is not None, + msg="If reason_for_missing_null_value_uid has a value, " + "then field eu_pas_number must be None or empty string", + ) diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/root.py b/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/root.py index 2fa43071..55b8b5a8 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/root.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/root.py @@ -79,6 +79,8 @@ class StudyMetadataSnapshot: eudamed_srn_number_null_value_code: str | None = None investigational_device_exemption_ide_number: str | None = None investigational_device_exemption_ide_number_null_value_code: str | None = None + eu_pas_number: str | None = None + eu_pas_number_null_value_code: str | None = None version_timestamp: datetime | None = None version_author: str | None = None version_description: str | None = None @@ -91,6 +93,7 @@ class StudyMetadataSnapshot: trial_type_null_value_code: str | None = None trial_phase_code: str | None = None trial_phase_null_value_code: str | None = None + development_stage_code: str | None = None is_extension_trial: bool | None = None is_extension_trial_null_value_code: str | None = None is_adaptive_design: bool | None = None @@ -191,6 +194,7 @@ class StudyMetadataSnapshot: is_adaptive_design=None, trial_type_codes=[], trial_phase_code=None, + development_stage_code=None, is_extension_trial=None, is_adaptive_design_null_value_code=None, study_stop_rules_null_value_code=None, @@ -444,18 +448,18 @@ def edit_metadata( investigational_device_exemption_ide_number_null_value_code=( new_id_metadata.registry_identifiers.investigational_device_exemption_ide_number_null_value_code ), + eu_pas_number=new_id_metadata.registry_identifiers.eu_pas_number, + eu_pas_number_null_value_code=( + new_id_metadata.registry_identifiers.eu_pas_number_null_value_code + ), ), ) else: # if the study has locked versions study_id_prefix stays the same new_id_metadata = StudyIdentificationMetadataVO( - _study_id_prefix=self.current_metadata.id_metadata.study_id_prefix, # here comes the substitution + _study_id_prefix=self.current_metadata.id_metadata.study_id_prefix, project_number=new_id_metadata.project_number, - study_number=( - new_id_metadata.study_number - if is_subpart or previous_is_subpart - else self.current_metadata.id_metadata.study_number - ), + study_number=new_id_metadata.study_number, subpart_id=new_id_metadata.subpart_id, study_acronym=new_id_metadata.study_acronym, study_subpart_acronym=new_id_metadata.study_subpart_acronym, @@ -501,10 +505,13 @@ def edit_metadata( investigational_device_exemption_ide_number_null_value_code=( new_id_metadata.registry_identifiers.investigational_device_exemption_ide_number_null_value_code ), + eu_pas_number=new_id_metadata.registry_identifiers.eu_pas_number, + eu_pas_number_null_value_code=( + new_id_metadata.registry_identifiers.eu_pas_number_null_value_code + ), ), ) - assert new_id_metadata is not None # making linter happy if new_id_metadata != self.current_metadata.id_metadata: new_id_metadata.validate( uid=self.uid, @@ -944,6 +951,10 @@ def study_metadata_values_from_snapshot( investigational_device_exemption_ide_number_null_value_code=( study_metadata_snapshot.investigational_device_exemption_ide_number_null_value_code ), + eu_pas_number=study_metadata_snapshot.eu_pas_number, + eu_pas_number_null_value_code=( + study_metadata_snapshot.eu_pas_number_null_value_code + ), ), ) study_creation_dict[value_object_name] = id_metadata @@ -967,6 +978,7 @@ def study_metadata_values_from_snapshot( assert study_snapshot.current_metadata is not None assert study_snapshot.study_status in ( StudyStatus.DRAFT.value, + StudyStatus.LOCKED.value, StudyStatus.RELEASED.value, ) or ( study_snapshot.locked_metadata_versions[ @@ -1043,6 +1055,7 @@ def from_initial_values( trial_intent_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_phase_exists_callback: Callable[[str], bool] = lambda _: True, + development_stage_exists_callback: Callable[[str], bool] = lambda _: True, null_value_exists_callback: Callable[[str], bool] = lambda _: True, intervention_type_exists_callback: Callable[[str], bool] = lambda _: True, control_type_exists_callback: Callable[[str], bool] = lambda _: True, @@ -1089,6 +1102,8 @@ def from_initial_values( :param trial_phase_exists_callback: (optional) callback for checking trial_phase_codes + :param development_stage_exists_callback: (optional) callback for checking development_stage_code + :param null_value_exists_callback: (optional) callback for checking null_value_codes :param therapeutic_area_exists_callback: (optional) callback for checking therapeutic_area_codes @@ -1165,6 +1180,10 @@ def from_initial_values( investigational_device_exemption_ide_number_null_value_code=( initial_id_metadata.registry_identifiers.investigational_device_exemption_ide_number_null_value_code ), + eu_pas_number=initial_id_metadata.registry_identifiers.eu_pas_number, + eu_pas_number_null_value_code=( + initial_id_metadata.registry_identifiers.eu_pas_number_null_value_code + ), ), _study_id_prefix=initial_id_metadata.project_number, ) @@ -1186,6 +1205,7 @@ def from_initial_values( initial_study_metadata.validate( study_type_exists_callback=study_type_exists_callback, trial_phase_exists_callback=trial_phase_exists_callback, + development_stage_exists_callback=development_stage_exists_callback, trial_type_exists_callback=trial_type_exists_callback, trial_intent_type_exists_callback=trial_intent_type_exists_callback, project_exists_callback=project_exists_callback, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/study_metadata.py b/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/study_metadata.py index 3521bda9..ba2703fa 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/study_metadata.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_definition_aggregates/study_metadata.py @@ -342,6 +342,8 @@ class HighLevelStudyDesignVO: trial_phase_code: str | None = None trial_phase_null_value_code: str | None = None + development_stage_code: str | None = None + is_extension_trial: bool | None = None is_extension_trial_null_value_code: str | None = None @@ -370,6 +372,7 @@ def validate( trial_intent_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_phase_exists_callback: Callable[[str], bool] = lambda _: True, + development_stage_exists_callback: Callable[[str], bool] = (lambda _: True), null_value_exists_callback: Callable[[str], bool] = lambda _: True, ) -> None: """ @@ -380,6 +383,7 @@ def validate( :param trial_intent_type_exists_callback: (optional) callback for checking intent_type_codes :param trial_type_exists_callback: (optional) callback for checking trail_type_codes :param trial_phase_exists_callback: (optional) callback for checking trial_phase_codes + :param development_stage_exists_callback: (optional) callback for checking development_stage_code :param null_value_exists_callback: (optional) callback for checking null_value_code for all specific values """ @@ -459,6 +463,12 @@ def validate_value_and_associated_null_value_valid( msg=f"Non-existent study type code provided '{self.study_type_code}'.", ) + exceptions.ValidationException.raise_if( + self.development_stage_code is not None + and not development_stage_exists_callback(self.development_stage_code), + msg=f"Non-existent development stage code provided '{self.development_stage_code}'.", + ) + for trial_type_code in self.trial_type_codes: exceptions.ValidationException.raise_if_not( trial_type_exists_callback(trial_type_code), @@ -471,6 +481,7 @@ def is_valid( trial_intent_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_phase_exists_callback: Callable[[str], bool] = lambda _: True, + development_stage_exists_callback: Callable[[str], bool] = lambda _: True, ) -> bool: """ Convenience method (mostly for testing purposes). @@ -482,6 +493,7 @@ def is_valid( trial_intent_type_exists_callback=trial_intent_type_exists_callback, trial_type_exists_callback=trial_type_exists_callback, trial_phase_exists_callback=trial_phase_exists_callback, + development_stage_exists_callback=development_stage_exists_callback, ) except exceptions.ValidationException: return False @@ -495,6 +507,7 @@ def fix_some_values( trial_type_null_value_code: str | None = FIX_SOME_VALUE_DEFAULT, trial_phase_code: str | None = FIX_SOME_VALUE_DEFAULT, trial_phase_null_value_code: str | None = FIX_SOME_VALUE_DEFAULT, + development_stage_code: str | None = FIX_SOME_VALUE_DEFAULT, is_extension_trial: bool | None = FIX_SOME_VALUE_DEFAULT, # type: ignore[assignment] is_extension_trial_null_value_code: str | None = FIX_SOME_VALUE_DEFAULT, is_adaptive_design: bool | None = FIX_SOME_VALUE_DEFAULT, # type: ignore[assignment] @@ -518,6 +531,7 @@ def fix_some_values( :param trial_type_null_value_code: :param trial_phase_code: :param trial_phase_null_value_code: + :param development_stage_code: :param is_extension_trial: :param is_extension_trial_null_value_code: :param is_adaptive_design: @@ -547,6 +561,9 @@ def helper(parameter: Any, def_value: Any): trial_phase_null_value_code=helper( trial_phase_null_value_code, self.trial_phase_null_value_code ), + development_stage_code=helper( + development_stage_code, self.development_stage_code + ), is_extension_trial=helper(is_extension_trial, self.is_extension_trial), is_extension_trial_null_value_code=helper( is_extension_trial_null_value_code, @@ -586,6 +603,7 @@ def from_input_values( trial_type_null_value_code: str | None, trial_phase_code: str | None, trial_phase_null_value_code: str | None, + development_stage_code: str | None, is_extension_trial: bool | None, is_extension_trial_null_value_code: str | None, is_adaptive_design: bool | None, @@ -604,6 +622,7 @@ def from_input_values( trial_type_null_value_code=trial_type_null_value_code, trial_phase_code=trial_phase_code, trial_phase_null_value_code=trial_phase_null_value_code, + development_stage_code=development_stage_code, is_extension_trial=is_extension_trial, is_extension_trial_null_value_code=is_extension_trial_null_value_code, is_adaptive_design=is_adaptive_design, @@ -697,7 +716,7 @@ def from_input_values( def normalize_code_set(codes: Iterable[str] | None) -> list[str]: if codes is None: codes = [] - return list( + return sorted( dict.fromkeys( [_ for _ in [normalize_string(_) for _ in codes] if _ is not None] ) @@ -1540,6 +1559,7 @@ def validate( trial_intent_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_type_exists_callback: Callable[[str], bool] = lambda _: True, trial_phase_exists_callback: Callable[[str], bool] = lambda _: True, + development_stage_exists_callback: Callable[[str], bool] = lambda _: True, null_value_exists_callback: Callable[[str], bool] = lambda _: True, therapeutic_area_exists_callback: Callable[[str], bool] = lambda _: True, disease_condition_or_indication_exists_callback: Callable[[str], bool] = ( @@ -1578,6 +1598,7 @@ def validate( self.high_level_study_design.validate( study_type_exists_callback=study_type_exists_callback, trial_phase_exists_callback=trial_phase_exists_callback, + development_stage_exists_callback=development_stage_exists_callback, trial_type_exists_callback=trial_type_exists_callback, trial_intent_type_exists_callback=trial_intent_type_exists_callback, null_value_exists_callback=null_value_exists_callback, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_design_cell.py b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_design_cell.py index 37b2e327..0080fe9f 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_design_cell.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_design_cell.py @@ -1,6 +1,8 @@ import datetime from dataclasses import dataclass +from common.utils import convert_to_datetime + @dataclass class StudyDesignCellVO: @@ -23,6 +25,10 @@ class StudyDesignCellVO: study_branch_arm_uid: str | None = None study_branch_arm_name: str | None = None + def __post_init__(self): + if not isinstance(self.start_date, datetime.datetime): + self.start_date = convert_to_datetime(self.start_date) + def edit_core_properties( self, order: int, diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_epoch.py b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_epoch.py index cddc24cb..09e8f496 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_epoch.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_epoch.py @@ -302,8 +302,7 @@ def update_visit(self, visit: StudyVisitVO): """ Updates visits to a list of visits - used for preparation of adding new visit """ - new_visits = [v for v in self._visits if v.uid != visit.uid] - new_visits.append(visit) + new_visits = [v if v.uid != visit.uid else visit for v in self._visits] self._visits = new_visits @property diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_activity_instance.py b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_activity_instance.py index a48dbbdb..24ee9668 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_activity_instance.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_selection_activity_instance.py @@ -80,8 +80,18 @@ class StudySelectionActivityInstanceVO(study_selection_base.StudySelectionBaseVO soa_group_term_uid: str | None = None soa_group_term_name: str | None = None study_selection_uid: str | None = None + # Data supplier and origin fields (L3 SoA) + study_data_supplier_uid: str | None = None + study_data_supplier_name: str | None = None + origin_type_uid: str | None = None + origin_type_name: str | None = None + origin_type_codelist_uid: str | None = None + origin_source_uid: str | None = None + origin_source_name: str | None = None + origin_source_codelist_uid: str | None = None @classmethod + # pylint: disable=too-many-arguments,too-many-locals def from_input_values( cls, study_uid: str, @@ -133,6 +143,14 @@ def from_input_values( soa_group_term_uid: str | None = None, soa_group_term_name: str | None = None, study_selection_uid: str | None = None, + study_data_supplier_uid: str | None = None, + study_data_supplier_name: str | None = None, + origin_type_uid: str | None = None, + origin_type_name: str | None = None, + origin_type_codelist_uid: str | None = None, + origin_source_uid: str | None = None, + origin_source_name: str | None = None, + origin_source_codelist_uid: str | None = None, ): if study_selection_uid is None: study_selection_uid = generate_uid_callback() @@ -209,6 +227,14 @@ def from_input_values( study_soa_group_uid=study_soa_group_uid, soa_group_term_uid=soa_group_term_uid, soa_group_term_name=soa_group_term_name, + study_data_supplier_uid=study_data_supplier_uid, + study_data_supplier_name=study_data_supplier_name, + origin_type_uid=origin_type_uid, + origin_type_name=origin_type_name, + origin_type_codelist_uid=origin_type_codelist_uid, + origin_source_uid=origin_source_uid, + origin_source_name=origin_source_name, + origin_source_codelist_uid=origin_source_codelist_uid, ) def validate( diff --git a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_visit.py b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_visit.py index 1f133259..373f3a6b 100644 --- a/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_visit.py +++ b/clinical-mdr-api/clinical_mdr_api/domains/study_selections/study_visit.py @@ -408,14 +408,19 @@ def delete(self): def compare_cons_group_equality( self, other_visit: "StudyVisitVO", - ) -> bool: - return ( - self.visit_type == other_visit.visit_type - and self.epoch_uid == other_visit.epoch_uid - and self.visit_contact_mode == other_visit.visit_contact_mode - and self.visit_window_min == other_visit.visit_window_min - and self.visit_window_max == other_visit.visit_window_max - ) + ) -> list[str]: + different_fields: list[str] = [] + if self.visit_type != other_visit.visit_type: + different_fields.append("visit_type") + if self.epoch_uid != other_visit.epoch_uid: + different_fields.append("epoch") + if self.visit_contact_mode != other_visit.visit_contact_mode: + different_fields.append("visit_contact_mode") + if self.visit_window_min != other_visit.visit_window_min: + different_fields.append("min_visit_window_value") + if self.visit_window_max != other_visit.visit_window_max: + different_fields.append("max_visit_window_value") + return different_fields @dataclass diff --git a/clinical-mdr-api/clinical_mdr_api/listings/query_service.py b/clinical-mdr-api/clinical_mdr_api/listings/query_service.py index 6f508751..756efd16 100644 --- a/clinical-mdr-api/clinical_mdr_api/listings/query_service.py +++ b/clinical-mdr-api/clinical_mdr_api/listings/query_service.py @@ -850,6 +850,7 @@ def get_ts( WHEN 'national_medical_products_administration_nmpa_number' THEN 'C98714' WHEN 'eudamed_srn_number' THEN 'C98714' WHEN 'investigational_device_exemption_ide_number' THEN 'C98714' + WHEN 'eu_pas_number' THEN 'C98714' WHEN 'confirmed_response_minimum_duration' THEN 'C98715' WHEN 'is_adaptive_design' THEN 'C146995' WHEN 'study_stop_rules' THEN 'C49698' @@ -893,6 +894,7 @@ def get_ts( WHEN sf.field_name='national_medical_products_administration_nmpa_number' THEN 'NMPA' WHEN sf.field_name='eudamed_srn_number' THEN 'ESN' WHEN sf.field_name='investigational_device_exemption_ide_number' THEN 'IDE' + WHEN sf.field_name='eu_pas_number' THEN 'EPN' WHEN sf:StudyTimeField THEN 'Not Controlled TimeField' WHEN sf:StudyIntField diff --git a/clinical-mdr-api/clinical_mdr_api/main.py b/clinical-mdr-api/clinical_mdr_api/main.py index 68d791f4..81c78172 100644 --- a/clinical-mdr-api/clinical_mdr_api/main.py +++ b/clinical-mdr-api/clinical_mdr_api/main.py @@ -160,6 +160,11 @@ async def lifespan(_app: FastAPI): When authentication is turned on, all requests to protected API endpoints must provide a valid bearer (JWT) token inside the `Authorization` http header. """, + # Putting False here as pydantic v2 separates input and output schemas if it sees any differences in usage the same model between GET and POST/PUT/PATCH flow + # but in this case it generates totally the same -Input and -Output models which duplicates model schema in openapi documentation. + # Model schema duplication causes issues for open-source users. + # For more information please lookup here https://fastapi.tiangolo.com/how-to/separate-openapi-schemas/ + separate_input_output_schemas=False, ) @@ -570,7 +575,7 @@ async def value_error_handler(request: Request, exception: ValueError): prefix="/integrations/ms-graph", tags=["MS Graph API integrations"], ) -app.include_router(routers.ddf_router, prefix="/usdm/v3", tags=["USDM endpoints"]) +app.include_router(routers.ddf_router, prefix="/usdm/v4", tags=["USDM endpoints"]) app.include_router( routers.data_suppliers_router, prefix="/data-suppliers", tags=["Data Suppliers"] ) @@ -586,6 +591,7 @@ def custom_openapi(): openapi_version=app.openapi_version, description=app.description, routes=app.routes, + separate_input_output_schemas=app.separate_input_output_schemas, ) openapi_schema["servers"] = [{"url": settings.openapi_schema_api_root_path}] diff --git a/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_instance_class.py b/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_instance_class.py index 529a2279..0a826efc 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_instance_class.py +++ b/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_instance_class.py @@ -54,9 +54,27 @@ class ParentActivityItemClass(BaseModel): } ), ] = None + is_additional_optional: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "parent_class.has_activity_item_class|is_additional_optional", + "nullable": True, + } + ), + ] = False + is_default_linked: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "parent_class.has_activity_item_class|is_default_linked", + "nullable": True, + } + ), + ] = False -class CompactActivityItemClass(BaseModel): +class CompactActivityItemClassForInstanceClass(BaseModel): model_config = ConfigDict(from_attributes=True) uid: Annotated[ @@ -77,6 +95,15 @@ class CompactActivityItemClass(BaseModel): } ), ] = None + display_name: Annotated[ + str | None, + Field( + json_schema_extra={ + "source": "has_activity_item_class.has_latest_value.display_name", + "nullable": True, + } + ), + ] = None mandatory: Annotated[ bool | None, Field( @@ -95,6 +122,24 @@ class CompactActivityItemClass(BaseModel): } ), ] = None + is_additional_optional: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "has_activity_item_class|is_additional_optional", + "nullable": True, + } + ), + ] = False + is_default_linked: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "has_activity_item_class|is_default_linked", + "nullable": True, + } + ), + ] = False class CompactActivityInstanceClass(BaseModel): diff --git a/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_item_class.py b/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_item_class.py index 6969ee99..82b6d52a 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_item_class.py +++ b/clinical-mdr-api/clinical_mdr_api/models/biomedical_concepts/activity_item_class.py @@ -27,7 +27,7 @@ from common.config import settings -class CompactActivityInstanceClass(BaseModel): +class CompactActivityInstanceClassForActivityItemClass(BaseModel): model_config = ConfigDict(from_attributes=True) uid: Annotated[ @@ -66,6 +66,24 @@ class CompactActivityInstanceClass(BaseModel): } ), ] = None + is_additional_optional: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "has_activity_instance_class|is_additional_optional", + "nullable": True, + } + ), + ] = False + is_default_linked: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "has_activity_instance_class|is_default_linked", + "nullable": True, + } + ), + ] = False class SimpleDataTypeTerm(BaseModel): @@ -128,7 +146,7 @@ class SimpleRoleTerm(BaseModel): ] = None -class SimpleVariableClass(BaseModel): +class SimpleVariableClassForActivityItemClass(BaseModel): model_config = ConfigDict(from_attributes=True) uid: Annotated[ @@ -158,14 +176,24 @@ class ActivityItemClass(VersionProperties): } ), ] = None + display_name: Annotated[ + str | None, + Field( + json_schema_extra={ + "source": "has_latest_value.display_name", + "nullable": True, + } + ), + ] = None order: Annotated[int, Field(json_schema_extra={"source": "has_latest_value.order"})] data_type: Annotated[SimpleDataTypeTerm, Field()] role: Annotated[SimpleRoleTerm, Field()] activity_instance_classes: Annotated[ - list[CompactActivityInstanceClass] | None, Field() + list[CompactActivityInstanceClassForActivityItemClass] | None, Field() ] variable_classes: Annotated[ - list[SimpleVariableClass] | None, Field(json_schema_extra={"nullable": True}) + list[SimpleVariableClassForActivityItemClass] | None, + Field(json_schema_extra={"nullable": True}), ] = None library_name: Annotated[ str, Field(json_schema_extra={"source": "has_library.name"}) @@ -235,11 +263,13 @@ def from_activity_item_class_ar( if item.uid == activity_instance_class.uid ) activity_instance_classes.append( - CompactActivityInstanceClass( + CompactActivityInstanceClassForActivityItemClass( uid=activity_instance_class.uid, name=rel.name, mandatory=activity_instance_class.mandatory, is_adam_param_specific_enabled=activity_instance_class.is_adam_param_specific_enabled, + is_additional_optional=activity_instance_class.is_additional_optional, + is_default_linked=activity_instance_class.is_default_linked, ) ) @@ -248,6 +278,7 @@ def from_activity_item_class_ar( name=activity_item_class_ar.name, definition=activity_item_class_ar.definition, nci_concept_id=activity_item_class_ar.nci_concept_id, + display_name=activity_item_class_ar.display_name, order=activity_item_class_ar.activity_item_class_vo.order, activity_instance_classes=activity_instance_classes, data_type=SimpleDataTypeTerm( @@ -262,7 +293,7 @@ def from_activity_item_class_ar( ), variable_classes=( [ - SimpleVariableClass(uid=variable_class_uid) + SimpleVariableClassForActivityItemClass(uid=variable_class_uid) for variable_class_uid in activity_item_class_ar.activity_item_class_vo.variable_class_uids ] if activity_item_class_ar.activity_item_class_vo.variable_class_uids @@ -297,6 +328,15 @@ class CompactActivityItemClass(BaseModel): } ), ] = None + display_name: Annotated[ + str | None, + Field( + json_schema_extra={ + "source": "has_latest_value.display_name", + "nullable": True, + } + ), + ] = None mandatory: Annotated[ bool | None, Field( @@ -315,11 +355,31 @@ class CompactActivityItemClass(BaseModel): } ), ] = None + is_additional_optional: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "has_activity_instance_class|is_additional_optional", + "nullable": True, + } + ), + ] = False + is_default_linked: Annotated[ + bool | None, + Field( + json_schema_extra={ + "source": "has_activity_instance_class|is_default_linked", + "nullable": True, + } + ), + ] = False class ActivityInstanceClassRelInput(InputModel): uid: Annotated[str, Field(min_length=1)] is_adam_param_specific_enabled: Annotated[bool, Field()] + is_additional_optional: Annotated[bool, Field()] + is_default_linked: Annotated[bool, Field()] mandatory: Annotated[bool, Field()] @@ -327,6 +387,7 @@ class ActivityItemClassCreateInput(PostInputModel): name: Annotated[str, Field()] definition: Annotated[str | None, Field(min_length=1)] = None nci_concept_id: Annotated[str | None, Field(min_length=1)] = None + display_name: Annotated[str | None, Field(min_length=1)] = None order: Annotated[int, Field(gt=0, lt=settings.max_int_neo4j)] activity_instance_classes: Annotated[list[ActivityInstanceClassRelInput], Field()] role_uid: Annotated[str, Field(min_length=1)] @@ -338,6 +399,7 @@ class ActivityItemClassEditInput(PatchInputModel): name: Annotated[str | None, Field(min_length=1)] = None definition: Annotated[str | None, Field(min_length=1)] = None nci_concept_id: Annotated[str | None, Field(min_length=1)] = None + display_name: Annotated[str | None, Field(min_length=1)] = None order: Annotated[int | None, Field(gt=0, lt=settings.max_int_neo4j)] = None activity_instance_classes: list[ActivityInstanceClassRelInput] = Field( default_factory=list @@ -393,6 +455,7 @@ class ActivityItemClassDetail(BaseModel): uid: Annotated[str, Field()] name: Annotated[str, Field()] definition: Annotated[str | None, Field()] = None + display_name: Annotated[str | None, Field()] = None nci_code: Annotated[str | None, Field()] = None library_name: Annotated[str | None, Field()] = None start_date: Annotated[str | None, Field()] = None @@ -410,6 +473,8 @@ class SimpleActivityInstanceClassForItem(BaseModel): uid: Annotated[str, Field()] name: Annotated[str, Field()] adam_param_specific_enabled: Annotated[bool, Field()] = False + is_additional_optional: Annotated[bool, Field()] = False + is_default_linked: Annotated[bool, Field()] = False mandatory: Annotated[bool, Field()] = False modified_date: Annotated[str | None, Field()] = None modified_by: Annotated[str | None, Field()] = None diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity.py index 7d736022..0cd99235 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity.py @@ -155,47 +155,6 @@ def validate_possible_actions(cls, _, info: ValidationInfo): return [] -class CompactActivity(BaseModel): - uid: Annotated[str, Field()] - name: Annotated[str, Field()] - definition: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( - None - ) - synonyms: Annotated[ - list[str] | None, - Field(json_schema_extra={"nullable": True, "remove_from_wildcard": True}), - ] = None - abbreviation: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( - None - ) - is_data_collected: Annotated[bool, Field()] - is_used_by_legacy_instances: Annotated[bool, Field()] - activity_group_uid: Annotated[str, Field()] - activity_group_name: Annotated[str, Field()] - activity_subgroup_uid: Annotated[str, Field()] - activity_subgroup_name: Annotated[str, Field()] - status: Annotated[str, Field()] - library_name: Annotated[str, Field()] - - @classmethod - def from_repository_output(cls, data: dict[str, Any]) -> Self: - return cls( - uid=data["uid"], - name=data["name"], - definition=data.get("definition"), - synonyms=data.get("synonyms") or [], - abbreviation=data.get("abbreviation"), - is_data_collected=data["is_data_collected"], - is_used_by_legacy_instances=data["is_used_by_legacy_instances"], - activity_group_uid=data["activity_group_uid"], - activity_group_name=data["activity_group_name"], - activity_subgroup_uid=data["activity_subgroup_uid"], - activity_subgroup_name=data["activity_subgroup_name"], - status=data["status"], - library_name=data["library_name"], - ) - - class Activity(ActivityBase): nci_concept_id: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) @@ -590,6 +549,9 @@ class SimpleActivity(BaseModel): end_date: Annotated[ datetime.datetime | None, Field(json_schema_extra={"nullable": True}) ] = None + author_username: Annotated[ + str | None, Field(json_schema_extra={"nullable": True}) + ] = None class SimpleActivitySubGroup(BaseModel): @@ -617,7 +579,7 @@ class SimpleActivityGrouping(BaseModel): activity_subgroup: Annotated[SimpleActivitySubGroup, Field()] -class SimpleActivityInstanceClass(BaseModel): +class SimpleActivityInstanceClassForActivity(BaseModel): name: Annotated[str, Field()] @@ -653,7 +615,7 @@ class SimpleActivityInstance(BaseModel): float | None, Field(json_schema_extra={"nullable": True}) ] = None library_name: Annotated[str, Field()] - activity_instance_class: Annotated[SimpleActivityInstanceClass, Field()] + activity_instance_class: Annotated[SimpleActivityInstanceClassForActivity, Field()] version: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None status: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None start_date: Annotated[ @@ -662,6 +624,9 @@ class SimpleActivityInstance(BaseModel): end_date: Annotated[ datetime.datetime | None, Field(json_schema_extra={"nullable": True}) ] = None + author_username: Annotated[ + str | None, Field(json_schema_extra={"nullable": True}) + ] = None class ActivityOverview(BaseModel): @@ -699,6 +664,7 @@ def from_repository_input(cls, overview: dict[str, Any]): end_date=convert_to_datetime( overview.get("has_version", {}).get("end_date") ), + author_username=overview.get("has_version", {}).get("author_username"), ), activity_groupings=[ SimpleActivityGrouping( @@ -746,7 +712,7 @@ def from_repository_input(cls, overview: dict[str, Any]): library_name=activity_instance.get( "activity_instance_library_name" ), - activity_instance_class=SimpleActivityInstanceClass( + activity_instance_class=SimpleActivityInstanceClassForActivity( name=activity_instance.get("activity_instance_class").get( "name" ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_group.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_group.py index 47ea6c5a..42b294e7 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_group.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_group.py @@ -84,6 +84,7 @@ class SimpleSubGroup(BaseModel): name: Annotated[str, Field()] version: Annotated[str, Field()] status: Annotated[str, Field()] + start_date: Annotated[str, Field()] definition: Annotated[str | None, Field()] = None diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_instance.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_instance.py index 0d9e0066..678a2acb 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_instance.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_instance.py @@ -23,17 +23,14 @@ SimpleActivityGroup, SimpleActivityGrouping, SimpleActivityInstance, - SimpleActivityInstanceClass, + SimpleActivityInstanceClassForActivity, SimpleActivitySubGroup, ) from clinical_mdr_api.models.concepts.activities.activity_item import ( ActivityItem, ActivityItemCreateInput, - CompactActivityItemClass, + CompactActivityItemClassForActivityItem, CompactCTTerm, - CompactOdmForm, - CompactOdmItem, - CompactOdmItemGroup, CompactUnitDefinition, ) from clinical_mdr_api.models.concepts.concept import ( @@ -149,46 +146,16 @@ def from_activity_ar( ) ) ct_terms.sort(key=lambda x: x.uid or "") - odm_form = ( - CompactOdmForm( - uid=activity_item.odm_form.uid, - oid=activity_item.odm_form.oid, - name=activity_item.odm_form.name, - ) - if activity_item.odm_form - else None - ) - odm_item_group = ( - CompactOdmItemGroup( - uid=activity_item.odm_item_group.uid, - oid=activity_item.odm_item_group.oid, - name=activity_item.odm_item_group.name, - ) - if activity_item.odm_item_group - else None - ) - odm_item = ( - CompactOdmItem( - uid=activity_item.odm_item.uid, - oid=activity_item.odm_item.oid, - name=activity_item.odm_item.name, - ) - if activity_item.odm_item - else None - ) activity_items.append( ActivityItem( - activity_item_class=CompactActivityItemClass( + activity_item_class=CompactActivityItemClassForActivityItem( uid=activity_item.activity_item_class_uid, name=activity_item.activity_item_class_name, ), ct_terms=ct_terms, unit_definitions=unit_definitions, is_adam_param_specific=activity_item.is_adam_param_specific, - odm_form=odm_form, - odm_item_group=odm_item_group, - odm_item=odm_item, ) ) @@ -295,40 +262,13 @@ def from_activity_instance_ar_objects( activity_items.append( ActivityItem( - activity_item_class=CompactActivityItemClass( + activity_item_class=CompactActivityItemClassForActivityItem( uid=activity_item.activity_item_class_uid, name=activity_item.activity_item_class_name, ), ct_terms=ct_terms, unit_definitions=unit_definitions, is_adam_param_specific=activity_item.is_adam_param_specific, - odm_form=( - CompactOdmForm( - uid=activity_item.odm_form.uid, - oid=activity_item.odm_form.oid, - name=activity_item.odm_form.name, - ) - if activity_item.odm_form - else None - ), - odm_item_group=( - CompactOdmItemGroup( - uid=activity_item.odm_item_group.uid, - oid=activity_item.odm_item_group.oid, - name=activity_item.odm_item_group.name, - ) - if activity_item.odm_item_group - else None - ), - odm_item=( - CompactOdmItem( - uid=activity_item.odm_item.uid, - oid=activity_item.odm_item.oid, - name=activity_item.odm_item.name, - ) - if activity_item.odm_item - else None - ), ) ) @@ -414,6 +354,12 @@ class ActivityInstanceCreateInput(ExtendedConceptPostInput): activity_instance_class_uid: Annotated[str, Field(min_length=1)] activity_items: Annotated[list[ActivityItemCreateInput] | None, Field()] = None library_name: Annotated[str, Field(min_length=1)] + strict_mode: Annotated[ + bool, + Field( + description="If True, enforces strict validation for parent mandatory activity item classes. Defaults to False (relaxed mode)." + ), + ] = False class ActivityInstancePreviewInput(ActivityInstanceCreateInput): @@ -442,6 +388,12 @@ class ActivityInstanceEditInput(ExtendedConceptPatchInput): activity_instance_class_uid: Annotated[str | None, Field(min_length=1)] = None activity_groupings: Annotated[list[ActivityInstanceGrouping] | None, Field()] = None activity_items: Annotated[list[ActivityItemCreateInput] | None, Field()] = None + strict_mode: Annotated[ + bool | None, + Field( + description="If True, enforces strict validation for parent mandatory activity item classes. Defaults to False (relaxed mode) when not provided." + ), + ] = None change_description: Annotated[str, Field(min_length=1)] @@ -453,7 +405,7 @@ class ActivityInstanceVersion(ActivityInstance): changes: list[str] = Field(description=CHANGES_FIELD_DESC, default_factory=list) -class SimpleActivity(BaseModel): +class SimpleActivityForActivityInstance(BaseModel): uid: Annotated[str, Field()] nci_concept_id: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) @@ -485,7 +437,7 @@ class SimpleActivity(BaseModel): status: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None -class SimpleActivityItemClass(BaseModel): +class SimpleActivityItemClassForActivityInstance(BaseModel): name: Annotated[str, Field()] order: Annotated[int, Field()] role_name: Annotated[str, Field()] @@ -497,15 +449,12 @@ class SimplifiedActivityItem(BaseModel): ct_terms: list[CTTermItem] = Field(default_factory=list) unit_definitions: list[CompactUnitDefinition] = Field(default_factory=list) - activity_item_class: Annotated[SimpleActivityItemClass, Field()] + activity_item_class: Annotated[SimpleActivityItemClassForActivityInstance, Field()] is_adam_param_specific: Annotated[bool, Field()] - odm_form: Annotated[CompactOdmForm | None, Field()] = None - odm_item_group: Annotated[CompactOdmItemGroup | None, Field()] = None - odm_item: Annotated[CompactOdmItem | None, Field()] = None class SimpleActivityInstanceGrouping(SimpleActivityGrouping): - activity: Annotated[SimpleActivity, Field()] + activity: Annotated[SimpleActivityForActivityInstance, Field()] class ActivityInstanceOverview(BaseModel): @@ -541,33 +490,6 @@ def from_repository_input(cls, overview: dict[str, Any]): ], key=lambda x: x.uid or "", ) - odm_form = ( - CompactOdmForm( - uid=activity_item["odm_form"]["uid"], - oid=activity_item["odm_form"]["oid"], - name=activity_item["odm_form"]["name"], - ) - if activity_item.get("odm_form", None) - else None - ) - odm_item_group = ( - CompactOdmItemGroup( - uid=activity_item["odm_item_group"]["uid"], - oid=activity_item["odm_item_group"]["oid"], - name=activity_item["odm_item_group"]["name"], - ) - if activity_item.get("odm_item_group", None) - else None - ) - odm_item = ( - CompactOdmItem( - uid=activity_item["odm_item"]["uid"], - oid=activity_item["odm_item"]["oid"], - name=activity_item["odm_item"]["name"], - ) - if activity_item.get("odm_item", None) - else None - ) # Extract activity_item_class handling Neo4j node format aic = activity_item.get("activity_item_class", {}) if "properties" in aic: @@ -583,10 +505,7 @@ def from_repository_input(cls, overview: dict[str, Any]): SimplifiedActivityItem( ct_terms=terms, unit_definitions=units, - odm_form=odm_form, - odm_item_group=odm_item_group, - odm_item=odm_item, - activity_item_class=SimpleActivityItemClass( + activity_item_class=SimpleActivityItemClassForActivityInstance( name=aic_name, order=aic_order, role_name=activity_item.get("activity_item_class_role"), @@ -603,7 +522,7 @@ def from_repository_input(cls, overview: dict[str, Any]): return cls( activity_groupings=[ SimpleActivityInstanceGrouping( - activity=SimpleActivity( + activity=SimpleActivityForActivityInstance( uid=activity_grouping.get("uid"), name=activity_grouping.get("activity_value").get("name"), definition=activity_grouping.get("activity_value").get( @@ -701,7 +620,7 @@ def from_repository_input(cls, overview: dict[str, Any]): "molecular_weight" ), library_name=overview["instance_library_name"], - activity_instance_class=SimpleActivityInstanceClass( + activity_instance_class=SimpleActivityInstanceClassForActivity( name=overview.get("activity_instance_class").get("name") ), status=overview.get("has_version", {}).get("status"), @@ -712,6 +631,7 @@ def from_repository_input(cls, overview: dict[str, Any]): end_date=convert_to_datetime( overview.get("has_version", {}).get("end_date") ), + author_username=overview.get("has_version", {}).get("author_username"), ), activity_items=activity_items, all_versions=overview["all_versions"], @@ -731,9 +651,9 @@ class ActivityInstanceDetail(BaseModel): ) version: Annotated[str, Field()] status: Annotated[str, Field()] - activity_instance_class: Annotated[SimpleActivityInstanceClass | None, Field()] = ( - None - ) + activity_instance_class: Annotated[ + SimpleActivityInstanceClassForActivity | None, Field() + ] = None start_date: Annotated[ datetime | None, Field(json_schema_extra={"nullable": True}) ] = None diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_item.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_item.py index 5214bec6..fca797d4 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_item.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_item.py @@ -5,7 +5,7 @@ from clinical_mdr_api.models.utils import BaseModel, PostInputModel -class CompactActivityItemClass(BaseModel): +class CompactActivityItemClassForActivityItem(BaseModel): model_config = ConfigDict(from_attributes=True) uid: Annotated[ @@ -92,103 +92,13 @@ class CompactUnitDefinition(BaseModel): ] = None -class CompactOdmForm(BaseModel): - model_config = ConfigDict(from_attributes=True) - - uid: Annotated[ - str | None, - Field(json_schema_extra={"source": "has_odm_form.uid", "nullable": True}), - ] = None - oid: Annotated[ - str | None, - Field( - json_schema_extra={ - "source": "has_odm_form.has_latest_value.oid", - "nullable": True, - }, - ), - ] = None - name: Annotated[ - str | None, - Field( - json_schema_extra={ - "source": "has_odm_form.has_latest_value.name", - "nullable": True, - }, - ), - ] = None - - -class CompactOdmItemGroup(BaseModel): - model_config = ConfigDict(from_attributes=True) - - uid: Annotated[ - str | None, - Field(json_schema_extra={"source": "has_odm_item_group.uid", "nullable": True}), - ] = None - oid: Annotated[ - str | None, - Field( - json_schema_extra={ - "source": "has_odm_item_group.has_latest_value.oid", - "nullable": True, - }, - ), - ] = None - name: Annotated[ - str | None, - Field( - json_schema_extra={ - "source": "has_odm_item_group.has_latest_value.name", - "nullable": True, - }, - ), - ] = None - - -class CompactOdmItem(BaseModel): - model_config = ConfigDict(from_attributes=True) - - uid: Annotated[ - str | None, - Field(json_schema_extra={"source": "has_odm_item.uid", "nullable": True}), - ] = None - oid: Annotated[ - str | None, - Field( - json_schema_extra={ - "source": "has_odm_item.has_latest_value.oid", - "nullable": True, - }, - ), - ] = None - name: Annotated[ - str | None, - Field( - json_schema_extra={ - "source": "has_odm_item.has_latest_value.name", - "nullable": True, - }, - ), - ] = None - - class ActivityItem(BaseModel): model_config = ConfigDict(from_attributes=True) - activity_item_class: Annotated[CompactActivityItemClass, Field()] + activity_item_class: Annotated[CompactActivityItemClassForActivityItem, Field()] ct_terms: list[CompactCTTerm] = Field(default_factory=list) unit_definitions: list[CompactUnitDefinition] = Field(default_factory=list) is_adam_param_specific: Annotated[bool, Field()] - odm_form: Annotated[ - CompactOdmForm | None, Field(json_schema_extra={"nullable": True}) - ] = None - odm_item_group: Annotated[ - CompactOdmItemGroup | None, Field(json_schema_extra={"nullable": True}) - ] = None - odm_item: Annotated[ - CompactOdmItem | None, Field(json_schema_extra={"nullable": True}) - ] = None class ActivityItemCreateInput(PostInputModel): @@ -200,6 +110,3 @@ class CTTermsInput(PostInputModel): ct_terms: Annotated[list[CTTermsInput], Field()] unit_definition_uids: Annotated[list[str], Field()] is_adam_param_specific: Annotated[bool, Field()] - odm_form_uid: Annotated[str | None, Field()] = None - odm_item_group_uid: Annotated[str | None, Field()] = None - odm_item_uid: Annotated[str | None, Field()] = None diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_sub_group.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_sub_group.py index c709db3a..f9e72682 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_sub_group.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/activities/activity_sub_group.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Annotated, Any, Callable, Self +from typing import Annotated, Any, Self from pydantic import Field @@ -8,11 +8,7 @@ from clinical_mdr_api.domains.concepts.activities.activity_sub_group import ( ActivitySubGroupAR, ) -from clinical_mdr_api.domains.concepts.concept_base import ConceptARBase -from clinical_mdr_api.models.concepts.activities.activity import ( - ActivityBase, - ActivityHierarchySimpleModel, -) +from clinical_mdr_api.models.concepts.activities.activity import ActivityBase from clinical_mdr_api.models.concepts.concept import ExtendedConceptPostInput from clinical_mdr_api.models.libraries.library import Library from clinical_mdr_api.models.utils import BaseModel, EditInputModel @@ -23,23 +19,7 @@ class ActivitySubGroup(ActivityBase): def from_activity_ar( cls, activity_subgroup_ar: ActivitySubGroupAR, - find_activity_by_uid: Callable[[str], ConceptARBase | None], - was_cascade_update_performed: bool | None = None, ) -> Self: - activity_groups = [] - for activity_group in activity_subgroup_ar.concept_vo.activity_groups: - if activity_group.activity_group_name: - translation = ActivityHierarchySimpleModel( - uid=activity_group.activity_group_uid, - name=activity_group.activity_group_name, - ) - else: - translation = ActivityHierarchySimpleModel.from_activity_uid( - uid=activity_group.activity_group_uid, - version=activity_group.activity_group_version, - find_activity_by_uid=find_activity_by_uid, - ) - activity_groups.append(translation) return cls( uid=activity_subgroup_ar.uid, @@ -47,7 +27,6 @@ def from_activity_ar( name_sentence_case=activity_subgroup_ar.concept_vo.name_sentence_case, definition=activity_subgroup_ar.concept_vo.definition, abbreviation=activity_subgroup_ar.concept_vo.abbreviation, - activity_groups=activity_groups, library_name=Library.from_library_vo(activity_subgroup_ar.library).name, start_date=activity_subgroup_ar.item_metadata.start_date, end_date=activity_subgroup_ar.item_metadata.end_date, @@ -58,12 +37,8 @@ def from_activity_ar( possible_actions=sorted( [_.value for _ in activity_subgroup_ar.get_possible_actions()] ), - was_cascade_update_performed=was_cascade_update_performed, ) - activity_groups: Annotated[list[ActivityHierarchySimpleModel], Field()] - was_cascade_update_performed: Annotated[bool | None, Field()] = None - class ActivitySubGroupCreateInput(ExtendedConceptPostInput): name: Annotated[ @@ -74,7 +49,6 @@ class ActivitySubGroupCreateInput(ExtendedConceptPostInput): ), ] name_sentence_case: Annotated[str, Field(min_length=1)] - activity_groups: Annotated[list[str] | None, Field()] = None library_name: Annotated[str, Field(min_length=1)] @@ -90,7 +64,7 @@ class ActivitySubGroupVersion(ActivitySubGroup): changes: list[str] = Field(description=CHANGES_FIELD_DESC, default_factory=list) -class ActivityGroup(BaseModel): +class ActivityGroupForActivitySubGroup(BaseModel): uid: Annotated[str, Field()] name: Annotated[str, Field()] version: Annotated[str | None, Field()] = None @@ -109,7 +83,6 @@ class ActivitySubGroupDetail(BaseModel): possible_actions: Annotated[list[str] | None, Field()] = None change_description: Annotated[str, Field()] author_username: Annotated[str | None, Field()] = None - activity_groups: Annotated[list[ActivityGroup], Field()] class ActivitySubGroupOverview(BaseModel): @@ -139,7 +112,6 @@ def from_repository_input(cls, overview: dict[str, Any]): end_date=convert_to_datetime(version_data.get("end_date")), status=version_data.get("status"), version=version_data.get("version"), - activity_groups=version_data.get("activity_groups", {}), possible_actions=version_data.get("possible_actions"), change_description=version_data["change_description"], ), diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_form.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_form.py index 05391e52..e8ae0105 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_form.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_form.py @@ -98,7 +98,7 @@ def from_odm_form_ar( ) for item_group_uid in odm_form_ar.concept_vo.item_group_uids ], - key=lambda item: item.order_number or "", + key=lambda item: item.order_number, ), vendor_elements=sorted( [ @@ -210,9 +210,7 @@ def from_odm_form_uid( uid: Annotated[str, Field()] name: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None version: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None - order_number: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = ( - None - ) + order_number: Annotated[int, Field(json_schema_extra={"nullable": True})] = 999999 mandatory: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None locked: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None collection_exception_condition_oid: Annotated[ diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item.py index 162f3a90..3ba851ca 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item.py @@ -52,7 +52,7 @@ SimpleDictionaryTermModel, SimpleTermModel, ) -from clinical_mdr_api.models.utils import BaseModel, InputModel +from clinical_mdr_api.models.utils import BaseModel, InputModel, PostInputModel from clinical_mdr_api.models.validators import has_english_description from common.config import settings @@ -252,6 +252,7 @@ class OdmItem(ConceptModel): Field(json_schema_extra={"nullable": True}), ] = None terms: Annotated[list[OdmItemTermRelationshipModel], Field()] + activity_instances: Annotated[list, Field()] vendor_elements: Annotated[list[OdmVendorElementRelationModel], Field()] vendor_attributes: Annotated[list[OdmVendorAttributeRelationModel], Field()] vendor_element_attributes: Annotated[ @@ -335,6 +336,22 @@ def from_odm_item_ar( ], key=lambda item: (item.order is not None, item.order), ), + activity_instances=[ + ActivityInstanceRel( + activity_instance_uid=activity_instance["activity_instance_uid"], + activity_item_class_uid=activity_instance[ + "activity_item_class_uid" + ], + odm_form_uid=activity_instance["odm_form_uid"], + odm_item_group_uid=activity_instance["odm_item_group_uid"], + order=activity_instance["order"], + primary=activity_instance.get("primary", False), + preset_response_value=activity_instance["preset_response_value"], + value_condition=activity_instance["value_condition"], + value_dependent_map=activity_instance["value_dependent_map"], + ) + for activity_instance in odm_item_ar.concept_vo.activity_instances + ], vendor_elements=sorted( [ OdmVendorElementRelationModel.from_uid( @@ -475,9 +492,7 @@ def from_odm_item_uid( oid: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None name: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None version: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None - order_number: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = ( - None - ) + order_number: Annotated[int, Field()] = 999999 mandatory: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None key_sequence: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None @@ -565,6 +580,18 @@ class OdmItemPostInput(ConceptPostInput): ) +class ActivityInstanceRel(PostInputModel): + activity_instance_uid: Annotated[str, Field(min_length=1)] + activity_item_class_uid: Annotated[str, Field(min_length=1)] + odm_form_uid: Annotated[str, Field(min_length=1)] + odm_item_group_uid: Annotated[str, Field(min_length=1)] + order: Annotated[int | None, Field()] = None + primary: Annotated[bool, Field()] = False + preset_response_value: Annotated[str | None, Field()] = None + value_condition: Annotated[str | None, Field()] = None + value_dependent_map: Annotated[str | None, Field()] = None + + class OdmItemPatchInput(ConceptPatchInput): name: Annotated[str, Field(min_length=1)] oid: Annotated[str | None, Field(min_length=1)] @@ -583,12 +610,38 @@ class OdmItemPatchInput(ConceptPatchInput): ) codelist_uid: Annotated[str | None, Field(min_length=1)] terms: Annotated[list[OdmItemTermRelationshipInput], Field()] + activity_instances: list[ActivityInstanceRel] = Field(default_factory=list) _ = model_validator(mode="after")(check_length_and_significant_digits) _english_description_validator = field_validator("descriptions")( has_english_description ) + @field_validator("activity_instances") + @classmethod + def check_activity_instance_uniqueness( + cls, v: list[ActivityInstanceRel] + ) -> list[ActivityInstanceRel]: + activity_instance = [ + ( + elm.activity_instance_uid, + elm.activity_item_class_uid, + elm.odm_form_uid, + elm.odm_item_group_uid, + ) + for elm in v + ] + + duplicates = { + f"{ai}" for ai in activity_instance if activity_instance.count(ai) > 1 + } + + if len(activity_instance) != len(set(activity_instance)): + raise ValueError( + f"Activity Instances must be unique. Following duplicates were found: {", ".join(duplicates)}" + ) + return v + class OdmItemVersion(OdmItem): """ diff --git a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item_group.py b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item_group.py index f6de611c..dd330da4 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item_group.py +++ b/clinical-mdr-api/clinical_mdr_api/models/concepts/odms/odm_item_group.py @@ -144,7 +144,7 @@ def from_odm_item_group_ar( ) for item_uid in odm_item_group_ar.concept_vo.item_uids ], - key=lambda item: item.order_number or "", + key=lambda item: item.order_number, ), vendor_elements=sorted( [ @@ -277,9 +277,7 @@ def from_odm_item_group_uid( oid: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None name: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None version: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None - order_number: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = ( - None - ) + order_number: Annotated[int, Field()] = 999999 mandatory: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None collection_exception_condition_oid: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) diff --git a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term.py b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term.py index 6221fb8f..6d620d20 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term.py +++ b/clinical-mdr-api/clinical_mdr_api/models/controlled_terminologies/ct_term.py @@ -118,7 +118,7 @@ def from_ct_term_name_and_attributes( class CTTermCodelistInput(BaseModel): codelist_uid: Annotated[str, Field()] submission_value: Annotated[str, Field()] - order: Annotated[int | None, Field()] + order: Annotated[int | None, Field()] = None class CTTermCreateInput(PostInputModel): @@ -521,14 +521,20 @@ def from_term_uid_and_codelist_submval( preferred_term: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None - codelist_uid: Annotated[str | None, Field(json_schema_extra={"nullable": True})] - codelist_name: Annotated[str | None, Field(json_schema_extra={"nullable": True})] + codelist_uid: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( + None + ) + codelist_name: Annotated[ + str | None, Field(json_schema_extra={"nullable": True}) + ] = None codelist_submission_value: Annotated[ str | None, Field(json_schema_extra={"nullable": True}), - ] - order: Annotated[int | None, Field(json_schema_extra={"nullable": True})] - submission_value: Annotated[str | None, Field(json_schema_extra={"nullable": True})] + ] = None + order: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = None + submission_value: Annotated[ + str | None, Field(json_schema_extra={"nullable": True}) + ] = None queried_effective_date: Annotated[datetime | None, Field()] = None date_conflict: Annotated[bool | None, Field()] = None diff --git a/clinical-mdr-api/clinical_mdr_api/models/data_suppliers/data_supplier.py b/clinical-mdr-api/clinical_mdr_api/models/data_suppliers/data_supplier.py index 4398ad93..0e6b9ddf 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/data_suppliers/data_supplier.py +++ b/clinical-mdr-api/clinical_mdr_api/models/data_suppliers/data_supplier.py @@ -216,7 +216,7 @@ class DataSupplierVersion(DataSupplier): class DataSupplierInput(InputModel): name: Annotated[str, Field(min_length=1)] - order: Annotated[int, Field()] = 999999 + order: Annotated[int | None, Field(gt=0)] = None supplier_type_uid: Annotated[str, Field(min_length=1)] description: Annotated[str | None, Field(min_length=1)] api_base_url: Annotated[str | None, Field(min_length=1)] @@ -226,5 +226,15 @@ class DataSupplierInput(InputModel): library_name: Annotated[str, Field(min_length=1)] = "Sponsor" -class DataSupplierEditInput(DataSupplierInput): +class DataSupplierEditInput(InputModel): + """Input model for PATCH endpoint - all fields optional except change_description.""" + + name: Annotated[str | None, Field(min_length=1)] = None + order: Annotated[int | None, Field(gt=0)] = None + supplier_type_uid: Annotated[str | None, Field(min_length=1)] = None + description: Annotated[str | None, Field(min_length=1)] = None + api_base_url: Annotated[str | None, Field(min_length=1)] = None + ui_base_url: Annotated[str | None, Field(min_length=1)] = None + origin_source_uid: Annotated[str | None, Field(min_length=1)] = None + origin_type_uid: Annotated[str | None, Field(min_length=1)] = None change_description: Annotated[str, Field(min_length=1)] diff --git a/clinical-mdr-api/clinical_mdr_api/models/listings/listings_study.py b/clinical-mdr-api/clinical_mdr_api/models/listings/listings_study.py index 955a8181..a7242ce4 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/listings/listings_study.py +++ b/clinical-mdr-api/clinical_mdr_api/models/listings/listings_study.py @@ -158,6 +158,7 @@ class RegistryIdentifiersListingModel(BaseModel): nmpa: Annotated[str, Field()] esn: Annotated[str, Field()] ide: Annotated[str, Field()] + eupn: Annotated[str, Field()] @classmethod def from_study_registry_identifiers_vo( @@ -191,6 +192,7 @@ def from_study_registry_identifiers_vo( ide=none_to_empty_str( registry_identifiers_vo.investigational_device_exemption_ide_number ), + eupn=none_to_empty_str(registry_identifiers_vo.eu_pas_number), ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset.py index 8d384da9..d061a9e1 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset.py @@ -9,14 +9,13 @@ class SimpleDatasetClass(BaseModel): dataset_class_uid: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None - ordinal: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None dataset_class_name: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None class SimpleDataModelIG(BaseModel): - ordinal: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None + ordinal: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = None data_model_ig_name: Annotated[str, Field()] @@ -29,7 +28,6 @@ class Dataset(BaseModel): description: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None ) - catalogue_name: Annotated[str, Field()] implemented_dataset_class: Annotated[ SimpleDatasetClass | None, Field(json_schema_extra={"nullable": True}) ] = None @@ -42,7 +40,6 @@ def from_repository_output(cls, input_dict: dict[str, Any]): label=input_dict.get("standard_value").get("label"), title=input_dict.get("standard_value").get("title"), description=input_dict.get("standard_value").get("description"), - catalogue_name=input_dict["catalogue_name"], implemented_dataset_class=( SimpleDatasetClass( dataset_class_uid=input_dict.get("implemented_dataset_class").get( @@ -51,7 +48,6 @@ def from_repository_output(cls, input_dict: dict[str, Any]): dataset_class_name=input_dict.get("implemented_dataset_class").get( "dataset_class_name" ), - ordinal=input_dict.get("implemented_dataset_class").get("ordinal"), ) if input_dict.get("implemented_dataset_class") else None diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_class.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_class.py index 13bdffc2..86cbfbe2 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_class.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_class.py @@ -5,12 +5,12 @@ from clinical_mdr_api.models.utils import BaseModel -class SimpleDataModel(BaseModel): +class SimpleDataModelForDatasetClass(BaseModel): data_model_name: Annotated[str, Field()] ordinal: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None -class SimpleDataset(BaseModel): +class SimpleDatasetForDatasetClass(BaseModel): uid: Annotated[str, Field()] dataset_name: Annotated[str, Field()] @@ -26,7 +26,7 @@ class DatasetClass(BaseModel): parent_class: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None ) - data_models: Annotated[list[SimpleDataModel], Field()] + data_models: Annotated[list[SimpleDataModelForDatasetClass], Field()] @classmethod def from_repository_output(cls, input_dict: dict[str, Any]): @@ -38,7 +38,7 @@ def from_repository_output(cls, input_dict: dict[str, Any]): catalogue_name=input_dict["catalogue_name"], parent_class=input_dict.get("parent_class_name"), data_models=[ - SimpleDataModel( + SimpleDataModelForDatasetClass( data_model_name=data_model.get("data_model_name"), ordinal=data_model.get("ordinal"), ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_scenario.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_scenario.py index ff3cc53b..a3d2054f 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_scenario.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_scenario.py @@ -2,7 +2,9 @@ from pydantic import Field -from clinical_mdr_api.models.standard_data_models.dataset_variable import SimpleDataset +from clinical_mdr_api.models.standard_data_models.dataset_variable import ( + SimpleDatasetForDatasetVariable, +) from clinical_mdr_api.models.utils import BaseModel @@ -10,7 +12,7 @@ class DatasetScenario(BaseModel): uid: Annotated[str, Field()] label: Annotated[str, Field()] catalogue_name: Annotated[str, Field()] - dataset: Annotated[SimpleDataset, Field()] + dataset: Annotated[SimpleDatasetForDatasetVariable, Field()] data_model_ig_names: Annotated[list[str], Field()] @classmethod @@ -19,7 +21,7 @@ def from_repository_output(cls, input_dict: dict[str, Any]): uid=input_dict["uid"], label=input_dict.get("standard_value").get("label"), catalogue_name=input_dict["catalogue_name"], - dataset=SimpleDataset( + dataset=SimpleDatasetForDatasetVariable( ordinal=input_dict.get("dataset").get("ordinal"), uid=input_dict.get("dataset").get("uid"), ), diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_variable.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_variable.py index 37bd21d5..8d171cbe 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_variable.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/dataset_variable.py @@ -15,8 +15,8 @@ class SimpleImplementsVariable(BaseModel): name: Annotated[str, Field()] -class SimpleDataset(BaseModel): - ordinal: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None +class SimpleDatasetForDatasetVariable(BaseModel): + ordinal: Annotated[int | None, Field(json_schema_extra={"nullable": True})] = None uid: Annotated[str, Field()] @@ -73,7 +73,7 @@ class DatasetVariable(BaseModel): analysis_variable_set: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None - dataset: Annotated[SimpleDataset, Field()] + dataset: Annotated[SimpleDatasetForDatasetVariable, Field()] data_model_ig_names: Annotated[ list[str], Field( @@ -86,9 +86,9 @@ class DatasetVariable(BaseModel): has_mapping_target: Annotated[ SimpleMappingTarget | None, Field(json_schema_extra={"nullable": True}) ] = None - catalogue_name: Annotated[str, Field()] - referenced_codelist: Annotated[ - SimpleReferencedCodelist | None, Field(json_schema_extra={"nullable": True}) + referenced_codelists: Annotated[ + list[SimpleReferencedCodelist] | None, + Field(json_schema_extra={"nullable": True}), ] = None @classmethod @@ -109,9 +109,8 @@ def from_repository_output(cls, input_dict: dict[str, Any]): described_value_domain=input_dict.get("described_value_domain"), value_list=input_dict.get("value_list") or [], analysis_variable_set=input_dict.get("analysis_variable_set"), - catalogue_name=input_dict["catalogue_name"], data_model_ig_names=input_dict["data_model_ig_names"], - dataset=SimpleDataset( + dataset=SimpleDatasetForDatasetVariable( uid=input_dict.get("dataset").get("uid"), ordinal=input_dict.get("dataset").get("ordinal"), ), @@ -131,12 +130,14 @@ def from_repository_output(cls, input_dict: dict[str, Any]): if input_dict.get("has_mapping_target") else None ), - referenced_codelist=( - SimpleReferencedCodelist( - uid=input_dict.get("referenced_codelist").get("uid"), - name=input_dict.get("referenced_codelist").get("name"), - ) - if input_dict.get("referenced_codelist") - else None + referenced_codelists=( + [ + SimpleReferencedCodelist( + uid=cl.get("uid"), + name=cl.get("name"), + ) + for cl in input_dict.get("referenced_codelists") + ] + or None ), ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/variable_class.py b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/variable_class.py index a1077dd0..f8ffe09e 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/variable_class.py +++ b/clinical-mdr-api/clinical_mdr_api/models/standard_data_models/variable_class.py @@ -8,12 +8,12 @@ from clinical_mdr_api.models.utils import BaseModel -class SimpleReferencedCodelist(BaseModel): +class SimpleReferencedCodelistForVariableClass(BaseModel): uid: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None name: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None -class SimpleDatasetClass(BaseModel): +class SimpleDatasetClassForVariableClass(BaseModel): ordinal: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None dataset_class_name: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) @@ -58,7 +58,7 @@ class VariableClass(BaseModel): str | None, Field(json_schema_extra={"nullable": True}) ] = None examples: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None - dataset_class: Annotated[SimpleDatasetClass, Field()] + dataset_class: Annotated[SimpleDatasetClassForVariableClass, Field()] dataset_variable_name: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None @@ -67,8 +67,9 @@ class VariableClass(BaseModel): has_mapping_target: Annotated[ SimpleMappingTarget | None, Field(json_schema_extra={"nullable": True}) ] = None - referenced_codelist: Annotated[ - SimpleReferencedCodelist | None, Field(json_schema_extra={"nullable": True}) + referenced_codelists: Annotated[ + list[SimpleReferencedCodelistForVariableClass] | None, + Field(json_schema_extra={"nullable": True}), ] = None qualifies_variable: Annotated[ SimpleVariableClass | None, Field(json_schema_extra={"nullable": True}) @@ -104,7 +105,7 @@ def from_repository_output(cls, input_dict: dict[str, Any]): ), examples=input_dict.get("standard_value").get("examples"), catalogue_name=input_dict["catalogue_name"], - dataset_class=SimpleDatasetClass( + dataset_class=SimpleDatasetClassForVariableClass( dataset_class_name=input_dict.get("dataset_class").get( "dataset_class_name" ), @@ -112,13 +113,15 @@ def from_repository_output(cls, input_dict: dict[str, Any]): ), dataset_variable_name=input_dict.get("dataset_variable_name"), data_model_names=input_dict["data_model_names"], - referenced_codelist=( - SimpleReferencedCodelist( - uid=input_dict.get("referenced_codelist").get("uid"), - name=input_dict.get("referenced_codelist").get("name"), - ) - if input_dict.get("referenced_codelist") - else None + referenced_codelists=( + [ + SimpleReferencedCodelistForVariableClass( + uid=cl.get("uid"), + name=cl.get("name"), + ) + for cl in input_dict.get("referenced_codelists") + ] + or None ), has_mapping_target=( SimpleMappingTarget( diff --git a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study.py b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study.py index 83793226..beb8d864 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study.py +++ b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study.py @@ -119,10 +119,17 @@ class StudySoaPreferences(StudySoaPreferencesInput): study_uid: Annotated[str, Field(description="Uid of study")] +class StudySoaSplitInput(PatchInputModel): + model_config = ConfigDict(title="SoA Split uid input") + uid: Annotated[str, Field(description="Uid of a StudyVisit")] + + +class StudySoaSplit(StudySoaSplitInput): + model_config = ConfigDict(title="SoA Split uid") + study_uid: Annotated[str, Field(description="Uid of study")] + + class RegistryIdentifiersJsonModel(BaseModel): - model_config = ConfigDict( - title="RegistryIdentifiersMetadata metadata for study definition" - ) ct_gov_id: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None ct_gov_id_null_value_code: Annotated[ @@ -206,6 +213,13 @@ class RegistryIdentifiersJsonModel(BaseModel): SimpleCTTermNameWithConflictFlag | None, Field(json_schema_extra={"nullable": True}), ] = None + eu_pas_number: Annotated[ + str | None, Field(json_schema_extra={"nullable": True}) + ] = None + eu_pas_number_null_value_code: Annotated[ + SimpleCTTermNameWithConflictFlag | None, + Field(json_schema_extra={"nullable": True}), + ] = None @classmethod def from_study_registry_identifiers_vo( @@ -230,6 +244,7 @@ def from_study_registry_identifiers_vo( registry_identifiers_vo.national_medical_products_administration_nmpa_number_null_value_code, registry_identifiers_vo.eudamed_srn_number_null_value_code, registry_identifiers_vo.investigational_device_exemption_ide_number_null_value_code, + registry_identifiers_vo.eu_pas_number_null_value_code, } ) if code is not None @@ -330,6 +345,11 @@ def from_study_registry_identifiers_vo( if registry_identifiers_vo.investigational_device_exemption_ide_number_null_value_code else None ), + eu_pas_number_null_value_code=( + terms[registry_identifiers_vo.eu_pas_number_null_value_code] + if registry_identifiers_vo.eu_pas_number_null_value_code + else None + ), ct_gov_id=registry_identifiers_vo.ct_gov_id, eudract_id=registry_identifiers_vo.eudract_id, universal_trial_number_utn=registry_identifiers_vo.universal_trial_number_utn, @@ -342,11 +362,11 @@ def from_study_registry_identifiers_vo( national_medical_products_administration_nmpa_number=registry_identifiers_vo.national_medical_products_administration_nmpa_number, eudamed_srn_number=registry_identifiers_vo.eudamed_srn_number, investigational_device_exemption_ide_number=registry_identifiers_vo.investigational_device_exemption_ide_number, + eu_pas_number=registry_identifiers_vo.eu_pas_number, ) class StudyIdentificationMetadataJsonModel(BaseModel): - model_config = ConfigDict(title="Identification metadata for study definition") study_number: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None @@ -436,7 +456,6 @@ def from_study_identification_vo( class CompactStudyIdentificationMetadataJsonModel(BaseModel): - model_config = ConfigDict(title="Identification metadata for study definition") study_number: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None @@ -494,7 +513,6 @@ def from_study_identification_vo( class StudyVersionMetadataJsonModel(BaseModel): - model_config = ConfigDict(title="Version metadata for study definition") study_status: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None @@ -529,9 +547,6 @@ def from_study_version_metadata_vo( class HighLevelStudyDesignJsonModel(BaseModel): - model_config = ConfigDict( - title="High level study design parameters for study definition" - ) study_type_code: Annotated[ SimpleCTTermNameWithConflictFlag | None, @@ -560,6 +575,11 @@ class HighLevelStudyDesignJsonModel(BaseModel): Field(json_schema_extra={"nullable": True}), ] = None + development_stage_code: Annotated[ + SimpleCTTermNameWithConflictFlag | None, + Field(json_schema_extra={"nullable": True}), + ] = None + is_extension_trial: Annotated[ bool | None, Field(json_schema_extra={"nullable": True}) ] = None @@ -636,6 +656,7 @@ def from_high_level_study_design_vo( high_level_study_design_vo.trial_type_null_value_code, high_level_study_design_vo.trial_phase_code, high_level_study_design_vo.trial_phase_null_value_code, + high_level_study_design_vo.development_stage_code, high_level_study_design_vo.is_extension_trial_null_value_code, high_level_study_design_vo.is_adaptive_design_null_value_code, high_level_study_design_vo.study_stop_rules_null_value_code, @@ -697,6 +718,11 @@ def from_high_level_study_design_vo( if high_level_study_design_vo.trial_phase_null_value_code else None ), + development_stage_code=( + terms[high_level_study_design_vo.development_stage_code] + if high_level_study_design_vo.development_stage_code + else None + ), is_extension_trial_null_value_code=( terms[high_level_study_design_vo.is_extension_trial_null_value_code] if high_level_study_design_vo.is_extension_trial_null_value_code @@ -741,7 +767,6 @@ def from_high_level_study_design_vo( class StudyPopulationJsonModel(BaseModel): - model_config = ConfigDict(title="Study population parameters for study definition") therapeutic_area_codes: Annotated[ list[SimpleTermModel] | None, Field(json_schema_extra={"nullable": True}) @@ -1077,9 +1102,6 @@ def from_study_population_vo( class StudyInterventionJsonModel(BaseModel): - model_config = ConfigDict( - title="Study interventions parameters for study definition" - ) intervention_type_code: Annotated[ SimpleCTTermNameWithConflictFlag | None, @@ -1316,7 +1338,6 @@ def from_study_intervention_vo( class StudyDescriptionJsonModel(BaseModel): - model_config = ConfigDict(title="Study description for the study definition") study_title: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = ( None @@ -1346,7 +1367,6 @@ def from_study_description_vo( class CompactStudyMetadataJsonModel(BaseModel): - model_config = ConfigDict(title="Compact Study Metadata") identification_metadata: Annotated[ CompactStudyIdentificationMetadataJsonModel | None, @@ -1383,7 +1403,6 @@ def from_study_metadata_vo( class StudyMetadataJsonModel(BaseModel): - model_config = ConfigDict(title="Study Metadata") identification_metadata: Annotated[ StudyIdentificationMetadataJsonModel | None, @@ -1455,7 +1474,6 @@ def from_study_metadata_vo( class StudyPatchRequestJsonModel(PatchInputModel): - model_config = ConfigDict(title="StudyPatchRequest") study_parent_part_uid: Annotated[ str | None, Field(description="UID of the Study Parent Part") @@ -1943,6 +1961,9 @@ class StudyProtocolTitle(BaseModel): substance_name: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None + development_stage_code: Annotated[ + SimpleTermModel | None, Field(json_schema_extra={"nullable": True}) + ] = None @classmethod def from_study_definition_ar( @@ -1965,6 +1986,10 @@ def from_study_definition_ar( c_code=current_metadata.high_level_study_design.trial_phase_code, find_term_by_uid=find_term_by_uid, ), + development_stage_code=SimpleTermModel.from_ct_code( + c_code=current_metadata.high_level_study_design.development_stage_code, + find_term_by_uid=find_term_by_uid, + ), ind_number=current_metadata.id_metadata.registry_identifiers.investigational_new_drug_application_number_ind, ) diff --git a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_pharma_cm.py b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_pharma_cm.py index 2a447fe2..223c6829 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_pharma_cm.py +++ b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_pharma_cm.py @@ -22,7 +22,7 @@ from clinical_mdr_api.services._utils import get_name_or_none -class CompactStudyArm(BaseModel): +class CompactStudyArmForPharmaCM(BaseModel): arm_type: Annotated[str | None, Field(json_schema_extra={"nullable": True})] = None arm_title: Annotated[str, Field()] arm_description: Annotated[ @@ -82,7 +82,7 @@ class StudyPharmaCM(BaseModel): number_of_subjects: Annotated[ int | None, Field(json_schema_extra={"nullable": True}) ] = None - study_arms: list[CompactStudyArm] = Field(default_factory=list) + study_arms: list[CompactStudyArmForPharmaCM] = Field(default_factory=list) intervention_type: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None @@ -246,6 +246,13 @@ def from_various_data( id_type=registry_identifier, ) ) + if study.current_metadata.id_metadata.registry_identifiers.eu_pas_number: + secondary_ids.append( + CompactRegistryIdentifier( + secondary_id=study.current_metadata.id_metadata.registry_identifiers.eu_pas_number, + id_type=registry_identifier, + ) + ) return cls( unique_protocol_identification_number=f"{study.current_metadata.id_metadata.study_id_prefix}-{study.current_metadata.id_metadata.study_number}", brief_title=study.current_metadata.study_description.study_short_title, @@ -279,7 +286,7 @@ def from_various_data( if study_arm.number_of_subjects ), study_arms=[ - CompactStudyArm( + CompactStudyArmForPharmaCM( arm_type=get_name_or_none(find_term_by_uid(study_arm.arm_type_uid)), arm_title=study_arm.name, arm_description=study_arm.description, diff --git a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_selection.py b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_selection.py index 9a860e42..cb18c210 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_selection.py @@ -12,7 +12,13 @@ TypeVar, ) -from pydantic import ConfigDict, Field, field_validator, model_validator +from pydantic import ( + ConfigDict, + Field, + StringConstraints, + field_validator, + model_validator, +) from clinical_mdr_api.domain_repositories.study_selections.study_activity_instance_repository import ( SelectionHistory as StudyActivityInstanceSelectionHistory, @@ -174,9 +180,13 @@ STUDY_ACTIVITY_UID_DESC = "uid for the study activity" STUDY_ACTIVITY_INSTANCE_UID_DESC = "uid for the study activity instance" STUDY_ARM_UID_DESC = "the uid of the related study arm" +STUDY_ARM_NAME_DESC = "the name of the related study arm" STUDY_EPOCH_UID_DESC = "the uid of the related study epoch" +STUDY_EPOCH_NAME_DESC = "the name of the related study epoch" STUDY_ELEMENT_UID_DESC = "the uid of the related study element" +STUDY_ELEMENT_NAME_DESC = "the name of the related study element" STUDY_BRANCH_ARM_UID_DESC = "the uid of the related study branch arm" +STUDY_BRANCH_ARM_NAME_DESC = "the name of the related study branch arm" STUDY_COHORT_ARM_UID_DESC = "the uid of the related study cohort" ARM_UID_DESC = "uid for the study arm" ELEMENT_UID_DESC = "uid for the study element" @@ -2481,6 +2491,16 @@ class StudyActivityReplaceActivityInput(StudySelectionActivityInput): activity_uid: Annotated[str, Field()] +class StudyActivityReplaceActivityListInput(BaseModel): + replacements: Annotated[ + list[StudyActivityReplaceActivityInput], + Field( + min_length=1, + description="List of activity replacements. First item replaces the original StudyActivity, rest create new ones.", + ), + ] + + class StudySelectionActivityRequestEditInput(StudySelectionActivityInput): soa_group_term_uid: Annotated[ str | None, Field(description="flowchart CT term uid") @@ -2561,7 +2581,7 @@ class StudySelectionActivityReviewBatchInput(BatchInputModel): # # Study Activity Instance # -class CompactActivity(BaseModel): +class CompactActivityForSelection(BaseModel): uid: Annotated[str, Field(description="Activity UID")] name: Annotated[ str | None, @@ -2758,7 +2778,7 @@ class StudySelectionActivityInstance(BaseModel): json_schema_extra={"nullable": True}, ), ] = None - activity: Annotated[CompactActivity, Field()] + activity: Annotated[CompactActivityForSelection, Field()] activity_instance: Annotated[ CompactActivityInstance | None, Field(json_schema_extra={"nullable": True}) ] = None @@ -2812,11 +2832,40 @@ class StudySelectionActivityInstance(BaseModel): ), ] = None order: Annotated[int | None, Field()] = None + # Data supplier and origin fields (L3 SoA) + study_data_supplier_uid: Annotated[ + str | None, + Field( + description="UID of the study data supplier linked to this activity instance", + json_schema_extra={"nullable": True}, + ), + ] = None + study_data_supplier_name: Annotated[ + str | None, + Field( + description="Name of the study data supplier", + json_schema_extra={"nullable": True}, + ), + ] = None + origin_type: Annotated[ + SimpleCodelistTermModel | None, + Field( + description="Origin type CT term (e.g. Collected, Derived, Assigned)", + json_schema_extra={"nullable": True}, + ), + ] = None + origin_source: Annotated[ + SimpleCodelistTermModel | None, + Field( + description="Origin source CT term (e.g. Sponsor, Investigator, Subject)", + json_schema_extra={"nullable": True}, + ), + ] = None @classmethod def _get_state_out_of_activity_and_activity_instance( cls, - activity: CompactActivity, + activity: CompactActivityForSelection, activity_instance: CompactActivityInstance | None, study_selection: StudySelectionActivityInstanceVO, keep_old_version: bool = False, @@ -2844,7 +2893,7 @@ def from_study_selection_history( study_selection_history: StudyActivityInstanceSelectionHistory, study_uid: str, ) -> Self: - activity = CompactActivity.activity_from_study_activity_instance_vo( + activity = CompactActivityForSelection.activity_from_study_activity_instance_vo( study_activity_instance_vo=study_selection_history ) activity_instance = ( @@ -2898,6 +2947,28 @@ def from_study_selection_history( author_username=UserInfoService.get_author_username_from_id( study_selection_history.author_id ), + study_data_supplier_uid=study_selection_history.study_data_supplier_uid, + study_data_supplier_name=study_selection_history.study_data_supplier_name, + origin_type=( + SimpleCodelistTermModel( + term_uid=study_selection_history.origin_type_uid, + term_name=study_selection_history.origin_type_name, + codelist_uid=study_selection_history.origin_type_codelist_uid, + ) + if study_selection_history.origin_type_uid + and study_selection_history.origin_type_name + else None + ), + origin_source=( + SimpleCodelistTermModel( + term_uid=study_selection_history.origin_source_uid, + term_name=study_selection_history.origin_source_name, + codelist_uid=study_selection_history.origin_source_codelist_uid, + ) + if study_selection_history.origin_source_uid + and study_selection_history.origin_source_name + else None + ), ) @classmethod @@ -2907,8 +2978,10 @@ def from_study_selection_activity_instance_vo_and_order( study_selection: StudySelectionActivityInstanceVO, ) -> Self: - selected_activity = CompactActivity.activity_from_study_activity_instance_vo( - study_activity_instance_vo=study_selection + selected_activity = ( + CompactActivityForSelection.activity_from_study_activity_instance_vo( + study_activity_instance_vo=study_selection + ) ) selected_activity_instance = ( CompactActivityInstance.activity_instance_from_study_activity_instance_vo( @@ -3022,6 +3095,27 @@ def from_study_selection_activity_instance_vo_and_order( if study_selection.study_activity_instance_baseline_visits else None ), + study_data_supplier_uid=study_selection.study_data_supplier_uid, + study_data_supplier_name=study_selection.study_data_supplier_name, + origin_type=( + SimpleCodelistTermModel( + term_uid=study_selection.origin_type_uid, + term_name=study_selection.origin_type_name, + codelist_uid=study_selection.origin_type_codelist_uid, + ) + if study_selection.origin_type_uid and study_selection.origin_type_name + else None + ), + origin_source=( + SimpleCodelistTermModel( + term_uid=study_selection.origin_source_uid, + term_name=study_selection.origin_source_name, + codelist_uid=study_selection.origin_source_codelist_uid, + ) + if study_selection.origin_source_uid + and study_selection.origin_source_name + else None + ), ) @@ -3039,6 +3133,9 @@ class StudySelectionActivityInstanceCreateInput(PostInputModel): ] = False is_important: Annotated[bool, Field()] = False baseline_visit_uids: Annotated[list[str] | None, Field()] = None + study_data_supplier_uid: Annotated[str | None, Field()] = None + origin_type_uid: Annotated[str | None, Field()] = None + origin_source_uid: Annotated[str | None, Field()] = None class StudySelectionActivityInstanceEditInput(PatchInputModel): @@ -3056,6 +3153,9 @@ class StudySelectionActivityInstanceEditInput(PatchInputModel): ] = False is_important: Annotated[bool, Field()] = False baseline_visit_uids: Annotated[list[str] | None, Field()] = None + study_data_supplier_uid: Annotated[str | None, Field()] = None + origin_type_uid: Annotated[str | None, Field()] = None + origin_source_uid: Annotated[str | None, Field()] = None class StudySelectionActivityInstanceBatchEditInput(InputModel): @@ -3070,6 +3170,9 @@ class StudySelectionActivityInstanceBatchEditInput(InputModel): ] = False is_important: Annotated[bool, Field()] = False baseline_visit_uids: Annotated[list[str] | None, Field()] = None + study_data_supplier_uid: Annotated[str | None, Field()] = None + origin_type_uid: Annotated[str | None, Field()] = None + origin_source_uid: Annotated[str | None, Field()] = None class StudySelectionActivityInstanceBatchInput(BatchInputModel): @@ -3438,6 +3541,11 @@ class StudyDesignCellHistory(BaseModel): Field(description=STUDY_ARM_UID_DESC, json_schema_extra={"nullable": True}), ] = None + study_arm_name: Annotated[ + str | None, + Field(description=STUDY_ARM_NAME_DESC, json_schema_extra={"nullable": True}), + ] = None + study_branch_arm_uid: Annotated[ str | None, Field( @@ -3445,13 +3553,32 @@ class StudyDesignCellHistory(BaseModel): ), ] = None + study_branch_arm_name: Annotated[ + str | None, + Field( + description=STUDY_BRANCH_ARM_NAME_DESC, json_schema_extra={"nullable": True} + ), + ] = None + study_epoch_uid: Annotated[str, Field(description=STUDY_EPOCH_UID_DESC)] + study_epoch_name: Annotated[ + str | None, + Field(description=STUDY_EPOCH_NAME_DESC, json_schema_extra={"nullable": True}), + ] = None + study_element_uid: Annotated[ str | None, Field(description=STUDY_ELEMENT_UID_DESC, json_schema_extra={"nullable": True}), ] = None + study_element_name: Annotated[ + str | None, + Field( + description=STUDY_ELEMENT_NAME_DESC, json_schema_extra={"nullable": True} + ), + ] = None + transition_rule: Annotated[ str | None, Field(description=TRANSITION_RULE_DESC, json_schema_extra={"nullable": True}), @@ -3459,6 +3586,14 @@ class StudyDesignCellHistory(BaseModel): change_type: Annotated[str | None, CHANGE_TYPE_FIELD] = None + author_username: Annotated[ + str | None, + Field( + description=AUTHOR_FIELD_DESC, + json_schema_extra={"nullable": True}, + ), + ] = None + modified: Annotated[ datetime | None, Field( @@ -3488,7 +3623,9 @@ class StudyDesignCellCreateInput(PostInputModel): study_element_uid: Annotated[str, Field(description=STUDY_ELEMENT_UID_DESC)] transition_rule: Annotated[ - str | None, Field(description="Optionally, a transition rule for the cell") + str | None, + StringConstraints(max_length=200), + Field(description="Optionally, a transition rule for the cell"), ] = None order: Annotated[ @@ -3523,6 +3660,7 @@ class StudyDesignCellEditInput(PatchInputModel): ] = None transition_rule: Annotated[ str | None, + StringConstraints(max_length=200), Field( json_schema_extra={"nullable": True}, description=TRANSITION_RULE_DESC, diff --git a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_visit.py b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_visit.py index bbac06b0..8037d88d 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_visit.py +++ b/clinical-mdr-api/clinical_mdr_api/models/study_selections/study_visit.py @@ -176,6 +176,9 @@ class StudyVisitBase(BaseModel): consecutive_visit_group: Annotated[ str | None, Field(json_schema_extra={"nullable": True}) ] = None + consecutive_visit_group_uid: Annotated[ + str | None, Field(json_schema_extra={"nullable": True}) + ] = None show_visit: Annotated[bool, Field()] min_visit_window_value: Annotated[ int | None, Field(json_schema_extra={"nullable": True}) @@ -398,6 +401,9 @@ def transform_to_response_model( consecutive_visit_group=( visit.study_visit_group.group_name if visit.study_visit_group else None ), + consecutive_visit_group_uid=( + visit.study_visit_group.uid if visit.study_visit_group else None + ), show_visit=visit.show_visit, min_visit_window_value=visit.visit_window_min, max_visit_window_value=visit.visit_window_max, @@ -455,7 +461,7 @@ class VisitConsecutiveGroupInput(PostInputModel): format: Annotated[ VisitGroupFormat, Field( - description="""The way how the Visits should be groupped. The possible values are: range or list. + description="""The way how the Visits should be grouped. The possible values are: range or list. The range technique will name the group in the following way (V4-V6), the list technique will generate the group name in the following way (V4,V5,V6)""", ), @@ -466,3 +472,8 @@ class VisitConsecutiveGroupInput(PostInputModel): description="The uid of the visit from which get properties to overwrite" ), ] = None + + +class StudyVisitGroup(BaseModel): + uid: Annotated[str, Field()] + group_name: Annotated[str, Field()] diff --git a/clinical-mdr-api/clinical_mdr_api/models/validators.py b/clinical-mdr-api/clinical_mdr_api/models/validators.py index 70f3e339..3434fe89 100644 --- a/clinical-mdr-api/clinical_mdr_api/models/validators.py +++ b/clinical-mdr-api/clinical_mdr_api/models/validators.py @@ -113,7 +113,7 @@ def is_language_supported(value: str): for key in keys: try: # This function will throw an exception if the language isn't found - get_iso_lang_data(query=value, key=key, return_key=key) # type: ignore[call-overload] + get_iso_lang_data(query=value, return_key=key) # type: ignore[call-overload] return value except ValidationException: if key == keys[-1]: diff --git a/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py b/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py index ab5bd5ee..f08f1289 100644 --- a/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py +++ b/clinical-mdr-api/clinical_mdr_api/repositories/_utils.py @@ -12,7 +12,7 @@ from pydantic.types import T from clinical_mdr_api.models.biomedical_concepts.activity_item_class import ( - CompactActivityInstanceClass, + CompactActivityInstanceClassForActivityItemClass, ) from clinical_mdr_api.models.concepts.activities.activity import ( ActivityGroupingHierarchySimpleModel, @@ -889,7 +889,7 @@ def build_filter_clause(self) -> None: ) elif ( get_field_type(attr_desc.annotation) - is CompactActivityInstanceClass + is CompactActivityInstanceClassForActivityItemClass ): # Wildcard filtering for activity_instance_classes # Search in the name field of each activity instance class diff --git a/clinical-mdr-api/clinical_mdr_api/routers/_generic_descriptions.py b/clinical-mdr-api/clinical_mdr_api/routers/_generic_descriptions.py index 9c14c418..c9f7df20 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/_generic_descriptions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/_generic_descriptions.py @@ -1,4 +1,9 @@ +import json +import typing +from typing import Annotated, Any, cast + from fastapi import Query +from pydantic import BeforeValidator from clinical_mdr_api.models.validators import FLOAT_REGEX from common.config import settings @@ -41,6 +46,26 @@ def study_fields_audit_trail_section_description(desc: str): LIBRARY_NAME = "Library name" + +def _parse_json_validator(value: typing.Any) -> typing.Any: + """ + Validator function that automatically parses JSON string to dict. + Used with BeforeValidator to parse query parameters (filters, sort_by, etc.). + """ + if value is None: + return None + if isinstance(value, dict): + return value # Already parsed + if isinstance(value, str): + if not value.strip(): + return None + try: + return json.loads(value) + except json.JSONDecodeError as exc: + raise ValueError(f"Invalid JSON format: {value}") from exc + raise ValueError(f"Invalid type: {type(value)}") + + SORT_BY = """ JSON dictionary of field names and boolean flags specifying the sort order. Supported values for sort order are: - `true` - ascending order\n @@ -55,12 +80,30 @@ def study_fields_audit_trail_section_description(desc: str): Example: `{"topic_code": true, "name": false}` sorts the returned list by `topic_code ascending`, then by `name descending`. """ +# Base Query definition for sort_by (for OpenAPI schema) +_SORT_BY_QUERY_BASE = Query(description=SORT_BY) + +# Reusable Query parameter for sort_by - automatically parses JSON string to dict +# Usage: sort_by: SORT_BY_QUERY = None +# The validator automatically parses the JSON string to a dict +# Note: Using Any as the type to work with FastAPI query parameters, validator converts to dict | None +SORT_BY_QUERY = Annotated[ + Any, + BeforeValidator(_parse_json_validator), + _SORT_BY_QUERY_BASE, +] + PAGE_NUMBER = """ Page number of the returned list of entities.\n Functionality : provided together with `page_size`, selects a page to retrieve for paginated results.\n Errors: `page_size` not provided, `page_number` must be equal or greater than 1. """ +# Reusable Query parameter for page_number with maximum constraint +PAGE_NUMBER_QUERY = Annotated[ + int, Query(ge=1, le=settings.max_page_number, description=PAGE_NUMBER) +] + PAGE_SIZE = f""" Number of items to be returned per page.\n Default: {settings.default_page_size}\n @@ -69,6 +112,16 @@ def study_fields_audit_trail_section_description(desc: str): Errors: `page_number` not provided. """ +# Reusable Query parameter for page_size with maximum constraint (allows page_size=0 for "all rows") +PAGE_SIZE_QUERY = Annotated[ + int, Query(ge=0, le=settings.max_page_size, description=PAGE_SIZE) +] + +# Reusable Query parameter for page_size with minimum constraint of 1 (does not allow page_size=0) +PAGE_SIZE_QUERY_MIN_1 = Annotated[ + int, Query(ge=1, le=settings.max_page_size, description=PAGE_SIZE) +] + FILTERS = """ JSON dictionary of field names and search strings, with a choice of operators for building complex filtering queries. @@ -113,11 +166,14 @@ def study_fields_audit_trail_section_description(desc: str): "Functionality: `and` will return entities having all filters matching, `or` will return entities with any matches.\n\n" ) +# Reusable Query parameter for operator +FILTER_OPERATOR_QUERY = Annotated[str, Query(description=FILTER_OPERATOR)] + FILTERS_EXAMPLE = { "none": { "summary": "No Filters", "description": "No filters are applied.", - "value": {}, + "value": "{}", }, "wildcard": { "summary": "Wildcard Filter", @@ -151,16 +207,22 @@ def study_fields_audit_trail_section_description(desc: str): "Functionality: retrieve total count of queried entities.\n\n" ) +# Reusable Query parameter for total_count +TOTAL_COUNT_QUERY = Annotated[bool, Query(description=TOTAL_COUNT)] + HEADER_FIELD_NAME = ( "The field name for which to lookup possible values in the database.\n\n" "Functionality: searches for possible values (aka 'headers') of this field in the database." "Errors: invalid field name specified" ) +HEADER_FIELD_NAME_QUERY = Annotated[str, Query(description=HEADER_FIELD_NAME)] HEADER_SEARCH_STRING = """Optionally, a (part of the) text for a given field. The query result will be values of the field that contain the provided search string.""" +HEADER_SEARCH_STRING_QUERY = Annotated[str, Query(description=HEADER_SEARCH_STRING)] HEADER_PAGE_SIZE = "Optionally, the number of results to return. Default = 10." +HEADER_PAGE_SIZE_QUERY = Annotated[int, Query(description=HEADER_PAGE_SIZE)] HEADERS_QUERY_LITE = "Whether to use the lightweight implementation of this endpoint, which doesn't support `filters` and `operator` parameters." @@ -174,7 +236,7 @@ def study_fields_audit_trail_section_description(desc: str): - `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`\n """ -STUDY_VALUE_VERSION_QUERY = Query( +STUDY_VALUE_VERSION_QUERY: Any = Query( description="""If specified, study data with specified version is returned. Only exact matches are considered. @@ -202,3 +264,39 @@ def study_fields_audit_trail_section_description(desc: str): If any provided search term for a given field name is other than a string type, then equal operator will automatically be applied overriding any provided operator. """ ) + + +# Alias for backward compatibility - use _parse_json_validator instead +_parse_filters_validator = _parse_json_validator + + +# Base Query definitions (for OpenAPI schema) +_FILTERS_QUERY_BASE = Query( + description=FILTERS, + openapi_examples=cast("dict[str, Any]", FILTERS_EXAMPLE), +) + +_SYNTAX_FILTERS_QUERY_BASE = Query( + description=SYNTAX_FILTERS, + openapi_examples=cast("dict[str, Any]", FILTERS_EXAMPLE), +) + +# Reusable Query parameter for filters - automatically parses JSON string to dict +# Usage: filters: FILTERS_QUERY = None +# The validator automatically parses the JSON string to a dict +# Note: Using Any as the type to work with FastAPI query parameters, validator converts to dict | None +FILTERS_QUERY = Annotated[ + Any, + BeforeValidator(_parse_filters_validator), + _FILTERS_QUERY_BASE, +] + +# Reusable Query parameter for syntax filters - automatically parses JSON string to dict +# Usage: filters: SYNTAX_FILTERS_QUERY = None +# The validator automatically parses the JSON string to a dict +# Note: Using Any as the type to work with FastAPI query parameters, validator converts to dict | None +SYNTAX_FILTERS_QUERY = Annotated[ + Any, + BeforeValidator(_parse_filters_validator), + _SYNTAX_FILTERS_QUERY_BASE, +] diff --git a/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_instance_classes.py b/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_instance_classes.py index bc38791c..a8a13f58 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_instance_classes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_instance_classes.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.biomedical_concepts.activity_instance_class import ( @@ -14,7 +13,7 @@ ActivityInstanceClassOverview, ActivityInstanceClassWithDataset, ActivityInstanceParentClassOverview, - CompactActivityItemClass, + CompactActivityItemClassForInstanceClass, SimpleActivityInstanceClass, SimpleActivityItemClass, ) @@ -80,33 +79,12 @@ # pylint: disable=unused-argument def get_activity_instance_classes( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstanceClass]: activity_instance_class_service = ActivityInstanceClassService() results = activity_instance_class_service.get_all_items( @@ -138,25 +116,11 @@ def get_activity_instance_classes( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: activity_instance_class_service = ActivityInstanceClassService() return activity_instance_class_service.get_distinct_values_for_header( @@ -231,7 +195,7 @@ def get_activity_item_classes( description="Optionally, the uid of a dataset to filter relevant activity item classes against" ), ] = None, -) -> list[CompactActivityItemClass]: +) -> list[CompactActivityItemClassForInstanceClass]: service = ActivityItemClassService() return service.get_all_for_activity_instance_class( activity_instance_class_uid, ig_uid, dataset_uid @@ -794,23 +758,10 @@ def get_child_instance_classes( str | None, Query(description="Select specific version, omit to view latest version"), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, ) -> GenericFilteringReturn[SimpleActivityInstanceClass]: if version == "": version = None @@ -881,23 +832,10 @@ def get_activity_item_classes_paginated( str | None, Query(description="Select specific version, omit to view latest version"), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, ) -> GenericFilteringReturn[SimpleActivityItemClass]: if version == "": version = None diff --git a/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_item_classes.py b/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_item_classes.py index a592f524..40f1515a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_item_classes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/biomedical_concepts/activity_item_classes.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.biomedical_concepts.activity_item_class import ( @@ -73,33 +72,12 @@ # pylint: disable=unused-argument def get_activity_item_classes( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityItemClass]: activity_item_class_service = ActivityItemClassService() results = activity_item_class_service.get_all_items( @@ -131,25 +109,11 @@ def get_activity_item_classes( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: activity_item_class_service = ActivityItemClassService() return activity_item_class_service.get_distinct_values_for_header( @@ -268,33 +232,12 @@ def get_all_codelists( description="Optionally, the name of a CT Catalogue to filter Codelists." ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityItemClassCodelist]: results = ActivityItemClassService().get_codelists_of_activity_item_class( activity_item_class_uid=activity_item_class_uid, @@ -733,20 +676,9 @@ def get_activity_instance_classes_using_item( str | None, Query(description="Select specific version, omit to view latest version"), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> GenericFilteringReturn[SimpleActivityInstanceClassForItem]: if version == "": version = None diff --git a/clinical-mdr-api/clinical_mdr_api/routers/clinical_programmes/clinical_programmes.py b/clinical-mdr-api/clinical_mdr_api/routers/clinical_programmes/clinical_programmes.py index 549af5e9..c0b32823 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/clinical_programmes/clinical_programmes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/clinical_programmes/clinical_programmes.py @@ -1,7 +1,6 @@ from typing import Annotated, Any -from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json +from fastapi import APIRouter, Body, Path, Request from clinical_mdr_api.models.clinical_programmes.clinical_programme import ( ClinicalProgramme, @@ -47,33 +46,12 @@ # pylint: disable=unused-argument def get_programmes( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> GenericFilteringReturn[ClinicalProgramme]: service = ClinicalProgrammeService() return service.get_all_clinical_programmes( @@ -99,25 +77,11 @@ def get_programmes( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ClinicalProgrammeService() return service.get_clinical_programme_headers( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/comments/comments.py b/clinical-mdr-api/clinical_mdr_api/routers/comments/comments.py index 8642e29b..e28a6007 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/comments/comments.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/comments/comments.py @@ -52,17 +52,8 @@ def get_comment_topics( If `true`, topics whose topic path partially matches the specified `topic_path` are returned.""", ), ] = False, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, ) -> CustomPage[CommentTopic]: results = Service().get_all_comment_topics( topic_path=topic_path, @@ -106,17 +97,8 @@ def get_comment_threads( description="The status of the comment thread. If not specified, comment threads with any status are returned.", ), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, ) -> CustomPage[CommentThread]: results = Service().get_all_comment_threads( topic_path=topic_path, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/active_substances.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/active_substances.py index 17fc266b..65fae63d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/active_substances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/active_substances.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.active_substance import ( @@ -62,6 +61,9 @@ "long_number", "inn", "external_id", + "unii=unii.substance_unii", + "pclass_name=unii.pclass_name", + "pclass_id=unii.pclass_id", "start_date", "version", "status", @@ -78,33 +80,12 @@ def get_active_substances( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActiveSubstance]: active_substance_service = ActiveSubstanceService() results = active_substance_service.get_all_concepts( @@ -175,30 +156,11 @@ def get_active_substances( def get_active_substances_versions( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActiveSubstance]: service = ActiveSubstanceService() results = service.get_all_concept_versions( @@ -231,26 +193,12 @@ def get_active_substances_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: active_substance_service = ActiveSubstanceService() return active_substance_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activities.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activities.py index f96ff51c..fa30b42a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activities.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activities.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.activities.activity import ( @@ -14,7 +13,6 @@ ActivityOverview, ActivityRequestRejectInput, ActivityVersionDetail, - CompactActivity, ) from clinical_mdr_api.models.concepts.activities.activity_instance import ( ActivityInstanceDetail, @@ -131,69 +129,29 @@ def get_activities( " so we won't loose the information about which activity instances has each group" ), ] = True, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, - split_activity_by_groupings: Annotated[ - bool, - Query( - description=""" -Specifies whether Activity should be split into separate rows if it contains multiple groupings.\n -If equals to true, only library_name, sort_by, page_number, page_size, total_count, filters and operator Query parameters will be applied into the query.""" - ), - ] = False, -) -> CustomPage[Activity | CompactActivity]: + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, +) -> CustomPage[Activity]: activity_service = ActivityService() - if split_activity_by_groupings: - results = activity_service.get_compact_activity_with_splitted_groupings( - library=library_name, - sort_by=sort_by, - page_number=page_number, - page_size=page_size, - total_count=total_count, - filter_by=filters, - filter_operator=FilterOperator.from_str(operator), - ) - else: - results = activity_service.get_all_concepts( - library=library_name, - sort_by=sort_by, - page_number=page_number, - page_size=page_size, - total_count=total_count, - filter_by=filters, - filter_operator=FilterOperator.from_str(operator), - activity_subgroup_uid=activity_subgroup_uid, - activity_group_uid=activity_group_uid, - activity_names=activity_names, - activity_subgroup_names=activity_subgroup_names, - activity_group_names=activity_group_names, - group_by_groupings=group_by_groupings, - ) + results = activity_service.get_all_concepts( + library=library_name, + sort_by=sort_by, + page_number=page_number, + page_size=page_size, + total_count=total_count, + filter_by=filters, + filter_operator=FilterOperator.from_str(operator), + activity_subgroup_uid=activity_subgroup_uid, + activity_group_uid=activity_group_uid, + activity_names=activity_names, + activity_subgroup_names=activity_subgroup_names, + activity_group_names=activity_group_names, + group_by_groupings=group_by_groupings, + ) return CustomPage( items=results.items, total=results.total, page=page_number, size=page_size ) @@ -277,30 +235,11 @@ def get_activities_versions( alias="activity_group_names[]", ), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Activity]: activity_service = ActivityService() results = activity_service.get_all_concept_versions( @@ -337,13 +276,9 @@ def get_activities_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", activity_names: Annotated[ list[str] | None, Query( @@ -365,57 +300,28 @@ def get_distinct_values_for_header( alias="activity_group_names[]", ), ] = None, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, - split_activity_by_groupings: Annotated[ - bool, - Query( - description=""" -Specifies whether Activity should be split into separate rows if it contains multiple groupings.\n -If equals to true, only library_name, sort_by, page_number, page_size, total_count, filters and operator Query parameters will be applied into the query.""" - ), - ] = False, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, lite: Annotated[ bool, Query(description=_generic_descriptions.HEADERS_QUERY_LITE), ] = False, ) -> list[Any]: activity_service = ActivityService() - if split_activity_by_groupings: - headers = activity_service.get_compact_activity_with_splitted_groupings_distinct_values_for_header( - library=library_name, - field_name=field_name, - search_string=search_string, - filter_by=filters, - filter_operator=FilterOperator.from_str(operator), - page_size=page_size, - ) - else: - headers = activity_service.get_distinct_values_for_header( - library=library_name, - field_name=field_name, - search_string=search_string, - filter_by=filters, - filter_operator=FilterOperator.from_str(operator), - page_size=page_size, - activity_names=activity_names, - activity_subgroup_names=activity_subgroup_names, - activity_group_names=activity_group_names, - group_by_groupings=False, - lite=lite, - ) - return headers + return activity_service.get_distinct_values_for_header( + library=library_name, + field_name=field_name, + search_string=search_string, + filter_by=filters, + filter_operator=FilterOperator.from_str(operator), + page_size=page_size, + activity_names=activity_names, + activity_subgroup_names=activity_subgroup_names, + activity_group_names=activity_group_names, + group_by_groupings=False, + lite=lite, + ) @router.get( @@ -574,20 +480,9 @@ def get_specific_activity_version_groupings( version: Annotated[ str, Path(description="The version of the activity in format 'x.y'") ], - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ): activity_service = ActivityService() results = activity_service.get_specific_activity_version_groupings( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_groups.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_groups.py index bd9b773b..0553c159 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_groups.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_groups.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.activities.activity_group import ( @@ -72,33 +71,12 @@ def get_activity_groups( request: Request, library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityGroup]: activity_group_service = ActivityGroupService() results = activity_group_service.get_all_concepts( @@ -156,30 +134,11 @@ def get_activity_groups( def get_activity_groups_versions( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityGroup]: activity_group_service = ActivityGroupService() results = activity_group_service.get_all_concept_versions( @@ -212,9 +171,7 @@ def get_activity_groups_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, activity_subgroup_names: Annotated[ list[str] | None, @@ -230,22 +187,10 @@ def get_distinct_values_for_header( alias="activity_names[]", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: activity_group_service = ActivityGroupService() return activity_group_service.get_distinct_values_for_header( @@ -411,20 +356,9 @@ def get_activity_group_subgroups( str | None, Query(description="Select specific version, omit to view latest version"), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[SimpleSubGroup]: if version == "": version = None diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_instances.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_instances.py index e3128d83..c5093c89 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_instances.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.activities.activity_instance import ( @@ -128,33 +127,12 @@ def get_activities( alias="activity_instance_class_names[]", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstance]: activity_instance_service = ActivityInstanceService() results = activity_instance_service.get_all_concepts( @@ -252,30 +230,11 @@ def get_activity_instances_versions( alias="activity_instance_class_names[]", ), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstance]: activity_instance_service = ActivityInstanceService() results = activity_instance_service.get_all_concept_versions( @@ -310,26 +269,12 @@ def get_activity_instances_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, lite: Annotated[ bool, Query(description=_generic_descriptions.HEADERS_QUERY_LITE), diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_sub_groups.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_sub_groups.py index 88025999..a6ad984a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_sub_groups.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/activities/activity_sub_groups.py @@ -3,12 +3,11 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.activities.activity import SimpleActivity -from clinical_mdr_api.models.concepts.activities.activity_group import ActivityGroup from clinical_mdr_api.models.concepts.activities.activity_sub_group import ( + ActivityGroupForActivitySubGroup, ActivitySubGroup, ActivitySubGroupCreateInput, ActivitySubGroupEditInput, @@ -56,43 +55,12 @@ ) def get_activity_subgroups( library_name: Annotated[str | None, Query()] = None, - activity_group_uid: Annotated[ - str | None, Query(description="The unique id of the activity group") - ] = None, - activity_group_names: Annotated[ - list[str] | None, - Query( - description="A list of activity group names to use as a specific filter", - alias="activity_group_names[]", - ), - ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivitySubGroup]: activity_subgroup_service = ActivitySubGroupService() results = activity_subgroup_service.get_all_concepts( @@ -103,8 +71,6 @@ def get_activity_subgroups( total_count=total_count, filter_by=filters, filter_operator=FilterOperator.from_str(operator), - activity_group_uid=activity_group_uid, - activity_group_names=activity_group_names, ) return CustomPage( items=results.items, total=results.total, page=page_number, size=page_size @@ -136,40 +102,11 @@ def get_activity_subgroups( ) def get_activity_subgroups_versions( library_name: Annotated[str | None, Query()] = None, - activity_group_uid: Annotated[ - str | None, Query(description="The unique id of the activity group") - ] = None, - activity_group_names: Annotated[ - list[str] | None, - Query( - description="A list of activity group names to use as a specific filter", - alias="activity_group_names[]", - ), - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivitySubGroup]: activity_subgroup_service = ActivitySubGroupService() results = activity_subgroup_service.get_all_concept_versions( @@ -180,8 +117,6 @@ def get_activity_subgroups_versions( total_count=total_count, filter_by=filters, filter_operator=FilterOperator.from_str(operator), - activity_group_uid=activity_group_uid, - activity_group_names=activity_group_names, ) return CustomPage( items=results.items, total=results.total, page=page_number, size=page_size @@ -204,40 +139,12 @@ def get_activity_subgroups_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - activity_group_names: Annotated[ - list[str] | None, - Query( - description="A list of activity group names to use as a specific filter", - alias="activity_group_names[]", - ), - ] = None, - activity_names: Annotated[ - list[str] | None, - Query( - description="A list of activity names to use as a specific filter", - alias="activity_names[]", - ), - ] = None, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: activity_subgroup_service = ActivitySubGroupService() return activity_subgroup_service.get_distinct_values_for_header( @@ -247,8 +154,6 @@ def get_distinct_values_for_header( filter_by=filters, filter_operator=FilterOperator.from_str(operator), page_size=page_size, - activity_group_names=activity_group_names, - activity_names=activity_names, ) @@ -423,18 +328,14 @@ def get_activities_for_activity_subgroup( description="Search string to filter activities by name or other fields. Case-insensitive partial match." ), ] = "", - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, page_size: Annotated[ int, Query( ge=0, le=settings.max_page_size, description=_generic_descriptions.PAGE_SIZE ), ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[SimpleActivity]: if version == "": version = None @@ -464,7 +365,7 @@ def get_activities_for_activity_subgroup( {_generic_descriptions.DATA_EXPORTS_HEADER} """, status_code=200, - response_model=CustomPage[ActivityGroup], + response_model=CustomPage[ActivityGroupForActivitySubGroup], responses={ 404: _generic_descriptions.ERROR_404, }, @@ -488,19 +389,15 @@ def get_activity_groups_for_subgroup( str | None, Query(description="Select specific version, omit to view latest version"), ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, page_size: Annotated[ int, Query( ge=0, le=settings.max_page_size, description=_generic_descriptions.PAGE_SIZE ), ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, -) -> CustomPage[ActivityGroup]: + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, +) -> CustomPage[ActivityGroupForActivitySubGroup]: if version == "": version = None diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/compound_aliases.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/compound_aliases.py index 84409732..1212bc6f 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/compound_aliases.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/compound_aliases.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.compound_alias import ( @@ -80,33 +79,12 @@ def get_all( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CompoundAlias]: service = CompoundAliasService() results = service.get_all_concepts( @@ -174,30 +152,11 @@ def get_all( def get_compounds_versions( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CompoundAlias]: service = CompoundAliasService() results = service.get_all_concept_versions( @@ -230,26 +189,12 @@ def get_compounds_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = CompoundAliasService() return service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/compounds.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/compounds.py index f8b73cff..98017713 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/compounds.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/compounds.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.compound import ( @@ -80,33 +79,12 @@ def get_compounds( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Compound]: compound_service = CompoundService() results = compound_service.get_all_concepts( @@ -173,30 +151,11 @@ def get_compounds( def get_compounds_versions( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Compound]: service = CompoundService() results = service.get_all_concept_versions( @@ -237,33 +196,12 @@ def get_compounds_versions( ) def get_compounds_simple( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[SimpleCompound]: compound_service = CompoundSimpleService() results = compound_service.get_all_concepts( @@ -296,26 +234,12 @@ def get_compounds_simple( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: compound_service = CompoundService() return compound_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/lag_times.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/lag_times.py index 3b936f9b..e1a1c192 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/lag_times.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/lag_times.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import LagTime, LagTimePostInput from clinical_mdr_api.models.utils import CustomPage @@ -45,33 +44,12 @@ ) def get_lag_times( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[LagTime]: lag_time_service = LagTimeService() results = lag_time_service.get_all_concepts( @@ -104,26 +82,12 @@ def get_lag_times( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = False, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = False, ) -> list[Any]: lag_time_service = LagTimeService() return lag_time_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/medicinal_products.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/medicinal_products.py index b8e00ed7..f8b2c631 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/medicinal_products.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/medicinal_products.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.medicinal_product import ( @@ -82,33 +81,12 @@ def get_medicinal_products( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[MedicinalProduct]: medicinal_product_service = MedicinalProductService() results = medicinal_product_service.get_all_concepts( @@ -181,30 +159,11 @@ def get_medicinal_products( def get_medicinal_products_versions( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[MedicinalProduct]: service = MedicinalProductService() results = service.get_all_concept_versions( @@ -237,26 +196,12 @@ def get_medicinal_products_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: medicinal_product_service = MedicinalProductService() return medicinal_product_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values.py index 6ba583c7..12727e40 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import NumericValue, NumericValuePostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_numeric_values( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[NumericValue]: numeric_value_service = NumericValueService() results = numeric_value_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_numeric_values( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: numeric_value_service = NumericValueService() return numeric_value_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values_with_unit.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values_with_unit.py index 8adda0b4..a92bb101 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values_with_unit.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/numeric_values_with_unit.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import ( NumericValueWithUnit, @@ -50,33 +49,12 @@ ) def get_numeric_values( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[NumericValueWithUnit]: numeric_value_service = NumericValueWithUnitService() results = numeric_value_service.get_all_concepts( @@ -109,26 +87,12 @@ def get_numeric_values( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: numeric_value_service = NumericValueWithUnitService() return numeric_value_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_conditions.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_conditions.py index d60ce07b..85fa9fcd 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_conditions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_conditions.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.odms.odm_condition import ( OdmCondition, @@ -36,33 +35,12 @@ ) def get_all_odm_conditions( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -99,26 +77,12 @@ def get_all_odm_conditions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_condition_service = OdmConditionService() return odm_condition_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_forms.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_forms.py index 4127656e..d1b490d7 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_forms.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_forms.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.odms.odm_common_models import ( @@ -105,33 +104,12 @@ def get_all_odm_forms( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -168,26 +146,12 @@ def get_all_odm_forms( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_form_service = OdmFormService() return odm_form_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_item_groups.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_item_groups.py index 835ee6b2..886f26c5 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_item_groups.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_item_groups.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.odms.odm_common_models import ( @@ -105,33 +104,12 @@ def get_all_odm_item_groups( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -168,26 +146,12 @@ def get_all_odm_item_groups( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_item_group_service = OdmItemGroupService() return odm_item_group_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_items.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_items.py index 0d05b355..fe51f8d0 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_items.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_items.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.odms.odm_common_models import ( @@ -122,33 +121,12 @@ def get_all_odm_items( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -185,26 +163,12 @@ def get_all_odm_items( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_item_service = OdmItemService() return odm_item_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_metadata.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_metadata.py index 93e2af0f..f59f7cfe 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_metadata.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_metadata.py @@ -48,10 +48,8 @@ }, ) def get_aliases( - page_size: Annotated[ - int, Query(ge=1, le=settings.max_page_size) - ] = settings.default_page_size, - page_number: Annotated[int, Query(ge=1)] = 1, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, search: Annotated[ str | None, Query(description="Search by name or context. Search is case insensitive."), @@ -73,17 +71,8 @@ def get_aliases( }, ) def get_descriptions( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, exclude_english: Annotated[ bool, Query(description="Exclude English descriptions (excludes `en` and `eng`)."), @@ -115,10 +104,8 @@ def get_descriptions( }, ) def get_formal_expressions( - page_size: Annotated[ - int, Query(ge=1, le=settings.max_page_size) - ] = settings.default_page_size, - page_number: Annotated[int, Query(ge=1)] = 1, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, search: Annotated[ str | None, Query( @@ -163,21 +150,19 @@ def get_formal_expressions( response_class=Response, ) def get_odm_document( - target_uids: Annotated[list[str], Query()], target_type: TargetType, + targets: Annotated[ + list[str], + Query( + description="List of UIDs and (optionally) versions separated by comma. E.g. `uid1,v1` or `uid1` for latest version.", + ), + ], allowed_namespaces: Annotated[ list[str] | None, Query( description="Names of the Vendor Namespaces to export or `*` to export all available Vendor Namespaces. If not specified, no Vendor Namespaces will be exported." ), ] = None, - version: Annotated[ - str | None, - Query( - description="Get a specific version of the ODM element", - regex="^$|^\\d+\\.\\d+$", - ), - ] = None, pdf: Annotated[ bool, Query(description="Whether or not to export the ODM as a PDF.") ] = False, @@ -192,9 +177,8 @@ def get_odm_document( if allowed_namespaces is None: allowed_namespaces = [] odm_xml_export_service = OdmXmlExporterService( - target_uids, target_type, - version or None, + targets, allowed_namespaces, pdf, stylesheet, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_methods.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_methods.py index d4a4fda7..b638d2e6 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_methods.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_methods.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.odms.odm_method import ( OdmMethod, @@ -36,33 +35,12 @@ ) def get_all_odm_methods( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -99,26 +77,12 @@ def get_all_odm_methods( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_method_service = OdmMethodService() return odm_method_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_study_events.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_study_events.py index b65b9aae..d5b01920 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_study_events.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_study_events.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.odms.odm_study_event import ( @@ -67,33 +66,12 @@ def get_all_odm_study_events( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -130,26 +108,12 @@ def get_all_odm_study_events( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_study_event_service = OdmStudyEventService() return odm_study_event_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_attributes.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_attributes.py index 0861be21..753f70f5 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_attributes.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.odms.odm_vendor_attribute import ( OdmVendorAttribute, @@ -38,33 +37,12 @@ ) def get_all_odm_vendor_attributes( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -101,26 +79,12 @@ def get_all_odm_vendor_attributes( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_vendor_attribute_service = OdmVendorAttributeService() return odm_vendor_attribute_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_elements.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_elements.py index 42234178..e377ad5c 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_elements.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_elements.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.odms.odm_vendor_element import ( OdmVendorElement, @@ -38,33 +37,12 @@ ) def get_all_odm_vendor_elements( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -101,26 +79,12 @@ def get_all_odm_vendor_elements( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_vendor_element_service = OdmVendorElementService() return odm_vendor_element_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_namespaces.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_namespaces.py index 51cb507b..de387dbe 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_namespaces.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/odms/odm_vendor_namespaces.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.odms.odm_vendor_namespace import ( OdmVendorNamespace, @@ -38,33 +37,12 @@ ) def get_all_odm_vendor_namespaces( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, version: Annotated[ str | None, Query(description="Get a specific version of the ODM element") ] = None, @@ -101,26 +79,12 @@ def get_all_odm_vendor_namespaces( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: odm_vendor_namespace_service = OdmVendorNamespaceService() return odm_vendor_namespace_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/pharmaceutical_products.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/pharmaceutical_products.py index 439f4f61..e5ab9915 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/pharmaceutical_products.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/pharmaceutical_products.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.concepts.pharmaceutical_product import ( @@ -60,8 +59,8 @@ "defaults": [ "uid", "external_id", - "dosage_forms=dosage_forms.name", - "routes_of_administration=routes_of_administration.name", + "dosage_forms=dosage_forms.term_name", + "routes_of_administration=routes_of_administration.term_name", "formulations", "start_date", "version", @@ -79,33 +78,12 @@ def get_pharmaceutical_products( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[PharmaceuticalProduct]: pharmaceutical_product_service = PharmaceuticalProductService() results = pharmaceutical_product_service.get_all_concepts( @@ -173,30 +151,11 @@ def get_pharmaceutical_products( def get_pharmaceutical_products_versions( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str | None, Query()] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[PharmaceuticalProduct]: service = PharmaceuticalProductService() results = service.get_all_concept_versions( @@ -229,26 +188,12 @@ def get_pharmaceutical_products_versions( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: pharmaceutical_product_service = PharmaceuticalProductService() return pharmaceutical_product_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/text_values.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/text_values.py index b6288740..713a460d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/text_values.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/text_values.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import TextValue, TextValuePostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_text_values( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[TextValue]: text_value_service = TextValueService() results = text_value_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_text_values( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = False, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = False, ) -> list[Any]: text_value_service = TextValueService() return text_value_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/unit_definitions/unit_definitions.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/unit_definitions/unit_definitions.py index ca7ffcd4..efadbdb2 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/unit_definitions/unit_definitions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/unit_definitions/unit_definitions.py @@ -2,7 +2,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Depends, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.concepts.unit_definitions.unit_definition import ( @@ -108,33 +107,12 @@ def get_all( description="The name of the unit subset to filter, for instance 'Age Unit'." ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[UnitDefinitionModel]: results = service.get_all( library_name=library_name, @@ -170,9 +148,7 @@ def get_all( ) def get_distinct_values_for_header( service: Annotated[UnitDefinitionService, Depends(UnitDefinitionService)], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, dimension: Annotated[ str | None, @@ -186,22 +162,10 @@ def get_distinct_values_for_header( description="The name of the unit subset to filter, for instance 'Age Unit'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return service.get_distinct_values_for_header( library=library_name, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/concepts/visit_names.py b/clinical-mdr-api/clinical_mdr_api/routers/concepts/visit_names.py index 03f80051..3b95c82f 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/concepts/visit_names.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/concepts/visit_names.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import VisitName, VisitNamePostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_visit_names( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[VisitName]: visit_name_service = VisitNameService() results = visit_name_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_visit_names( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: visit_name_service = VisitNameService() return visit_name_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/configuration.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/configuration.py index ba161a8a..dbf286e2 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/configuration.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/configuration.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Annotated -from fastapi import APIRouter, Body, Depends, Path, Query, Request +from fastapi import APIRouter, Body, Path, Query, Request from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.controlled_terminologies.configuration import ( @@ -74,9 +74,8 @@ # pylint: disable=unused-argument def get_all( request: Request, # request is actually required by the allow_exports decorator - service: Annotated[CTConfigService, Depends(CTConfigService)], ) -> list[CTConfigOGM]: - return service.get_all() + return CTConfigService().get_all() @router.get( @@ -96,7 +95,6 @@ def get_all( }, ) def get_by_uid( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], at_specified_date_time: Annotated[ datetime | None, @@ -127,7 +125,7 @@ def get_by_uid( ), ] = None, ) -> CTConfigModel: - return service.get_by_uid( + return CTConfigService().get_by_uid( configuration_uid, version=version, status=status, @@ -189,10 +187,9 @@ def get_by_uid( # pylint: disable=unused-argument def get_versions( request: Request, # request is actually required by the allow_exports decorator - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], ) -> list[CTConfigModel]: - return service.get_versions(configuration_uid) + return CTConfigService().get_versions(configuration_uid) @router.post( @@ -219,12 +216,11 @@ def get_versions( }, ) def post( - service: Annotated[CTConfigService, Depends(CTConfigService)], post_input: Annotated[ CTConfigPostInput, Body(description="The configuration that shall be created.") - ], + ] ) -> CTConfigModel: - return service.post(post_input) # type: ignore + return CTConfigService().post(post_input) # type: ignore @router.patch( @@ -255,7 +251,6 @@ def post( }, ) def patch( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], patch_input: Annotated[ CTConfigPatchInput, @@ -264,7 +259,7 @@ def patch( ), ], ) -> CTConfigModel: - return service.patch(configuration_uid, patch_input) + return CTConfigService().patch(configuration_uid, patch_input) @router.post( @@ -296,10 +291,9 @@ def patch( }, ) def new_version( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], ) -> CTConfigModel: - return service.new_version(configuration_uid) + return CTConfigService().new_version(configuration_uid) @router.post( @@ -330,10 +324,9 @@ def new_version( }, ) def approve( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], ) -> CTConfigModel: - return service.approve(configuration_uid) + return CTConfigService().approve(configuration_uid) @router.delete( @@ -364,10 +357,9 @@ def approve( }, ) def inactivate( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], ) -> CTConfigModel: - return service.inactivate(configuration_uid) + return CTConfigService().inactivate(configuration_uid) @router.post( @@ -398,10 +390,9 @@ def inactivate( }, ) def reactivate( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], ) -> CTConfigModel: - return service.reactivate(configuration_uid) + return CTConfigService().reactivate(configuration_uid) @router.delete( @@ -431,7 +422,6 @@ def reactivate( }, ) def delete( - service: Annotated[CTConfigService, Depends(CTConfigService)], configuration_uid: Annotated[str, CodelistConfigUID], ): - service.delete(configuration_uid) + CTConfigService().delete(configuration_uid) diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_attributes.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_attributes.py index c8da4a3e..2cad1a1e 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_attributes.py @@ -4,7 +4,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.controlled_terminologies.ct_codelist_attributes import ( @@ -58,33 +57,12 @@ def get_codelists( description="If specified, only codelists from given package are returned.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CTCodelistAttributes]: ct_codelist_attribute_service = CTCodelistAttributesService() results = ct_codelist_attribute_service.get_all_ct_codelists( @@ -119,9 +97,7 @@ def get_codelists( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, catalogue_name: Annotated[ str | None, Query( @@ -136,22 +112,10 @@ def get_distinct_values_for_header( str | None, Query(description="If specified, only terms from given package are returned."), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: ct_codelist_attribute_service = CTCodelistAttributesService() return ct_codelist_attribute_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_names.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_names.py index 12ac48ab..17afdb2b 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_names.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelist_names.py @@ -4,7 +4,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.controlled_terminologies.ct_codelist_name import ( @@ -58,33 +57,12 @@ def get_codelists( description="If specified, only codelists from given package are returned.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CTCodelistName]: ct_codelist_name_service = CTCodelistNameService() results = ct_codelist_name_service.get_all_ct_codelists( @@ -119,9 +97,7 @@ def get_codelists( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, catalogue_name: Annotated[ str | None, Query( @@ -136,22 +112,10 @@ def get_distinct_values_for_header( str | None, Query(description="If specified, only terms from given package are returned."), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: ct_codelist_name_service = CTCodelistNameService() return ct_codelist_name_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelists.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelists.py index 8d66b7db..bb793d67 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelists.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_codelists.py @@ -133,33 +133,12 @@ def get_codelists( description="Boolean value to indicate desired package is a sponsor package. Defaults to False.", ), ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, term_filter: Annotated[ Json | None, Query( @@ -225,23 +204,10 @@ def get_sub_codelists_that_have_given_terms( description="If specified, only codelists from given library are returned.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CTCodelistNameAndAttributes]: ct_codelist_service = CTCodelistService() results = ct_codelist_service.get_sub_codelists_that_have_given_terms( @@ -320,9 +286,7 @@ def update_paired_codelist( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, catalogue_name: Annotated[ str | None, Query( @@ -343,22 +307,10 @@ def get_distinct_values_for_header( description="Boolean value to indicate desired package is a sponsor package. Defaults to False.", ), ] = False, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: ct_codelist_service = CTCodelistService() return ct_codelist_service.get_distinct_values_for_header( @@ -400,31 +352,14 @@ def get_codelist_terms( description="""If specified, return the terms that were part of the codelist at the specified date and time in format YYYY-MM-DDThh:mm:ss+hh:mm'""", ), ] = None, - sort_by: Json = Query(None, description=_generic_descriptions.SORT_BY), - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, operator: str | None = Query( "and", description=_generic_descriptions.FILTER_OPERATOR ), - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ): ct_codelist_service = CTCodelistService() @@ -512,31 +447,14 @@ def get_codelist_terms_by_name_or_submval( description="""If specified, return the terms that were part of the codelist at the specified date and time in format YYYY-MM-DDThh:mm:ss+hh:mm'""", ), ] = None, - sort_by: Json = Query(None, description=_generic_descriptions.SORT_BY), - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, operator: str | None = Query( "and", description=_generic_descriptions.FILTER_OPERATOR ), - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ): ct_codelist_service = CTCodelistService() @@ -577,7 +495,8 @@ def get_codelist_terms_by_name_or_submval( "- The codelist doesn't exist.\n" "- The term doesn't exist.\n" "- The codelist is not extensible.\n" - "- The codelist already has passed term.\n", + "- The codelist already has passed term.\n" + "- The term submission value is a new one for this term.\n", }, }, ) @@ -654,19 +573,11 @@ def get_distinct_term_values_for_header( search_string: str | None = Query( "", description=_generic_descriptions.HEADER_SEARCH_STRING ), - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, + filters: _generic_descriptions.FILTERS_QUERY = None, operator: str | None = Query( "and", description=_generic_descriptions.FILTER_OPERATOR ), - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ): ct_codelist_service = CTCodelistService() return ct_codelist_service.get_distinct_term_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_attributes.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_attributes.py index 803d8068..4ea1ed06 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_attributes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_attributes.py @@ -4,7 +4,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.controlled_terminologies.ct_term_attributes import ( @@ -64,33 +63,12 @@ def get_terms( "If true, only terms connected to at least one codelist are returned.", ), ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CTTermAttributes]: ct_term_attribute_service = CTTermAttributesService() results = ct_term_attribute_service.get_all_ct_terms( @@ -127,9 +105,7 @@ def get_terms( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, codelist_uid: Annotated[ str | None, Query(description="If specified, only terms from given codelist are returned."), @@ -146,22 +122,10 @@ def get_distinct_values_for_header( str | None, Query(description="If specified, only terms from given package are returned."), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: ct_term_service = CTTermAttributesService() return ct_term_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_names.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_names.py index 6f16c898..f529ac1d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_names.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_term_names.py @@ -4,7 +4,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.controlled_terminologies.ct_term_name import ( @@ -70,33 +69,12 @@ def get_terms( "If true, only terms connected to at least one codelist are returned.", ), ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CTTermName | CTTermNameSimple]: ct_term_name_service = CTTermNameService() results = ct_term_name_service.get_all_ct_terms( @@ -153,9 +131,7 @@ def get_terms( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, codelist_uid: Annotated[ str | None, Query(description="If specified, only terms from given codelist are returned."), @@ -172,22 +148,10 @@ def get_distinct_values_for_header( str | None, Query(description="If specified, only terms from given package are returned."), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: ct_term_service = CTTermNameService() return ct_term_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_terms.py b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_terms.py index df18d5f7..2589b213 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_terms.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/controlled_terminologies/ct_terms.py @@ -4,7 +4,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.controlled_terminologies.ct_term import ( @@ -140,33 +139,12 @@ def get_all_terms( description="Boolean value to indicate whether or not to include terms removed from codelists. Defaults to False." ), ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CTTermNameAndAttributes]: ct_term_service = CTTermService() results = ct_term_service.get_all_terms( @@ -204,9 +182,7 @@ def get_all_terms( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, codelist_uid: Annotated[ str | None, Query(description="If specified, only terms from given codelist are returned."), @@ -223,22 +199,10 @@ def get_distinct_values_for_header( str | None, Query(description="If specified, only terms from given package are returned."), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, lite: Annotated[ bool, Query(description=_generic_descriptions.HEADERS_QUERY_LITE), diff --git a/clinical-mdr-api/clinical_mdr_api/routers/data_suppliers/data_suppliers.py b/clinical-mdr-api/clinical_mdr_api/routers/data_suppliers/data_suppliers.py index 27ad3162..d87c813d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/data_suppliers/data_suppliers.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/data_suppliers/data_suppliers.py @@ -1,7 +1,6 @@ from typing import Annotated, Any -from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Body, Path from starlette.requests import Request from clinical_mdr_api.models.data_suppliers.data_supplier import ( @@ -64,32 +63,15 @@ # pylint: disable=unused-argument def get_data_suppliers( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, operator: Annotated[ - str | None, Query(description=_generic_descriptions.FILTER_OPERATOR) + str | None, _generic_descriptions.FILTER_OPERATOR_QUERY ] = settings.default_filter_operator, total_count: Annotated[ - bool | None, Query(description=_generic_descriptions.TOTAL_COUNT) + bool | None, _generic_descriptions.TOTAL_COUNT_QUERY ] = False, ) -> CustomPage[DataSupplier]: data_supplier_service = DataSupplierService() @@ -119,25 +101,11 @@ def get_data_suppliers( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: data_supplier_service = DataSupplierService() return data_supplier_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/ddf/study_definitions.py b/clinical-mdr-api/clinical_mdr_api/routers/ddf/study_definitions.py index bd57c8d1..d0a571cb 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/ddf/study_definitions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/ddf/study_definitions.py @@ -18,6 +18,7 @@ from common.auth import rbac from common.auth.dependencies import security from common.models.error import ErrorResponse +from common.telemetry import trace_block router = APIRouter(prefix="/studyDefinitions") @@ -52,7 +53,7 @@ """, ) def get_study( - study_uid: Annotated[str, Path(description="The unique uid of the study.")] + study_uid: Annotated[str, Path(description="The unique uid of the study.")], ) -> dict[str, Any]: usdm_service = USDMService() ddf_study_wrapper = usdm_service.get_by_uid(study_uid) @@ -100,186 +101,234 @@ def get_study_m11_protocol( study_uid, study_value_version=None ) - context = { - "study_id": study_uid, - "study_indications": ddf_study.versions[0].studyDesigns[0].indications, - "study_interventions": ddf_study.versions[0].studyInterventions[0], - "study_name": ddf_study.name, - "protocol_full_title": ddf_study.description, - "study_design_figure_svg": study_design_figure, - "study_flowchart_html_table": study_flowchart_html_table_str, - "protocol_short_title": ddf_study.label, - "protocol_acronym": study_uid, - # "sponsor_name": ddf_study.versions[0].studyIdentifiers[0].scopeId, - "sponsor_legal_address": "Novo Nordisk A/S Novo Allé, 2880 Bagsvaerd Denmark Tel: +45 4444 8888", - # "protocol_number": ddf_study.versions[0].studyIdentifiers[0].id, - "protocol_version": ddf_study.documentedBy[0].versions[0].version, - "trial_phase": ddf_study.versions[0].studyDesigns[0].studyPhase.standardCode, - "primary_objectives": [ - objective.dict() - for objective in ddf_study.versions[0].studyDesigns[0].objectives - if "primary" in objective.level.decode.lower() - ], - "secondary_objectives": [ - objective.dict() - for objective in ddf_study.versions[0].studyDesigns[0].objectives - if "secondary" in objective.level.decode.lower() - ], - # TODO: reactivate when intervention model is available in package as InterventionalStudyDesign attribute - # "intervention_model": ddf_study.versions[0] - # .studyDesigns[0] - # .interventionModel.decode - # or "", - "population_healthy_subjects": ddf_study.versions[0] - .studyDesigns[0] - .population.includesHealthySubjects, - "population_planned_maximum_age": ( - ddf_study.versions[0].studyDesigns[0].population.plannedAge.maxValue.value - if ddf_study.versions[0].studyDesigns[0].population.plannedAge is not None - else "Missing" - ), - "population_planned_maximum_age_unit": ( - ddf_study.versions[0] - .studyDesigns[0] - .population.plannedAge.maxValue.unit.standardCode.decode - if ddf_study.versions[0].studyDesigns[0].population.plannedAge is not None - else "Missing" - ), - "population_planned_minimum_age": ( - ddf_study.versions[0].studyDesigns[0].population.plannedAge.minValue.value - if ddf_study.versions[0].studyDesigns[0].population.plannedAge is not None - else "Missing" - ), - "population_planned_minimum_age_unit": ( - ddf_study.versions[0] + with trace_block("context_creation", "Creating context for M11 template rendering"): + context = { + "study_id": study_uid, + "study_indications": ddf_study.versions[0].studyDesigns[0].indications, + "study_interventions": ddf_study.versions[0].studyInterventions[0], + "study_name": ddf_study.name, + "protocol_full_title": ddf_study.description, + "study_design_figure_svg": study_design_figure, + "study_flowchart_html_table": study_flowchart_html_table_str, + "protocol_short_title": ddf_study.label, + "protocol_acronym": study_uid, + # "sponsor_name": ddf_study.versions[0].studyIdentifiers[0].scopeId, + "sponsor_legal_address": "Novo Nordisk A/S Novo Allé, 2880 Bagsvaerd Denmark Tel: +45 4444 8888", + # "protocol_number": ddf_study.versions[0].studyIdentifiers[0].id, + "protocol_version": ddf_study.documentedBy[0].versions[0].version, + "trial_phase": ( + ddf_study.versions[0].studyDesigns[0].studyPhase.standardCode + ), + "primary_objectives": [ + objective.dict() + for objective in ddf_study.versions[0].studyDesigns[0].objectives + if "primary" in objective.level.decode.lower() + ], + "secondary_objectives": [ + objective.dict() + for objective in ddf_study.versions[0].studyDesigns[0].objectives + if "secondary" in objective.level.decode.lower() + ], + # TODO: reactivate when intervention model is available in package as InterventionalStudyDesign attribute + # "intervention_model": ddf_study.versions[0] + # .studyDesigns[0] + # .interventionModel.decode + # or "", + "population_healthy_subjects": ddf_study.versions[0] .studyDesigns[0] - .population.plannedAge.minValue.unit.standardCode.decode - if ddf_study.versions[0].studyDesigns[0].population.plannedAge is not None - else "Missing" - ), - "population_planned_enrollment_number_quantity_value": ( - int( + .population.includesHealthySubjects, + "population_planned_maximum_age": ( + ( + ddf_study.versions[0] + .studyDesigns[0] + .population.plannedAge.maxValue.value + ) + if ( + ddf_study.versions[0].studyDesigns[0].population.plannedAge + is not None + ) + else "Missing" + ), + "population_planned_maximum_age_unit": ( + ( + ddf_study.versions[0] + .studyDesigns[0] + .population.plannedAge.maxValue.unit.standardCode.decode + ) + if ( + ddf_study.versions[0].studyDesigns[0].population.plannedAge + is not None + ) + else "Missing" + ), + "population_planned_minimum_age": ( + ( + ddf_study.versions[0] + .studyDesigns[0] + .population.plannedAge.minValue.value + ) + if ( + ddf_study.versions[0].studyDesigns[0].population.plannedAge + is not None + ) + else "Missing" + ), + "population_planned_minimum_age_unit": ( + ( + ddf_study.versions[0] + .studyDesigns[0] + .population.plannedAge.minValue.unit.standardCode.decode + ) + if ( + ddf_study.versions[0].studyDesigns[0].population.plannedAge + is not None + ) + else "Missing" + ), + "population_planned_enrollment_number_quantity_value": ( + int( + ddf_study.versions[0] + .studyDesigns[0] + .population.plannedEnrollmentNumber.value + ) + if ( + ddf_study.versions[0] + .studyDesigns[0] + .population.plannedEnrollmentNumber + ) + is not None + else "Missing" + ), + "trial_intervention_total_duration": ( ddf_study.versions[0] .studyDesigns[0] - .population.plannedEnrollmentNumberQuantity.value - ) - if ddf_study.versions[0] - .studyDesigns[0] - .population.plannedEnrollmentNumberQuantity - is not None - else "Missing" - ), - "trial_intervention_total_duration": ( - ddf_study.versions[0] - .studyDesigns[0] - .scheduleTimelines[0] - .timings[-1] - .valueLabel - if len(ddf_study.versions[0].studyDesigns[0].scheduleTimelines[0].timings) - > 0 - else None - ), - "number_of_arms": len(ddf_study.versions[0].studyDesigns[0].arms), - "civ_id_sin_number": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "civ_id_sin_number" + .scheduleTimelines[0] + .timings[-1] + .valueLabel + if len( + ddf_study.versions[0].studyDesigns[0].scheduleTimelines[0].timings + ) + > 0 + else None ), - None, - ), - "ct_gov_id": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "ct_gov_id" + "number_of_arms": len(ddf_study.versions[0].studyDesigns[0].arms), + "civ_id_sin_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "civ_id_sin_number" + ), + None, ), - None, - ), - "eudamed_srn_number": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "eudamed_srn_number" + "ct_gov_id": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "ct_gov_id" + ), + None, ), - None, - ), - "eudract_id": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "eudract_id" + "eudamed_srn_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "eudamed_srn_number" + ), + None, ), - None, - ), - "eu_trial_number": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "eu_trial_number" + "eudract_id": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "eudract_id" + ), + None, ), - None, - ), - "investigational_device_exemption_ide_number": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "investigational_device_exemption_ide_number" + "eu_trial_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "eu_trial_number" + ), + None, ), - None, - ), - "investigational_new_drug_application_number_ind": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId - == "investigational_new_drug_application_number_ind" + "investigational_device_exemption_ide_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if ( + identifier.scopeId + == "investigational_device_exemption_ide_number" + ) + ), + None, ), - None, - ), - "japanese_trial_registry_id_japic": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "japanese_trial_registry_id_japic" + "investigational_new_drug_application_number_ind": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if ( + identifier.scopeId + == "investigational_new_drug_application_number_ind" + ) + ), + None, ), - None, - ), - "japanese_trial_registry_number_jrct": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "japanese_trial_registry_number_jrct" + "japanese_trial_registry_id_japic": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "japanese_trial_registry_id_japic" + ), + None, ), - None, - ), - "national_clinical_trial_number": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "national_clinical_trial_number" + "japanese_trial_registry_number_jrct": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "japanese_trial_registry_number_jrct" + ), + None, ), - None, - ), - "national_medical_products_administration_nmpa_number": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId - == "national_medical_products_administration_nmpa_number" + "national_clinical_trial_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "national_clinical_trial_number" + ), + None, ), - None, - ), - "universal_trial_number_utn": next( - ( - identifier.text - for identifier in ddf_study.versions[0].studyIdentifiers - if identifier.scopeId == "universal_trial_number_utn" + "national_medical_products_administration_nmpa_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if ( + identifier.scopeId + == "national_medical_products_administration_nmpa_number" + ) + ), + None, ), - None, - ), - } + "universal_trial_number_utn": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "universal_trial_number_utn" + ), + None, + ), + "eu_pas_number": next( + ( + identifier.text + for identifier in ddf_study.versions[0].studyIdentifiers + if identifier.scopeId == "eu_pas_number" + ), + None, + ), + } - return templates.TemplateResponse( - request=request, name="m11-template.html", context=context - ) + with trace_block("template_rendering", "Rendering M11 template"): + template_response = templates.TemplateResponse( + request=request, name="m11-template.html", context=context + ) + return template_response diff --git a/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_codelists.py b/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_codelists.py index 9bb7bfb1..13c047fa 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_codelists.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_codelists.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.dictionaries.dictionary_codelist import ( @@ -83,33 +82,12 @@ def get_codelists( request: Request, # request is actually required by the allow_exports decorator library_name: Annotated[str, DictionaryCodelistLibrary], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DictionaryCodelist]: dictionary_codelist_service = DictionaryCodelistGenericService() results = dictionary_codelist_service.get_all_dictionary_codelists( @@ -143,25 +121,11 @@ def get_codelists( ) def get_distinct_values_for_header( library_name: Annotated[str, DictionaryCodelistLibrary], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dictionary_codelist_service = DictionaryCodelistGenericService() return dictionary_codelist_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_terms.py b/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_terms.py index 2b7a692f..6ed39603 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_terms.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/dictionaries/dictionary_terms.py @@ -88,33 +88,12 @@ def get_terms( codelist_uid: Annotated[ str, Query(description="The unique id of the DictionaryCodelist") ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DictionaryTerm]: dictionary_term_service: DictionaryTermGenericService = ( DictionaryTermGenericService() @@ -152,25 +131,11 @@ def get_distinct_values_for_header( codelist_uid: Annotated[ str, Query(description="The unique id of the DictionaryCodelist") ], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dictionary_term_service: DictionaryTermGenericService = ( DictionaryTermGenericService() @@ -619,12 +584,8 @@ def create_substance( }, ) def get_distinct_values_for_substances_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", filters: Annotated[ Json | None, Query( @@ -635,9 +596,7 @@ def get_distinct_values_for_substances_header( operator: Annotated[ str, Query(description=_generic_descriptions.FILTER_OPERATOR) ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dictionary_term_service: DictionaryTermSubstanceService = ( DictionaryTermSubstanceService() @@ -727,33 +686,12 @@ def get_substance_by_id( # pylint: disable=unused-argument def get_substances( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DictionaryTermSubstance]: dictionary_term_service = DictionaryTermSubstanceService() results = dictionary_term_service.get_all_dictionary_terms( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/libraries/time_points.py b/clinical-mdr-api/clinical_mdr_api/routers/libraries/time_points.py index 2d8f17d4..5794a930 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/libraries/time_points.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/libraries/time_points.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import TimePoint, TimePointPostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_time_points( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[TimePoint]: time_point_service = TimePointService() results = time_point_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_time_points( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: time_point_service = TimePointService() return time_point_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/listings/listings.py b/clinical-mdr-api/clinical_mdr_api/routers/listings/listings.py index 02a09a26..25172ece 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/listings/listings.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/listings/listings.py @@ -2,7 +2,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.listings.listings import ( @@ -48,33 +47,12 @@ def get_metadata( " Multiple datasets are separated by commas", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[MetaData]: service = ListingsService() all_items = service.list_metadata( @@ -111,25 +89,11 @@ def get_metadata( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ListingsService() return service.get_distinct_values_for_header( @@ -184,33 +148,12 @@ def get_all_activities_report( "ISO Format with timezone, compatible with Neo4j e.g. 2021-01-01T09:00:00Z", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[TopicCdDef]: service = ListingsService() all_items = service.list_topic_cd( @@ -247,25 +190,11 @@ def get_all_activities_report( }, ) def get_distinct_topic_cd_def_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ListingsService() return service.get_distinct_values_for_header( @@ -304,33 +233,12 @@ def get_cdisc_ct_ver_data( "Date must be in ISO format e.g. 2021-01-01", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CDISCCTVer]: service = ListingsService() all_items = service.list_cdisc_ct_ver( @@ -368,25 +276,11 @@ def get_cdisc_ct_ver_data( }, ) def get_distinct_cdisc_ct_ver_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ListingsService() return service.get_distinct_values_for_header( @@ -425,33 +319,12 @@ def get_cdisc_ct_pkg_data( "Date must be in ISO format e.g. 2021-01-01", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CDISCCTPkg]: service = ListingsService() all_items = service.list_cdisc_ct_pkg( @@ -489,25 +362,11 @@ def get_cdisc_ct_pkg_data( }, ) def get_distinct_cdisc_ct_pkg_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ListingsService() return service.get_distinct_values_for_header( @@ -553,33 +412,12 @@ def get_cdisc_ct_list_data( "Date must be in ISO format e.g. 2021-01-01", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CDISCCTList]: service = ListingsService() all_items = service.list_cdisc_ct_list( @@ -618,25 +456,11 @@ def get_cdisc_ct_list_data( }, ) def get_distinct_cdisc_ct_list_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ListingsService() return service.get_distinct_values_for_header( @@ -682,33 +506,12 @@ def get_cdisc_ct_val_data( "Date must be in ISO format e.g. 2021-01-01", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CDISCCTVal]: service = ListingsService() all_items = service.list_cdisc_ct_val( @@ -747,25 +550,11 @@ def get_cdisc_ct_val_data( }, ) def get_distinct_cdisc_ct_val_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ListingsService() return service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_adam.py b/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_adam.py index 401fce8b..60b95f62 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_adam.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_adam.py @@ -1,7 +1,6 @@ from typing import Annotated, Any -from fastapi import APIRouter, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Path from clinical_mdr_api.domains.listings.utils import AdamReport from clinical_mdr_api.models.listings.listings_adam import ( @@ -44,33 +43,12 @@ def get_adam_listing( description="Return study data of a given study and for a given ADaM report domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -120,25 +98,11 @@ def get_distinct_adam_listing_values_for_header( description="Return study data of a given study and for a given ADaM report domain format.", ), ], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_sdtm.py b/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_sdtm.py index 487668da..203e7d15 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_sdtm.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/listings/listings_sdtm.py @@ -1,7 +1,6 @@ from typing import Annotated -from fastapi import APIRouter, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Path from starlette.requests import Request from clinical_mdr_api.models.listings.listings_sdtm import ( @@ -67,33 +66,12 @@ def get_tv( description="Return study visit data of a given study in SDTM TV domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -160,33 +138,12 @@ def get_ta( description="Return study arm data of a given study number in SDTM TA domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -251,33 +208,12 @@ def get_ti( description="Return study criterion data of a given study in SDTM TI domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -343,33 +279,12 @@ def get_ts( description="Return study summary data of a given study in SDTM TS domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -433,33 +348,12 @@ def get_te( description="Return study element data of a given study number in SDTM TE domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -521,33 +415,12 @@ def get_tdm( description="Return study disease milestone data of a given study number in SDTM TDM domain format." ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/projects/projects.py b/clinical-mdr-api/clinical_mdr_api/routers/projects/projects.py index 16a4fe53..495c5c41 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/projects/projects.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/projects/projects.py @@ -1,7 +1,6 @@ from typing import Annotated, Any -from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json +from fastapi import APIRouter, Body, Path, Request from clinical_mdr_api.models.projects.project import ( Project, @@ -55,33 +54,12 @@ # pylint: disable=unused-argument def get_projects( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> GenericFilteringReturn[Project]: service = ProjectService() return service.get_all_projects( @@ -107,25 +85,11 @@ def get_projects( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = ProjectService() return service.get_project_headers( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_model_igs.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_model_igs.py index fdc00b4a..19018a0d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_model_igs.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_model_igs.py @@ -2,8 +2,7 @@ from typing import Annotated, Any -from fastapi import APIRouter, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Path from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.data_model_ig import DataModelIG @@ -59,33 +58,12 @@ # pylint: disable=unused-argument def get_data_model_igs( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DataModelIG]: data_model_ig_service = DataModelIGService() results = data_model_ig_service.get_all_items( @@ -117,25 +95,11 @@ def get_data_model_igs( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: data_model_ig_service = DataModelIGService() return data_model_ig_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_models.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_models.py index 337ffac3..8e06cb44 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_models.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/data_models.py @@ -2,8 +2,7 @@ from typing import Annotated, Any -from fastapi import APIRouter, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Path from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.data_model import DataModel @@ -57,33 +56,12 @@ # pylint: disable=unused-argument def get_data_models( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DataModel]: data_model_service = DataModelService() results = data_model_service.get_all_items( @@ -115,25 +93,11 @@ def get_data_models( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: data_model_service = DataModelService() return data_model_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_classes.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_classes.py index a109fa25..f625b278 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_classes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_classes.py @@ -2,8 +2,7 @@ from typing import Annotated, Any -from fastapi import APIRouter, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Path from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.dataset_class import DatasetClass @@ -59,33 +58,12 @@ # pylint: disable=unused-argument def get_dataset_classes( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DatasetClass]: dataset_class_service = DatasetClassService() results = dataset_class_service.get_all_items( @@ -117,25 +95,11 @@ def get_dataset_classes( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dataset_class_service = DatasetClassService() return dataset_class_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_scenarios.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_scenarios.py index c0653106..411f12ad 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_scenarios.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_scenarios.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.dataset_scenario import ( @@ -72,33 +71,12 @@ def get_dataset_scenarios( description="The version of the selected Data model IG, for instance '1.4'", ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DatasetScenario]: dataset_scenario_service = DatasetScenarioService() results = dataset_scenario_service.get_all_items( @@ -144,25 +122,11 @@ def get_distinct_values_for_header( description="The version of the selected Data model IG, for instance '1.4'", ), ], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dataset_scenario_service = DatasetScenarioService() return dataset_scenario_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_variables.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_variables.py index 616cf05c..90369e36 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_variables.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/dataset_variables.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.dataset_variable import ( @@ -79,33 +78,12 @@ def get_dataset_variables( description="The uid of the selected dataset scenario", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DatasetVariable]: dataset_variable_service = DatasetVariableService() results = dataset_variable_service.get_all_items( @@ -152,25 +130,11 @@ def get_distinct_values_for_header( description="The version of the selected Data model IG, for instance '1.4'", ), ], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dataset_variable_service = DatasetVariableService() return dataset_variable_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/datasets.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/datasets.py index 5ce96f75..a55158be 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/datasets.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/datasets.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.dataset import Dataset @@ -69,33 +68,12 @@ def get_datasets( description="The version of the selected Data model implementation guide, for instance '2.2'", ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Dataset]: dataset_service = DatasetService() results = dataset_service.get_all_items( @@ -141,25 +119,11 @@ def get_distinct_values_for_header( description="The version of the selected Data model implementation guide, for instance '2.2'", ), ], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: dataset_service = DatasetService() return dataset_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_dataset_variables.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_dataset_variables.py index dd87d355..d5cb549a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_dataset_variables.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_dataset_variables.py @@ -2,8 +2,7 @@ from typing import Annotated, Any -from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Body, Path from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.sponsor_model_dataset_variable import ( @@ -63,33 +62,12 @@ # pylint: disable=unused-argument def get_sponsor_model_dataset_variables( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[SponsorModelDatasetVariable]: sponsor_model_dataset_variable_service = SponsorModelDatasetVariableService() results = sponsor_model_dataset_variable_service.get_all_items( @@ -121,25 +99,11 @@ def get_sponsor_model_dataset_variables( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: sponsor_model_dataset_variable_service = SponsorModelDatasetVariableService() return sponsor_model_dataset_variable_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_datasets.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_datasets.py index 865a0c59..9d62d242 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_datasets.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_model_datasets.py @@ -2,8 +2,7 @@ from typing import Annotated, Any -from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Body, Path from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.sponsor_model_dataset import ( @@ -61,33 +60,12 @@ # pylint: disable=unused-argument def get_sponsor_model_datasets( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[SponsorModelDataset]: sponsor_model_dataset_service = SponsorModelDatasetService() results = sponsor_model_dataset_service.get_all_items( @@ -119,25 +97,11 @@ def get_sponsor_model_datasets( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: sponsor_model_dataset_service = SponsorModelDatasetService() return sponsor_model_dataset_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_models.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_models.py index 0cf5c33b..504b563d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_models.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/sponsor_models.py @@ -2,8 +2,7 @@ from typing import Annotated, Any -from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json +from fastapi import APIRouter, Body, Path from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.sponsor_model import ( @@ -61,33 +60,12 @@ # pylint: disable=unused-argument def get_sponsor_models( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[SponsorModel]: sponsor_model_service = SponsorModelService() results = sponsor_model_service.get_all_items( @@ -119,25 +97,11 @@ def get_sponsor_models( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: sponsor_model_service = SponsorModelService() return sponsor_model_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/variable_classes.py b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/variable_classes.py index ca53d1b6..894ddcbe 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/variable_classes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/standard_data_models/variable_classes.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.standard_data_models.variable_class import VariableClass @@ -78,33 +77,12 @@ def get_class_variables( description="The name of the selected DatasetClass, for instance 'General Observations'", ), ], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[VariableClass]: class_variable_service = VariableClassService() results = class_variable_service.get_all_items( @@ -157,25 +135,11 @@ def get_distinct_values_for_header( description="The name of the selected DatasetClass, for instance 'General Observations'", ), ], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: class_variable_service = VariableClassService() return class_variable_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/studies.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/studies.py index 7fa6dec4..a5aeb74c 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/studies.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/studies.py @@ -27,6 +27,8 @@ StudySimple, StudySoaPreferences, StudySoaPreferencesInput, + StudySoaSplit, + StudySoaSplitInput, StudyStructureOverview, StudyStructureStatistics, StudySubpartAuditTrail, @@ -42,7 +44,10 @@ study_section_description, ) from clinical_mdr_api.services.studies.complexity_score import ComplexityScoreService -from clinical_mdr_api.services.studies.study import StudyService +from clinical_mdr_api.services.studies.study import ( + StudyService, + validate_if_study_is_not_locked, +) from clinical_mdr_api.services.studies.study_pharma_cm import StudyPharmaCMService from common.auth import rbac from common.auth.dependencies import security @@ -163,33 +168,12 @@ def get_all( description="Optionally, filter studies based on the existence of related sTudy Activity Instruction or not", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, deleted: Annotated[ bool, Query( @@ -328,31 +312,12 @@ def get_studies_list( # pylint: disable=unused-argument def get_study_structure_overview( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[Json, Query(description=_generic_descriptions.SORT_BY)] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: Annotated[Json, _generic_descriptions.SORT_BY_QUERY] = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudyStructureOverview]: study_service = StudyService() results = study_service.get_study_structure_overview( @@ -385,25 +350,11 @@ def get_study_structure_overview( }, ) def get_study_structure_overview_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: study_service = StudyService() return study_service.get_study_structure_overview_header( @@ -431,25 +382,11 @@ def get_study_structure_overview_header( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: study_service = StudyService() return study_service.get_distinct_values_for_header( @@ -812,33 +749,12 @@ def patch( ) def get_snapshot_history( study_uid: Annotated[str, StudyUID], # , - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CompactStudy]: study_service = StudyService() snapshot_history = study_service.get_study_snapshot_history( @@ -1226,6 +1142,111 @@ def patch_soa_preferences( ) +@router.get( + "/{study_uid}/soa-splits", + dependencies=[security, rbac.STUDY_READ], + summary="Get SoA split uids", + status_code=200, + responses={ + 403: _generic_descriptions.ERROR_403, + 404: { + "model": ErrorResponse, + "description": "Not Found - study with the specified 'study_uid' doesn't exist or has no SoA splits configured", + }, + }, +) +def get_soa_splits( + study_uid: Annotated[str, StudyUID], + study_value_version: Annotated[ + str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY + ] = None, +) -> list[StudySoaSplit]: + study_service = StudyService() + return study_service.get_study_soa_splits( + study_uid=study_uid, + study_value_version=study_value_version, + ) + + +@router.put( + "/{study_uid}/soa-splits", + dependencies=[security, rbac.STUDY_WRITE], + summary="Add a uid to SoA splits", + status_code=200, + responses={ + 403: _generic_descriptions.ERROR_403, + 404: { + "model": ErrorResponse, + "description": "Not Found - study with the specified 'study_uid' doesn't exist", + }, + 409: { + "model": ErrorResponse, + "description": "Conflict - The uid to add is already in the SoA splits", + }, + }, +) +@validate_if_study_is_not_locked("study_uid", 1) +def put_soa_split( + study_uid: Annotated[str, StudyUID], + soa_splits_input: Annotated[ + StudySoaSplitInput, Body(description="SoA split input") + ], +) -> list[StudySoaSplit]: + study_service = StudyService() + return study_service.add_study_soa_split( + study_uid=study_uid, + soa_split_input=soa_splits_input, + ) + + +@router.delete( + "/{study_uid}/soa-splits/{study_visit_uid}", + dependencies=[security, rbac.STUDY_WRITE], + summary="Remove a uid from SoA splits", + status_code=200, + responses={ + 403: _generic_descriptions.ERROR_403, + 404: { + "model": ErrorResponse, + "description": "Not Found - study with the specified 'study_uid' doesn't exist", + }, + }, +) +@validate_if_study_is_not_locked("study_uid", 1) +def remove_soa_split( + study_uid: Annotated[str, StudyUID], + study_visit_uid: Annotated[str, Path(description="SoA split uid")], +) -> list[StudySoaSplit]: + study_service = StudyService() + return study_service.remove_study_soa_split( + study_uid=study_uid, + uid=study_visit_uid, + ) + + +@router.delete( + "/{study_uid}/soa-splits", + dependencies=[security, rbac.STUDY_WRITE], + summary="Remove all SoA splits", + status_code=204, + responses={ + 403: _generic_descriptions.ERROR_403, + 404: { + "model": ErrorResponse, + "description": "Not Found - study with the specified 'study_uid' doesn't exist", + }, + }, +) +@validate_if_study_is_not_locked("study_uid", 1) +def remove_soa_splits( + study_uid: Annotated[str, StudyUID], +) -> None: + study_service = StudyService() + study_service.remove_study_soa_splits( + study_uid=study_uid, + ) + + @router.get( "/{study_uid}/complexity-score", dependencies=[security, rbac.STUDY_READ], diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study.py index 936bf9cb..208b1a6a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study.py @@ -6,13 +6,12 @@ from fastapi import APIRouter, Body, Path, Query, Request from fastapi.responses import HTMLResponse, StreamingResponse from pydantic import Field -from pydantic.types import Json from clinical_mdr_api.models.study_selections.study_selection import ( CompactStudyArm, StudyActivityGroup, StudyActivityGroupEditInput, - StudyActivityReplaceActivityInput, + StudyActivityReplaceActivityListInput, StudyActivitySubGroup, StudyActivitySubGroupEditInput, StudyActivitySyncLatestVersionInput, @@ -192,33 +191,12 @@ }, ) def get_a_paginated_list_of_study_data_suppliers( - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -259,25 +237,11 @@ def get_a_paginated_list_of_study_data_suppliers( }, ) def get_distinct_study_data_supplier_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -304,33 +268,12 @@ def get_distinct_study_data_supplier_values_for_header( ) def get_a_paginated_list_of_study_data_suppliers_of_a_study( study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -372,26 +315,12 @@ def get_a_paginated_list_of_study_data_suppliers_of_a_study( }, ) def get_distinct_study_data_supplier_values_of_a_study_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, study_uid: Annotated[str, studyUID], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -582,33 +511,12 @@ def get_all_selected_objectives_for_all_studies( ] = False, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionObjective]: service = StudyObjectiveSelectionService() all_selections = service.get_all_selections_for_all_studies( @@ -646,27 +554,13 @@ def get_all_selected_objectives_for_all_studies( }, ) def get_distinct_objective_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyObjectiveSelectionService() return service.get_distinct_values_for_header( @@ -719,9 +613,7 @@ def get_distinct_objective_values_for_header( def get_all_selected_objectives( request: Request, # request is actually required by the allow_exports decorator study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, no_brackets: Annotated[ bool, Query( @@ -729,30 +621,11 @@ def get_all_selected_objectives( "should be returned", ), ] = False, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + filters: _generic_descriptions.FILTERS_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -787,28 +660,14 @@ def get_all_selected_objectives( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, study_uid: Annotated[str, studyUID], project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -1202,33 +1061,12 @@ def get_all_selected_endpoints_for_all_studies( ] = False, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionEndpoint]: service = StudyEndpointSelectionService() all_selections = service.get_all_selections_for_all_studies( @@ -1266,27 +1104,13 @@ def get_all_selected_endpoints_for_all_studies( }, ) def get_distinct_endpoint_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyEndpointSelectionService() return service.get_distinct_values_for_header( @@ -1364,33 +1188,12 @@ def get_all_selected_endpoints( "and Endpoint should be returned", ), ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + filters: _generic_descriptions.FILTERS_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -1426,27 +1229,13 @@ def get_all_selected_endpoints( ) def get_distinct_study_endpoint_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -1951,33 +1740,12 @@ def get_all_selected_objectives_and_endpoints_standard_html( def get_all_selected_compounds_for_all_studies( project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionCompound]: service = StudyCompoundSelectionService() all_selections = service.get_all_selections_for_all_studies( @@ -2014,27 +1782,13 @@ def get_all_selected_compounds_for_all_studies( }, ) def get_distinct_compound_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyCompoundSelectionService() return service.get_distinct_values_for_header( @@ -2111,30 +1865,11 @@ def get_all_selected_compounds( study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + filters: _generic_descriptions.FILTERS_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> GenericFilteringReturn[StudySelectionCompound]: service = StudyCompoundSelectionService() return service.get_all_selection( @@ -2165,30 +1900,16 @@ def get_all_selected_compounds( ) def get_distinct_compounds_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyCompoundSelectionService() return service.get_distinct_values_for_header( @@ -2707,33 +2428,12 @@ def get_all_selected_criteria_for_all_studies( ] = False, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionCriteria]: service = StudyCriteriaSelectionService() all_selections = service.get_all_selections_for_all_studies( @@ -2771,27 +2471,13 @@ def get_all_selected_criteria_for_all_studies( }, ) def get_distinct_criteria_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyCriteriaSelectionService() return service.get_distinct_values_for_header( @@ -2884,33 +2570,12 @@ def get_all_selected_criteria( "should be returned", ), ] = False, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -2953,25 +2618,11 @@ def get_all_selected_criteria( ) def get_distinct_study_criteria_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -3600,33 +3251,12 @@ def get_all_selected_activity_instances_for_all_studies( alias="activity_instance_names[]", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionActivityInstance]: service = StudyActivityInstanceSelectionService() all_selections: GenericFilteringReturn[StudySelectionActivityInstance] = ( @@ -3738,36 +3368,19 @@ def get_all_selected_activity_instances( alias="activity_instance_names[]", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, + has_activity_instance: Annotated[ + bool | None, + Query(description="Filter to only return rows with a linked activity instance"), + ] = None, ) -> CustomPage[StudySelectionActivityInstance]: service = StudyActivityInstanceSelectionService() all_items = service.get_all_selection( @@ -3784,6 +3397,7 @@ def get_all_selected_activity_instances( sort_by=sort_by, study_value_version=study_value_version, include_placeholders=True, + has_activity_instance=has_activity_instance, ) return CustomPage( items=all_items.items, @@ -3845,25 +3459,11 @@ def get_all_selected_activity_instances_lite( ) def get_distinct_study_activity_instances_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -3877,6 +3477,7 @@ def get_distinct_study_activity_instances_values_for_header( filter_operator=FilterOperator.from_str(operator), page_size=page_size, study_value_version=study_value_version, + include_placeholders=True, ) @@ -3943,11 +3544,16 @@ def post_new_activity_instance_selection( State before: - Study must exist and be in status draft Business logic: + - To edit is_important or baseline visits, the object must have is_reviewed=False before the patch request State after: - Added new entry in the audit trail for the update of the study-activity-instance.""", response_model_exclude_unset=True, status_code=200, responses={ + 400: { + "model": ErrorResponse, + "description": "Cannot modify 'is_important' property on a reviewed StudyActivityInstance", + }, 403: _generic_descriptions.ERROR_403, 404: { "model": ErrorResponse, @@ -4178,33 +3784,12 @@ def get_all_selected_activities_for_all_studies( alias="activity_group_names[]", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionActivity]: service = StudyActivitySelectionService() all_selections: GenericFilteringReturn[StudySelectionActivity] = ( @@ -4293,33 +3878,12 @@ def get_all_selected_activities( alias="activity_group_names[]", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -4330,6 +3894,9 @@ def get_all_selected_activities( ), ] = False, ) -> CustomPage[StudySelectionActivity]: + StudyService().check_if_study_uid_and_version_exists( + study_uid=study_uid, study_value_version=study_value_version + ) service = StudyActivitySelectionService() all_items = service.get_all_selection( study_uid=study_uid, @@ -4403,25 +3970,11 @@ def get_all_selected_activities_lite( ) def get_distinct_activity_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -4639,14 +4192,14 @@ def patch_study_activity_sync_to_latest_activity( @router.patch( "/studies/{study_uid}/study-activities/{study_activity_uid}/activity-replacements", dependencies=[security, rbac.STUDY_WRITE], - summary="Exchanging selected activity for given StudyActivity based on the input data", + summary="Exchanging selected activity for given StudyActivity based on the input data. First item replaces the original StudyActivity, remaining items create new StudyActivities.", response_model_exclude_unset=True, status_code=200, responses={ 403: _generic_descriptions.ERROR_403, 400: { "model": ErrorResponse, - "description": "Forbidden - There already exists a selection of the activity", + "description": "Forbidden - There already exists a selection of the activity or duplicate combinations in the replacement list", }, 404: { "model": ErrorResponse, @@ -4659,17 +4212,17 @@ def replace_selected_activity_for_study_activity( study_uid: Annotated[str, studyUID], study_activity_uid: Annotated[str, study_activity_uid_path], selection: Annotated[ - StudyActivityReplaceActivityInput, + StudyActivityReplaceActivityListInput, Body( - description="Parameters for the StudyActivity that will replace old StudyActivity" + description="List of activity replacements. First item replaces the original StudyActivity, rest create new ones." ), ], -) -> StudySelectionActivity: +) -> list[StudySelectionActivity]: service = StudyActivitySelectionService() - return service.patch_selection( + return service.replace_study_activity_with_multiple_activities( study_uid=study_uid, - study_selection_uid=study_activity_uid, - selection_update_input=selection, + study_activity_uid=study_activity_uid, + replacements=selection.replacements, ) @@ -4691,33 +4244,12 @@ def replace_selected_activity_for_study_activity( ) def get_all_selected_activity_subgroups( study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -4827,33 +4359,12 @@ def patch_activity_subgroup_new_order( ) def get_all_selected_activity_groups( study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -4963,33 +4474,12 @@ def patch_activity_group_new_order( ) def get_all_selected_soa_groups( study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -5367,33 +4857,12 @@ def get_all_study_arms_branches_and_cohorts( def get_all_selected_arms( request: Request, # request is actually required by the allow_exports decorator study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -5435,25 +4904,11 @@ def get_all_selected_arms( ) def get_distinct_arm_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyArmSelectionService() return service.get_distinct_values_for_header( @@ -5480,33 +4935,12 @@ def get_distinct_arm_values_for_header( def get_all_selected_arms_for_all_studies( project_name: Annotated[str | None, PROJECT_NAME] = None, project_number: Annotated[str | None, PROJECT_NUMBER] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionArmWithConnectedBranchArms]: service = StudyArmSelectionService() all_selections = service.get_all_selections_for_all_studies( @@ -5824,33 +5258,12 @@ def post_new_element_selection_create( def get_all_selected_elements( request: Request, # request is actually required by the allow_exports decorator study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -5892,25 +5305,11 @@ def get_all_selected_elements( ) def get_distinct_element_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -6199,33 +5598,14 @@ def get_all_selected_branch_arms( study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, operator: Annotated[ - str | None, Query(description=_generic_descriptions.FILTER_OPERATOR) + str | None, _generic_descriptions.FILTER_OPERATOR_QUERY ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySelectionBranchArm]: service = StudyBranchArmSelectionService() all_selections = service.get_all_selection( @@ -6565,33 +5945,12 @@ def post_new_cohort_selection_create( def get_all_selected_cohorts( request: Request, # request is actually required by the allow_exports decorator study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, arm_uid: Annotated[ str | None, Query( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_activity_instructions.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_activity_instructions.py index 41c84868..ec4d120a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_activity_instructions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_activity_instructions.py @@ -1,7 +1,6 @@ from typing import Annotated -from fastapi import Body, Query -from pydantic.types import Json +from fastapi import Body from clinical_mdr_api.models.study_selections.study_selection import ( StudyActivityInstruction, @@ -34,33 +33,12 @@ }, ) def get_all_activity_instructions_for_all_studies( - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudyActivityInstruction]: service = StudyActivityInstructionService() all_selections = service.get_all_instructions_for_all_studies( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_compound_dosing.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_compound_dosing.py index cc76037f..bcc4cb3c 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_compound_dosing.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_compound_dosing.py @@ -1,7 +1,6 @@ from typing import Annotated, Any -from fastapi import Body, Query, Request -from pydantic.types import Json +from fastapi import Body, Request from clinical_mdr_api.models.study_selections.study_selection import ( StudyCompoundDosing, @@ -66,30 +65,11 @@ def get_all_selected_compound_dosings( study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + filters: _generic_descriptions.FILTERS_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudyCompoundDosing]: service = StudyCompoundDosingSelectionService() all_items = service.get_all_compound_dosings( @@ -127,25 +107,11 @@ def get_all_selected_compound_dosings( ) def get_distinct_values_for_header( study_uid: Annotated[str, utils.studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyCompoundDosingSelectionService() return service.get_distinct_values_for_header( @@ -174,25 +140,11 @@ def get_distinct_values_for_header( }, ) def get_distinct_compound_dosings_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyCompoundDosingSelectionService() return service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_days.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_days.py index b53bdc8e..1c153a9a 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_days.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_days.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import NumericValue, NumericValuePostInput from clinical_mdr_api.models.utils import CustomPage @@ -45,33 +44,12 @@ ) def get_study_days( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[NumericValue]: study_day_service = StudyDayService() results = study_day_service.get_all_concepts( @@ -104,26 +82,12 @@ def get_study_days( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: study_day_service = StudyDayService() return study_day_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_disease_milestones.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_disease_milestones.py index 04028b5c..11f7c8c1 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_disease_milestones.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_disease_milestones.py @@ -1,7 +1,6 @@ from typing import Annotated, Any -from fastapi import Body, Path, Query, Request -from pydantic.types import Json +from fastapi import Body, Path, Request from clinical_mdr_api.models.study_selections import study_disease_milestone from clinical_mdr_api.models.utils import CustomPage @@ -83,33 +82,12 @@ def get_all( request: Request, # request is actually required by the allow_exports decorator study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -151,29 +129,15 @@ def get_all( ) # pylint: disable=unused-argument def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, study_uid: Annotated[str, studyUID], # TODO: Use this argument! study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudyDiseaseMilestoneService() return service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_days.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_days.py index 853785a2..152f1fd5 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_days.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_days.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import NumericValue, NumericValuePostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_study_duration_days( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[NumericValue]: study_duration_days_service = StudyDurationDaysService() results = study_duration_days_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_study_duration_days( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: study_duration_days_service = StudyDurationDaysService() return study_duration_days_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_weeks.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_weeks.py index 289f2842..0cdf517f 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_weeks.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_duration_weeks.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import NumericValue, NumericValuePostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_study_duration_weeks( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[NumericValue]: study_duration_weeks_service = StudyDurationWeeksService() results = study_duration_weeks_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_study_duration_weeks( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: study_duration_weeks_service = StudyDurationWeeksService() return study_duration_weeks_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_epochs.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_epochs.py index 47085fdd..a295f0e9 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_epochs.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_epochs.py @@ -2,9 +2,9 @@ from typing import Annotated, Any from fastapi import Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.models.study_selections import study_epoch +from clinical_mdr_api.models.study_selections.study_visit import StudyVisitGroup from clinical_mdr_api.models.utils import CustomPage from clinical_mdr_api.repositories._utils import FilterOperator from clinical_mdr_api.routers import _generic_descriptions, decorators @@ -87,33 +87,12 @@ def get_all( request: Request, # request is actually required by the allow_exports decorator study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -154,25 +133,11 @@ def get_all( ) def get_distinct_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -568,7 +533,7 @@ def get_all_consecutive_groups( study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, -) -> set[str]: +) -> list[StudyVisitGroup]: service = StudyVisitService( study_uid=study_uid, study_value_version=study_value_version ) diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_flowchart.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_flowchart.py index 1b2154b6..a4cedf70 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_flowchart.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_flowchart.py @@ -4,7 +4,7 @@ import os from typing import Annotated, Any -from fastapi import Path, Query +from fastapi import Path, Query, Response, status from fastapi.responses import HTMLResponse, StreamingResponse from starlette.requests import Request @@ -344,20 +344,9 @@ def get_detailed_soa_html( ) def get_detailed_soa_history( study_uid: Annotated[str, STUDY_UID_PATH], - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[DetailedSoAHistory]: detailed_soa_history = StudyActivitySelectionService().get_detailed_soa_history( study_uid=study_uid, @@ -532,6 +521,58 @@ def export_protocol_soa_content( return soa_content +@router.get( + "/{study_uid}/flowchart/snapshot", + dependencies=[security, rbac.ADMIN_READ], + summary="Retrieve the saved SoA snapshot for a study version. If no SoA snapshot saved, returns 404.", + status_code=status.HTTP_200_OK, + responses={ + status.HTTP_404_NOT_FOUND: _generic_descriptions.ERROR_404, + }, + response_model=TableWithFootnotes, + response_model_exclude_none=True, + tags=["Data Migration"], +) +def get_soa_snapshot( + study_uid: Annotated[str, STUDY_UID_PATH], + study_value_version: Annotated[ + str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY + ] = None, + layout: Annotated[SoALayout, LAYOUT_QUERY] = SoALayout.PROTOCOL, +) -> TableWithFootnotes: + return StudyFlowchartService().load_soa_snapshot( + study_uid=study_uid, + study_value_version=study_value_version, + layout=layout, + ) + + +@router.post( + "/{study_uid}/flowchart/snapshot", + dependencies=[security, rbac.ADMIN_WRITE], + summary="Update SoA snapshot for a study version based on the recent SoA rules (intended for data migration only)", + responses={ + status.HTTP_201_CREATED: {"model": None, "description": "SoA snapshot updated"}, + status.HTTP_404_NOT_FOUND: _generic_descriptions.ERROR_404, + }, + response_model=None, + tags=["Data Migration"], +) +def update_soa_snapshot( + study_uid: Annotated[str, STUDY_UID_PATH], + study_value_version: Annotated[ + str, _generic_descriptions.STUDY_VALUE_VERSION_QUERY + ], + layout: Annotated[SoALayout, LAYOUT_QUERY] = SoALayout.PROTOCOL, +) -> Response: + StudyFlowchartService().update_soa_snapshot( + study_uid=study_uid, + study_value_version=study_value_version, + layout=layout, + ) + return Response(content=None, status_code=status.HTTP_201_CREATED) + + def _get_study_id(study_uid, study_value_version): """gets study_id of study""" diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_soa_footnotes.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_soa_footnotes.py index 5d251547..de50270e 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_soa_footnotes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_soa_footnotes.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import Body, Query -from pydantic import Json from clinical_mdr_api.models.study_selections.study_soa_footnote import ( StudySoAFootnote, @@ -34,33 +33,12 @@ }, ) def get_all_study_soa_footnotes_from_all_studies( - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[StudySoAFootnote]: service = StudySoAFootnoteService() all_footnotes = service.get_all( @@ -95,33 +73,12 @@ def get_all_study_soa_footnotes_from_all_studies( ) def get_all_study_soa_footnotes( study_uid: Annotated[str, utils.studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -169,25 +126,11 @@ def get_all_study_soa_footnotes( ) def get_distinct_values_for_header( study_uid: Annotated[str, utils.studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -220,25 +163,11 @@ def get_distinct_values_for_header( }, ) def get_distinct_values_for_header_top_level( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: service = StudySoAFootnoteService() return service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_visits.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_visits.py index 454e0b0d..0c54e07b 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_visits.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_visits.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import Body, Path, Query -from pydantic.types import Json from starlette.requests import Request from clinical_mdr_api.models.study_selections.study_visit import ( @@ -116,33 +115,12 @@ def get_all( request: Request, # request is actually required by the allow_exports decorator, study_uid: Annotated[str, studyUID], - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -179,25 +157,11 @@ def get_all( ) def get_distinct_values_for_header( study_uid: Annotated[str, studyUID], - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, study_value_version: Annotated[ str | None, _generic_descriptions.STUDY_VALUE_VERSION_QUERY ] = None, @@ -775,7 +739,7 @@ def assign_consecutive_visit_group_for_selected_study_visit( @router.delete( - "/studies/{study_uid}/consecutive-visit-groups/{consecutive_visit_group_name}", + "/studies/{study_uid}/consecutive-visit-groups/{consecutive_visit_group_uid}", dependencies=[security, rbac.STUDY_WRITE], summary="Remove consecutive visit group specified by consecutive-visit-group-name for a selected study referenced by 'study_uid' ", description=""" @@ -800,11 +764,11 @@ def assign_consecutive_visit_group_for_selected_study_visit( @decorators.validate_if_study_is_not_locked("study_uid") def remove_consecutive_group( study_uid: Annotated[str, studyUID], - consecutive_visit_group_name: Annotated[ + consecutive_visit_group_uid: Annotated[ str, Path(description="The name of the consecutive-visit-group that is removed") ], ): service = StudyVisitService(study_uid=study_uid) service.remove_visit_consecutive_group( - study_uid=study_uid, consecutive_visit_group=consecutive_visit_group_name + study_uid=study_uid, consecutive_visit_group_uid=consecutive_visit_group_uid ) diff --git a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_weeks.py b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_weeks.py index 526b8c77..96c34cd9 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/studies/study_weeks.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/studies/study_weeks.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query -from pydantic.types import Json from clinical_mdr_api.models.concepts.concept import NumericValue, NumericValuePostInput from clinical_mdr_api.models.utils import CustomPage @@ -47,33 +46,12 @@ ) def get_study_weeks( library_name: Annotated[str | None, Query()] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[NumericValue]: study_week_service = StudyWeekService() results = study_week_service.get_all_concepts( @@ -106,26 +84,12 @@ def get_study_weeks( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, library_name: Annotated[str | None, Query()] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: study_week_service = StudyWeekService() return study_week_service.get_distinct_values_for_header( diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/activity_instructions.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/activity_instructions.py index 23384b7a..b977ab2d 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/activity_instructions.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/activity_instructions.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domain_repositories.models.syntax import ActivityInstructionValue from clinical_mdr_api.domains.study_definition_aggregates.study_metadata import ( @@ -85,33 +84,12 @@ # pylint: disable=unused-argument def get_all( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstruction]: all_items = Service().get_all( return_study_count=True, @@ -147,9 +125,7 @@ def get_all( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -161,22 +137,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -198,30 +162,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstruction]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/criteria.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/criteria.py index f7529ab4..42c0ba98 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/criteria.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/criteria.py @@ -2,7 +2,6 @@ from fastapi import APIRouter, Path, Query, Request from fastapi.param_functions import Body -from pydantic.types import Json from clinical_mdr_api.domain_repositories.models.syntax import CriteriaValue from clinical_mdr_api.domains.study_definition_aggregates.study_metadata import ( @@ -108,33 +107,12 @@ # pylint: disable=unused-argument def get_all( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CriteriaWithType]: all_items = CriteriaService().get_all( page_number=page_number, @@ -169,9 +147,7 @@ def get_all( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -183,22 +159,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return CriteriaService().get_distinct_values_for_header( status=status, @@ -220,30 +184,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Criteria]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/endpoints.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/endpoints.py index 3dcacb82..f0288738 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/endpoints.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/endpoints.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domain_repositories.models.syntax import EndpointValue from clinical_mdr_api.domains.study_definition_aggregates.study_metadata import ( @@ -106,33 +105,12 @@ # pylint: disable=unused-argument def get_all( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Endpoint]: all_items = EndpointService().get_all( status=LibraryItemStatus.FINAL.value, @@ -165,9 +143,7 @@ def get_all( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -179,22 +155,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -216,30 +180,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Endpoint]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/footnotes.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/footnotes.py index 79bd19b9..b3dd39de 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/footnotes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/footnotes.py @@ -2,7 +2,6 @@ from fastapi import APIRouter, Path, Query, Request from fastapi.param_functions import Body -from pydantic.types import Json from clinical_mdr_api.domain_repositories.models.syntax import FootnoteValue from clinical_mdr_api.domains.study_definition_aggregates.study_metadata import ( @@ -108,33 +107,12 @@ # pylint: disable=unused-argument def get_all( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[FootnoteWithType]: all_items = FootnoteService().get_all( page_number=page_number, @@ -169,9 +147,7 @@ def get_all( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -183,22 +159,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return FootnoteService().get_distinct_values_for_header( status=status, @@ -220,30 +184,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Footnote]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/objectives.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/objectives.py index c1ddc839..0e90da44 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/objectives.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/objectives.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domain_repositories.models.syntax import ObjectiveValue from clinical_mdr_api.domains.study_definition_aggregates.study_metadata import ( @@ -83,33 +82,12 @@ # pylint: disable=unused-argument def get_all( request: Request, # request is actually required by the allow_exports decorator - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Objective]: all_items = Service().get_all( page_number=page_number, @@ -144,9 +122,7 @@ def get_all( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -158,22 +134,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -195,30 +159,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Objective]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/timeframes.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/timeframes.py index 1481755a..d17babd8 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/timeframes.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_instances/timeframes.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domain_repositories.models.syntax import TimeframeValue from clinical_mdr_api.domains.study_definition_aggregates.study_metadata import ( @@ -94,33 +93,12 @@ def get_all( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Timeframe]: data = Service().get_all( status=status, @@ -153,9 +131,7 @@ def get_all( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -167,22 +143,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -204,30 +168,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[Timeframe]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/activity_instruction_pre_instances.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/activity_instruction_pre_instances.py index d272c2b5..4a9e9559 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/activity_instruction_pre_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/activity_instruction_pre_instances.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.activity_instruction_pre_instance import ( @@ -80,33 +79,12 @@ def activity_instruction_pre_instances( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstructionPreInstance]: results = ActivityInstructionPreInstanceService().get_all( status=status, @@ -140,9 +118,7 @@ def activity_instruction_pre_instances( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -152,22 +128,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -189,30 +153,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstructionPreInstanceVersion]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/criteria_pre_instances.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/criteria_pre_instances.py index fadf9c83..f30c24d1 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/criteria_pre_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/criteria_pre_instances.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.criteria_pre_instance import ( @@ -78,33 +77,12 @@ def criteria_pre_instances( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CriteriaPreInstance]: results = CriteriaPreInstanceService().get_all( status=status, @@ -138,9 +116,7 @@ def criteria_pre_instances( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -150,22 +126,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -187,30 +151,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CriteriaPreInstance]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/endpoint_pre_instances.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/endpoint_pre_instances.py index 9543cdd0..1416e669 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/endpoint_pre_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/endpoint_pre_instances.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.endpoint_pre_instance import ( @@ -77,33 +76,12 @@ def get_endpoint_pre_instances( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[EndpointPreInstance]: results = EndpointPreInstanceService().get_all( status=status, @@ -137,9 +115,7 @@ def get_endpoint_pre_instances( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -149,22 +125,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -186,30 +150,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[EndpointPreInstance]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/footnote_pre_instances.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/footnote_pre_instances.py index 2680f3eb..0d760841 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/footnote_pre_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/footnote_pre_instances.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.footnote_pre_instance import ( @@ -78,33 +77,12 @@ def footnote_pre_instances( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[FootnotePreInstance]: results = FootnotePreInstanceService().get_all( status=status, @@ -138,9 +116,7 @@ def footnote_pre_instances( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -150,22 +126,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -187,30 +151,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[FootnotePreInstance]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/objective_pre_instances.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/objective_pre_instances.py index dab38bf7..53b025b5 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/objective_pre_instances.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_pre_instances/objective_pre_instances.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.objective_pre_instance import ( @@ -79,33 +78,12 @@ def objective_pre_instances( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ObjectivePreInstance]: results = ObjectivePreInstanceService().get_all( status=status, @@ -139,9 +117,7 @@ def objective_pre_instances( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -151,22 +127,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final' or 'Draft'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -188,30 +152,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ObjectivePreInstance]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/activity_instruction_templates.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/activity_instruction_templates.py index eba5e2a8..7f52a3c9 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/activity_instruction_templates.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/activity_instruction_templates.py @@ -4,7 +4,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.activity_instruction_pre_instance import ( @@ -148,33 +147,12 @@ def get_activity_instruction_templates( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstructionTemplate]: results = Service().get_all( status=status, @@ -208,9 +186,7 @@ def get_activity_instruction_templates( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -222,22 +198,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -259,30 +223,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ActivityInstructionTemplate]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/criteria_templates.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/criteria_templates.py index afebe3c9..dac76db2 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/criteria_templates.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/criteria_templates.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.criteria_pre_instance import ( @@ -145,33 +144,12 @@ def get_criteria_templates( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CriteriaTemplate]: results = Service().get_all( status=status, @@ -205,9 +183,7 @@ def get_criteria_templates( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -219,22 +195,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -256,30 +220,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[CriteriaTemplate]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/endpoint_templates.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/endpoint_templates.py index 60e670cf..2cb18892 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/endpoint_templates.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/endpoint_templates.py @@ -1,7 +1,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.endpoint_pre_instance import ( @@ -145,33 +144,12 @@ def get_endpoint_templates( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[EndpointTemplate]: results = Service().get_all( status=status, @@ -205,9 +183,7 @@ def get_endpoint_templates( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -219,22 +195,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -256,30 +220,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[EndpointTemplate]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/footnote_templates.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/footnote_templates.py index 9a21f787..0a992ac6 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/footnote_templates.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/footnote_templates.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.footnote_pre_instance import ( @@ -144,33 +143,12 @@ def get_footnote_templates( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[FootnoteTemplate]: results = Service().get_all( status=status, @@ -204,9 +182,7 @@ def get_footnote_templates( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -218,22 +194,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -255,30 +219,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[FootnoteTemplate]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/objective_templates.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/objective_templates.py index e6b4fbdd..9fd43cff 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/objective_templates.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/objective_templates.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_pre_instances.objective_pre_instance import ( @@ -131,33 +130,12 @@ def get_objective_templates( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ObjectiveTemplate]: results = Service().get_all( status=status, @@ -191,9 +169,7 @@ def get_objective_templates( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -205,22 +181,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -242,30 +206,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[ObjectiveTemplate]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/timeframe_templates.py b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/timeframe_templates.py index 8e160b26..b29eb5f5 100644 --- a/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/timeframe_templates.py +++ b/clinical-mdr-api/clinical_mdr_api/routers/syntax_templates/timeframe_templates.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from fastapi import APIRouter, Body, Path, Query, Request -from pydantic.types import Json from clinical_mdr_api.domains.versioned_object_aggregate import LibraryItemStatus from clinical_mdr_api.models.syntax_templates.template_parameter import ( @@ -116,33 +115,12 @@ def get_timeframe_templates( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - sort_by: Annotated[ - Json | None, Query(description=_generic_descriptions.SORT_BY) - ] = None, - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + sort_by: _generic_descriptions.SORT_BY_QUERY = None, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[TimeframeTemplate]: data = Service().get_all( status=status, @@ -175,9 +153,7 @@ def get_timeframe_templates( }, ) def get_distinct_values_for_header( - field_name: Annotated[ - str, Query(description=_generic_descriptions.HEADER_FIELD_NAME) - ], + field_name: _generic_descriptions.HEADER_FIELD_NAME_QUERY, status: Annotated[ LibraryItemStatus | None, Query( @@ -189,22 +165,10 @@ def get_distinct_values_for_header( "Valid values are: 'Final', 'Draft' or 'Retired'.", ), ] = None, - search_string: Annotated[ - str, Query(description=_generic_descriptions.HEADER_SEARCH_STRING) - ] = "", - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - page_size: Annotated[ - int, Query(description=_generic_descriptions.HEADER_PAGE_SIZE) - ] = settings.default_header_page_size, + search_string: _generic_descriptions.HEADER_SEARCH_STRING_QUERY = "", + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + page_size: _generic_descriptions.HEADER_PAGE_SIZE_QUERY = settings.default_header_page_size, ) -> list[Any]: return Service().get_distinct_values_for_header( status=status, @@ -226,30 +190,11 @@ def get_distinct_values_for_header( }, ) def retrieve_audit_trail( - page_number: Annotated[ - int, Query(ge=1, description=_generic_descriptions.PAGE_NUMBER) - ] = settings.default_page_number, - page_size: Annotated[ - int, - Query( - ge=0, - le=settings.max_page_size, - description=_generic_descriptions.PAGE_SIZE, - ), - ] = settings.default_page_size, - filters: Annotated[ - Json | None, - Query( - description=_generic_descriptions.SYNTAX_FILTERS, - openapi_examples=_generic_descriptions.FILTERS_EXAMPLE, - ), - ] = None, - operator: Annotated[ - str, Query(description=_generic_descriptions.FILTER_OPERATOR) - ] = settings.default_filter_operator, - total_count: Annotated[ - bool, Query(description=_generic_descriptions.TOTAL_COUNT) - ] = False, + page_number: _generic_descriptions.PAGE_NUMBER_QUERY = settings.default_page_number, + page_size: _generic_descriptions.PAGE_SIZE_QUERY = settings.default_page_size, + filters: _generic_descriptions.SYNTAX_FILTERS_QUERY = None, + operator: _generic_descriptions.FILTER_OPERATOR_QUERY = settings.default_filter_operator, + total_count: _generic_descriptions.TOTAL_COUNT_QUERY = False, ) -> CustomPage[TimeframeTemplate]: results = Service().get_all( page_number=page_number, diff --git a/clinical-mdr-api/clinical_mdr_api/services/_utils.py b/clinical-mdr-api/clinical_mdr_api/services/_utils.py index 111e1704..45d20cca 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/_utils.py +++ b/clinical-mdr-api/clinical_mdr_api/services/_utils.py @@ -1,3 +1,4 @@ +import dataclasses import functools import inspect import json @@ -989,7 +990,11 @@ def _getattr(obj, attr): return [_getattr(element, attr) for element in obj] if isinstance(obj, dict): return [_getattr(element, attr) for element in obj.values()] - return getattr(obj, attr, None) + + attr_value = getattr(obj, attr, None) + if isinstance(attr_value, Enum): + return attr_value.value + return attr_value return functools.reduce(_getattr, attr.split("."), obj) @@ -1014,14 +1019,26 @@ def _getattr(obj, attr): if isinstance(obj, dict): return [_getattr(element, attr) for element in obj.values()] inner_obj = getattr(obj, attr, None) - if not isinstance(inner_obj, BaseModel): + + if isinstance(obj, BaseModel): prop = obj.model_fields.get(attr) if prop: return get_field_type(prop.annotation) raise ValidationException( msg=f"Cannot resolve a model field for attribute {attr}" ) - return inner_obj + if dataclasses.is_dataclass(obj): + field_types = {field.name: field.type for field in dataclasses.fields(obj)} + field_type = field_types.get(attr) + # Using issubclass here as field_type can be Union and when Union is placed as 2nd argument to issublass(..) + # then it can validate if any types inside Union matches str or int type + if issubclass(str, field_type): # type: ignore[arg-type] + return str + if issubclass(int, field_type): # type: ignore[arg-type] + return int + return type(field_types.get(attr)) + + return type(inner_obj) return functools.reduce(_getattr, attr.split("."), obj) diff --git a/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_item_class.py b/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_item_class.py index 6f47c19b..a483430e 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_item_class.py +++ b/clinical-mdr-api/clinical_mdr_api/services/biomedical_concepts/activity_item_class.py @@ -103,11 +103,14 @@ def _create_aggregate_root( definition=concept_input.definition, nci_concept_id=concept_input.nci_concept_id, order=concept_input.order, + display_name=concept_input.display_name, activity_instance_classes=[ ActivityInstanceClassActivityItemClassRelVO( uid=item.uid, mandatory=item.mandatory, is_adam_param_specific_enabled=item.is_adam_param_specific_enabled, + is_additional_optional=item.is_additional_optional, + is_default_linked=item.is_default_linked, ) for item in concept_input.activity_instance_classes ], @@ -136,11 +139,18 @@ def _edit_aggregate( definition=concept_edit_input.definition, nci_concept_id=concept_edit_input.nci_concept_id, order=concept_edit_input.order or item.activity_item_class_vo.order, + display_name=( + concept_edit_input.display_name + if concept_edit_input.display_name is not None + else item.activity_item_class_vo.display_name + ), activity_instance_classes=[ ActivityInstanceClassActivityItemClassRelVO( uid=inst.uid, mandatory=inst.mandatory, is_adam_param_specific_enabled=inst.is_adam_param_specific_enabled, + is_additional_optional=inst.is_additional_optional, + is_default_linked=inst.is_default_linked, ) for inst in concept_edit_input.activity_instance_classes ], @@ -255,10 +265,17 @@ def get_all_for_activity_instance_class( seen_uids[uid] = CompactActivityItemClass( uid=uid, name=item_class["aicv"]["name"], + display_name=item_class["aicv"]["display_name"], mandatory=item_class["has_activity_instance_class"]["mandatory"], is_adam_param_specific_enabled=item_class[ "has_activity_instance_class" ]["is_adam_param_specific_enabled"], + is_additional_optional=item_class["has_activity_instance_class"][ + "is_additional_optional" + ], + is_default_linked=item_class["has_activity_instance_class"][ + "is_default_linked" + ], ) # Order the results by name return sorted(seen_uids.values(), key=lambda x: x.name or "") @@ -287,6 +304,7 @@ def get_activity_item_class_overview( uid=item_class_ar.uid, name=item_class_ar.name, definition=item_class_ar.activity_item_class_vo.definition, + display_name=item_class_ar.display_name, nci_code=item_class_ar.activity_item_class_vo.nci_concept_id, library_name=item_class_ar.library.name if item_class_ar.library else None, start_date=( @@ -350,6 +368,16 @@ def get_activity_instance_classes_using_item_paginated( if instance.get("adam_param_specific_enabled") is not None else False ), + is_additional_optional=( + bool(instance.get("is_additional_optional")) + if instance.get("is_additional_optional") is not None + else False + ), + is_default_linked=( + bool(instance.get("is_default_linked")) + if instance.get("is_default_linked") is not None + else False + ), mandatory=( bool(instance.get("mandatory")) if instance.get("mandatory") is not None diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_group_service.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_group_service.py index ba0da545..dd7f9c49 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_group_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_group_service.py @@ -8,10 +8,11 @@ ActivityGroupAR, ActivityGroupVO, ) -from clinical_mdr_api.domains.concepts.activities.activity_sub_group import ( - SimpleActivityGroupVO, -) from clinical_mdr_api.domains.enums import LibraryItemStatus +from clinical_mdr_api.models.concepts.activities.activity import ( + ActivityEditInput, + ActivityGrouping, +) from clinical_mdr_api.models.concepts.activities.activity_group import ( ActivityGroup, ActivityGroupCreateInput, @@ -21,13 +22,10 @@ ActivityGroupVersion, SimpleSubGroup, ) -from clinical_mdr_api.models.concepts.activities.activity_sub_group import ( - ActivitySubGroupEditInput, -) from clinical_mdr_api.models.utils import GenericFilteringReturn from clinical_mdr_api.services.concepts import constants -from clinical_mdr_api.services.concepts.activities.activity_sub_group_service import ( - ActivitySubGroupService, +from clinical_mdr_api.services.concepts.activities.activity_service import ( + ActivityService, ) from clinical_mdr_api.services.concepts.concept_generic_service import ( ConceptGenericService, @@ -177,6 +175,7 @@ def get_group_subgroups( name=subgroup["name"], version=subgroup["version"], status=subgroup["status"], + start_date=subgroup["start_date"].isoformat(), definition=subgroup["definition"], ) for subgroup in linked_subgroups @@ -287,68 +286,70 @@ def get_cosmos_group_overview(self, group_uid: str) -> dict[str, Any]: def cascade_edit_and_approve(self, item: ActivityGroupAR): last_final_version = f"{item.item_metadata.major_version-1}.0" - linked_activity_subgroups = self._repos.activity_group_repository.get_linked_upgradable_activity_subgroups( - uid=item.uid, version=last_final_version + linked_activities = ( + self._repos.activity_group_repository.get_linked_upgradable_activities( + uid=item.uid, version=last_final_version + ) ) - if linked_activity_subgroups is None: + if linked_activities is None: return - self.batch_cascade_update(linked_activity_subgroups=linked_activity_subgroups) - - def batch_cascade_update(self, linked_activity_subgroups: dict[str, Any]): - activity_subgroup_service = ActivitySubGroupService() - activity_subgroup_uids = [ - activity_subgroup["uid"] - for activity_subgroup in linked_activity_subgroups.get( - "activity_subgroups", [] - ) + self.batch_cascade_update(linked_activities=linked_activities) + + def batch_cascade_update(self, linked_activities: dict[str, Any]): + activity_service = ActivityService() + activity_uids = [ + activity["uid"] for activity in linked_activities.get("activities", []) ] - if activity_subgroup_uids: - self._repos.activity_subgroup_repository.lock_objects( - uids=activity_subgroup_uids - ) - activity_subgroup_ars, _ = ( - self._repos.activity_subgroup_repository.find_all( - uids=activity_subgroup_uids, - ) + if activity_uids: + self._repos.activity_repository.lock_objects(uids=activity_uids) + activity_ars, _ = self._repos.activity_repository.find_all( + uids=activity_uids, ) - for activity_subgroup in activity_subgroup_ars: - # Only process FINAL status subgroups - skip DRAFT subgroups entirely - if ( - activity_subgroup.item_metadata.status.value - != LibraryItemStatus.FINAL.value - ): + for activity in activity_ars: + # Only process FINAL status activities - skip DRAFT activities entirely + if activity.item_metadata.status.value != LibraryItemStatus.FINAL.value: continue - activity_groups: list[SimpleActivityGroupVO] = ( - activity_subgroup.concept_vo.activity_groups - ) - - if not activity_groups: - # No matching groupings found, skip this subgroup + activity_groupings: list[ActivityGrouping] | None = [] + for grouping in activity.concept_vo.activity_groupings: + grp = { + "activity_group_uid": grouping.activity_group_uid, + "activity_subgroup_uid": grouping.activity_subgroup_uid, + } + activity_groupings.append(ActivityGrouping(**grp)) + if not activity_groupings: + # No matching groupings found, skip this activity continue - # For FINAL subgroups: create new version, edit, and approve - activity_subgroup.create_new_version(author_id=self.author_id) - - edit_input = ActivitySubGroupEditInput( + # For FINAL activities: create new version, edit, and approve + activity.create_new_version(author_id=self.author_id) + + edit_input = ActivityEditInput( + name=activity.concept_vo.name, + name_sentence_case=activity.concept_vo.name_sentence_case, + activity_groupings=activity_groupings, + definition=activity.concept_vo.definition, + abbreviation=activity.concept_vo.abbreviation, + nci_concept_id=activity.concept_vo.nci_concept_id, + nci_concept_name=activity.concept_vo.nci_concept_name, + synonyms=activity.concept_vo.synonyms, + request_rationale=activity.concept_vo.request_rationale, + is_request_final=activity.concept_vo.is_request_final, + is_data_collected=activity.concept_vo.is_data_collected, + is_multiple_selection_allowed=activity.concept_vo.is_multiple_selection_allowed, + library_name=activity.library.name, change_description="Cascade edit", - activity_groups=[ - ag.activity_group_uid - for ag in activity_subgroup.concept_vo.activity_groups - ], - name=activity_subgroup.concept_vo.name, - name_sentence_case=activity_subgroup.concept_vo.name_sentence_case, - library_name=activity_subgroup.library.name, ) - activity_subgroup = activity_subgroup_service._edit_aggregate( - item=activity_subgroup, + activity = activity_service._edit_aggregate( + item=activity, concept_edit_input=edit_input, perform_validation=False, ) - activity_subgroup.approve(author_id=self.author_id) - self._repos.activity_subgroup_repository.copy_activity_subgroup_and_recreate_activity_groupings( - activity_subgroup, author_id=self.author_id + activity.approve(author_id=self.author_id) + self._repos.activity_repository.copy_activity_and_recreate_activity_groupings( + activity, author_id=self.author_id ) - activity_subgroup_service.cascade_edit_and_approve(activity_subgroup) + activity_service.cascade_edit_and_approve(activity) + return True diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_instance_service.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_instance_service.py index 31527ac3..b1a36483 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_instance_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_instance_service.py @@ -24,9 +24,6 @@ SimplifiedActivityItem, ) from clinical_mdr_api.models.concepts.activities.activity_item import ( - CompactOdmForm, - CompactOdmItem, - CompactOdmItemGroup, CompactUnitDefinition, ) from clinical_mdr_api.services.concepts import constants @@ -42,6 +39,11 @@ class ActivityInstanceService(ConceptGenericService[ActivityInstanceAR]): repository_interface = ActivityInstanceRepository version_class = ActivityInstanceVersion + def _get_parent_class_uid(self, uid: str) -> str | None: + """Get parent class UID for a given activity instance class UID""" + parent = self._repos.activity_instance_class_repository.get_parent_class(uid) + return parent[0] if parent else None + def _transform_aggregate_root_to_pydantic_model( self, item_ar: ActivityInstanceAR ) -> ActivityInstance: @@ -83,9 +85,6 @@ def _create_aggregate_root( activity_item_class_name=None, ct_terms=ct_terms, unit_definitions=unit_definitions, - odm_form=CompactOdmForm(uid=item.odm_form_uid), - odm_item_group=CompactOdmItemGroup(uid=item.odm_item_group_uid), - odm_item=CompactOdmItem(uid=item.odm_item_uid), ) ) @@ -133,9 +132,6 @@ def _create_aggregate_root( concept_exists_by_library_and_property_value_callback=self._repos.activity_instance_repository.latest_concept_in_library_exists_by_property_value, ct_term_exists_by_uid_callback=self._repos.ct_term_name_repository.term_exists, unit_definition_exists_by_uid_callback=self._repos.unit_definition_repository.final_concept_exists, - get_odm_form_by_uid_callback=self._repos.odm_form_repository.find_by_uid_2, - get_odm_item_group_by_uid_callback=self._repos.odm_item_group_repository.find_by_uid_2, - get_odm_item_by_uid_callback=self._repos.odm_item_repository.find_by_uid_2, get_final_activity_value_by_uid_callback=self._repos.activity_repository.final_concept_value, activity_group_exists=self._repos.activity_group_repository.final_concept_exists, activity_subgroup_exists=self._repos.activity_subgroup_repository.final_concept_exists, @@ -147,6 +143,8 @@ def _create_aggregate_root( find_activity_instance_class_by_uid_callback=self._repos.activity_instance_class_repository.find_by_uid_2, preview=preview, get_dimension_names_by_unit_definition_uids=self._repos.unit_definition_repository.get_dimension_names_by_unit_definition_uids, + get_parent_class_uid_callback=self._get_parent_class_uid, + strict_mode=getattr(concept_input, "strict_mode", False), ) def _edit_aggregate( @@ -206,11 +204,6 @@ def _edit_aggregate( activity_item_class_name=None, ct_terms=ct_terms, unit_definitions=unit_definitions, - odm_form=CompactOdmForm(uid=activity_item.odm_form_uid), - odm_item_group=CompactOdmItemGroup( - uid=activity_item.odm_item_group_uid - ), - odm_item=CompactOdmItem(uid=activity_item.odm_item_uid), ) ) else: @@ -283,15 +276,18 @@ def _edit_aggregate( concept_exists_by_library_and_property_value_callback=self._repos.activity_instance_repository.latest_concept_in_library_exists_by_property_value, ct_term_exists_by_uid_callback=self._repos.ct_term_name_repository.term_exists, unit_definition_exists_by_uid_callback=self._repos.unit_definition_repository.final_concept_exists, - get_odm_form_by_uid_callback=self._repos.odm_form_repository.find_by_uid_2, - get_odm_item_group_by_uid_callback=self._repos.odm_item_group_repository.find_by_uid_2, - get_odm_item_by_uid_callback=self._repos.odm_item_repository.find_by_uid_2, get_final_activity_value_by_uid_callback=self._repos.activity_repository.final_concept_value, activity_group_exists=self._repos.activity_group_repository.final_concept_exists, activity_subgroup_exists=self._repos.activity_subgroup_repository.final_concept_exists, find_activity_item_class_by_uid_callback=self._repos.activity_item_class_repository.find_by_uid_2, find_activity_instance_class_by_uid_callback=self._repos.activity_instance_class_repository.find_by_uid_2, get_dimension_names_by_unit_definition_uids=self._repos.unit_definition_repository.get_dimension_names_by_unit_definition_uids, + get_parent_class_uid_callback=self._get_parent_class_uid, + strict_mode=( + concept_edit_input.strict_mode + if concept_edit_input.strict_mode is not None + else False + ), perform_validation=perform_validation, ) return item diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_service.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_service.py index 46f48051..0197a853 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_service.py @@ -25,7 +25,6 @@ ActivityRequestRejectInput, ActivityVersion, ActivityVersionDetail, - CompactActivity, ) from clinical_mdr_api.models.concepts.activities.activity_instance import ( ActivityInstanceDetail, @@ -33,7 +32,6 @@ ActivityInstanceGrouping, ) from clinical_mdr_api.models.utils import GenericFilteringReturn -from clinical_mdr_api.repositories._utils import FilterOperator from clinical_mdr_api.services._utils import is_library_editable from clinical_mdr_api.services.concepts import constants from clinical_mdr_api.services.concepts.activities.activity_instance_service import ( @@ -231,9 +229,14 @@ def _edit_aggregate( activity_groups_by_uid = {} activity_subgroups_by_uid = {} else: - activity_groupings = [] - activity_groups_by_uid = set() - activity_subgroups_by_uid = set() + # Preserve existing groupings when not explicitly provided + activity_groupings = item.concept_vo.activity_groupings + activity_groups_by_uid = { + g.activity_group_uid for g in item.concept_vo.activity_groupings + } + activity_subgroups_by_uid = { + g.activity_subgroup_uid for g in item.concept_vo.activity_groupings + } synonyms = ( [] if concept_edit_input.synonyms is None else concept_edit_input.synonyms ) @@ -670,56 +673,3 @@ def get_flattened_activity_instances_for_version( paginated_items = flattened_items return GenericFilteringReturn(items=paginated_items, total=total) - - def get_compact_activity_with_splitted_groupings( - self, - library: str | None = None, - sort_by: dict[str, bool] | None = None, - page_number: int = 1, - page_size: int = 0, - filter_by: dict[str, dict[str, Any]] | None = None, - filter_operator: FilterOperator = FilterOperator.AND, - total_count: bool = False, - ) -> GenericFilteringReturn[CompactActivity]: - self.enforce_library(library) - - items, total = self.repository.get_compact_activity_with_splitted_groupings( - library=library, - total_count=total_count, - sort_by=sort_by, - filter_by=filter_by, - filter_operator=filter_operator, - page_number=page_number, - page_size=page_size, - ) - - all_concepts = GenericFilteringReturn(items=items, total=total) - all_concepts.items = [ - CompactActivity.from_repository_output(item) for item in all_concepts.items - ] - - return all_concepts - - def get_compact_activity_with_splitted_groupings_distinct_values_for_header( - self, - library: str | None, - field_name: str, - search_string: str = "", - filter_by: dict[str, dict[str, Any]] | None = None, - filter_operator: FilterOperator = FilterOperator.AND, - page_size: int = 10, - **kwargs, - ) -> list[Any]: - self.enforce_library(library) - - header_values = self.repository.get_compact_activity_with_splitted_groupings_distinct_headers( - library=library, - field_name=field_name, - search_string=search_string, - filter_by=filter_by, - filter_operator=filter_operator, - page_size=page_size, - **kwargs, - ) - - return header_values diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_sub_group_service.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_sub_group_service.py index 0c8266f6..11cc1195 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_sub_group_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/activities/activity_sub_group_service.py @@ -9,7 +9,6 @@ from clinical_mdr_api.domains.concepts.activities.activity_sub_group import ( ActivitySubGroupAR, ActivitySubGroupVO, - SimpleActivityGroupVO, ) from clinical_mdr_api.domains.enums import LibraryItemStatus from clinical_mdr_api.models.concepts.activities.activity import ( @@ -18,7 +17,7 @@ SimpleActivity, ) from clinical_mdr_api.models.concepts.activities.activity_sub_group import ( - ActivityGroup, + ActivityGroupForActivitySubGroup, ActivitySubGroup, ActivitySubGroupCreateInput, ActivitySubGroupDetail, @@ -47,12 +46,9 @@ class ActivitySubGroupService(ConceptGenericService[ActivitySubGroupAR]): def _transform_aggregate_root_to_pydantic_model( self, item_ar: ActivitySubGroupAR, - was_cascade_update_performed: bool | None = None, ) -> ActivitySubGroup: return ActivitySubGroup.from_activity_ar( activity_subgroup_ar=item_ar, - find_activity_by_uid=self._repos.activity_group_repository.find_by_uid_2, - was_cascade_update_performed=was_cascade_update_performed, ) def _create_aggregate_root( @@ -65,35 +61,17 @@ def _create_aggregate_root( name_sentence_case=concept_input.name_sentence_case, definition=concept_input.definition, abbreviation=concept_input.abbreviation, - activity_groups=( - [ - SimpleActivityGroupVO(activity_group_uid=activity_group) - for activity_group in concept_input.activity_groups - ] - if concept_input.activity_groups - else [] - ), ), library=library, generate_uid_callback=self.repository.generate_uid, concept_exists_by_library_and_name_callback=self._repos.activity_subgroup_repository.latest_concept_in_library_exists_by_name, - activity_group_exists=self._repos.activity_group_repository.final_concept_exists, ) def _edit_aggregate( self, item: ActivitySubGroupAR, concept_edit_input: ActivitySubGroupEditInput, - perform_validation: bool = True, ) -> ActivitySubGroupAR: - activity_groups = ( - [ - SimpleActivityGroupVO(activity_group_uid=activity_group) - for activity_group in concept_edit_input.activity_groups - ] - if concept_edit_input.activity_groups - else [] - ) item.edit_draft( author_id=self.author_id, change_description=concept_edit_input.change_description, @@ -102,11 +80,8 @@ def _edit_aggregate( name_sentence_case=concept_edit_input.name_sentence_case, definition=concept_edit_input.definition, abbreviation=concept_edit_input.abbreviation, - activity_groups=activity_groups, ), concept_exists_by_library_and_name_callback=self._repos.activity_subgroup_repository.latest_concept_in_library_exists_by_name, - activity_group_exists=self._repos.activity_group_repository.final_concept_exists, - perform_validation=perform_validation, ) return item @@ -134,64 +109,6 @@ def get_subgroup_overview( linked_activity_group_data, ) - activity_groups: list[ActivityGroup] = [] - if linked_activity_group_data: - # Dynamically import ActivityGroupService to avoid circular imports - from clinical_mdr_api.services.concepts.activities.activity_group_service import ( - ActivityGroupService, - ) - - activity_group_service = ActivityGroupService() - - for group_data in linked_activity_group_data: - try: - logger.debug( - "Fetching activity group with UID: %s and version: %s", - group_data["uid"], - group_data["version"], - ) - activity_group = activity_group_service.get_by_uid( - uid=group_data["uid"], version=group_data["version"] - ) - # Add the linked version to the activity group information - activity_groups.append( - ActivityGroup( - uid=activity_group.uid, - name=activity_group.name, - version=group_data[ - "version" - ], # Use the version from the relationship - status=activity_group.status, - ) - ) - logger.debug( - "Added activity group: %s with version %s", - activity_group, - group_data["version"], - ) - except exceptions.NotFoundException: - logger.debug( - "Activity group with UID '%s' not found - skipping", - group_data["uid"], - ) - continue - except exceptions.BusinessLogicException as e: - logger.info( - "Business logic prevented access to activity group '%s': %s", - group_data["uid"], - str(e), - ) - continue - except db.DatabaseError as e: - logger.warning( - "Database error retrieving activity group '%s': %s", - group_data["uid"], - str(e), - ) - continue - - logger.debug("Final activity groups: %s", activity_groups) - activity_subgroup_detail = ActivitySubGroupDetail( name=subgroup.name, name_sentence_case=subgroup.name_sentence_case, @@ -204,17 +121,12 @@ def get_subgroup_overview( possible_actions=subgroup.possible_actions, change_description=subgroup.change_description, author_username=subgroup.author_username, - activity_groups=activity_groups, ) result = ActivitySubGroupOverview( activity_subgroup=activity_subgroup_detail, all_versions=all_versions, ) - logger.debug( - "Created overview with %s activity groups", - len(activity_subgroup_detail.activity_groups), - ) return result def get_activities_for_subgroup( @@ -305,7 +217,7 @@ def get_activity_groups_for_subgroup_paginated( page_number: int = 1, page_size: int = 10, total_count: bool = False, - ) -> CustomPage[ActivityGroup]: + ) -> CustomPage[ActivityGroupForActivitySubGroup]: """ Get activity groups for a specific activity subgroup with pagination. @@ -319,23 +231,35 @@ def get_activity_groups_for_subgroup_paginated( Returns: CustomPage containing paginated ActivityGroup objects """ - # Get the overview which contains all activity groups - overview = self.get_subgroup_overview( - subgroup_uid=subgroup_uid, version=version - ) - activity_groups = overview.activity_subgroup.activity_groups - - # Handle pagination - start_idx = (page_number - 1) * page_size - end_idx = start_idx + page_size if page_size > 0 else len(activity_groups) - paginated_groups = ( - activity_groups[start_idx:end_idx] if page_size > 0 else activity_groups + linked_groups = ( + self._repos.activity_subgroup_repository.get_linked_activity_group_uids( + subgroup_uid=subgroup_uid, version=version + ) ) - total = len(activity_groups) if total_count else 0 + # Apply pagination + if page_size == 0: + paginated_groups = linked_groups + else: + start_index = (page_number - 1) * page_size + end_index = start_index + page_size + paginated_groups = linked_groups[start_index:end_index] + + total = len(linked_groups) if total_count else 0 + + # Transform to ActivityGroup models + activity_groups: list[ActivityGroupForActivitySubGroup] = [] + for group in paginated_groups: + activity_group = ActivityGroupForActivitySubGroup( + uid=group["uid"], + name=group["name"], + version=group["version"], + status=group["status"], + ) + activity_groups.append(activity_group) return CustomPage( - items=paginated_groups, total=total, page=page_number, size=page_size + items=activity_groups, total=total, page=page_number, size=page_size ) def get_cosmos_subgroup_overview(self, subgroup_uid: str) -> dict[str, Any]: @@ -369,25 +293,9 @@ def cascade_edit_and_approve(self, item: ActivitySubGroupAR): ) ) if linked_activities is None: - return False + return - activity_groups_before_subgroup_update = self._repos.activity_subgroup_repository.get_activity_group_uids_linked_by_subgroup_in_specific_version( - activity_subgroup_uid=item.uid, version=last_final_version - ) - removed_activity_groups = set(activity_groups_before_subgroup_update) - { - ag.activity_group_uid for ag in item.concept_vo.activity_groups - } - if removed_activity_groups: - for activity in linked_activities.get("activities", []): - for grouping in activity["activity_groupings"]: - # If any of the activity's groupings reference a removed activity group, skip Activity cascade update - if grouping["activity_group_uid"] in removed_activity_groups: - return False - - was_cascade_update_performed = self.batch_cascade_update( - linked_activities=linked_activities - ) - return was_cascade_update_performed + self.batch_cascade_update(linked_activities=linked_activities) def batch_cascade_update(self, linked_activities: dict[str, Any]): activity_service = ActivityService() @@ -446,7 +354,6 @@ def batch_cascade_update(self, linked_activities: dict[str, Any]): activity, author_id=self.author_id ) activity_service.cascade_edit_and_approve(activity) - return True @ensure_transaction(db) def approve( @@ -459,9 +366,6 @@ def approve( except exceptions.BusinessLogicException as exc: if not ignore_exc or exc.msg != "The object isn't in draft status.": raise - was_cascade_update_performed = None if cascade_edit_and_approve: - was_cascade_update_performed = self.cascade_edit_and_approve(item) - return self._transform_aggregate_root_to_pydantic_model( - item, was_cascade_update_performed=was_cascade_update_performed - ) + self.cascade_edit_and_approve(item) + return self._transform_aggregate_root_to_pydantic_model(item) diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/concept_generic_service.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/concept_generic_service.py index a3b43b42..87895a68 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/concept_generic_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/concept_generic_service.py @@ -200,7 +200,7 @@ def get_input_or_previous_property( ): return input_property if input_property is not None else previous_property - @db.transaction + @ensure_transaction(db) def get_all_concepts( self, library: str | None = None, @@ -211,29 +211,7 @@ def get_all_concepts( filter_operator: FilterOperator = FilterOperator.AND, total_count: bool = False, **kwargs, - ) -> GenericFilteringReturn[BaseModel]: - return self.non_transactional_get_all_concepts( - library, - sort_by, - page_number, - page_size, - filter_by, - filter_operator, - total_count, - **kwargs, - ) - - def non_transactional_get_all_concepts( - self, - library: str | None = None, - sort_by: dict[str, bool] | None = None, - page_number: int = 1, - page_size: int = 0, - filter_by: dict[str, dict[str, Any]] | None = None, - filter_operator: FilterOperator = FilterOperator.AND, - total_count: bool = False, - **kwargs, - ) -> GenericFilteringReturn[BaseModel]: + ) -> GenericFilteringReturn[Any]: self.enforce_library(library) item_ars, total = self.repository.find_all( @@ -386,17 +364,6 @@ def create_new_version( cascade_new_version: bool = False, force_new_value_node: bool = False, ignore_exc: bool = False, - ) -> BaseModel: - return self.non_transactional_create_new_version( - uid, cascade_new_version, force_new_value_node, ignore_exc - ) - - def non_transactional_create_new_version( - self, - uid: str, - cascade_new_version: bool = False, - force_new_value_node: bool = False, - ignore_exc: bool = False, ) -> BaseModel: item = self._find_by_uid_or_raise_not_found(uid, for_update=True) try: @@ -417,11 +384,6 @@ def non_transactional_create_new_version( @ensure_transaction(db) def edit_draft( self, uid: str, concept_edit_input: BaseModel, patch_mode: bool = True - ) -> BaseModel: - return self.non_transactional_edit(uid, concept_edit_input, patch_mode) - - def non_transactional_edit( - self, uid: str, concept_edit_input: BaseModel, patch_mode: bool = True ) -> BaseModel: item = self._find_by_uid_or_raise_not_found(uid=uid, for_update=True) if patch_mode: @@ -436,10 +398,6 @@ def non_transactional_edit( self.repository.save(item) return self._transform_aggregate_root_to_pydantic_model(item) - @ensure_transaction(db) - def create(self, concept_input: BaseModel, preview: bool = False) -> BaseModel: - return self.non_transactional_create(concept_input, preview) - def generate_default_name(self, response_model): param_specific_item_classes = {} current_activity_item_ctterms = set() @@ -478,45 +436,6 @@ def generate_default_name(self, response_model): and activity_class_vo.name != "standard_unit" ): current_activity_item_ctterms.add(ct_term.uid) - cypher_terms_filering = " AND ".join( - f"'{item}' in ct_term_uid_collected" - for item in sorted(current_activity_item_ctterms) - ) - cypher_counting_filtering = ( - f" AND counting = {len(current_activity_item_ctterms)}" - if cypher_terms_filering - else "" - ) - cypher_activity_filtering = ( - f"AND act_root.uid = '{response_model.activity_groupings[0].activity.uid}'" - ) - cypher_expression = f""" - MATCH (act_inst_root:ActivityInstanceRoot)--(act_inst_val:ActivityInstanceValue)--(activity_item:ActivityItem)-[:HAS_CT_TERM]-(ct_term:CTTermRoot) - MATCH (act_inst_val)--(act_group:ActivityGrouping)-[:HAS_GROUPING]-(act_val:ActivityValue)--(act_root:ActivityRoot) - // CHECK THAT THE ACTIVITY INSTANCE BELONGS TO NUMERIC FINDGS LVL 2 - MATCH (act_inst_val)--(act_inst_class_root:ActivityInstanceClassRoot)--(act_inst_class_val:ActivityInstanceClassValue) - WHERE act_inst_class_val.name = "NumericFindings" - // CHECK THAT THE ACTIVITY ITEMS are connected to an activity_item_class_root and activity_instance_class_root - match (:ActivityInstanceClassRoot)--(act_item_class_root:ActivityItemClassRoot)--(activity_item) - // filter those activity items that are connected to unit_dimention activity_item_class - MATCH (act_item_class_root:ActivityItemClassRoot)--(act_item_class_val:ActivityItemClassValue) - WHERE act_item_class_val.name <> "unit_dimension" - WITH act_root,act_inst_root,act_inst_val, collect(distinct ct_term.uid) as ct_term_uid_collected - WITH act_root,act_inst_root,act_inst_val, ct_term_uid_collected, size(ct_term_uid_collected) as counting - WHERE - {cypher_terms_filering} - {cypher_counting_filtering} - {cypher_activity_filtering} - return act_inst_val - """ - if cypher_terms_filering and cypher_activity_filtering: - existent_equal_instances, _ = db.cypher_query( - cypher_expression, - resolve_objects=True, - ) - else: - existent_equal_instances = [] - if_exists = bool(len(list(existent_equal_instances)) > 0) activity_item_classes_order = [ "location", "laterality", @@ -528,12 +447,51 @@ def generate_default_name(self, response_model): "fasting_status", "standard_unit", ] - - standard_unit_suffix = ( - f" ({param_specific_item_classes['standard_unit']})" - if "standard_unit" in param_specific_item_classes - else "" - ) + # TODO: Uncomment this when the standard unit is implemented + # cypher_terms_filering = " AND ".join( + # f"'{item}' in ct_term_uid_collected" + # for item in sorted(current_activity_item_ctterms) + # ) + # cypher_counting_filtering = ( + # f" AND counting = {len(current_activity_item_ctterms)}" + # if cypher_terms_filering + # else "" + # ) + # cypher_activity_filtering = ( + # f"AND act_root.uid = '{response_model.activity_groupings[0].activity.uid}'" + # ) + # cypher_expression = f""" + # MATCH (act_inst_root:ActivityInstanceRoot)--(act_inst_val:ActivityInstanceValue)--(activity_item:ActivityItem)-[:HAS_CT_TERM]-(ct_term:CTTermRoot) + # MATCH (act_inst_val)--(act_group:ActivityGrouping)-[:HAS_GROUPING]-(act_val:ActivityValue)--(act_root:ActivityRoot) + # // CHECK THAT THE ACTIVITY INSTANCE BELONGS TO NUMERIC FINDGS LVL 2 + # MATCH (act_inst_val)--(act_inst_class_root:ActivityInstanceClassRoot)--(act_inst_class_val:ActivityInstanceClassValue) + # WHERE act_inst_class_val.name = "NumericFindings" + # // CHECK THAT THE ACTIVITY ITEMS are connected to an activity_item_class_root and activity_instance_class_root + # match (:ActivityInstanceClassRoot)--(act_item_class_root:ActivityItemClassRoot)--(activity_item) + # // filter those activity items that are connected to unit_dimention activity_item_class + # MATCH (act_item_class_root:ActivityItemClassRoot)--(act_item_class_val:ActivityItemClassValue) + # WHERE act_item_class_val.name <> "unit_dimension" + # WITH act_root,act_inst_root,act_inst_val, collect(distinct ct_term.uid) as ct_term_uid_collected + # WITH act_root,act_inst_root,act_inst_val, ct_term_uid_collected, size(ct_term_uid_collected) as counting + # WHERE + # {cypher_terms_filering} + # {cypher_counting_filtering} + # {cypher_activity_filtering} + # return act_inst_val + # """ + # if cypher_terms_filering and cypher_activity_filtering: + # existent_equal_instances, _ = db.cypher_query( + # cypher_expression, + # resolve_objects=True, + # ) + # else: + # existent_equal_instances = [] + # if_exists = bool(len(list(existent_equal_instances)) > 0) + # standard_unit_suffix = ( + # f" ({param_specific_item_classes['standard_unit']})" + # if "standard_unit" in param_specific_item_classes + # else "" + # ) param_names = [ param_specific_item_classes[cls] for cls in activity_item_classes_order @@ -550,17 +508,21 @@ def generate_default_name(self, response_model): if response_model.is_research_lab: generated_name += " Research" activity_sequence_number = 0 - final_generated_name = ( - f"{generated_name}{standard_unit_suffix}" - if if_exists - else f"{generated_name}" - ) + # TODO: Uncomment this when the standard unit is implemented + # final_generated_name = ( + # f"{generated_name}{standard_unit_suffix}" + # if if_exists + # else f"{generated_name}" + # ) + final_generated_name = generated_name while self.repository.exists_by("name", final_generated_name, False): activity_sequence_number += 1 final_generated_name = ( - f"{generated_name} {activity_sequence_number}{standard_unit_suffix}" + f"{generated_name} {activity_sequence_number}" + # TODO: Uncomment this when the standard unit is implemented + # {standard_unit_suffix}" ) - return final_generated_name + return final_generated_name.strip() def generate_default_topic_code(self, response_model): without_special_characters = "".join( @@ -575,15 +537,17 @@ def generate_default_topic_code(self, response_model): def generate_default_name_sentence_case(self, response_model): current_name: str = response_model.name - split = re.split(r"(\(.*\))", current_name) - standard_unit = "" - name_without_standard_unit = "" - if len(split) > 1: - name_without_standard_unit = split[0] - standard_unit = split[1] - else: - name_without_standard_unit = current_name - final_name = name_without_standard_unit.lower() + standard_unit + # TODO: Uncomment this when the standard unit is implemented + # split = re.split(r"(\(.*\))", current_name) + # standard_unit = "" + # name_without_standard_unit = "" + # if len(split) > 1: + # name_without_standard_unit = split[0] + # standard_unit = split[1] + # else: + # name_without_standard_unit = current_name + # final_name = name_without_standard_unit.lower() + standard_unit + final_name = current_name.lower() return final_name.strip() def generate_default_adam_code(self, response_model): @@ -697,9 +661,8 @@ def generate_default_adam_code(self, response_model): final_generated_name = f"{adam_final[:7-number_of_letters_to_remove]}{activity_sequence_number}" return final_generated_name - def non_transactional_create( - self, concept_input: BaseModel, preview: bool = False - ) -> BaseModel: + @ensure_transaction(db) + def create(self, concept_input: BaseModel, preview: bool = False) -> BaseModel: BusinessLogicException.raise_if_not( self._repos.library_repository.library_exists( normalize_string(concept_input.library_name) # type: ignore[arg-type] diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_clinspark_import.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_clinspark_import.py index 130befcb..19117a75 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_clinspark_import.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_clinspark_import.py @@ -157,7 +157,7 @@ def _get_and_create_codelists(self): ) if submission_value not in existing_codelist_submission_values: - ct_codelist = self.ct_codelist_service.non_transactional_create( + ct_codelist = self.ct_codelist_service.create( CTCodelistCreateInput( catalogue_names=["CDASH CT"], name=codelist.getAttribute("Name"), @@ -173,12 +173,8 @@ def _get_and_create_codelists(self): terms=[], ) ) - self.ct_codelist_name_service.non_transactional_approve( - ct_codelist.codelist_uid - ) - self.ct_codelist_attributes_service.non_transactional_approve( - ct_codelist.codelist_uid - ) + self.ct_codelist_name_service.approve(ct_codelist.codelist_uid) + self.ct_codelist_attributes_service.approve(ct_codelist.codelist_uid) new_codelist_uids.add(ct_codelist.codelist_uid) existing_codelist_submission_values.add(submission_value) @@ -199,7 +195,7 @@ def manage_codelist_item(): # the codelist_uid field on CTCodelistAttributes is optional return None - ct_term = self.ct_term_service.non_transactional_create( + ct_term = self.ct_term_service.create( CTTermCreateInput( catalogue_names=["CDASH CT"], codelists=[ @@ -218,10 +214,8 @@ def manage_codelist_item(): library_name="Sponsor", ) ) - self.ct_term_name_service.non_transactional_approve(ct_term.term_uid) - self.ct_term_attributes_service.non_transactional_approve( - ct_term.term_uid - ) + self.ct_term_name_service.approve(ct_term.term_uid) + self.ct_term_attributes_service.approve(ct_term.term_uid) term_attribute_uids.add(ct_term.term_uid) term_submission_value = next( ( @@ -247,7 +241,7 @@ def manage_codelist_item(): term_attribute_uids.add(item.uid) return True - self.ct_codelist_service.non_transactional_add_term( + self.ct_codelist_service.add_term( active_codelist.codelist_uid, item.uid, idx, @@ -336,7 +330,7 @@ def _get_item_unit_definition_inputs(self, item_def): def _get_odm_item_post_input(self, item_def): descriptions = self._extract_descriptions(item_def) - plausible_duplicates = self.odm_item_service.non_transactional_get_all_concepts( + plausible_duplicates = self.odm_item_service.get_all_concepts( filter_by={"name": {"v": [item_def.getAttribute("Name")], "op": "co"}} ).items @@ -421,13 +415,9 @@ def _get_odm_item_post_input(self, item_def): def _get_odm_item_group_post_input(self, item_group_def): descriptions = self._extract_descriptions(item_group_def) - plausible_duplicates = ( - self.odm_item_group_service.non_transactional_get_all_concepts( - filter_by={ - "name": {"v": [item_group_def.getAttribute("Name")], "op": "co"} - } - ).items - ) + plausible_duplicates = self.odm_item_group_service.get_all_concepts( + filter_by={"name": {"v": [item_group_def.getAttribute("Name")], "op": "co"}} + ).items return OdmItemGroupPostInput( oid=item_group_def.getAttribute("OID"), @@ -455,7 +445,7 @@ def _get_odm_item_group_post_input(self, item_group_def): def _get_odm_form_post_input(self, form_def): descriptions = self._extract_descriptions(form_def) - plausible_duplicates = self.odm_form_service.non_transactional_get_all_concepts( + plausible_duplicates = self.odm_form_service.get_all_concepts( filter_by={"name": {"v": [form_def.getAttribute("Name")], "op": "co"}} ).items diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_data_extractor.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_data_extractor.py index 5aeaefcb..5ab7652c 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_data_extractor.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_data_extractor.py @@ -41,12 +41,13 @@ class OdmDataExtractor: - target_uids: list[str] - target_name: str + targets: list[str] + first_target_uid: str + first_target_name: str odm_vendor_namespaces: dict[str, dict[str, str]] odm_vendor_elements: dict[str, dict[str, dict[str, str]]] - odm_study_event: list[OdmStudyEvent] + odm_study_events: list[OdmStudyEvent] odm_forms: list[OdmForm] odm_item_groups: list[OdmItemGroup] odm_items: list[OdmItem] @@ -69,12 +70,7 @@ class OdmDataExtractor: ct_term_attributes_service: CTTermAttributesService unit_definition_service: UnitDefinitionService - def __init__( - self, - target_uids: list[str], - target_type: TargetType, - version: str | None = None, - ): + def __init__(self, target_type: TargetType, targets: list[str]): self.unit_definition_service = UnitDefinitionService() self.vendor_namespace_service = OdmVendorNamespaceService() self.vendor_element_service = OdmVendorElementService() @@ -91,6 +87,7 @@ def __init__( self.odm_vendor_namespaces = {} self.odm_vendor_elements = {} self.ref_odm_vendor_attributes: dict[str, dict[str, dict[str, str]]] = {} + self.odm_study_events = [] self.odm_forms = [] self.odm_item_groups = [] self.odm_items = [] @@ -100,55 +97,87 @@ def __init__( self.ct_terms = [] self.unit_definitions = [] - self.target_uids = target_uids + self.targets = targets if target_type == TargetType.STUDY_EVENT: - self.odm_study_event = self.study_event_service.get_all_concepts( - filter_by={"uid": {"v": target_uids, "op": "eq"}}, version=version - ).items - - if not self.odm_study_event: - raise NotFoundException( - msg=f"No ODM Study Event found for the given target UID(s): {target_uids}." + for target in targets: + uid, version = ( + target.rsplit(",", maxsplit=1) if "," in target else (target, None) ) - self.target_name = self.odm_study_event[0].name - self.set_forms_of_study_event(self.odm_study_event) - elif target_type == TargetType.FORM: - self.odm_forms = self.form_service.get_all_concepts( - filter_by={"uid": {"v": target_uids, "op": "eq"}}, version=version - ).items + self.odm_study_events += self.study_event_service.get_all_concepts( + filter_by={"uid": {"v": [uid], "op": "eq"}}, version=version or None + ).items - if not self.odm_forms: - raise NotFoundException( - msg=f"No ODM Form found for the given target UID(s): {target_uids}." + if not self.odm_study_events: + raise NotFoundException( + msg=f"ODM Study Event with UID '{uid}'" + + (f"and version '{version}'" if version is not None else "") + + "doesn't exist" + ) + + self.first_target_name = self.odm_study_events[0].name + self.first_target_uid = self.odm_study_events[0].uid + self.set_forms_of_study_event(self.odm_study_events) + elif target_type == TargetType.FORM: + for target in targets: + uid, version = ( + target.rsplit(",", maxsplit=1) if "," in target else (target, None) ) - self.target_name = self.odm_forms[0].name + self.odm_forms += self.form_service.get_all_concepts( + filter_by={"uid": {"v": [uid], "op": "eq"}}, version=version or None + ).items + + if not self.odm_forms: + raise NotFoundException( + msg=f"ODM Form with UID '{uid}'" + + (f"and version '{version}'" if version is not None else "") + + "doesn't exist" + ) + + self.first_target_name = self.odm_forms[0].name + self.first_target_uid = self.odm_forms[0].uid self.set_item_groups_of_forms(self.odm_forms) elif target_type == TargetType.ITEM_GROUP: - self.odm_item_groups = self.item_group_service.get_all_concepts( - filter_by={"uid": {"v": target_uids, "op": "eq"}}, version=version - ).items - - if not self.odm_item_groups: - raise NotFoundException( - msg=f"No ODM Item Group found for the given target UID(s): {target_uids}." + for target in targets: + uid, version = ( + target.rsplit(",", maxsplit=1) if "," in target else (target, None) ) - self.target_name = self.odm_item_groups[0].name + self.odm_item_groups += self.item_group_service.get_all_concepts( + filter_by={"uid": {"v": [uid], "op": "eq"}}, version=version or None + ).items + + if not self.odm_item_groups: + raise NotFoundException( + msg=f"ODM Item Group with UID '{uid}'" + + (f"and version '{version}'" if version is not None else "") + + "doesn't exist" + ) + + self.first_target_name = self.odm_item_groups[0].name + self.first_target_uid = self.odm_item_groups[0].uid self.set_items_of_item_groups(self.odm_item_groups) elif target_type == TargetType.ITEM: - self.odm_items = self.item_service.get_all_concepts( - filter_by={"uid": {"v": target_uids, "op": "eq"}}, version=version - ).items - - if not self.odm_items: - raise NotFoundException( - msg=f"No ODM Item found for the given target UID(s): {target_uids}." + for target in targets: + uid, version = ( + target.rsplit(",", maxsplit=1) if "," in target else (target, None) ) - self.target_name = self.odm_items[0].name + self.odm_items = self.item_service.get_all_concepts( + filter_by={"uid": {"v": [uid], "op": "eq"}}, version=version or None + ).items + + if not self.odm_items: + raise NotFoundException( + msg=f"ODM Item with UID '{uid}'" + + (f"and version '{version}'" if version is not None else "") + + "doesn't exist" + ) + + self.first_target_name = self.odm_items[0].name + self.first_target_uid = self.odm_items[0].uid self.set_unit_definitions_of_items(self.odm_items) self.set_codelists_of_items(self.odm_items) else: @@ -241,59 +270,63 @@ def set_vendor_namespaces(self): } def set_forms_of_study_event(self, study_events: list[OdmStudyEvent]): - self.odm_forms = sorted( - self.form_service.get_all_concepts( - filter_by={ - "uid": { - "v": [ - form.uid - for study_event in study_events - for form in study_event.forms - ], - "op": "eq", - } - }, - ).items, - key=lambda elm: elm.name, - ) + self.odm_forms = [] + + for study_event in study_events: + for form in study_event.forms: + _forms = self.form_service.get_all_concepts( + filter_by={ + "uid": { + "v": [form.uid], + "op": "eq", + } + }, + version=form.version, + ).items + + self.odm_forms += _forms + + self.odm_forms.sort(key=lambda elm: elm.name) self.set_item_groups_of_forms(self.odm_forms) def set_item_groups_of_forms(self, forms: list[OdmForm]): - self.odm_item_groups = sorted( - self.item_group_service.get_all_concepts( - filter_by={ - "uid": { - "v": [ - item_group.uid - for form in forms - for item_group in form.item_groups - ], - "op": "eq", - } - }, - ).items, - key=lambda elm: elm.name, - ) + self.odm_item_groups = [] + for form in forms: + for item_group in form.item_groups: + _item_groups = self.item_group_service.get_all_concepts( + filter_by={ + "uid": { + "v": [item_group.uid], + "op": "eq", + } + }, + version=item_group.version, + ).items + + self.odm_item_groups += _item_groups + + self.odm_item_groups.sort(key=lambda elm: elm.name) self.set_items_of_item_groups(self.odm_item_groups) def set_items_of_item_groups(self, item_groups: list[OdmItemGroup]): - self.odm_items = sorted( - self.item_service.get_all_concepts( - filter_by={ - "uid": { - "v": [ - item.uid - for item_group in item_groups - for item in item_group.items - ], - "op": "eq", - } - }, - ).items, - key=lambda elm: elm.name, - ) + self.odm_items = [] + for item_group in item_groups: + for item in item_group.items: + _items = self.item_service.get_all_concepts( + filter_by={ + "uid": { + "v": [item.uid], + "op": "eq", + } + }, + version=item.version, + ).items + + self.odm_items += _items + + self.odm_items.sort(key=lambda elm: elm.name) self.set_unit_definitions_of_items(self.odm_items) self.set_codelists_of_items(self.odm_items) diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_forms.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_forms.py index 93661adf..bf28bbff 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_forms.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_forms.py @@ -22,7 +22,7 @@ OdmFormPostInput, OdmFormVersion, ) -from clinical_mdr_api.services._utils import get_input_or_new_value +from clinical_mdr_api.services._utils import ensure_transaction, get_input_or_new_value from clinical_mdr_api.services.concepts.odms.odm_generic_service import ( OdmGenericService, ) @@ -95,22 +95,12 @@ def _edit_aggregate( ) return item - @db.transaction + @ensure_transaction(db) def add_item_groups( self, uid: str, odm_form_item_group_post_input: list[OdmFormItemGroupPostInput], override: bool = False, - ) -> OdmForm: - return self.non_transactional_add_item_groups( - uid, odm_form_item_group_post_input, override - ) - - def non_transactional_add_item_groups( - self, - uid: str, - odm_form_item_group_post_input: list[OdmFormItemGroupPostInput], - override: bool = False, ) -> OdmForm: odm_form_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_item_groups.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_item_groups.py index 0c4ad745..52da70c4 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_item_groups.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_item_groups.py @@ -25,7 +25,7 @@ OdmItemGroupPostInput, OdmItemGroupVersion, ) -from clinical_mdr_api.services._utils import get_input_or_new_value +from clinical_mdr_api.services._utils import ensure_transaction, get_input_or_new_value from clinical_mdr_api.services.concepts.odms.odm_generic_service import ( OdmGenericService, ) @@ -110,22 +110,12 @@ def _edit_aggregate( ) return item - @db.transaction + @ensure_transaction(db) def add_items( self, uid: str, odm_item_group_item_post_input: list[OdmItemGroupItemPostInput], override: bool = False, - ) -> OdmItemGroup: - return self.non_transactional_add_items( - uid, odm_item_group_item_post_input, override - ) - - def non_transactional_add_items( - self, - uid: str, - odm_item_group_item_post_input: list[OdmItemGroupItemPostInput], - override: bool = False, ) -> OdmItemGroup: odm_item_group_ar = self._find_by_uid_or_raise_not_found(normalize_string(uid)) diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_items.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_items.py index 16edcfe0..1ff8ecba 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_items.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_items.py @@ -87,6 +87,7 @@ def _create_aggregate_root( ], codelist_uid=concept_input.codelist_uid, term_uids=[term.uid for term in concept_input.terms], + activity_instances=[], vendor_element_uids=[], vendor_attribute_uids=[], vendor_element_attribute_uids=[], @@ -102,6 +103,31 @@ def _create_aggregate_root( def _edit_aggregate( self, item: OdmItemAR, concept_edit_input: OdmItemPatchInput ) -> OdmItemAR: + if concept_edit_input.activity_instances: + parent_item_groups = self.get_active_relationships(item.uid).get( + "OdmItemGroup", [] + ) + + for activity_instance in concept_edit_input.activity_instances: + if activity_instance.odm_item_group_uid not in parent_item_groups: + raise BusinessLogicException( + msg=f"Cannot assign Activity Instance ({activity_instance.activity_instance_uid}, {activity_instance.activity_item_class_uid}) " + f"to ODM Item with UID '{item.uid}' because it isn't part of ODM Item Group with UID '{activity_instance.odm_item_group_uid}'" + ) + + parent_forms = ( + self._repos.odm_item_group_repository.get_active_relationships( + activity_instance.odm_item_group_uid, ["item_group_ref"] + ).get("OdmForm", []) + ) + + if activity_instance.odm_form_uid not in parent_forms: + raise BusinessLogicException( + msg=f"Cannot assign Activity Instance ({activity_instance.activity_instance_uid}, {activity_instance.activity_item_class_uid}) " + f"to ODM Item with UID '{item.uid}' because its Item Group with UID '{activity_instance.odm_item_group_uid}' " + f"isn't part of ODM Form with UID '{activity_instance.odm_form_uid}'" + ) + item.edit_draft( author_id=self.author_id, change_description=concept_edit_input.change_description, @@ -124,6 +150,10 @@ def _edit_aggregate( ], codelist_uid=concept_edit_input.codelist_uid, term_uids=[term.uid for term in concept_edit_input.terms], + activity_instances=[ + model.model_dump() + for model in concept_edit_input.activity_instances + ], vendor_element_uids=item.concept_vo.vendor_element_uids, vendor_attribute_uids=item.concept_vo.vendor_attribute_uids, vendor_element_attribute_uids=item.concept_vo.vendor_element_attribute_uids, @@ -132,6 +162,7 @@ def _edit_aggregate( unit_definition_exists_by_callback=self._repos.unit_definition_repository.exists_by, find_codelist_attribute_callback=self._repos.ct_codelist_attribute_repository.find_by_uid, find_all_terms_callback=self._repos.ct_term_name_repository.find_all, + find_activity_instance_callback=self._repos.activity_instance_repository.find_by_uid_2, ) return item @@ -579,7 +610,7 @@ def manage_vendors( return self.get_by_uid(uid) - @db.transaction + @ensure_transaction(db) def get_active_relationships(self, uid: str): NotFoundException.raise_if_not( self._repos.odm_item_repository.exists_by("uid", uid, True), @@ -591,6 +622,6 @@ def get_active_relationships(self, uid: str): uid, ["item_ref"] ) - @db.transaction + @ensure_transaction(db) def get_items_that_belongs_to_item_groups(self): return self._repos.odm_item_repository.get_if_has_relationship("item_ref") diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_metadata.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_metadata.py index 787b2616..f39c43a1 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_metadata.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_metadata.py @@ -30,10 +30,7 @@ def _query( validate_max_skip_clause(page_number=page_number, page_size=page_size) - params: dict[str, list[Any] | str | int] = { - "skip": page_size * (page_number - 1), - "limit": page_size, - } + params: dict[str, list[Any] | str | int] = {} where_stmt = "" @@ -69,6 +66,13 @@ def _query( ) """ + if page_size > 0: + limit = "SKIP $skip LIMIT $limit" + params["skip"] = page_size * (page_number - 1) + params["limit"] = page_size + else: + limit = "" + results, columns = db.cypher_query( dedent( f""" @@ -77,7 +81,7 @@ def _query( {exclude_old} RETURN DISTINCT {', '.join([f'n.{field} AS {field}' for field in fields])} ORDER BY n.{fields[0]} - SKIP $skip LIMIT $limit + {limit} """ ), params=params, diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_exporter.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_exporter.py index 8ded013a..a81b3189 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_exporter.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_exporter.py @@ -8,7 +8,7 @@ from lxml import etree from weasyprint import HTML -from clinical_mdr_api.domains._utils import get_iso_lang_data +from clinical_mdr_api.domains._utils import get_iso_lang_data, is_language_english from clinical_mdr_api.domains.concepts.odms.odm_xml_definition import ( ODM, Alias, @@ -41,7 +41,7 @@ Symbol, TranslatedText, ) -from clinical_mdr_api.domains.concepts.utils import ENG_LANGUAGE, TargetType +from clinical_mdr_api.domains.concepts.utils import EN_LANGUAGE, TargetType from clinical_mdr_api.models.concepts.odms.odm_common_models import ( OdmRefVendorAttributeModel, ) @@ -74,9 +74,8 @@ class OdmXmlExporterService: def __init__( self, - target_uids: list[str], target_type: TargetType, - version: str | None, + targets: list[str], allowed_namespaces: list[str], pdf: bool, stylesheet: str | None, @@ -86,19 +85,17 @@ def __init__( Initializes a new instance of the `OdmXmlGenerator` class. Args: - target_uids (list[str]): The UIDs of the ODM elements to generate XML for. target_type (TargetType): The type of the ODM element to generate XML for. - version (str | None): The version of the ODM elements to generate XML for. + targets (list[str]): The UIDs and versions of the ODM elements to generate XML for. allowed_namespaces (list[str]): A list of allowed vendor namespace prefixes. pdf (bool | None): A flag indicating whether to generate a PDF. stylesheet (str | None): The name of the stylesheet to include as the XML stylesheet. mapper_file (UploadFile | None): The mapper file to use for the XML generation. - unit_definition_service: The service that provides functionality for unit definitions. Returns: None """ - self.odm_data_extractor = OdmDataExtractor(target_uids, target_type, version) + self.odm_data_extractor = OdmDataExtractor(target_type, targets) self.mapper_file = mapper_file self.allowed_namespaces = allowed_namespaces self.pdf = pdf @@ -384,7 +381,7 @@ def create_odm_form_def(): ( description.instruction for description in form.descriptions - if description.language == ENG_LANGUAGE + if is_language_english(description.language) and description.instruction ), None, @@ -396,7 +393,7 @@ def create_odm_form_def(): ( description.sponsor_instruction for description in form.descriptions - if description.language == ENG_LANGUAGE + if is_language_english(description.language) and description.sponsor_instruction ), None, @@ -412,9 +409,8 @@ def create_odm_form_def(): description.description, lang=Attribute( self.XML_LANG, - get_iso_lang_data( - query=description.language or "en", - return_key="639-1", + get_iso_lang_data( # type: ignore[arg-type] + query=description.language or EN_LANGUAGE ), ), ) @@ -486,7 +482,7 @@ def create_odm_item_group_def(): ( description.instruction for description in item_group.descriptions - if description.language == ENG_LANGUAGE + if is_language_english(description.language) and description.instruction ), None, @@ -498,7 +494,7 @@ def create_odm_item_group_def(): ( description.sponsor_instruction for description in item_group.descriptions - if description.language == ENG_LANGUAGE + if is_language_english(description.language) and description.sponsor_instruction ), None, @@ -528,9 +524,8 @@ def create_odm_item_group_def(): description.description, lang=Attribute( self.XML_LANG, - get_iso_lang_data( - query=description.language or "en", - return_key="639-1", + get_iso_lang_data( # type: ignore[arg-type] + query=description.language or EN_LANGUAGE, ), ), ) @@ -587,7 +582,7 @@ def create_odm_item_def(): ( description.instruction for description in item.descriptions - if description.language == ENG_LANGUAGE + if is_language_english(description.language) and description.instruction ), None, @@ -599,7 +594,7 @@ def create_odm_item_def(): ( description.sponsor_instruction for description in item.descriptions - if description.language == ENG_LANGUAGE + if is_language_english(description.language) and description.sponsor_instruction ), None, @@ -622,9 +617,8 @@ def create_odm_item_def(): description.description, lang=Attribute( self.XML_LANG, - get_iso_lang_data( - query=description.language or "en", - return_key="639-1", + get_iso_lang_data( # type: ignore[arg-type] + query=description.language or EN_LANGUAGE, ), ), ) @@ -638,9 +632,8 @@ def create_odm_item_def(): description.name, lang=Attribute( self.XML_LANG, - get_iso_lang_data( - query=description.language or "en", - return_key="639-1", + get_iso_lang_data( # type: ignore[arg-type] + query=description.language or EN_LANGUAGE, ), ), ) @@ -698,9 +691,8 @@ def create_odm_condition_def(): description.description, lang=Attribute( self.XML_LANG, - get_iso_lang_data( - query=description.language or "en", - return_key="639-1", + get_iso_lang_data( # type: ignore[arg-type] + query=description.language or EN_LANGUAGE, ), ), ) @@ -741,9 +733,8 @@ def create_odm_method_def(): description.description, lang=Attribute( self.XML_LANG, - get_iso_lang_data( - query=description.language or "en", - return_key="639-1", + get_iso_lang_data( # type: ignore[arg-type] + query=description.language or EN_LANGUAGE, ), ), ) @@ -808,9 +799,7 @@ def create_odm_codelist(): or codelist_item["nci_preferred_name"], Attribute( self.XML_LANG, - get_iso_lang_data( - query="eng", return_key="639-1" - ), + get_iso_lang_data(query=EN_LANGUAGE), # type: ignore[arg-type] ), ) ), @@ -868,8 +857,7 @@ def create_odm_measurement_unit(): TranslatedText( unit_definition.name, lang=Attribute( - self.XML_LANG, - get_iso_lang_data(query="eng", return_key="639-1"), + self.XML_LANG, get_iso_lang_data(query=EN_LANGUAGE) # type: ignore[arg-type] ), ) ), @@ -897,7 +885,7 @@ def create_odm_measurement_unit(): study=Study( oid=Attribute( "OID", - f"{self.odm_data_extractor.target_name}-{self.odm_data_extractor.target_uids[0]}", + f"{self.odm_data_extractor.first_target_name}-{self.odm_data_extractor.first_target_uid}", ), meta_data_version=MetaDataVersion( oid=Attribute("OID", "MDV.0.1"), @@ -914,10 +902,12 @@ def create_odm_measurement_unit(): measurement_units=create_odm_measurement_unit() ), global_variables=GlobalVariables( - protocol_name=ProtocolName(self.odm_data_extractor.target_name), - study_name=StudyName(self.odm_data_extractor.target_name), + protocol_name=ProtocolName( + self.odm_data_extractor.first_target_name + ), + study_name=StudyName(self.odm_data_extractor.first_target_name), study_description=StudyDescription( - self.odm_data_extractor.target_name + self.odm_data_extractor.first_target_name ), ), ), diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_importer.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_importer.py index a7286293..58fdb114 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_importer.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/odms/odm_xml_importer.py @@ -8,9 +8,9 @@ from clinical_mdr_api.domain_repositories.concepts.odms.odm_generic_repository import ( OdmGenericRepository, ) -from clinical_mdr_api.domains._utils import get_iso_lang_data +from clinical_mdr_api.domains._utils import get_iso_lang_data, is_language_english from clinical_mdr_api.domains.concepts.utils import ( - ENG_LANGUAGE, + EN_LANGUAGE, RelationType, VendorAttributeCompatibleType, VendorElementCompatibleType, @@ -928,9 +928,7 @@ def _create_item_groups_with_relations(self): ) ) - self.odm_item_group_service.non_transactional_add_items( - rs.uid, odm_item_group_items - ) + self.odm_item_group_service.add_items(rs.uid, odm_item_group_items) self._approve( self._repos.odm_item_group_repository, self.odm_item_group_service, rs @@ -991,9 +989,7 @@ def _create_forms_with_relations(self): ) ) - self.odm_form_service.non_transactional_add_item_groups( - rs.uid, odm_form_item_groups - ) + self.odm_form_service.add_item_groups(rs.uid, odm_form_item_groups) self._approve(self._repos.odm_form_repository, self.odm_form_service, rs) @@ -1047,7 +1043,7 @@ def _create_study_event_with_relations(self): def _create_description( self, name: str | minidom.Text, - lang: str = ENG_LANGUAGE, + lang: str = EN_LANGUAGE, description: str | None = None, instruction: str | None = None, sponsor_instruction: str | None = None, @@ -1068,8 +1064,10 @@ def _create_description( name=str(name) or "TBD", language=lang, description=description, - instruction=instruction if lang == ENG_LANGUAGE else None, - sponsor_instruction=sponsor_instruction if lang == ENG_LANGUAGE else None, + instruction=instruction if is_language_english(lang) else None, + sponsor_instruction=( + sponsor_instruction if is_language_english(lang) else None + ), ) def _extract_descriptions(self, elm): @@ -1116,9 +1114,7 @@ def _extract_descriptions(self, elm): break for description in descriptions: - description["lang"] = get_iso_lang_data( - description["lang"] or "en", "639-1", "639-2/B" - ) + description["lang"] = get_iso_lang_data(description["lang"] or EN_LANGUAGE) return descriptions @@ -1337,8 +1333,6 @@ def _get_newly_created_terms(self): )[0] rs.sort(key=lambda elm: elm[0].uid) - print("ååååå") - print(rs) return [ CTTerm.from_ct_term_ars( ct_term_name_ar, ct_term_attributes_ar, ct_term_codelists diff --git a/clinical-mdr-api/clinical_mdr_api/services/concepts/unit_definitions/unit_definition.py b/clinical-mdr-api/clinical_mdr_api/services/concepts/unit_definitions/unit_definition.py index 88804226..df65c1c5 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/concepts/unit_definitions/unit_definition.py +++ b/clinical-mdr-api/clinical_mdr_api/services/concepts/unit_definitions/unit_definition.py @@ -17,7 +17,7 @@ ) from clinical_mdr_api.models.utils import GenericFilteringReturn from clinical_mdr_api.repositories._utils import FilterOperator -from clinical_mdr_api.services._utils import validate_is_dict +from clinical_mdr_api.services._utils import ensure_transaction, validate_is_dict from clinical_mdr_api.services.concepts.concept_generic_service import ( ConceptGenericService, ) @@ -114,7 +114,7 @@ def _edit_aggregate( return item - @db.transaction + @ensure_transaction(db) def get_all( self, library_name: str | None, @@ -134,7 +134,7 @@ def get_all( validate_is_dict("sort_by", sort_by) sort_by["size(name)"] = True - return self.non_transactional_get_all_concepts( + return self.get_all_concepts( library=library_name, dimension=dimension, subset=subset, diff --git a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/configuration.py b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/configuration.py index 22551c8d..5c9ac222 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/configuration.py +++ b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/configuration.py @@ -19,6 +19,7 @@ ) from clinical_mdr_api.models.utils import BaseModel from clinical_mdr_api.services._meta_repository import MetaRepository +from common.auth.user import user from common.exceptions import NotFoundException @@ -26,13 +27,9 @@ class CTConfigService: _repos: MetaRepository _author_id: str - def __init__( - self, - *, - author_id: str = "unknown-user", - ): + def __init__(self): self._repos = MetaRepository() - self._author_id = author_id + self._author_id = user().id() @db.transaction def get_all(self) -> list[CTConfigOGM]: diff --git a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist.py b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist.py index 5eabbdfe..88d2d29d 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist.py +++ b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist.py @@ -55,7 +55,8 @@ def __init__(self): def __del__(self): self._repos.close() - def non_transactional_create( + @ensure_transaction(db) + def create( self, codelist_input: CTCodelistCreateInput, start_date: datetime | None = None, @@ -216,17 +217,6 @@ def non_transactional_create( paired_names_codelist_uid=codelist_input.paired_names_codelist_uid, ) - @ensure_transaction(db) - def create( - self, - codelist_input: CTCodelistCreateInput, - start_date: datetime | None = None, - approve: bool = False, - ) -> CTCodelist: - return self.non_transactional_create( - codelist_input, start_date=start_date, approve=approve - ) - def get_all_codelists( self, catalogue_name: str | None = None, @@ -363,8 +353,9 @@ def get_distinct_values_for_header( return header_values - def non_transactional_add_term( - self, codelist_uid: str, term_uid: str, order: int, submission_value: str + @ensure_transaction(db) + def add_term( + self, codelist_uid: str, term_uid: str, order: int | None, submission_value: str ) -> CTCodelist: ct_codelist_attributes_ar = ( self._repos.ct_codelist_attribute_repository.find_by_uid( @@ -414,6 +405,54 @@ def non_transactional_add_term( codelist_uid=codelist_uid ) ) + + # Validation logic for adding terms to codelists + # Get library name for the term to check if it's CDISC or Sponsor + term_library_name = ( + self._repos.ct_term_name_repository.get_library_name_for_term(term_uid) + ) + + # Get all existing submission values for this term + existing_submission_values = ( + self._repos.ct_term_name_repository.get_submission_values_for_term(term_uid) + ) + + # Validation for CDISC terms (library = "CDISC") + if term_library_name == "CDISC": + # CDISC terms: all possible submission values are already defined, no new submission values can be added + BusinessLogicException.raise_if( + submission_value not in existing_submission_values, + msg=f"Term with UID '{term_uid}' is a CDISC term. Cannot add a new submission value '{submission_value}'. All possible submission values are already defined.", + ) + else: + # Sponsor terms validation + if submission_value not in existing_submission_values: + # New submission value: + # Either it is a term with no pre-existing submission value + # Or it is targeting a paired codelist + # If it is neither, then it is not allowed + + # This means it is not a term with no pre-existing submission value + if len(existing_submission_values) > 0: + # Check if is targeting a paired codelist + paired_codelist_uid = self._repos.ct_codelist_attribute_repository.get_paired_codelist_uid( + codelist_uid + ) + + term_in_paired = False + if paired_codelist_uid: + # Check if term is in the paired codelist with the same submission value + term_in_paired = self._repos.ct_codelist_attribute_repository.is_term_in_codelist( + term_uid, paired_codelist_uid + ) + + # If so, continue to creation ; otherwise raise an exception + BusinessLogicException.raise_if( + not term_in_paired, + # pylint: disable=line-too-long + msg=f"Term with UID '{term_uid}' is already part of a codelist with submission value '{existing_submission_values[0]}'. Cannot add a new submission value '{submission_value}', except for a paired codelist. Please reuse the existing submission value.", + ) + self._repos.ct_codelist_attribute_repository.add_term( codelist_uid=codelist_uid, term_uid=term_uid, @@ -434,14 +473,6 @@ def non_transactional_add_term( paired_names_codelist_uid=paired_names_codelist_uid, ) - @db.transaction - def add_term( - self, codelist_uid: str, term_uid: str, order: int, submission_value: str - ) -> CTCodelist: - return self.non_transactional_add_term( - codelist_uid, term_uid, order, submission_value - ) - @db.transaction def remove_term(self, codelist_uid: str, term_uid: str) -> CTCodelist: ct_codelist_attributes_ar = ( diff --git a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_generic_service.py b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_generic_service.py index d4b179e3..68d5d5e9 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_generic_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_codelist_generic_service.py @@ -12,7 +12,7 @@ from clinical_mdr_api.models.utils import GenericFilteringReturn from clinical_mdr_api.repositories._utils import FilterOperator from clinical_mdr_api.services._meta_repository import MetaRepository # type: ignore -from clinical_mdr_api.services._utils import calculate_diffs +from clinical_mdr_api.services._utils import calculate_diffs, ensure_transaction from clinical_mdr_api.utils import normalize_string from common.auth.user import user from common.exceptions import NotFoundException @@ -177,7 +177,8 @@ def create_new_version(self, codelist_uid: str) -> BaseModel: def edit_draft(self, codelist_uid: str, codelist_input: BaseModel) -> BaseModel: raise NotImplementedError() - def non_transactional_approve(self, codelist_uid: str) -> BaseModel: + @ensure_transaction(db) + def approve(self, codelist_uid: str) -> BaseModel: item = self._find_by_uid_or_raise_not_found( codelist_uid=codelist_uid, for_update=True ) @@ -185,10 +186,6 @@ def non_transactional_approve(self, codelist_uid: str) -> BaseModel: self.repository.save(item) return self._transform_aggregate_root_to_pydantic_model(item) - @db.transaction - def approve(self, codelist_uid: str) -> BaseModel: - return self.non_transactional_approve(codelist_uid) - def enforce_catalogue_library_package( self, catalogue_name: str | None, diff --git a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term.py b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term.py index e9856299..aafabd21 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term.py +++ b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term.py @@ -60,7 +60,8 @@ def __init__(self): def __del__(self): self._repos.close() - def non_transactional_create( + @ensure_transaction(db) + def create( self, term_input: CTTermCreateInput, start_date: datetime | None = None, @@ -193,17 +194,6 @@ def non_transactional_create( ct_term_name_ar, ct_term_attributes_ar, codelists_vo ) - @ensure_transaction(db) - def create( - self, - term_input: CTTermCreateInput, - start_date: datetime | None = None, - approve: bool = False, - ) -> CTTerm: - return self.non_transactional_create( - term_input, start_date=start_date, approve=approve - ) - def get_all_terms( self, codelist_uid: str | None, diff --git a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term_generic_service.py b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term_generic_service.py index d34fa0b4..7a7a7e73 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term_generic_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/controlled_terminologies/ct_term_generic_service.py @@ -12,7 +12,7 @@ from clinical_mdr_api.models.utils import GenericFilteringReturn from clinical_mdr_api.repositories._utils import FilterOperator from clinical_mdr_api.services._meta_repository import MetaRepository # type: ignore -from clinical_mdr_api.services._utils import calculate_diffs +from clinical_mdr_api.services._utils import calculate_diffs, ensure_transaction from clinical_mdr_api.utils import normalize_string from common.auth.user import user from common.exceptions import NotFoundException @@ -50,7 +50,8 @@ def repository(self) -> CTTermGenericRepository[_AggregateRootType]: assert self._repos is not None return self.repository_interface() - def non_transactional_get_all_ct_terms( + @ensure_transaction(db) + def get_all_ct_terms( self, codelist_uid: str | None = None, codelist_name: str | None = None, @@ -63,7 +64,7 @@ def non_transactional_get_all_ct_terms( filter_by: dict[str, dict[str, Any]] | None = None, filter_operator: FilterOperator = FilterOperator.AND, total_count: bool = False, - ) -> GenericFilteringReturn[BaseModel]: + ) -> GenericFilteringReturn[Any]: self.enforce_codelist_package_library( codelist_uid, codelist_name, library, package ) @@ -89,35 +90,6 @@ def non_transactional_get_all_ct_terms( return all_ct_terms - @db.transaction - def get_all_ct_terms( - self, - codelist_uid: str | None = None, - codelist_name: str | None = None, - library: str | None = None, - package: str | None = None, - in_codelist: bool = False, - sort_by: dict[str, bool] | None = None, - page_number: int = 1, - page_size: int = 0, - filter_by: dict[str, dict[str, Any]] | None = None, - filter_operator: FilterOperator = FilterOperator.AND, - total_count: bool = False, - ) -> GenericFilteringReturn[BaseModel]: - return self.non_transactional_get_all_ct_terms( - codelist_uid, - codelist_name, - library, - package, - in_codelist, - sort_by, - page_number, - page_size, - filter_by, - filter_operator, - total_count, - ) - def get_distinct_values_for_header( self, codelist_uid: str | None, @@ -227,16 +199,13 @@ def create_new_version(self, term_uid: str) -> BaseModel: def edit_draft(self, term_uid: str, term_input: BaseModel) -> BaseModel: raise NotImplementedError() - def non_transactional_approve(self, term_uid: str) -> BaseModel: + @ensure_transaction(db) + def approve(self, term_uid: str) -> BaseModel: item = self._find_by_uid_or_raise_not_found(term_uid=term_uid, for_update=True) item.approve(author_id=self.author_id) self.repository.save(item) return self._transform_aggregate_root_to_pydantic_model(item) - @db.transaction - def approve(self, term_uid: str) -> BaseModel: - return self.non_transactional_approve(term_uid) - @db.transaction def inactivate_final(self, term_uid: str) -> BaseModel: item = self._find_by_uid_or_raise_not_found(term_uid, for_update=True) diff --git a/clinical-mdr-api/clinical_mdr_api/services/data_suppliers/data_supplier.py b/clinical-mdr-api/clinical_mdr_api/services/data_suppliers/data_supplier.py index 0acb3369..92819459 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/data_suppliers/data_supplier.py +++ b/clinical-mdr-api/clinical_mdr_api/services/data_suppliers/data_supplier.py @@ -33,11 +33,19 @@ def _transform_aggregate_root_to_pydantic_model( def _create_aggregate_root( self, item_input: DataSupplierInput, library: LibraryVO ) -> DataSupplierAR: + if item_input.order is None: + order = self.repository.get_next_available_order( + item_input.supplier_type_uid + ) + else: + order = item_input.order + self.repository.bump_orders(item_input.supplier_type_uid, order) + return DataSupplierAR.from_input_values( author_id=self.author_id, data_supplier_vo=DataSupplierVO.from_repository_values( name=item_input.name, - order=item_input.order, + order=order, description=item_input.description, supplier_type_uid=item_input.supplier_type_uid, origin_source_uid=item_input.origin_source_uid, @@ -54,18 +62,72 @@ def _create_aggregate_root( def _edit_aggregate( self, item: DataSupplierAR, item_edit_input: DataSupplierEditInput ) -> DataSupplierAR: + # For partial updates, use existing values when input field is None + current_vo = item.data_supplier_vo + old_order = current_vo.order + old_type_uid = current_vo.supplier_type_uid + + # Determine new values, falling back to existing values when input is None + new_type_uid = ( + item_edit_input.supplier_type_uid + if item_edit_input.supplier_type_uid is not None + else old_type_uid + ) + new_order = item_edit_input.order + new_name = ( + item_edit_input.name + if item_edit_input.name is not None + else current_vo.name + ) + new_description = ( + item_edit_input.description + if item_edit_input.description is not None + else current_vo.description + ) + new_origin_source_uid = ( + item_edit_input.origin_source_uid + if item_edit_input.origin_source_uid is not None + else current_vo.origin_source_uid + ) + new_origin_type_uid = ( + item_edit_input.origin_type_uid + if item_edit_input.origin_type_uid is not None + else current_vo.origin_type_uid + ) + new_api_base_url = ( + item_edit_input.api_base_url + if item_edit_input.api_base_url is not None + else current_vo.api_base_url + ) + new_ui_base_url = ( + item_edit_input.ui_base_url + if item_edit_input.ui_base_url is not None + else current_vo.ui_base_url + ) + + if new_type_uid != old_type_uid: + self.repository.close_gap(old_type_uid, old_order) + if new_order is None: + new_order = self.repository.get_next_available_order(new_type_uid) + else: + self.repository.bump_orders(new_type_uid, new_order) + elif new_order is not None and new_order != old_order: + self.repository.reorder(new_type_uid, old_order, new_order) + else: + new_order = old_order + item.edit_draft( author_id=self.author_id, change_description=item_edit_input.change_description, data_supplier_vo=DataSupplierVO.from_repository_values( - name=item_edit_input.name, - order=item_edit_input.order, - description=item_edit_input.description, - supplier_type_uid=item_edit_input.supplier_type_uid, - origin_source_uid=item_edit_input.origin_source_uid, - origin_type_uid=item_edit_input.origin_type_uid, - api_base_url=item_edit_input.api_base_url, - ui_base_url=item_edit_input.ui_base_url, + name=new_name, + order=new_order, + description=new_description, + supplier_type_uid=new_type_uid, + origin_source_uid=new_origin_source_uid, + origin_type_uid=new_origin_type_uid, + api_base_url=new_api_base_url, + ui_base_url=new_ui_base_url, ), data_supplier_exists_by_name_callback=self._repos.data_supplier_repository.check_exists_by_name, ct_term_exists_by_uid_callback=self._repos.ct_term_name_repository.term_exists, diff --git a/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_mapper.py b/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_mapper.py index 1fdea6ed..8f5aa8cb 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_mapper.py +++ b/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_mapper.py @@ -41,6 +41,7 @@ ) from clinical_mdr_api.models.study_selections.study import Study as OSBStudy from clinical_mdr_api.services.ddf.usdm_utils import IdManager +from common.telemetry import trace_calls DDF_CT_PACKAGE_EFFECTIVE_DATE = "2023-12-15" DDF_STUDY_ARM_DATA_ORIGIN_TYPE_GENERATED_WITHIN_STUDY = "C188866" @@ -76,7 +77,7 @@ def get_ddf_timing_iso_duration_value(time_value: int, time_unit_name: str) -> s def extract_c_code_from_simple_term(term_uid: str) -> str | None: - regex_match = re.search("(^C[0-9]+)_", term_uid) + regex_match = re.search(r"(^C\d+)_?", term_uid) if regex_match: return regex_match.group(1) return None @@ -109,6 +110,7 @@ def _update_ddf_encounter_scheduled_at(encounters, schedule_timelines): class USDMMapper: + @trace_calls def __init__( self, get_osb_study_design_cells: Callable, @@ -140,11 +142,12 @@ def get_void_usdm_code(self): instanceType="Code", ) + @trace_calls(args=[1], kwargs=["concept_id"]) def get_ct_package_term_as_usdm_code(self, concept_id: str | None) -> USDMCode: if concept_id is None: return self.get_void_usdm_code() query = """ - MATCH (l:Library)-[:CONTAINS_TERM]->(cttr:CTTermRoot)-[:HAS_ATTRIBUTES_ROOT]->()-[:LATEST]->(cttav) + MATCH (l:Library)-[:CONTAINS_TERM]->(cttr:CTTermRoot)-[:HAS_NAME_ROOT]->()-[:LATEST]->(cttav) WHERE cttr.uid STARTS WITH $concept_id RETURN l, cttav """ @@ -157,17 +160,18 @@ def get_ct_package_term_as_usdm_code(self, concept_id: str | None) -> USDMCode: if len(result) == 0: return self.get_void_usdm_code() library = result[0][0] - ct_term_attributes_value = result[0][1] + ct_term_name_value = result[0][1] code = USDMCode( id=self._id_manager.get_id(USDMCode.__name__, concept_id), code=concept_id, codeSystem=library["name"], codeSystemVersion=str(date.today()), - decode=ct_term_attributes_value["preferred_term"], + decode=ct_term_name_value["name"], instanceType="Code", ) return code + @trace_calls(args=[1], kwargs=["time_unit_name"]) def get_ddf_study_population_duration_unit_from_name_as_code( self, time_unit_name: str ) -> USDMCode: @@ -236,6 +240,7 @@ def get_ddf_timing_relative_to_from(self): DDF_TIME_RELATIVE_TO_FROM_START_TO_START ) + @trace_calls def get_dictionary_term_as_usdm_code(self, term_uid: str) -> USDMCode: if term_uid is None: return self.get_void_usdm_code() @@ -264,6 +269,7 @@ def get_dictionary_term_as_usdm_code(self, term_uid: str) -> USDMCode: ) return code + @trace_calls def map(self, study: OSBStudy) -> dict[str, Any]: usdm_study = USDMStudy(name=self._get_study_name(study), instanceType="Study") usdm_study.id = uuid.uuid4() @@ -317,6 +323,7 @@ def map(self, study: OSBStudy) -> dict[str, Any]: return wrapped_study + @trace_calls def _get_intervention_model(self, study: OSBStudy): osb_current_metadata = getattr(study, "current_metadata", None) osb_study_intervention = getattr( @@ -333,6 +340,7 @@ def _get_intervention_model(self, study: OSBStudy): ) return self.get_void_usdm_code() + @trace_calls def _get_study_arms(self, study: OSBStudy): osb_study_arms = self._get_osb_study_arms(study.uid).items return [ @@ -354,6 +362,7 @@ def _get_study_arms(self, study: OSBStudy): for sa in osb_study_arms ] + @trace_calls def _get_study_cells(self, study: OSBStudy): osb_design_cells = self._get_osb_study_design_cells(study.uid) return [ @@ -375,12 +384,14 @@ def _get_study_cells(self, study: OSBStudy): and dc.study_element_uid is not None ] + @trace_calls def _get_study_description(self, study: OSBStudy): study_description = getattr( getattr(study, "current_metadata", None), "study_description", None ) return getattr(study_description, "study_title", None) + @trace_calls def _get_study_designs(self, study: OSBStudy): # Create DDF study design and set intervention model ddf_study_design = USDMStudyDesign( @@ -446,6 +457,7 @@ def _get_study_designs(self, study: OSBStudy): return [ddf_study_design] + @trace_calls def _get_study_activities(self, study: OSBStudy): osb_study_activities = self._get_osb_study_activities(study.uid).items return [ @@ -479,6 +491,7 @@ def _get_study_activities(self, study: OSBStudy): for a in osb_study_activities ] + @trace_calls def _get_study_elements(self, study: OSBStudy): osb_study_elements = self._get_osb_study_elements(study.uid).items ddf_study_elements = [] @@ -496,6 +509,7 @@ def _get_study_elements(self, study: OSBStudy): ddf_study_elements.append(ddf_se) return ddf_study_elements + @trace_calls def _get_study_epochs(self, study: OSBStudy): osb_study_epochs = self._get_osb_study_epochs(study.uid).items @@ -539,6 +553,7 @@ def _get_study_epochs(self, study: OSBStudy): ] return ddf_study_epochs + @trace_calls def _get_study_name(self, study: OSBStudy): osb_identification_metadata = getattr( getattr(study, "current_metadata", None), "identification_metadata", None @@ -546,6 +561,7 @@ def _get_study_name(self, study: OSBStudy): osb_study_id = getattr(osb_identification_metadata, "study_id", "") return osb_study_id + @trace_calls def _get_study_identifiers(self, study: OSBStudy): osb_identification_metadata = getattr( getattr(study, "current_metadata", None), "identification_metadata", None @@ -567,6 +583,7 @@ def _get_study_identifiers(self, study: OSBStudy): "national_clinical_trial_number", "national_medical_products_administration_nmpa_number", "universal_trial_number_utn", + "eu_pas_number", ] return [ @@ -580,6 +597,7 @@ def _get_study_identifiers(self, study: OSBStudy): if (osb_curr_id := getattr(osb_registry_identifiers, selected_id, None)) ] + @trace_calls def _get_study_indications(self, study: OSBStudy): osb_study_population = getattr( getattr(study, "current_metadata", None), "study_population", None @@ -613,6 +631,7 @@ def _get_study_indications(self, study: OSBStudy): ddf_study_indications.append(ddf_study_indication) return ddf_study_indications + @trace_calls def _get_study_interventions(self, study: OSBStudy): osb_study_intervention = study.current_metadata.study_intervention usdm_study_intervention_codes = [] @@ -678,12 +697,14 @@ def _get_study_interventions(self, study: OSBStudy): ) ] + @trace_calls def _get_study_label(self, study: OSBStudy): if study.current_metadata is not None: if study.current_metadata.study_description is not None: return study.current_metadata.study_description.study_short_title return None + @trace_calls def _get_study_objectives(self, study: OSBStudy): osb_study_endpoints = self._get_osb_study_endpoints( study.uid, no_brackets=True @@ -750,6 +771,7 @@ def _get_study_objectives(self, study: OSBStudy): if se.study_objective is not None ] + @trace_calls def _get_study_phase(self, study: OSBStudy): osb_study_design = getattr( getattr(study, "current_metadata", None), "high_level_study_design", None @@ -768,6 +790,7 @@ def _get_study_phase(self, study: OSBStudy): ) return study_phase + @trace_calls def _get_study_population(self, study: OSBStudy): osb_study_population = study.current_metadata.study_population planned_sex_usdm_code = None @@ -835,9 +858,9 @@ def _get_study_population(self, study: OSBStudy): isApproximate=False, instanceType="Range", ) - planned_enrollment_number_quantity = None + planned_enrollment_number = None if osb_study_population.number_of_expected_subjects is not None: - planned_enrollment_number_quantity = USDMQuantity( + planned_enrollment_number = USDMQuantity( id=self._id_manager.get_id(USDMQuantity.__name__), value=osb_study_population.number_of_expected_subjects, unit=USDMAliasCode( @@ -855,7 +878,7 @@ def _get_study_population(self, study: OSBStudy): id=self._id_manager.get_id(USDMStudyDesignPopulation.__name__), name="Study Design Population", plannedSex=[planned_sex_usdm_code], - plannedEnrollmentNumberQuantity=planned_enrollment_number_quantity, + plannedEnrollmentNumber=planned_enrollment_number, plannedAge=planned_age, includesHealthySubjects=( osb_study_population.healthy_subject_indicator @@ -891,6 +914,7 @@ def _get_study_population(self, study: OSBStudy): return population + @trace_calls def _get_study_definition_document(self, study: OSBStudy): ddf_study_definition_document = USDMStudyDefinitionDocument( id=self._id_manager.get_id(USDMStudyDefinitionDocument.__name__), @@ -931,6 +955,7 @@ def _get_study_definition_document(self, study: OSBStudy): ddf_study_definition_document.versions = [ddf_study_definition_document_version] return ddf_study_definition_document + @trace_calls def _get_study_schedule_timelines(self, study): osb_study_activity_schedules = self._get_osb_activity_schedules(study.uid) osb_study_visits = self._get_osb_study_visits(study.uid).items @@ -1059,6 +1084,7 @@ def _get_study_schedule_timelines(self, study): usdm_timeline.instances = timeline_instances return [usdm_timeline] + @trace_calls def _get_study_title(self, study: OSBStudy): osb_current_metadata = getattr(study, "current_metadata", None) study_title = getattr( @@ -1068,6 +1094,7 @@ def _get_study_title(self, study: OSBStudy): return study_title return "Study title not available" + @trace_calls def _get_study_type(self, study: OSBStudy): osb_study_design = getattr( getattr(study, "current_metadata", None), "high_level_study_design", None @@ -1079,6 +1106,7 @@ def _get_study_type(self, study: OSBStudy): ) return self.get_void_usdm_code() + @trace_calls def _get_study_version(self, study: OSBStudy): osb_current_metadata = getattr(study, "current_metadata", None) return str( @@ -1089,6 +1117,7 @@ def _get_study_version(self, study: OSBStudy): ) ) + @trace_calls def _get_study_encounters(self, study: OSBStudy): osb_study_visits = self._get_osb_study_visits(study.uid).items ordered_osb_study_visits = sorted( @@ -1134,6 +1163,7 @@ def _get_study_encounters(self, study: OSBStudy): return ddf_encounters + @trace_calls def _get_therapeutic_areas(self, study): osb_current_metadata = getattr(study, "current_metadata", None) osb_study_population = getattr(osb_current_metadata, "study_population", None) @@ -1146,6 +1176,7 @@ def _get_therapeutic_areas(self, study): ] return [] + @trace_calls def _get_trial_intent_types_codes(self, study): osb_current_metadata = getattr(study, "current_metadata", None) osb_study_intervention = getattr( @@ -1165,6 +1196,7 @@ def _get_trial_intent_types_codes(self, study): ] return [] + @trace_calls def _get_trial_type_codes(self, study: OSBStudy): return [ ( diff --git a/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_service.py b/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_service.py index 7508a203..78a06f0b 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_service.py +++ b/clinical-mdr-api/clinical_mdr_api/services/ddf/usdm_service.py @@ -23,11 +23,13 @@ ) from clinical_mdr_api.services.studies.study_epoch import StudyEpochService from clinical_mdr_api.services.studies.study_visit import StudyVisitService +from common.telemetry import trace_calls class USDMService: _usdm_mapper: USDMMapper + @trace_calls def __init__(self): self._usdm_mapper = USDMMapper( get_osb_study_design_cells=StudyDesignCellService().get_all_design_cells, @@ -40,6 +42,7 @@ def __init__(self): get_osb_activity_schedules=StudyActivityScheduleService().get_all_schedules, ) + @trace_calls(args=[1], kwargs=["uid"]) def get_by_uid(self, uid: str) -> dict[str, Any]: osb_study = StudyService().get_by_uid( uid, diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/complexity_score.py b/clinical-mdr-api/clinical_mdr_api/services/studies/complexity_score.py index b9dd08e0..4538c64f 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/complexity_score.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/complexity_score.py @@ -127,7 +127,7 @@ def get_soa(cls, study_uid: str, study_version_number: str | None) -> list[SoaRo if not visit: visit = SoaRow.Visit( uid=row["visit_uid"], - short_name=row["visit_short_name"], + short_name=str(row["visit_short_name"]), visit_contact_mode=( row["visit_contact_mode"] if "visit_contact_mode" in row diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study.py index c6758410..4eb0d649 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study.py @@ -7,6 +7,7 @@ from neomodel import NodeSet, db # type: ignore from opencensus.trace import execution_context +from clinical_mdr_api.domain_repositories.models.study_field import StudyArrayField from clinical_mdr_api.domain_repositories.study_definitions.study_definition_repository_impl import ( StudyDefinitionRepositoryImpl, ) @@ -64,6 +65,8 @@ StudySimple, StudySoaPreferences, StudySoaPreferencesInput, + StudySoaSplit, + StudySoaSplitInput, StudyStructureOverview, StudyStructureStatistics, StudySubpartAuditTrail, @@ -369,23 +372,13 @@ def get_study_structure_overview_header( filter_operator: FilterOperator = FilterOperator.AND, page_size: int = 10, ): - all_items = ( - self._repos.study_definition_repository.get_study_structure_overview() - ) - - parsed_items = self._group_study_structure_overview_by_data(all_items[0]) - - # Do filtering, sorting, pagination and count - header_values = service_level_generic_header_filtering( - items=parsed_items, + return self._repos.study_definition_repository.get_study_structure_overview_headers( field_name=field_name, search_string=search_string, filter_by=filter_by, filter_operator=filter_operator, page_size=page_size, ) - # Return values for field_name - return header_values @db.transaction def get_study_structure_statistics(self, uid: str) -> StudyStructureStatistics: @@ -682,6 +675,11 @@ def unlock(self, uid: str) -> Study: @db.transaction def release(self, uid: str, change_description: str | None) -> Study: + # avoid circular imports + from clinical_mdr_api.services.studies.study_flowchart import ( + StudyFlowchartService, + ) + try: study_definition = self._repos.study_definition_repository.find_by_uid( uid, for_update=True @@ -694,6 +692,14 @@ def release(self, uid: str, change_description: str | None) -> Study: study_definition.study_parent_part_uid, msg=f"Study Subparts cannot be released independently from its Study Parent Part with UID '{study_definition.study_parent_part_uid}'.", ) + + # save Protocol SoA snapshot + StudyFlowchartService().update_soa_snapshot( + study_uid=uid, + layout=SoALayout.PROTOCOL, + study_status=study_definition.study_status, + ) + study_definition.release( change_description=change_description, author_id=self.author_id ) @@ -723,7 +729,7 @@ def release(self, uid: str, change_description: str | None) -> Study: finally: self._close_all_repos() - @db.transaction + @ensure_transaction(db) def soft_delete(self, uid: str) -> None: try: study_definition = self._repos.study_definition_repository.find_by_uid( @@ -744,9 +750,7 @@ def soft_delete(self, uid: str) -> None: ) if is_subpart: - self.non_transactional_reorder_study_subparts( - study_definition.study_parent_part_uid - ) + self.reorder_study_subparts(study_definition.study_parent_part_uid) finally: self._close_all_repos() @@ -1051,13 +1055,7 @@ def _get_next_available_study_subpart_id( return available_letters[0] - @db.transaction - def create( - self, study_create_input: StudySubpartCreateInput | StudyCreateInput - ) -> Study: - return self.non_transactional_create(study_create_input) - - @db.transaction + @ensure_transaction(db) def clone_study( self, study_src_uid: str, @@ -1073,7 +1071,7 @@ def clone_study( else study_clone_input.description ), ) - study_created = self.non_transactional_create(study_create_input) + study_created = self.create(study_create_input) list_of_items_to_copy = [] if study_clone_input.copy_study_arm: @@ -1155,7 +1153,8 @@ def clone_study( ) return study_created - def non_transactional_create( + @ensure_transaction(db) + def create( self, study_create_input: StudySubpartCreateInput | StudyCreateInput ) -> Study: try: @@ -1216,6 +1215,8 @@ def non_transactional_create( eudamed_srn_number_null_value_code=None, investigational_device_exemption_ide_number=None, investigational_device_exemption_ide_number_null_value_code=None, + eu_pas_number=None, + eu_pas_number_null_value_code=None, ) # now we invoke our domain layer @@ -1561,6 +1562,9 @@ def _helper(array: list[Any] | None) -> list[Any]: trial_phase_null_value_code=get_term_uid_or_none( request_high_level_study_design.trial_phase_null_value_code ), + development_stage_code=get_term_uid_or_none( + request_high_level_study_design.development_stage_code + ), is_extension_trial=request_high_level_study_design.is_extension_trial, is_extension_trial_null_value_code=get_term_uid_or_none( request_high_level_study_design.is_extension_trial_null_value_code @@ -1686,6 +1690,10 @@ def _patch_prepare_new_id_metadata( investigational_device_exemption_ide_number_null_value_code=get_term_uid_or_none( request_id_metadata.registry_identifiers.investigational_device_exemption_ide_number_null_value_code ), + eu_pas_number=request_id_metadata.registry_identifiers.eu_pas_number, + eu_pas_number_null_value_code=get_term_uid_or_none( + request_id_metadata.registry_identifiers.eu_pas_number_null_value_code + ), ), ) @@ -1710,7 +1718,7 @@ def _patch_prepare_new_study_description_metadata( return new_id_metadata - @db.transaction + @ensure_transaction(db) def patch( self, uid: str, dry: bool, study_patch_request: StudyPatchRequestJsonModel ) -> Study: @@ -1812,6 +1820,15 @@ def patch( study_patch_request.study_parent_part_uid ) + if ( + previous_is_subpart + and study_patch_request.study_parent_part_uid is None + and _study_number is not None + ): + raise BusinessLogicException( + msg="When removing a Study Subpart from its Study Parent Part the Study Number must be set to null." + ) + new_id_metadata: StudyIdentificationMetadataVO | None = None new_high_level_study_design: HighLevelStudyDesignVO | None = None new_study_population: StudyPopulationVO | None = None @@ -1948,9 +1965,7 @@ def patch( previous_is_subpart and not study_patch_request.study_parent_part_uid ): - self.non_transactional_reorder_study_subparts( - previous_study_parent_part_uid - ) + self.reorder_study_subparts(previous_study_parent_part_uid) return self._models_study_from_study_definition_ar( study_definition_ar, @@ -2183,7 +2198,8 @@ def patch_study_preferred_time_unit( ) return StudyPreferredTimeUnit.model_validate(return_node) - def non_transactional_reorder_study_subparts( + @ensure_transaction(db) + def reorder_study_subparts( self, study_parent_part_uid: str, study_subpart_reordering_input: StudySubpartReorderingInput | None = None, @@ -2248,16 +2264,6 @@ def non_transactional_reorder_study_subparts( key=lambda x: x.uid, ) - @db.transaction - def reorder_study_subparts( - self, - study_parent_part_uid: str, - study_subpart_reordering_input: StudySubpartReorderingInput | None = None, - ): - return self.non_transactional_reorder_study_subparts( - study_parent_part_uid, study_subpart_reordering_input - ) - def _update_study_subpart_id(self, study: StudyDefinitionAR, new_subpart_id: str): if study.current_metadata.id_metadata is None: raise BusinessLogicException( @@ -2389,3 +2395,56 @@ def _study_fields_to_study_soa_preferences( preferences = {node.field_name: node.value for node in nodes} return StudySoaPreferences(study_uid=study_uid, **preferences) + + @ensure_transaction(db) + def get_study_soa_splits( + self, study_uid: str, study_value_version: str | None = None + ) -> list[StudySoaSplit]: + self.check_if_study_uid_and_version_exists( + study_uid=study_uid, study_value_version=study_value_version + ) + node = self._repos.study_definition_repository.get_soa_split_uids( + study_uid=study_uid, study_value_version=study_value_version + ) + if node is None: + return [] + return self._study_array_field_to_study_soa_splits(study_uid, node) + + @staticmethod + def _study_array_field_to_study_soa_splits( + study_uid: str, node: StudyArrayField + ) -> list[StudySoaSplit]: + return [StudySoaSplit(study_uid=study_uid, uid=uid) for uid in node.value] + + @ensure_transaction(db) + def add_study_soa_split( + self, + study_uid: str, + soa_split_input: StudySoaSplitInput, + ) -> list[StudySoaSplit]: + node = self._repos.study_definition_repository.add_soa_split_uid( + study_uid=study_uid, uid=soa_split_input.uid + ) + return self._study_array_field_to_study_soa_splits(study_uid, node) + + @ensure_transaction(db) + def remove_study_soa_split( + self, + study_uid: str, + uid: str, + ) -> list[StudySoaSplit]: + node = self._repos.study_definition_repository.remove_soa_split_uid( + study_uid=study_uid, uid=uid + ) + return ( + self._study_array_field_to_study_soa_splits(study_uid, node) if node else [] + ) + + @ensure_transaction(db) + def remove_study_soa_splits( + self, + study_uid: str, + ) -> None: + self._repos.study_definition_repository.remove_soa_splits( + study_uid=study_uid, + ) diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instance_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instance_selection.py index fc7b46f4..7b1948ff 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instance_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instance_selection.py @@ -265,6 +265,9 @@ def _create_value_object( generate_uid_callback=self.repository.generate_uid, is_reviewed=activity_instance_ar.concept_vo.is_required_for_activity or selection_create_input.is_reviewed, + study_data_supplier_uid=selection_create_input.study_data_supplier_uid, + origin_type_uid=selection_create_input.origin_type_uid, + origin_source_uid=selection_create_input.origin_source_uid, ) return new_selection @@ -379,6 +382,9 @@ def _patch_prepare_new_value_object( baseline_visit["uid"] for baseline_visit in current_object.study_activity_instance_baseline_visits ], + study_data_supplier_uid=current_object.study_data_supplier_uid, + origin_type_uid=current_object.origin_type_uid, + origin_source_uid=current_object.origin_source_uid, ) keep_old_version_date = None if request_object.keep_old_version is True: @@ -431,6 +437,9 @@ def _patch_prepare_new_value_object( activity_uid=current_object.activity_uid, activity_subgroup_uid=current_object.activity_subgroup_uid, activity_group_uid=current_object.activity_group_uid, + study_data_supplier_uid=request_object.study_data_supplier_uid, + origin_type_uid=request_object.origin_type_uid, + origin_source_uid=request_object.origin_source_uid, ) def get_specific_selection( @@ -513,6 +522,9 @@ def handle_batch_operations( is_reviewed=operation.content.is_reviewed, is_important=operation.content.is_important, baseline_visit_uids=operation.content.baseline_visit_uids, + study_data_supplier_uid=operation.content.study_data_supplier_uid, + origin_type_uid=operation.content.origin_type_uid, + origin_source_uid=operation.content.origin_source_uid, ), ) response_code = status.HTTP_200_OK @@ -525,6 +537,9 @@ def handle_batch_operations( is_reviewed=operation.content.is_reviewed, is_important=operation.content.is_important, baseline_visit_uids=operation.content.baseline_visit_uids, + study_data_supplier_uid=operation.content.study_data_supplier_uid, + origin_type_uid=operation.content.origin_type_uid, + origin_source_uid=operation.content.origin_source_uid, ), ) response_code = status.HTTP_201_CREATED @@ -543,6 +558,7 @@ def handle_batch_operations( content=BatchErrorResponse(message=str(error)), ) ) + raise error return results @ensure_transaction(db) diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instruction.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instruction.py index 640c8cc9..2901f99f 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instruction.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_instruction.py @@ -263,4 +263,5 @@ def handle_batch_operations( content=BatchErrorResponse(message=str(error)), ) ) + raise error return results diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_schedule.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_schedule.py index 7f9879ea..a89a2877 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_schedule.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_schedule.py @@ -271,4 +271,5 @@ def handle_batch_operations( content=BatchErrorResponse(message=str(error)), ) ) + raise error return results diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection.py index 5350dd23..d492df02 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection.py @@ -52,6 +52,7 @@ from clinical_mdr_api.models.study_selections.study_selection import ( DetailedSoAHistory, StudyActivityReplaceActivityInput, + StudyActivitySchedule, StudyActivityScheduleCreateInput, StudyActivitySyncLatestVersionInput, StudySelectionActivity, @@ -397,6 +398,186 @@ def update_dependent_objects( study_activity_selection=study_selection, ) + def _replicate_schedules_to_study_activity( + self, + study_uid: str, + target_study_activity_uid: str, + schedules_to_replicate: list[StudyActivitySchedule], + ) -> list[str]: + """ + Replicates StudyActivitySchedules to target StudyActivity. + Creates new schedule nodes for the target StudyActivity with the same study_visit_uid. + + Args: + study_uid: Study UID + target_study_activity_uid: Target StudyActivity UID to create schedules for + schedules_to_replicate: List of StudyActivitySchedule objects to replicate + + Returns: + List of newly created schedule UIDs + """ + schedule_service = StudyActivityScheduleService() + new_schedule_uids = [] + + for schedule in schedules_to_replicate: + # Create a new schedule for the target StudyActivity with the same visit + new_schedule = schedule_service.create( + study_uid=study_uid, + schedule_input=StudyActivityScheduleCreateInput( + study_activity_uid=target_study_activity_uid, + study_visit_uid=schedule.study_visit_uid, + ), + ) + new_schedule_uids.append(new_schedule.study_activity_schedule_uid) + + return new_schedule_uids + + @ensure_transaction(db) + def replace_study_activity_with_multiple_activities( + self, + study_uid: str, + study_activity_uid: str, + replacements: list[StudyActivityReplaceActivityInput], + ) -> list[StudySelectionActivity]: + """ + Replaces a StudyActivity with multiple activities. + - Multiple replacements are only allowed when replacing a StudyActivity placeholder (activity in 'Requested' library). + - First item in the list replaces the original StudyActivity + - Remaining items create new StudyActivities + - Schedules are preserved for the replaced StudyActivity and replicated to all new ones + - StudyActivityInstances are recreated for the replaced StudyActivity only (existing behavior) + - StudyActivityInstances are NOT created for newly created StudyActivities + """ + if not replacements: + raise ValidationException( + msg="At least one replacement must be provided in the replacements list." + ) + + # Get the original StudyActivity + _, current_vo = self._find_ar_to_patch( + study_uid=study_uid, study_selection_uid=study_activity_uid + ) + NotFoundException.raise_if_not( + current_vo, + "Study Activity", + study_activity_uid, + ) + + # Only allow multiple replacements if the original StudyActivity is a placeholder + # (i.e., activity is in the "Requested" library) + is_placeholder = ( + current_vo.activity_library_name == settings.requested_library_name + ) + if not is_placeholder and len(replacements) > 1: + raise BusinessLogicException( + msg=( + "Multiple activity replacements are only allowed when replacing " + "a StudyActivity placeholder (activity in 'Requested' library). " + "For regular StudyActivities, only one replacement is allowed." + ) + ) + + # Get existing schedules for the original StudyActivity + schedule_service = StudyActivityScheduleService() + existing_schedules = schedule_service.get_all_schedules_for_specific_activity( + study_uid=study_uid, study_activity_uid=study_activity_uid + ) + results: list[StudySelectionActivity] = [] + + # Process first item: Replace the original StudyActivity + first_replacement = replacements[0] + replaced_study_activity = self.patch_selection( + study_uid=study_uid, + study_selection_uid=study_activity_uid, + selection_update_input=first_replacement, + ) + results.append(replaced_study_activity) + + # Process remaining items: Create new StudyActivities + for replacement in replacements[1:]: + # Validate activity groupings using the same validation as patch_selection + activity_service = ActivityService() + activity_ar = activity_service.repository.find_by_uid_2( + replacement.activity_uid, for_update=True + ) + NotFoundException.raise_if_not( + activity_ar, "Activity", replacement.activity_uid + ) + + # Create a minimal current_object for validation (only activity_uid is used in error messages) + minimal_current_object = StudySelectionActivityVO.from_input_values( + study_uid=study_uid, + activity_uid=replacement.activity_uid, + activity_version=activity_ar.item_metadata.version, + study_soa_group_uid="", # Not used in validation + soa_group_term_uid="", # Not used in validation + author_id=self.author, + ) + + self._validate_new_activity_groupings( + request_object=replacement, + activity_ar=activity_ar, + current_object=minimal_current_object, + ) + + # Validate soa_group_term_uid is provided (required for create) + if not replacement.soa_group_term_uid: + raise ValidationException( + msg="soa_group_term_uid is required when creating a new StudyActivity." + ) + + # Convert replacement to create input + create_input = StudySelectionActivityCreateInput( + activity_uid=replacement.activity_uid, + activity_group_uid=replacement.activity_group_uid, + activity_subgroup_uid=replacement.activity_subgroup_uid, + soa_group_term_uid=replacement.soa_group_term_uid, + ) + + # Create new StudyActivity + # Note: make_selection will automatically create StudyActivityInstances for non-placeholder activities + new_study_activity = self.make_selection( + study_uid=study_uid, selection_create_input=create_input + ) + + # Remove StudyActivityInstances that were automatically created by make_selection + # We don't want instances for newly created StudyActivities, only for the replaced one + study_activity_instances = self._repos.study_activity_instance_repository.get_all_study_activity_instances_for_study_activity( + study_uid=study_uid, + study_activity_uid=new_study_activity.study_activity_uid, + ) + for study_activity_instance in study_activity_instances: + ( + study_activity_instance_ar, + _, + _, + ) = self._get_specific_activity_instance_selection_by_uids( + study_uid=study_uid, + study_selection_uid=study_activity_instance.uid, + for_update=True, + ) + study_activity_instance_ar.remove_object_selection( + study_activity_instance.uid + ) + self._repos.study_activity_instance_repository.save( + study_activity_instance_ar, self.author + ) + + # Replicate schedules to the new StudyActivity + # Use the schedules we fetched before replacement (they're still valid) + assert ( + new_study_activity.study_activity_uid is not None + ), "Newly created StudyActivity must have a study_activity_uid" + self._replicate_schedules_to_study_activity( + study_uid=study_uid, + target_study_activity_uid=new_study_activity.study_activity_uid, + schedules_to_replicate=existing_schedules, + ) + + results.append(new_study_activity) + + return results + def _create_value_object( self, study_uid: str, @@ -639,7 +820,7 @@ def _validate_activity_subgroup( and perform_subgroup_validation, msg=f"Activity Subgroup '{activity_subgroup_ar.concept_vo.name}' with UID '{activity_subgroup_uid}' has status {activity_subgroup_ar.item_metadata.status.value}." " Only Final subgroups can be added to a study." - " Contact StudyBuilder library responsible for updates.", + " Contact OpenStudyBuilder library responsible for updates.", ) return activity_subgroup_ar @@ -668,7 +849,7 @@ def _validate_activity_group( and perform_group_validation, msg=f"Activity Group '{activity_group_ar.concept_vo.name}' with UID '{activity_group_uid}' has status {activity_group_ar.item_metadata.status.value}." " Only Final groups can be added to a study." - " Contact StudyBuilder library responsible for updates.", + " Contact OpenStudyBuilder library responsible for updates.", ) return activity_group_ar @@ -1747,6 +1928,7 @@ def handle_batch_operations( content=BatchErrorResponse(message=str(error)), ) ) + raise error return results @ensure_transaction(db) diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection_base.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection_base.py index 3a881dc6..6c5c8ddd 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection_base.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_activity_selection_base.py @@ -419,6 +419,7 @@ def get_distinct_values_for_header( filter_operator: FilterOperator = FilterOperator.AND, page_size: int = 10, study_value_version: str | None = None, + include_placeholders: bool = False, ): all_items = self.get_all_selection( study_uid=study_uid, @@ -426,6 +427,7 @@ def get_distinct_values_for_header( filter_by=filter_by, filter_operator=filter_operator, for_field_name=field_name, + include_placeholders=include_placeholders, ) if isinstance(all_items, list): # We got a list of StudySelectionBaseAR, diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_arm_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_arm_selection.py index cf054fac..999d5373 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_arm_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_arm_selection.py @@ -37,6 +37,7 @@ from clinical_mdr_api.services.studies.study_selection_base import StudySelectionMixin from common import exceptions from common.auth.user import user +from common.telemetry import trace_calls class StudyArmSelectionService(StudySelectionMixin): @@ -181,6 +182,7 @@ def get_distinct_values_for_header( # Return values for field_name return header_values + @trace_calls def get_all_selection( self, study_uid: str, @@ -255,7 +257,7 @@ def delete_selection(self, study_uid: str, study_selection_uid: str): study_uid=study_uid, branch_arm_uid=i_branch_arm.uid, ): - design_cells_on_branch_arm = self._repos.study_design_cell_repository.get_design_cells_connected_to_branch_arm( + design_cells_on_branch_arm = self._repos.study_design_cell_repository.find_all_design_cells_by_study( study_uid=study_uid, study_branch_arm_uid=i_branch_arm.uid ) # if the study_branch_arm is the last StudyBranchArm of its StudyArm root @@ -269,10 +271,8 @@ def delete_selection(self, study_uid: str, study_selection_uid: str): # else the study_branch_arm is not last StudyBranchArm of its StudyArm root, so delete studyDesignCells else: - design_cells_to_delete_in_desc_order = sorted( - design_cells_on_branch_arm, - key=lambda dc: dc.order, - reverse=True, + design_cells_to_delete_in_desc_order = reversed( + design_cells_on_branch_arm ) for i_design_cell in design_cells_to_delete_in_desc_order: study_design_cell = ( @@ -321,16 +321,12 @@ def delete_selection(self, study_uid: str, study_selection_uid: str): if repos.study_arm_repository.arm_specific_has_connected_cell( study_uid=study_uid, arm_uid=study_selection_uid ): - design_cells_on_arm = self._repos.study_design_cell_repository.get_design_cells_connected_to_arm( + design_cells_on_arm = self._repos.study_design_cell_repository.find_all_design_cells_by_study( study_uid=study_uid, study_arm_uid=study_selection_uid ) if design_cells_on_arm or design_cells_to_delete_from_branch_arm: # get design cells to delete that are connected to arm - design_cells_to_delete_in_desc_order = sorted( - design_cells_on_arm, - key=lambda dc: dc.order, - reverse=True, - ) + design_cells_to_delete_in_desc_order = reversed(design_cells_on_arm) for i_design_cell in design_cells_to_delete_in_desc_order: study_design_cell = ( self._repos.study_design_cell_repository.find_by_uid( diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_branch_arm_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_branch_arm_selection.py index 8a6de245..dc79b4de 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_branch_arm_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_branch_arm_selection.py @@ -164,7 +164,7 @@ def delete_selection(self, study_uid: str, study_selection_uid: str): study_uid=study_uid, branch_arm_uid=study_selection_uid, ): - design_cells_on_branch_arm = self._repos.study_design_cell_repository.get_design_cells_connected_to_branch_arm( + design_cells_on_branch_arm = self._repos.study_design_cell_repository.find_all_design_cells_by_study( study_uid=study_uid, study_branch_arm_uid=study_selection_uid ) # if the study_branch_arm is the last StudyBranchArm of its StudyArm root @@ -350,7 +350,7 @@ def _cascade_creation( ) -> list[StudyDesignCellBatchOutput]: repos = self._repos design_cells_on_arm = ( - repos.study_design_cell_repository.get_design_cells_connected_to_arm( + repos.study_design_cell_repository.find_all_design_cells_by_study( study_uid=study_uid, study_arm_uid=study_arm_uid ) ) diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_design_cell.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_design_cell.py index 99bc7db6..67af2c64 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_design_cell.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_design_cell.py @@ -1,3 +1,4 @@ +from copy import deepcopy from datetime import datetime, timezone from fastapi import status @@ -75,21 +76,16 @@ def get_all_selection_within_arm( study_arm_uid: str, study_value_version: str | None = None, ) -> list[StudyDesignCell]: - sdc_nodes = ( - self._repos.study_design_cell_repository.get_design_cells_connected_to_arm( - study_uid, study_arm_uid, study_value_version=study_value_version + sdc_vos = ( + self._repos.study_design_cell_repository.find_all_design_cells_by_study( + study_uid=study_uid, + study_arm_uid=study_arm_uid, + study_value_version=study_value_version, ) ) return [ - StudyDesignCell.from_vo( - self._repos.study_design_cell_repository._from_repository_values( - study_uid=study_uid, - design_cell=sdc_node, - study_value_version=study_value_version, - ), - study_value_version=study_value_version, - ) - for sdc_node in sdc_nodes + StudyDesignCell.from_vo(sdc_vo, study_value_version=study_value_version) + for sdc_vo in sdc_vos ] @db.transaction @@ -99,21 +95,16 @@ def get_all_selection_within_branch_arm( study_branch_arm_uid: str, study_value_version: str | None = None, ) -> list[StudyDesignCell]: - sdc_nodes = self._repos.study_design_cell_repository.get_design_cells_connected_to_branch_arm( - study_uid, - study_branch_arm_uid, - study_value_version=study_value_version, - ) - return [ - StudyDesignCell.from_vo( - self._repos.study_design_cell_repository._from_repository_values( - study_uid=study_uid, - design_cell=sdc_node, - study_value_version=study_value_version, - ), + sdc_vos = ( + self._repos.study_design_cell_repository.find_all_design_cells_by_study( + study_uid=study_uid, + study_branch_arm_uid=study_branch_arm_uid, study_value_version=study_value_version, ) - for sdc_node in sdc_nodes + ) + return [ + StudyDesignCell.from_vo(sdc_vo, study_value_version=study_value_version) + for sdc_vo in sdc_vos ] @db.transaction @@ -123,19 +114,16 @@ def get_all_selection_within_epoch( study_epoch_uid: str, study_value_version: str | None = None, ) -> list[StudyDesignCell]: - sdc_nodes = self._repos.study_design_cell_repository.get_design_cells_connected_to_epoch( - study_uid, study_epoch_uid, study_value_version=study_value_version - ) - return [ - StudyDesignCell.from_vo( - self._repos.study_design_cell_repository._from_repository_values( - study_uid=study_uid, - design_cell=sdc_node, - study_value_version=study_value_version, - ), + sdc_vos = ( + self._repos.study_design_cell_repository.find_all_design_cells_by_study( + study_uid=study_uid, + study_epoch_uid=study_epoch_uid, study_value_version=study_value_version, ) - for sdc_node in sdc_nodes + ) + return [ + StudyDesignCell.from_vo(sdc_vo, study_value_version=study_value_version) + for sdc_vo in sdc_vos ] def get_specific_design_cell( @@ -230,14 +218,18 @@ def _edit_study_design_cell_vo( order=study_design_cell_edit_input.order, # type: ignore[arg-type] ) + @trace_calls @ensure_transaction(db) def patch( self, study_uid: str, design_cell_update_input: StudyDesignCellEditInput ) -> StudyDesignCell: - # study_design_cell: StudyDesignCellVO - study_design_cell = self._repos.study_design_cell_repository.find_by_uid( - study_uid=study_uid, uid=design_cell_update_input.study_design_cell_uid + study_design_cell: StudyDesignCellVO = ( + self._repos.study_design_cell_repository.find_by_uid( + study_uid=study_uid, uid=design_cell_update_input.study_design_cell_uid + ) ) + previous_study_design_cell = deepcopy(study_design_cell) + if design_cell_update_input.study_branch_arm_uid is not None: design_cell_update_input.study_arm_uid = None elif design_cell_update_input.study_arm_uid is not None: @@ -255,11 +247,15 @@ def patch( ) # updated_item: StudyDesignCellVO updated_item = self._repos.study_design_cell_repository.save( - study_design_cell, self.author, create=False + study_design_cell, + self.author, + create=False, + previous_vo=previous_study_design_cell, ) # return json response model return StudyDesignCell.from_vo(updated_item) + @trace_calls @ensure_transaction(db) def delete(self, study_uid: str, design_cell_uid: str): study_design_cell = self._repos.study_design_cell_repository.find_by_uid( @@ -287,10 +283,15 @@ def _transform_each_history_to_response_model( study_uid=study_uid, study_design_cell_uid=study_selection_history.study_selection_uid, study_arm_uid=study_selection_history.study_arm_uid, + study_arm_name=study_selection_history.study_arm_name, study_branch_arm_uid=study_selection_history.study_branch_arm_uid, + study_branch_arm_name=study_selection_history.study_branch_arm_name, study_epoch_uid=study_selection_history.study_epoch_uid, + study_epoch_name=study_selection_history.study_epoch_name, study_element_uid=study_selection_history.study_element_uid, + study_element_name=study_selection_history.study_element_name, transition_rule=study_selection_history.transition_rule, + author_username=study_selection_history.author_id, change_type=study_selection_history.change_type, modified=study_selection_history.start_date, order=study_selection_history.order, @@ -359,6 +360,7 @@ def get_specific_selection_audit_trail( finally: repos.close() + @trace_calls @ensure_transaction(db) def handle_batch_operations( self, study_uid: str, operations: list[StudyDesignCellBatchInput] @@ -400,4 +402,5 @@ def handle_batch_operations( content=BatchErrorResponse(message=str(error)), ) ) + raise error return results diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_element_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_element_selection.py index bee5ed80..3466ff2b 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_element_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_element_selection.py @@ -312,7 +312,7 @@ def delete_selection(self, study_uid: str, study_selection_uid: str): study_uid=study_uid, element_uid=study_selection_uid ): design_cells_on_element = ( - StudyDesignCellRepository.get_design_cells_connected_to_element( + StudyDesignCellRepository.find_all_design_cells_by_study( study_uid=study_uid, study_element_uid=study_selection_uid, ) diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_endpoint_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_endpoint_selection.py index 29e9b4f5..fb50bc88 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_endpoint_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_endpoint_selection.py @@ -292,8 +292,13 @@ def make_selection( finally: repos.close() - def non_transactional_make_selection_create_endpoint( - self, study_uid: str, selection_create_input: StudySelectionEndpointCreateInput + @ensure_transaction(db) + def make_selection_create_endpoint( + self, + study_uid: str, + selection_create_input: ( + StudySelectionEndpointCreateInput | StudySelectionEndpointInput + ), ) -> StudySelectionEndpoint: repos = self._repos try: @@ -427,14 +432,6 @@ def non_transactional_make_selection_create_endpoint( finally: repos.close() - @db.transaction - def make_selection_create_endpoint( - self, study_uid: str, selection_create_input: StudySelectionEndpointCreateInput - ) -> StudySelectionEndpoint: - return self.non_transactional_make_selection_create_endpoint( - study_uid, selection_create_input - ) - @ensure_transaction(db) def batch_select_endpoint_template( self, @@ -542,7 +539,7 @@ def batch_select_endpoint_template( if template_input.parameter_terms is not None else [] ) - new_selection = self.non_transactional_make_selection_create_endpoint( + new_selection = self.make_selection_create_endpoint( study_uid=study_uid, selection_create_input=StudySelectionEndpointCreateInput( endpoint_data=EndpointCreateInput( diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_epoch.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_epoch.py index 77fdad96..676739e5 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_epoch.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_epoch.py @@ -207,10 +207,8 @@ def _instantiate_epoch_items( # if epoch was previously calculated in preview call then we can just take it from the study_epoch_create_input # but we need to synchronize the orders because we don't synchronize them in a preview call if study_epoch_create_input.epoch is not None: - epoch_ar = self._repos.ct_term_name_repository.find_by_uid( - study_epoch_create_input.epoch - ) - epoch = SimpleCTTermNameWithConflictFlag.from_ct_term_ar(epoch_ar) + epoch = self.study_epoch_epochs_by_uid[study_epoch_create_input.epoch] + self._synchronize_epoch_orders( epochs_to_synchronize=epochs_in_subtype, all_epochs=all_epochs_in_study, @@ -346,8 +344,7 @@ def _get_or_create_epoch_in_specific_subtype( # the following section applies if the name of the epoch is the same as the name of the send epoch subtype # in such case we should reuse epoch subtype node and add it to the epoch hierarchy epoch_uid = subtype.term_uid - epoch_ar = self._repos.ct_term_name_repository.find_by_uid(epoch_uid) - epoch = SimpleCTTermNameWithConflictFlag.from_ct_term_ar(epoch_ar) + epoch = self.study_epoch_subtypes_by_uid[epoch_uid] try: # adding the epoch sub type term to the epoch codelist @@ -563,10 +560,7 @@ def _edit_study_epoch_vo( ] epoch_type = self._get_epoch_type_object(subtype=subtype.term_uid) if study_epoch_edit_input.epoch is not None: - epoch_ar = self._repos.ct_term_name_repository.find_by_uid( - study_epoch_edit_input.epoch - ) - epoch = SimpleCTTermNameWithConflictFlag.from_ct_term_ar(epoch_ar) + epoch = self.study_epoch_epochs_by_uid[study_epoch_edit_input.epoch] else: epoch = self._get_epoch_object( epochs_in_subtype=epochs_in_subtype, subtype=subtype # type: ignore[arg-type] @@ -740,6 +734,15 @@ def preview(self, study_uid: str, study_epoch_input: StudyEpochCreateInput): else: created_study_epoch.order = len(all_epochs) + 1 created_study_epoch.uid = "preview" + created_study_epoch.epoch = self.study_epoch_epochs_by_uid[ + created_study_epoch.epoch.term_uid + ] + created_study_epoch.subtype = self.study_epoch_subtypes_by_uid[ + created_study_epoch.subtype.term_uid + ] + created_study_epoch.epoch_type = self.study_epoch_types_by_uid[ + created_study_epoch.epoch_type.term_uid + ] return self._transform_all_to_response_model(created_study_epoch) @@ -758,7 +761,13 @@ def edit( study_visits = StudyVisitRepository.find_all_visits_by_study_uid(study_uid) timeline = TimelineAR(study_uid, _visits=study_visits) timeline.collect_visits_to_epochs(self.repo.find_all_epochs_by_study(study_uid)) - + study_epoch.epoch = self.study_epoch_epochs_by_uid[study_epoch.epoch.term_uid] + study_epoch.subtype = self.study_epoch_subtypes_by_uid[ + study_epoch.subtype.term_uid + ] + study_epoch.epoch_type = self.study_epoch_types_by_uid[ + study_epoch.epoch_type.term_uid + ] fill_missing_values_in_base_model_from_reference_base_model( base_model_with_missing_values=study_epoch_input, reference_base_model=self._transform_all_to_response_model(study_epoch), @@ -768,7 +777,13 @@ def edit( ) updated_item = self.repo.save(study_epoch) - + updated_item.epoch = self.study_epoch_epochs_by_uid[updated_item.epoch.term_uid] + updated_item.subtype = self.study_epoch_subtypes_by_uid[ + updated_item.subtype.term_uid + ] + updated_item.epoch_type = self.study_epoch_types_by_uid[ + updated_item.epoch_type.term_uid + ] return self._transform_all_to_response_model(updated_item) @db.transaction @@ -841,8 +856,10 @@ def delete(self, study_uid: str, study_epoch_uid: str): if self.repo.epoch_specific_has_connected_design_cell( study_uid=study_uid, epoch_uid=study_epoch_uid ): - design_cells_on_epoch = self._repos.study_design_cell_repository.get_design_cells_connected_to_epoch( - study_uid=study_uid, study_epoch_uid=study_epoch_uid + design_cells_on_epoch = ( + self._repos.study_design_cell_repository.find_all_design_cells_by_study( + study_uid=study_uid, study_epoch_uid=study_epoch_uid + ) ) # delete those StudyDesignCells attached to the StudyEpoch diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_flowchart.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_flowchart.py index e8b7723e..a58178d2 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_flowchart.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_flowchart.py @@ -67,16 +67,17 @@ TableCell, TableRow, TableWithFootnotes, - table_to_docx, table_to_html, table_to_xlsx, + tables_to_docx, + tables_to_html, ) from clinical_mdr_api.utils import enumerate_letters from common.auth.user import user from common.config import settings from common.exceptions import BusinessLogicException, NotFoundException from common.telemetry import trace_calls -from common.utils import VisitClass +from common.utils import VisitClass, insert_space_after_commas NUM_OPERATIONAL_CODE_COLS = 2 SOA_CHECK_MARK = "X" @@ -102,6 +103,7 @@ "group": "group", "subgroup": "subgroup", "activity": "activity", + "footnotes_in_last_table_slice": "Footnotes are found under the last SoA table.", }.get log = logging.getLogger(__name__) @@ -621,6 +623,102 @@ def get_flowchart_table( return table + @staticmethod + @trace_calls + def split_flowchart_table( + table: TableWithFootnotes, split_uids: Iterable[str] + ) -> list[TableWithFootnotes]: + """Splits flowchart table into multiple tables at columns referencing StudyVisit.uids in split_uids""" + + split_uids = set(split_uids) + + # Iterate through visit header row to find columns indexes to split at + split_indices = set() + visit_row_idx = max(0, table.num_header_rows - 3) + for col_idx, cell in enumerate( + table.rows[visit_row_idx].cells[table.num_header_cols :], + start=table.num_header_cols, + ): + if cell.refs: + for ref in cell.refs: + if ref.uid in split_uids: + split_indices.add(col_idx) + + table_slices: list[TableWithFootnotes] = [ + table.model_copy(update={"rows": [], "footnotes": None}) + for _ in range(len(split_indices) + 1) + ] + for row_idx, row in enumerate(table.rows): + spanning_cell, remaining_span = None, 0 + + # row prototype includes only header cells + row_proto = row.model_copy( + update={"cells": row.cells[: table.num_header_cols]} + ) + + # select the first slice and append new row based on prototype + iter_slices = iter(table_slices) + table_slice: TableWithFootnotes = next(iter_slices) + table_slice.rows.append( + slice_row := row_proto.model_copy( + update={"cells": row_proto.cells.copy()} + ) + ) + + for col_idx, cell in enumerate( + row.cells[table.num_header_cols :], start=table.num_header_cols + ): + # split at indexes referencing split_uids + if col_idx in split_indices: + # select next slice and create new row based on prototype + table_slice = next(iter_slices) + table_slice.rows.append( + slice_row := row_proto.model_copy( + update={"cells": row_proto.cells.copy()} + ) + ) + + # continue with a spanning cell from the previous slice + if ( + row_idx < table.num_header_rows + and cell.span == 0 + and remaining_span > 0 + ): + spanning_cell = cell = spanning_cell.model_copy( + update={"span": 0} + ) + + # handle cell spanning across slices (header rows only) + if row_idx < table.num_header_rows: + if cell.span > 1: + spanning_cell = cell = ( + cell.model_copy() + ) # shallow copy to avoid modifying original table when updating `spanning_cell` + remaining_span = cell.span - 1 + spanning_cell.span = 1 + + elif cell.span == 0 and remaining_span > 0: + spanning_cell.span += 1 + remaining_span -= 1 + + else: + spanning_cell, remaining_span = None, 0 + + # append cell to current slice row + slice_row.cells.append(cell) + + # add a comment footnote to each slice that footnotes are in the last slice + comment = str(_T("footnotes_in_last_table_slice")) + for table_slice in table_slices[:-1]: + table_slice.footnotes = { + "": SimpleFootnote(uid="", text_html=comment, text_plain=comment) + } + + # last slice inherits all footnotes + table_slices[-1].footnotes = table.footnotes + + return table_slices + @trace_calls def build_flowchart_table( self, @@ -816,8 +914,27 @@ def get_study_flowchart_html( if debug_uids: self.add_uid_debug(table) + # splitting + if layout == SoALayout.PROTOCOL: + tables = self.split_soa(study_uid, study_value_version, table) + else: + tables = [table] + # convert flowchart to HTML document - return table_to_html(table) + return tables_to_html(tables) + + def split_soa(self, study_uid: str, study_value_version: str | None, table) -> Any: + # Get StudyVisit.uids for slicing the SoA table + split_uids = set( + sp.uid + for sp in self._study_service.get_study_soa_splits( + study_uid=study_uid, study_value_version=study_value_version + ) + ) + + # Split SoA table into multiple tables at specified StudyVisit.uids + tables = self.split_flowchart_table(table, split_uids) + return tables @trace_calls def get_study_flowchart_docx( @@ -845,9 +962,15 @@ def get_study_flowchart_docx( if layout == SoALayout.PROTOCOL: self.add_protocol_section_column(table) + # splitting + if layout == SoALayout.PROTOCOL: + tables = self.split_soa(study_uid, study_value_version, table) + else: + tables = [table] + # convert flowchart to DOCX document applying styles - return table_to_docx( - table, + return tables_to_docx( + tables, styles=( OPERATIONAL_DOCX_STYLES if layout == SoALayout.OPERATIONAL @@ -1374,34 +1497,8 @@ def _get_header_rows( prev_visit_type_uid = None milestones_row.cells.append(TableCell()) - visit_timing = "" - - # Visit group - if len(group) > 1: - visit_name = visit.consecutive_visit_group - - if not ( - getattr(visit, visit_timing_prop) is None - or getattr(group[-1], visit_timing_prop) is None - ): - # If there is a comma it means that group was made in the LIST grouping way - if visit_name and "," in visit_name: - visit_timings = [ - f"{getattr(visit, visit_timing_prop):d}" - for visit in group - ] - visit_timing = ",".join(visit_timings) - else: - visit_timing = f"{getattr(visit, visit_timing_prop):d}-{getattr(group[-1], visit_timing_prop):d}" - - # Single Visit - else: - visit_name = visit.visit_short_name - if ( - getattr(visit, visit_timing_prop) is not None - and visit.visit_class != VisitClass.SPECIAL_VISIT.name - ): - visit_timing = f"{getattr(visit, visit_timing_prop):d}" + visit_name = cls._get_visit_name(group) + visit_timing = cls._get_visit_timing(group, visit_timing_prop) # Visit name cell visits_row.cells.append( @@ -1443,35 +1540,58 @@ def _get_visit_timing_property( return "study_week_number" @staticmethod - def _get_visit_name(visit: StudyVisit, num_visits_in_group: int = 0) -> str: - if num_visits_in_group: - return ( - visit.consecutive_visit_group - if num_visits_in_group > 1 - else visit.visit_short_name + def _get_visit_name(visits_in_group: Sequence[StudyVisit]) -> str: + visit: StudyVisit = visits_in_group[0] + visit_name = visit.consecutive_visit_group or visit.visit_short_name + + if len(visits_in_group) > 1 and "," in visit_name: + # insert line-breaks after certain commas when the cell text gets too long + visit_name = insert_space_after_commas( + visit_name, settings.soa_insert_space_after_commas_length, space="\n" ) - return visit.consecutive_visit_group or visit.visit_short_name + + return visit_name @staticmethod def _get_visit_timing(visits: list[StudyVisit], visit_timing_property: str) -> str: - visit = visits[0] - - # Visit group - if len(visits) > 1: - if not ( - getattr(visit, visit_timing_property) is None - or getattr(visits[-1], visit_timing_property) is None - ): - return f"{getattr(visit, visit_timing_property):d}-{getattr(visits[-1], visit_timing_property):d}" + visit: StudyVisit = visits[0] + num_visits_in_group = len(visits) # Single Visit - else: + if num_visits_in_group == 1: if ( getattr(visit, visit_timing_property) is not None and visit.visit_class != VisitClass.SPECIAL_VISIT.name ): return f"{getattr(visit, visit_timing_property):d}" + # Visit Group + if not ( + getattr(visit, visit_timing_property) is None + or getattr(visits[-1], visit_timing_property) is None + ): + visit_name = ( + num_visits_in_group > 1 + and visit.consecutive_visit_group + or visit.visit_short_name + ) + + # If there is a comma it means that group was made in the LIST grouping way + if visit_name and "," in visit_name: + visit_timings = [ + f"{getattr(visit, visit_timing_property):d}" for visit in visits + ] + visit_timing = ",".join(visit_timings) + # insert line-breaks after certain commas when the cell text gets too long + visit_timing = insert_space_after_commas( + visit_timing, + settings.soa_insert_space_after_commas_length, + space="\n", + ) + return visit_timing + + return f"{getattr(visit, visit_timing_property):d}-{getattr(visits[-1], visit_timing_property):d}" + return "" @staticmethod @@ -2690,7 +2810,7 @@ def load_soa_snapshot( ) visit_row.cells[col_idx] = TableCell( - self._get_visit_name(visit, num_visits_in_group=len(visits_in_group)), + self._get_visit_name(visits_in_group), style="header2", refs=[ Ref( diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_objective_selection.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_objective_selection.py index 7ccd4eec..d812c95f 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_objective_selection.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_objective_selection.py @@ -308,8 +308,13 @@ def make_selection( finally: repos.close() - def non_transactional_make_selection_create_objective( - self, study_uid: str, selection_create_input: StudySelectionObjectiveCreateInput + @ensure_transaction(db) + def make_selection_create_objective( + self, + study_uid: str, + selection_create_input: ( + StudySelectionObjectiveCreateInput | StudySelectionObjectiveInput + ), ) -> StudySelectionObjective: repos = self._repos try: @@ -407,14 +412,6 @@ def non_transactional_make_selection_create_objective( finally: repos.close() - @db.transaction - def make_selection_create_objective( - self, study_uid: str, selection_create_input: StudySelectionObjectiveCreateInput - ) -> StudySelectionObjective: - return self.non_transactional_make_selection_create_objective( - study_uid, selection_create_input - ) - @ensure_transaction(db) def batch_select_objective_template( self, @@ -515,7 +512,7 @@ def batch_select_objective_template( if template_input.parameter_terms is not None else [] ) - new_selection = self.non_transactional_make_selection_create_objective( + new_selection = self.make_selection_create_objective( study_uid=study_uid, selection_create_input=StudySelectionObjectiveCreateInput( objective_data=ObjectiveCreateInput( diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_selection_base.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_selection_base.py index e6371822..e8d0a699 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_selection_base.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_selection_base.py @@ -96,6 +96,7 @@ def update_ctterm_maps(self, terms_at_specific_datetime: datetime | None = None) term_uids=list(ctterm_uids), at_specific_date=terms_at_specific_datetime, return_simple_object=True, + include_retired_versions=True, ) self.study_epoch_types_by_uid = { diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_soa_footnote.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_soa_footnote.py index fe720f49..08a1f535 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_soa_footnote.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_soa_footnote.py @@ -31,6 +31,7 @@ from clinical_mdr_api.services._utils import ( calculate_diffs, calculate_diffs_history, + ensure_transaction, extract_filtering_values, service_level_generic_filtering, service_level_generic_header_filtering, @@ -441,7 +442,8 @@ def validate( msg=f"The SoaFootnote already exists for the Footnote with Name '{existing_footnote}'.", ) - def non_transactional_edit( + @ensure_transaction(db) + def edit( self, study_uid: str, study_soa_footnote_uid: str, @@ -546,24 +548,7 @@ def non_transactional_edit( return self._transform_vo_to_pydantic_model(new_footnote_vo) - @db.transaction - def edit( - self, - study_uid: str, - study_soa_footnote_uid: str, - footnote_edit_input: StudySoAFootnoteEditInput, - accept_version: bool = False, - sync_latest_version: bool = False, - ): - return self.non_transactional_edit( - study_uid=study_uid, - study_soa_footnote_uid=study_soa_footnote_uid, - footnote_edit_input=footnote_edit_input, - accept_version=accept_version, - sync_latest_version=sync_latest_version, - ) - - @db.transaction + @ensure_transaction(db) def batch_edit( self, study_uid: str, @@ -572,7 +557,7 @@ def batch_edit( results = [] for edit_payload in edit_payloads: try: - item = self.non_transactional_edit( + item = self.edit( study_uid=study_uid, study_soa_footnote_uid=edit_payload.study_soa_footnote_uid, footnote_edit_input=edit_payload, diff --git a/clinical-mdr-api/clinical_mdr_api/services/studies/study_visit.py b/clinical-mdr-api/clinical_mdr_api/services/studies/study_visit.py index 06354ee6..42d8488d 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/studies/study_visit.py +++ b/clinical-mdr-api/clinical_mdr_api/services/studies/study_visit.py @@ -80,8 +80,11 @@ StudyVisitBase, StudyVisitCreateInput, StudyVisitEditInput, - StudyVisitVersion, ) +from clinical_mdr_api.models.study_selections.study_visit import ( + StudyVisitGroup as StudyVisitGroupModel, +) +from clinical_mdr_api.models.study_selections.study_visit import StudyVisitVersion from clinical_mdr_api.models.utils import GenericFilteringReturn from clinical_mdr_api.repositories._utils import FilterOperator from clinical_mdr_api.services._meta_repository import MetaRepository @@ -158,7 +161,7 @@ def _transform_all_to_response_history_model( ) -> StudyVisitBase: # For audit trail return model we shouldn't derive properties based on their position in the timeline as we don't know how the visit timeline looked for past visit versions # Due to this we have to take the values that are derived based on timeline directly from database representation - self.amend_study_visit_vo(visit) + self.update_ct_term_properties_of_study_visit(visit) study_visit: StudyVisitBase = StudyVisitBase.transform_to_response_model( visit, derive_props_based_on_timeline=False ) @@ -370,7 +373,6 @@ def get_all_references(self, study_uid: str) -> list[StudyVisit]: ] for visit in visits: if visit.visit_type.sponsor_preferred_name in sponsor_names: - self.amend_study_visit_vo(visit) result.append(StudyVisit.transform_to_response_model(visit)) return result @@ -664,6 +666,7 @@ def _validate_visit( msg="Special Visit has to time reference to some other visit.", ) + ordered_visits = timeline.ordered_study_visits if create: timeline.add_visit(visit_vo) ordered_visits = timeline.ordered_study_visits @@ -681,50 +684,58 @@ def _validate_visit( ) visit_vo.week_in_study.value = visit.derive_week_in_study_number() - for index, visit in enumerate(ordered_visits): - if visit_vo.visit_class != VisitClass.SPECIAL_VISIT: - if ( - visit.get_absolute_duration() - == visit_vo.get_absolute_duration() - and visit.uid != visit_vo.uid - ): - raise exceptions.AlreadyExistsException( - msg=f"There already exists a visit with timing set to {visit.timepoint.visit_value}" - ) - if index + 2 < len(ordered_visits): - # we check whether the created visit is not from the epoch that sits - # out of the epoch schedule - ValidationException.raise_if( - visit.epoch.order - > ordered_visits[index + 2].epoch.order - and ordered_visits[index + 2].visit_class - not in ( - VisitClass.NON_VISIT, - VisitClass.UNSCHEDULED_VISIT, - ), - msg=f"Visit with Study Day '{visit.study_day_number}' from " - f"Epoch with order '{visit.epoch.order}' '{visit.epoch.epoch.sponsor_preferred_name}' is out of order with " - f"Visit with Study Day '{ordered_visits[index + 2].study_day_number}' from Epoch with order " - f"'{ordered_visits[index + 2].epoch.order}' '{ordered_visits[index + 2].epoch.epoch.sponsor_preferred_name}'", - ) - self._validate_derived_properties( - visit_vo=visit_vo, ordered_visits=ordered_visits + self._validate_derived_properties( + visit_vo=visit_vo, ordered_visits=ordered_visits + ) + # Perform validation check if visit is not being placed in the middle of Visit group, grouped in the range way + for index, visit in enumerate(ordered_visits): + if visit.uid == visit_vo.uid: + if index > 0: + previous_visit = ordered_visits[index - 1] + else: + previous_visit = None + if index < len(ordered_visits) - 1: + next_visit = ordered_visits[index + 1] + else: + next_visit = None + break + + if ( + previous_visit + and next_visit + and previous_visit.study_visit_group + and next_visit.study_visit_group + and previous_visit.study_visit_group.uid + == next_visit.study_visit_group.uid + ): + group = previous_visit.study_visit_group + if group.group_format == VisitGroupFormat.RANGE.value: + raise ValidationException( + msg=f"The visit can't be placed in the middle of Visit Group '{group.group_name}' which is grouped in the Range way. Uncollapse the '{group.group_name}' Visit Group first." + ) + + # Perform check for timing uniqueness excluding Special Visits. + # There can exist 2 visits with the same timing unless timing is 0, then there can exist only one such visit + if visit_vo.visit_class != VisitClass.SPECIAL_VISIT: + all_visit_timings = [ + visit.get_absolute_duration() + for visit in ordered_visits + if visit.uid != visit_vo.uid + and visit.visit_class != VisitClass.SPECIAL_VISIT + ] + existing_visits_with_same_timing = all_visit_timings.count( + visit_vo.get_absolute_duration() ) - else: - ordered_visits = timeline.ordered_study_visits - for index, visit in enumerate(ordered_visits): - if ( - VisitClass.SPECIAL_VISIT - not in (visit_vo.visit_class, visit.visit_class) - and visit.get_absolute_duration() - == visit_vo.get_absolute_duration() - and visit.uid != visit_vo.uid - ): - raise exceptions.AlreadyExistsException( - msg=f"There already exists a visit with timing set to {visit.timepoint.visit_value}" - ) - self._validate_derived_properties( - visit_vo=visit_vo, ordered_visits=ordered_visits + exceptions.AlreadyExistsException.raise_if( + ( + existing_visits_with_same_timing > 0 + and visit_vo.get_absolute_duration() == 0 + ) + or ( + existing_visits_with_same_timing > 1 + and visit_vo.get_absolute_duration() != 0 + ), + msg=f"There already exists a visit with timing set to {visit_vo.timepoint.visit_value}", ) if not preview: @@ -1176,10 +1187,14 @@ def create(self, study_uid: str, study_visit_input: StudyVisitCreateInput): ordered_visits=ordered_visits, start_index_to_synchronize=int(added_item.visit_number), ) - self.amend_study_visit_vo(added_item) return StudyVisit.transform_to_response_model(added_item) - def amend_study_visit_vo(self, visit: StudyVisitVO) -> StudyVisitVO: + def update_ct_term_properties_of_study_visit( + self, visit: StudyVisitVO + ) -> StudyVisitVO: + """ + Amends CTTerm related properties of study visit with value stored in dictionary for each CTTerm. + """ timepoint = visit.timepoint if timepoint: visit_timereference = self.study_visit_time_references_by_uid.get( @@ -1236,7 +1251,6 @@ def preview(self, study_uid: str, study_visit_input: StudyVisitCreateInput): study_visit.uid = "preview" timeline.add_visit(study_visit) self.assign_props_derived_from_visit_absolute_timing(study_visit_vo=study_visit) - self.amend_study_visit_vo(study_visit) return StudyVisit.transform_to_response_model(study_visit) @db.transaction @@ -1306,7 +1320,6 @@ def edit( self.repo.save(new_study_visit) - self.amend_study_visit_vo(new_study_visit) return StudyVisit.transform_to_response_model(new_study_visit) @ensure_transaction(db) @@ -1376,15 +1389,23 @@ def delete(self, study_uid: str, study_visit_uid: str): ) @db.transaction - def get_consecutive_groups(self, study_uid: str): + def get_consecutive_groups(self, study_uid: str) -> list[StudyVisitGroupModel]: all_visits = self.repo.find_all_visits_by_study_uid(study_uid) - groups = [ - visit.study_visit_group.group_name - for visit in all_visits - if visit.study_visit_group is not None - ] - groups_set = set(groups) - return groups_set + known_groups = set() + study_visit_groups: list[StudyVisitGroupModel] = [] + + for visit in all_visits: + if (study_visit_group := visit.study_visit_group) is not None: + if study_visit_group.uid not in known_groups: + known_groups.add(study_visit_group.uid) + study_visit_groups.append( + StudyVisitGroupModel( + uid=study_visit_group.uid, + group_name=study_visit_group.group_name, + ) + ) + + return study_visit_groups @trace_calls @db.transaction @@ -1471,7 +1492,7 @@ def audit_trail_all_visits( @db.transaction def remove_visit_consecutive_group( - self, study_uid: str, consecutive_visit_group: str + self, study_uid: str, consecutive_visit_group_uid: str ): study_visits = self.repo.find_all_visits_by_study_uid(study_uid=study_uid) timeline = TimelineAR(study_uid=study_uid, _visits=study_visits) @@ -1479,7 +1500,7 @@ def remove_visit_consecutive_group( for visit in ordered_visits: if ( visit.study_visit_group - and visit.study_visit_group.group_name == consecutive_visit_group + and visit.study_visit_group.uid == consecutive_visit_group_uid ): visit.study_visit_group = None self.repo.save(visit) @@ -1514,6 +1535,8 @@ def assign_visit_consecutive_group( study_uid=study_uid, visits_to_be_assigned=visits_to_be_assigned, overwrite_visit_from_template=overwrite_visit_from_template, + group_format=group_format, + validate_only=validate_only, ) if not validate_only: # Create StudyVisit group node @@ -1549,7 +1572,7 @@ def assign_visit_consecutive_group( return [ StudyVisit.transform_to_response_model(visit) - for visit in map(self.amend_study_visit_vo, visits_to_be_assigned) + for visit in visits_to_be_assigned ] def _validate_consecutive_visit_group_assignment( @@ -1557,9 +1580,14 @@ def _validate_consecutive_visit_group_assignment( study_uid: str, visits_to_be_assigned: list[StudyVisitVO], overwrite_visit_from_template: str | None = None, + group_format: VisitGroupFormat = VisitGroupFormat.RANGE, + validate_only: bool = False, ): - visit_epochs = list( - {visit.epoch_connector.epoch.term_uid for visit in visits_to_be_assigned} + visit_epochs = sorted( + { + visit.epoch_connector.epoch.sponsor_preferred_name + for visit in visits_to_be_assigned + } ) BusinessLogicException.raise_if( len(visit_epochs) > 1, @@ -1587,7 +1615,9 @@ def _validate_consecutive_visit_group_assignment( order = visits_to_be_assigned[0].visit_order for visit_to_assign in visits_to_be_assigned: BusinessLogicException.raise_if( - visit_to_assign.visit_order != order, + visit_to_assign.visit_order != order + and group_format == VisitGroupFormat.RANGE + and not validate_only, msg="To create visits group please select consecutive visits.", ) order += 1 @@ -1620,9 +1650,9 @@ def _validate_consecutive_visit_group_assignment( other_visit_study_activities ) # if not are_visits_the_same: - BusinessLogicException.raise_if_not( + BusinessLogicException.raise_if( are_visits_the_same, - msg=f"Visit '{reference_visit.visit_short_name}' is not the same as '{visit.visit_short_name}'", + msg=f"Visit '{reference_visit.visit_short_name}' and '{visit.visit_short_name}' have the following properties different {are_visits_the_same}", ) if not are_schedules_the_same: VisitsAreNotEqualException.raise_if_not( diff --git a/clinical-mdr-api/clinical_mdr_api/services/utils/docx_builder.py b/clinical-mdr-api/clinical_mdr_api/services/utils/docx_builder.py index cf9b752f..e2951aee 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/utils/docx_builder.py +++ b/clinical-mdr-api/clinical_mdr_api/services/utils/docx_builder.py @@ -221,3 +221,6 @@ def replace_content( style = self.styles.get(style, (None,))[0] container.add_paragraph(text, style=style) self.delete_paragraph(container.paragraphs[0]) + + def add_page_break(self): + self.document.add_page_break() diff --git a/clinical-mdr-api/clinical_mdr_api/services/utils/table_f.py b/clinical-mdr-api/clinical_mdr_api/services/utils/table_f.py index e2f2ff89..29e11b39 100644 --- a/clinical-mdr-api/clinical_mdr_api/services/utils/table_f.py +++ b/clinical-mdr-api/clinical_mdr_api/services/utils/table_f.py @@ -1,6 +1,6 @@ import os from collections import defaultdict -from typing import Annotated, Any, Mapping +from typing import Annotated, Any, Iterable, Mapping import yattag from docx.enum.text import WD_ALIGN_PARAGRAPH @@ -208,111 +208,181 @@ def table_to_docx( styles: Mapping[str, tuple[str, Any]] | None = None, template: str | None = None, ) -> DocxBuilder: - # assume horizontal table dimension from number of cells in first row - num_cols = sum((c.span for c in table.rows[0].cells)) - - # parses an empty template DOCX file into a helper class - docx = DocxBuilder( - styles=styles, landscape=True, margins=[0.5, 0.5, 0.5, 0.5], template=template + return tables_to_docx( + [table], + styles, + template, ) - # adds a table to the document - x_table = docx.create_table( - num_rows=sum(1 for row in table.rows if not row.hide), - num_columns=num_cols, - ) - # set width of first column - x_table.columns[0].width = Inches(4) +@trace_calls() +def tables_to_docx( + tables: Iterable[TableWithFootnotes], + styles: Mapping[str, tuple[str, Any]] | None = None, + template: str | None = None, +) -> DocxBuilder: + + def add_table(table: TableWithFootnotes): + # assume horizontal table dimension from number of cells in first row + num_cols = sum((c.span for c in table.rows[0].cells)) + + # adds a table to the document + x_table = docx.create_table( + num_rows=sum(1 for row in table.rows if not row.hide), + num_columns=num_cols, + ) + + # set width of first column + x_table.columns[0].width = Inches(4) + + # cache porperty for performance issues, see github.com/python-openxml/python-docx/issues/174 + x_cells = x_table._cells + x_rows = x_table.rows + + for r, t_row in enumerate((row for row in table.rows if not row.hide)): + num_merge, merge_to = 0, None - # cache porperty for performance issues, see github.com/python-openxml/python-docx/issues/174 - x_cells = x_table._cells - x_rows = x_table.rows + # set header row to repeat on each page + if r < table.num_header_rows: + docx.repeat_table_header(x_rows[r]) - for r, t_row in enumerate((row for row in table.rows if not row.hide)): - num_merge, merge_to = 0, None + for c, t_cell in enumerate(t_row.cells): + x_cell = x_cells[r * num_cols + c] - # set header row to repeat on each page - if r < table.num_header_rows: - docx.repeat_table_header(x_rows[r]) + # merge with previous spanning cell + if num_merge: + merge_to.merge(x_cell) + num_merge -= 1 + continue - for c, t_cell in enumerate(t_row.cells): - x_cell = x_cells[r * num_cols + c] + # skip invisible cells (should not get here if spans are coherent) + if t_cell.span < 1: + continue - # merge with previous spanning cell - if num_merge: - merge_to.merge(x_cell) - num_merge -= 1 - continue + # when cell span > 1 merge the following N cells into this one + num_merge = t_cell.span - 1 + merge_to = x_cell - # skip invisible cells (should not get here if spans are coherent) - if t_cell.span < 1: - continue + # all docx cells host one or more paragraph for contents + x_para = x_cell.paragraphs[0] - # when cell span > 1 merge the following N cells into this one - num_merge = t_cell.span - 1 - merge_to = x_cell + # set cell text in the paragraph + if t_cell.text: + x_para.text = t_cell.text - # all docx cells host one or more paragraph for contents - x_para = x_cell.paragraphs[0] + # resolve style name and apply to paragraph + style_name = styles.get(t_cell.style, [None])[0] if styles else None # type: ignore[arg-type] + if style_name: + x_para.style = style_name - # set cell text in the paragraph - if t_cell.text: - x_para.text = t_cell.text + # center non-header columns + if c >= table.num_header_cols: + x_para.alignment = WD_ALIGN_PARAGRAPH.CENTER - # resolve style name and apply to paragraph - style_name = styles.get(t_cell.style, [None])[0] if styles else None # type: ignore[arg-type] - if style_name: - x_para.style = style_name + # set vertical text direction + if t_cell.vertical: + docx.set_vertical_cell_direction(x_cell, "btLr") - # center non-header columns - if c >= table.num_header_cols: - x_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + # add footnote symbols to a run within the paragraph + if t_cell.footnotes: + run = x_para.add_run("\u00A0".join(t_cell.footnotes)) + run.font.bold = True + run.font.superscript = True - # set vertical text direction - if t_cell.vertical: - docx.set_vertical_cell_direction(x_cell, "btLr") + # add footnotes + if table.footnotes: + style_name = styles.get("footnote", [None])[0] if styles else None - # add footnote symbols to a run within the paragraph - if t_cell.footnotes: - run = x_para.add_run("\u00A0".join(t_cell.footnotes)) + for symbol, footnote in table.footnotes.items(): + # each footnote is a new paragraph at the end of the document + x_para = docx.document.add_paragraph(style=style_name) + + # footnote symbols into a run (like ) with superscript + run = x_para.add_run(symbol) run.font.bold = True run.font.superscript = True - # add footnotes - if table.footnotes: - style_name = styles.get("footnote", [None])[0] if styles else None + # footnote text with glue and spacing into a distinct run + x_para.add_run(footnote.text_plain) - for symbol, footnote in table.footnotes.items(): - # each footnote is a new paragraph at the end of the document - x_para = docx.document.add_paragraph(style=style_name) + # parses an empty template DOCX file into a helper class + docx = DocxBuilder( + styles=styles, landscape=True, margins=[0.5, 0.5, 0.5, 0.5], template=template + ) - # footnote symbols into a run (like ) with superscript - run = x_para.add_run(symbol) - run.font.bold = True - run.font.superscript = True + for i, table in enumerate(tables): + if i: + # add a page break between tables + docx.add_page_break() - # footnote text with glue and spacing into a distinct run - x_para.add_run(footnote.text_plain) + add_table(table) return docx @trace_calls def table_to_html(table: TableWithFootnotes, css_style: str | None = None) -> str: - """Renders TableWithFootnotes into an HTML document + """Renders a single TableWithFootnotes into an HTML document with a single table""" + return tables_to_html([table], css_style) + + +@trace_calls +def tables_to_html( + tables: Iterable[TableWithFootnotes], css_style: str | None = None +) -> str: + """Renders a list of TableWithFootnotes into an HTML document with multiple tables Renders TableWithFootnotes into an HTML document with a TABLE and footnotes into a DL (if they exist). Optional CSS text can be provided in `css_style` added as diff --git a/studybuilder/src/components/library/crfs/CrfAliasSelection.vue b/studybuilder/src/components/library/crfs/CrfAliasSelection.vue index 506c107b..c24a8aa8 100644 --- a/studybuilder/src/components/library/crfs/CrfAliasSelection.vue +++ b/studybuilder/src/components/library/crfs/CrfAliasSelection.vue @@ -15,7 +15,7 @@ :label="$t('CRFAliases.context')" data-cy="alias-context" density="compact" - :disabled="props.disabled" + :readonly="props.readOnly" /> @@ -24,7 +24,7 @@ :label="$t('CRFAliases.name')" data-cy="alias-name" density="compact" - :disabled="props.disabled" + :readonly="props.readOnly" /> @@ -33,7 +33,7 @@ class="mr-2" data-cy="alias-add-button" block - :disabled="props.disabled" + :readonly="props.readOnly" @click="addAlias" > {{ t('_global.add') }} @@ -57,9 +57,9 @@ :items="aliases" hide-default-switches hide-export-button - :show-select="!disabled" - :hide-default-footer="props.disabled" - :hide-search-field="props.disabled" + :show-select="!readOnly" + :hide-default-footer="props.readOnly" + :hide-search-field="props.readOnly" table-height="400px" :items-length="total" disable-filtering @@ -81,7 +81,7 @@ import crfs from '@/api/crfs' const { t } = useI18n() const props = defineProps({ - disabled: { + readOnly: { type: Boolean, default: false, }, @@ -110,7 +110,7 @@ onMounted(() => { }) const getAliases = (filters, options, filtersUpdated) => { - if (props.disabled) { + if (props.readOnly) { aliases.value = [...modelValue.value] } else { const params = filteringParameters.prepareParameters( diff --git a/studybuilder/src/components/library/crfs/CrfDescriptionSelection.vue b/studybuilder/src/components/library/crfs/CrfDescriptionSelection.vue index 0f00f734..77ee8126 100644 --- a/studybuilder/src/components/library/crfs/CrfDescriptionSelection.vue +++ b/studybuilder/src/components/library/crfs/CrfDescriptionSelection.vue @@ -17,7 +17,7 @@ item-title="submission_value" item-value="submission_value" density="compact" - :disabled="props.disabled" + :readonly="props.readOnly" /> @@ -37,7 +37,7 @@ " return-object density="compact" - :disabled="props.disabled" + :readonly="props.readOnly" @update:model-value="autocompleteValues" /> @@ -46,7 +46,7 @@ color="secondary" class="mr-2" block - :disabled="props.disabled" + :readonly="props.readOnly" @click="addDescription" > {{ t('_global.add') }} @@ -61,7 +61,7 @@ v-model:content="inputDesc.description" content-type="html" :toolbar="customToolbar" - :read-only="props.disabled" + :read-only="props.readOnly" :placeholder=" inputDesc.description ? '' : t('CRFDescriptions.description') " @@ -74,7 +74,7 @@ v-model:content="inputDesc.sponsor_instruction" content-type="html" :toolbar="customToolbar" - :read-only="props.disabled" + :read-only="props.readOnly" :placeholder=" inputDesc.sponsor_instruction ? '' @@ -89,7 +89,7 @@ v-model:content="inputDesc.instruction" content-type="html" :toolbar="customToolbar" - :read-only="props.disabled" + :read-only="props.readOnly" :placeholder=" inputDesc.instruction ? '' : t('CRFDescriptions.instruction') " @@ -114,9 +114,9 @@ :items="descriptions" hide-default-switches hide-export-button - :show-select="!disabled" - :hide-default-footer="props.disabled" - :hide-search-field="props.disabled" + :show-select="!readOnly" + :hide-default-footer="props.readOnly" + :hide-search-field="props.readOnly" table-height="400px" :items-length="total" disable-filtering @@ -153,7 +153,7 @@ import { sanitizeHTML } from '@/utils/sanitize' const { t } = useI18n() const props = defineProps({ - disabled: { + readOnly: { type: Boolean, default: false, }, @@ -196,7 +196,10 @@ const total = ref(0) onMounted(() => { terms.getTermsByCodelist('language').then((resp) => { languages.value = resp.data.items.filter( - (el) => el.submission_value.toLowerCase() !== parameters.ENG + (el) => + ![parameters.EN, parameters.ENG].includes( + el.submission_value.toLowerCase() + ) ) }) @@ -204,7 +207,7 @@ onMounted(() => { }) const getDescriptions = (filters, options, filtersUpdated) => { - if (props.disabled) { + if (props.readOnly) { descriptions.value = [...modelValue.value] } else { const params = filteringParameters.prepareParameters( diff --git a/studybuilder/src/components/library/crfs/CrfFormForm.vue b/studybuilder/src/components/library/crfs/CrfFormForm.vue index 8acc5c9f..d6c98ec2 100644 --- a/studybuilder/src/components/library/crfs/CrfFormForm.vue +++ b/studybuilder/src/components/library/crfs/CrfFormForm.vue @@ -8,6 +8,7 @@ :form-url="formUrl" :editable="isEdit()" :save-from-any-step="isEdit()" + :read-only="isReadOnly" @close="close" @save="submit" > @@ -24,8 +25,8 @@ :label="$t('CRFForms.name') + '*'" data-cy="form-oid-name" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" :rules="[formRules.required]" /> @@ -35,8 +36,8 @@ :label="$t('CRFForms.oid')" data-cy="form-oid" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -45,7 +46,7 @@ @@ -60,7 +61,7 @@ v-model:content="engDescription.description" content-type="html" :toolbar="customToolbar" - :read-only="isDisabled" + :read-only="isReadOnly" /> @@ -73,7 +74,7 @@ v-model:content="engDescription.sponsor_instruction" content-type="html" :toolbar="customToolbar" - :read-only="isDisabled" + :read-only="isReadOnly" data-cy="help-for-sponsor" /> @@ -91,8 +92,8 @@ :label="$t('CRFDescriptions.name')" data-cy="form-oid-displayed-text" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -104,7 +105,7 @@ v-model:content="engDescription.instruction" content-type="html" :toolbar="customToolbar" - :read-only="isDisabled" + :read-only="isReadOnly" data-cy="form-help-for-site" /> @@ -116,19 +117,19 @@ - @@ -174,7 +170,6 @@ import constants from '@/constants/libraries' import { QuillEditor } from '@vueup/vue-quill' import '@vueup/vue-quill/dist/vue-quill.snow.css' import ActionsMenu from '@/components/tools/ActionsMenu.vue' -import CrfActivitiesModelsLinkForm from '@/components/library/crfs/CrfActivitiesModelsLinkForm.vue' import actions from '@/constants/actions' import parameters from '@/constants/parameters' import CrfExtensionsManagementTable from '@/components/library/crfs/CrfExtensionsManagementTable.vue' @@ -194,7 +189,6 @@ export default { CrfDescriptionSelection, QuillEditor, ActionsMenu, - CrfActivitiesModelsLinkForm, CrfExtensionsManagementTable, ConfirmDialog, CrfApprovalSummaryConfirmDialog, @@ -278,9 +272,8 @@ export default { [{ script: 'sub' }, { script: 'super' }], [{ list: 'ordered' }, { list: 'bullet' }], ], - engDescription: { language: parameters.ENG }, + engDescription: { language: parameters.EN }, readOnly: this.readOnlyProp, - linkForm: false, selectedExtensions: [], actions: [ { @@ -309,18 +302,11 @@ export default { : false, click: this.delete, }, - { - label: this.$t('CRFLinkingForm.link_activities'), - icon: 'mdi-plus', - iconColor: 'primary', - condition: () => this.readOnly, - click: this.openLinkForm, - }, ], } }, computed: { - isDisabled() { + isReadOnly() { return this.readOnly || !this.checkPermission(this.$roles.LIBRARY_WRITE) }, title() { @@ -358,7 +344,7 @@ export default { selectedForm: { handler(value) { if (this.isEdit()) { - this.steps = this.readOnly ? this.createSteps : this.editSteps + this.steps = this.editSteps this.initForm(value) } else { this.steps = this.createSteps @@ -374,7 +360,7 @@ export default { }, async mounted() { if (this.isEdit()) { - this.steps = this.readOnly ? this.createSteps : this.editSteps + this.steps = this.editSteps } else { this.steps = this.createSteps } @@ -390,13 +376,6 @@ export default { this.initForm(resp.data) }) }, - openLinkForm() { - this.linkForm = true - }, - closeLinkForm() { - this.linkForm = false - this.getForm() - }, async newVersion() { if ( await this.$refs.confirmNewVersion.open({ @@ -478,7 +457,7 @@ export default { aliases: [], } this.engDescription = { - language: parameters.ENG, + language: parameters.EN, } this.desc = [] this.selectedExtensions = [] @@ -486,7 +465,7 @@ export default { this.$emit('close') }, async submit() { - if (this.isDisabled) { + if (this.isReadOnly) { this.close() return } @@ -600,13 +579,17 @@ export default { this.form = item this.form.aliases = item.aliases this.form.change_description = this.$t('_global.draft_change') - if (item.descriptions.find((el) => el.language === parameters.ENG)) { - this.engDescription = item.descriptions.find( - (el) => el.language === parameters.ENG + if ( + item.descriptions.some((el) => + [parameters.EN, parameters.ENG].includes(el.language) + ) + ) { + this.engDescription = item.descriptions.find((el) => + [parameters.EN, parameters.ENG].includes(el.language) ) } this.desc = item.descriptions.filter( - (el) => el.language !== parameters.ENG + (el) => ![parameters.EN, parameters.ENG].includes(el.language) ) item.vendor_attributes.forEach((attr) => (attr.type = 'attr')) item.vendor_elements.forEach((element) => { diff --git a/studybuilder/src/components/library/crfs/CrfFormTable.vue b/studybuilder/src/components/library/crfs/CrfFormTable.vue index c8cbbbd8..ef0fae55 100644 --- a/studybuilder/src/components/library/crfs/CrfFormTable.vue +++ b/studybuilder/src/components/library/crfs/CrfFormTable.vue @@ -115,12 +115,6 @@ @close="closeFormHistory" /> - @@ -134,7 +128,6 @@ import ActionsMenu from '@/components/tools/ActionsMenu.vue' import crfs from '@/api/crfs' import CrfFormForm from '@/components/library/crfs/CrfFormForm.vue' import HistoryTable from '@/components/tools/HistoryTable.vue' -import CrfActivitiesModelsLinkForm from '@/components/library/crfs/CrfActivitiesModelsLinkForm.vue' import statuses from '@/constants/statuses' import filteringParameters from '@/utils/filteringParameters' import ConfirmDialog from '@/components/tools/ConfirmDialog.vue' @@ -154,7 +147,6 @@ export default { ActionsMenu, CrfFormForm, HistoryTable, - CrfActivitiesModelsLinkForm, ConfirmDialog, CrfApprovalSummaryConfirmDialog, CrfNewVersionSummaryConfirmDialog, @@ -278,7 +270,6 @@ export default { selectedForm: null, filters: '', showFormHistory: false, - linkForm: false, formHistoryItems: [], } }, @@ -316,8 +307,8 @@ export default { return sanitizeHTML(html) }, getDescriptionAttribute(item, attr, short) { - const engDesc = item.descriptions.find( - (el) => el.language === parameters.ENG + const engDesc = item.descriptions.find((el) => + [parameters.EN, parameters.ENG].includes(el.language) ) if (engDesc && engDesc[attr]) { return short @@ -437,15 +428,6 @@ export default { this.selectedForm = null this.showFormHistory = false }, - openLinkForm(item) { - this.selectedForm = item - this.linkForm = true - }, - closeLinkForm() { - this.linkForm = false - this.selectedForm = null - this.$refs.table.filterTable() - }, async getForms(filters, options, filtersUpdated) { if (filters) { this.filters = filters diff --git a/studybuilder/src/components/library/crfs/CrfItemForm.vue b/studybuilder/src/components/library/crfs/CrfItemForm.vue index d22fb70f..25a374c4 100644 --- a/studybuilder/src/components/library/crfs/CrfItemForm.vue +++ b/studybuilder/src/components/library/crfs/CrfItemForm.vue @@ -8,6 +8,7 @@ :form-url="formUrl" :editable="isEdit()" :save-from-any-step="isEdit()" + :read-only="isReadOnly" @close="close" @save="submit" > @@ -25,8 +26,8 @@ data-cy="item-name" :rules="[formRules.required]" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -35,8 +36,8 @@ :label="$t('CRFItems.oid')" data-cy="item-oid" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -51,9 +52,9 @@ item-value="submission_value" :rules="[formRules.required]" density="compact" - clearable + :clearable="!isReadOnly" class="mt-3" - :disabled="isDisabled" + :readonly="isReadOnly" @update:model-value="checkIfNumeric()" /> @@ -64,10 +65,10 @@ data-cy="item-length" density="compact" :rules="[lengthRequired]" - clearable + :clearable="!isReadOnly" class="mt-3" type="number" - :disabled="isDisabled" + :readonly="isReadOnly" /> @@ -77,10 +78,10 @@ data-cy="item-significant-digits" density="compact" :rules="[significantDigitsRequired]" - clearable + :clearable="!isReadOnly" class="mt-3" type="number" - :disabled="isDisabled" + :readonly="isReadOnly" /> @@ -94,7 +95,7 @@ v-model:content="engDescription.description" content-type="html" :toolbar="customToolbar" - :read-only="isDisabled" + :read-only="isReadOnly" /> @@ -107,7 +108,7 @@ v-model:content="engDescription.sponsor_instruction" content-type="html" :toolbar="customToolbar" - :read-only="isDisabled" + :read-only="isReadOnly" /> @@ -124,8 +125,8 @@ :label="$t('CRFDescriptions.name')" data-cy="form-oid-name" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -137,7 +138,7 @@ v-model:content="engDescription.instruction" content-type="html" :toolbar="customToolbar" - :read-only="isDisabled" + :read-only="isReadOnly" /> @@ -154,8 +155,8 @@ :label="$t('CRFItems.sas_name')" data-cy="item-sas-name" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -164,8 +165,8 @@ :label="$t('CRFItems.sds_name')" data-cy="item-sds-name" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -179,8 +180,8 @@ item-title="nci_preferred_name" item-value="nci_preferred_name" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -189,8 +190,8 @@ :label="$t('CRFItems.comment')" data-cy="item-comment" density="compact" - clearable - :disabled="isDisabled" + :clearable="!isReadOnly" + :readonly="isReadOnly" /> @@ -200,19 +201,19 @@ @@ -259,7 +260,7 @@ size="small" variant="outlined" color="nnBaseBlue" - :disabled="isDisabled" + :readonly="isReadOnly" @click="addCodelist(item)" /> @@ -270,12 +271,12 @@ @@ -284,7 +285,7 @@ icon="mdi-delete-outline" class="mt-1" variant="text" - :disabled="isDisabled" + :readonly="isReadOnly" @click="removeTerm(item)" /> @@ -300,6 +301,7 @@ :items-length="totalTerms" hide-export-button only-text-search + :column-data-parameters="{ include_removed: false }" hide-default-switches @filter="getCodeListTerms" > @@ -308,8 +310,8 @@ icon="mdi-plus" class="mt-1" variant="text" - :disabled=" - isDisabled || + :readonly=" + isReadOnly || selectedTerms.find((e) => e.term_uid === item.term_uid) " @click="addTerm(item)" @@ -335,7 +337,7 @@ variant="outlined" color="nnBaseBlue" :label="$t('CRFItemGroups.new_translation')" - :disabled="isDisabled" + :readonly="isReadOnly" icon="mdi-plus" @click.stop="addUnit" /> @@ -351,25 +353,337 @@ item-title="name" item-value="name" return-object - :disabled="isDisabled" + :readonly="isReadOnly" @update:model-value="setUnit(index)" /> + - @@ -407,7 +715,9 @@ diff --git a/studybuilder/src/components/studies/StudyActivityInstancesTable.vue b/studybuilder/src/components/studies/StudyActivityInstancesTable.vue index c8c0eec5..6d27baaf 100644 --- a/studybuilder/src/components/studies/StudyActivityInstancesTable.vue +++ b/studybuilder/src/components/studies/StudyActivityInstancesTable.vue @@ -1,6 +1,6 @@